1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 09:23:21 +01:00

Feat/15438/create ds checkbox component (#19808)

* add ds checkbox

---------

Co-authored-by: Garrett Bear <gwhisten@gmail.com>

---------

Co-authored-by: georgewrmarshall <george.marshall@consensys.net>
This commit is contained in:
Garrett Bear 2023-07-14 11:50:47 -07:00 committed by GitHub
parent 1295474dc3
commit 775ca0dc31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 854 additions and 0 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m412 145c12 12 12 32 0 45l-185 183c-12 12-32 12-45 0l-81-80c-13-13-13-33 0-46 12-12 32-12 45 0l59 58 161-161c13-12 33-12 46 1z"/></svg>

After

Width:  |  Height:  |  Size: 207 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m93 224l320 0c18 0 32 14 32 32 0 18-14 32-32 32l-320 0c-18 0-32-14-32-32 0-18 14-32 32-32z"/></svg>

After

Width:  |  Height:  |  Size: 171 B

View File

@ -0,0 +1,218 @@
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import { Checkbox } from './checkbox';
# Checkbox
Checkbox is a graphical element that allows users to select one or more options from a set of choices.
<Canvas>
<Story id="components-componentlibrary-checkbox--default-story" />
</Canvas>
## Props
The `Checkbox` accepts all props below as well as all [Box](/docs/components-componentlibrary-box--docs#props) component props
<ArgsTable of={Checkbox} />
### Label
Use the `label` string prop to set the label of the `Checkbox`
<Canvas>
<Story id="components-componentlibrary-checkbox--label" />
</Canvas>
```jsx
import { Checkbox } from '../../component-library';
<Checkbox label="Checkbox Label" />;
```
### IsChecked
Use the `isChecked` boolean prop to set the checked state of the `Checkbox`
<Canvas>
<Story id="components-componentlibrary-checkbox--is-checked" />
</Canvas>
```jsx
import { Checkbox } from '../../component-library';
<Checkbox isChecked={true} label="isChecked demo" />;
```
### IsIndeterminate
Use the `isIndeterminate` boolean prop to set the indeterminate state of the `Checkbox`
<Canvas>
<Story id="components-componentlibrary-checkbox--is-indeterminate" />
</Canvas>
```jsx
import React from 'react';
import { Checkbox, Box } from '../../component-library';
const [checkedItems, setCheckedItems] = React.useState([false, true, false]);
const allChecked = checkedItems.every(Boolean);
const isIndeterminate = checkedItems.some(Boolean) && !allChecked;
const handleIndeterminateChange = () => {
if (allChecked || isIndeterminate) {
setCheckedItems([false, false, false]);
} else {
setCheckedItems([true, true, true]);
}
};
const handleCheckboxChange = (index, value) => {
const newCheckedItems = [...checkedItems];
newCheckedItems[index] = value;
setCheckedItems(newCheckedItems);
};
<Checkbox
label="isIndeterminate demo"
isChecked={allChecked}
isIndeterminate={isIndeterminate}
onChange={handleIndeterminateChange}
marginBottom={2}
/>
<Box
marginLeft={2}
gap={1}
display={Display.Flex}
flexDirection={FlexDirection.Column}
>
<Checkbox
isChecked={checkedItems[0]}
onChange={(e) => handleCheckboxChange(0, e.target.checked)}
label="Checkbox 1"
/>
<Checkbox
isChecked={checkedItems[1]}
onChange={(e) => handleCheckboxChange(1, e.target.checked)}
label="Checkbox 2"
/>
<Checkbox
isChecked={checkedItems[2]}
onChange={(e) => handleCheckboxChange(2, e.target.checked)}
label="Checkbox 3"
/>
</Box>
```
### IsDisabled
Use the `isDisabled` boolean prop to set the disabled state of the `Checkbox`
<Canvas>
<Story id="components-componentlibrary-checkbox--is-disabled" />
</Canvas>
```jsx
import { Checkbox } from '../../component-library';
<Checkbox isDisabled={true} label="isDisabled demo" />;
```
### IsReadOnly
Use the `isReadOnly` boolean prop to set the readOnly attribute of the `Checkbox`
<Canvas>
<Story id="components-componentlibrary-checkbox--is-read-only" />
</Canvas>
```jsx
import { Checkbox } from '../../component-library';
<Checkbox isReadOnly={true} label="isReadOnly demo" />;
```
### OnChange
Use the `onChange` function prop to set the onChange handler of the `Checkbox`
<Canvas>
<Story id="components-componentlibrary-checkbox--on-change" />
</Canvas>
```jsx
import React from 'react';
import { Checkbox } from '../../component-library';
const [isChecked, setIsChecked] = React.useState(false);
<Checkbox
onChange={() => setIsChecked(!isChecked)}
isChecked={isChecked}
label="isReadOnly demo"
/>;
```
### IsRequired
Use the `isRequired` boolean prop to set the required attribute of the `Checkbox`
<Canvas>
<Story id="components-componentlibrary-checkbox--is-required" />
</Canvas>
```jsx
import { Checkbox } from '../../component-library';
<Checkbox isRequired={true} label="isRequired demo" />;
```
### Title
Use the `title` string prop to set the title attribute of the `Checkbox`
The title attribute is used to provide additional context or information about the checkbox. It is primarily used for browser native tooltip functionality.
<Canvas>
<Story id="components-componentlibrary-checkbox--title" />
</Canvas>
```jsx
import { Checkbox } from '../../component-library';
<Checkbox title="Apples" label="Inspect to see title attribute" />;
```
### Name
Use the `name` string prop to set the name attribute of the `Checkbox`
This is best used when working with a form and submitting the value of the `Checkbox`
<Canvas>
<Story id="components-componentlibrary-checkbox--name" />
</Canvas>
```jsx
import { Checkbox } from '../../component-library';
<Checkbox name="pineapple" label="Inspect to see name attribute" />;
```
### InputProps
Use the `inputProps` object prop to add additonal props or attributes to the hidden input element
If needing to pass a ref to the input element, use the `inputRef` prop
<Canvas>
<Story id="components-componentlibrary-checkbox--input-props" />
</Canvas>
```jsx
import { Checkbox } from '../../component-library';
<Checkbox
label="inputProps demo"
inputProps={{ borderColor: BorderColor.errorDefault }}
/>;
```

View File

@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Checkbox should render the Checkbox without crashing 1`] = `
<div>
<label
class="box mm-text mm-checkbox mm-text--body-md box--display-inline-flex box--flex-direction-row box--align-items-center box--color-text-default"
>
<span
class="mm-checkbox__input-wrapper"
>
<input
class="mm-box mm-checkbox__input mm-box--margin-0 mm-box--margin-right-0 mm-box--display-flex mm-box--background-color-transparent mm-box--rounded-sm mm-box--border-color-border-default mm-box--border-width-2 box--border-style-solid"
type="checkbox"
/>
</span>
</label>
</div>
`;

View File

@ -0,0 +1,53 @@
.mm-checkbox {
cursor: pointer;
&__input-wrapper {
position: relative;
}
&__input {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
&:hover:not(:disabled) {
background-color: var(--color-background-default-hover);
}
&:focus {
border-color: var(--color-primary-default);
}
&:disabled {
color: var(--color-icon-muted);
cursor: not-allowed;
}
}
&__input--checked:hover:not(:disabled),
&__input--indeterminate:hover:not(:disabled) {
border-color: var(--color-primary-alternative);
background-color: var(--color-primary-alternative);
}
&__input--checked#{&}__input--readonly,
&__input--checked#{&}__input--readonly:hover,
&__input--indeterminate#{&}__input--readonly,
&__input--indeterminate#{&}__input--readonly:hover {
border-color: var(--color-icon-alternative);
background-color: var(--color-icon-alternative);
cursor: not-allowed;
}
&--disabled {
opacity: var(--opacity-disabled);
}
&__icon {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
}

View File

@ -0,0 +1,177 @@
import { StoryFn, Meta } from '@storybook/react';
import { useArgs } from '@storybook/client-api';
import React from 'react';
import { Box } from '..';
import {
BorderColor,
Display,
FlexDirection,
} from '../../../helpers/constants/design-system';
import README from './README.mdx';
import { Checkbox } from '.';
export default {
title: 'Components/ComponentLibrary/Checkbox',
component: Checkbox,
parameters: {
docs: {
page: README,
},
},
argTypes: {
label: {
control: 'text',
},
name: {
control: 'text',
},
id: {
control: 'text',
},
},
} as Meta<typeof Checkbox>;
const Template: StoryFn<typeof Checkbox> = (args) => {
const [{ isChecked }, updateArgs] = useArgs();
return (
<Checkbox
{...args}
onChange={() =>
updateArgs({
isChecked: !isChecked,
})
}
isChecked={isChecked}
/>
);
};
export const DefaultStory = Template.bind({});
DefaultStory.storyName = 'Default';
export const Label = Template.bind({});
Label.args = {
label: 'Checkbox label',
};
export const Id = Template.bind({});
Id.args = {
label: 'Id demo',
id: 'id-demo',
};
export const IsChecked = Template.bind({});
IsChecked.args = {
isChecked: true,
label: 'isChecked demo',
};
export const IsIndeterminate = (args) => {
const [checkedItems, setCheckedItems] = React.useState([false, true, false]);
const allChecked = checkedItems.every(Boolean);
const isIndeterminate = checkedItems.some(Boolean) && !allChecked;
const handleIndeterminateChange = () => {
if (allChecked || isIndeterminate) {
setCheckedItems([false, false, false]);
} else {
setCheckedItems([true, true, true]);
}
};
const handleCheckboxChange = (index, value) => {
const newCheckedItems = [...checkedItems];
newCheckedItems[index] = value;
setCheckedItems(newCheckedItems);
};
return (
<div>
<Checkbox
{...args}
isChecked={allChecked}
isIndeterminate={isIndeterminate}
onChange={handleIndeterminateChange}
marginBottom={2}
/>
<Box
marginLeft={2}
gap={1}
display={Display.Flex}
flexDirection={FlexDirection.Column}
>
<Checkbox
isChecked={checkedItems[0]}
onChange={(e) => handleCheckboxChange(0, e.target.checked)}
label="Checkbox 1"
/>
<Checkbox
isChecked={checkedItems[1]}
onChange={(e) => handleCheckboxChange(1, e.target.checked)}
label="Checkbox 2"
/>
<Checkbox
isChecked={checkedItems[2]}
onChange={(e) => handleCheckboxChange(2, e.target.checked)}
label="Checkbox 3"
/>
</Box>
</div>
);
};
IsIndeterminate.args = {
label: 'isIndeterminate demo',
isIndeterminate: true,
};
export const IsDisabled = Template.bind({});
IsDisabled.args = {
isDisabled: true,
label: 'isDisabled demo',
};
export const IsReadOnly = Template.bind({});
IsReadOnly.args = {
isReadOnly: true,
isChecked: true,
label: 'isReadOnly demo',
};
export const OnChange = Template.bind({});
OnChange.args = {
label: 'onChange demo',
};
export const IsRequired = Template.bind({});
IsRequired.args = {
isRequired: true,
isChecked: true,
label: 'isRequired demo',
};
export const Title = Template.bind({});
Title.args = {
title: 'Apples',
label: 'Inspect to see title attribute',
};
export const Name = Template.bind({});
Name.args = {
name: 'pineapple',
label: 'Inspect to see name attribute',
};
export const InputProps = Template.bind({});
InputProps.args = {
inputProps: { borderColor: BorderColor.errorDefault },
label: 'inputProps demo',
};

View File

@ -0,0 +1,182 @@
import * as React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { BorderColor } from '../../../helpers/constants/design-system';
import { Checkbox } from '.';
describe('Checkbox', () => {
it('should render the Checkbox without crashing', () => {
const { getByRole, container } = render(<Checkbox />);
expect(getByRole('checkbox')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
it('should render the Checkbox with additional className', () => {
const { getByTestId } = render(
<Checkbox data-testid="classname" className="mm-test" />,
);
expect(getByTestId('classname')).toHaveClass('mm-checkbox mm-test');
});
it('should render the Checkbox with additional className on the input', () => {
const { getByRole } = render(
<Checkbox
data-testid="classname"
inputProps={{ className: 'mm-test' }}
/>,
);
expect(getByRole('checkbox')).toHaveClass('mm-checkbox__input mm-test');
});
it('should render the Checkbox with border color changed from inputProps', () => {
const { getByRole } = render(
<Checkbox
data-testid="classname"
inputProps={{ borderColor: BorderColor.errorDefault }}
/>,
);
expect(getByRole('checkbox')).toHaveClass(
'mm-box--border-color-error-default',
);
});
it('should render isChecked', () => {
const { getByRole, getByTestId } = render(
<Checkbox isChecked={true} iconProps={{ 'data-testid': 'check-bold' }} />,
);
expect(getByRole('checkbox')).toBeChecked();
expect(window.getComputedStyle(getByTestId('check-bold')).maskImage).toBe(
`url('./images/icons/check-bold.svg')`,
);
});
it('should render isIndeterminate', () => {
const { getByRole, getByTestId } = render(
<Checkbox
isIndeterminate={true}
iconProps={{ 'data-testid': 'minus-bold' }}
/>,
);
expect(getByRole('checkbox').getAttribute('data-indeterminate')).toBe(
'true',
);
expect(window.getComputedStyle(getByTestId('minus-bold')).maskImage).toBe(
`url('./images/icons/minus-bold.svg')`,
);
});
it('should render checkbox with label', () => {
const { getByText } = render(<Checkbox label="Option 1" />);
expect(getByText('Option 1')).toBeDefined();
});
it('should render checkbox with id and label has matching htmlfor', () => {
const { getByTestId, getByRole } = render(
<Checkbox label="Option 1" id="option-1" data-testid="label" />,
);
const checkbox = getByRole('checkbox');
expect(checkbox).toHaveAttribute('id', 'option-1');
expect(getByTestId('label')).toHaveAttribute('for', 'option-1');
});
test('Checkbox component is disabled when isDisabled is true', () => {
const { getByRole, getByTestId } = render(
<Checkbox
label="Option 1"
id="option-1"
data-testid="option-disabled"
isDisabled={true}
/>,
);
const checkbox = getByRole('checkbox');
expect(checkbox).toBeDisabled();
expect(getByTestId('option-disabled')).toHaveClass('mm-checkbox--disabled');
});
test('Checkbox component is readOnly when isReadOnly is true', () => {
const { getByLabelText } = render(
<Checkbox label="Option 1" id="option-1" isReadOnly={true} />,
);
const checkbox = getByLabelText('Option 1');
expect(checkbox).toHaveAttribute('readonly');
expect(checkbox).toHaveClass('mm-checkbox__input--readonly');
});
it('Checkbox component fires onChange function when clicked', () => {
const onChange = jest.fn();
const { getByTestId } = render(
<Checkbox data-testid="checkbox" onChange={onChange} />,
);
const checkbox = getByTestId('checkbox');
fireEvent.click(checkbox);
expect(onChange).toHaveBeenCalled();
});
it('Checkbox component fires onChange function label clicked', () => {
const onChange = jest.fn();
const { getByText } = render(
<Checkbox label="Click label" onChange={onChange} />,
);
const label = getByText('Click label');
fireEvent.click(label);
expect(onChange).toHaveBeenCalled();
});
test('Checkbox component is required when isRequired is true', () => {
const { getByLabelText } = render(
<Checkbox label="Option 1" id="option-1" isRequired={true} />,
);
const checkbox = getByLabelText('Option 1');
expect(checkbox).toHaveAttribute('required');
});
test('Checkbox component renders with the correct title attribute', () => {
const { getByLabelText } = render(
<Checkbox label="Option 1" id="option-1" title="pineapple" />,
);
const checkbox = getByLabelText('Option 1');
expect(checkbox).toHaveAttribute('title', 'pineapple');
});
test('Checkbox component renders with the correct title attribute used from the label', () => {
const { getByLabelText } = render(
<Checkbox label="Option 1" id="option-1" />,
);
const checkbox = getByLabelText('Option 1');
expect(checkbox).toHaveAttribute('title', 'Option 1');
});
test('Checkbox component renders with the correct title attribute used from the id', () => {
const { getByRole } = render(<Checkbox id="option-1" />);
const checkbox = getByRole('checkbox');
expect(checkbox).toHaveAttribute('title', 'option-1');
});
test('Checkbox component renders with the correct name attribute', () => {
const { getByRole } = render(<Checkbox name="option-1" />);
const checkbox = getByRole('checkbox');
expect(checkbox).toHaveAttribute('name', 'option-1');
});
});

View File

@ -0,0 +1,124 @@
import React, { ChangeEvent, KeyboardEvent } from 'react';
import classnames from 'classnames';
import {
BackgroundColor,
BorderColor,
BorderRadius,
IconColor,
Display,
AlignItems,
} from '../../../helpers/constants/design-system';
import type { PolymorphicRef } from '../box';
import { Box, Icon, IconName, Text } from '..';
import { CheckboxProps, CheckboxComponent } from './checkbox.types';
export const Checkbox: CheckboxComponent = React.forwardRef(
<C extends React.ElementType = 'div'>(
{
id,
isChecked,
isIndeterminate,
isDisabled,
isReadOnly,
isRequired,
onChange,
className = '',
iconProps,
inputProps,
inputRef,
title,
name,
label,
...props
}: CheckboxProps<C>,
ref?: PolymorphicRef<C>,
) => {
const handleCheckboxKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
onChange?.(event as unknown as ChangeEvent<HTMLInputElement>);
}
};
// If no title is provided, use the label as the title only if the label is a string
const sanitizedTitle =
!title && typeof label === 'string' ? label : title || id;
return (
<Text
className={classnames('mm-checkbox', className, {
'mm-checkbox--disabled': Boolean(isDisabled),
})}
as="label"
display={Display.InlineFlex}
alignItems={AlignItems.center}
ref={ref}
htmlFor={id}
{...props}
>
<span className="mm-checkbox__input-wrapper">
<Box
as="input"
type="checkbox"
title={sanitizedTitle}
name={name}
id={id}
checked={isChecked}
disabled={isDisabled}
readOnly={isReadOnly}
required={isRequired}
data-indeterminate={isIndeterminate}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
if (isReadOnly) {
event.preventDefault();
} else {
onChange?.(event);
}
}}
onKeyDown={handleCheckboxKeyDown}
margin={0}
marginRight={label ? 2 : 0}
backgroundColor={
isChecked || isIndeterminate
? BackgroundColor.primaryDefault
: BackgroundColor.transparent
}
borderColor={
isChecked || isIndeterminate
? BorderColor.primaryDefault
: BorderColor.borderDefault
}
borderRadius={BorderRadius.SM}
borderWidth={2}
display={Display.Flex}
ref={inputRef}
{...inputProps}
className={classnames(
'mm-checkbox__input',
inputProps?.className ?? '',
{
'mm-checkbox__input--checked': Boolean(isChecked),
'mm-checkbox__input--indeterminate': Boolean(isIndeterminate),
'mm-checkbox__input--readonly': Boolean(isReadOnly),
},
)}
/>
{(isChecked || isIndeterminate) && (
<Icon
color={IconColor.primaryInverse}
name={isChecked ? IconName.CheckBold : IconName.MinusBold}
className={classnames(
'mm-checkbox__icon',
iconProps?.className ?? '',
)}
{...iconProps}
/>
)}
</span>
{label ? <span>{label}</span> : null}
</Text>
);
},
);

View File

@ -0,0 +1,74 @@
import { IconProps } from '../icon';
import type {
StyleUtilityProps,
PolymorphicComponentPropWithRef,
} from '../box';
export interface CheckboxStyleUtilityProps extends StyleUtilityProps {
/*
* Additional classNames to be added to the Checkbox component
*/
className?: string;
/*
* id - the id for the Checkbox and used for the htmlFor attribute of the label
*/
id?: string;
/*
* isDisabled - if true, the Checkbox will be disabled
*/
isDisabled?: boolean;
/*
* isChecked - if true, the Checkbox will be checked
*/
isChecked?: boolean;
/*
* isIndeterminate - if true, the Checkbox will be indeterminate
*/
isIndeterminate?: boolean;
/*
* isReadOnly - if true, the Checkbox will be read only
*/
isReadOnly?: boolean;
/*
* isRequired - if true, the Checkbox will be required
*/
isRequired?: boolean;
/*
* title can help add additional context to the Checkbox for screen readers and will work for native tooltip elements
* if no title is passed, then it will try to use the label prop if it is a string
*/
title?: string;
/*
* name - to identify the checkbox and associate it with its value during form submission
*/
name?: string;
/*
* onChange - the function to call when the Checkbox is changed
*/
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
/*
* label is the string or ReactNode to be rendered next to the Checkbox
*/
label?: any;
/*
* Use inputProps for additional props to be spread to the checkbox input element
*/
inputProps?: any; // TODO: Replace with Box types when the syntax and typing is properly figured out. Needs to accept everything Box accepts
/*
* Use inputRef to pass a ref to the html input element
*/
inputRef?:
| React.RefObject<HTMLInputElement>
| ((instance: HTMLInputElement | null) => void);
/*
* iconProps - additional props to be spread to the Icon component used for the Checkbox
*/
iconProps?: IconProps;
}
export type CheckboxProps<C extends React.ElementType> =
PolymorphicComponentPropWithRef<C, CheckboxStyleUtilityProps>;
export type CheckboxComponent = <C extends React.ElementType = 'div'>(
props: CheckboxProps<C>,
) => React.ReactElement | null;

View File

@ -0,0 +1,2 @@
export { Checkbox } from './checkbox';
export type { CheckboxProps } from './checkbox.types';

View File

@ -27,6 +27,7 @@
@import 'button-link/button-link';
@import 'button-primary/button-primary';
@import 'button-secondary/button-secondary';
@import 'checkbox/checkbox';
@import 'input/input';
// Molecules
@import 'picker-network/picker-network';

View File

@ -44,6 +44,7 @@ export enum IconName {
Card = 'card',
Category = 'category',
Chart = 'chart',
CheckBold = 'check-bold',
Check = 'check',
Clock = 'clock',
Close = 'close',
@ -93,6 +94,7 @@ export enum IconName {
Menu = 'menu',
MessageQuestion = 'message-question',
Messages = 'messages',
MinusBold = 'minus-bold',
MinusSquare = 'minus-square',
Minus = 'minus',
Mobile = 'mobile',

View File

@ -21,6 +21,7 @@ export { ButtonIcon, ButtonIconSize } from './button-icon';
export { ButtonLink, BUTTON_LINK_SIZES } from './button-link';
export { ButtonPrimary, BUTTON_PRIMARY_SIZES } from './button-primary';
export { ButtonSecondary, BUTTON_SECONDARY_SIZES } from './button-secondary';
export { Checkbox } from './checkbox';
export { FormTextField } from './form-text-field';
export { HeaderBase } from './header-base';
export { HelpText } from './help-text';