Skip to content

Commit d341b75

Browse files
ellingemrchief
authored andcommitted
fix: Adjust aria labels to reference selections(dowjones#275)
and ensure "Remove" label is read by Screen readers
1 parent ed74a78 commit d341b75

File tree

14 files changed

+92
-26
lines changed

14 files changed

+92
-26
lines changed

__snapshots__/src/index.test.js.md

+17
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ Generated by [AVA](https://ava.li).
1616
className="dropdown"
1717
>
1818
<Trigger
19+
clientId="rdts"
1920
onTrigger={Function {}}
2021
showDropdown={true}
22+
tags={[]}
2123
texts={{}}
2224
>
2325
<Input
26+
clientId="rdts"
2427
inputRef={Function inputRef {}}
2528
onBlur={Function {}}
2629
onFocus={Function {}}
@@ -177,12 +180,15 @@ Generated by [AVA](https://ava.li).
177180
className="dropdown"
178181
>
179182
<Trigger
183+
clientId="rdts"
180184
disabled={true}
181185
onTrigger={Function {}}
182186
showDropdown={false}
187+
tags={[]}
183188
texts={{}}
184189
>
185190
<Input
191+
clientId="rdts"
186192
disabled={true}
187193
inputRef={Function inputRef {}}
188194
onBlur={Function {}}
@@ -287,21 +293,25 @@ Generated by [AVA](https://ava.li).
287293
className="dropdown radio-select"
288294
>
289295
<Trigger
296+
clientId="rdts"
290297
mode="radioSelect"
291298
onTrigger={Function {}}
292299
showDropdown={false}
300+
tags={[]}
293301
texts={{}}
294302
>
295303
<a
296304
"aria-expanded"="false"
297305
"aria-haspopup"="tree"
298306
className="dropdown-trigger arrow bottom"
307+
id="rdts_trigger"
299308
onClick={Function {}}
300309
onKeyDown={Function {}}
301310
role="button"
302311
tabIndex={0}
303312
>
304313
<Input
314+
clientId="rdts"
305315
inputRef={Function inputRef {}}
306316
mode="radioSelect"
307317
onBlur={Function {}}
@@ -426,20 +436,24 @@ Generated by [AVA](https://ava.li).
426436
className="dropdown"
427437
>
428438
<Trigger
439+
clientId="rdts"
429440
onTrigger={Function {}}
430441
showDropdown={false}
442+
tags={[]}
431443
texts={{}}
432444
>
433445
<a
434446
"aria-expanded"="false"
435447
"aria-haspopup"="tree"
436448
className="dropdown-trigger arrow bottom"
449+
id="rdts_trigger"
437450
onClick={Function {}}
438451
onKeyDown={Function {}}
439452
role="button"
440453
tabIndex={0}
441454
>
442455
<Input
456+
clientId="rdts"
443457
inputRef={Function inputRef {}}
444458
onBlur={Function {}}
445459
onFocus={Function {}}
@@ -486,11 +500,14 @@ Generated by [AVA](https://ava.li).
486500
className="dropdown"
487501
>
488502
<Trigger
503+
clientId="rdts"
489504
onTrigger={Function {}}
490505
showDropdown={true}
506+
tags={[]}
491507
texts={{}}
492508
>
493509
<Input
510+
clientId="rdts"
494511
inputRef={Function inputRef {}}
495512
onBlur={Function {}}
496513
onFocus={Function {}}

__snapshots__/src/index.test.js.snap

90 Bytes
Binary file not shown.

__snapshots__/src/tag/index.test.js.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ Generated by [AVA](https://ava.li).
99
> Snapshot 1
1010
1111
<span
12+
"aria-label"="hello"
1213
className="tag"
1314
id="abc_tag"
1415
>
1516
hello
1617
<button
1718
"aria-label"="Remove"
18-
"aria-labelledby"="abc_tag"
19+
"aria-labelledby"="abc_button abc_tag"
1920
className="tag-remove"
21+
id="abc_button"
2022
onClick={Function {}}
2123
onKeyDown={Function {}}
2224
onKeyUp={Function {}}
19 Bytes
Binary file not shown.

__snapshots__/src/trigger/index.test.js.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Generated by [AVA](https://ava.li).
1212
"aria-expanded"="false"
1313
"aria-haspopup"="tree"
1414
className="dropdown-trigger arrow bottom"
15+
id="rtds_trigger"
1516
onClick={Function {}}
1617
onKeyDown={Function {}}
1718
role="button"
15 Bytes
Binary file not shown.

docs/src/stories/Options/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ class WithOptions extends PureComponent {
7070
</select>
7171
</div>
7272
<div style={{ marginBottom: '10px' }}>
73-
<label htmlFor={showDropdown}>ShowDropdown: </label>
73+
<label htmlFor={showDropdown}>Show dropdown: </label>
7474
<select
7575
id="showDropdown"
7676
value={showDropdown}
@@ -123,6 +123,7 @@ class WithOptions extends PureComponent {
123123
disabled={disabled}
124124
readOnly={readOnly}
125125
showDropdown={showDropdown}
126+
texts={{ label: 'Demo Dropdown' }}
126127
/>
127128
</div>
128129
</div>

src/a11y/index.js

+16-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1-
export function getAriaLabel(label) {
2-
if (!label) return undefined
1+
export function getAriaLabel(label, additionalLabelledBy) {
2+
const attributes = getAriaAttributeForLabel(label)
33

4-
if (label && label[0] === '#') {
5-
/* See readme for label. When label starts with # it references ids of dom nodes instead.
6-
When used on aria-labelledby, they should be referenced without a starting hash/# */
7-
return { 'aria-labelledby': label.replace(/#/g, '') }
4+
if (additionalLabelledBy) {
5+
attributes['aria-labelledby'] = `${attributes['aria-labelledby'] || ''} ${additionalLabelledBy}`.trim()
6+
}
7+
8+
return attributes
9+
}
10+
11+
function getAriaAttributeForLabel(label) {
12+
if (!label) return {}
13+
14+
/* See readme for label. When label starts with # it references ids of dom nodes instead.
15+
When used on aria-labelledby, they should be referenced without a starting hash/# */
16+
if (label[0] === '#') {
17+
return { 'aria-labelledby': label.substring(1).replace(/ #/g, ' ') }
818
}
919
return { 'aria-label': label }
1020
}

src/index.js

+4-5
Original file line numberDiff line numberDiff line change
@@ -277,11 +277,11 @@ class DropdownTreeSelect extends Component {
277277

278278
render() {
279279
const { disabled, readOnly, mode, texts } = this.props
280-
const { showDropdown, currentFocus } = this.state
280+
const { showDropdown, currentFocus, tags } = this.state
281281

282282
const activeDescendant = currentFocus ? `${currentFocus}_li` : undefined
283283

284-
const commonProps = { disabled, readOnly, activeDescendant, texts, mode }
284+
const commonProps = { disabled, readOnly, activeDescendant, texts, mode, clientId: this.clientId }
285285

286286
return (
287287
<div
@@ -298,12 +298,12 @@ class DropdownTreeSelect extends Component {
298298
{ 'radio-select': mode === 'radioSelect' }
299299
)}
300300
>
301-
<Trigger onTrigger={this.onTrigger} showDropdown={showDropdown} {...commonProps}>
301+
<Trigger onTrigger={this.onTrigger} showDropdown={showDropdown} {...commonProps} tags={tags}>
302302
<Input
303303
inputRef={el => {
304304
this.searchInput = el
305305
}}
306-
tags={this.state.tags}
306+
tags={tags}
307307
onInputChange={this.onInputChange}
308308
onFocus={this.onInputFocus}
309309
onBlur={this.onInputBlur}
@@ -327,7 +327,6 @@ class DropdownTreeSelect extends Component {
327327
onNodeToggle={this.onNodeToggle}
328328
mode={mode}
329329
showPartiallySelected={this.props.showPartiallySelected}
330-
clientId={this.clientId}
331330
{...commonProps}
332331
/>
333332
)}

src/index.test.js

+16
Original file line numberDiff line numberDiff line change
@@ -290,3 +290,19 @@ test('adds aria-label when having label on search input', t => {
290290
t.deepEqual(wrapper.find('.search').prop('aria-labelledby'), undefined)
291291
t.deepEqual(wrapper.find('.search').prop('aria-label'), 'hello world')
292292
})
293+
294+
test('appends selected tags to aria-labelledby with provided aria-labelledby', t => {
295+
const { tree } = t.context
296+
tree[0].checked = true
297+
const wrapper = mount(<DropdownTreeSelect id="rdts" data={tree} texts={{ label: '#hello #world' }} />)
298+
t.deepEqual(wrapper.find('.dropdown-trigger').prop('aria-labelledby'), 'hello world rdts-0_tag')
299+
t.deepEqual(wrapper.find('.dropdown-trigger').prop('aria-label'), undefined)
300+
})
301+
302+
test('appends selected tags to aria-labelledby with text label', t => {
303+
const { tree } = t.context
304+
tree[0].checked = true
305+
const wrapper = mount(<DropdownTreeSelect id="rdts" data={tree} texts={{ label: 'hello world' }} />)
306+
t.deepEqual(wrapper.find('.dropdown-trigger').prop('aria-labelledby'), 'rdts_trigger rdts-0_tag')
307+
t.deepEqual(wrapper.find('.dropdown-trigger').prop('aria-label'), 'hello world')
308+
})

src/tag/index.js

+10-9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import styles from './index.css'
66

77
const cx = cn.bind(styles)
88

9+
export const getTagId = id => `${id}_tag`
10+
911
class Tag extends PureComponent {
1012
static propTypes = {
1113
id: PropTypes.string.isRequired,
@@ -40,24 +42,23 @@ class Tag extends PureComponent {
4042
render() {
4143
const { id, label, labelRemove = 'Remove', readOnly, disabled } = this.props
4244

43-
const tagId = `${id}_tag`
45+
const tagId = getTagId(id)
46+
const buttonId = `${id}_button`
4447
const className = cx('tag-remove', { readOnly }, { disabled })
4548
const isDisabled = readOnly || disabled
46-
const onClick = !isDisabled ? this.handleClick : undefined
47-
const onKeyDown = !isDisabled ? this.onKeyDown : undefined
48-
const onKeyUp = !isDisabled ? this.onKeyUp : undefined
4949

5050
return (
51-
<span className={cx('tag')} id={tagId}>
51+
<span className={cx('tag')} id={tagId} aria-label={label}>
5252
{label}
5353
<button
54-
onClick={onClick}
55-
onKeyDown={onKeyDown}
56-
onKeyUp={onKeyUp}
54+
id={buttonId}
55+
onClick={!isDisabled ? this.handleClick : undefined}
56+
onKeyDown={!isDisabled ? this.onKeyDown : undefined}
57+
onKeyUp={!isDisabled ? this.onKeyUp : undefined}
5758
className={className}
5859
type="button"
5960
aria-label={labelRemove}
60-
aria-labelledby={tagId}
61+
aria-labelledby={`${buttonId} ${tagId}`}
6162
aria-disabled={isDisabled}
6263
>
6364
x

src/tree-manager/keyboardNavigation.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import nodeVisitor from './nodeVisitor'
2+
import { getTagId } from '../tag'
23

34
const Keys = {
45
Up: 'ArrowUp',
@@ -150,7 +151,7 @@ const getNextFocusAfterTagDelete = (deletedId, prevTags, tags, fallback) => {
150151

151152
index = tags.length > index ? index : tags.length - 1
152153
const newFocusId = tags[index]._id
153-
const focusNode = document.getElementById(`${newFocusId}_tag`)
154+
const focusNode = document.getElementById(getTagId(newFocusId))
154155
if (focusNode) {
155156
return focusNode.firstElementChild || fallback
156157
}

src/trigger/index.js

+20-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
33
import cn from 'classnames/bind'
44

55
import { getAriaLabel } from '../a11y'
6+
import { getTagId } from '../tag'
67

78
import styles from '../index.css'
89

@@ -16,17 +17,34 @@ class Trigger extends PureComponent {
1617
showDropdown: PropTypes.bool,
1718
mode: PropTypes.oneOf(['multiSelect', 'simpleSelect', 'radioSelect', 'hierarchical']),
1819
texts: PropTypes.object,
20+
clientId: PropTypes.string,
21+
tags: PropTypes.array,
1922
}
2023

2124
getAriaAttributes = () => {
22-
const { mode, texts = {}, showDropdown } = this.props
25+
const { mode, texts = {}, showDropdown, clientId, tags } = this.props
26+
27+
const triggerId = `${clientId}_trigger`
28+
const labelledBy = []
29+
let labelAttributes = getAriaLabel(texts.label)
30+
if (tags && tags.length) {
31+
if (labelAttributes['aria-label']) {
32+
// Adds reference to self when having aria-label
33+
labelledBy.push(triggerId)
34+
}
35+
tags.forEach(t => {
36+
labelledBy.push(getTagId(t._id))
37+
})
38+
labelAttributes = getAriaLabel(texts.label, labelledBy.join(' '))
39+
}
2340

2441
const attributes = {
42+
id: triggerId,
2543
role: 'button',
2644
tabIndex: 0,
2745
'aria-haspopup': mode === 'simpleSelect' ? 'listbox' : 'tree',
2846
'aria-expanded': showDropdown ? 'true' : 'false',
29-
...getAriaLabel(texts.label),
47+
...labelAttributes,
3048
}
3149

3250
return attributes

src/trigger/index.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ import toJson from 'enzyme-to-json'
66
import Trigger from './index'
77

88
test('Trigger component', t => {
9-
const input = toJson(shallow(<Trigger />))
9+
const input = toJson(shallow(<Trigger clientId={'rtds'} />))
1010
t.snapshot(input)
1111
})

0 commit comments

Comments
 (0)