Skip to content

Commit

Permalink
feat: add checkbox and radio button components (EightfoldAI#56)
Browse files Browse the repository at this point in the history
* feat: add Checkbox and RadioButton components

* fix: update styles according to figma, implement comments on pr

* fix: remove colors, styling polish

* fix: resolve errors in console

* fix: resolve console errors for checkbox

* fix: resolve comments, add style variables, refactor

* fix: connect radio and checkbox to input with htmlFor={id} and use provider pattern for radio group

* fix: sync/resolve with main

* chore: refactor

* chore: refactor radio

* chore: remove rem conversion from css, add keydown handlers

* fix: focus-visible on checkbox/radio, adjust tabindex, use arrow keys when tab pressed

* fix: initial radio button selected, index was off after tab

* fix: off index update when tab and clicking

* fix: shift + tab to go to prev section, current radio always has focus

* chore: add unit tests

* fix: update tests

* fix: update snapshots

* fix: update snapshot yarn

* chore: snapshots: updates snapshots

* fix: useState on id's for snapshot tests to pass

Co-authored-by: yash <yash@MacBook-Pro-Yash-Kelkar-RQMM9RC70H.local>
Co-authored-by: yash <yash@MBPYashMM9RC70H.home>
Co-authored-by: yash <yash@MBPYashMM9RC70H.socal.rr.com>
Co-authored-by: Dylan Kilgore <dkilgore@eightfold.ai>
  • Loading branch information
5 people authored May 24, 2022
1 parent a00a1c8 commit fe3caf7
Show file tree
Hide file tree
Showing 19 changed files with 86,944 additions and 6 deletions.
86,110 changes: 86,110 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
"html-replace-webpack-plugin": "2.5.6",
"html-webpack-plugin": "5.5.0",
"husky": "7.0.4",
"icomoon-react": "3.0.0",
"icomoon-react": "^3.0.0",
"identity-obj-proxy": "3.0.0",
"install-peers": "1.0.3",
"install-peers-cli": "2.2.0",
Expand Down
4 changes: 2 additions & 2 deletions src/__snapshots__/storybook.test.js.snap
Git LFS file not shown
42 changes: 42 additions & 0 deletions src/components/Selectors/CheckBox/CheckBox.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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',
id: 'test-1',
},
{
checked: true,
name: 'group',
value: 'Second',
id: 'test-2',
},
{
checked: true,
name: 'group',
value: 'Third',
id: 'test-3',
},
];

return (
<>
<h1>Check Boxes</h1>
<h2>Default Check Box</h2>
<CheckBox checked={true} id="test-4" />
<h2>Label Check Box</h2>
<CheckBox checked={true} value="Label" id="test-5" />
<h2>Check Box Groups</h2>
<CheckBoxGroup items={checkboxGroupItems} />
</>
);
};
18 changes: 18 additions & 0 deletions src/components/Selectors/CheckBox/CheckBox.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import Enzyme, { mount } from 'enzyme';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
import { CheckBox } from '../';

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

describe('RadioButton', () => {
/*
* Functionality Tests
*/
test('Checkbox renders', () => {
const wrapper = mount(<CheckBox checked={true} />);
expect(
wrapper.containsMatchingElement(<CheckBox checked={true} />)
).toEqual(true);
});
});
58 changes: 58 additions & 0 deletions src/components/Selectors/CheckBox/CheckBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { FC, useState } from 'react';
import { CheckBoxProps } from '../';
import { mergeClasses, generateId } from '../../../shared/utilities';

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 : ''}
>
<span className={checkBoxCheckClassNames} tabIndex={0}></span>
<span className={styles.selectorLabel}>{value}</span>
</label>
</div>
);
};
26 changes: 26 additions & 0 deletions src/components/Selectors/CheckBox/CheckBoxGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { FC } from 'react';
import { CheckBoxProps } from '../';
import { CheckBox } from './CheckBox';
import { generateId } from '../../../shared/utilities';

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}
/>
))}
</>
);
};
144 changes: 144 additions & 0 deletions src/components/Selectors/CheckBox/checkbox.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
.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,
&:focus-visible {
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,
&:focus-visible {
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,
&:focus-visible {
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,
&:focus-visible {
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;
}
36 changes: 36 additions & 0 deletions src/components/Selectors/RadioButton/RadioButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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',
id: `oea2exk-${i}`,
}));

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

function _radioClicked(radio: RadioButtonChecked): void {
console.log(radio);
}
18 changes: 18 additions & 0 deletions src/components/Selectors/RadioButton/RadioButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import Enzyme, { mount } from 'enzyme';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
import { RadioButton } from '../';

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

describe('RadioButton', () => {
/*
* Functionality Tests
*/
test('Radio button renders', () => {
const wrapper = mount(<RadioButton checked={true} />);
expect(
wrapper.containsMatchingElement(<RadioButton checked={true} />)
).toEqual(true);
});
});
Loading

0 comments on commit fe3caf7

Please sign in to comment.