Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "add react-spinbutton component",
"packageName": "@fluentui-contrib/react-cap-theme",
"email": "Oleksandr.Katrukhin@microsoft.com",
"dependentChangeType": "patch"
}
5 changes: 5 additions & 0 deletions packages/react-cap-theme/src/capStyleHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ import { usePopoverSurfaceStyles } from './components/react-popover';
import type { PopoverSurfaceState } from '@fluentui/react-popover';
import { useSearchBoxStyles } from './components/react-search';
import type { SearchBoxState } from './components/react-search';
import { useSpinButtonStyles } from './components/react-spinbutton';
import type { SpinButtonState } from './components/react-spinbutton';
import {
useTagStyles,
useInteractionTagStyles,
Expand Down Expand Up @@ -319,6 +321,9 @@ export const CAP_STYLE_HOOKS: NonNullable<
useSearchBoxStyles_unstable: (state) => {
return useSearchBoxStyles(state as SearchBoxState);
},
useSpinButtonStyles_unstable: (state) => {
return useSpinButtonStyles(state as SpinButtonState);
},
useSplitButtonStyles_unstable: (state) => {
return useSplitButtonStyles(state as SplitButtonState);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { SpinButtonState as BaseSpinButtonState } from '@fluentui/react-components';

/**
* State used in rendering the CAP SpinButton.
*
* CAP additionally supports a `'large'` size, which the base SpinButton does not
* expose, so `size` is widened accordingly.
* @alpha
*/
export type SpinButtonState = Omit<BaseSpinButtonState, 'size'> & {
size: BaseSpinButtonState['size'] | 'large';
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import * as React from 'react';
import { makeStyles, mergeClasses, shorthands } from '@griffel/react';
import { tokens } from '@fluentui/tokens';
import { capTokens } from '../../../tokens';

import {
bundleIcon,
ChevronDownFilled,
ChevronDownRegular,
ChevronUpFilled,
ChevronUpRegular,
iconFilledClassName,
iconRegularClassName,
} from '@fluentui/react-icons';
import { getSlotClassNameProp_unstable } from '@fluentui/react-utilities';
import type { SpinButtonState } from './SpinButton.types';

const stepperBoldSwap = {
[`& .${iconRegularClassName}`]: { display: 'none' },
[`& .${iconFilledClassName}`]: { display: 'inline' },
} as const;

const ChevronUp = bundleIcon(ChevronUpFilled, ChevronUpRegular);
const ChevronDown = bundleIcon(ChevronDownFilled, ChevronDownRegular);

const removeFluentUIStyleAdditions = {
'::before': {
display: 'none',
},
'::after': {
display: 'none',
},
};

const stepperButton = {
borderRadius: tokens.borderRadiusNone,
':hover': {
...stepperBoldSwap,
},
':active': {
...stepperBoldSwap,
},
['&.fui-SpinButton__button_active']: {
...stepperBoldSwap,
},
};

const useRootStyles = makeStyles({
root: {
minHeight: '16px',
overflow: 'hidden',
},
disabled: {
...shorthands.borderColor(tokens.colorNeutralStrokeDisabled),
},
stepperButton: {
width: '34px',
height: '16px',
fontSize: '13px',
...stepperButton,
},
incrementButton: {
top: '-8px', // no other way
right: 0, // rtl/ltr issue
padding: `${tokens.spacingVerticalXS} ${tokens.spacingHorizontalMNudge} ${tokens.spacingVerticalNone}`,
},
decrementButton: {
bottom: 0,
right: 0, // rtl/ltr
padding: `${tokens.spacingVerticalNone} ${tokens.spacingHorizontalMNudge} ${tokens.spacingVerticalXS}`,
},
stepperButtonSmall: {
width: '26px',
height: tokens.lineHeightBase100,
fontSize: '13px',
...stepperButton,
},
incrementButtonSmall: {
top: '-6px', // no other way
right: 0, // rtl/ltr issue
padding: `${tokens.spacingVerticalXXS} ${tokens.spacingHorizontalSNudge} ${tokens.spacingVerticalNone}`,
},
decrementButtonSmall: {
bottom: 0,
right: 0, // rtl/ltr
padding: `${tokens.spacingVerticalNone} ${tokens.spacingHorizontalSNudge} ${tokens.spacingVerticalXXS}`,
},

stepperButtonLarge: {
width: '42px',
height: '22px',
fontSize: tokens.fontSizeBase400,
...stepperButton,
},
incrementButtonLarge: {
top: '-10px', // no other way
right: 0, // rtl/ltr issue
padding: `${tokens.spacingVerticalXS} ${tokens.spacingHorizontalM} ${tokens.spacingVerticalNone}`,
},
decrementButtonLarge: {
bottom: 0,
right: 0, // rtl/ltr
padding: `${tokens.spacingVerticalNone} ${tokens.spacingHorizontalM} ${tokens.spacingVerticalXS}`,
},
});

const useRootSizeStyles = makeStyles({
small: {
padding: `${tokens.spacingVerticalSNudge} ${tokens.spacingHorizontalS}`,
borderRadius: tokens.borderRadiusXLarge,
...removeFluentUIStyleAdditions,
},
medium: {
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalMNudge}`,
borderRadius: capTokens.borderRadius2XLarge,
...removeFluentUIStyleAdditions,
},
large: {
minHeight: '22px',
padding: `${tokens.spacingVerticalMNudge} ${tokens.spacingHorizontalM}`,
borderRadius: capTokens.borderRadius2XLarge,
...removeFluentUIStyleAdditions,
},
});

const useRootAppearanceStyles = makeStyles({
outline: {
...shorthands.borderColor(tokens.colorNeutralStrokeAccessible),
...shorthands.borderStyle('solid'),
...shorthands.borderWidth(tokens.strokeWidthThin),
'::before': {
...shorthands.borderColor(tokens.colorNeutralStrokeAccessible),
},
':hover': {
...shorthands.borderColor(tokens.colorNeutralStrokeAccessibleHover),
},
':focus-within:not(:active)': {
...shorthands.borderColor(tokens.colorCompoundBrandStroke),
},
':active': {
...shorthands.borderColor(tokens.colorNeutralStrokeAccessiblePressed),
},
},
underline: {
borderRadius: tokens.borderRadiusNone,
borderBottomWidth: tokens.strokeWidthThin,
borderBottomStyle: 'solid',
borderBottomColor: tokens.colorNeutralStrokeAccessible,
},
filled: {
backgroundColor: tokens.colorNeutralBackground1,
},
'filled-darker': {
...shorthands.borderStyle('solid'),
...shorthands.borderWidth(tokens.strokeWidthThin),
},
'filled-lighter': {
...shorthands.borderStyle('solid'),
...shorthands.borderWidth(tokens.strokeWidthThin),
},
});

/**
* Apply CAP styling to the SpinButton slots based on the state.
* @alpha
*/
export const useSpinButtonStyles = (
state: SpinButtonState
): SpinButtonState => {
const rootStyles = useRootStyles();
const rootSizeStyles = useRootSizeStyles();
const rootAppearanceStyles = useRootAppearanceStyles();

const { size, appearance } = state;

state.root.className = mergeClasses(
state.root.className,
rootStyles.root,
rootSizeStyles[size],
rootAppearanceStyles[appearance],
state.input.disabled && rootStyles.disabled
);

if (state.incrementButton) {
state.incrementButton.children = React.createElement(ChevronUp);
state.incrementButton.className = mergeClasses(
state.incrementButton.className,
size === 'small'
? rootStyles.stepperButtonSmall
: size === 'large'
? rootStyles.stepperButtonLarge
: rootStyles.stepperButton,
size === 'small'
? rootStyles.incrementButtonSmall
: size === 'large'
? rootStyles.incrementButtonLarge
: rootStyles.incrementButton,
getSlotClassNameProp_unstable(state.incrementButton)
);
}

if (state.decrementButton) {
state.decrementButton.children = React.createElement(ChevronDown);
state.decrementButton.className = mergeClasses(
state.decrementButton.className,
size === 'small'
? rootStyles.stepperButtonSmall
: size === 'large'
? rootStyles.stepperButtonLarge
: rootStyles.stepperButton,
size === 'small'
? rootStyles.decrementButtonSmall
: size === 'large'
? rootStyles.decrementButtonLarge
: rootStyles.decrementButton,
getSlotClassNameProp_unstable(state.decrementButton)
);
}

return state;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { useSpinButtonStyles } from './components/SpinButton/useSpinButtonStyles.styles';
export type { SpinButtonState } from './components/SpinButton/SpinButton.types';
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as React from 'react';
import {
makeStyles,
tokens,
useId,
Label,
SpinButton,
} from '@fluentui/react-components';

const useStyles = makeStyles({
base: {
display: 'flex',
flexDirection: 'column',
maxWidth: '500px',
},

field: {
display: 'grid',
gridRowGap: tokens.spacingVerticalXXS,
marginTop: tokens.spacingVerticalMNudge,
padding: tokens.spacingHorizontalMNudge,
},
fieldDark: {
display: 'grid',
gridRowGap: tokens.spacingVerticalXXS,
marginTop: tokens.spacingVerticalMNudge,
padding: tokens.spacingHorizontalMNudge,
backgroundColor: tokens.colorNeutralBackground2,
borderRadius: tokens.borderRadiusMedium,
},
});

export const Appearance = () => {
const styles = useStyles();

const outlineId = useId('outline-id');
const underlineId = useId('underline-id');
const filledLighterId = useId('filledLighter-id');
const filledDarkerId = useId('filledDarker-id');

return (
<div className={styles.base}>
<div className={styles.field}>
<Label htmlFor={outlineId}>Outline (default)</Label>
<SpinButton id={outlineId} />
</div>

<div className={styles.field}>
<Label htmlFor={underlineId}>Underline</Label>
<SpinButton appearance="underline" id={underlineId} />
</div>

<div className={styles.fieldDark}>
<Label htmlFor={filledLighterId}>Filled Lighter</Label>
<SpinButton appearance="filled-lighter" id={filledLighterId} />
</div>

<div className={styles.fieldDark}>
<Label htmlFor={filledDarkerId}>Filled Darker</Label>
<SpinButton appearance="filled-darker" id={filledDarkerId} />
</div>
</div>
);
};
Loading
Loading