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

fix: improve usability of checkbox, checkbox group, radio and radio group component #156

Merged
247 changes: 209 additions & 38 deletions src/__snapshots__/storybook.test.js.snap

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions src/components/ConfigProvider/ConfigProvider.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ConfigProvider, OcThemeNames, useConfig } from './';
import { MatchScore } from '../MatchScore';
import { Spinner } from '../Spinner';
import { Stack } from '../Stack';
import { CheckBoxGroup, CheckboxValueType, RadioGroup } from '../Selectors';

const theme: OcThemeNames[] = [
'red',
Expand Down Expand Up @@ -206,6 +207,44 @@ const ThemedComponents: FC = () => {
</Tabs>
<MatchScore score={3} />
<Spinner />
<CheckBoxGroup
{...{
value: ['First'],
defaultChecked: ['First'],
items: [
{
name: 'group',
value: 'First',
label: 'First',
id: 'test-1',
},
{
name: 'group',
value: 'Second',
label: 'Second',
id: 'test-2',
},
{
name: 'group',
value: 'Third',
label: 'Third',
id: 'test-3',
},
],
}}
/>
<RadioGroup
{...{
ariaLabel: 'Radio Group',
value: 'Radio1',
items: [1, 2, 3].map((i) => ({
value: `Radio${i}`,
label: `Radio${i}`,
name: 'group',
id: `oea2exk-${i}`,
})),
}}
/>
</Stack>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/Inputs/input.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@

.text-area {
background-color: var(--background-color);
border: 1px solid --border-color;
border: 1px solid var(--border-color);
border-radius: $corner-radius-s;
box-sizing: border-box;
color: var(--grey-color-60);
Expand Down
35 changes: 21 additions & 14 deletions src/components/Selectors/CheckBox/CheckBox.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import React, { useState } from 'react';
import { Stories } from '@storybook/addon-docs';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { CheckBox, CheckBoxGroup } from '../index';
import { CheckBox, CheckBoxGroup } from '../';
import { CheckboxValueType } from './Checkbox.types';

export default {
title: 'Check Box',
Expand Down Expand Up @@ -74,10 +75,6 @@ export default {
defaultValue: true,
control: { type: 'boolean' },
},
defaultChecked: {
defaultValue: false,
control: { type: 'boolean' },
},
},
} as ComponentMeta<typeof CheckBox>;

Expand All @@ -87,9 +84,16 @@ const CheckBox_Story: ComponentStory<typeof CheckBox> = (args) => (

export const Check_Box = CheckBox_Story.bind({});

const CheckBoxGroup_Story: ComponentStory<typeof CheckBox> = (args) => (
<CheckBoxGroup {...args} />
);
const CheckBoxGroup_Story: ComponentStory<typeof CheckBoxGroup> = (args) => {
const [selected, setSelected] = useState<CheckboxValueType[]>([]);
return (
<CheckBoxGroup
{...args}
value={selected}
onChange={(newSelected) => setSelected([...newSelected])}
/>
);
};

export const Check_Box_Group = CheckBoxGroup_Story.bind({});

Expand All @@ -99,33 +103,36 @@ const checkBoxArgs: Object = {
classNames: 'my-checkbox-class',
disabled: false,
name: 'myCheckBoxName',
value: 'Label',
value: 'label',
label: 'Label',
id: 'myCheckBoxId',
defaultChecked: false,
};

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

Check_Box_Group.args = {
...checkBoxArgs,
value: ['First'],
defaultChecked: ['First'],
items: [
{
checked: true,
name: 'group',
value: 'First',
label: 'First',
id: 'test-1',
},
{
checked: true,
name: 'group',
value: 'Second',
label: 'Second',
id: 'test-2',
},
{
checked: true,
name: 'group',
value: 'Third',
label: 'Third',
id: 'test-3',
},
],
Expand Down
129 changes: 75 additions & 54 deletions src/components/Selectors/CheckBox/CheckBox.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,79 @@
import React, { FC, useState } from 'react';
import { CheckBoxProps } from '../';
import { mergeClasses, generateId } from '../../../shared/utilities';
import React, { FC, Ref, useEffect, useRef, useState } from 'react';
import { generateId, mergeClasses } from '../../../shared/utilities';
import { CheckboxProps } from './Checkbox.types';

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

export const CheckBox: FC<CheckBoxProps> = ({
ariaLabel,
checked = false,
defaultChecked,
disabled = false,
name,
value = '',
id,
onChange,
}) => {
const [checkBoxId] = useState<string>(id || generateId());
const [isChecked, setIsChecked] = useState<boolean>(checked);

const checkBoxCheckClassNames: string = mergeClasses([
styles.checkmark,
{ [styles.disabled]: disabled },
]);

const toggleChecked = (): void => {
if (!disabled) setIsChecked(!isChecked);
};

const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key !== 'Tab') event.preventDefault();
if (event.key === 'Enter' || event.key === ' ') toggleChecked();
};

return (
<div className={styles.selector} onKeyDown={handleKeyDown}>
<input
aria-label={ariaLabel}
checked={isChecked}
defaultChecked={defaultChecked}
disabled={disabled}
id={checkBoxId}
onChange={onChange ? onChange : toggleChecked}
name={name}
tabIndex={-1}
type={'checkbox'}
value={value}
readOnly
/>
<label
htmlFor={checkBoxId}
className={value === '' ? styles.labelNoValue : ''}
export const CheckBox: FC<CheckboxProps> = React.forwardRef(
(
{
ariaLabel,
checked = false,
defaultChecked,
disabled = false,
name,
value,
id,
onChange,
label,
classNames,
style,
'data-test-id': dataTestId,
},
ref: Ref<HTMLInputElement>
) => {
const checkBoxId = useRef<string>(id || generateId());
const [isChecked, setIsChecked] = useState<boolean>(
defaultChecked || checked
);

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

const checkboxWrapperClassNames: string = mergeClasses([
styles.selector,
classNames,
]);

const checkBoxCheckClassNames: string = mergeClasses([
styles.checkmark,
]);

const labelClassNames: string = mergeClasses([
{ [styles.labelNoValue]: value === '' },
]);

const toggleChecked = (
e: React.ChangeEvent<HTMLInputElement>
): void => {
setIsChecked(e.target.checked);
onChange?.(e);
};

return (
<div
className={checkboxWrapperClassNames}
style={style}
data-test-id={dataTestId}
>
<span className={checkBoxCheckClassNames} tabIndex={0}></span>
<span className={styles.selectorLabel}>{value}</span>
</label>
</div>
);
};
<input
ref={ref}
aria-label={ariaLabel}
checked={isChecked}
disabled={disabled}
id={checkBoxId.current}
onChange={toggleChecked}
name={name}
type={'checkbox'}
value={value}
readOnly
/>
<label htmlFor={checkBoxId.current} className={labelClassNames}>
<span className={checkBoxCheckClassNames} />
<span className={styles.selectorLabel}>{label}</span>
</label>
</div>
);
}
);
69 changes: 45 additions & 24 deletions src/components/Selectors/CheckBox/CheckBoxGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,47 @@
import React, { FC } from 'react';
import { CheckBoxProps } from '../';
import React, { FC, Ref } from 'react';
import { CheckBox } from './CheckBox';
import { generateId } from '../../../shared/utilities';
import { mergeClasses } from '../../../shared/utilities';
import { CheckboxGroupProps } from './Checkbox.types';

export const CheckBoxGroup: FC<CheckBoxProps> = ({
defaultChecked = true,
items,
onChange,
}) => {
return (
<>
{items.map((item, index) => (
<CheckBox
ariaLabel={item.ariaLabel}
checked={item.checked ? item.checked : defaultChecked}
id={item.id || generateId()}
key={index}
name={item.name}
value={item.value}
onChange={onChange}
/>
))}
</>
);
};
import styles from './checkbox.module.scss';

export const CheckBoxGroup: FC<CheckboxGroupProps> = React.forwardRef(
(
{ items = [], onChange, value, classNames, style, ariaLabel, ...rest },
ref: Ref<HTMLDivElement>
) => {
const checkboxGroupClassNames = mergeClasses([
styles.checkboxGroup,
classNames,
]);

return (
<div
className={checkboxGroupClassNames}
style={style}
role="group"
aria-label={ariaLabel}
ref={ref}
{...rest}
>
{items.map((item) => (
<CheckBox
{...item}
checked={value?.includes(item.value)}
key={item.value}
onChange={() => {
const optionIndex = value?.indexOf(item.value);
const newValue = [...value];
if (optionIndex === -1) {
newValue.push(item.value);
} else {
newValue.splice(optionIndex, 1);
}
onChange?.(newValue);
}}
/>
))}
</div>
);
}
);
Loading