diff --git a/packages/autocomplete-js/src/__tests__/api.test.ts b/packages/autocomplete-js/src/__tests__/api.test.ts index 9bf013ba5..cbf0c05a7 100644 --- a/packages/autocomplete-js/src/__tests__/api.test.ts +++ b/packages/autocomplete-js/src/__tests__/api.test.ts @@ -259,6 +259,79 @@ describe('api', () => { ); }); + test('correctly merges DOM Element options', async () => { + let inputElement: HTMLInputElement; + const onStateChange = jest.fn(); + const container = document.createElement('div'); + document.body.appendChild(container); + const panelContainer = document.createElement('div'); + document.body.appendChild(panelContainer); + const panelContainer2 = document.createElement('div'); + document.body.appendChild(panelContainer2); + + const { update } = autocomplete<{ label: string }>({ + container, + onStateChange, + panelContainer, + shouldPanelOpen: () => true, + openOnFocus: true, + getSources() { + return [ + { + sourceId: 'testSource', + getItems() { + return []; + }, + templates: { + item({ item }) { + return item.label; + }, + noResults() { + return 'No results template'; + }, + }, + }, + ]; + }, + }); + + // Focusing the input should render the panel + inputElement = container.querySelector('.aa-Input'); + inputElement.focus(); + + // Focusing the input should open the panel + expect(onStateChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + state: expect.objectContaining({ + isOpen: true, + }), + }) + ); + + // Panel is rendered into the original container + await waitFor(() => { + expect( + panelContainer.querySelector('.aa-Panel') + ).toHaveTextContent('No results template'); + }); + + // Update options (set `panelContainer` to a different element) + update({ + panelContainer: panelContainer2, + }); + + // Focusing the input should render the panel + inputElement = container.querySelector('.aa-Input'); + inputElement.focus(); + + // Panel is rendered into the new container + await waitFor(() => { + expect( + panelContainer2.querySelector('.aa-Panel') + ).toHaveTextContent('No results template'); + }); + }); + test('overrides the default id', async () => { const container = document.createElement('div'); diff --git a/packages/autocomplete-js/src/utils/__tests__/mergeDeep.test.ts b/packages/autocomplete-js/src/utils/__tests__/mergeDeep.test.ts new file mode 100644 index 000000000..b47308b87 --- /dev/null +++ b/packages/autocomplete-js/src/utils/__tests__/mergeDeep.test.ts @@ -0,0 +1,46 @@ +import { mergeDeep } from '../mergeDeep'; + +describe('mergeDeep', () => { + test('arrays', () => { + expect(mergeDeep({ key: ['test1'] }, { key: ['test2'] })).toEqual({ + key: ['test1', 'test2'], + }); + }); + + test('plain objects', () => { + expect( + mergeDeep({ key: { test1: 'value1' } }, { key: { test2: 'value2' } }) + ).toEqual({ + key: { test1: 'value1', test2: 'value2' }, + }); + }); + + test('HTML Elements', () => { + expect( + mergeDeep( + { key: document.createElement('div') }, + { key: document.createElement('span') } + ) + ).toEqual({ + key: document.createElement('span'), + }); + }); + + test('primitives', () => { + expect(mergeDeep({ key: 1 }, { key: 2 })).toEqual({ + key: 2, + }); + }); + + test('null', () => { + expect(mergeDeep({ key: 1 }, { key: null })).toEqual({ + key: null, + }); + }); + + test('undefined', () => { + expect(mergeDeep({ key: 1 }, { key: undefined })).toEqual({ + key: undefined, + }); + }); +}); diff --git a/packages/autocomplete-js/src/utils/mergeDeep.ts b/packages/autocomplete-js/src/utils/mergeDeep.ts index ac464e88e..bfe148c6c 100644 --- a/packages/autocomplete-js/src/utils/mergeDeep.ts +++ b/packages/autocomplete-js/src/utils/mergeDeep.ts @@ -1,4 +1,7 @@ -const isObject = (value: unknown) => value && typeof value === 'object'; +const isPlainObject = (value: unknown) => + value && + typeof value === 'object' && + Object.prototype.toString.call(value) === '[object Object]'; export function mergeDeep(...values: any[]) { return values.reduce((acc, current) => { @@ -8,7 +11,7 @@ export function mergeDeep(...values: any[]) { if (Array.isArray(accValue) && Array.isArray(currentValue)) { acc[key] = accValue.concat(...currentValue); - } else if (isObject(accValue) && isObject(currentValue)) { + } else if (isPlainObject(accValue) && isPlainObject(currentValue)) { acc[key] = mergeDeep(accValue, currentValue); } else { acc[key] = currentValue;