From 9f5462791373ab3514324d747ad1624a2ce78ce1 Mon Sep 17 00:00:00 2001 From: Dhaya <154633+dhayab@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:21:27 +0200 Subject: [PATCH 1/5] fix(refinement-list): re-apply focus on facet checkbox after render --- .../RefinementList/RefinementList.tsx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) 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)}
    ); From 0fdd54a59b444f4a59e8b29faa0178e2eab9e068 Mon Sep 17 00:00:00 2001 From: Dhaya <154633+dhayab@users.noreply.github.com> Date: Thu, 17 Oct 2024 18:17:42 +0200 Subject: [PATCH 2/5] add tests --- .../__tests__/RefinementList-test.tsx | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) 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..2ff84ba646 100644 --- a/packages/instantsearch.js/src/components/RefinementList/__tests__/RefinementList-test.tsx +++ b/packages/instantsearch.js/src/components/RefinementList/__tests__/RefinementList-test.tsx @@ -14,6 +14,7 @@ import type { RefinementListTemplates, } from '../../../widgets/refinement-list/refinement-list'; import type { RefinementListProps } from '../RefinementList'; +import userEvent from '@testing-library/user-event'; const defaultProps = { createURL: () => '#', @@ -660,7 +661,7 @@ describe('RefinementList', () => { `, showMoreText: '', }; @@ -692,5 +693,57 @@ describe('RefinementList', () => { expect(toggleRefinement).toHaveBeenCalledTimes(0); }); + + it('should keep focus on toggled input between re-renders', () => { + const templates = { + item: (item: RefinementListItemData) => ` + + `, + }; + + const props: Omit< + RefinementListProps, + 'facetValues' + > = { + cssClasses: defaultProps.cssClasses, + templateProps: { + templatesConfig: {}, + templates, + useCustomCompileOptions: {}, + }, + toggleRefinement: () => {}, + createURL: () => '', + }; + + const { container, rerender } = render( + + ); + + const initialTargetItem = container.querySelector('input[value="foo"]')!; + userEvent.click(initialTargetItem); + expect(document.activeElement).toBe(initialTargetItem); + + rerender( + + ); + + const updatedTargetItem = container.querySelector('input[value="foo"]')!; + expect(document.activeElement).toBe(updatedTargetItem); + }); }); }); From 94270e54316ce24f8f2fb20fe95b8956e8ec0d87 Mon Sep 17 00:00:00 2001 From: Dhaya <154633+dhayab@users.noreply.github.com> Date: Thu, 17 Oct 2024 18:48:36 +0200 Subject: [PATCH 3/5] sort imports --- .../components/RefinementList/__tests__/RefinementList-test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2ff84ba646..fc4789998b 100644 --- a/packages/instantsearch.js/src/components/RefinementList/__tests__/RefinementList-test.tsx +++ b/packages/instantsearch.js/src/components/RefinementList/__tests__/RefinementList-test.tsx @@ -4,6 +4,7 @@ /** @jsx h */ import { render, fireEvent } from '@testing-library/preact'; +import userEvent from '@testing-library/user-event'; import { h } from 'preact'; import defaultTemplates from '../../../widgets/refinement-list/defaultTemplates'; @@ -14,7 +15,6 @@ import type { RefinementListTemplates, } from '../../../widgets/refinement-list/refinement-list'; import type { RefinementListProps } from '../RefinementList'; -import userEvent from '@testing-library/user-event'; const defaultProps = { createURL: () => '#', From 2499f8a780121bcd64dc5840838bd3e985d58435 Mon Sep 17 00:00:00 2001 From: Dhaya <154633+dhayab@users.noreply.github.com> Date: Fri, 18 Oct 2024 15:48:24 +0200 Subject: [PATCH 4/5] move test to cts --- .../__tests__/RefinementList-test.tsx | 53 ------------------- .../common/widgets/refinement-list/options.ts | 34 ++++++++++++ 2 files changed, 34 insertions(+), 53 deletions(-) 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 fc4789998b..ec46893370 100644 --- a/packages/instantsearch.js/src/components/RefinementList/__tests__/RefinementList-test.tsx +++ b/packages/instantsearch.js/src/components/RefinementList/__tests__/RefinementList-test.tsx @@ -4,7 +4,6 @@ /** @jsx h */ import { render, fireEvent } from '@testing-library/preact'; -import userEvent from '@testing-library/user-event'; import { h } from 'preact'; import defaultTemplates from '../../../widgets/refinement-list/defaultTemplates'; @@ -693,57 +692,5 @@ describe('RefinementList', () => { expect(toggleRefinement).toHaveBeenCalledTimes(0); }); - - it('should keep focus on toggled input between re-renders', () => { - const templates = { - item: (item: RefinementListItemData) => ` - - `, - }; - - const props: Omit< - RefinementListProps, - 'facetValues' - > = { - cssClasses: defaultProps.cssClasses, - templateProps: { - templatesConfig: {}, - templates, - useCustomCompileOptions: {}, - }, - toggleRefinement: () => {}, - createURL: () => '', - }; - - const { container, rerender } = render( - - ); - - const initialTargetItem = container.querySelector('input[value="foo"]')!; - userEvent.click(initialTargetItem); - expect(document.activeElement).toBe(initialTargetItem); - - rerender( - - ); - - const updatedTargetItem = container.querySelector('input[value="foo"]')!; - expect(document.activeElement).toBe(updatedTargetItem); - }); }); }); diff --git a/tests/common/widgets/refinement-list/options.ts b/tests/common/widgets/refinement-list/options.ts index 87abf5e225..12cb904c69 100644 --- a/tests/common/widgets/refinement-list/options.ts +++ b/tests/common/widgets/refinement-list/options.ts @@ -580,6 +580,40 @@ 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 () => { + 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(); From 289d7aec008d3253ba6642f3e66c15c399e0d403 Mon Sep 17 00:00:00 2001 From: Dhaya <154633+dhayab@users.noreply.github.com> Date: Mon, 21 Oct 2024 15:21:20 +0200 Subject: [PATCH 5/5] workaround for vue 3 test --- tests/common/widgets/refinement-list/options.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/common/widgets/refinement-list/options.ts b/tests/common/widgets/refinement-list/options.ts index 12cb904c69..2c6f3c530e 100644 --- a/tests/common/widgets/refinement-list/options.ts +++ b/tests/common/widgets/refinement-list/options.ts @@ -602,6 +602,13 @@ export function createOptionsTests( )!; 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);