From a41ccc6fe6755f4b4cc7d6421ce830858a3f4616 Mon Sep 17 00:00:00 2001 From: Fabien Motte Date: Thu, 2 Mar 2023 14:11:05 +0100 Subject: [PATCH] fix(autocomplete-js): `query` is reflected in the detached search `button` (#1100) Co-authored-by: Sarah Dayan <5370675+sarahdayan@users.noreply.github.com> --- .../src/__tests__/detached.test.ts | 104 +++++++++++++++++- packages/autocomplete-js/src/autocomplete.ts | 2 - .../src/createAutocompleteDom.ts | 22 +++- .../autocomplete-js/src/getDefaultOptions.ts | 1 + packages/autocomplete-js/src/render.tsx | 6 + .../src/types/AutocompleteClassNames.ts | 1 + .../src/types/AutocompleteDom.ts | 2 + .../autocomplete-theme-classic/src/theme.scss | 13 +++ yarn.lock | 13 +-- 9 files changed, 143 insertions(+), 21 deletions(-) diff --git a/packages/autocomplete-js/src/__tests__/detached.test.ts b/packages/autocomplete-js/src/__tests__/detached.test.ts index 52aeb9789..01005d638 100644 --- a/packages/autocomplete-js/src/__tests__/detached.test.ts +++ b/packages/autocomplete-js/src/__tests__/detached.test.ts @@ -57,13 +57,13 @@ describe('detached', () => { const searchButton = container.querySelector( '.aa-DetachedSearchButton' - ); + )!; // Open detached overlay searchButton.click(); await waitFor(() => { - const input = document.querySelector('.aa-Input'); + const input = document.querySelector('.aa-Input')!; expect(document.querySelector('.aa-DetachedOverlay')).toBeInTheDocument(); expect(document.body).toHaveClass('aa-Detached'); @@ -81,7 +81,7 @@ describe('detached', () => { const firstItem = document.querySelector( '#autocomplete-0-item-0' - ); + )!; // Select the first item firstItem.click(); @@ -106,7 +106,7 @@ describe('detached', () => { const searchButton = container.querySelector( '.aa-DetachedSearchButton' - ); + )!; // Open detached overlay searchButton.click(); @@ -118,7 +118,7 @@ describe('detached', () => { const cancelButton = document.querySelector( '.aa-DetachedCancelButton' - ); + )!; // Prevent `onTouchStart` event from closing detached overlay const windowTouchStartListener = jest.fn(); @@ -372,4 +372,98 @@ describe('detached', () => { ); }); }); + + test('preserves `query` in the detached search `button` after closing', async () => { + const container = document.createElement('div'); + document.body.appendChild(container); + const onStateChange = jest.fn(); + autocomplete({ + id: 'autocomplete', + detachedMediaQuery: '', + container, + onStateChange, + }); + + const searchButton = container.querySelector( + '.aa-DetachedSearchButton' + )!; + + // Open detached overlay + searchButton.click(); + + // Type a query in the focused input + await waitFor(() => { + const input = document.querySelector('.aa-Input')!; + + expect(document.querySelector('.aa-DetachedOverlay')).toBeInTheDocument(); + expect(document.body).toHaveClass('aa-Detached'); + expect(input).toHaveFocus(); + + fireEvent.input(input, { target: { value: 'a' } }); + }); + + // Wait for the panel to open + await waitFor(() => { + expect( + document.querySelector('.aa-Panel') + ).toBeInTheDocument(); + }); + + const cancelButton = document.querySelector( + '.aa-DetachedCancelButton' + )!; + + // Close detached overlay + cancelButton.click(); + + // The detached overlay should close + await waitFor(() => { + expect( + document.querySelector('.aa-DetachedOverlay') + ).not.toBeInTheDocument(); + expect(document.body).not.toHaveClass('aa-Detached'); + }); + + // The `query` should still be present + expect(onStateChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + state: expect.objectContaining({ query: 'a' }), + }) + ); + + // The detached search `button` should contain the `query` + expect( + container.querySelector('.aa-DetachedSearchButtonQuery') + ).toHaveTextContent('a'); + + // The detached search `button` placeholder should be hidden when `query` exists + expect( + container.querySelector('.aa-DetachedSearchButtonPlaceholder') + ).toHaveAttribute('hidden'); + }); + + test('reflects the initial `query` in the detached search `button`', async () => { + const container = document.createElement('div'); + document.body.appendChild(container); + autocomplete({ + id: 'autocomplete', + detachedMediaQuery: '', + container, + initialState: { + query: 'a', + }, + }); + + await waitFor(() => { + // The detached search `button` should have the initial `query` + expect( + container.querySelector('.aa-DetachedSearchButtonQuery') + ).toHaveTextContent('a'); + + // The detached search `button` placeholder should be hidden when `query` exists + expect( + container.querySelector('.aa-DetachedSearchButtonPlaceholder') + ).toHaveAttribute('hidden'); + }); + }); }); diff --git a/packages/autocomplete-js/src/autocomplete.ts b/packages/autocomplete-js/src/autocomplete.ts index 57296d63a..ce0ef2e9e 100644 --- a/packages/autocomplete-js/src/autocomplete.ts +++ b/packages/autocomplete-js/src/autocomplete.ts @@ -393,8 +393,6 @@ export function autocomplete( props.value.core.environment.document.body.classList.remove( 'aa-Detached' ); - autocomplete.value.setQuery(''); - autocomplete.value.refresh(); } }); } diff --git a/packages/autocomplete-js/src/createAutocompleteDom.ts b/packages/autocomplete-js/src/createAutocompleteDom.ts index 7803d71d4..79fdb1264 100644 --- a/packages/autocomplete-js/src/createAutocompleteDom.ts +++ b/packages/autocomplete-js/src/createAutocompleteDom.ts @@ -137,6 +137,16 @@ export function createAutocompleteDom({ ...panelProps, }); + const detachedSearchButtonQuery = createDomElement('div', { + class: classNames.detachedSearchButtonQuery, + textContent: state.query, + }); + const detachedSearchButtonPlaceholder = createDomElement('div', { + class: classNames.detachedSearchButtonPlaceholder, + hidden: Boolean(state.query), + textContent: placeholder, + }); + if (__TEST__) { setProperties(panel, { 'data-testid': 'panel', @@ -148,17 +158,17 @@ export function createAutocompleteDom({ class: classNames.detachedSearchButtonIcon, children: [SearchIcon({ environment })], }); - const detachedSearchButtonPlaceholder = createDomElement('div', { - class: classNames.detachedSearchButtonPlaceholder, - textContent: placeholder, - }); const detachedSearchButton = createDomElement('button', { type: 'button', class: classNames.detachedSearchButton, onClick() { setIsModalOpen(true); }, - children: [detachedSearchButtonIcon, detachedSearchButtonPlaceholder], + children: [ + detachedSearchButtonIcon, + detachedSearchButtonPlaceholder, + detachedSearchButtonQuery, + ], }); const detachedCancelButton = createDomElement('button', { type: 'button', @@ -188,6 +198,8 @@ export function createAutocompleteDom({ return { detachedContainer, detachedOverlay, + detachedSearchButtonQuery, + detachedSearchButtonPlaceholder, inputWrapper, input, root, diff --git a/packages/autocomplete-js/src/getDefaultOptions.ts b/packages/autocomplete-js/src/getDefaultOptions.ts index 556bf6b54..ef864a4aa 100644 --- a/packages/autocomplete-js/src/getDefaultOptions.ts +++ b/packages/autocomplete-js/src/getDefaultOptions.ts @@ -35,6 +35,7 @@ const defaultClassNames: AutocompleteClassNames = { detachedSearchButton: 'aa-DetachedSearchButton', detachedSearchButtonIcon: 'aa-DetachedSearchButtonIcon', detachedSearchButtonPlaceholder: 'aa-DetachedSearchButtonPlaceholder', + detachedSearchButtonQuery: 'aa-DetachedSearchButtonQuery', form: 'aa-Form', input: 'aa-Input', inputWrapper: 'aa-InputWrapper', diff --git a/packages/autocomplete-js/src/render.tsx b/packages/autocomplete-js/src/render.tsx index f33bb337e..8b50cc80f 100644 --- a/packages/autocomplete-js/src/render.tsx +++ b/packages/autocomplete-js/src/render.tsx @@ -58,6 +58,12 @@ export function renderSearchBox({ setProperties(dom.label, { hidden: state.status === 'stalled' }); setProperties(dom.loadingIndicator, { hidden: state.status !== 'stalled' }); setProperties(dom.clearButton, { hidden: !state.query }); + setProperties(dom.detachedSearchButtonQuery, { + textContent: state.query, + }); + setProperties(dom.detachedSearchButtonPlaceholder, { + hidden: Boolean(state.query), + }); } export function renderPanel( diff --git a/packages/autocomplete-js/src/types/AutocompleteClassNames.ts b/packages/autocomplete-js/src/types/AutocompleteClassNames.ts index 1d19ef727..972a73d8a 100644 --- a/packages/autocomplete-js/src/types/AutocompleteClassNames.ts +++ b/packages/autocomplete-js/src/types/AutocompleteClassNames.ts @@ -6,6 +6,7 @@ export type AutocompleteClassNames = { detachedSearchButton: string; detachedSearchButtonIcon: string; detachedSearchButtonPlaceholder: string; + detachedSearchButtonQuery: string; form: string; input: string; inputWrapper: string; diff --git a/packages/autocomplete-js/src/types/AutocompleteDom.ts b/packages/autocomplete-js/src/types/AutocompleteDom.ts index b398df195..71eb58ea1 100644 --- a/packages/autocomplete-js/src/types/AutocompleteDom.ts +++ b/packages/autocomplete-js/src/types/AutocompleteDom.ts @@ -10,4 +10,6 @@ export type AutocompleteDom = { panel: HTMLDivElement; detachedContainer: HTMLDivElement; detachedOverlay: HTMLDivElement; + detachedSearchButtonQuery: HTMLDivElement; + detachedSearchButtonPlaceholder: HTMLDivElement; }; diff --git a/packages/autocomplete-theme-classic/src/theme.scss b/packages/autocomplete-theme-classic/src/theme.scss index 076457f68..67f794fc3 100644 --- a/packages/autocomplete-theme-classic/src/theme.scss +++ b/packages/autocomplete-theme-classic/src/theme.scss @@ -930,10 +930,23 @@ body { color: rgba(var(--aa-primary-color-rgb), 1); cursor: initial; display: flex; + flex-shrink: 0; height: 100%; justify-content: center; width: calc(var(--aa-icon-size) + var(--aa-spacing)); } + @at-root .aa-DetachedSearchButtonQuery { + color: rgba(var(--aa-text-color-rgb), 1); + line-height: 1.25em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + @at-root .aa-DetachedSearchButtonPlaceholder { + &[hidden] { + display: none; + } + } } // Remove scroll on `body` diff --git a/yarn.lock b/yarn.lock index 11a3685c3..9d2fcbf2f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7554,15 +7554,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001179, caniuse-lite@^1.0.30001286: - version "1.0.30001303" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001303.tgz#9b168e4f43ccfc372b86f4bc5a551d9b909c95c9" - integrity sha512-/Mqc1oESndUNszJP0kx0UaQU9kEv9nNtJ7Kn8AdA0mNnH8eR1cj0kG+NbNuC1Wq/b21eA8prhKRA3bbkjONegQ== - -caniuse-lite@^1.0.30001161, caniuse-lite@^1.0.30001400: - version "1.0.30001439" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz#ab7371faeb4adff4b74dad1718a6fd122e45d9cb" - integrity sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001161, caniuse-lite@^1.0.30001179, caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001400: + version "1.0.30001458" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001458.tgz" + integrity sha512-lQ1VlUUq5q9ro9X+5gOEyH7i3vm+AYVT1WDCVB69XOZ17KZRhnZ9J0Sqz7wTHQaLBJccNCHq8/Ww5LlOIZbB0w== capital-case@^1.0.3, capital-case@^1.0.4: version "1.0.4"