Skip to content

Commit

Permalink
STCOM-977 Aria-labeling MultiSelection (#1868)
Browse files Browse the repository at this point in the history
* Storybook for multi-select with no aria label

* add ariaLabel prop and functionality

* add axe test to multiselection ariaLabel

* add aria-label test

* lint

* tweak the comment

* include storybook dep to silence lint

* additional story lint

* Update CHANGELOG.md

Co-authored-by: Ryan Berger <[email protected]>
  • Loading branch information
JohnC-80 and ryandberger authored Jul 18, 2022
1 parent 15b5f7f commit 5fcea71
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 2 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
* Add underline to Button focus indicator. Refs STCOM-1007
* Fix selection bug where pressing the enter key while no options are available will clear the selected value. fixes STCOM-1024.
* Fix TextField bug where "focused" state is retained if component is disabled while it's in focus. fixes STCOM-818.
* Provide ability to disable an Icon Button. Refs STCOM-1028
* Provide ability to disable an Icon Button. Refs STCOM-1028.
* `MultiSelection` support for `aria-label`. Refs STCOM-977.

## [10.2.0](https://github.com/folio-org/stripes-components/tree/v10.2.0) (2022-06-14)
[Full Changelog](https://github.com/folio-org/stripes-components/compare/v10.1.0...v10.2.0)
Expand Down
15 changes: 14 additions & 1 deletion lib/MultiSelection/MultiSelection.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const filterOptions = (filterText, list) => {
class MultiSelection extends React.Component {
static propTypes = {
actions: PropTypes.arrayOf(PropTypes.object),
ariaLabel: PropTypes.string,
ariaLabelledBy: PropTypes.string,
asyncFiltering: PropTypes.bool,
autoFocus: PropTypes.bool,
Expand Down Expand Up @@ -278,12 +279,13 @@ class MultiSelection extends React.Component {
render() {
const {
ariaLabelledBy: ariaLabelledByProp,
ariaLabel: ariaLabelProp,
asyncFiltering,
backspaceDeletes,
itemToString,
value,
formatter,
label,
label : labelProp,
placeholder,
maxHeight,
renderToOverlay,
Expand Down Expand Up @@ -316,6 +318,8 @@ class MultiSelection extends React.Component {
const valueDescriptionId = `multi-describe-action-${uiId}`;
const controlDescriptionId = `multi-describe-control-${uiId}`;
const controlValueStatusId = `multi-value-status-${uiId}`;
const ariaLabel = ariaLabelProp || rest['aria-label'];
const label = labelProp || ariaLabel || rest['aria-label'];

let filterResults;
if (!asyncFiltering) {
Expand Down Expand Up @@ -456,6 +460,7 @@ class MultiSelection extends React.Component {
{label &&
<Label // eslint-disable-line
{...labelProps}
className={`${ariaLabel ? 'sr-only' : ''}`}
>
{label}
</Label>
Expand All @@ -471,6 +476,14 @@ class MultiSelection extends React.Component {
<span className="sr-only" id={controlDescriptionId}>
<FormattedMessage id="stripes-components.multiSelection.controlDescription" />
</span>
{/*
Multiselection handles accessible announcement by following the label - value announcement pattern
that's common to standard controls, inputs etc. The announcement includes the label as well as
the number of selected items (if any).
This is assembled via a collection of elements used with the aria-labelledby attribute. Other labeling
attributes suche as `aria-label` are rendered into these elements and applied through the
aria-labelledby attribute.
*/}
<ControlWrapper {...wrapperProps}>
<div // eslint-disable-line
className={this.getControlClass()}
Expand Down
37 changes: 37 additions & 0 deletions lib/MultiSelection/stories/HiddenLabel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { useState } from 'react';
import MultiSelection from '../MultiSelection';
import Headline from '../../Headline';

const optionList = [
{ value: 'test0', label: 'Option 0' },
{ value: 'test1', label: 'Option 1' },
{ value: 'test2', label: 'Option 2' },
];

const HiddenLabel = () => {
const [values, setValues] = useState([]);

return (
<div>
<Headline size="large">
Multiselect with no visible label
</Headline>
<p>
The &quot;ariaLabel&quot; prop is used to apply a label that
is not visible to the user, but accessible to screenreaders.
This is preferable for &quot;required&quot; fields over
&quot;ariaLabelledby&quot; since the labeling elements will
not need to display an asterisk.
</p>
<MultiSelection
id="my-multiselect"
dataOptions={optionList}
value={values}
onChange={setValues}
ariaLabel="My aria multiselect"
/>
</div>
);
};

export default HiddenLabel;
2 changes: 2 additions & 0 deletions lib/MultiSelection/stories/MultiSelection.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { OptionSegment } from '../../Selection';
import BasicUsage from './BasicUsage';
import Required from './Required';
import CustomAriaLabelledBy from './CustomAriaLabelledBy';
import AriaLabel from './HiddenLabel';

const optionList = [
{ value: 'test0', label: 'Option 0', extra: 'nulla' },
Expand Down Expand Up @@ -153,3 +154,4 @@ DisabledState.story = {

export const _Required = () => <Required />;
export const CustomAriaLabel = () => <CustomAriaLabelledBy />;
export const HiddenLabel = () => <AriaLabel />;
49 changes: 49 additions & 0 deletions lib/MultiSelection/tests/MultiSelection-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
MultiSelectOption as OptionInteractor,
Button,
TextInput,
Label,
Keyboard,
converge,
including,
Expand Down Expand Up @@ -118,6 +119,54 @@ describe('MultiSelect', () => {
});
});

describe('supplying an ariaLabel prop', () => {
beforeEach(async () => {
await mountWithContext(
<MultiSelectionHarness
id={testId}
dataOptions={listOptions}
ariaLabel="test aria selection"
/>
);
});

it('renders the label', () => {
MultiSelectInteractor('test aria selection').has({ visible: false });
});

it('control\'s aria-labelledBy attribute is set', () => {
multiselection.has({ arialabelledBy: `${testId}-label multi-value-status-${testId}` });
});

it('renders the label with an sr-only classname', () => Label('test aria selection').has({ className: including('sr-only'), visible: false }));

it('contains no axe errors - Multiselect: ariaLabel prop', runAxeTest);
});

describe('supplying an aria-label prop', () => {
beforeEach(async () => {
await mountWithContext(
<MultiSelectionHarness
id={testId}
dataOptions={listOptions}
aria-label="test aria selection"
/>
);
});

it('renders the label', () => {
MultiSelectInteractor('test aria selection').has({ visible: false });
});

it('control\'s aria-labelledBy attribute is set', () => {
multiselection.has({ arialabelledBy: `${testId}-label multi-value-status-${testId}` });
});

it('renders the label with an sr-only classname', () => Label('test aria selection').has({ className: including('sr-only'), visible: false }));

it('contains no axe errors - Multiselect: aria-label prop', runAxeTest);
});

describe('clicking the control', () => {
beforeEach(async () => {
await multiselection.toggle();
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@folio/stripes-cli": "^2.4.0",
"@folio/stripes-testing": "^4.3.0",
"@mdx-js/loader": "^1.6.22",
"@storybook/addon-actions": "^6.3.6",
"@storybook/addon-essentials": "^6.3.6",
"@storybook/addons": "^6.3.6",
"@storybook/builder-webpack5": "^6.3.12",
Expand Down

0 comments on commit 5fcea71

Please sign in to comment.