Skip to content

Commit 1f2f8aa

Browse files
authored
Improve searchwithin labeling structure (#2838)
1 parent 6ff8800 commit 1f2f8aa

File tree

4 files changed

+32
-23
lines changed

4 files changed

+32
-23
lines changed

packages/@react-aria/select/src/useSelect.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import {AriaButtonProps} from '@react-types/button';
1414
import {AriaListBoxOptions} from '@react-aria/listbox';
1515
import {AriaSelectProps} from '@react-types/select';
16-
import {chain, filterDOMProps, mergeProps} from '@react-aria/utils';
16+
import {chain, filterDOMProps, mergeProps, useId} from '@react-aria/utils';
1717
import {FocusEvent, HTMLAttributes, RefObject, useMemo} from 'react';
1818
import {KeyboardDelegate} from '@react-types/shared';
1919
import {ListKeyboardDelegate, useTypeSelect} from '@react-aria/selection';
@@ -121,8 +121,7 @@ export function useSelect<T>(props: AriaSelectOptions<T>, state: SelectState<T>,
121121
let domProps = filterDOMProps(props, {labelable: true});
122122
let triggerProps = mergeProps(typeSelectProps, menuTriggerProps, fieldProps);
123123

124-
// used to make predictable id's based on the trigger which is already generated, this aids us in testing
125-
let valueId = `${triggerProps.id}-value`;
124+
let valueId = useId();
126125

127126
return {
128127
labelProps: {

packages/@react-spectrum/searchwithin/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@
3333
"dependencies": {
3434
"@babel/runtime": "^7.6.2",
3535
"@react-aria/i18n": "^3.3.2",
36-
"@react-spectrum/form": "^3.2.3",
3736
"@react-aria/label": "^3.2.1",
3837
"@react-aria/utils": "^3.10.0",
38+
"@react-aria/visually-hidden": "^3.2.3",
39+
"@react-spectrum/form": "^3.2.3",
3940
"@react-spectrum/label": "^3.4.1",
4041
"@react-spectrum/utils": "^3.6.3",
4142
"@react-types/searchwithin": "3.0.0-alpha.0",

packages/@react-spectrum/searchwithin/src/SearchWithin.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {useLabel} from '@react-aria/label';
2424
import {useLayoutEffect} from '@react-aria/utils';
2525
import {useMessageFormatter} from '@react-aria/i18n';
2626
import {useProvider, useProviderProps} from '@react-spectrum/provider';
27+
import {VisuallyHidden} from '@react-aria/visually-hidden';
2728

2829
function SearchWithin(props: SpectrumSearchWithinProps, ref: FocusableRef<HTMLElement>) {
2930
props = useProviderProps(props);
@@ -87,13 +88,14 @@ function SearchWithin(props: SpectrumSearchWithinProps, ref: FocusableRef<HTMLEl
8788

8889
let searchFieldClassName = classNames(styles, 'spectrum-SearchWithin-searchfield');
8990
let pickerClassName = classNames(styles, 'spectrum-SearchWithin-picker');
91+
let visuallyHiddenId = useId();
9092

9193
let slots = {
9294
searchfield: {
9395
...defaultSlotValues,
9496
UNSAFE_className: searchFieldClassName,
9597
// Apply aria-labelledby of group or the group id to searchfield. No need to pass the group id (we want a new one) and aria-label (aria-labelledby will suffice)
96-
'aria-labelledby': `${labelledBy} ${pickerId} ${pickerId}-value`,
98+
'aria-labelledby': `${labelledBy} ${visuallyHiddenId} ${pickerId}`,
9799
// When label is provided, input should have id referenced by htmlFor of label, instead of group
98100
id: label && fieldProps.id
99101
},
@@ -103,8 +105,7 @@ function SearchWithin(props: SpectrumSearchWithinProps, ref: FocusableRef<HTMLEl
103105
UNSAFE_className: pickerClassName,
104106
menuWidth,
105107
align: 'end',
106-
'aria-label': formatMessage('searchWithin'),
107-
'aria-labelledby': `${labelledBy} ${pickerId}`
108+
'aria-labelledby': `${labelledBy} ${visuallyHiddenId}`
108109
}
109110
};
110111

@@ -127,6 +128,7 @@ function SearchWithin(props: SpectrumSearchWithinProps, ref: FocusableRef<HTMLEl
127128
role="group"
128129
className={classNames(styles, 'spectrum-SearchWithin', styleProps.className)}
129130
ref={groupRef}>
131+
<VisuallyHidden id={visuallyHiddenId}>{formatMessage('searchWithin')}</VisuallyHidden>
130132
<SlotProvider slots={slots}>
131133
{children}
132134
</SlotProvider>

packages/@react-spectrum/searchwithin/test/SearchWithin.test.js

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -94,17 +94,18 @@ describe('SearchWithin', function () {
9494
});
9595

9696
it('searchfield and picker are labelled correctly', function () {
97-
let {getByRole, getAllByText} = renderSearchWithin();
97+
let {getByRole, getAllByText, getByText} = renderSearchWithin();
9898

9999
let searchfield = getByRole('searchbox');
100100
let picker = getByRole('button');
101101
let group = getByRole('group');
102+
let hiddenLabel = getByText('Search within');
102103
triggerPress(picker);
103104

104105
let listbox = getByRole('listbox');
105106
let label = getAllByText('Test')[0];
106-
expect(listbox).toHaveAttribute('aria-labelledby', `${label.id} ${picker.id}`);
107-
expect(searchfield).toHaveAttribute('aria-labelledby', `${label.id} ${picker.id} ${picker.childNodes[0].id}`);
107+
expect(listbox).toHaveAttribute('aria-labelledby', `${label.id} ${hiddenLabel.id}`);
108+
expect(searchfield).toHaveAttribute('aria-labelledby', `${label.id} ${hiddenLabel.id} ${picker.id}`);
108109
expect(group).toHaveAttribute('aria-labelledby', label.id);
109110
});
110111

@@ -135,7 +136,7 @@ describe('SearchWithin', function () {
135136
});
136137

137138
it('slot props override props provided to children', function () {
138-
let {getByRole, getAllByText} = renderSearchWithin(
139+
let {getByRole, getAllByText, getByText} = renderSearchWithin(
139140
{isDisabled: true, isRequired: false, label: 'Test1'},
140141
{isDisabled: false, isRequired: true, label: 'Test2', isQuiet: true},
141142
{isDisabled: false, isRequired: true, label: 'Test3', isQuiet: true}
@@ -144,6 +145,7 @@ describe('SearchWithin', function () {
144145
let searchfield = getByRole('searchbox');
145146
let picker = getByRole('button');
146147
let group = getByRole('group');
148+
let hiddenLabel = getByText('Search within');
147149
triggerPress(picker);
148150
let label = getAllByText('Test1')[0];
149151

@@ -152,7 +154,7 @@ describe('SearchWithin', function () {
152154

153155
expect(searchfield).not.toHaveAttribute('aria-required');
154156

155-
expect(searchfield).toHaveAttribute('aria-labelledby', `${label.id} ${picker.id} ${picker.childNodes[0].id}`);
157+
expect(searchfield).toHaveAttribute('aria-labelledby', `${label.id} ${hiddenLabel.id} ${picker.id}`);
156158
expect(group).toHaveAttribute('aria-labelledby', label.id);
157159

158160
expect(searchfield.classList.contains('is-quiet')).toBeFalsy();
@@ -162,17 +164,18 @@ describe('SearchWithin', function () {
162164

163165
describe('SearchWithin labeling', function () {
164166
it('no label - default', function () {
165-
let {getByRole} = renderSearchWithin({label: undefined});
167+
let {getByRole, getByText} = renderSearchWithin({label: undefined});
166168

167169
let group = getByRole('group');
168170
let searchfield = getByRole('searchbox');
169171
let picker = getByRole('button');
172+
let hiddenLabel = getByText('Search within');
170173

171174
expect(group).toHaveAttribute('aria-label', 'Search');
172175

173176
expect(group).not.toHaveAttribute('aria-labelledby');
174-
expect(searchfield).toHaveAttribute('aria-labelledby', `${picker.id} ${picker.childNodes[0].id}`);
175-
expect(picker).toHaveAttribute('aria-labelledby', `${picker.id} ${picker.childNodes[0].id}`);
177+
expect(searchfield).toHaveAttribute('aria-labelledby', `${hiddenLabel.id} ${picker.id}`);
178+
expect(picker).toHaveAttribute('aria-labelledby', `${hiddenLabel.id} ${picker.childNodes[0].id}`);
176179
});
177180

178181
it('label = foo', function () {
@@ -182,32 +185,34 @@ describe('SearchWithin labeling', function () {
182185
let searchfield = getByRole('searchbox');
183186
let picker = getByRole('button');
184187
let label = getByText('foo');
188+
let hiddenLabel = getByText('Search within');
185189

186190
expect(group).not.toHaveAttribute('aria-label');
187191

188192
expect(group).toHaveAttribute('aria-labelledby', label.id);
189-
expect(searchfield).toHaveAttribute('aria-labelledby', `${label.id} ${picker.id} ${picker.childNodes[0].id}`);
190-
expect(picker).toHaveAttribute('aria-labelledby', `${label.id} ${picker.id} ${picker.childNodes[0].id}`);
193+
expect(searchfield).toHaveAttribute('aria-labelledby', `${label.id} ${hiddenLabel.id} ${picker.id}`);
194+
expect(picker).toHaveAttribute('aria-labelledby', `${label.id} ${hiddenLabel.id} ${picker.childNodes[0].id}`);
191195

192196
expect(label).toHaveAttribute('for', searchfield.id);
193197
});
194198

195199
it('aria-label = bar', function () {
196-
let {getByRole} = renderSearchWithin({'aria-label': 'bar', label: undefined});
200+
let {getByRole, getByText} = renderSearchWithin({'aria-label': 'bar', label: undefined});
197201

198202
let group = getByRole('group');
199203
let searchfield = getByRole('searchbox');
200204
let picker = getByRole('button');
205+
let hiddenLabel = getByText('Search within');
201206

202207
expect(group).toHaveAttribute('aria-label', 'bar');
203208

204209
expect(group).not.toHaveAttribute('aria-labelledby');
205-
expect(searchfield).toHaveAttribute('aria-labelledby', `${group.id} ${picker.id} ${picker.childNodes[0].id}`);
206-
expect(picker).toHaveAttribute('aria-labelledby', `${group.id} ${picker.id} ${picker.childNodes[0].id}`);
210+
expect(searchfield).toHaveAttribute('aria-labelledby', `${group.id} ${hiddenLabel.id} ${picker.id}`);
211+
expect(picker).toHaveAttribute('aria-labelledby', `${group.id} ${hiddenLabel.id} ${picker.childNodes[0].id}`);
207212
});
208213

209214
it('aria-labelledby = {id}', function () {
210-
let {getByRole} = render(
215+
let {getByRole, debug, getByText} = render(
211216
<Provider theme={theme}>
212217
<label id="id-foo-label" htmlFor="id-searchfield">
213218
Foo
@@ -223,16 +228,18 @@ describe('SearchWithin labeling', function () {
223228
</SearchWithin>
224229
</Provider>
225230
);
231+
debug();
226232

227233
let group = getByRole('group');
228234
let searchfield = getByRole('searchbox');
229235
let picker = getByRole('button');
236+
let hiddenLabel = getByText('Search within');
230237

231238
expect(group).not.toHaveAttribute('aria-label');
232239

233240
expect(group).toHaveAttribute('aria-labelledby', 'id-foo-label');
234-
expect(searchfield).toHaveAttribute('aria-labelledby', `id-foo-label ${picker.id} ${picker.childNodes[0].id}`);
241+
expect(searchfield).toHaveAttribute('aria-labelledby', `id-foo-label ${hiddenLabel.id} ${picker.id}`);
235242
expect(searchfield).toHaveAttribute('id', 'id-searchfield');
236-
expect(picker).toHaveAttribute('aria-labelledby', `id-foo-label ${picker.id} ${picker.childNodes[0].id}`);
243+
expect(picker).toHaveAttribute('aria-labelledby', `id-foo-label ${hiddenLabel.id} ${picker.childNodes[0].id}`);
237244
});
238245
});

0 commit comments

Comments
 (0)