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: add checkbox and radio button components #56

Merged
merged 27 commits into from
May 24, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f928252
feat: add Checkbox and RadioButton components
Apr 12, 2022
a172cab
fix: update styles according to figma, implement comments on pr
Apr 22, 2022
6d17d20
fix: remove colors, styling polish
Apr 26, 2022
757a866
fix: resolve errors in console
Apr 26, 2022
f5bbdb5
fix: resolve console errors for checkbox
Apr 26, 2022
0b7c7bb
fix: resolve comments, add style variables, refactor
Apr 27, 2022
2e43e08
chore: pull latest
Apr 28, 2022
02d0738
fix: connect radio and checkbox to input with htmlFor={id} and use pr…
May 5, 2022
472dc34
Merge remote-tracking branch 'upstream/main' into yash-dev
May 5, 2022
d1fb3e6
fix: sync/resolve with main
May 5, 2022
2f2f7fc
chore: refactor
May 5, 2022
a2c6b6a
chore: refactor radio
May 5, 2022
33eb973
chore: remove rem conversion from css, add keydown handlers
May 6, 2022
42bf2c4
fix: focus-visible on checkbox/radio, adjust tabindex, use arrow keys…
May 10, 2022
f3979ff
Merge remote-tracking branch 'upstream/main' into yash-dev
May 10, 2022
47a1d56
fix: initial radio button selected, index was off after tab
May 18, 2022
7bcf8f6
fix: off index update when tab and clicking
May 18, 2022
47a27c0
fix: shift + tab to go to prev section, current radio always has focus
May 23, 2022
29949e0
Merge remote-tracking branch 'upstream/main' into yash-dev
May 23, 2022
97c8aaf
chore: add unit tests
May 23, 2022
5836f9b
fix: update tests
May 24, 2022
e8f7192
fix: update snapshots
May 24, 2022
dd03594
fix: update snapshot yarn
May 24, 2022
3f25819
chore: snapshots: updates snapshots
dkilgore-eightfold May 24, 2022
59597f9
fix: useState on id's for snapshot tests to pass
May 24, 2022
91d33a5
Merge remote-tracking branch 'upstream/main' into yash-dev
May 24, 2022
890c5f7
chore: merge
May 24, 2022
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
39 changes: 39 additions & 0 deletions src/components/Selectors/CheckBox/CheckBox.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import { CheckBox, CheckBoxGroup } from '../index';

export default {
title: 'Check Box',
component: CheckBox,
};

export const Box = () => {
const checkboxGroupItems = [
{
checked: true,
name: 'group',
value: 'First',
},
{
checked: true,
name: 'group',
value: 'Second',
},
{
checked: true,
name: 'group',
value: 'Third',
},
];

return (
<>
<h1>Check Boxes</h1>
<h2>Default Check Box</h2>
<CheckBox checked={true} />
<h2>Label Check Box</h2>
<CheckBox checked={true} value="Label" />
<h2>Check Box Groups</h2>
<CheckBoxGroup items={checkboxGroupItems} />
</>
);
};
Empty file.
51 changes: 51 additions & 0 deletions src/components/Selectors/CheckBox/CheckBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { FC, useState } from 'react';
import { CheckBoxProps } from '../';
import { mergeClasses } from '../../../shared/utilities';

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

export const CheckBox: FC<CheckBoxProps> = ({
ariaLabel,
checked = false,
defaultChecked,
disabled = false,
name,
value = '',
id = `checkbox-${value}-${Math.random().toString(36).slice(2)}`,
ykelkar-eightfold marked this conversation as resolved.
Show resolved Hide resolved
onChange,
}) => {
const [isChecked, setIsChecked] = useState<boolean>(checked);

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

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

return (
<div className={styles.selector}>
<input
ykelkar-eightfold marked this conversation as resolved.
Show resolved Hide resolved
aria-label={ariaLabel}
checked={isChecked}
defaultChecked={defaultChecked}
disabled={disabled}
id={id}
onChange={onChange ? onChange : toggleChecked}
name={name}
type={'checkbox'}
value={value}
readOnly
/>
<label
htmlFor={id}
className={value === '' ? styles.labelNoValue : ''}
>
<span className={checkBoxCheckClassNames}></span>
ykelkar-eightfold marked this conversation as resolved.
Show resolved Hide resolved
<span className={styles.selectorLabel}>{value}</span>
</label>
</div>
);
};
25 changes: 25 additions & 0 deletions src/components/Selectors/CheckBox/CheckBoxGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { FC } from 'react';
import { CheckBoxProps } from '../';
import { CheckBox } from './CheckBox';

export const CheckBoxGroup: FC<CheckBoxProps> = ({
defaultChecked = true,
items,
ykelkar-eightfold marked this conversation as resolved.
Show resolved Hide resolved
onChange,
}) => {
return (
<>
ykelkar-eightfold marked this conversation as resolved.
Show resolved Hide resolved
{items.map((item, index) => (
<CheckBox
ariaLabel={item.ariaLabel}
checked={item.checked ? item.checked : defaultChecked}
id={item.id}
key={index}
name={item.name}
value={item.value}
onChange={onChange}
/>
))}
</>
);
};
140 changes: 140 additions & 0 deletions src/components/Selectors/CheckBox/checkbox.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
.selector {
margin-bottom: $selector-margin-bottom;
flex-shrink: 1;
position: relative;
display: flex;
text-overflow: elipses;
vertical-align: baseline;

input {
position: absolute;
background: none;
opacity: 0;
height: $selector-input-width;
width: $selector-input-height;
top: $space-xxxs;
left: $space-xxxs;
cursor: pointer;
}

input:checked + label {
.checkmark {
background-color: var(--primary-color);
border: $space-xxxs solid var(--primary-color);

&.disabled {
opacity: 50%;

&:hover {
background-color: var(--primary-color);
border: $space-xxxs solid var(--primary-color);
}

&:focus {
outline: none;
}

&:active {
transform: none;
background-color: var(--primary-color);
border: $space-xxxs solid var(--primary-color);
}
}

&:hover {
background-color: var(--primary-color-60);
border: $space-xxxs solid var(--primary-color-60);
}

&:focus {
ykelkar-eightfold marked this conversation as resolved.
Show resolved Hide resolved
outline: $space-xxxs solid var(--primary-color-50);
outline-offset: $selector-outline-offset;
}

&:active {
transform: scale(0.98);
background-color: var(--primary-color-80);
border: $space-xxxs solid var(--primary-color-80);
}

&:after {
left: $checkmark-after-left;
top: $checkmark-after-top;
width: $checkmark-after-width;
height: $icon-font-size-material-xs;
border: solid white;
border-width: 0 $space-xxxs $space-xxxs 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
display: block;
}
}
}

.checkmark {
height: $checkmark-height;
width: $checkmark-width;
position: absolute;
left: 0;
border-radius: $corner-radius-s;
cursor: pointer;
border: $space-xxxs solid var(--grey-color-70);

&.disabled {
opacity: 50%;

&:hover {
border: $space-xxxs solid var(--grey-color-70);
}

&:focus {
ykelkar-eightfold marked this conversation as resolved.
Show resolved Hide resolved
outline: none;
border: $space-xxxs solid var(--grey-color-70);
}

&:active {
border: $space-xxxs solid var(--grey-color-70);
}
}

&:hover {
border: $space-xxxs solid var(--primary-color-60);
}

&:focus {
border: $space-xxxs solid var(--primary-color);
outline: $space-xxxs solid var(--primary-color-50);
outline-offset: $selector-outline-offset;
}

&:active {
border: $space-xxxs solid var(--primary-color-80);
}

&:after {
content: '';
position: absolute;
display: none;
}
}

label {
display: flex;
align-items: flex-start;
cursor: pointer;
position: relative;
user-select: none;
vertical-align: baseline;
}

.selector-label {
margin-left: $selector-label-margin-left;
font-size: medium;
margin-top: $space-xxxs;
}
}

.label-no-value {
margin-bottom: $label-no-value-margin-bottom;
}
35 changes: 35 additions & 0 deletions src/components/Selectors/RadioButton/RadioButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import { RadioButton, RadioGroup } from '../index';
import { RadioButtonChecked } from '../Selectors.types';

export default {
title: 'Radio Button',
component: RadioButton,
};

export const Radio = () => {
const radioGroupItems = [1, 2, 3].map((i) => ({
value: `Radio${i}`,
name: 'group',
}));

return (
<>
<h1>Radio Buttons</h1>
<h2>Default Radio Button</h2>
<RadioButton checked={true} />
<h2>Label Radio Button</h2>
<RadioButton checked={true} value="Label" />
<h2>Radio Button Groups</h2>
<RadioGroup onChange={_radioClicked} activeRadioButton={'Radio1'}>
{radioGroupItems.map((item) => (
<RadioButton key={item.value} {...item} />
))}
</RadioGroup>
</>
);
};

function _radioClicked(radio: RadioButtonChecked): void {
console.log(radio);
}
Empty file.
46 changes: 46 additions & 0 deletions src/components/Selectors/RadioButton/RadioButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { FC } from 'react';
import { RadioButtonProps } from '../';
import { mergeClasses } from '../../../shared/utilities';
import { useRadioGroup } from './RadioGroup.context';

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

export const RadioButton: FC<RadioButtonProps> = ({
ariaLabel,
checked = false,
disabled = false,
name,
value = '',
id = `radiobox-${value}-${Math.random().toString(36).slice(2)}`,
ykelkar-eightfold marked this conversation as resolved.
Show resolved Hide resolved
}) => {
const { onRadioButtonClick, currentRadioButton } = useRadioGroup();
const isActive: boolean = value === currentRadioButton;

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

return (
<div className={styles.selector}>
ykelkar-eightfold marked this conversation as resolved.
Show resolved Hide resolved
<input
ykelkar-eightfold marked this conversation as resolved.
Show resolved Hide resolved
aria-label={ariaLabel}
checked={isActive ? isActive : checked}
disabled={disabled}
id={id}
name={name}
type={'radio'}
value={value}
onClick={(e) => onRadioButtonClick(value, e)}
readOnly
/>
<label
htmlFor={id}
className={value === '' ? styles.labelNoValue : ''}
>
<span className={radioButtonClassNames}></span>
ykelkar-eightfold marked this conversation as resolved.
Show resolved Hide resolved
<span className={styles.selectorLabel}>{value}</span>
</label>
</div>
);
};
44 changes: 44 additions & 0 deletions src/components/Selectors/RadioButton/RadioGroup.context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { createContext, useState } from 'react';
import {
RadioGroupContextProps,
IRadioButtonsContext,
RadioButtonChecked,
SelectRadioButtonEvent,
} from '../Selectors.types';

const RadioGroupContext = createContext<Partial<IRadioButtonsContext>>({});

const RadioGroupProvider = ({
children,
onChange,
activeRadioButton,
}: RadioGroupContextProps) => {
const [currentRadioButton, setCurrentRadioButton] =
useState<RadioButtonChecked>(activeRadioButton);

const onRadioButtonClick = (
value: RadioButtonChecked,
e: SelectRadioButtonEvent
) => {
setCurrentRadioButton(value);
onChange(value, e);
};

return (
<RadioGroupContext.Provider
value={{ onRadioButtonClick, currentRadioButton }}
>
{children}
</RadioGroupContext.Provider>
);
};

const useRadioGroup = () => {
const context = React.useContext(RadioGroupContext);
if (context === undefined) {
throw new Error('RadioButton component must be used within RadioGroup');
}
return context;
};

export { RadioGroupProvider, useRadioGroup };
Loading