Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions packages/autocomplete-js/src/__tests__/detached.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { fireEvent, waitFor } from '@testing-library/dom';

import { autocomplete } from '../autocomplete';

describe('detached', () => {
const originalMatchMedia = window.matchMedia;

beforeAll(() => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn((query) => ({
matches: true,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
});

afterAll(() => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: originalMatchMedia,
});
});

test('closes after onSelect', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
autocomplete<{ label: string }>({
id: 'autocomplete',
detachedMediaQuery: '',
container,
getSources() {
return [
{
sourceId: 'testSource',
getItems() {
return [
{ label: 'Item 1' },
{ label: 'Item 2' },
{ label: 'Item 3' },
];
},
templates: {
item({ item }) {
return item.label;
},
},
},
];
},
});

const searchButton = container.querySelector<HTMLButtonElement>(
'.aa-DetachedSearchButton'
);

// Open detached overlay
searchButton.click();

await waitFor(() => {
const input = document.querySelector<HTMLInputElement>('.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<HTMLElement>('.aa-Panel')
).toBeInTheDocument();
});

const firstItem = document.querySelector<HTMLLIElement>(
'#autocomplete-item-0'
);

// Select the first item
firstItem.click();

// The detached overlay should close
await waitFor(() => {
expect(
document.querySelector('.aa-DetachedOverlay')
).not.toBeInTheDocument();
expect(document.body).not.toHaveClass('aa-Detached');
});
});
});
43 changes: 37 additions & 6 deletions packages/autocomplete-js/src/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,28 @@ export function autocomplete<TItem extends BaseItem>(
const autocomplete = reactive(() =>
createAutocomplete<TItem>({
...props.value.core,
onStateChange(options) {
hasNoResultsSourceTemplateRef.current = options.state.collections.some(
onStateChange(params) {
if (
isDetached.value &&
params.prevState.isOpen !== params.state.isOpen
) {
setIsModalOpen(params.state.isOpen);
}

hasNoResultsSourceTemplateRef.current = params.state.collections.some(
(collection) =>
(collection.source as AutocompleteSource<TItem>).templates.noResults
);
onStateChangeRef.current?.(options as any);
props.value.core.onStateChange?.(options as any);
onStateChangeRef.current?.(params as any);
props.value.core.onStateChange?.(params as any);
},
shouldPanelOpen:
optionsRef.current.shouldPanelOpen ||
(({ state }) => {
if (isDetached.value) {
return true;
}

const hasItems = getItemsCount(state) > 0;

if (!props.value.core.openOnFocus && !state.query) {
Expand Down Expand Up @@ -111,6 +122,7 @@ export function autocomplete<TItem extends BaseItem>(
isDetached: isDetached.value,
placeholder: props.value.core.placeholder,
propGetters,
setIsModalOpen,
state: lastStateRef.current,
})
);
Expand Down Expand Up @@ -188,7 +200,7 @@ export function autocomplete<TItem extends BaseItem>(
: dom.value.panel;

if (isDetached.value && lastStateRef.current.isOpen) {
dom.value.openDetachedOverlay();
setIsModalOpen(true);
}

scheduleRender(lastStateRef.current);
Expand Down Expand Up @@ -221,7 +233,7 @@ export function autocomplete<TItem extends BaseItem>(
// positioned. The layout might have shifted vertically for instance.
// It's therefore safer to re-calculate the panel position before opening
// it again.
if (state.isOpen && !prevState.isOpen) {
if (!isDetached.value && state.isOpen && !prevState.isOpen) {
setPanelPosition();
}

Expand Down Expand Up @@ -325,6 +337,25 @@ export function autocomplete<TItem extends BaseItem>(
});
}

function setIsModalOpen(value: boolean) {
const prevValue = document.body.contains(dom.value.detachedOverlay);

if (value === prevValue) {
return;
}

if (value) {
document.body.appendChild(dom.value.detachedOverlay);
document.body.classList.add('aa-Detached');
dom.value.input.focus();
} else {
document.body.removeChild(dom.value.detachedOverlay);
document.body.classList.remove('aa-Detached');
autocomplete.value.setQuery('');
autocomplete.value.refresh();
}
}

return {
...autocompleteScopeApi,
update,
Expand Down
33 changes: 7 additions & 26 deletions packages/autocomplete-js/src/createAutocompleteDom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,20 @@ type CreateDomProps<TItem extends BaseItem> = {
isDetached: boolean;
placeholder?: string;
propGetters: AutocompletePropGetters<TItem>;
setIsModalOpen(value: boolean): void;
state: AutocompleteState<TItem>;
};

type CreateAutocompleteDomReturn = AutocompleteDom & {
openDetachedOverlay(): void;
};

export function createAutocompleteDom<TItem extends BaseItem>({
autocomplete,
autocompleteScopeApi,
classNames,
isDetached,
placeholder = 'Search',
propGetters,
setIsModalOpen,
state,
}: CreateDomProps<TItem>): CreateAutocompleteDomReturn {
function onDetachedOverlayClose() {
autocomplete.setQuery('');
autocomplete.setIsOpen(false);
autocomplete.refresh();
document.body.classList.remove('aa-Detached');
}

}: CreateDomProps<TItem>): AutocompleteDom {
const rootProps = propGetters.getRootProps({
state,
props: autocomplete.getRootProps({}),
Expand All @@ -63,8 +54,7 @@ export function createAutocompleteDom<TItem extends BaseItem>({
class: classNames.detachedOverlay,
children: [detachedContainer],
onMouseDown() {
document.body.removeChild(detachedOverlay);
onDetachedOverlayClose();
setIsModalOpen(false);
},
});

Expand Down Expand Up @@ -103,8 +93,7 @@ export function createAutocompleteDom<TItem extends BaseItem>({
autocompleteScopeApi,
onDetachedEscape: isDetached
? () => {
document.body.removeChild(detachedOverlay);
onDetachedOverlayClose();
setIsModalOpen(false);
}
: undefined,
});
Expand Down Expand Up @@ -148,12 +137,6 @@ export function createAutocompleteDom<TItem extends BaseItem>({
});
}

function openDetachedOverlay() {
document.body.appendChild(detachedOverlay);
document.body.classList.add('aa-Detached');
input.focus();
}

if (isDetached) {
const detachedSearchButtonIcon = createDomElement('div', {
class: classNames.detachedSearchButtonIcon,
Expand All @@ -167,16 +150,15 @@ export function createAutocompleteDom<TItem extends BaseItem>({
class: classNames.detachedSearchButton,
onClick(event: MouseEvent) {
event.preventDefault();
openDetachedOverlay();
setIsModalOpen(true);
},
children: [detachedSearchButtonIcon, detachedSearchButtonPlaceholder],
});
const detachedCancelButton = createDomElement('button', {
class: classNames.detachedCancelButton,
textContent: 'Cancel',
onClick() {
document.body.removeChild(detachedOverlay);
onDetachedOverlayClose();
setIsModalOpen(false);
},
});
const detachedFormContainer = createDomElement('div', {
Expand All @@ -191,7 +173,6 @@ export function createAutocompleteDom<TItem extends BaseItem>({
}

return {
openDetachedOverlay,
detachedContainer,
detachedOverlay,
inputWrapper,
Expand Down