Skip to content

Commit

Permalink
[terra-list] Add programmatic support for single/multi-select (cerner…
Browse files Browse the repository at this point in the history
…#3703)

* Added prop for single/multi select a11y support

* Updated ariaSelectionStyle logic

* Updated terra-list single and multi select guides

* Updated jest snapshots

* Revert snapshot

* Added intl for helper text

* Moved translations

* Updated tests

* Updated react-intl version

* Update react-intl version

* Update tests and react-intl dependency version

* Added tests for ariaSelectionStyle

* Added translations

Co-authored-by: Parkeenvincha,Art <[email protected]>
  • Loading branch information
artpark and Parkeenvincha,Art authored Dec 16, 2022
1 parent 7698853 commit a7f16b6
Show file tree
Hide file tree
Showing 20 changed files with 725 additions and 57 deletions.
6 changes: 6 additions & 0 deletions packages/terra-core-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ 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) {
super(props);

+ this.state = { selectedKeys: [] };
}

render() {
return (
);
Expand All @@ -35,7 +35,7 @@ class MyList extends React.Component {
this.state = { collapsedKeys: [] };
+ this.handleItemSelection = this.handleItemSelection.bind(this)
}

+ handleItemSelection(event, metaData) {
+
+ }
Expand Down Expand Up @@ -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) {
Expand All @@ -138,12 +138,7 @@ class MyList extends React.Component {

render() {
return (
+ <List
+ dividerStyle="standard"
+ role="listbox"
+ aria-multiselectable
+ aria-label="MultiSelectList-label"
+ >
+ <List ariaSelectionStyle="multi-select">
+ {this.createListItems(mockData)}
+ </List>
+ );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ 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) {
super(props);

+ this.state = { selectedKey: null };
}

render() {
return (
);
Expand All @@ -35,7 +35,7 @@ class MyList extends React.Component {
this.state = { selectedKey: null };
+ this.handleItemSelection = this.handleItemSelection.bind(this)
}

+ handleItemSelection(event, metaData) {
+
+ }
Expand Down Expand Up @@ -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 (
+ <List role="listbox" aria-label="SingleSelectList-label">
+ <List ariaSelectionStyle="single-select">
+ {data.map(childItem => this.createListItem(mockData))}
+ </List>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,7 @@ class MutliSelectList extends React.Component {

render() {
return (
<List
dividerStyle="standard"
role="listbox"
aria-multiselectable
aria-label="MultiSelectList-label"
>
<List ariaSelectionStyle="multi-select">
{this.createListItems(mockData)}
</List>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class SingleSelectList extends React.Component {

render() {
return (
<List dividerStyle="standard" role="listbox" aria-label="SingleSelectList-label">
<List dividerStyle="standard" ariaSelectionStyle="single-select">
{this.createListItems(mockData)}
</List>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Item
key={itemData.key}
isSelectable={Utils.shouldBeMultiSelectable(maxSectionCount, this.state.selectedKeys, itemData.key)}
isSelected={this.state.selectedKeys.indexOf(itemData.key) >= 0}
metaData={{ key: itemData.key }}
onSelect={this.handleItemSelection}
>
<Placeholder title={itemData.title} />
</Item>
);
}

createListItems(data) {
return data.map(childItem => this.createListItem(childItem));
}

render() {
return (
<List dividerStyle="standard" ariaSelectionStyle="multi-select">
{this.createListItems(mockData)}
</List>
);
}
}

export default MutliSelectList;
Original file line number Diff line number Diff line change
@@ -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 (
<Item
key={itemData.key}
isSelectable
isSelected={this.state.selectedKey === itemData.key}
metaData={{ key: itemData.key }}
onSelect={this.handleItemSelection}
>
<Placeholder title={itemData.title} />
</Item>
);
}

createListItems(data) {
return data.map(childItem => this.createListItem(childItem));
}

render() {
return (
<List dividerStyle="standard" ariaSelectionStyle="single-select">
{this.createListItems(mockData)}
</List>
);
}
}

export default SingleSelectList;
3 changes: 3 additions & 0 deletions packages/terra-list/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/terra-list/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
30 changes: 28 additions & 2 deletions packages/terra-list/src/List.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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'`.
Expand All @@ -61,27 +67,36 @@ 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 = {
children: [],
dividerStyle: 'none',
paddingStyle: 'none',
role: 'none',
ariaSelectionStyle: 'none',
};

const List = ({
ariaDescribedBy,
ariaDescription,
ariaDetails,
children,
intl,
dividerStyle,
paddingStyle,
refCallback,
role,
ariaSelectionStyle,
...customProps
}) => {
const theme = React.useContext(ThemeContext);
Expand All @@ -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 (
<ul
{...customProps}
Expand All @@ -121,4 +147,4 @@ const List = ({
List.propTypes = propTypes;
List.defaultProps = defaultProps;

export default List;
export default injectIntl(List);
Loading

0 comments on commit a7f16b6

Please sign in to comment.