Skip to content

Commit

Permalink
fix: fix checkbox group all selection
Browse files Browse the repository at this point in the history
  • Loading branch information
glenkurniawan-adslot committed Mar 7, 2024
1 parent 4711886 commit 64b5e51
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 9 deletions.
4 changes: 2 additions & 2 deletions src/components/CheckboxGroup/CheckboxGroup.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,13 @@ export const NestedCheckboxes: Story = {
<CheckboxGroup indent>
<CheckboxGroup.All label="Sports" values={allHobbies.sports} />
{allHobbies.sports.map((item) => (
<CheckboxGroup.Item key={item} value={item} label={_.capitalize(item)} />
<CheckboxGroup.Item key={item} value={item} label={_.capitalize(item)} disabled={item === 'swimming' }/>
))}
</CheckboxGroup>
<CheckboxGroup indent>
<CheckboxGroup.All label="Other" values={allHobbies.other} />
{allHobbies.other.map((item) => (
<CheckboxGroup.Item key={item} value={item} label={_.capitalize(item)} />
<CheckboxGroup.Item key={item} value={item} label={_.capitalize(item)} disabled/>
))}
</CheckboxGroup>
</CheckboxGroup>
Expand Down
22 changes: 20 additions & 2 deletions src/components/CheckboxGroup/CheckboxGroupContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const CheckboxGroupProvider = ({
const name = parentCtx.name || nameProp;
const value = parentCtx.value || valueProp;

const [disabledValues, setDisabledValues] = React.useState([]);

const context = React.useMemo(() => {
const getIsItemChecked = (checkboxValue) => {
if (getIsChecked) return getIsChecked(checkboxValue, value);
Expand All @@ -28,11 +30,11 @@ const CheckboxGroupProvider = ({
};

const getIsAllChecked = (values) => {
if (_.isEmpty(values)) return false;
const result = _(values)
.map((item) => getIsItemChecked(item))
.uniq()
.value();

return result.length === 1 ? result[0] : 'partial';
};

Expand All @@ -54,6 +56,18 @@ const CheckboxGroupProvider = ({
}
};

const registerDisabledValue = (disabledValue) => {
if (!_.includes(disabledValues, disabledValue)) {
setDisabledValues((prevValues) => [...prevValues, disabledValue]);
}
};

const unregisterDisabledValue = (disabledValue) => {
if (_.includes(disabledValues, disabledValue)) {
setDisabledValues((prevValues) => _.filter(prevValues, (v) => v !== disabledValue));
}
};

return {
variant,
value,
Expand All @@ -64,8 +78,12 @@ const CheckboxGroupProvider = ({
getIsAllChecked,
onItemChange,
onAllChange,

registerDisabledValue,
unregisterDisabledValue,
disabledValues,
};
}, [getIsChecked, value, name, onChange, variant]);
}, [getIsChecked, value, name, onChange, variant, disabledValues]);

return <CheckboxGroupContext.Provider value={context}>{children}</CheckboxGroupContext.Provider>;
};
Expand Down
22 changes: 17 additions & 5 deletions src/components/CheckboxGroup/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,23 @@ import CheckboxGroupProvider, { useCheckboxGroup } from './CheckboxGroupContext'
import '../RadioGroup/style.css';
import './styles.css';

const CheckboxGroupItem = ({ value, ...rest }) => {
const CheckboxGroupItem = ({ value, disabled, ...rest }) => {
const groupCtx = useCheckboxGroup();
invariant(!_.isEmpty(groupCtx), 'CheckboxGroup.Item: must be used as children of CheckboxGroup');
invariant(!rest.name, 'CheckboxGroup.Item: name will be overridden by CheckboxGroup name');
invariant(!rest.variant, 'CheckboxGroup.Item: variant will be overridden by CheckboxGroup variant');
invariant(!rest.onChange, 'CheckboxGroup.Item: onChange will be overridden by CheckboxGroup onChange');

const { onItemChange, getIsItemChecked, name, variant } = groupCtx;
const { onItemChange, getIsItemChecked, name, variant, registerDisabledValue, unregisterDisabledValue } = groupCtx;

React.useEffect(() => {
if (disabled) {
registerDisabledValue(value);
}
return () => {
unregisterDisabledValue(value);
};
}, [disabled, registerDisabledValue, unregisterDisabledValue, value]);

return (
<Checkbox
Expand All @@ -26,6 +35,7 @@ const CheckboxGroupItem = ({ value, ...rest }) => {
variant={variant}
checked={getIsItemChecked(value)}
onChange={() => onItemChange(value)}
disabled={disabled}
/>
);
};
Expand All @@ -36,17 +46,19 @@ const CheckboxGroupAll = ({ className, label = 'All', values, ...rest }) => {
const groupCtx = useCheckboxGroup();
invariant(!_.isEmpty(groupCtx), 'CheckboxGroup.All: must be used as children of CheckboxGroup');

const { onAllChange, getIsAllChecked, name, variant } = groupCtx;
const { onAllChange, getIsAllChecked, name, variant, disabledValues } = groupCtx;
const enabledValues = _.filter(values, (value) => !_.includes(disabledValues, value));

return (
<Checkbox
{...rest}
className={classnames(className, 'is-all')}
name={name}
label={label}
checked={getIsAllChecked(values)}
onChange={onAllChange(values)}
checked={getIsAllChecked(enabledValues)}
onChange={onAllChange(enabledValues)}
variant={variant}
disabled={_.isEqual(values, disabledValues)}
/>
);
};
Expand Down
139 changes: 139 additions & 0 deletions src/components/CheckboxGroup/index.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -339,3 +339,142 @@ it('should work when the values are updated', async () => {
// eslint-disable-next-line jest-dom/prefer-checked
expect(items[0]).toHaveAttribute('aria-checked', 'mixed');
});

it('should disable checkbox group all if all the items are disabled', () => {
const onChange = jest.fn();
render(
<CheckboxGroup name="movies" value={['terminator']} onChange={onChange}>
<CheckboxGroup.All label="All" dts="target" values={['terminator', 'predator', 'soundofmusic']} />
<CheckboxGroup.Item label="The Terminator" value="terminator" disabled />
<CheckboxGroup.Item label="Predator" value="predator" disabled />
<CheckboxGroup.Item label="The Sound of Music" value="soundofmusic" disabled />
</CheckboxGroup>
);
const checkbox = within(screen.getByDts('target')).getByTestId('checkbox-input');
expect(checkbox).toBeDisabled();
});

it('should not select disabled checkbox item', async () => {
const onChange = jest.fn();
render(
<CheckboxGroup name="movies" value={[]} onChange={onChange}>
<CheckboxGroup.All label="All" dts="target" values={['terminator', 'predator', 'soundofmusic']} />
<CheckboxGroup.Item label="The Terminator" value="terminator" disabled />
<CheckboxGroup.Item label="Predator" value="predator" />
<CheckboxGroup.Item label="The Sound of Music" value="soundofmusic" />
</CheckboxGroup>
);

const checkbox = within(screen.getByDts('target')).getByTestId('checkbox-input');
await user.click(checkbox);
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith(['predator', 'soundofmusic'], 'movies');
});

it('should work when re-render after disabled item is changed to non disabled', async () => {
const Component = () => {
const [value, setValue] = React.useState([]);
const allValues = ['terminator', 'predator', 'soundofmusic'];
const [checkboxDisabled, setCheckboxDisabled] = React.useState(true);

const handleButtonClick = () => {
setCheckboxDisabled(false);
};

return (
<div>
<button data-testid="button" onClick={handleButtonClick}>
enable terminator
</button>
<CheckboxGroup name="movies" value={value} onChange={setValue}>
<CheckboxGroup.All label="All" values={allValues} />
{allValues.map((item) => (
<CheckboxGroup.Item
key={item}
label={item}
value={item}
disabled={item === 'terminator' ? checkboxDisabled : false}
/>
))}
</CheckboxGroup>
</div>
);
};

render(<Component />);

await user.click(screen.getByTestId('button'));
const items = screen.queryAllByTestId('checkbox');
await user.click(within(items[0]).getByTestId('checkbox-input')); // click checkbox all
expect(items[1]).toBeChecked(); // terminator is no longer disabled, so it should be checked
expect(items[2]).toBeChecked(); // predator is checked
expect(items[3]).toBeChecked(); // soundofmusic is checked
});

it('should work when non disabled values are updated', async () => {
const Component = () => {
const [value, setValue] = React.useState([]);
const [allValues, setAllValues] = React.useState(['terminator', 'predator', 'soundofmusic']);

return (
<CheckboxGroup name="movies" value={value} onChange={setValue}>
<CheckboxGroup.All label="All" values={allValues} />
{allValues.map((item) => (
<CheckboxGroup.Item key={item} label={item} value={item} disabled={item === 'terminator' ? true : false} />
))}
<button
data-testid="button"
onClick={() => {
setAllValues((prev) => [...prev, 'batman']);
}}
>
Change All
</button>
</CheckboxGroup>
);
};

render(<Component />);

await user.click(screen.getByTestId('button'));
const items = screen.queryAllByTestId('checkbox');
await user.click(within(items[0]).getByTestId('checkbox-input')); // click checkbox all
expect(items[1]).not.toBeChecked(); // terminator is disabled hence it should not be checked
expect(items[2]).toBeChecked(); // predator is checked
expect(items[3]).toBeChecked(); // soundofmusic is checked
expect(items[4]).toBeChecked(); // batman is checked
});

it('should work when disabled values are updated', async () => {
const Component = () => {
const [value, setValue] = React.useState([]);
const [allValues, setAllValues] = React.useState(['terminator', 'predator', 'soundofmusic']);

return (
<CheckboxGroup name="movies" value={value} onChange={setValue}>
<CheckboxGroup.All label="All" values={allValues} />
{allValues.map((item) => (
<CheckboxGroup.Item key={item} label={item} value={item} disabled={item === 'batman' ? true : false} />
))}
<button
data-testid="button"
onClick={() => {
setAllValues((prev) => [...prev, 'batman']);
}}
>
Change All
</button>
</CheckboxGroup>
);
};

render(<Component />);

await user.click(screen.getByTestId('button'));
const items = screen.queryAllByTestId('checkbox');
await user.click(within(items[0]).getByTestId('checkbox-input')); // click checkbox all
expect(items[1]).toBeChecked(); // terminator is checked
expect(items[2]).toBeChecked(); // predator is checked
expect(items[3]).toBeChecked(); // soundofmusic is checked
expect(items[4]).not.toBeChecked(); // batman is disabled hence it should not be checked
});

0 comments on commit 64b5e51

Please sign in to comment.