361 lines
14 KiB
JavaScript
361 lines
14 KiB
JavaScript
'use client';
|
|
import { makeResetStyles, makeStyles, mergeClasses, shorthands } from '@griffel/react';
|
|
import { tokens, typographyStyles } from '@fluentui/react-theme';
|
|
export const spinButtonClassNames = {
|
|
root: 'fui-SpinButton',
|
|
input: 'fui-SpinButton__input',
|
|
incrementButton: 'fui-SpinButton__incrementButton',
|
|
decrementButton: 'fui-SpinButton__decrementButton'
|
|
};
|
|
const spinButtonExtraClassNames = {
|
|
buttonActive: 'fui-SpinButton__button_active'
|
|
};
|
|
const fieldHeights = {
|
|
small: '24px',
|
|
medium: '32px'
|
|
};
|
|
const useRootClassName = makeResetStyles({
|
|
display: 'inline-grid',
|
|
gridTemplateColumns: `1fr 24px`,
|
|
gridTemplateRows: '1fr 1fr',
|
|
columnGap: tokens.spacingHorizontalXS,
|
|
rowGap: 0,
|
|
position: 'relative',
|
|
isolation: 'isolate',
|
|
verticalAlign: 'middle',
|
|
backgroundColor: tokens.colorNeutralBackground1,
|
|
minHeight: fieldHeights.medium,
|
|
padding: `0 0 0 ${tokens.spacingHorizontalMNudge}`,
|
|
borderRadius: tokens.borderRadiusMedium,
|
|
// Apply border styles on the ::before pseudo element.
|
|
// We cannot use ::after since that is used for selection.
|
|
// Using the pseudo element allows us to place the border
|
|
// above content in the component which ensures the buttons
|
|
// line up visually with the border as expected. Without this
|
|
// there is a bit of a gap which can become very noticeable
|
|
// at high zoom or when OS zoom levels are not divisible by 2
|
|
// (e.g., 150% on Windows in Firefox)
|
|
// This is most noticeable on the "outline" appearance which is
|
|
// also the default so it feels worth the extra ceremony to get right.
|
|
'::before': {
|
|
content: '""',
|
|
boxSizing: 'border-box',
|
|
position: 'absolute',
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
pointerEvents: 'none',
|
|
zIndex: 10,
|
|
border: `1px solid ${tokens.colorNeutralStroke1}`,
|
|
borderBottomColor: tokens.colorNeutralStrokeAccessible,
|
|
borderRadius: tokens.borderRadiusMedium
|
|
},
|
|
'::after': {
|
|
boxSizing: 'border-box',
|
|
content: '""',
|
|
position: 'absolute',
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
zIndex: 20,
|
|
// Maintaining the correct corner radius:
|
|
// Use the whole border-radius as the height and only put radii on the bottom corners.
|
|
// (Otherwise the radius would be automatically reduced to fit available space.)
|
|
// max() ensures the focus border still shows up even if someone sets tokens.borderRadiusMedium to 0.
|
|
height: `max(2px, ${tokens.borderRadiusMedium})`,
|
|
borderBottomLeftRadius: tokens.borderRadiusMedium,
|
|
borderBottomRightRadius: tokens.borderRadiusMedium,
|
|
// Flat 2px border:
|
|
// By default borderBottom will cause little "horns" on the ends. The clipPath trims them off.
|
|
// (This could be done without trimming using `background: linear-gradient(...)`, but using
|
|
// borderBottom makes it easier for people to override the color if needed.)
|
|
borderBottom: `2px solid ${tokens.colorCompoundBrandStroke}`,
|
|
clipPath: 'inset(calc(100% - 2px) 0 0 0)',
|
|
// Animation for focus OUT
|
|
transform: 'scaleX(0)',
|
|
transitionProperty: 'transform',
|
|
transitionDuration: tokens.durationUltraFast,
|
|
transitionDelay: tokens.curveAccelerateMid,
|
|
'@media screen and (prefers-reduced-motion: reduce)': {
|
|
transitionDuration: '0.01ms',
|
|
transitionDelay: '0.01ms'
|
|
}
|
|
},
|
|
':focus-within::after': {
|
|
// Animation for focus IN
|
|
transform: 'scaleX(1)',
|
|
transitionProperty: 'transform',
|
|
transitionDuration: tokens.durationNormal,
|
|
transitionDelay: tokens.curveDecelerateMid,
|
|
'@media screen and (prefers-reduced-motion: reduce)': {
|
|
transitionDuration: '0.01ms',
|
|
transitionDelay: '0.01ms'
|
|
}
|
|
},
|
|
':focus-within:active::after': {
|
|
// This is if the user clicks the field again while it's already focused
|
|
borderBottomColor: tokens.colorCompoundBrandStrokePressed
|
|
},
|
|
':focus-within': {
|
|
outline: '2px solid transparent'
|
|
}
|
|
});
|
|
const useRootStyles = makeStyles({
|
|
small: {
|
|
minHeight: fieldHeights.small,
|
|
...typographyStyles.caption1,
|
|
paddingLeft: tokens.spacingHorizontalS
|
|
},
|
|
medium: {
|
|
},
|
|
outline: {
|
|
},
|
|
outlineInteractive: {
|
|
':hover::before': {
|
|
...shorthands.borderColor(tokens.colorNeutralStroke1Hover),
|
|
borderBottomColor: tokens.colorNeutralStrokeAccessibleHover
|
|
},
|
|
// DO NOT add a space between the selectors! It changes the behavior of make-styles.
|
|
':active,:focus-within': {
|
|
'::before': {
|
|
...shorthands.borderColor(tokens.colorNeutralStroke1Pressed),
|
|
borderBottomColor: tokens.colorNeutralStrokeAccessiblePressed
|
|
}
|
|
}
|
|
},
|
|
underline: {
|
|
'::before': {
|
|
...shorthands.borderWidth(0, 0, '1px', 0),
|
|
borderRadius: tokens.borderRadiusNone
|
|
}
|
|
},
|
|
underlineInteractive: {
|
|
':hover::before': {
|
|
borderBottomColor: tokens.colorNeutralStrokeAccessibleHover
|
|
},
|
|
// DO NOT add a space between the selectors! It changes the behavior of make-styles.
|
|
':active,:focus-within': {
|
|
'::before': {
|
|
borderBottomColor: tokens.colorNeutralStrokeAccessiblePressed
|
|
}
|
|
},
|
|
'::after': {
|
|
borderRadius: tokens.borderRadiusNone
|
|
}
|
|
},
|
|
filled: {
|
|
'::before': {
|
|
border: `1px solid ${tokens.colorTransparentStroke}`
|
|
}
|
|
},
|
|
'filled-darker': {
|
|
backgroundColor: tokens.colorNeutralBackground3
|
|
},
|
|
'filled-lighter': {
|
|
backgroundColor: tokens.colorNeutralBackground1
|
|
},
|
|
filledInteractive: {
|
|
// DO NOT add a space between the selectors! It changes the behavior of make-styles.
|
|
':hover,:focus-within': {
|
|
'::before': {
|
|
// also handles pressed border color (:active)
|
|
...shorthands.borderColor(tokens.colorTransparentStrokeInteractive)
|
|
}
|
|
}
|
|
},
|
|
invalid: {
|
|
':not(:focus-within),:hover:not(:focus-within)': {
|
|
'::before': {
|
|
...shorthands.borderColor(tokens.colorPaletteRedBorder2)
|
|
}
|
|
}
|
|
},
|
|
disabled: {
|
|
cursor: 'not-allowed',
|
|
backgroundColor: tokens.colorTransparentBackground,
|
|
'::before': {
|
|
...shorthands.borderColor(tokens.colorNeutralStrokeDisabled),
|
|
'@media (forced-colors: active)': {
|
|
...shorthands.borderColor('GrayText')
|
|
}
|
|
}
|
|
}
|
|
});
|
|
const useInputClassName = makeResetStyles({
|
|
gridColumnStart: '1',
|
|
gridColumnEnd: '2',
|
|
gridRowStart: '1',
|
|
gridRowEnd: '3',
|
|
outlineStyle: 'none',
|
|
border: '0',
|
|
padding: '0',
|
|
color: tokens.colorNeutralForeground1,
|
|
// Use literal "transparent" (not from the theme) to always let the color from the root show through
|
|
backgroundColor: 'transparent',
|
|
fontFamily: 'inherit',
|
|
fontSize: 'inherit',
|
|
fontWeight: 'inherit',
|
|
lineHeight: 'inherit',
|
|
width: '100%',
|
|
'::placeholder': {
|
|
color: tokens.colorNeutralForeground4,
|
|
opacity: 1
|
|
}
|
|
});
|
|
const useInputStyles = makeStyles({
|
|
disabled: {
|
|
color: tokens.colorNeutralForegroundDisabled,
|
|
cursor: 'not-allowed',
|
|
backgroundColor: tokens.colorTransparentBackground,
|
|
'::placeholder': {
|
|
color: tokens.colorNeutralForegroundDisabled
|
|
}
|
|
}
|
|
});
|
|
const useBaseButtonClassName = makeResetStyles({
|
|
display: 'inline-flex',
|
|
width: '24px',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
border: '0',
|
|
position: 'absolute',
|
|
outlineStyle: 'none',
|
|
height: '16px',
|
|
// Use literal "transparent" (not from the theme) to always let the color from the root show through
|
|
backgroundColor: 'transparent',
|
|
color: tokens.colorNeutralForeground3,
|
|
// common button layout
|
|
gridColumnStart: '2',
|
|
borderRadius: '0',
|
|
padding: '0 5px 0 5px',
|
|
':active': {
|
|
outlineStyle: 'none'
|
|
},
|
|
':enabled': {
|
|
':hover': {
|
|
cursor: 'pointer',
|
|
color: tokens.colorNeutralForeground3Hover,
|
|
backgroundColor: tokens.colorSubtleBackgroundHover
|
|
},
|
|
':active': {
|
|
color: tokens.colorNeutralForeground3Pressed,
|
|
backgroundColor: tokens.colorSubtleBackgroundPressed
|
|
},
|
|
[`&.${spinButtonExtraClassNames.buttonActive}`]: {
|
|
color: tokens.colorNeutralForeground3Pressed,
|
|
backgroundColor: tokens.colorSubtleBackgroundPressed
|
|
}
|
|
},
|
|
':disabled': {
|
|
cursor: 'not-allowed',
|
|
color: tokens.colorNeutralForegroundDisabled
|
|
}
|
|
});
|
|
const useButtonStyles = makeStyles({
|
|
increment: {
|
|
gridRowStart: '1',
|
|
borderTopRightRadius: tokens.borderRadiusMedium,
|
|
paddingTop: '4px',
|
|
paddingBottom: '1px'
|
|
},
|
|
decrement: {
|
|
gridRowStart: '2',
|
|
borderBottomRightRadius: tokens.borderRadiusMedium,
|
|
paddingTop: '1px',
|
|
paddingBottom: '4px'
|
|
},
|
|
// Padding values numbers don't align with design specs
|
|
// but visually the padding aligns.
|
|
// The icons are set in a 16x16px square but the artwork is inset from that
|
|
// so these padding values are computed by hand.
|
|
// Additionally the design uses fractional values so these are
|
|
// rounded to the nearest integer.
|
|
incrementButtonSmall: {
|
|
padding: '3px 6px 0px 4px',
|
|
height: '12px'
|
|
},
|
|
decrementButtonSmall: {
|
|
padding: '0px 6px 3px 4px',
|
|
height: '12px'
|
|
},
|
|
outline: {
|
|
},
|
|
underline: {
|
|
backgroundColor: 'transparent',
|
|
color: tokens.colorNeutralForeground3,
|
|
':enabled': {
|
|
':hover': {
|
|
color: tokens.colorNeutralForeground3Hover,
|
|
backgroundColor: tokens.colorSubtleBackgroundHover
|
|
},
|
|
':active': {
|
|
color: tokens.colorNeutralForeground3Pressed,
|
|
backgroundColor: tokens.colorSubtleBackgroundPressed
|
|
},
|
|
[`&.${spinButtonExtraClassNames.buttonActive}`]: {
|
|
color: tokens.colorNeutralForeground3Pressed,
|
|
backgroundColor: tokens.colorSubtleBackgroundPressed
|
|
}
|
|
},
|
|
':disabled': {
|
|
color: tokens.colorNeutralForegroundDisabled
|
|
}
|
|
},
|
|
'filled-darker': {
|
|
backgroundColor: 'transparent',
|
|
color: tokens.colorNeutralForeground3,
|
|
':enabled': {
|
|
':hover': {
|
|
color: tokens.colorNeutralForeground3Hover,
|
|
backgroundColor: tokens.colorNeutralBackground3Hover
|
|
},
|
|
':active': {
|
|
color: tokens.colorNeutralForeground3Pressed,
|
|
backgroundColor: tokens.colorNeutralBackground3Pressed
|
|
},
|
|
[`&.${spinButtonExtraClassNames.buttonActive}`]: {
|
|
color: tokens.colorNeutralForeground3Pressed,
|
|
backgroundColor: tokens.colorNeutralBackground3Pressed
|
|
}
|
|
},
|
|
':disabled': {
|
|
color: tokens.colorNeutralForegroundDisabled
|
|
}
|
|
},
|
|
'filled-lighter': {
|
|
backgroundColor: 'transparent',
|
|
color: tokens.colorNeutralForeground3,
|
|
':enabled': {
|
|
':hover': {
|
|
color: tokens.colorNeutralForeground3Hover,
|
|
backgroundColor: tokens.colorNeutralBackground1Hover
|
|
},
|
|
[`:active,&.${spinButtonExtraClassNames.buttonActive}`]: {
|
|
color: tokens.colorNeutralForeground3Pressed,
|
|
backgroundColor: tokens.colorNeutralBackground1Pressed
|
|
}
|
|
},
|
|
':disabled': {
|
|
color: tokens.colorNeutralForegroundDisabled
|
|
}
|
|
}
|
|
});
|
|
/**
|
|
* Apply styling to the SpinButton slots based on the state
|
|
*/ export const useSpinButtonStyles_unstable = (state)=>{
|
|
'use no memo';
|
|
const { appearance, spinState, size } = state;
|
|
const disabled = state.input.disabled;
|
|
const invalid = `${state.input['aria-invalid']}` === 'true';
|
|
const filled = appearance.startsWith('filled');
|
|
const rootStyles = useRootStyles();
|
|
const buttonStyles = useButtonStyles();
|
|
const inputStyles = useInputStyles();
|
|
state.root.className = mergeClasses(spinButtonClassNames.root, useRootClassName(), rootStyles[size], rootStyles[appearance], filled && rootStyles.filled, !disabled && appearance === 'outline' && rootStyles.outlineInteractive, !disabled && appearance === 'underline' && rootStyles.underlineInteractive, !disabled && filled && rootStyles.filledInteractive, !disabled && invalid && rootStyles.invalid, disabled && rootStyles.disabled, state.root.className);
|
|
state.incrementButton.className = mergeClasses(spinButtonClassNames.incrementButton, spinState === 'up' && `${spinButtonExtraClassNames.buttonActive}`, useBaseButtonClassName(), buttonStyles.increment, buttonStyles[appearance], size === 'small' && buttonStyles.incrementButtonSmall, state.incrementButton.className);
|
|
state.decrementButton.className = mergeClasses(spinButtonClassNames.decrementButton, spinState === 'down' && `${spinButtonExtraClassNames.buttonActive}`, useBaseButtonClassName(), buttonStyles.decrement, buttonStyles[appearance], size === 'small' && buttonStyles.decrementButtonSmall, state.decrementButton.className);
|
|
state.input.className = mergeClasses(spinButtonClassNames.input, useInputClassName(), disabled && inputStyles.disabled, state.input.className);
|
|
return state;
|
|
};
|