Skip to content

Commit

Permalink
feat(checkbox): add intermediate state
Browse files Browse the repository at this point in the history
Add intermediate state to checkbox

CTRIB-4794
  • Loading branch information
npankov authored and gcornut committed Aug 13, 2024
1 parent 97df888 commit 93ddd25
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 24 deletions.
7 changes: 4 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- `Heading`: fix the default typography when the `as` prop is set.
- `ImageBlock`: internal changes on caption styles (simply and make reusable).
- `Heading`: fix the default typography when the `as` prop is set.
- `ImageBlock`: internal changes on caption styles (simply and make reusable).

### Added

- `Thumbnail`: add `loadingPlaceholderImageRef` to re-use a loaded image as the loading placeholder.
- `Thumbnail`: add `loadingPlaceholderImageRef` to re-use a loaded image as the loading placeholder.
- `Checkbox`: add intermediate state via `isChecked="intermediate"`

## [3.7.5][] - 2024-07-25

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ export const LabelAndHelper = {
},
};

/**
* With intermediate state
*/
export const IntermediateState = {
args: {
isChecked: 'intermediate',
},
};

/**
* Disabled
*/
Expand Down
12 changes: 12 additions & 0 deletions packages/lumx-react/src/components/checkbox/Checkbox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe(`<${Checkbox.displayName}>`, () => {

expect(input).toBeInTheDocument();
expect(input).not.toBeChecked();
expect(input).toHaveAttribute('aria-checked', 'false');
expect(input).not.toBeDisabled();
});

Expand All @@ -51,9 +52,20 @@ describe(`<${Checkbox.displayName}>`, () => {
expect(checkbox).toHaveClass('lumx-checkbox--is-checked');

expect(input).toBeChecked();
expect(input).toHaveAttribute('aria-checked', 'true');
expect(input).toBeDisabled();
});

it('should render intermediate state', () => {
const { checkbox, input } = setup({
isChecked: 'intermediate',
});
expect(checkbox).toHaveClass('lumx-checkbox--is-checked');

expect(input).toBeChecked();
expect(input).toHaveAttribute('aria-checked', 'mixed');
});

it('should render helper and label', () => {
const id = 'checkbox1';
const { props, helper, label, input } = setup({
Expand Down
34 changes: 25 additions & 9 deletions packages/lumx-react/src/components/checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import React, { useMemo, forwardRef, ReactNode, SyntheticEvent, InputHTMLAttributes } from 'react';
import React, { forwardRef, InputHTMLAttributes, ReactNode, SyntheticEvent, useMemo } from 'react';

import classNames from 'classnames';
import { uid } from 'uid';

import { mdiCheck } from '@lumx/icons';
import { mdiCheck, mdiMinus } from '@lumx/icons';

import { Icon, InputHelper, InputLabel, Theme } from '@lumx/react';
import { Comp, GenericProps, HasTheme } from '@lumx/react/utils/type';
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
import { useMergeRefs } from '@lumx/react/utils/mergeRefs';

/**
* Intermediate state of checkbox.
*/
const INTERMEDIATE_STATE = 'intermediate';

/**
* Defines the props of the component.
Expand All @@ -19,8 +25,8 @@ export interface CheckboxProps extends GenericProps, HasTheme {
id?: string;
/** Native input ref. */
inputRef?: React.Ref<HTMLInputElement>;
/** Whether it is checked or not. */
isChecked?: boolean;
/** Whether it is checked or not or intermediate. */
isChecked?: boolean | 'intermediate';
/** Whether the component is disabled or not. */
isDisabled?: boolean;
/** Label text. */
Expand All @@ -29,10 +35,10 @@ export interface CheckboxProps extends GenericProps, HasTheme {
name?: string;
/** Native input value property. */
value?: string;
/** On change callback. */
onChange?(isChecked: boolean, value?: string, name?: string, event?: SyntheticEvent): void;
/** optional props for input */
inputProps?: InputHTMLAttributes<HTMLInputElement>;
/** On change callback. */
onChange?(isChecked: boolean, value?: string, name?: string, event?: SyntheticEvent): void;
}

/**
Expand Down Expand Up @@ -77,6 +83,7 @@ export const Checkbox: Comp<CheckboxProps, HTMLDivElement> = forwardRef((props,
inputProps = {},
...forwardedProps
} = props;
const localInputRef = React.useRef<HTMLInputElement>(null);
const inputId = useMemo(() => id || `${CLASSNAME.toLowerCase()}-${uid()}`, [id]);

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
Expand All @@ -85,14 +92,22 @@ export const Checkbox: Comp<CheckboxProps, HTMLDivElement> = forwardRef((props,
}
};

const intermediateState = isChecked === INTERMEDIATE_STATE;

React.useEffect(() => {
const input = localInputRef.current;
if (input) input.indeterminate = intermediateState;
}, [intermediateState]);

return (
<div
ref={ref}
{...forwardedProps}
className={classNames(
className,
handleBasicClasses({
isChecked,
// Whether state is intermediate class name will "-checked"
isChecked: intermediateState ? true : isChecked,
isDisabled,
isUnchecked: !isChecked,
prefix: CLASSNAME,
Expand All @@ -102,7 +117,7 @@ export const Checkbox: Comp<CheckboxProps, HTMLDivElement> = forwardRef((props,
>
<div className={`${CLASSNAME}__input-wrapper`}>
<input
ref={inputRef}
ref={useMergeRefs(inputRef, localInputRef)}
type="checkbox"
id={inputId}
className={`${CLASSNAME}__input-native`}
Expand All @@ -113,14 +128,15 @@ export const Checkbox: Comp<CheckboxProps, HTMLDivElement> = forwardRef((props,
checked={isChecked}
onChange={handleChange}
aria-describedby={helper ? `${inputId}-helper` : undefined}
aria-checked={intermediateState ? 'mixed' : Boolean(isChecked)}
{...inputProps}
/>

<div className={`${CLASSNAME}__input-placeholder`}>
<div className={`${CLASSNAME}__input-background`} />

<div className={`${CLASSNAME}__input-indicator`}>
<Icon icon={mdiCheck} />
<Icon icon={intermediateState ? mdiMinus : mdiCheck} />
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
import { Checkbox } from '@lumx/react';
import { Checkbox, CheckboxProps } from '@lumx/react';
import React, { useState } from 'react';

export const App = ({ theme }: any) => {
const [value, setValue] = useState(true);
const [value2, setValue2] = useState(false);
const [value3, setValue3] = useState(false);
const useCheckBoxState = (initial: CheckboxProps['isChecked']) => {
const [isChecked, onChange] = useState(initial);
return { isChecked, onChange };
};

return (
<>
<Checkbox isChecked={value} label="Checkbox" theme={theme} onChange={setValue} />
<Checkbox label="Checkbox" {...useCheckBoxState(true)} theme={theme} />

<Checkbox
isChecked={value2}
helper="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed posuere faucibus efficitur."
label="Checkbox with help"
helper="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
{...useCheckBoxState(false)}
theme={theme}
onChange={setValue2}
/>

<Checkbox
isChecked={value3}
helper="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed posuere faucibus efficitur."
label="Disabled checkbox with help"
theme={theme}
helper="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
isDisabled
onChange={setValue3}
theme={theme}
/>

<Checkbox label="Checkbox intermediate state" {...useCheckBoxState('intermediate')} theme={theme} />
</>
);
};

0 comments on commit 93ddd25

Please sign in to comment.