Skip to content

Commit

Permalink
fix(checkbox): Fix indeterminate state not getting correctly applied (#…
Browse files Browse the repository at this point in the history
…2281)

Fixes: #2280   

The underlying checkbox was not getting the indeterminate attribute applied and we were relying on it being both checked and indeterminate to update the visual display. This change sets the attribute and updates the CSS to also check the `:indeterminate` pseudo class. Indeterminate checkboxes are a bit odd as [the state can't be directly changed via HTML and needs to be set with a ref](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#indeterminate_state_checkboxes)

[category:Components]
  • Loading branch information
vibdev authored Jul 7, 2023
1 parent bf423f6 commit aab9333
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 18 deletions.
20 changes: 20 additions & 0 deletions cypress/integration/Checkbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,24 @@ describe('Checkbox', () => {
getCheckbox().should('be.disabled');
});
});

context(`given the 'Indeterminate' story is rendered`, () => {
beforeEach(() => {
h.stories.load('Components/Inputs/Checkbox', 'Indeterminate');
});

it('should not have any axe errors', () => {
cy.checkA11y();
});

it('should have the correct attributes', () => {
getCheckbox()
.eq(1)
.click();
getCheckbox()
.eq(0)
.should('have.prop', 'indeterminate', true)
.should('have.attr', 'aria-checked', 'mixed');
});
});
});
43 changes: 26 additions & 17 deletions modules/react/checkbox/lib/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
styled,
useTheme,
Themeable,
useLocalRef,
} from '@workday/canvas-kit-react/common';
import {borderRadius, colors, inputColors, spaceNumbers} from '@workday/canvas-kit-react/tokens';
import {SystemIcon} from '@workday/canvas-kit-react/icon';
Expand Down Expand Up @@ -132,12 +133,12 @@ const CheckboxInput = styled('input')<CheckboxProps & StyledType>(
},

// States
'&:not(:checked):not(:disabled):not(:focus):hover, &:not(:checked):not(:disabled):active': {
'&:not(:checked):not(:indeterminate):not(:disabled):not(:focus):hover, &:not(:checked):not(:indeterminate):not(:disabled):active': {
'~ div:first-of-type': {
borderColor: variant === 'inverse' ? colors.soap300 : inputColors.hoverBorder,
},
},
'&:checked ~ div:first-of-type': {
'&:checked ~ div:first-of-type, &:indeterminate ~ div:first-of-type': {
borderColor: variant === 'inverse' ? colors.soap300 : themePrimary.main,
backgroundColor: variant === 'inverse' ? colors.frenchVanilla100 : themePrimary.main,
},
Expand All @@ -146,7 +147,7 @@ const CheckboxInput = styled('input')<CheckboxProps & StyledType>(
backgroundColor: variant === 'inverse' ? colors.soap300 : inputColors.disabled.background,
opacity: variant === 'inverse' ? '.4' : '1',
},
'&:disabled:checked ~ div:first-of-type': {
'&:disabled:checked ~ div:first-of-type, &:disabled:indeterminate ~ div:first-of-type': {
borderColor: variant === 'inverse' ? colors.soap300 : themePrimary.light,
backgroundColor: variant === 'inverse' ? colors.soap300 : themePrimary.light,
},
Expand All @@ -167,7 +168,7 @@ const CheckboxInput = styled('input')<CheckboxProps & StyledType>(
outerColor: variant === 'inverse' ? colors.frenchVanilla100 : undefined,
}),
},
'&:checked:focus ~ div:first-of-type': {
'&:checked:focus ~ div:first-of-type, &:indeterminate:focus ~ div:first-of-type': {
...focusRing({
width: 2,
separation: 2,
Expand All @@ -192,10 +193,10 @@ const CheckboxInput = styled('input')<CheckboxProps & StyledType>(
marginLeft: '-6px',
},
},
'&:checked ~ div:first-of-type': {
'&:checked ~ div:first-of-type, &:indeterminate ~ div:first-of-type': {
borderColor: variant === 'inverse' ? colors.soap300 : themePrimary.main,
},
'&:disabled:checked ~ div:first-of-type': {
'&:disabled:checked ~ div:first-of-type, &:disabled:indeterminate ~ div:first-of-type': {
borderColor: themePrimary.light,
backgroundColor: variant === 'inverse' ? colors.soap300 : themePrimary.light,
},
Expand All @@ -219,12 +220,12 @@ const CheckboxInput = styled('input')<CheckboxProps & StyledType>(
variant === 'inverse' ? `1px solid ${colors.soap300}` : `1px solid ${errorColors.inner}`,
boxShadow: `0 0 0 1px ${errorColors.inner}, 0 0 0 2px ${errorColors.outer}`,
},
'&:not(:checked):not(:disabled):not(:focus):hover, &:not(:checked):not(:disabled):active': {
'&:not(:checked):not(:indeterminate):not(:disabled):not(:focus):hover, &:not(:checked):not(:indeterminate):not(:disabled):active': {
'~ div:first-of-type': {
borderColor: variant === 'inverse' ? `1px solid ${colors.soap300}` : errorColors.inner,
},
},
'&:checked ~ div:first-of-type': {
'&:checked ~ div:first-of-type, &:indeterminate ~ div:first-of-type': {
borderColor: variant === 'inverse' ? colors.soap300 : theme.canvas.palette.primary.main,
boxShadow: `
0 0 0 2px ${colors.frenchVanilla100},
Expand All @@ -236,7 +237,7 @@ const CheckboxInput = styled('input')<CheckboxProps & StyledType>(
// Error rings take precedence over focus
...mouseFocusBehavior({
...errorStyles,
'&:not(:checked):focus ~ div:first-of-type': {
'&:not(:checked):not(:indeterminate):focus ~ div:first-of-type': {
border: `1px solid ${errorColors.inner}`,
boxShadow: `0 0 0 1px ${errorColors.inner}, 0 0 0 2px ${errorColors.outer}`,
},
Expand Down Expand Up @@ -266,7 +267,7 @@ const CheckboxBackground = styled('div')<CheckboxProps>(
})
);

const CheckboxCheck = styled('div')<Pick<CheckboxProps, 'checked'>>(
const CheckboxCheck = styled('div')<Pick<CheckboxProps, 'checked' | 'indeterminate'>>(
{
display: 'flex',
flexDirection: 'column',
Expand All @@ -284,9 +285,9 @@ const CheckboxCheck = styled('div')<Pick<CheckboxProps, 'checked'>>(
transition: 'margin 200ms ease',
},
},
({checked}) => ({
opacity: checked ? 1 : 0,
transform: checked ? 'scale(1)' : 'scale(0.5)',
({checked, indeterminate}) => ({
opacity: checked || indeterminate ? 1 : 0,
transform: checked || indeterminate ? 'scale(1)' : 'scale(0.5)',
})
);

Expand Down Expand Up @@ -322,6 +323,13 @@ export const Checkbox = createComponent('input')({
Element
) => {
const inputId = useUniqueId(id);
const {localRef, elementRef} = useLocalRef(ref);
React.useEffect(() => {
if (typeof indeterminate === 'boolean' && localRef.current) {
localRef.current.indeterminate = indeterminate;
}
}, [indeterminate, localRef]);

return (
<CheckboxContainer>
<CheckboxInputWrapper disabled={disabled}>
Expand All @@ -330,18 +338,18 @@ export const Checkbox = createComponent('input')({
checked={checked}
disabled={disabled}
id={inputId}
ref={ref}
ref={elementRef}
type="checkbox"
variant={variant}
aria-checked={indeterminate ? 'mixed' : checked}
{...elemProps}
/>
<CheckboxRipple variant={variant} />
<CheckboxBackground variant={variant} checked={checked} disabled={disabled}>
<CheckboxCheck checked={checked}>
<CheckboxCheck checked={checked} indeterminate={indeterminate}>
{indeterminate ? (
<IndeterminateBox variant={variant} />
) : (
) : checked ? (
<SystemIcon
icon={checkSmallIcon}
color={
Expand All @@ -350,7 +358,7 @@ export const Checkbox = createComponent('input')({
: theme.canvas.palette.primary.contrast
}
/>
)}
) : null}
</CheckboxCheck>
</CheckboxBackground>
</CheckboxInputWrapper>
Expand All @@ -360,6 +368,7 @@ export const Checkbox = createComponent('input')({
disabled={disabled}
variant={variant}
paddingInlineStart={checkboxLabelDistance}
cursor="pointer"
>
{label}
</LabelText>
Expand Down
3 changes: 2 additions & 1 deletion modules/react/checkbox/stories/examples/Indeterminate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ export const Indeterminate = () => {

const anyToppingChecked = newToppings.filter(topping => topping.checked).length > 0;
const anyToppingUnchecked = newToppings.filter(topping => !topping.checked).length > 0;
setPizzaChecked(anyToppingChecked);
const allToppingChecked = !anyToppingUnchecked;
setPizzaIndeterminate(anyToppingChecked && anyToppingUnchecked);
setPizzaChecked(allToppingChecked);
};

return (
Expand Down

0 comments on commit aab9333

Please sign in to comment.