Skip to content

Commit

Permalink
feat(js): sync detached mode open state (#556)
Browse files Browse the repository at this point in the history
  • Loading branch information
francoischalifour authored Apr 30, 2021
1 parent 32678dd commit 1239b63
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 32 deletions.
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');
});
});
});
42 changes: 36 additions & 6 deletions packages/autocomplete-js/src/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,21 @@ export function autocomplete<TItem extends BaseItem>(
const autocomplete = reactive(() =>
createAutocomplete<TItem>({
...props.value.core,
onStateChange(options) {
hasNoResultsSourceTemplateRef.current = options.state.collections.some(
onStateChange(params) {
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 +115,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 +193,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 @@ -217,11 +222,15 @@ export function autocomplete<TItem extends BaseItem>(
}, 0);

onStateChangeRef.current = ({ state, prevState }) => {
if (isDetached.value && prevState.isOpen !== state.isOpen) {
setIsModalOpen(state.isOpen);
}

// The outer DOM might have changed since the last time the panel was
// 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 +334,27 @@ export function autocomplete<TItem extends BaseItem>(
});
}

function setIsModalOpen(value: boolean) {
requestAnimationFrame(() => {
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
36 changes: 10 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,8 @@ export function createAutocompleteDom<TItem extends BaseItem>({
class: classNames.detachedOverlay,
children: [detachedContainer],
onMouseDown() {
document.body.removeChild(detachedOverlay);
onDetachedOverlayClose();
setIsModalOpen(false);
autocomplete.setIsOpen(false);
},
});

Expand Down Expand Up @@ -103,8 +94,8 @@ export function createAutocompleteDom<TItem extends BaseItem>({
autocompleteScopeApi,
onDetachedEscape: isDetached
? () => {
document.body.removeChild(detachedOverlay);
onDetachedOverlayClose();
autocomplete.setIsOpen(false);
setIsModalOpen(false);
}
: undefined,
});
Expand Down Expand Up @@ -148,12 +139,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 +152,16 @@ 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();
autocomplete.setIsOpen(false);
setIsModalOpen(false);
},
});
const detachedFormContainer = createDomElement('div', {
Expand All @@ -191,7 +176,6 @@ export function createAutocompleteDom<TItem extends BaseItem>({
}

return {
openDetachedOverlay,
detachedContainer,
detachedOverlay,
inputWrapper,
Expand Down
1 change: 1 addition & 0 deletions packages/autocomplete-js/src/elements/Input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const Input: AutocompleteElement<InputProps, HTMLInputElement> = ({
...inputProps,
onKeyDown(event: KeyboardEvent) {
if (onDetachedEscape && event.key === 'Escape') {
event.preventDefault();
onDetachedEscape();
return;
}
Expand Down

0 comments on commit 1239b63

Please sign in to comment.