Skip to content

Commit

Permalink
fix: generate elements ids in a consistent manner (#1194)
Browse files Browse the repository at this point in the history
  • Loading branch information
dhayab authored Sep 28, 2023
1 parent 973feaf commit a76b914
Show file tree
Hide file tree
Showing 12 changed files with 111 additions and 55 deletions.
17 changes: 14 additions & 3 deletions packages/autocomplete-core/src/__tests__/getInputProps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,21 @@ describe('getInputProps', () => {
test('returns aria-controls with list ID when panel is open', () => {
const { getInputProps, inputElement } = createPlayground(
createAutocomplete,
{ id: 'autocomplete', initialState: { isOpen: true } }
{
id: 'autocomplete',
initialState: {
isOpen: true,
collections: [
createCollection({
source: { sourceId: 'testSource' },
}),
],
},
}
);
const inputProps = getInputProps({ inputElement });

expect(inputProps['aria-controls']).toEqual('autocomplete-list');
expect(inputProps['aria-controls']).toEqual('autocomplete-testSource-list');
});

test('returns aria-labelledby with label ID', () => {
Expand Down Expand Up @@ -669,14 +679,15 @@ describe('getInputProps', () => {
initialState: {
collections: [
createCollection({
source: { sourceId: 'testSource' },
items: [{ label: '1' }],
}),
],
},
...props,
});
const item = document.createElement('div');
item.setAttribute('id', 'autocomplete-item-0');
item.setAttribute('id', 'autocomplete-testSource-item-0');
document.body.appendChild(item);

return { ...playground, item };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('getItemProps', () => {
...defaultItemProps,
});

expect(itemProps.id).toEqual('autocomplete-item-0');
expect(itemProps.id).toEqual('autocomplete-testSource-item-0');
});

test('returns the role to option', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createCollection } from '../../../../test/utils';
import { createAutocomplete } from '../createAutocomplete';

describe('getRootProps', () => {
Expand Down Expand Up @@ -60,11 +61,16 @@ describe('getRootProps', () => {
id: 'autocomplete',
initialState: {
isOpen: true,
collections: [
createCollection({
source: { sourceId: 'testSource' },
}),
],
},
});
const rootProps = autocomplete.getRootProps({});

expect(rootProps['aria-owns']).toEqual('autocomplete-list');
expect(rootProps['aria-owns']).toEqual('autocomplete-testSource-list');
});

test('returns label id in aria-labelledby', () => {
Expand Down
67 changes: 42 additions & 25 deletions packages/autocomplete-core/src/getPropGetters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ import {
GetRootProps,
InternalAutocompleteOptions,
} from './types';
import { getActiveItem, isOrContainsNode, isSamsung } from './utils';
import {
getActiveItem,
getAutocompleteElementId,
isOrContainsNode,
isSamsung,
} from './utils';

interface GetPropGettersOptions<TItem extends BaseItem>
extends AutocompleteScopeApi<TItem> {
Expand Down Expand Up @@ -104,8 +109,15 @@ export function getPropGetters<
role: 'combobox',
'aria-expanded': store.getState().isOpen,
'aria-haspopup': 'listbox',
'aria-owns': store.getState().isOpen ? `${props.id}-list` : undefined,
'aria-labelledby': `${props.id}-label`,
'aria-owns': store.getState().isOpen
? store
.getState()
.collections.map(({ source }) =>
getAutocompleteElementId(props.id, 'list', source)
)
.join(' ')
: undefined,
'aria-labelledby': getAutocompleteElementId(props.id, 'label'),
...rest,
};
};
Expand Down Expand Up @@ -180,12 +192,23 @@ export function getPropGetters<
'aria-autocomplete': 'both',
'aria-activedescendant':
store.getState().isOpen && store.getState().activeItemId !== null
? `${props.id}-item-${store.getState().activeItemId}`
? getAutocompleteElementId(
props.id,
`item-${store.getState().activeItemId}`,
activeItem?.source
)
: undefined,
'aria-controls': store.getState().isOpen ? `${props.id}-list` : undefined,
'aria-labelledby': `${props.id}-label`,
'aria-controls': store.getState().isOpen
? store
.getState()
.collections.map(({ source }) =>
getAutocompleteElementId(props.id, 'list', source)
)
.join(' ')
: undefined,
'aria-labelledby': getAutocompleteElementId(props.id, 'label'),
value: store.getState().completion || store.getState().query,
id: `${props.id}-input`,
id: getAutocompleteElementId(props.id, 'input'),
autoComplete: 'off',
autoCorrect: 'off',
autoCapitalize: 'off',
Expand Down Expand Up @@ -241,29 +264,21 @@ export function getPropGetters<
};
};

const getAutocompleteId = (instanceId: string, sourceId?: number) => {
return typeof sourceId !== 'undefined'
? `${instanceId}-${sourceId}`
: instanceId;
};

const getLabelProps: GetLabelProps = (providedProps) => {
const { sourceIndex, ...rest } = providedProps || {};

const getLabelProps: GetLabelProps = (rest) => {
return {
htmlFor: `${getAutocompleteId(props.id, sourceIndex)}-input`,
id: `${getAutocompleteId(props.id, sourceIndex)}-label`,
htmlFor: getAutocompleteElementId(props.id, 'input'),
id: getAutocompleteElementId(props.id, 'label'),
...rest,
};
};

const getListProps: GetListProps = (providedProps) => {
const { sourceIndex, ...rest } = providedProps || {};
const { source, ...rest } = providedProps || {};

return {
role: 'listbox',
'aria-labelledby': `${getAutocompleteId(props.id, sourceIndex)}-label`,
id: `${getAutocompleteId(props.id, sourceIndex)}-list`,
'aria-labelledby': getAutocompleteElementId(props.id, 'label'),
id: getAutocompleteElementId(props.id, 'list', source),
...rest,
};
};
Expand All @@ -284,12 +299,14 @@ export function getPropGetters<
};

const getItemProps: GetItemProps<any, TMouseEvent> = (providedProps) => {
const { item, source, sourceIndex, ...rest } = providedProps;
const { item, source, ...rest } = providedProps;

return {
id: `${getAutocompleteId(props.id, sourceIndex as number)}-item-${
item.__autocomplete_id
}`,
id: getAutocompleteElementId(
props.id,
`item-${item.__autocomplete_id}`,
source
),
role: 'option',
'aria-selected': store.getState().activeItemId === item.__autocomplete_id,
onMouseMove(event) {
Expand Down
10 changes: 8 additions & 2 deletions packages/autocomplete-core/src/onKeyDown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
BaseItem,
InternalAutocompleteOptions,
} from './types';
import { getActiveItem } from './utils';
import { getActiveItem, getAutocompleteElementId } from './utils';

interface OnKeyDownOptions<TItem extends BaseItem>
extends AutocompleteScopeApi<TItem> {
Expand All @@ -25,8 +25,14 @@ export function onKeyDown<TItem extends BaseItem>({
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
// eslint-disable-next-line no-inner-declarations
function triggerScrollIntoView() {
const highlightedItem = getActiveItem(store.getState());

const nodeItem = props.environment.document.getElementById(
`${props.id}-item-${store.getState().activeItemId}`
getAutocompleteElementId(
props.id,
`item-${store.getState().activeItemId}`,
highlightedItem?.source
)
);

if (nodeItem) {
Expand Down
19 changes: 19 additions & 0 deletions packages/autocomplete-core/src/utils/getAutocompleteElementId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { InternalAutocompleteSource } from '../types';

/**
* Returns a full element id for an autocomplete element.
*
* @param autocompleteInstanceId The id of the autocomplete instance
* @param elementId The specific element id
* @param source The source of the element, when it needs to be scoped
*/
export function getAutocompleteElementId(
autocompleteInstanceId: string,
elementId: string,
source?: InternalAutocompleteSource<any>
) {
return [autocompleteInstanceId, source?.sourceId, elementId]
.filter(Boolean)
.join('-')
.replace(/\s/g, '');
}
1 change: 1 addition & 0 deletions packages/autocomplete-core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './createConcurrentSafePromise';
export * from './getNextActiveItemId';
export * from './getNormalizedSources';
export * from './getActiveItem';
export * from './getAutocompleteElementId';
export * from './isOrContainsNode';
export * from './isSamsung';
export * from './mapToAlgoliaResponse';
2 changes: 1 addition & 1 deletion packages/autocomplete-js/src/__tests__/detached.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ describe('detached', () => {
});

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

// Select the first item
Expand Down
6 changes: 3 additions & 3 deletions packages/autocomplete-js/src/__tests__/renderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,15 +221,15 @@ describe('renderer', () => {
data-autocomplete-source-id="testSource"
>
<ul
aria-labelledby="autocomplete-0-0-label"
aria-labelledby="autocomplete-0-label"
class="aa-List"
id="autocomplete-0-0-list"
id="autocomplete-0-testSource-list"
role="listbox"
>
<li
aria-selected="false"
class="aa-Item"
id="autocomplete-0-0-item-0"
id="autocomplete-0-testSource-item-0"
role="option"
>
1
Expand Down
24 changes: 12 additions & 12 deletions packages/autocomplete-js/src/__tests__/templates.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,15 @@ describe('templates', () => {
expect(within(panelContainer).getByRole('listbox'))
.toMatchInlineSnapshot(`
<ul
aria-labelledby="autocomplete-0-0-label"
aria-labelledby="autocomplete-0-label"
class="aa-List"
id="autocomplete-0-0-list"
id="autocomplete-0-testSource-list"
role="listbox"
>
<li
aria-selected="false"
class="aa-Item"
id="autocomplete-0-0-item-0"
id="autocomplete-0-testSource-item-0"
role="option"
>
<div
Expand Down Expand Up @@ -208,15 +208,15 @@ describe('templates', () => {
expect(within(panelContainer).getByRole('listbox'))
.toMatchInlineSnapshot(`
<ul
aria-labelledby="autocomplete-0-0-label"
aria-labelledby="autocomplete-0-label"
class="aa-List"
id="autocomplete-0-0-list"
id="autocomplete-0-testSource-list"
role="listbox"
>
<li
aria-selected="false"
class="aa-Item"
id="autocomplete-0-0-item-0"
id="autocomplete-0-testSource-item-0"
role="option"
>
<div
Expand Down Expand Up @@ -343,15 +343,15 @@ describe('templates', () => {
</header>
</div>
<ul
aria-labelledby="autocomplete-0-0-label"
aria-labelledby="autocomplete-0-label"
class="aa-List"
id="autocomplete-0-0-list"
id="autocomplete-0-testSource-list"
role="listbox"
>
<li
aria-selected="false"
class="aa-Item"
id="autocomplete-0-0-item-0"
id="autocomplete-0-testSource-item-0"
role="option"
>
<div
Expand Down Expand Up @@ -507,15 +507,15 @@ describe('templates', () => {
</header>
</div>
<ul
aria-labelledby="autocomplete-0-0-label"
aria-labelledby="autocomplete-0-label"
class="aa-List"
id="autocomplete-0-0-list"
id="autocomplete-0-testSource-list"
role="listbox"
>
<li
aria-selected="false"
class="aa-Item"
id="autocomplete-0-0-item-0"
id="autocomplete-0-testSource-item-0"
role="option"
>
div
Expand Down
3 changes: 1 addition & 2 deletions packages/autocomplete-js/src/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export function renderPanel<TItem extends BaseItem>(
{...propGetters.getListProps({
state,
props: autocomplete.getListProps({
sourceIndex,
source,
}),
...autocompleteScopeApi,
})}
Expand All @@ -147,7 +147,6 @@ export function renderPanel<TItem extends BaseItem>(
const itemProps = autocomplete.getItemProps({
item,
source,
sourceIndex,
});

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,7 @@ export type GetFormProps<TEvent = Event> = (props: {
onReset(event: TEvent): void;
};

export type GetLabelProps = (props?: {
[key: string]: unknown;
sourceIndex?: number;
}) => {
export type GetLabelProps = (props?: { [key: string]: unknown }) => {
htmlFor: string;
id: string;
};
Expand Down Expand Up @@ -101,7 +98,7 @@ export type GetPanelProps<TMouseEvent> = (props?: {

export type GetListProps = (props?: {
[key: string]: unknown;
sourceIndex?: number;
source: InternalAutocompleteSource<any>;
}) => {
role: 'listbox';
'aria-labelledby': string;
Expand Down

0 comments on commit a76b914

Please sign in to comment.