diff --git a/packages/autocomplete-js/src/__tests__/panelPlacement.test.ts b/packages/autocomplete-js/src/__tests__/panelPlacement.test.ts index 11795da54..f1c88fd5d 100644 --- a/packages/autocomplete-js/src/__tests__/panelPlacement.test.ts +++ b/packages/autocomplete-js/src/__tests__/panelPlacement.test.ts @@ -1,4 +1,4 @@ -import { waitFor } from '@testing-library/dom'; +import { fireEvent, waitFor } from '@testing-library/dom'; import { autocomplete } from '../autocomplete'; @@ -10,6 +10,7 @@ const LEFT = 11; const RIGHT = 13; const TOP = 17; const WIDTH = 19; +const SCROLL = 100; describe('panelPlacement', () => { let container: HTMLDivElement; @@ -45,6 +46,7 @@ describe('panelPlacement', () => { afterEach(() => { document.body.innerHTML = ''; + fireEvent.scroll(document.body, { target: { scrollTop: 0 } }); }); afterAll(() => { @@ -75,7 +77,37 @@ describe('panelPlacement', () => { await waitFor(() => { expect(document.querySelector('.aa-Panel')).toHaveStyle({ - top: '24px', // TOP + HEIGHT + top: '24px', // TOP + HEIGHT + SCROLL + left: '11px', // LEFT + right: '1890px', // CLIENT_WIDTH - (LEFT + WIDTH) + width: 'unset', + 'max-width': 'unset', + }); + }); + }); + + test('keeps the panel positionned after scrolling', async () => { + autocomplete({ + container, + panelPlacement: 'input-wrapper-width', + initialState: { + isOpen: true, + }, + }); + + fireEvent.scroll(document.body, { target: { scrollTop: SCROLL } }); + + // Mock `getBoundingClientRect` for elements used in the panel placement calculation + document.querySelector( + '.aa-Form' + ).getBoundingClientRect = mockedGetBoundingClientRect; + document.querySelector( + '.aa-Autocomplete' + ).getBoundingClientRect = mockedGetBoundingClientRect; + + await waitFor(() => { + expect(document.querySelector('.aa-Panel')).toHaveStyle({ + top: '124px', // TOP + HEIGHT + SCROLL left: '11px', // LEFT right: '1890px', // CLIENT_WIDTH - (LEFT + WIDTH) width: 'unset', @@ -105,7 +137,34 @@ describe('panelPlacement', () => { await waitFor(() => { expect(document.querySelector('.aa-Panel')).toHaveStyle({ - top: '24px', // TOP + HEIGHT + top: '24px', // TOP + HEIGHT + SCROLL + left: '11px', // LEFT + }); + }); + }); + + test('keeps the panel positionned after scrolling', async () => { + autocomplete({ + container, + panelPlacement: 'start', + initialState: { + isOpen: true, + }, + }); + + fireEvent.scroll(document.body, { target: { scrollTop: SCROLL } }); + + // Mock `getBoundingClientRect` for elements used in the panel placement calculation + document.querySelector( + '.aa-Form' + ).getBoundingClientRect = mockedGetBoundingClientRect; + document.querySelector( + '.aa-Autocomplete' + ).getBoundingClientRect = mockedGetBoundingClientRect; + + await waitFor(() => { + expect(document.querySelector('.aa-Panel')).toHaveStyle({ + top: '124px', // TOP + HEIGHT + SCROLL left: '11px', // LEFT }); }); @@ -132,7 +191,34 @@ describe('panelPlacement', () => { await waitFor(() => { expect(document.querySelector('.aa-Panel')).toHaveStyle({ - top: '24px', // TOP + HEIGHT + top: '24px', // TOP + HEIGHT + SCROLL + right: '1890px', // CLIENT_WIDTH - (LEFT + WIDTH) + }); + }); + }); + + test('keeps the panel positionned after scrolling', async () => { + autocomplete({ + container, + panelPlacement: 'end', + initialState: { + isOpen: true, + }, + }); + + fireEvent.scroll(document.body, { target: { scrollTop: SCROLL } }); + + // Mock `getBoundingClientRect` for elements used in the panel placement calculation + document.querySelector( + '.aa-Form' + ).getBoundingClientRect = mockedGetBoundingClientRect; + document.querySelector( + '.aa-Autocomplete' + ).getBoundingClientRect = mockedGetBoundingClientRect; + + await waitFor(() => { + expect(document.querySelector('.aa-Panel')).toHaveStyle({ + top: '124px', // TOP + HEIGHT + SCROLL right: '1890px', // CLIENT_WIDTH - (LEFT + WIDTH) }); }); @@ -159,7 +245,37 @@ describe('panelPlacement', () => { await waitFor(() => { expect(document.querySelector('.aa-Panel')).toHaveStyle({ - top: '24px', // TOP + HEIGHT + top: '24px', // TOP + HEIGHT + SCROLL + left: 0, + right: 0, + width: 'unset', + 'max-width': 'unset', + }); + }); + }); + + test('keeps the panel positionned after scrolling', async () => { + autocomplete({ + container, + panelPlacement: 'full-width', + initialState: { + isOpen: true, + }, + }); + + fireEvent.scroll(document.body, { target: { scrollTop: SCROLL } }); + + // Mock `getBoundingClientRect` for elements used in the panel placement calculation + document.querySelector( + '.aa-Form' + ).getBoundingClientRect = mockedGetBoundingClientRect; + document.querySelector( + '.aa-Autocomplete' + ).getBoundingClientRect = mockedGetBoundingClientRect; + + await waitFor(() => { + expect(document.querySelector('.aa-Panel')).toHaveStyle({ + top: '124px', // TOP + HEIGHT + SCROLL left: 0, right: 0, width: 'unset', @@ -187,7 +303,7 @@ describe('panelPlacement', () => { await waitFor(() => { expect(document.querySelector('.aa-Panel')).toHaveStyle({ - top: '24px', // TOP + HEIGHT + top: '24px', // TOP + HEIGHT + SCROLL left: '11px', // LEFT right: '1890px', // CLIENT_WIDTH - (LEFT + WIDTH) width: 'unset', @@ -223,4 +339,33 @@ describe('panelPlacement', () => { expect(document.querySelector('.aa-Panel')).toHaveStyle({}); }); }); + + test('default value keeps the panel positionned after scrolling', async () => { + autocomplete({ + container, + initialState: { + isOpen: true, + }, + }); + + fireEvent.scroll(document.body, { target: { scrollTop: SCROLL } }); + + // Mock `getBoundingClientRect` for elements used in the panel placement calculation + document.querySelector( + '.aa-Form' + ).getBoundingClientRect = mockedGetBoundingClientRect; + document.querySelector( + '.aa-Autocomplete' + ).getBoundingClientRect = mockedGetBoundingClientRect; + + await waitFor(() => { + expect(document.querySelector('.aa-Panel')).toHaveStyle({ + top: '124px', // TOP + HEIGHT + SCROLL + left: '11px', // LEFT + right: '1890px', // CLIENT_WIDTH - (LEFT + WIDTH) + width: 'unset', + 'max-width': 'unset', + }); + }); + }); }); diff --git a/packages/autocomplete-js/src/__tests__/positioning.test.ts b/packages/autocomplete-js/src/__tests__/positioning.test.ts index 57c51482b..a47bf59b8 100644 --- a/packages/autocomplete-js/src/__tests__/positioning.test.ts +++ b/packages/autocomplete-js/src/__tests__/positioning.test.ts @@ -1,5 +1,5 @@ import { AutocompletePlugin } from '@algolia/autocomplete-core'; -import { waitFor, getByTestId } from '@testing-library/dom'; +import { waitFor, getByTestId, fireEvent } from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; import { autocomplete } from '../'; @@ -130,6 +130,39 @@ describe('Panel positioning', () => { }); }); + test('keeps the panel positionned after scrolling', async () => { + const container = document.createElement('div'); + const panelContainer = document.body; + document.body.appendChild(container); + + autocomplete({ + id: 'autocomplete-0', + container, + panelContainer, + plugins: [querySuggestionsFixturePlugin], + }); + + const root = document.querySelector('.aa-Autocomplete'); + root.getBoundingClientRect = jest.fn().mockReturnValue(rootPosition); + const form = document.querySelector('.aa-Form'); + form.getBoundingClientRect = jest.fn().mockReturnValue(formPosition); + + const input = document.querySelector('.aa-Input'); + userEvent.type(input, 'a'); + + fireEvent.scroll(document.body, { target: { scrollTop: 100 } }); + + await waitFor(() => getByTestId(panelContainer, 'panel')); + + expect(getByTestId(panelContainer, 'panel')).toHaveStyle({ + top: '140px', + left: '300px', + right: '1020px', + }); + + fireEvent.scroll(document.body, { target: { scrollTop: 0 } }); + }); + test('repositions the panel below the root element after a UI change', async () => { const container = document.createElement('div'); const panelContainer = document.body; diff --git a/packages/autocomplete-js/src/getPanelPlacementStyle.ts b/packages/autocomplete-js/src/getPanelPlacementStyle.ts index fad8bc9f9..4d59eb21e 100644 --- a/packages/autocomplete-js/src/getPanelPlacementStyle.ts +++ b/packages/autocomplete-js/src/getPanelPlacementStyle.ts @@ -15,7 +15,10 @@ export function getPanelPlacementStyle({ environment, }: GetPanelPlacementStyleParams) { const containerRect = container.getBoundingClientRect(); - const top = containerRect.top + containerRect.height; + const top = + environment.document.body.scrollTop + + containerRect.top + + containerRect.height; switch (panelPlacement) { case 'start': {