+ 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 (
+
+
+
+ handleCheckboxChange(0, e.target.checked)}
+ label="Checkbox 1"
+ />
+ handleCheckboxChange(1, e.target.checked)}
+ label="Checkbox 2"
+ />
+ handleCheckboxChange(2, e.target.checked)}
+ label="Checkbox 3"
+ />
+
+
+ );
+};
+
+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',
+};
diff --git a/ui/components/component-library/checkbox/checkbox.test.tsx b/ui/components/component-library/checkbox/checkbox.test.tsx
new file mode 100644
index 000000000..fb7983842
--- /dev/null
+++ b/ui/components/component-library/checkbox/checkbox.test.tsx
@@ -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();
+ expect(getByRole('checkbox')).toBeInTheDocument();
+ expect(container).toMatchSnapshot();
+ });
+
+ it('should render the Checkbox with additional className', () => {
+ const { getByTestId } = render(
+ ,
+ );
+ expect(getByTestId('classname')).toHaveClass('mm-checkbox mm-test');
+ });
+
+ it('should render the Checkbox with additional className on the input', () => {
+ const { getByRole } = render(
+ ,
+ );
+ expect(getByRole('checkbox')).toHaveClass('mm-checkbox__input mm-test');
+ });
+
+ it('should render the Checkbox with border color changed from inputProps', () => {
+ const { getByRole } = render(
+ ,
+ );
+ expect(getByRole('checkbox')).toHaveClass(
+ 'mm-box--border-color-error-default',
+ );
+ });
+
+ it('should render isChecked', () => {
+ const { getByRole, getByTestId } = render(
+ ,
+ );
+ 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(
+ ,
+ );
+ 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();
+ expect(getByText('Option 1')).toBeDefined();
+ });
+
+ it('should render checkbox with id and label has matching htmlfor', () => {
+ const { getByTestId, getByRole } = render(
+ ,
+ );
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ const label = getByText('Click label');
+
+ fireEvent.click(label);
+
+ expect(onChange).toHaveBeenCalled();
+ });
+
+ test('Checkbox component is required when isRequired is true', () => {
+ const { getByLabelText } = render(
+ ,
+ );
+
+ const checkbox = getByLabelText('Option 1');
+
+ expect(checkbox).toHaveAttribute('required');
+ });
+
+ test('Checkbox component renders with the correct title attribute', () => {
+ const { getByLabelText } = render(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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();
+
+ const checkbox = getByRole('checkbox');
+
+ expect(checkbox).toHaveAttribute('title', 'option-1');
+ });
+
+ test('Checkbox component renders with the correct name attribute', () => {
+ const { getByRole } = render();
+
+ const checkbox = getByRole('checkbox');
+
+ expect(checkbox).toHaveAttribute('name', 'option-1');
+ });
+});
diff --git a/ui/components/component-library/checkbox/checkbox.tsx b/ui/components/component-library/checkbox/checkbox.tsx
new file mode 100644
index 000000000..83d0a8113
--- /dev/null
+++ b/ui/components/component-library/checkbox/checkbox.tsx
@@ -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(
+ (
+ {
+ id,
+ isChecked,
+ isIndeterminate,
+ isDisabled,
+ isReadOnly,
+ isRequired,
+ onChange,
+ className = '',
+ iconProps,
+ inputProps,
+ inputRef,
+ title,
+ name,
+ label,
+ ...props
+ }: CheckboxProps,
+ ref?: PolymorphicRef,
+ ) => {
+ const handleCheckboxKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'Enter') {
+ onChange?.(event as unknown as ChangeEvent);
+ }
+ };
+
+ // 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 (
+
+
+ ) => {
+ 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) && (
+
+ )}
+
+ {label ? {label} : null}
+
+ );
+ },
+);
diff --git a/ui/components/component-library/checkbox/checkbox.types.ts b/ui/components/component-library/checkbox/checkbox.types.ts
new file mode 100644
index 000000000..cc7871b8c
--- /dev/null
+++ b/ui/components/component-library/checkbox/checkbox.types.ts
@@ -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) => 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
+ | ((instance: HTMLInputElement | null) => void);
+ /*
+ * iconProps - additional props to be spread to the Icon component used for the Checkbox
+ */
+ iconProps?: IconProps;
+}
+
+export type CheckboxProps =
+ PolymorphicComponentPropWithRef;
+
+export type CheckboxComponent = (
+ props: CheckboxProps,
+) => React.ReactElement | null;
diff --git a/ui/components/component-library/checkbox/index.ts b/ui/components/component-library/checkbox/index.ts
new file mode 100644
index 000000000..cf2067124
--- /dev/null
+++ b/ui/components/component-library/checkbox/index.ts
@@ -0,0 +1,2 @@
+export { Checkbox } from './checkbox';
+export type { CheckboxProps } from './checkbox.types';
diff --git a/ui/components/component-library/component-library-components.scss b/ui/components/component-library/component-library-components.scss
index d03fb28f1..f6bbd81e4 100644
--- a/ui/components/component-library/component-library-components.scss
+++ b/ui/components/component-library/component-library-components.scss
@@ -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';
diff --git a/ui/components/component-library/icon/icon.types.ts b/ui/components/component-library/icon/icon.types.ts
index 011ca4700..1ad735719 100644
--- a/ui/components/component-library/icon/icon.types.ts
+++ b/ui/components/component-library/icon/icon.types.ts
@@ -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',
diff --git a/ui/components/component-library/index.js b/ui/components/component-library/index.js
index 4da644ec7..564643063 100644
--- a/ui/components/component-library/index.js
+++ b/ui/components/component-library/index.js
@@ -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';