Skip to content

Commit

Permalink
feat(js): introduce panelContainer option
Browse files Browse the repository at this point in the history
  • Loading branch information
francoischalifour committed Nov 21, 2020
1 parent 371fae0 commit 98dfe4b
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 47 deletions.
1 change: 0 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ module.exports = {
'eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }],
'import/extensions': 0,
'@typescript-eslint/camelcase': ['error', { allow: ['__autocomplete_id'] }],
'@typescript-eslint/no-use-before-define': 0,
// Useful to call functions like `nodeItem?.scrollIntoView()`.
'no-unused-expressions': 0,
complexity: 0,
Expand Down
4 changes: 0 additions & 4 deletions packages/autocomplete-js/src/__tests__/autocomplete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,6 @@ describe('autocomplete-js', () => {
</button>
</div>
</form>
<div
class="aa-Panel"
hidden=""
/>
</div>
</div>
`);
Expand Down
80 changes: 48 additions & 32 deletions packages/autocomplete-js/src/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createAutocomplete } from '@algolia/autocomplete-core';
import { createRef } from '@algolia/autocomplete-shared';

import { createAutocompleteDom } from './createAutocompleteDom';
import { createEffectWrapper } from './createEffectWrapper';
Expand All @@ -19,20 +20,25 @@ function defaultRenderer({ root, sections }) {

export function autocomplete<TItem>({
container,
panelContainer = document.body,
render: renderer = defaultRenderer,
panelPlacement = 'input-wrapper-width',
classNames = {},
...props
}: AutocompleteOptions<TItem>): AutocompleteApi<TItem> {
const { runEffect, cleanupEffects } = createEffectWrapper();
const onStateChangeRef = createRef<
| ((params: {
state: AutocompleteState<TItem>;
prevState: AutocompleteState<TItem>;
}) => void)
| undefined
>(undefined);
const autocomplete = createAutocomplete<TItem>({
...props,
onStateChange(options) {
onStateChange(options.state);

if (props.onStateChange) {
props.onStateChange(options);
}
onStateChangeRef.current?.(options as any);
props.onStateChange?.(options);
},
});

Expand All @@ -49,28 +55,6 @@ export function autocomplete<TItem>({
classNames,
});

// This batches state changes to limit DOM mutations.
// Every time we call a setter in `autocomplete-core` (e.g., in `onInput`),
// the core `onStateChange` function is called.
// We don't need to be notified of all these state changes to render.
// As an example:
// - without debouncing: "iphone case" query → 85 renders
// - with debouncing: "iphone case" query → 12 renders
const onStateChange = debounce((state: AutocompleteState<TItem>) => {
render(renderer, {
state,
...autocomplete,
classNames,
root,
form,
input,
inputWrapper,
label,
panel,
resetButton,
});
}, 0);

function setPanelPosition() {
setProperties(panel, {
style: getPanelPositionStyle({
Expand All @@ -82,10 +66,6 @@ export function autocomplete<TItem>({
});
}

requestAnimationFrame(() => {
setPanelPosition();
});

runEffect(() => {
const environmentProps = autocomplete.getEnvironmentProps({
searchBoxElement: form,
Expand All @@ -108,6 +88,38 @@ export function autocomplete<TItem>({
};
});

runEffect(() => {
const panelRoot = getHTMLElement(panelContainer);
const unmountRef = createRef<(() => void) | undefined>(undefined);
// This batches state changes to limit DOM mutations.
// Every time we call a setter in `autocomplete-core` (e.g., in `onInput`),
// the core `onStateChange` function is called.
// We don't need to be notified of all these state changes to render.
// As an example:
// - without debouncing: "iphone case" query → 85 renders
// - with debouncing: "iphone case" query → 12 renders
onStateChangeRef.current = debounce(({ state }) => {
unmountRef.current = render(renderer, {
state,
...autocomplete,
classNames,
panelRoot,
root,
form,
input,
inputWrapper,
label,
panel,
resetButton,
});
}, 0);

return () => {
unmountRef.current?.();
onStateChangeRef.current = undefined;
};
});

runEffect(() => {
const containerElement = getHTMLElement(container);
containerElement.appendChild(root);
Expand All @@ -118,7 +130,7 @@ export function autocomplete<TItem>({
});

runEffect(() => {
const onResize = debounce(() => {
const onResize = debounce<Event>(() => {
setPanelPosition();
}, 100);

Expand All @@ -129,6 +141,10 @@ export function autocomplete<TItem>({
};
});

requestAnimationFrame(() => {
setPanelPosition();
});

return {
setSelectedItemId: autocomplete.setSelectedItemId,
setQuery: autocomplete.setQuery,
Expand Down
18 changes: 12 additions & 6 deletions packages/autocomplete-js/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { setPropertiesWithoutEvents } from './utils';
type RenderProps<TItem> = {
state: AutocompleteState<TItem>;
classNames: AutocompleteClassNames;
panelRoot: HTMLElement;
} & AutocompleteCoreApi<TItem> &
AutocompleteDom;

Expand All @@ -32,26 +33,27 @@ export function render<TItem>(
getListProps,
getItemProps,
classNames,
panelRoot,
root,
input,
panel,
}: RenderProps<TItem>
): void {
): () => void {
setPropertiesWithoutEvents(root, getRootProps());
setPropertiesWithoutEvents(input, getInputProps({ inputElement: input }));

panel.innerHTML = '';

if (!state.isOpen) {
if (root.contains(panel)) {
root.removeChild(panel);
if (panelRoot.contains(panel)) {
panelRoot.removeChild(panel);
}

return;
return () => {};
}

if (!root.contains(panel)) {
root.appendChild(panel);
if (!panelRoot.contains(panel)) {
panelRoot.appendChild(panel);
}

if (state.status === 'stalled') {
Expand Down Expand Up @@ -116,4 +118,8 @@ export function render<TItem>(
panel.appendChild(panelLayoutElement);

renderer({ root: panelLayoutElement, sections, state });

return () => {
panelRoot.removeChild(panel);
};
}
10 changes: 9 additions & 1 deletion packages/autocomplete-js/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,19 @@ export type AutocompleteRenderer<TItem> = (params: {
export interface AutocompleteOptions<TItem>
extends AutocompleteCoreOptions<TItem> {
/**
* The container for the autocomplete search box.
* The container for the Autocomplete search box.
*
* You can either pass a [CSS selector](https://developer.mozilla.org/docs/Web/CSS/CSS_Selectors) or an [Element](https://developer.mozilla.org/docs/Web/API/HTMLElement). The first element matching the provided selector will be used as container.
*/
container: string | HTMLElement;
/**
* The container for the Autocomplete panel.
*
* You can either pass a [CSS selector](https://developer.mozilla.org/docs/Web/CSS/CSS_Selectors) or an [Element](https://developer.mozilla.org/docs/Web/API/HTMLElement). The first element matching the provided selector will be used as container.
*
* @default document.body
*/
panelContainer: string | HTMLElement;
getSources?: (
params: GetSourcesParams<TItem>
) => MaybePromise<Array<AutocompleteSource<TItem>>>;
Expand Down
7 changes: 5 additions & 2 deletions packages/autocomplete-js/src/utils/debounce.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
export function debounce(fn: Function, time: number) {
export function debounce<TParams>(
fn: (...params: TParams[]) => void,
time: number
) {
let timerId: ReturnType<typeof setTimeout> | undefined = undefined;

return function (...args: unknown[]) {
return function (...args: TParams[]) {
if (timerId) {
clearTimeout(timerId);
}
Expand Down
5 changes: 5 additions & 0 deletions packages/autocomplete-shared/src/createRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function createRef<TValue>(initialValue: TValue) {
return {
current: initialValue,
};
}
1 change: 1 addition & 0 deletions packages/autocomplete-shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './createRef';
export * from './warn';
export * from './MaybePromise';
8 changes: 7 additions & 1 deletion packages/website/docs/autocomplete-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,18 @@ import { autocomplete } from '@algolia/autocomplete-js';

> `string | HTMLElement` | **required**
The container for the autocomplete search box. You can either pass a [CSS selector](https://developer.mozilla.org/docs/Web/CSS/CSS_Selectors) or an [Element](https://developer.mozilla.org/docs/Web/API/HTMLElement). The first element matching the provided selector will be used as container.
The container for the Autocomplete search box. You can either pass a [CSS selector](https://developer.mozilla.org/docs/Web/CSS/CSS_Selectors) or an [Element](https://developer.mozilla.org/docs/Web/API/HTMLElement). The first element matching the provided selector will be used as container.

import CreateAutocompleteProps from './partials/createAutocomplete-props.md'

<CreateAutocompleteProps />

### `panelContainer`

> `string | HTMLElement`
The container for the Autocomplete panel. You can either pass a [CSS selector](https://developer.mozilla.org/docs/Web/CSS/CSS_Selectors) or an [Element](https://developer.mozilla.org/docs/Web/API/HTMLElement). The first element matching the provided selector will be used as container.

### `panelPlacement`

> `"start" | "end" | "full-width" | "input-wrapper-width" | defaults to `"input-wrapper-width"`
Expand Down

0 comments on commit 98dfe4b

Please sign in to comment.