From 199c41b0ed2e129954c5803cd358b1fe0798abda Mon Sep 17 00:00:00 2001 From: Sarah Higley Date: Tue, 29 Mar 2022 00:19:26 -0700 Subject: [PATCH] Select vNext styles (#21068) --- packages/react-select/package.json | 3 +- .../Select/__snapshots__/Select.test.tsx.snap | 16 +- .../Select/{useSelect.ts => useSelect.tsx} | 6 +- .../src/components/Select/useSelectStyles.ts | 206 ++++++++++++++++-- workspace.json | 5 + 5 files changed, 220 insertions(+), 16 deletions(-) rename packages/react-select/src/components/Select/{useSelect.ts => useSelect.tsx} (87%) diff --git a/packages/react-select/package.json b/packages/react-select/package.json index 2a41d982d2b4b..40488558b3700 100644 --- a/packages/react-select/package.json +++ b/packages/react-select/package.json @@ -23,7 +23,6 @@ "test": "jest --passWithNoTests", "docs": "api-extractor run --config=config/api-extractor.local.json --local", "build:local": "tsc -p ./tsconfig.lib.json --module esnext --emitDeclarationOnly && node ../../scripts/typescript/normalize-import --output ./dist/packages/react-select/src && yarn docs", - "bundle:storybook": "just-scripts storybook:build", "storybook": "start-storybook", "type-check": "tsc -b tsconfig.json" }, @@ -33,6 +32,8 @@ "@fluentui/react-conformance-griffel": "9.0.0-beta.3" }, "dependencies": { + "@fluentui/react-icons": "^2.0.159-beta.10", + "@fluentui/react-theme": "9.0.0-rc.4", "@fluentui/react-utilities": "9.0.0-rc.5", "@griffel/react": "1.0.0", "tslib": "^2.1.0" diff --git a/packages/react-select/src/components/Select/__snapshots__/Select.test.tsx.snap b/packages/react-select/src/components/Select/__snapshots__/Select.test.tsx.snap index e5b09ecd5fdb5..979e81e6ddd86 100644 --- a/packages/react-select/src/components/Select/__snapshots__/Select.test.tsx.snap +++ b/packages/react-select/src/components/Select/__snapshots__/Select.test.tsx.snap @@ -10,7 +10,21 @@ exports[`Select renders the default state 1`] = ` /> + > + + `; diff --git a/packages/react-select/src/components/Select/useSelect.ts b/packages/react-select/src/components/Select/useSelect.tsx similarity index 87% rename from packages/react-select/src/components/Select/useSelect.ts rename to packages/react-select/src/components/Select/useSelect.tsx index edae0126d5cdf..ef3a8e70bf633 100644 --- a/packages/react-select/src/components/Select/useSelect.ts +++ b/packages/react-select/src/components/Select/useSelect.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { getPartitionedNativeProps, resolveShorthand } from '@fluentui/react-utilities'; +import { ChevronDownRegular } from '@fluentui/react-icons'; import type { SelectProps, SelectState } from './Select.types'; /** @@ -36,7 +37,10 @@ export const useSelect_unstable = (props: SelectProps, ref: React.Ref }, + }), root: resolveShorthand(root, { required: true, defaultProps: nativeProps.root, diff --git a/packages/react-select/src/components/Select/useSelectStyles.ts b/packages/react-select/src/components/Select/useSelectStyles.ts index 39aad4e7f4955..68174e24a9b82 100644 --- a/packages/react-select/src/components/Select/useSelectStyles.ts +++ b/packages/react-select/src/components/Select/useSelectStyles.ts @@ -1,5 +1,6 @@ +import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; +import { tokens } from '@fluentui/react-theme'; import { SlotClassNames } from '@fluentui/react-utilities'; -import { makeStyles, mergeClasses } from '@griffel/react'; import type { SelectSlots, SelectState } from './Select.types'; /** @@ -12,15 +13,188 @@ export const selectClassNames: SlotClassNames = { icon: 'fui-Select__icon', }; -const useStyles = makeStyles({ - wrapper: { - // TODO: add styles +const iconSizes = { + small: '16px', + medium: '20px', + large: '24px', +}; + +/* + * TODO: a number of spacing and animation values are shared with react-input. + * We should probably find a way to share these values between form controls in the theme. + */ + +const contentSizes = { + body1: { + fontSize: tokens.fontSizeBase300, + lineHeight: tokens.lineHeightBase300, + }, + caption1: { + fontSize: tokens.fontSizeBase200, + lineHeight: tokens.lineHeightBase200, + }, + 400: { + fontSize: tokens.fontSizeBase400, + lineHeight: tokens.lineHeightBase400, + }, +}; + +const horizontalSpacing = { + xxs: '2px', + xs: '4px', + sNudge: '6px', + s: '8px', + mNudge: '10px', + m: '12px', +}; + +const fieldHeights = { + small: '24px', + medium: '32px', + large: '40px', +}; + +const motionDurations = { + ultraFast: '0.05s', + normal: '0.2s', +}; + +const motionCurves = { + accelerateMid: 'cubic-bezier(0.7,0,1,0.5)', + decelerateMid: 'cubic-bezier(0.1,0.9,0.2,1)', +}; + +/* end of shared values */ + +const useRootStyles = makeStyles({ + base: { + alignItems: 'center', + boxSizing: 'border-box', + display: 'flex', + flexWrap: 'nowrap', + fontFamily: tokens.fontFamilyBase, + position: 'relative', + + '&:after': { + backgroundImage: `linear-gradient( + 0deg, + ${tokens.colorCompoundBrandStroke} 0%, + ${tokens.colorCompoundBrandStroke} 50%, + transparent 50%, + transparent 100% + )`, + ...shorthands.borderRadius(0, 0, tokens.borderRadiusMedium, tokens.borderRadiusMedium), + boxSizing: 'border-box', + content: '""', + height: tokens.borderRadiusMedium, + position: 'absolute', + bottom: '0', + left: '0', + right: '0', + transform: 'scaleX(0)', + transitionProperty: 'transform', + transitionDuration: motionDurations.ultraFast, + transitionDelay: motionCurves.accelerateMid, + }, + + '&:focus-within::after': { + transform: 'scaleX(1)', + transitionProperty: 'transform', + transitionDuration: motionDurations.normal, + transitionDelay: motionCurves.decelerateMid, + }, + }, +}); + +const useSelectStyles = makeStyles({ + base: { + appearance: 'none', + ...shorthands.border('1px', 'solid', 'transparent'), + ...shorthands.borderRadius(tokens.borderRadiusMedium), + boxShadow: 'none', + boxSizing: 'border-box', + color: tokens.colorNeutralForeground1, + flexGrow: 1, + + ':focus-visible': { + outlineWidth: '2px', + outlineStyle: 'solid', + outlineColor: 'transparent', + }, + }, + disabled: { + backgroundColor: tokens.colorTransparentBackground, + color: tokens.colorNeutralForegroundDisabled, + cursor: 'not-allowed', + }, + small: { + height: fieldHeights.small, + ...shorthands.padding('0', horizontalSpacing.sNudge), + ...contentSizes.caption1, + }, + medium: { + height: fieldHeights.medium, + ...shorthands.padding('0', horizontalSpacing.mNudge), + ...contentSizes.body1, + }, + large: { + height: fieldHeights.large, + ...shorthands.padding('0', horizontalSpacing.m), + ...contentSizes[400], + }, + outline: { + backgroundColor: tokens.colorNeutralBackground1, + ...shorthands.border('1px', 'solid', tokens.colorNeutralStroke1), + borderBottomColor: tokens.colorNeutralStrokeAccessible, }, - select: { - // TODO: add styles + underline: { + backgroundColor: tokens.colorTransparentBackground, + ...shorthands.borderBottom('1px', 'solid', tokens.colorNeutralStrokeAccessible), + ...shorthands.borderRadius(0), }, - selectDisabled: { - // TODO: add styles + filledLighter: { + backgroundColor: tokens.colorNeutralBackground1, + }, + filledDarker: { + backgroundColor: tokens.colorNeutralBackground3, + }, +}); + +const useIconStyles = makeStyles({ + icon: { + boxSizing: 'border-box', + color: tokens.colorNeutralStrokeAccessible, + display: 'block', + position: 'absolute', + right: '0', + pointerEvents: 'none', + + // the SVG must have display: block for accurate positioning + // otherwise an extra inline space is inserted after the svg element + '& svg': { + display: 'block', + }, + }, + small: { + fontSize: iconSizes.small, + height: iconSizes.small, + paddingRight: horizontalSpacing.sNudge, + paddingLeft: horizontalSpacing.xxs, + width: iconSizes.small, + }, + medium: { + fontSize: iconSizes.medium, + height: iconSizes.medium, + paddingRight: horizontalSpacing.mNudge, + paddingLeft: horizontalSpacing.xxs, + width: iconSizes.medium, + }, + large: { + fontSize: iconSizes.large, + height: iconSizes.large, + paddingRight: horizontalSpacing.m, + paddingLeft: horizontalSpacing.sNudge, + width: iconSizes.large, }, }); @@ -28,20 +202,26 @@ const useStyles = makeStyles({ * Apply styling to the Select slots based on the state */ export const useSelectStyles_unstable = (state: SelectState): SelectState => { + const { size, appearance } = state; const disabled = state.select.disabled; - const selectStyles = useStyles(); - state.root.className = mergeClasses(selectClassNames.root, selectStyles.wrapper, state.root.className); + const iconStyles = useIconStyles(); + const rootStyles = useRootStyles(); + const selectStyles = useSelectStyles(); + + state.root.className = mergeClasses(selectClassNames.root, rootStyles.base, state.root.className); state.select.className = mergeClasses( selectClassNames.select, - selectStyles.select, - disabled && selectStyles.selectDisabled, + selectStyles.base, + selectStyles[size], + selectStyles[appearance], + disabled && selectStyles.disabled, state.select.className, ); if (state.icon) { - state.icon.className = mergeClasses(selectClassNames.icon, state.icon.className); + state.icon.className = mergeClasses(selectClassNames.icon, iconStyles.icon, iconStyles[size], state.icon.className); } return state; diff --git a/workspace.json b/workspace.json index 055dcf29c85b9..4f0b7633662b7 100644 --- a/workspace.json +++ b/workspace.json @@ -532,6 +532,11 @@ "projectType": "library", "sourceRoot": "packages/react-select/src" }, + "@fluentui/react-select": { + "root": "packages/react-select", + "projectType": "library", + "sourceRoot": "packages/react-select/src" + }, "@fluentui/react-shared-contexts": { "root": "packages/react-shared-contexts", "projectType": "library",