diff --git a/packages/terra-core-docs/CHANGELOG.md b/packages/terra-core-docs/CHANGELOG.md index a6442c26783..b858861cc05 100644 --- a/packages/terra-core-docs/CHANGELOG.md +++ b/packages/terra-core-docs/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +* Added + * Added single and multi select list test examples for `terra-list`. + +* Updated + * Updated single and multi select guides with the new ariaSelectionStyle prop for `terra-list`. + ## 1.16.0 - (December 7, 2022) * Added diff --git a/packages/terra-core-docs/src/terra-dev-site/doc/list/guides.7/MultiSelectList.2.doc.mdx b/packages/terra-core-docs/src/terra-dev-site/doc/list/guides.7/MultiSelectList.2.doc.mdx index 4c4bc191c6f..ad3fb80e2ef 100644 --- a/packages/terra-core-docs/src/terra-dev-site/doc/list/guides.7/MultiSelectList.2.doc.mdx +++ b/packages/terra-core-docs/src/terra-dev-site/doc/list/guides.7/MultiSelectList.2.doc.mdx @@ -11,7 +11,7 @@ The terra-list implementation requires controlled state if selections are requir ## State Management The state of selection needs to be managed for the list in a High Order Component (HOC). In this example we are going to be a unique key, but the type of state used is open to the implementor of the HOC. -First defaulting our state to an empty array in the constructor. +First defaulting our state to an empty array in the constructor. ```diff class MyList extends React.Component { constructor(props) { @@ -19,7 +19,7 @@ class MyList extends React.Component { + this.state = { selectedKeys: [] }; } - + render() { return ( ); @@ -35,7 +35,7 @@ class MyList extends React.Component { this.state = { collapsedKeys: [] }; + this.handleItemSelection = this.handleItemSelection.bind(this) } - + + handleItemSelection(event, metaData) { + + } @@ -120,7 +120,7 @@ Finally we need to check if the item is selected. As we support IE10 & 11, we ca ); } ``` -Then we can implement a method to loop through our data and create the list item with our methods and call it from our render method. Making special note to assign the aria role of `"listbox"` and a string to `aria-label` for the list, as it is required for accessibility with selectable list options. In addition, we need to assign the aria role for multiple selections, `aria-multiselectable`. +Then we can implement a method to loop through our data and create the list item with our methods and call it from our render method. Making special note to assign the prop `ariaSelectionStyle` with the value `multi-select` for the list, as it is required for accessibility with selectable list options. ```diff class MyList extends React.Component { constructor(props) { @@ -138,12 +138,7 @@ class MyList extends React.Component { render() { return ( -+ ++ + {this.createListItems(mockData)} + + ); diff --git a/packages/terra-core-docs/src/terra-dev-site/doc/list/guides.7/SingleSelectList.1.doc.mdx b/packages/terra-core-docs/src/terra-dev-site/doc/list/guides.7/SingleSelectList.1.doc.mdx index 0b7b5656719..ab0b4a851f3 100644 --- a/packages/terra-core-docs/src/terra-dev-site/doc/list/guides.7/SingleSelectList.1.doc.mdx +++ b/packages/terra-core-docs/src/terra-dev-site/doc/list/guides.7/SingleSelectList.1.doc.mdx @@ -11,7 +11,7 @@ The terra-list implementation requires controlled state if selections are requir ## State Management The state of selection needs to be managed for the list in a HOC. In this example we are going to be a unique key, but the type of state used is open to the implementor of the HOC. - First defaulting our state to a null value in the constructor. + First defaulting our state to a null value in the constructor. ```diff class MyList extends React.Component { constructor(props) { @@ -19,7 +19,7 @@ class MyList extends React.Component { + this.state = { selectedKey: null }; } - + render() { return ( ); @@ -35,7 +35,7 @@ class MyList extends React.Component { this.state = { selectedKey: null }; + this.handleItemSelection = this.handleItemSelection.bind(this) } - + + handleItemSelection(event, metaData) { + + } @@ -123,11 +123,11 @@ Finally we need to check if the item matches the selectedKey in state to set `is ); } ``` -Then we can implement a method to loop through our data and create the list item with our methods and call it from our render method. Making special note to assign the aria role of `"listbox"` and a string to `aria-label` for the list, as it is required for accessibility with selectable list options. +Then we can implement a method to loop through our data and create the list item with our methods and call it from our render method. Making special note to assign the prop `ariaSelectionStyle` with the value `single-select` for the list, as it is required for accessibility with selectable list options. ```diff render() { return ( -+ ++ + {data.map(childItem => this.createListItem(mockData))} + ); diff --git a/packages/terra-core-docs/src/terra-dev-site/doc/list/guides/MultiSelectList.jsx b/packages/terra-core-docs/src/terra-dev-site/doc/list/guides/MultiSelectList.jsx index cfec42d6295..f9fcf800dd4 100644 --- a/packages/terra-core-docs/src/terra-dev-site/doc/list/guides/MultiSelectList.jsx +++ b/packages/terra-core-docs/src/terra-dev-site/doc/list/guides/MultiSelectList.jsx @@ -42,12 +42,7 @@ class MutliSelectList extends React.Component { render() { return ( - + {this.createListItems(mockData)} ); diff --git a/packages/terra-core-docs/src/terra-dev-site/doc/list/guides/SingleSelectList.jsx b/packages/terra-core-docs/src/terra-dev-site/doc/list/guides/SingleSelectList.jsx index 715a219642b..cb758eb9814 100644 --- a/packages/terra-core-docs/src/terra-dev-site/doc/list/guides/SingleSelectList.jsx +++ b/packages/terra-core-docs/src/terra-dev-site/doc/list/guides/SingleSelectList.jsx @@ -42,7 +42,7 @@ class SingleSelectList extends React.Component { render() { return ( - + {this.createListItems(mockData)} ); diff --git a/packages/terra-core-docs/src/terra-dev-site/test/list/MultiSelectList.test.jsx b/packages/terra-core-docs/src/terra-dev-site/test/list/MultiSelectList.test.jsx new file mode 100644 index 00000000000..eff1efa5d53 --- /dev/null +++ b/packages/terra-core-docs/src/terra-dev-site/test/list/MultiSelectList.test.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import List, { Item, Utils } from 'terra-list/lib/index'; +import { Placeholder } from '@cerner/terra-docs'; + +const mockData = [ + { + title: 'Item 0', + key: 'unique-0', + }, + { + title: 'Item 1', + key: 'unique-1', + }, + { + title: 'Item 2', + key: 'unique-2', + }, + { + title: 'Item 3', + key: 'unique-3', + }, + { + title: 'Item 4', + key: 'unique-4', + }, +]; + +const maxSectionCount = 3; + +class MutliSelectList extends React.Component { + constructor(props) { + super(props); + this.createListItem = this.createListItem.bind(this); + this.handleItemSelection = this.handleItemSelection.bind(this); + this.state = { selectedKeys: [] }; + } + + handleItemSelection(event, metaData) { + event.preventDefault(); + this.setState(state => ({ selectedKeys: Utils.updatedMultiSelectedKeys(state.selectedKeys, metaData.key) })); + } + + createListItem(itemData) { + return ( + = 0} + metaData={{ key: itemData.key }} + onSelect={this.handleItemSelection} + > + + + ); + } + + createListItems(data) { + return data.map(childItem => this.createListItem(childItem)); + } + + render() { + return ( + + {this.createListItems(mockData)} + + ); + } +} + +export default MutliSelectList; diff --git a/packages/terra-core-docs/src/terra-dev-site/test/list/SingleSelectList.test.jsx b/packages/terra-core-docs/src/terra-dev-site/test/list/SingleSelectList.test.jsx new file mode 100644 index 00000000000..7540ab106dc --- /dev/null +++ b/packages/terra-core-docs/src/terra-dev-site/test/list/SingleSelectList.test.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import List, { Item } from 'terra-list/lib/index'; +import { Placeholder } from '@cerner/terra-docs'; + +const mockData = [ + { + title: 'Item 0', + key: 'unique-0', + }, + { + title: 'Item 1', + key: 'unique-1', + }, + { + title: 'Item 2', + key: 'unique-2', + }, + { + title: 'Item 3', + key: 'unique-3', + }, + { + title: 'Item 4', + key: 'unique-4', + }, +]; + +class SingleSelectList extends React.Component { + constructor(props) { + super(props); + this.createListItem = this.createListItem.bind(this); + this.handleItemSelection = this.handleItemSelection.bind(this); + this.state = { selectedKey: null }; + } + + handleItemSelection(event, metaData) { + event.preventDefault(); + if (this.state.selectedKey !== metaData.key) { + this.setState({ selectedKey: metaData.key }); + } + } + + createListItem(itemData) { + return ( + + + + ); + } + + createListItems(data) { + return data.map(childItem => this.createListItem(childItem)); + } + + render() { + return ( + + {this.createListItems(mockData)} + + ); + } +} + +export default SingleSelectList; diff --git a/packages/terra-list/CHANGELOG.md b/packages/terra-list/CHANGELOG.md index 33ad9ac0bb9..f5b038f69ae 100644 --- a/packages/terra-list/CHANGELOG.md +++ b/packages/terra-list/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +* Changed + * Added a `ariaSelectionStyle` prop that provides accessibility support for single and multi select lists + ## 4.52.0 - (December 7, 2022) * Changed diff --git a/packages/terra-list/package.json b/packages/terra-list/package.json index 514dcfdc49d..f6c0b0e594f 100644 --- a/packages/terra-list/package.json +++ b/packages/terra-list/package.json @@ -30,6 +30,7 @@ "classnames": "^2.2.5", "keycode-js": "^3.1.0", "prop-types": "^15.5.8", + "react-intl": "^2.9.0", "terra-icon": "^3.49.0", "terra-mixins": "^1.40.0", "terra-theme-context": "^1.0.0" diff --git a/packages/terra-list/src/List.jsx b/packages/terra-list/src/List.jsx index a72b1a13733..86ebb0b5870 100644 --- a/packages/terra-list/src/List.jsx +++ b/packages/terra-list/src/List.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import classNamesBind from 'classnames/bind'; +import { injectIntl } from 'react-intl'; import ThemeContext from 'terra-theme-context'; import styles from './List.module.scss'; @@ -46,6 +47,11 @@ const propTypes = { * The children list items passed to the component. */ children: PropTypes.node, + /** + * @private + * The intl object to be injected for translations. + */ + intl: PropTypes.shape({ formatMessage: PropTypes.func }), /** * Whether or not the list's child items should have a border color applied. * One of `'none'`, `'standard'`, `'bottom-only'`. @@ -61,9 +67,15 @@ const propTypes = { */ refCallback: PropTypes.func, /** - * Accessibility role of the list, defaults to 'none'. If creating a list with selectable items, pass 'listbox'. + * Accessibility role of the list, defaults to 'none'. */ role: PropTypes.string, + /** + * Sets the role to `'listbox'` and provides an aria-description of whether its a single or multi-select list. + * For multi-select lists, it sets aria-multiselectable to true. + * One of `'none'`, `'single-select'`, `'multi-select'`. + */ + ariaSelectionStyle: PropTypes.oneOf(['none', 'single-select', 'multi-select']), }; const defaultProps = { @@ -71,6 +83,7 @@ const defaultProps = { dividerStyle: 'none', paddingStyle: 'none', role: 'none', + ariaSelectionStyle: 'none', }; const List = ({ @@ -78,10 +91,12 @@ const List = ({ ariaDescription, ariaDetails, children, + intl, dividerStyle, paddingStyle, refCallback, role, + ariaSelectionStyle, ...customProps }) => { const theme = React.useContext(ThemeContext); @@ -103,6 +118,17 @@ const List = ({ attrSpread.role = role; } + if (ariaSelectionStyle === 'single-select') { + attrSpread.role = 'listbox'; + attrSpread['aria-label'] = intl.formatMessage({ id: 'Terra.list.singleSelect' }); + } + + if (ariaSelectionStyle === 'multi-select') { + attrSpread.role = 'listbox'; + attrSpread['aria-multiselectable'] = true; + attrSpread['aria-label'] = intl.formatMessage({ id: 'Terra.list.multiSelect' }); + } + return (
    { const item4 = ; const item5 = ; const items = [item1, item2, item3, item4, item5]; - const shallowComponent = shallow({items}); + const shallowComponent = shallowWithIntl({items}).dive(); expect(shallowComponent).toMatchSnapshot(); }); it('should render with no items', () => { - const shallowComponent = shallow(); + const shallowComponent = shallowWithIntl().dive(); expect(shallowComponent).toMatchSnapshot(); }); @@ -27,7 +28,7 @@ it('should render with standard divided items', () => { const item4 = ; const item5 = ; const items = [item1, item2, item3, item4, item5]; - const shallowComponent = shallow({items}); + const shallowComponent = shallowWithIntl({items}).dive(); expect(shallowComponent).toMatchSnapshot(); }); @@ -38,7 +39,7 @@ it('should render with bottom only divided items', () => { const item4 = ; const item5 = ; const items = [item1, item2, item3, item4, item5]; - const shallowComponent = shallow({items}); + const shallowComponent = shallowWithIntl({items}).dive(); expect(shallowComponent).toMatchSnapshot(); }); @@ -49,7 +50,7 @@ it('should render with standard padded items', () => { const item4 = ; const item5 = ; const items = [item1, item2, item3, item4, item5]; - const shallowComponent = shallow({items}); + const shallowComponent = shallowWithIntl({items}).dive(); expect(shallowComponent).toMatchSnapshot(); }); @@ -60,7 +61,52 @@ it('should render with thin padded items', () => { const item4 = ; const item5 = ; const items = [item1, item2, item3, item4, item5]; - const shallowComponent = shallow({items}); + const shallowComponent = shallowWithIntl({items}).dive(); + expect(shallowComponent).toMatchSnapshot(); +}); + +it('should render with ariaDescribedBy', () => { + const item1 = ; + const item2 = ; + const item3 = ; + const item4 = ; + const item5 = ; + const items = [item1, item2, item3, item4, item5]; + const shallowComponent = shallowWithIntl( +
    +

    Navigate this list using the arrow keys.

    + {items} +
    , + ); + expect(shallowComponent).toMatchSnapshot(); +}); + +it('should render with ariaDescription', () => { + const item1 = ; + const item2 = ; + const item3 = ; + const item4 = ; + const item5 = ; + const items = [item1, item2, item3, item4, item5]; + const shallowComponent = shallowWithIntl( + {items}, + ).dive(); + expect(shallowComponent).toMatchSnapshot(); +}); + +it('should render with ariaDetails', () => { + const item1 = ; + const item2 = ; + const item3 = ; + const item4 = ; + const item5 = ; + const items = [item1, item2, item3, item4, item5]; + const shallowComponent = shallowWithIntl( +
    +

    Here is some more information about this list.

    + {items} +
    , + ); expect(shallowComponent).toMatchSnapshot(); }); @@ -71,7 +117,7 @@ it('should render with ariaDescribedBy', () => { const item4 = ; const item5 = ; const items = [item1, item2, item3, item4, item5]; - const shallowComponent = shallow( + const shallowComponent = shallowWithIntl(

    Navigate this list using the arrow keys.

    {items} @@ -87,7 +133,7 @@ it('should render with ariaDescription', () => { const item4 = ; const item5 = ; const items = [item1, item2, item3, item4, item5]; - const shallowComponent = shallow( + const shallowComponent = shallowWithIntl( {items}, ); expect(shallowComponent).toMatchSnapshot(); @@ -100,7 +146,7 @@ it('should render with ariaDetails', () => { const item4 = ; const item5 = ; const items = [item1, item2, item3, item4, item5]; - const shallowComponent = shallow( + const shallowComponent = shallowWithIntl(

    Here is some more information about this list.

    {items} @@ -109,8 +155,34 @@ it('should render with ariaDetails', () => { expect(shallowComponent).toMatchSnapshot(); }); +it('should render with single select aria attributes with ariaSelectionStyle single-select', () => { + const item1 = ; + const item2 = ; + const item3 = ; + const item4 = ; + const item5 = ; + const items = [item1, item2, item3, item4, item5]; + const shallowComponent = shallowWithIntl( + {items}, + ).dive(); + expect(shallowComponent).toMatchSnapshot(); +}); + +it('should render with mutli select aria attributes with ariaSelectionStyle mutli-select', () => { + const item1 = ; + const item2 = ; + const item3 = ; + const item4 = ; + const item5 = ; + const items = [item1, item2, item3, item4, item5]; + const shallowComponent = shallowWithIntl( + {items}, + ).dive(); + expect(shallowComponent).toMatchSnapshot(); +}); + it('correctly applies the theme context className', () => { - const wrapper = mount( + const wrapper = mountWithIntl( , diff --git a/packages/terra-list/tests/jest/__snapshots__/List.test.jsx.snap b/packages/terra-list/tests/jest/__snapshots__/List.test.jsx.snap index 335a06b4641..cbfe6677828 100644 --- a/packages/terra-list/tests/jest/__snapshots__/List.test.jsx.snap +++ b/packages/terra-list/tests/jest/__snapshots__/List.test.jsx.snap @@ -2,37 +2,191 @@ exports[`correctly applies the theme context className 1`] = ` - -
      - + + +
        + + `; exports[`should render with ariaDescribedBy 1`] = ` -
        +
        +

        + Navigate this list using the arrow keys. +

        + + + + + + + +
        +`; + +exports[`should render with ariaDescribedBy 2`] = ` +

        Navigate this list using the arrow keys.

        - - +
        `; @@ -107,18 +261,111 @@ exports[`should render with ariaDescription 1`] = `
      `; +exports[`should render with ariaDescription 2`] = ` + + + + + + + +`; + exports[`should render with ariaDetails 1`] = ` -
      +

      Here is some more information about this list.

      - - + +
      +`; + +exports[`should render with ariaDetails 2`] = ` +
      +

      + Here is some more information about this list. +

      + + + + + + +
      `; @@ -230,6 +549,46 @@ exports[`should render with items 1`] = `
    `; +exports[`should render with mutli select aria attributes with ariaSelectionStyle mutli-select 1`] = ` +
      + + + + + +
    +`; + exports[`should render with no items 1`] = `
      `; +exports[`should render with single select aria attributes with ariaSelectionStyle single-select 1`] = ` +
        + + + + + +
      +`; + exports[`should render with standard divided items 1`] = `