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

MOON-404: Add CheckboxItem and CheckboxGroup components #399

Merged
merged 5 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 26 additions & 1 deletion src/components/Checkbox/Checkbox.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import {render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import {Checkbox} from './index';

describe('Checkbox', () => {
Expand All @@ -16,6 +17,15 @@ describe('Checkbox', () => {
expect(screen.getByRole('checkbox')).toHaveAttribute('data-custom', customAttribute);
});

it('should call onChange function', () => {
const handleOnChange = jest.fn();
render(<Checkbox data-testid="moonstone-checkbox" onChange={event => handleOnChange(event)}/>);
const checkbox = screen.getByTestId('moonstone-checkbox');

userEvent.click(checkbox);
expect(handleOnChange).toHaveBeenCalled();
});

it('should check off when clicked on', () => {
render(<Checkbox aria-label="checkbox"/>);
const checkbox = screen.getByRole('checkbox');
Expand All @@ -31,11 +41,26 @@ describe('Checkbox', () => {
expect(checkbox).not.toBeChecked();
});

it('should initially be checked off when the defaultSelected prop is set', () => {
it('should be unchecked by default', () => {
render(<Checkbox aria-label="checkbox"/>);
expect(screen.getByRole('checkbox')).not.toBeChecked();
});

it('should initially be checked on when the defaultChecked prop is set', () => {
render(<Checkbox defaultChecked aria-label="checkbox"/>);
expect(screen.getByRole('checkbox')).toBeChecked();
});

it('should initially be checked on when the checked prop is set', () => {
render(<Checkbox checked aria-label="checkbox"/>);
expect(screen.getByRole('checkbox')).toBeChecked();
});

it('should initially be mixed state on when the indeterminate prop is set', () => {
render(<Checkbox indeterminate aria-label="checkbox"/>);
expect(screen.getByRole('checkbox')).toBePartiallyChecked();
});

it('should have mixed state when specified with the isIndeterminate prop', () => {
render(<Checkbox checked="mixed" aria-label="checkbox" onChange={() => null}/>);
expect(screen.getByRole('checkbox')).toBePartiallyChecked();
Expand Down
27 changes: 21 additions & 6 deletions src/components/Checkbox/Checkbox.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, {useState} from 'react';
import {Story} from '@storybook/react';

import {Checkbox} from '~/components';
Expand All @@ -8,22 +8,37 @@ export default {
title: 'Components/Checkbox',
component: Checkbox,
parameters: {
layout: 'centered',
actions: {argTypesRegex: '^on.*'}
layout: 'centered'
// When enabled, the controlledCheckbox doesn't work anymore. maybe it's fixed with storybook 7.4 (https://github.com/storybookjs/storybook/pull/23804)
// Actions: {argTypesRegex: '^on.*'}
}
};

const Template: Story<CheckboxProps> = args => <Checkbox {...args}/>;

export const DefaultControlled = Template.bind({});
DefaultControlled.args = {
export const Uncontrolled = Template.bind({});
Uncontrolled.args = {
'aria-label': 'default example checkbox'
};
DefaultControlled.storyName = 'Default and uncontrolled';

export const Indeterminate = Template.bind({});
Indeterminate.args = {
indeterminate: true,
'aria-label': 'indeterminate example checkbox'
};

export const Controlled: Story<CheckboxProps> = args => {
const [checked, setChecked] = useState(false);

const handleOnChange = () => {
setChecked(!checked);
};

return (
<Checkbox
checked={checked}
onChange={() => handleOnChange()}
{...args}
/>
);
};
16 changes: 10 additions & 6 deletions src/components/Checkbox/UncontrolledCheckbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ import './Checkbox.scss';
import {CheckboxProps} from './Checkbox.types';
import {ControlledCheckbox} from '~/components/Checkbox/ControlledCheckbox';

export const UncontrolledCheckbox: React.FC<CheckboxProps> = ({defaultChecked = false, ...props}) => {
export const UncontrolledCheckbox: React.FC<CheckboxProps> = ({defaultChecked = false, onChange, ...props}) => {
const [checked, setChecked] = useState(defaultChecked);

return (
<ControlledCheckbox {...props}
checked={checked}
onChange={event => {
setChecked(event.target.checked);
}}
<ControlledCheckbox
{...props}
checked={checked}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setChecked(!checked);
if (typeof onChange !== 'undefined') {
onChange(event);
}
}}
/>
);
};
Expand Down
4 changes: 4 additions & 0 deletions src/components/CheckboxGroup/CheckboxGroup.context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import {createContext} from 'react';
import {CheckboxGroupContextProps} from './CheckboxGroup.types';

export const CheckboxGroupContext = createContext<CheckboxGroupContextProps | undefined>(undefined);
89 changes: 89 additions & 0 deletions src/components/CheckboxGroup/CheckboxGroup.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from 'react';
import {render, screen} from '@testing-library/react';

import userEvent from '@testing-library/user-event';
import {CheckboxGroup} from './index';
import {CheckboxItem} from './CheckboxItem';

describe('CheckboxGroup', () => {
it('should render', () => {
render(
<CheckboxGroup name="test-grouped-checkboxes">
<CheckboxItem id="checkbox-01" label="checkbox 01" value="01"/>
<CheckboxItem id="checkbox-02" label="checkbox 02" value="02"/>
</CheckboxGroup>
);
expect(screen.getAllByRole('checkbox')).toHaveLength(2);
});

it('should display additional attributes', () => {
render(
<CheckboxGroup data-testid="moonstone-checkboxGroup" name="test-grouped-checkboxes">
<CheckboxItem id="checkbox-01" label="checkbox 01" value="01"/>
<CheckboxItem id="checkbox-02" label="checkbox 02" value="02"/>
</CheckboxGroup>
);
expect(screen.getByTestId('moonstone-checkboxGroup')).toBeInTheDocument();
});

it('should display additional className', () => {
const className = 'test-class';
render(
<CheckboxGroup className={className} data-testid="moonstone-checkboxGroup" name="test-grouped-checkboxes">
<CheckboxItem id="checkbox-01" label="checkbox 01" value="01"/>
<CheckboxItem id="checkbox-02" label="checkbox 02" value="02"/>
</CheckboxGroup>
);
expect(screen.getByTestId('moonstone-checkboxGroup')).toHaveClass(className);
});

it('should not display the CheckboxGroup when children is empty', () => {
render(<CheckboxGroup data-testid="moonstone-radioGroup" name="test-grouped-checkboxes">{[]}</CheckboxGroup>);
expect(screen.queryByTestId('moonstone-checkboxGroup')).not.toBeInTheDocument();
});

it('should be disabled all checkboxItems', () => {
render(
<CheckboxGroup isDisabled name="test-grouped-checkboxes">
<CheckboxItem id="checkbox-01" label="checkbox 01" value="01"/>
<CheckboxItem isDisabled={false} id="checkbox-02" label="checkbox 02" value="02"/>
</CheckboxGroup>
);
expect(screen.getByLabelText('checkbox 01')).toBeDisabled();
expect(screen.getByLabelText('checkbox 02')).toBeDisabled();
});

it('should set the name attribute to all checkboxItems', () => {
render(
<CheckboxGroup name="test-grouped-checkboxes">
<CheckboxItem id="checkbox-01" label="checkbox 01" value="01"/>
<CheckboxItem id="checkbox-02" label="checkbox 02" value="02"/>
</CheckboxGroup>
);
expect(screen.getAllByRole('checkbox')).toHaveLength(2);
});

it('should be read-only all checkboxItems', () => {
render(
<CheckboxGroup isReadOnly name="test-grouped-checkboxes">
<CheckboxItem id="checkbox-01" label="checkbox 01" value="01"/>
<CheckboxItem id="checkbox-02" label="checkbox 02" value="02" isReadOnly={false}/>
</CheckboxGroup>
);
expect(screen.getByLabelText('checkbox 01')).toHaveAttribute('aria-readonly', 'true');
expect(screen.getByLabelText('checkbox 02')).toHaveAttribute('aria-readonly', 'true');
});

it('should call onChange function', () => {
const handleOnChange = jest.fn();
render(
<CheckboxGroup name="test-grouped-checkboxes" onChange={() => handleOnChange()}>
<CheckboxItem id="checkbox-01" label="checkbox 01" value="01"/>
<CheckboxItem id="checkbox-02" label="checkbox 02" value="02"/>
</CheckboxGroup>
);
userEvent.click(screen.getByLabelText('checkbox 01'));
userEvent.click(screen.getByLabelText('checkbox 02'));
expect(handleOnChange).toHaveBeenCalledTimes(2);
});
});
65 changes: 65 additions & 0 deletions src/components/CheckboxGroup/CheckboxGroup.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react';
import {ComponentStory, ComponentMeta} from '@storybook/react';

import {CheckboxGroup} from './index';
import {CheckboxItem} from './CheckboxItem';

export default {
title: 'Components/CheckboxGroup',
component: CheckboxGroup,
parameters: {
layout: 'centered',
knobs: {disable: true},
storysource: {disable: true},
actions: {argTypesRegex: '^on.*'}
},
argTypes: {
children: {
table: {
disable: true
}
}
}
} as ComponentMeta<typeof CheckboxGroup>;

const Template: ComponentStory<typeof CheckboxGroup> = args => {
return (
<CheckboxGroup {...args}>
<CheckboxItem
id="cat"
label="Cat"
description="Miaouw"
value="cat"
/>
<CheckboxItem
id="dog"
label="Dog"
description="Ouah-ouah"
value="dog"
/>
<CheckboxItem
isDisabled
id="horse"
label="Horse"
description="Disabled element"
value="horse"
/>
<CheckboxItem
id="bird"
label="Bird without description"
value="bird"
/>
</CheckboxGroup>
);
};

export const Default = Template.bind({});
Default.args = {
name: 'default'
};

export const Disabled = Template.bind({});
Disabled.args = {
name: 'disabled',
isDisabled: true
};
27 changes: 27 additions & 0 deletions src/components/CheckboxGroup/CheckboxGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import clsx from 'clsx';
import {CheckboxGroupContext} from './CheckboxGroup.context';
import type {CheckboxGroupProps} from './CheckboxGroup.types';

export const CheckboxGroup: React.FC<CheckboxGroupProps> = ({children, name, isDisabled, isReadOnly, className, onChange, ...props}) => {
const provider = {
name: name,
isDisabled,
isReadOnly,
onChange: onChange
};

return (
<CheckboxGroupContext.Provider value={provider}>
<div
{...props}
className={clsx(
'flexCol',
className
)}
>
{children}
</div>
</CheckboxGroupContext.Provider>
);
};
57 changes: 57 additions & 0 deletions src/components/CheckboxGroup/CheckboxGroup.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as React from 'react';
import {CheckboxItemProps} from './CheckboxItem/CheckboxItem.types';

export type CheckboxGroupProps = {
/**
* Set the same name to all CheckboxItem
*/
name: string;

/**
* CheckboxItem component
*/
children: React.ReactElement<CheckboxItemProps>[];

/**
* Additional classname
*/
className?: string;

/**
* Function triggered on change of all CheckboxItems. That function is not replaced the onChange function set on a CheckboxItem, In that case both functions will be executed.
*/
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;

/**
* Whether the checkboxes should be disabled
*/
isDisabled?: boolean;

/**
* Whether the checkboxes can be selected but not changed by the user
*/
isReadOnly?: boolean;
}

export type CheckboxGroupContextProps = {
/**
* Checkboxes' name
*/
name: string;

/**
* Function triggered on change of the checkboxes
*/
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;

/**
* Whether all CheckboxItems should be disabled
*/
isDisabled?: boolean;

/**
* Whether all CheckboxItems should be read-only
*/
isReadOnly?: boolean;
}

Loading