From f29e3e04fed2c3b5ad526ec08099f0189c259a10 Mon Sep 17 00:00:00 2001 From: Maxwell Frank Date: Thu, 20 Jul 2023 18:45:59 +0000 Subject: [PATCH] fix: selectable box set props forwarding BREAKING CHANGE: aria-label is required when not using aria-labelledby in the SelectableBoxSet component. An associated label for the component is a WCAG requirement. --- src/SelectableBox/README.md | 51 +++++++++++++++++++ src/SelectableBox/SelectableBoxSet.jsx | 20 ++++++++ .../tests/SelectableBoxSet.test.jsx | 28 +++++++++- .../SelectableBoxSet.test.jsx.snap | 1 + 4 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/SelectableBox/README.md b/src/SelectableBox/README.md index e1bf00d3a4..4ab1f417da 100644 --- a/src/SelectableBox/README.md +++ b/src/SelectableBox/README.md @@ -42,6 +42,7 @@ As ``Checkbox`` onChange={handleChange} name="cheeses" columns={isExtraSmall ? 1 : 2} + ariaLabel="cheese selection" >
@@ -83,6 +84,7 @@ As ``Checkbox`` onChange={handleChange} name="colors" columns={isExtraSmall ? 1 : 3} + ariaLabel="color selection" >
@@ -144,6 +146,7 @@ As ``Checkbox`` with ``isIndeterminate`` onChange={handleChange} name="cheeses" columns={isExtraSmall ? 1 : 3} + ariaLabel="cheese selection" >
@@ -162,3 +165,51 @@ As ``Checkbox`` with ``isIndeterminate`` ); } ``` + +As ``Checkbox`` with ``ariaLabelledby`` + +```jsx live +() => { + const type = 'checkbox'; + const allCheeseOptions = ['swiss', 'cheddar', 'pepperjack']; + const [checkedCheeses, { add, remove, set, clear }] = useCheckboxSetValues(['swiss']); + + const handleChange = e => { + e.target.checked ? add(e.target.value) : remove(e.target.value); + }; + + const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth }); + + return ( +
+

+ Select your favorite cheese +

+ + +

+ Swiss +

+
+ +

+ Cheddar +

+
+ +

+ Pepperjack +

+
+
+
+ ); +} +``` diff --git a/src/SelectableBox/SelectableBoxSet.jsx b/src/SelectableBox/SelectableBoxSet.jsx index 29a9d751ca..f26f3e1f20 100644 --- a/src/SelectableBox/SelectableBoxSet.jsx +++ b/src/SelectableBox/SelectableBoxSet.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { getInputType } from './utils'; +import { requiredWhenNot } from '../utils/propTypes'; const INPUT_TYPES = [ 'radio', @@ -19,6 +20,9 @@ const SelectableBoxSet = React.forwardRef(({ type, columns, className, + ariaLabel, + ariaLabelledby, + ...props }, ref) => { const inputType = getInputType('SelectableBoxSet', type); @@ -35,6 +39,9 @@ const SelectableBoxSet = React.forwardRef(({ `pgn__selectable_box-set--${columns || DEFAULT_COLUMNS_NUMBER}`, className, ), + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledby, + ...props, }, children, ); @@ -62,6 +69,17 @@ SelectableBoxSet.propTypes = { columns: PropTypes.number, /** A class that is be appended to the base element. */ className: PropTypes.string, + /** + * The ID of the label for the `SelectableBoxSet`. + * + * An accessible label must be provided to the `SelectableBoxSet`. + */ + ariaLabelledby: PropTypes.string, + /** + * A label for the `SelectableBoxSet`. + * + * If not using `ariaLabelledby`, then `ariaLabel` must be provided */ + ariaLabel: requiredWhenNot(PropTypes.string, 'ariaLabelledby'), }; SelectableBoxSet.defaultProps = { @@ -72,6 +90,8 @@ SelectableBoxSet.defaultProps = { type: 'radio', columns: DEFAULT_COLUMNS_NUMBER, className: undefined, + ariaLabelledby: undefined, + ariaLabel: undefined, }; export default SelectableBoxSet; diff --git a/src/SelectableBox/tests/SelectableBoxSet.test.jsx b/src/SelectableBox/tests/SelectableBoxSet.test.jsx index b017bd9eee..f21274a519 100644 --- a/src/SelectableBox/tests/SelectableBoxSet.test.jsx +++ b/src/SelectableBox/tests/SelectableBoxSet.test.jsx @@ -1,6 +1,8 @@ import React from 'react'; import { mount } from 'enzyme'; import renderer from 'react-test-renderer'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; import { Form } from '../..'; import SelectableBox from '..'; @@ -11,9 +13,11 @@ const checkboxText = (text) => `SelectableCheckbox${text}`; const radioType = 'radio'; const radioText = (text) => `SelectableRadio${text}`; +const ariaLabel = 'test-default-label'; + function SelectableCheckboxSet(props) { return ( - + {checkboxText(1)} {checkboxText(2)} {checkboxText(3)} @@ -23,7 +27,7 @@ function SelectableCheckboxSet(props) { function SelectableRadioSet(props) { return ( - + {radioText(1)} {radioText(2)} {radioText(3)} @@ -37,6 +41,10 @@ describe('', () => { const tree = renderer.create(()).toJSON(); expect(tree).toMatchSnapshot(); }); + it('forwards props', () => { + render(()); + expect(screen.getByTestId('test-radio-set-name')).toBeInTheDocument(); + }); it('correct render when type prop is changed', () => { const setWrapper = mount(); expect(setWrapper.find(Form.RadioSet).length).toBeGreaterThan(0); @@ -84,5 +92,21 @@ describe('', () => { const selectableBoxSet = wrapper.find(Form.RadioSet); expect(selectableBoxSet.hasClass(`pgn__selectable_box-set--${columns}`)).toBe(true); }); + it('renders with an aria-label attribute', () => { + render(()); + expect(screen.getByLabelText('test-radio-set-label')).toBeInTheDocument(); + }); + it('renders with an aria-labelledby attribute', () => { + render(( + <> +

Radio Set Label text

+ + + )); + expect(screen.getByLabelText('Radio Set Label text')).toBeInTheDocument(); + }); }); }); diff --git a/src/SelectableBox/tests/__snapshots__/SelectableBoxSet.test.jsx.snap b/src/SelectableBox/tests/__snapshots__/SelectableBoxSet.test.jsx.snap index f464052336..c6cba31a1e 100644 --- a/src/SelectableBox/tests/__snapshots__/SelectableBoxSet.test.jsx.snap +++ b/src/SelectableBox/tests/__snapshots__/SelectableBoxSet.test.jsx.snap @@ -2,6 +2,7 @@ exports[` correct rendering renders without props 1`] = `