diff --git a/packages/instantsearch.js/src/components/RefinementList/RefinementList.tsx b/packages/instantsearch.js/src/components/RefinementList/RefinementList.tsx index 885fa80c85..3fcaf35c33 100644 --- a/packages/instantsearch.js/src/components/RefinementList/RefinementList.tsx +++ b/packages/instantsearch.js/src/components/RefinementList/RefinementList.tsx @@ -108,8 +108,11 @@ class RefinementList extends Component< > { public static defaultProps = defaultProps; + private listRef = createRef(); private searchBox = createRef(); + private lastRefinedValue: string | undefined = undefined; + public shouldComponentUpdate( nextProps: RefinementListPropsWithDefaultProps ) { @@ -122,6 +125,7 @@ class RefinementList extends Component< } private refine(facetValueToRefine: string) { + this.lastRefinedValue = facetValueToRefine; this.props.toggleRefinement(facetValueToRefine); } @@ -270,6 +274,20 @@ class RefinementList extends Component< } } + /** + * This sets focus on the last refined input element after a render + * because Preact does not perform it automatically. + * @see https://github.com/preactjs/preact/issues/3242 + */ + public componentDidUpdate() { + this.listRef.current + ?.querySelector( + `input[value="${this.lastRefinedValue}"]` + ) + ?.focus(); + this.lastRefinedValue = undefined; + } + private refineFirstValue() { const firstValue = this.props.facetValues && this.props.facetValues[0]; if (firstValue) { @@ -330,7 +348,7 @@ class RefinementList extends Component< const facetValues = this.props.facetValues && this.props.facetValues.length > 0 && ( -
    +
      {this.props.facetValues.map(this._generateFacetItem, this)}
    ); diff --git a/packages/instantsearch.js/src/components/RefinementList/__tests__/RefinementList-test.tsx b/packages/instantsearch.js/src/components/RefinementList/__tests__/RefinementList-test.tsx index 51ab4c0a68..ec46893370 100644 --- a/packages/instantsearch.js/src/components/RefinementList/__tests__/RefinementList-test.tsx +++ b/packages/instantsearch.js/src/components/RefinementList/__tests__/RefinementList-test.tsx @@ -660,7 +660,7 @@ describe('RefinementList', () => { `, showMoreText: '', }; diff --git a/tests/common/widgets/refinement-list/options.ts b/tests/common/widgets/refinement-list/options.ts index 87abf5e225..2c6f3c530e 100644 --- a/tests/common/widgets/refinement-list/options.ts +++ b/tests/common/widgets/refinement-list/options.ts @@ -580,6 +580,47 @@ export function createOptionsTests( ]); }); + test('keeps focus on toggled input between re-renders', async () => { + const searchClient = createMockedSearchClient(); + + await setup({ + instantSearchOptions: { + indexName: 'indexName', + searchClient, + }, + widgetParams: { attribute: 'brand' }, + }); + + await act(async () => { + await wait(0); + }); + + expect(document.activeElement).toEqual(document.body); + + const initialTargetItem = document.querySelector( + '.ais-RefinementList-checkbox[value="Samsung"]' + )!; + + await act(async () => { + /** + * Jest wrongly fails the last assertion when running in Vue 3 + * (`activeElement` is reset to body). + * Duplicating the following call fixes this as a workaround, + * and doesn't change the objective of this test. + */ + userEvent.click(initialTargetItem); + userEvent.click(initialTargetItem); + expect(document.activeElement).toEqual(initialTargetItem); + await wait(0); + }); + + const updatedTargetItem = document.querySelector( + '.ais-RefinementList-checkbox[value="Samsung"]' + )!; + + expect(document.activeElement).toEqual(updatedTargetItem); + }); + describe('sorting', () => { test('sorts the items by ascending name', async () => { const searchClient = createMockedSearchClient();