mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-21 17:37:01 +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:
parent
1295474dc3
commit
775ca0dc31
1
app/images/icons/check-bold.svg
Normal file
1
app/images/icons/check-bold.svg
Normal 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 |
1
app/images/icons/minus-bold.svg
Normal file
1
app/images/icons/minus-bold.svg
Normal 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 |
218
ui/components/component-library/checkbox/README.mdx
Normal file
218
ui/components/component-library/checkbox/README.mdx
Normal 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 }}
|
||||
/>;
|
||||
```
|
@ -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>
|
||||
`;
|
53
ui/components/component-library/checkbox/checkbox.scss
Normal file
53
ui/components/component-library/checkbox/checkbox.scss
Normal 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;
|
||||
}
|
||||
}
|
177
ui/components/component-library/checkbox/checkbox.stories.tsx
Normal file
177
ui/components/component-library/checkbox/checkbox.stories.tsx
Normal 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',
|
||||
};
|
182
ui/components/component-library/checkbox/checkbox.test.tsx
Normal file
182
ui/components/component-library/checkbox/checkbox.test.tsx
Normal 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');
|
||||
});
|
||||
});
|
124
ui/components/component-library/checkbox/checkbox.tsx
Normal file
124
ui/components/component-library/checkbox/checkbox.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
);
|
74
ui/components/component-library/checkbox/checkbox.types.ts
Normal file
74
ui/components/component-library/checkbox/checkbox.types.ts
Normal 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;
|
2
ui/components/component-library/checkbox/index.ts
Normal file
2
ui/components/component-library/checkbox/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { Checkbox } from './checkbox';
|
||||
export type { CheckboxProps } from './checkbox.types';
|
@ -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';
|
||||
|
@ -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',
|
||||
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user