Skip to content

Commit 572d56f

Browse files
committed
Problem
Aria-live of TreeSelect option was always updated with the value of the option. In some cases there is some sort of ID in the value field (not relevant for user), and the textual representation in label. In those cases, users with screenreaders are confused, as they hear only some ID-numbers instead of option labels. This is a huge problem for accessibility. Solution Allow developer to decide whether to use option label in aria-live area. There is a new prop ´bool labelInAriaLive (default=false)´ , that the developer can use if he/she wishes. This is a huge enabler for improving accessibility. Notes Also testcase was added and documentation was updated in this commit. This commit will not break any existing usage of treeSelect component, as the default for new prop is false. If ´labelInAriaLive´ is false, everything will work as expected.
1 parent 50509e7 commit 572d56f

File tree

5 files changed

+68
-26
lines changed

5 files changed

+68
-26
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ online example: https://tree-select-react-component.vercel.app/
7171
|defaultValue | initial selected treeNode(s) | same as value type | - |
7272
|value | current selected treeNode(s). | normal: String/Array<String>. labelInValue: {value:String,label:React.Node}/Array<{value,label}>. treeCheckStrictly(halfChecked default false): {value:String,label:React.Node, halfChecked}/Array<{value,label,halfChecked}>. | - |
7373
|labelInValue| whether to embed label in value, see above value type | Bool | false |
74+
|labelInAriaLive| whether to use option label instead of value for screenreader | Bool | false |
7475
|onChange | called when select treeNode or input value change | function(value, label(null), extra) | - |
7576
|onSelect | called when select treeNode | function(value, node, extra) | - |
7677
|onSearch | called when input changed | function | - |

src/OptionList.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import * as React from 'react';
2-
import KeyCode from 'rc-util/lib/KeyCode';
3-
import useMemo from 'rc-util/lib/hooks/useMemo';
4-
import type { RefOptionListProps } from 'rc-select/lib/OptionList';
51
import { useBaseProps } from 'rc-select';
6-
import type { TreeProps } from 'rc-tree';
2+
import type { RefOptionListProps } from 'rc-select/lib/OptionList';
73
import Tree from 'rc-tree';
4+
import type { TreeProps } from 'rc-tree';
85
import type { EventDataNode, ScrollTo } from 'rc-tree/lib/interface';
9-
import type { TreeDataNode, Key } from './interface';
6+
import useMemo from 'rc-util/lib/hooks/useMemo';
7+
import KeyCode from 'rc-util/lib/KeyCode';
8+
import * as React from 'react';
9+
import type { Key, TreeDataNode } from './interface';
1010
import LegacyContext from './LegacyContext';
1111
import TreeSelectContext from './TreeSelectContext';
1212
import { getAllKeys, isCheckDisabled } from './utils/valueUtil';
@@ -42,6 +42,7 @@ const OptionList: React.RefForwardingComponent<ReviseRefOptionListProps> = (_, r
4242
onSelect,
4343
dropdownMatchSelectWidth,
4444
treeExpandAction,
45+
labelInAriaLive,
4546
} = React.useContext(TreeSelectContext);
4647

4748
const {
@@ -211,7 +212,7 @@ const OptionList: React.RefForwardingComponent<ReviseRefOptionListProps> = (_, r
211212
<div onMouseDown={onListMouseDown}>
212213
{activeEntity && open && (
213214
<span style={HIDDEN_STYLE} aria-live="assertive">
214-
{activeEntity.node.value}
215+
{!labelInAriaLive ? activeEntity.node.value : activeEntity.node.label}
215216
</span>
216217
)}
217218

src/TreeSelect.tsx

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
1-
import * as React from 'react';
21
import { BaseSelect } from 'rc-select';
3-
import type { IconType } from 'rc-tree/lib/interface';
4-
import type { ExpandAction } from 'rc-tree/lib/Tree';
52
import type {
6-
BaseSelectRef,
7-
BaseSelectPropsWithoutPrivate,
83
BaseSelectProps,
4+
BaseSelectPropsWithoutPrivate,
5+
BaseSelectRef,
96
SelectProps,
107
} from 'rc-select';
11-
import { conductCheck } from 'rc-tree/lib/utils/conductUtil';
128
import useId from 'rc-select/lib/hooks/useId';
9+
import type { IconType } from 'rc-tree/lib/interface';
10+
import type { ExpandAction } from 'rc-tree/lib/Tree';
11+
import { conductCheck } from 'rc-tree/lib/utils/conductUtil';
1312
import useMergedState from 'rc-util/lib/hooks/useMergedState';
13+
import warning from 'rc-util/lib/warning';
14+
import * as React from 'react';
15+
import useCache from './hooks/useCache';
16+
import useCheckedKeys from './hooks/useCheckedKeys';
17+
import useDataEntities from './hooks/useDataEntities';
18+
import useFilterTreeData from './hooks/useFilterTreeData';
19+
import useRefFunc from './hooks/useRefFunc';
20+
import useTreeData from './hooks/useTreeData';
21+
import LegacyContext from './LegacyContext';
1422
import OptionList from './OptionList';
1523
import TreeNode from './TreeNode';
16-
import { formatStrategyValues, SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './utils/strategyUtil';
17-
import type { CheckedStrategy } from './utils/strategyUtil';
1824
import TreeSelectContext from './TreeSelectContext';
1925
import type { TreeSelectContextProps } from './TreeSelectContext';
20-
import LegacyContext from './LegacyContext';
21-
import useTreeData from './hooks/useTreeData';
22-
import { toArray, fillFieldNames, isNil } from './utils/valueUtil';
23-
import useCache from './hooks/useCache';
24-
import useRefFunc from './hooks/useRefFunc';
25-
import useDataEntities from './hooks/useDataEntities';
2626
import { fillAdditionalInfo, fillLegacyProps } from './utils/legacyUtil';
27-
import useCheckedKeys from './hooks/useCheckedKeys';
28-
import useFilterTreeData from './hooks/useFilterTreeData';
27+
import { formatStrategyValues, SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './utils/strategyUtil';
28+
import type { CheckedStrategy } from './utils/strategyUtil';
29+
import { fillFieldNames, isNil, toArray } from './utils/valueUtil';
2930
import warningProps from './utils/warningPropsUtil';
30-
import warning from 'rc-util/lib/warning';
3131

3232
export type OnInternalSelect = (value: RawValueType, info: { selected: boolean }) => void;
3333

@@ -140,6 +140,7 @@ export interface TreeSelectProps<
140140
treeCheckable?: boolean | React.ReactNode;
141141
treeCheckStrictly?: boolean;
142142
labelInValue?: boolean;
143+
labelInAriaLive?: boolean;
143144

144145
// >>> Data
145146
treeData?: OptionType[];
@@ -202,6 +203,7 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((props, ref)
202203
treeCheckable,
203204
treeCheckStrictly,
204205
labelInValue,
206+
labelInAriaLive,
205207

206208
// FieldNames
207209
fieldNames,
@@ -651,6 +653,7 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((props, ref)
651653
fieldNames: mergedFieldNames,
652654
onSelect: onOptionSelect,
653655
treeExpandAction,
656+
labelInAriaLive,
654657
}),
655658
[
656659
virtual,
@@ -661,6 +664,7 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((props, ref)
661664
mergedFieldNames,
662665
onOptionSelect,
663666
treeExpandAction,
667+
labelInAriaLive,
664668
],
665669
);
666670

src/TreeSelectContext.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import * as React from 'react';
21
import type { ExpandAction } from 'rc-tree/lib/Tree';
2+
import * as React from 'react';
33
import type { DefaultOptionType, InternalFieldName, OnInternalSelect } from './TreeSelect';
44

55
export interface TreeSelectContextProps {
@@ -11,6 +11,7 @@ export interface TreeSelectContextProps {
1111
fieldNames: InternalFieldName;
1212
onSelect: OnInternalSelect;
1313
treeExpandAction?: ExpandAction;
14+
labelInAriaLive?: boolean;
1415
}
1516

1617
const TreeSelectContext = React.createContext<TreeSelectContextProps>(null as any);

tests/Select.props.spec.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
/* eslint-disable no-undef, react/no-multi-comp, no-console */
2-
import React from 'react';
32
import { mount } from 'enzyme';
43
import Tree, { TreeNode } from 'rc-tree';
4+
import KeyCode from 'rc-util/lib/KeyCode';
5+
import React from 'react';
56
import TreeSelect, { SHOW_ALL, SHOW_CHILD, SHOW_PARENT, TreeNode as SelectNode } from '../src';
67

78
// Promisify timeout to let jest catch works
@@ -155,6 +156,40 @@ describe('TreeSelect.props', () => {
155156
);
156157
});
157158

159+
it('labelInAriaLive', () => {
160+
function keyDown(code) {
161+
wrapper.find('input').first().simulate('keyDown', { which: code });
162+
wrapper.update();
163+
}
164+
165+
function keyUp(code) {
166+
wrapper.find('input').first().simulate('keyUp', { which: code });
167+
wrapper.update();
168+
}
169+
170+
const wrapper = mount(
171+
<TreeSelect
172+
labelInAriaLive={true}
173+
treeDefaultExpandAll
174+
treeData={[
175+
{
176+
value: 'parent',
177+
label: 'parent-label',
178+
children: [{ value: 'child', label: 'child-label' }],
179+
},
180+
]}
181+
multiple
182+
/>,
183+
);
184+
185+
wrapper.openSelect();
186+
keyDown(KeyCode.DOWN);
187+
keyUp(KeyCode.DOWN);
188+
189+
const ariaLiveSpan = wrapper.find('[aria-live="assertive"]');
190+
expect(ariaLiveSpan.text()).toEqual('parent-label');
191+
});
192+
158193
it('set illegal value', () => {
159194
const wrapper = mount(
160195
createSelect({

0 commit comments

Comments
 (0)