From 1871761c0276cb1bc70229d3d88f59561be5e5c1 Mon Sep 17 00:00:00 2001 From: Hayden Hung Hoang Date: Tue, 13 Oct 2026 20:37:13 +0700 Subject: [PATCH] fix(searchbox): preserve input focus when loading indicator changes --- .../src/__tests__/searchboxPreact.test.tsx | 84 +++++++++++++++++++ .../src/components/ui/SearchBoxForm.tsx | 3 +- 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 packages/docsearch-react/src/__tests__/searchboxPreact.test.tsx diff --git a/packages/docsearch-react/src/__tests__/searchboxPreact.test.tsx b/packages/docsearch-react/src/__tests__/searchboxPreact.test.tsx new file mode 100644 index 00000000..dc1547d5 --- /dev/null +++ b/packages/docsearch-react/src/__tests__/searchboxPreact.test.tsx @@ -0,0 +1,84 @@ +import { + createAutocomplete, + type AutocompleteState, +} from '@algolia/autocomplete-core'; +import { render, type VNode } from 'preact'; +import { act } from 'preact/test-utils'; +import React from 'react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import '@testing-library/jest-dom/vitest'; + +import { SearchBoxForm } from '../components/ui/SearchBoxForm'; +import type { InternalDocSearchHit } from '../types'; + +// The vanilla JS bundles render the shared React components using Preact. +vi.mock('react', async () => await import('preact/compat')); + +describe('SearchBoxForm with Preact', () => { + let container: HTMLDivElement; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + act(() => render(null, container)); + container.remove(); + }); + + it('preserves the input node and focus when the loading icon appears and disappears', () => { + const inputRef = React.createRef(); + const autocomplete = createAutocomplete< + InternalDocSearchHit, + React.FormEvent, + React.MouseEvent, + React.KeyboardEvent + >({}); + const renderForm = ( + status: AutocompleteState['status'] + ): void => { + const form = ( + + ); + // JSX uses the mocked Preact runtime, while TS retains React's JSX types. + act(() => render(form as unknown as VNode, container)); + }; + + renderForm('idle'); + const input = inputRef.current!; + input.focus(); + expect(input).toHaveFocus(); + + for (const status of ['loading', 'stalled', 'idle'] as const) { + renderForm(status); + expect( + Boolean(container.querySelector('.DocSearch-LoadingIndicator')) + ).toBe(status === 'stalled'); + expect(container.querySelector('input')).toBe(input); + expect(inputRef.current).toBe(input); + expect(input).toHaveFocus(); + } + }); +}); diff --git a/packages/docsearch-react/src/components/ui/SearchBoxForm.tsx b/packages/docsearch-react/src/components/ui/SearchBoxForm.tsx index 991c85c5..0c15d25b 100644 --- a/packages/docsearch-react/src/components/ui/SearchBoxForm.tsx +++ b/packages/docsearch-react/src/components/ui/SearchBoxForm.tsx @@ -85,11 +85,12 @@ export function SearchBoxForm({ > {leadingElement || (isKeywordSearchLoading ? ( -
+
) : (