Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: selectors: adds checkbox indeterminate state and selector pill variants #694

Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
dkilgore-eightfold marked this conversation as resolved.
Show resolved Hide resolved
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 => {
dkilgore-eightfold marked this conversation as resolved.
Show resolved Hide resolved
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,
dkilgore-eightfold marked this conversation as resolved.
Show resolved Hide resolved
},
{
[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,
dkilgore-eightfold marked this conversation as resolved.
Show resolved Hide resolved
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
Loading