Skip to content

Commit

Permalink
feat: selectors: adds checkbox indeterminate state and selector pill …
Browse files Browse the repository at this point in the history
…variants (#694)

* feat: selectors: adds checkbox indeterminate state and selector pill variants

* chore: selectors: export types and add another checkbox unit test
  • Loading branch information
dkilgore-eightfold committed Aug 24, 2023
1 parent c9010ea commit 7941608
Show file tree
Hide file tree
Showing 16 changed files with 1,033 additions and 28 deletions.
58 changes: 50 additions & 8 deletions src/components/CheckBox/CheckBox.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React, { useState } from 'react';
import { Stories } from '@storybook/addon-docs';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { useArgs } from '@storybook/client-api';
import {
CheckBox,
CheckBoxGroup,
CheckboxValueType,
LabelAlign,
LabelPosition,
SelectorSize,
SelectorVariant,
SelectorWidth,
} from './';

export default {
Expand Down Expand Up @@ -99,18 +102,44 @@ export default {
],
control: { type: 'radio' },
},
variant: {
options: [SelectorVariant.Default, SelectorVariant.Pill],
control: { type: 'inline-radio' },
},
selectorWidth: {
options: [SelectorWidth.fitContent, SelectorWidth.fill],
control: { type: 'inline-radio' },
},
},
} as ComponentMeta<typeof CheckBox>;

const CheckBox_Story: ComponentStory<typeof CheckBox> = (args) => (
<CheckBox checked={true} {...args} />
);
const CheckBox_Story: ComponentStory<typeof CheckBox> = (args) => {
const [_, updateArgs] = useArgs();
const onSelectionChange = (event: React.ChangeEvent<HTMLInputElement>) => {
updateArgs({
...args,
checked: event.currentTarget.checked,
indeterminate: false,
});
};
return <CheckBox {...args} onChange={onSelectionChange} />;
};

const CheckBox_Long_text_Story: ComponentStory<typeof CheckBox> = (args) => (
<div style={{ width: 200 }}>
<CheckBox checked={true} {...args} />
</div>
);
const CheckBox_Long_text_Story: ComponentStory<typeof CheckBox> = (args) => {
const [_, updateArgs] = useArgs();
const onSelectionChange = (event: React.ChangeEvent<HTMLInputElement>) => {
updateArgs({
...args,
checked: event.currentTarget.checked,
indeterminate: false,
});
};
return (
<div style={{ width: 200 }}>
<CheckBox {...args} onChange={onSelectionChange} />
</div>
);
};

const CheckBoxGroup_Story: ComponentStory<typeof CheckBoxGroup> = (args) => {
const [selected, setSelected] = useState<CheckboxValueType[]>([]);
Expand All @@ -127,6 +156,7 @@ const CheckBoxGroup_Story: ComponentStory<typeof CheckBoxGroup> = (args) => {
};

export const Check_Box = CheckBox_Story.bind({});
export const Check_Box_Pill = CheckBox_Story.bind({});
export const Check_Box_Long_Text = CheckBox_Long_text_Story.bind({});
export const Check_Box_Group = CheckBoxGroup_Story.bind({});

Expand All @@ -135,30 +165,40 @@ export const Check_Box_Group = CheckBoxGroup_Story.bind({});
// See https://www.npmjs.com/package/babel-plugin-named-exports-order
export const __namedExportsOrder = [
'Check_Box',
'Check_Box_Pill',
'Check_Box_Long_Text',
'Check_Box_Group',
];

const checkBoxArgs: Object = {
allowDisabledFocus: false,
ariaLabel: 'Label',
checked: true,
classNames: 'my-checkbox-class',
disabled: false,
indeterminate: false,
name: 'myCheckBoxName',
value: 'label',
label: 'Label',
labelPosition: LabelPosition.End,
labelAlign: LabelAlign.Center,
id: 'myCheckBoxId',
defaultChecked: false,
selectorWidth: SelectorWidth.fitContent,
size: SelectorSize.Medium,
toggle: false,
variant: SelectorVariant.Default,
};

Check_Box.args = {
...checkBoxArgs,
};

Check_Box_Pill.args = {
...checkBoxArgs,
variant: SelectorVariant.Pill,
};

Check_Box_Long_Text.args = {
...checkBoxArgs,
label:
Expand Down Expand Up @@ -191,5 +231,7 @@ Check_Box_Group.args = {
},
],
layout: 'vertical',
selectorWidth: SelectorWidth.fitContent,
size: SelectorSize.Medium,
variant: SelectorVariant.Default,
};
33 changes: 31 additions & 2 deletions src/components/CheckBox/CheckBox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import React from 'react';
import Enzyme, { mount } from 'enzyme';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
import MatchMediaMock from 'jest-matchmedia-mock';
import { CheckBox, CheckBoxGroup, SelectorSize } from './';
import {
CheckBox,
CheckBoxGroup,
SelectorSize,
SelectorWidth,
SelectorVariant,
} from './';

Enzyme.configure({ adapter: new Adapter() });

Expand All @@ -29,11 +35,34 @@ describe('CheckBox', () => {
expect(wrapper.find('.toggle')).toBeTruthy();
});

test('simulate disabled CheckBox', () => {
test('Simulate disabled CheckBox', () => {
const wrapper = mount(<CheckBox disabled label="test label" />);
wrapper.find('input').html().includes('disabled=""');
});

test('Simulate indeterminate CheckBox', () => {
const wrapper = mount(<CheckBox indeterminate label="test label" />);
wrapper.find('input').html().includes('indeterminate');
});

test('Checkbox is pill', () => {
const wrapper = mount(
<CheckBox variant={SelectorVariant.Pill} label="test label" />
);
expect(wrapper.find('.selector-pill')).toBeTruthy();
});

test('Checkbox is fill pill', () => {
const wrapper = mount(
<CheckBox
selectorWidth={SelectorWidth.fill}
variant={SelectorVariant.Pill}
label="test label"
/>
);
expect(wrapper.find('.selector-pill-stretch')).toBeTruthy();
});

test('CheckBox is large', () => {
const wrapper = mount(
<CheckBox size={SelectorSize.Large} label="test label" />
Expand Down
50 changes: 47 additions & 3 deletions src/components/CheckBox/CheckBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@ import React, { FC, Ref, useContext, useEffect, useRef, useState } from 'react';
import DisabledContext, { Disabled } from '../ConfigProvider/DisabledContext';
import { SizeContext, Size } from '../ConfigProvider';
import { generateId, mergeClasses } from '../../shared/utilities';
import { CheckboxProps, LabelAlign, LabelPosition, SelectorSize } from './';
import {
CheckboxProps,
LabelAlign,
LabelPosition,
SelectorSize,
SelectorVariant,
SelectorWidth,
} from './';
import { Breakpoints, useMatchMedia } from '../../hooks/useMatchMedia';
import { FormItemInputContext } from '../Form/Context';
import { useCanvasDirection } from '../../hooks/useCanvasDirection';
import { useMergedRefs } from '../../hooks/useMergedRefs';

import styles from './checkbox.module.scss';

Expand All @@ -24,15 +32,18 @@ export const CheckBox: FC<CheckboxProps> = React.forwardRef(
disabled = false,
formItemInput = false,
id,
indeterminate = false,
label,
labelPosition = LabelPosition.End,
labelAlign = LabelAlign.Center,
name,
onChange,
selectorWidth = SelectorWidth.fitContent,
size = SelectorSize.Medium,
style,
toggle = false,
value,
variant = SelectorVariant.Default,
'data-test-id': dataTestId,
},
ref: Ref<HTMLInputElement>
Expand All @@ -44,10 +55,20 @@ export const CheckBox: FC<CheckboxProps> = React.forwardRef(

const htmlDir: string = useCanvasDirection();

const internalRef: React.MutableRefObject<HTMLInputElement> =
useRef<HTMLInputElement>(null);

const mergedRef: (node: HTMLInputElement) => void = useMergedRefs(
internalRef,
ref
);

const checkBoxId = useRef<string>(id || generateId());
const [isChecked, setIsChecked] = useState<boolean>(
defaultChecked || checked
);
const [isIndeterminate, setIsIndeterminate] =
useState<boolean>(indeterminate);

const { isFormItemInput } = useContext(FormItemInputContext);
const mergedFormItemInput: boolean = isFormItemInput || formItemInput;
Expand All @@ -62,12 +83,35 @@ export const CheckBox: FC<CheckboxProps> = React.forwardRef(
? size
: contextuallySized || size;

useEffect(() => {
useEffect((): void => {
setIsChecked(checked);
}, [checked]);

useEffect((): void => {
setIsIndeterminate(indeterminate);
if (internalRef.current) {
internalRef.current.indeterminate = indeterminate;
}
}, [indeterminate]);

const checkboxWrapperClassNames: string = mergeClasses([
styles.selector,
{
[styles.selectorPill]: variant === SelectorVariant.Pill,
},
{
[styles.selectorPillActive]:
variant === SelectorVariant.Pill && isChecked,
},
{
[styles.selectorPillIndeterminate]:
variant === SelectorVariant.Pill && isIndeterminate,
},
{
[styles.selectorPillStretch]:
variant === SelectorVariant.Pill &&
selectorWidth === SelectorWidth.fill,
},
{
[styles.selectorSmall]:
mergedSize === SelectorSize.Flex && largeScreenActive,
Expand Down Expand Up @@ -126,7 +170,7 @@ export const CheckBox: FC<CheckboxProps> = React.forwardRef(
data-test-id={dataTestId}
>
<input
ref={ref}
ref={mergedRef}
aria-disabled={mergedDisabled}
aria-label={ariaLabel}
checked={isChecked}
Expand Down
6 changes: 6 additions & 0 deletions src/components/CheckBox/CheckBoxGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
LabelAlign,
LabelPosition,
SelectorSize,
SelectorVariant,
SelectorWidth,
} from './';
import { Breakpoints, useMatchMedia } from '../../hooks/useMatchMedia';
import { FormItemInputContext } from '../Form/Context';
Expand All @@ -33,9 +35,11 @@ export const CheckBoxGroup: FC<CheckboxGroupProps> = React.forwardRef(
labelAlign = LabelAlign.Center,
layout = 'vertical',
onChange,
selectorWidth = SelectorWidth.fitContent,
size = SelectorSize.Medium,
style,
value,
variant = SelectorVariant.Default,
...rest
},
ref: Ref<HTMLInputElement>
Expand Down Expand Up @@ -118,7 +122,9 @@ export const CheckBoxGroup: FC<CheckboxGroupProps> = React.forwardRef(
onChange?.(newValue);
}
}}
selectorWidth={selectorWidth}
size={mergedSize}
variant={variant}
/>
))}
</div>
Expand Down
36 changes: 36 additions & 0 deletions src/components/CheckBox/Checkbox.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ export enum SelectorSize {
Small = 'small',
}

export enum SelectorWidth {
fitContent = 'fitContent',
fill = 'fill',
}

export enum SelectorVariant {
Default = 'default',
Pill = 'pill',
}

export interface CheckboxProps extends OcBaseProps<HTMLInputElement> {
/**
* Allows focus on the checkbox when it's disabled.
Expand Down Expand Up @@ -53,6 +63,10 @@ export interface CheckboxProps extends OcBaseProps<HTMLInputElement> {
* @default false
*/
formItemInput?: boolean;
/**
* Whether or not the checkbox state is indeterminate.
*/
indeterminate?: boolean;
/**
* The checkbox input name.
*/
Expand All @@ -75,6 +89,12 @@ export interface CheckboxProps extends OcBaseProps<HTMLInputElement> {
* The checkbox onChange event handler.
*/
onChange?: React.ChangeEventHandler<HTMLInputElement>;
/**
* The checkbox width type
* Use when variant is `SelectorVariant.Pill`
* @default fitContent
*/
selectorWidth?: SelectorWidth;
/**
* The checkbox size.
* @default SelectorSize.Medium
Expand All @@ -89,6 +109,11 @@ export interface CheckboxProps extends OcBaseProps<HTMLInputElement> {
* The checkbox value.
*/
value?: CheckboxValueType;
/**
* Determines the checkbox variant.
* @default SelectorVariant.Default
*/
variant?: SelectorVariant;
}

export interface CheckboxGroupProps
Expand Down Expand Up @@ -139,6 +164,12 @@ export interface CheckboxGroupProps
* @param checkedValue
*/
onChange?: (checkedValue: CheckboxValueType[]) => void;
/**
* The checkbox group width type
* Use when variant is `SelectorVariant.Pill`
* @default fitContent
*/
selectorWidth?: SelectorWidth;
/**
* The checkbox size.
* @default SelectorSize.Medium
Expand All @@ -148,4 +179,9 @@ export interface CheckboxGroupProps
* The checkbox value.
*/
value?: CheckboxValueType[];
/**
* Determines the checkbox group variant.
* @default SelectorVariant.Default
*/
variant?: SelectorVariant;
}
Loading

0 comments on commit 7941608

Please sign in to comment.