diff --git a/packages/react-select/package.json b/packages/react-select/package.json
index 2a41d982d2b4b1..40488558b3700f 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 e5b09ecd5fdb51..979e81e6ddd86d 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 edae0126d5cdfe..ef3a8e70bf633f 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 39aad4e7f4955b..68174e24a9b828 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 055dcf29c85b9c..4f0b7633662b77 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",