Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore: Simplify Checkbox style selectors using component checked/disabled state",
"packageName": "@fluentui/react-checkbox",
"email": "behowell@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export type CheckboxSlots = {
};

// @public
export type CheckboxState = ComponentState<CheckboxSlots> & Required<Pick<CheckboxProps, 'checked' | 'labelPosition' | 'shape' | 'size'>>;
export type CheckboxState = ComponentState<CheckboxSlots> & Required<Pick<CheckboxProps, 'checked' | 'disabled' | 'labelPosition' | 'shape' | 'size'>>;

// @public (undocumented)
export const renderCheckbox_unstable: (state: CheckboxState) => JSX.Element;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,4 @@ export interface CheckboxOnChangeData {
* State used in rendering Checkbox
*/
export type CheckboxState = ComponentState<CheckboxSlots> &
Required<Pick<CheckboxProps, 'checked' | 'labelPosition' | 'shape' | 'size'>>;
Required<Pick<CheckboxProps, 'checked' | 'disabled' | 'labelPosition' | 'shape' | 'size'>>;
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { useFocusWithin } from '@fluentui/react-tabster';
* @param ref - reference to `<input>` element of Checkbox
*/
export const useCheckbox_unstable = (props: CheckboxProps, ref: React.Ref<HTMLInputElement>): CheckboxState => {
const { disabled, required, shape = 'square', size = 'medium', labelPosition = 'after', onChange } = props;
const { disabled = false, required, shape = 'square', size = 'medium', labelPosition = 'after', onChange } = props;

const [checked, setChecked] = useControllableState({
defaultState: props.defaultChecked,
Expand Down Expand Up @@ -60,6 +60,7 @@ export const useCheckbox_unstable = (props: CheckboxProps, ref: React.Ref<HTMLIn
const state: CheckboxState = {
shape,
checked,
disabled,
size,
labelPosition,
components: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,125 +18,107 @@ const indicatorSizeLarge = '20px';
const useRootBaseClassName = makeResetStyles({
position: 'relative',
display: 'inline-flex',
cursor: 'pointer',
color: tokens.colorNeutralForeground3,
...createFocusOutlineStyle({ style: {}, selector: 'focus-within' }),
});

const useInputBaseClassName = makeResetStyles({
boxSizing: 'border-box',
cursor: 'pointer',
height: '100%',
margin: 0,
opacity: 0,
position: 'absolute',
top: 0,
// 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 checkbox.
width: `calc(${indicatorSizeMedium} + 2 * ${tokens.spacingHorizontalS})`,

// Colors for the unchecked state
':enabled:not(:checked):not(:indeterminate)': {
[`& ~ .${checkboxClassNames.label}`]: {
color: tokens.colorNeutralForeground3,
},
[`& ~ .${checkboxClassNames.indicator}`]: {
borderColor: tokens.colorNeutralStrokeAccessible,
},

const useRootStyles = makeStyles({
unchecked: {
':hover': {
[`& ~ .${checkboxClassNames.label}`]: {
color: tokens.colorNeutralForeground2,
},
[`& ~ .${checkboxClassNames.indicator}`]: {
borderColor: tokens.colorNeutralStrokeAccessibleHover,
color: tokens.colorNeutralForeground2,
[`> .${checkboxClassNames.indicator}`]: {
...shorthands.borderColor(tokens.colorNeutralStrokeAccessibleHover),
},
},

':active:hover': {
[`& ~ .${checkboxClassNames.label}`]: {
color: tokens.colorNeutralForeground1,
},
[`& ~ .${checkboxClassNames.indicator}`]: {
borderColor: tokens.colorNeutralStrokeAccessiblePressed,
':active': {
color: tokens.colorNeutralForeground1,
[`> .${checkboxClassNames.indicator}`]: {
...shorthands.borderColor(tokens.colorNeutralStrokeAccessiblePressed),
},
},
},

// Colors for the checked state
':enabled:checked:not(:indeterminate)': {
[`& ~ .${checkboxClassNames.label}`]: {
color: tokens.colorNeutralForeground1,
},
[`& ~ .${checkboxClassNames.indicator}`]: {
checked: {
color: tokens.colorNeutralForeground1,

[`> .${checkboxClassNames.indicator}`]: {
backgroundColor: tokens.colorCompoundBrandBackground,
color: tokens.colorNeutralForegroundInverted,
borderColor: tokens.colorCompoundBrandBackground,
...shorthands.borderColor(tokens.colorCompoundBrandBackground),
},

':hover': {
[`& ~ .${checkboxClassNames.indicator}`]: {
[`> .${checkboxClassNames.indicator}`]: {
backgroundColor: tokens.colorCompoundBrandBackgroundHover,
borderColor: tokens.colorCompoundBrandBackgroundHover,
...shorthands.borderColor(tokens.colorCompoundBrandBackgroundHover),
},
},

':active:hover': {
[`& ~ .${checkboxClassNames.indicator}`]: {
':active': {
[`> .${checkboxClassNames.indicator}`]: {
backgroundColor: tokens.colorCompoundBrandBackgroundPressed,
borderColor: tokens.colorCompoundBrandBackgroundPressed,
...shorthands.borderColor(tokens.colorCompoundBrandBackgroundPressed),
},
},
},

// Colors for the mixed state
':enabled:indeterminate': {
[`& ~ .${checkboxClassNames.label}`]: {
color: tokens.colorNeutralForeground1,
},
[`& ~ .${checkboxClassNames.indicator}`]: {
borderColor: tokens.colorCompoundBrandStroke,
mixed: {
color: tokens.colorNeutralForeground1,

[`> .${checkboxClassNames.indicator}`]: {
...shorthands.borderColor(tokens.colorCompoundBrandStroke),
color: tokens.colorCompoundBrandForeground1,
},

':hover': {
[`& ~ .${checkboxClassNames.indicator}`]: {
borderColor: tokens.colorCompoundBrandStrokeHover,
[`> .${checkboxClassNames.indicator}`]: {
...shorthands.borderColor(tokens.colorCompoundBrandStrokeHover),
color: tokens.colorCompoundBrandForeground1Hover,
},
},

':active:hover': {
[`& ~ .${checkboxClassNames.indicator}`]: {
borderColor: tokens.colorCompoundBrandStrokePressed,
':active': {
[`> .${checkboxClassNames.indicator}`]: {
...shorthands.borderColor(tokens.colorCompoundBrandStrokePressed),
color: tokens.colorCompoundBrandForeground1Pressed,
},
},
},

':disabled': {
disabled: {
cursor: 'default',

[`& ~ .${checkboxClassNames.label}`]: {
cursor: 'default',
color: tokens.colorNeutralForegroundDisabled,
'@media (forced-colors: active)': {
color: 'GrayText',
},
},
[`& ~ .${checkboxClassNames.indicator}`]: {
borderColor: tokens.colorNeutralStrokeDisabled,
color: tokens.colorNeutralForegroundDisabled,

[`> .${checkboxClassNames.indicator}`]: {
...shorthands.borderColor(tokens.colorNeutralStrokeDisabled),
color: tokens.colorNeutralForegroundDisabled,
'@media (forced-colors: active)': {
color: 'GrayText',
},
},
[`& ~ .${checkboxClassNames.indicator} svg`]: {
'@media (forced-colors: active)': {

'@media (forced-colors: active)': {
color: 'GrayText',
[`> .${checkboxClassNames.indicator}`]: {
color: 'GrayText',
},
},
},
});

const useInputBaseClassName = makeResetStyles({
boxSizing: 'border-box',
cursor: 'inherit',
height: '100%',
margin: 0,
opacity: 0,
position: 'absolute',
top: 0,
// 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 checkbox.
width: `calc(${indicatorSizeMedium} + 2 * ${tokens.spacingHorizontalS})`,
});

const useInputStyles = makeStyles({
before: {
right: 0,
Expand All @@ -160,7 +142,7 @@ const useIndicatorBaseClassName = makeResetStyles({
justifyContent: 'center',
overflow: 'hidden',

border: tokens.strokeWidthThin + ' solid',
border: tokens.strokeWidthThin + ' solid ' + tokens.colorNeutralStrokeAccessible,
borderRadius: tokens.borderRadiusSmall,
margin: tokens.spacingVerticalS + ' ' + tokens.spacingHorizontalS,
fill: 'currentColor',
Expand All @@ -187,7 +169,8 @@ const useIndicatorStyles = makeStyles({
const useLabelStyles = makeStyles({
base: {
alignSelf: 'center',
cursor: 'pointer',
color: 'inherit',
cursor: 'inherit',
...shorthands.padding(tokens.spacingVerticalS, tokens.spacingHorizontalS),
},

Expand All @@ -214,10 +197,22 @@ const useLabelStyles = makeStyles({
* Apply styling to the Checkbox slots based on the state
*/
export const useCheckboxStyles_unstable = (state: CheckboxState): CheckboxState => {
const { labelPosition, shape, size } = state;
const { checked, disabled, labelPosition, shape, size } = state;

const rootBaseClassName = useRootBaseClassName();
state.root.className = mergeClasses(checkboxClassNames.root, rootBaseClassName, state.root.className);
const rootStyles = useRootStyles();
state.root.className = mergeClasses(
checkboxClassNames.root,
rootBaseClassName,
disabled
? rootStyles.disabled
: checked === 'mixed'
? rootStyles.mixed
: checked
? rootStyles.checked
: rootStyles.unchecked,
state.root.className,
);

const inputBaseClassName = useInputBaseClassName();
const inputStyles = useInputStyles();
Expand Down