275 lines
10 KiB
JavaScript
275 lines
10 KiB
JavaScript
'use client';
|
|
import { createFocusOutlineStyle } from '@fluentui/react-tabster';
|
|
import { tokens } from '@fluentui/react-theme';
|
|
import { makeResetStyles, makeStyles, mergeClasses } from '@griffel/react';
|
|
export const switchClassNames = {
|
|
root: 'fui-Switch',
|
|
indicator: 'fui-Switch__indicator',
|
|
input: 'fui-Switch__input',
|
|
label: 'fui-Switch__label'
|
|
};
|
|
/**
|
|
* @deprecated Use `switchClassNames.root` instead.
|
|
*/ export const switchClassName = switchClassNames.root;
|
|
// Thumb and track sizes used by the component.
|
|
const spaceBetweenThumbAndTrack = 2;
|
|
// Medium size dimensions
|
|
const trackHeightMedium = 20;
|
|
const trackWidthMedium = 40;
|
|
const thumbSizeMedium = trackHeightMedium - spaceBetweenThumbAndTrack;
|
|
// Small size dimensions (from design mockup)
|
|
const trackHeightSmall = 16;
|
|
const trackWidthSmall = 32;
|
|
const thumbSizeSmall = trackHeightSmall - spaceBetweenThumbAndTrack;
|
|
const useRootBaseClassName = makeResetStyles({
|
|
alignItems: 'flex-start',
|
|
boxSizing: 'border-box',
|
|
display: 'inline-flex',
|
|
position: 'relative',
|
|
...createFocusOutlineStyle({
|
|
style: {},
|
|
selector: 'focus-within'
|
|
})
|
|
});
|
|
const useRootStyles = makeStyles({
|
|
vertical: {
|
|
flexDirection: 'column'
|
|
}
|
|
});
|
|
const useIndicatorBaseClassName = makeResetStyles({
|
|
borderRadius: tokens.borderRadiusCircular,
|
|
border: '1px solid',
|
|
lineHeight: 0,
|
|
boxSizing: 'border-box',
|
|
fill: 'currentColor',
|
|
flexShrink: 0,
|
|
fontSize: `${thumbSizeMedium}px`,
|
|
height: `${trackHeightMedium}px`,
|
|
margin: tokens.spacingVerticalS + ' ' + tokens.spacingHorizontalS,
|
|
pointerEvents: 'none',
|
|
transitionDuration: tokens.durationNormal,
|
|
transitionTimingFunction: tokens.curveEasyEase,
|
|
transitionProperty: 'background, border, color',
|
|
width: `${trackWidthMedium}px`,
|
|
'@media screen and (prefers-reduced-motion: reduce)': {
|
|
transitionDuration: '0.01ms'
|
|
},
|
|
'@media (forced-colors: active)': {
|
|
color: 'CanvasText',
|
|
'> i': {
|
|
forcedColorAdjust: 'none'
|
|
}
|
|
},
|
|
'> *': {
|
|
transitionDuration: tokens.durationNormal,
|
|
transitionTimingFunction: tokens.curveEasyEase,
|
|
transitionProperty: 'transform',
|
|
'@media screen and (prefers-reduced-motion: reduce)': {
|
|
transitionDuration: '0.01ms'
|
|
}
|
|
}
|
|
});
|
|
const useIndicatorStyles = makeStyles({
|
|
labelAbove: {
|
|
marginTop: 0
|
|
},
|
|
sizeSmall: {
|
|
fontSize: `${thumbSizeSmall}px`,
|
|
height: `${trackHeightSmall}px`,
|
|
width: `${trackWidthSmall}px`
|
|
}
|
|
});
|
|
const useInputBaseClassName = makeResetStyles({
|
|
boxSizing: 'border-box',
|
|
cursor: 'pointer',
|
|
height: '100%',
|
|
margin: 0,
|
|
opacity: 0,
|
|
position: 'absolute',
|
|
// Calculate the width of the hidden input by taking into account the size of the indicator + the padding around it.
|
|
// This is done so that clicking on that "empty space" still toggles the switch.
|
|
width: `calc(${trackWidthMedium}px + 2 * ${tokens.spacingHorizontalS})`,
|
|
// Checked (both enabled and disabled)
|
|
':checked': {
|
|
[`& ~ .${switchClassNames.indicator}`]: {
|
|
'> *': {
|
|
transform: `translateX(${trackWidthMedium - thumbSizeMedium - spaceBetweenThumbAndTrack}px)`
|
|
}
|
|
}
|
|
},
|
|
// Disabled (both checked and unchecked)
|
|
':disabled, &[aria-disabled="true"]': {
|
|
cursor: 'default',
|
|
[`& ~ .${switchClassNames.indicator}`]: {
|
|
color: tokens.colorNeutralForegroundDisabled
|
|
},
|
|
[`& ~ .${switchClassNames.label}`]: {
|
|
cursor: 'default',
|
|
color: tokens.colorNeutralForegroundDisabled
|
|
}
|
|
},
|
|
// Enabled and unchecked
|
|
':enabled:not(:checked):not([aria-disabled="true"])': {
|
|
[`& ~ .${switchClassNames.indicator}`]: {
|
|
color: tokens.colorNeutralStrokeAccessible,
|
|
borderColor: tokens.colorNeutralStrokeAccessible
|
|
},
|
|
[`& ~ .${switchClassNames.label}`]: {
|
|
color: tokens.colorNeutralForeground1
|
|
},
|
|
':hover': {
|
|
[`& ~ .${switchClassNames.indicator}`]: {
|
|
color: tokens.colorNeutralStrokeAccessibleHover,
|
|
borderColor: tokens.colorNeutralStrokeAccessibleHover
|
|
}
|
|
},
|
|
':hover:active': {
|
|
[`& ~ .${switchClassNames.indicator}`]: {
|
|
color: tokens.colorNeutralStrokeAccessiblePressed,
|
|
borderColor: tokens.colorNeutralStrokeAccessiblePressed
|
|
}
|
|
}
|
|
},
|
|
// Enabled and checked
|
|
':enabled:checked:not([aria-disabled="true"])': {
|
|
[`& ~ .${switchClassNames.indicator}`]: {
|
|
backgroundColor: tokens.colorCompoundBrandBackground,
|
|
color: tokens.colorNeutralForegroundInverted,
|
|
borderColor: tokens.colorTransparentStroke
|
|
},
|
|
':hover': {
|
|
[`& ~ .${switchClassNames.indicator}`]: {
|
|
backgroundColor: tokens.colorCompoundBrandBackgroundHover,
|
|
borderColor: tokens.colorTransparentStrokeInteractive
|
|
}
|
|
},
|
|
':hover:active': {
|
|
[`& ~ .${switchClassNames.indicator}`]: {
|
|
backgroundColor: tokens.colorCompoundBrandBackgroundPressed,
|
|
borderColor: tokens.colorTransparentStrokeInteractive
|
|
}
|
|
}
|
|
},
|
|
// Disabled and unchecked
|
|
':disabled:not(:checked), &[aria-disabled="true"]:not(:checked)': {
|
|
[`& ~ .${switchClassNames.indicator}`]: {
|
|
borderColor: tokens.colorNeutralStrokeDisabled
|
|
}
|
|
},
|
|
// Disabled and checked
|
|
':disabled:checked, &[aria-disabled="true"]:checked': {
|
|
[`& ~ .${switchClassNames.indicator}`]: {
|
|
backgroundColor: tokens.colorNeutralBackgroundDisabled,
|
|
borderColor: tokens.colorTransparentStrokeDisabled
|
|
}
|
|
},
|
|
'@media (forced-colors: active)': {
|
|
':disabled, &[aria-disabled="true"]': {
|
|
[`& ~ .${switchClassNames.indicator}`]: {
|
|
color: 'GrayText',
|
|
borderColor: 'GrayText'
|
|
},
|
|
[`& ~ .${switchClassNames.label}`]: {
|
|
color: 'GrayText'
|
|
}
|
|
},
|
|
':hover': {
|
|
color: 'CanvasText'
|
|
},
|
|
':hover:active': {
|
|
color: 'CanvasText'
|
|
},
|
|
':enabled:checked:not([aria-disabled="true"])': {
|
|
':hover': {
|
|
[`& ~ .${switchClassNames.indicator}`]: {
|
|
backgroundColor: 'Highlight',
|
|
color: 'Canvas'
|
|
}
|
|
},
|
|
':hover:active': {
|
|
[`& ~ .${switchClassNames.indicator}`]: {
|
|
backgroundColor: 'Highlight',
|
|
color: 'Canvas'
|
|
}
|
|
},
|
|
[`& ~ .${switchClassNames.indicator}`]: {
|
|
backgroundColor: 'Highlight',
|
|
color: 'Canvas'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
const useInputStyles = makeStyles({
|
|
before: {
|
|
right: 0,
|
|
top: 0
|
|
},
|
|
after: {
|
|
left: 0,
|
|
top: 0
|
|
},
|
|
above: {
|
|
bottom: 0,
|
|
height: `calc(${trackHeightMedium}px + ${tokens.spacingVerticalS})`,
|
|
width: '100%'
|
|
},
|
|
sizeSmall: {
|
|
width: `calc(${trackWidthSmall}px + 2 * ${tokens.spacingHorizontalS})`,
|
|
':checked': {
|
|
[`& ~ .${switchClassNames.indicator}`]: {
|
|
'> *': {
|
|
transform: `translateX(${trackWidthSmall - thumbSizeSmall - spaceBetweenThumbAndTrack}px)`
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
// Can't use makeResetStyles here because Label is a component that may itself use makeResetStyles.
|
|
const useLabelStyles = makeStyles({
|
|
base: {
|
|
cursor: 'pointer',
|
|
// Use a (negative) margin to account for the difference between the track's height and the label's line height.
|
|
// This prevents the label from expanding the height of the switch, but preserves line height if the label wraps.
|
|
marginBottom: `calc((${trackHeightMedium}px - ${tokens.lineHeightBase300}) / 2)`,
|
|
marginTop: `calc((${trackHeightMedium}px - ${tokens.lineHeightBase300}) / 2)`,
|
|
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalS}`
|
|
},
|
|
sizeSmall: {
|
|
fontSize: tokens.fontSizeBase200,
|
|
lineHeight: tokens.lineHeightBase200,
|
|
marginBottom: `calc((${trackHeightSmall}px - ${tokens.lineHeightBase200}) / 2)`,
|
|
marginTop: `calc((${trackHeightSmall}px - ${tokens.lineHeightBase200}) / 2)`
|
|
},
|
|
above: {
|
|
paddingTop: tokens.spacingVerticalXS,
|
|
paddingBottom: tokens.spacingVerticalXS,
|
|
width: '100%'
|
|
},
|
|
after: {
|
|
paddingLeft: tokens.spacingHorizontalXS
|
|
},
|
|
before: {
|
|
paddingRight: tokens.spacingHorizontalXS
|
|
}
|
|
});
|
|
/**
|
|
* Apply styling to the Switch slots based on the state
|
|
*/ export const useSwitchStyles_unstable = (state)=>{
|
|
'use no memo';
|
|
const rootBaseClassName = useRootBaseClassName();
|
|
const rootStyles = useRootStyles();
|
|
const indicatorBaseClassName = useIndicatorBaseClassName();
|
|
const indicatorStyles = useIndicatorStyles();
|
|
const inputBaseClassName = useInputBaseClassName();
|
|
const inputStyles = useInputStyles();
|
|
const labelStyles = useLabelStyles();
|
|
const { label, labelPosition, size } = state;
|
|
state.root.className = mergeClasses(switchClassNames.root, rootBaseClassName, labelPosition === 'above' && rootStyles.vertical, state.root.className);
|
|
state.indicator.className = mergeClasses(switchClassNames.indicator, indicatorBaseClassName, label && labelPosition === 'above' && indicatorStyles.labelAbove, size === 'small' && indicatorStyles.sizeSmall, state.indicator.className);
|
|
state.input.className = mergeClasses(switchClassNames.input, inputBaseClassName, label && inputStyles[labelPosition], size === 'small' && inputStyles.sizeSmall, state.input.className);
|
|
if (state.label) {
|
|
state.label.className = mergeClasses(switchClassNames.label, labelStyles.base, labelStyles[labelPosition], size === 'small' && labelStyles.sizeSmall, state.label.className);
|
|
}
|
|
return state;
|
|
};
|