Skip to content
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
3 changes: 3 additions & 0 deletions packages/@react-aria/checkbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
"@react-aria/label": "^3.1.0",
"@react-aria/toggle": "^3.1.0",
"@react-aria/utils": "^3.1.0",
"@react-stately/checkbox": "^3.1.0",
"@react-stately/toggle": "^3.1.0",
"@react-types/checkbox": "^3.1.0"
},
Expand Down
2 changes: 2 additions & 0 deletions packages/@react-aria/checkbox/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@
*/

export * from './useCheckbox';
export * from './useCheckboxGroup';
export * from './useCheckboxGroupItem';
51 changes: 51 additions & 0 deletions packages/@react-aria/checkbox/src/useCheckboxGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import {AriaCheckboxGroupProps} from '@react-types/checkbox';
import {filterDOMProps, mergeProps} from '@react-aria/utils';
import {HTMLAttributes} from 'react';
import {useLabel} from '@react-aria/label';

interface CheckboxGroupAria {
/** Props for the checkbox group wrapper element. */
checkboxGroupProps: HTMLAttributes<HTMLElement>,
/** Props for the checkbox group's visible label (if any). */
labelProps: HTMLAttributes<HTMLElement>
}

/**
* Provides the behavior and accessibility implementation for a checkbox group component.
* Checkbox groups allow users to select multiple items from a list of options.
* @param props - Props for the checkbox group.
* @param state - State for the checkbox group, as returned by `useCheckboxGroupState`.
*/
export function useCheckboxGroup(props: AriaCheckboxGroupProps): CheckboxGroupAria {
let {isDisabled} = props;

let {labelProps, fieldProps} = useLabel({
...props,
// Checkbox group is not an HTML input element so it
// shouldn't be labeled by a <label> element.
labelElementType: 'span'
});

let domProps = filterDOMProps(props, {labelable: true});

return {
checkboxGroupProps: mergeProps(domProps, {
role: 'group',
'aria-disabled': isDisabled || undefined,
...fieldProps
}),
labelProps
};
}
43 changes: 43 additions & 0 deletions packages/@react-aria/checkbox/src/useCheckboxGroupItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import {AriaCheckboxGroupItemProps} from '@react-types/checkbox';
import {CheckboxAria, useCheckbox} from './useCheckbox';
import {CheckboxGroupState} from '@react-stately/checkbox';
import {RefObject} from 'react';
import {useToggleState} from '@react-stately/toggle';

/**
* Provides the behavior and accessibility implementation for a checkbox component contained within a checkbox group.
* Checkbox groups allow users to select multiple items from a list of options.
* @param props - Props for the checkbox.
* @param state - State for the checkbox, as returned by `useCheckboxGroupState`.
* @param inputRef - A ref for the HTML input element.
*/
export function useCheckboxGroupItem(props: AriaCheckboxGroupItemProps, state: CheckboxGroupState, inputRef: RefObject<HTMLInputElement>): CheckboxAria {
const toggleState = useToggleState({
isReadOnly: props.isReadOnly,
isSelected: state.value.includes(props.value),
onChange(isSelected) {
if (isSelected) {
state.addValue(props.value);
} else {
state.removeValue(props.value);
}

if (props.onChange) {
props.onChange(isSelected);
}
}
});
return useCheckbox({...props, name: state.name}, toggleState, inputRef);
}
229 changes: 229 additions & 0 deletions packages/@react-aria/checkbox/test/useCheckboxGroup.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import {act, render} from '@testing-library/react';
import {AriaCheckboxGroupProps, AriaCheckboxProps} from '@react-types/checkbox';
import {CheckboxGroupState, useCheckboxGroupState} from '@react-stately/checkbox';
import React, {useRef} from 'react';
import {useCheckboxGroup, useCheckboxGroupItem} from '../';
import userEvent from '@testing-library/user-event';

function Checkbox({checkboxGroupState, ...props}: AriaCheckboxProps & { checkboxGroupState: CheckboxGroupState }) {
const ref = useRef<HTMLInputElement>();
const {children} = props;
const {inputProps} = useCheckboxGroupItem(props, checkboxGroupState, ref);
return <label>{children}<input ref={ref} {...inputProps} /></label>;
}

function CheckboxGroup({groupProps, checkboxProps}: {groupProps: AriaCheckboxGroupProps, checkboxProps: AriaCheckboxProps[]}) {
const state = useCheckboxGroupState(groupProps);
const {checkboxGroupProps, labelProps} = useCheckboxGroup(groupProps);
return (
<div {...checkboxGroupProps}>
{groupProps.label && <span {...labelProps}>{groupProps.label}</span>}
<Checkbox checkboxGroupState={state} {...checkboxProps[0]} />
<Checkbox checkboxGroupState={state} {...checkboxProps[1]} />
<Checkbox checkboxGroupState={state} {...checkboxProps[2]} />
</div>
);
}

describe('useCheckboxGroup', () => {
it('handles defaults', () => {
let onChangeSpy = jest.fn();
let {getByRole, getAllByRole, getByLabelText} = render(
<CheckboxGroup
groupProps={{label: 'Favorite Pet', onChange: onChangeSpy}}
checkboxProps={[
{value: 'dogs', children: 'Dogs'},
{value: 'cats', children: 'Cats'},
{value: 'dragons', children: 'Dragons'}
]} />
);

let checkboxGroup = getByRole('group', {exact: true});
let checkboxes = getAllByRole('checkbox') as HTMLInputElement[];
expect(checkboxGroup).toBeInTheDocument();
expect(checkboxes.length).toBe(3);

let groupName = checkboxes[0].getAttribute('name');
expect(checkboxes[0]).toHaveAttribute('name', groupName);
expect(checkboxes[1]).toHaveAttribute('name', groupName);
expect(checkboxes[2]).toHaveAttribute('name', groupName);

expect(checkboxes[0].value).toBe('dogs');
expect(checkboxes[1].value).toBe('cats');
expect(checkboxes[2].value).toBe('dragons');

expect(checkboxes[0].checked).toBe(false);
expect(checkboxes[1].checked).toBe(false);
expect(checkboxes[2].checked).toBe(false);

let dragons = getByLabelText('Dragons');
act(() => {userEvent.click(dragons);});
expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(onChangeSpy).toHaveBeenCalledWith(['dragons']);

expect(checkboxes[0].checked).toBe(false);
expect(checkboxes[1].checked).toBe(false);
expect(checkboxes[2].checked).toBe(true);
});

it('can have a default value', () => {
let {getByLabelText} = render(
<CheckboxGroup
groupProps={{label: 'Favorite Pet', value: ['cats']}}
checkboxProps={[
{value: 'dogs', children: 'Dogs'},
{value: 'cats', children: 'Cats'},
{value: 'dragons', children: 'Dragons'}
]} />
);

expect((getByLabelText('Cats') as HTMLInputElement).checked).toBe(true);
});

it('name can be controlled', () => {
let {getAllByRole} = render(
<CheckboxGroup
groupProps={{name: 'awesome-react-aria', label: 'Favorite Pet'}}
checkboxProps={[
{value: 'dogs', children: 'Dogs'},
{value: 'cats', children: 'Cats'},
{value: 'dragons', children: 'Dragons'}
]} />
);

let checkboxes = getAllByRole('checkbox') as HTMLInputElement[];

expect(checkboxes[0]).toHaveAttribute('name', 'awesome-react-aria');
expect(checkboxes[1]).toHaveAttribute('name', 'awesome-react-aria');
expect(checkboxes[2]).toHaveAttribute('name', 'awesome-react-aria');
});

it('supports labeling', () => {
let {getByRole} = render(
<CheckboxGroup
groupProps={{label: 'Favorite Pet'}}
checkboxProps={[
{value: 'dogs', children: 'Dogs'},
{value: 'cats', children: 'Cats'},
{value: 'dragons', children: 'Dragons'}
]} />
);
let checkboxGroup = getByRole('group', {exact: true});

let labelId = checkboxGroup.getAttribute('aria-labelledby');
expect(labelId).toBeDefined();
let label = document.getElementById(labelId);
expect(label).toHaveTextContent('Favorite Pet');
});

it('supports aria-label', () => {
let {getByRole} = render(
<CheckboxGroup
groupProps={{'aria-label': 'My Favorite Pet'}}
checkboxProps={[
{value: 'dogs', children: 'Dogs'},
{value: 'cats', children: 'Cats'},
{value: 'dragons', children: 'Dragons'}
]} />
);
let checkboxGroup = getByRole('group', {exact: true});

expect(checkboxGroup).toHaveAttribute('aria-label', 'My Favorite Pet');
});

it('supports custom props', () => {
const groupProps = {label: 'Favorite Pet', 'data-testid': 'favorite-pet'};
let {getByRole} = render(
<CheckboxGroup
groupProps={groupProps}
checkboxProps={[
{value: 'dogs', children: 'Dogs'},
{value: 'cats', children: 'Cats'},
{value: 'dragons', children: 'Dragons'}
]} />
);
let checkboxGroup = getByRole('group', {exact: true});

expect(checkboxGroup).toHaveAttribute('data-testid', 'favorite-pet');
});

it('sets aria-disabled when isDisabled is true', () => {
let {getByRole} = render(
<CheckboxGroup
groupProps={{label: 'Favorite Pet', isDisabled: true}}
checkboxProps={[
{value: 'dogs', children: 'Dogs'},
{value: 'cats', children: 'Cats'},
{value: 'dragons', children: 'Dragons'}
]} />
);
let checkboxGroup = getByRole('group', {exact: true});

expect(checkboxGroup).toHaveAttribute('aria-disabled', 'true');
});

it('doesn\'t set aria-disabled by default', () => {
let {getByRole} = render(
<CheckboxGroup
groupProps={{label: 'Favorite Pet'}}
checkboxProps={[
{value: 'dogs', children: 'Dogs'},
{value: 'cats', children: 'Cats'},
{value: 'dragons', children: 'Dragons'}
]} />
);
let checkboxGroup = getByRole('group', {exact: true});

expect(checkboxGroup).not.toHaveAttribute('aria-disabled');
});

it('doesn\'t set aria-disabled when isDisabled is false', () => {
let {getByRole} = render(
<CheckboxGroup
groupProps={{label: 'Favorite Pet', isDisabled: false}}
checkboxProps={[
{value: 'dogs', children: 'Dogs'},
{value: 'cats', children: 'Cats'},
{value: 'dragons', children: 'Dragons'}
]} />
);
let checkboxGroup = getByRole('group', {exact: true});

expect(checkboxGroup).not.toHaveAttribute('aria-disabled');
});

it('should not update state for readonly checkbox', () => {
let groupOnChangeSpy = jest.fn();
let checkboxOnChangeSpy = jest.fn();
let {getAllByRole, getByLabelText} = render(
<CheckboxGroup
groupProps={{label: 'Favorite Pet', onChange: groupOnChangeSpy}}
checkboxProps={[
{value: 'dogs', children: 'Dogs'},
{value: 'cats', children: 'Cats'},
{value: 'dragons', children: 'Dragons', isReadOnly: true, onChange: checkboxOnChangeSpy}
]} />
);

let checkboxes = getAllByRole('checkbox') as HTMLInputElement[];
let dragons = getByLabelText('Dragons');

act(() => {userEvent.click(dragons);});

expect(groupOnChangeSpy).toHaveBeenCalledTimes(0);
expect(checkboxOnChangeSpy).toHaveBeenCalledTimes(0);
expect(checkboxes[2].checked).toBe(false);
});
});
1 change: 1 addition & 0 deletions packages/@react-spectrum/checkbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@react-aria/focus": "^3.1.0",
"@react-aria/interactions": "^3.1.0",
"@react-spectrum/utils": "^3.1.0",
"@react-stately/checkbox": "^3.1.0",
"@react-stately/toggle": "^3.1.0",
"@react-types/checkbox": "^3.1.0",
"@react-types/shared": "^3.1.0",
Expand Down
3 changes: 3 additions & 0 deletions packages/@react-stately/checkbox/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @react-stately/checkbox

This package is part of [react-spectrum](https://github.com/adobe/react-spectrum). See the repo for more details.
36 changes: 36 additions & 0 deletions packages/@react-stately/checkbox/docs/useCheckboxGroupState.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!-- Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License. -->

import {Layout} from '@react-spectrum/docs';
export default Layout;

import docs from 'docs:@react-stately/checkbox';
import {ClassAPI, HeaderInfo, TypeContext, FunctionAPI, TypeLink} from '@react-spectrum/docs';
import packageData from '@react-stately/checkbox/package.json';

---
category: Forms
keywords: [toggle, checkbox, switch, input, state]
---

# useCheckboxGroupState

<p>{docs.exports.useCheckboxGroupState.description}</p>

<HeaderInfo
packageData={packageData}
componentNames={['useCheckboxGroupState']} />

## API

<FunctionAPI function={docs.exports.useCheckboxGroupState} links={docs.links} />

## Interface

<ClassAPI links={docs.links} class={docs.links[docs.exports.useCheckboxGroupState.return.id]} />
Loading