Skip to content

Commit a76b914

Browse files
authored
fix: generate elements ids in a consistent manner (#1194)
1 parent 973feaf commit a76b914

File tree

12 files changed

+111
-55
lines changed

12 files changed

+111
-55
lines changed

packages/autocomplete-core/src/__tests__/getInputProps.test.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,21 @@ describe('getInputProps', () => {
8989
test('returns aria-controls with list ID when panel is open', () => {
9090
const { getInputProps, inputElement } = createPlayground(
9191
createAutocomplete,
92-
{ id: 'autocomplete', initialState: { isOpen: true } }
92+
{
93+
id: 'autocomplete',
94+
initialState: {
95+
isOpen: true,
96+
collections: [
97+
createCollection({
98+
source: { sourceId: 'testSource' },
99+
}),
100+
],
101+
},
102+
}
93103
);
94104
const inputProps = getInputProps({ inputElement });
95105

96-
expect(inputProps['aria-controls']).toEqual('autocomplete-list');
106+
expect(inputProps['aria-controls']).toEqual('autocomplete-testSource-list');
97107
});
98108

99109
test('returns aria-labelledby with label ID', () => {
@@ -669,14 +679,15 @@ describe('getInputProps', () => {
669679
initialState: {
670680
collections: [
671681
createCollection({
682+
source: { sourceId: 'testSource' },
672683
items: [{ label: '1' }],
673684
}),
674685
],
675686
},
676687
...props,
677688
});
678689
const item = document.createElement('div');
679-
item.setAttribute('id', 'autocomplete-item-0');
690+
item.setAttribute('id', 'autocomplete-testSource-item-0');
680691
document.body.appendChild(item);
681692

682693
return { ...playground, item };

packages/autocomplete-core/src/__tests__/getItemProps.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ describe('getItemProps', () => {
2929
...defaultItemProps,
3030
});
3131

32-
expect(itemProps.id).toEqual('autocomplete-item-0');
32+
expect(itemProps.id).toEqual('autocomplete-testSource-item-0');
3333
});
3434

3535
test('returns the role to option', () => {

packages/autocomplete-core/src/__tests__/getRootProps.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createCollection } from '../../../../test/utils';
12
import { createAutocomplete } from '../createAutocomplete';
23

34
describe('getRootProps', () => {
@@ -60,11 +61,16 @@ describe('getRootProps', () => {
6061
id: 'autocomplete',
6162
initialState: {
6263
isOpen: true,
64+
collections: [
65+
createCollection({
66+
source: { sourceId: 'testSource' },
67+
}),
68+
],
6369
},
6470
});
6571
const rootProps = autocomplete.getRootProps({});
6672

67-
expect(rootProps['aria-owns']).toEqual('autocomplete-list');
73+
expect(rootProps['aria-owns']).toEqual('autocomplete-testSource-list');
6874
});
6975

7076
test('returns label id in aria-labelledby', () => {

packages/autocomplete-core/src/getPropGetters.ts

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ import {
1616
GetRootProps,
1717
InternalAutocompleteOptions,
1818
} from './types';
19-
import { getActiveItem, isOrContainsNode, isSamsung } from './utils';
19+
import {
20+
getActiveItem,
21+
getAutocompleteElementId,
22+
isOrContainsNode,
23+
isSamsung,
24+
} from './utils';
2025

2126
interface GetPropGettersOptions<TItem extends BaseItem>
2227
extends AutocompleteScopeApi<TItem> {
@@ -104,8 +109,15 @@ export function getPropGetters<
104109
role: 'combobox',
105110
'aria-expanded': store.getState().isOpen,
106111
'aria-haspopup': 'listbox',
107-
'aria-owns': store.getState().isOpen ? `${props.id}-list` : undefined,
108-
'aria-labelledby': `${props.id}-label`,
112+
'aria-owns': store.getState().isOpen
113+
? store
114+
.getState()
115+
.collections.map(({ source }) =>
116+
getAutocompleteElementId(props.id, 'list', source)
117+
)
118+
.join(' ')
119+
: undefined,
120+
'aria-labelledby': getAutocompleteElementId(props.id, 'label'),
109121
...rest,
110122
};
111123
};
@@ -180,12 +192,23 @@ export function getPropGetters<
180192
'aria-autocomplete': 'both',
181193
'aria-activedescendant':
182194
store.getState().isOpen && store.getState().activeItemId !== null
183-
? `${props.id}-item-${store.getState().activeItemId}`
195+
? getAutocompleteElementId(
196+
props.id,
197+
`item-${store.getState().activeItemId}`,
198+
activeItem?.source
199+
)
184200
: undefined,
185-
'aria-controls': store.getState().isOpen ? `${props.id}-list` : undefined,
186-
'aria-labelledby': `${props.id}-label`,
201+
'aria-controls': store.getState().isOpen
202+
? store
203+
.getState()
204+
.collections.map(({ source }) =>
205+
getAutocompleteElementId(props.id, 'list', source)
206+
)
207+
.join(' ')
208+
: undefined,
209+
'aria-labelledby': getAutocompleteElementId(props.id, 'label'),
187210
value: store.getState().completion || store.getState().query,
188-
id: `${props.id}-input`,
211+
id: getAutocompleteElementId(props.id, 'input'),
189212
autoComplete: 'off',
190213
autoCorrect: 'off',
191214
autoCapitalize: 'off',
@@ -241,29 +264,21 @@ export function getPropGetters<
241264
};
242265
};
243266

244-
const getAutocompleteId = (instanceId: string, sourceId?: number) => {
245-
return typeof sourceId !== 'undefined'
246-
? `${instanceId}-${sourceId}`
247-
: instanceId;
248-
};
249-
250-
const getLabelProps: GetLabelProps = (providedProps) => {
251-
const { sourceIndex, ...rest } = providedProps || {};
252-
267+
const getLabelProps: GetLabelProps = (rest) => {
253268
return {
254-
htmlFor: `${getAutocompleteId(props.id, sourceIndex)}-input`,
255-
id: `${getAutocompleteId(props.id, sourceIndex)}-label`,
269+
htmlFor: getAutocompleteElementId(props.id, 'input'),
270+
id: getAutocompleteElementId(props.id, 'label'),
256271
...rest,
257272
};
258273
};
259274

260275
const getListProps: GetListProps = (providedProps) => {
261-
const { sourceIndex, ...rest } = providedProps || {};
276+
const { source, ...rest } = providedProps || {};
262277

263278
return {
264279
role: 'listbox',
265-
'aria-labelledby': `${getAutocompleteId(props.id, sourceIndex)}-label`,
266-
id: `${getAutocompleteId(props.id, sourceIndex)}-list`,
280+
'aria-labelledby': getAutocompleteElementId(props.id, 'label'),
281+
id: getAutocompleteElementId(props.id, 'list', source),
267282
...rest,
268283
};
269284
};
@@ -284,12 +299,14 @@ export function getPropGetters<
284299
};
285300

286301
const getItemProps: GetItemProps<any, TMouseEvent> = (providedProps) => {
287-
const { item, source, sourceIndex, ...rest } = providedProps;
302+
const { item, source, ...rest } = providedProps;
288303

289304
return {
290-
id: `${getAutocompleteId(props.id, sourceIndex as number)}-item-${
291-
item.__autocomplete_id
292-
}`,
305+
id: getAutocompleteElementId(
306+
props.id,
307+
`item-${item.__autocomplete_id}`,
308+
source
309+
),
293310
role: 'option',
294311
'aria-selected': store.getState().activeItemId === item.__autocomplete_id,
295312
onMouseMove(event) {

packages/autocomplete-core/src/onKeyDown.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
BaseItem,
77
InternalAutocompleteOptions,
88
} from './types';
9-
import { getActiveItem } from './utils';
9+
import { getActiveItem, getAutocompleteElementId } from './utils';
1010

1111
interface OnKeyDownOptions<TItem extends BaseItem>
1212
extends AutocompleteScopeApi<TItem> {
@@ -25,8 +25,14 @@ export function onKeyDown<TItem extends BaseItem>({
2525
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
2626
// eslint-disable-next-line no-inner-declarations
2727
function triggerScrollIntoView() {
28+
const highlightedItem = getActiveItem(store.getState());
29+
2830
const nodeItem = props.environment.document.getElementById(
29-
`${props.id}-item-${store.getState().activeItemId}`
31+
getAutocompleteElementId(
32+
props.id,
33+
`item-${store.getState().activeItemId}`,
34+
highlightedItem?.source
35+
)
3036
);
3137

3238
if (nodeItem) {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { InternalAutocompleteSource } from '../types';
2+
3+
/**
4+
* Returns a full element id for an autocomplete element.
5+
*
6+
* @param autocompleteInstanceId The id of the autocomplete instance
7+
* @param elementId The specific element id
8+
* @param source The source of the element, when it needs to be scoped
9+
*/
10+
export function getAutocompleteElementId(
11+
autocompleteInstanceId: string,
12+
elementId: string,
13+
source?: InternalAutocompleteSource<any>
14+
) {
15+
return [autocompleteInstanceId, source?.sourceId, elementId]
16+
.filter(Boolean)
17+
.join('-')
18+
.replace(/\s/g, '');
19+
}

packages/autocomplete-core/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from './createConcurrentSafePromise';
44
export * from './getNextActiveItemId';
55
export * from './getNormalizedSources';
66
export * from './getActiveItem';
7+
export * from './getAutocompleteElementId';
78
export * from './isOrContainsNode';
89
export * from './isSamsung';
910
export * from './mapToAlgoliaResponse';

packages/autocomplete-js/src/__tests__/detached.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ describe('detached', () => {
8080
});
8181

8282
const firstItem = document.querySelector<HTMLLIElement>(
83-
'#autocomplete-0-item-0'
83+
'#autocomplete-testSource-item-0'
8484
)!;
8585

8686
// Select the first item

packages/autocomplete-js/src/__tests__/renderer.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,15 +221,15 @@ describe('renderer', () => {
221221
data-autocomplete-source-id="testSource"
222222
>
223223
<ul
224-
aria-labelledby="autocomplete-0-0-label"
224+
aria-labelledby="autocomplete-0-label"
225225
class="aa-List"
226-
id="autocomplete-0-0-list"
226+
id="autocomplete-0-testSource-list"
227227
role="listbox"
228228
>
229229
<li
230230
aria-selected="false"
231231
class="aa-Item"
232-
id="autocomplete-0-0-item-0"
232+
id="autocomplete-0-testSource-item-0"
233233
role="option"
234234
>
235235
1

packages/autocomplete-js/src/__tests__/templates.test.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,15 @@ describe('templates', () => {
9292
expect(within(panelContainer).getByRole('listbox'))
9393
.toMatchInlineSnapshot(`
9494
<ul
95-
aria-labelledby="autocomplete-0-0-label"
95+
aria-labelledby="autocomplete-0-label"
9696
class="aa-List"
97-
id="autocomplete-0-0-list"
97+
id="autocomplete-0-testSource-list"
9898
role="listbox"
9999
>
100100
<li
101101
aria-selected="false"
102102
class="aa-Item"
103-
id="autocomplete-0-0-item-0"
103+
id="autocomplete-0-testSource-item-0"
104104
role="option"
105105
>
106106
<div
@@ -208,15 +208,15 @@ describe('templates', () => {
208208
expect(within(panelContainer).getByRole('listbox'))
209209
.toMatchInlineSnapshot(`
210210
<ul
211-
aria-labelledby="autocomplete-0-0-label"
211+
aria-labelledby="autocomplete-0-label"
212212
class="aa-List"
213-
id="autocomplete-0-0-list"
213+
id="autocomplete-0-testSource-list"
214214
role="listbox"
215215
>
216216
<li
217217
aria-selected="false"
218218
class="aa-Item"
219-
id="autocomplete-0-0-item-0"
219+
id="autocomplete-0-testSource-item-0"
220220
role="option"
221221
>
222222
<div
@@ -343,15 +343,15 @@ describe('templates', () => {
343343
</header>
344344
</div>
345345
<ul
346-
aria-labelledby="autocomplete-0-0-label"
346+
aria-labelledby="autocomplete-0-label"
347347
class="aa-List"
348-
id="autocomplete-0-0-list"
348+
id="autocomplete-0-testSource-list"
349349
role="listbox"
350350
>
351351
<li
352352
aria-selected="false"
353353
class="aa-Item"
354-
id="autocomplete-0-0-item-0"
354+
id="autocomplete-0-testSource-item-0"
355355
role="option"
356356
>
357357
<div
@@ -507,15 +507,15 @@ describe('templates', () => {
507507
</header>
508508
</div>
509509
<ul
510-
aria-labelledby="autocomplete-0-0-label"
510+
aria-labelledby="autocomplete-0-label"
511511
class="aa-List"
512-
id="autocomplete-0-0-list"
512+
id="autocomplete-0-testSource-list"
513513
role="listbox"
514514
>
515515
<li
516516
aria-selected="false"
517517
class="aa-Item"
518-
id="autocomplete-0-0-item-0"
518+
id="autocomplete-0-testSource-item-0"
519519
role="option"
520520
>
521521
div

0 commit comments

Comments
 (0)