Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(js): sync detached mode open state #556

Merged
merged 3 commits into from
Apr 30, 2021
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