Skip to content

Commit

Permalink
feat(js): introduce Autocomplete Touch (#379)
Browse files Browse the repository at this point in the history
This introduces a brand new Autocomplete experience on touch devices (mobiles, tablets, etc.). This new experience is available via a media query so that it matches when it's triggered given your website requirements.
  • Loading branch information
francoischalifour committed Dec 12, 2020
1 parent 3e2f87b commit 5cfbdf2
Show file tree
Hide file tree
Showing 46 changed files with 733 additions and 290 deletions.
7 changes: 5 additions & 2 deletions .stylelintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
"selector-class-pattern": ["^aa-[A-Za-z0-9-]*$"],
"prettier/prettier": true,
"max-nesting-depth": [
1,
2,
{
"ignore": ["pseudo-classes"],
"ignoreAtRules": ["media"]
}
],
"rule-empty-line-before": "always",
"rule-empty-line-before": [
"always",
{ "ignore": ["after-comment", "first-nested", "inside-block"] }
],
"plugin/no-unsupported-browser-features": [
null,
{
Expand Down
2 changes: 1 addition & 1 deletion bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
{
"path": "packages/autocomplete-js/dist/umd/index.production.js",
"maxSize": "9.75 kB"
"maxSize": "10 kB"
},
{
"path": "packages/autocomplete-preset-algolia/dist/umd/index.production.js",
Expand Down
1 change: 1 addition & 0 deletions examples/js/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const querySuggestionsPlugin = createQuerySuggestionsPlugin({

autocomplete({
container: '#autocomplete',
placeholder: 'Search',
openOnFocus: true,
plugins: [
algoliaInsightsPlugin,
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,11 @@
"jest-diff": "26.6.2",
"jest-watch-typeahead": "0.6.1",
"lerna": "3.22.1",
"postcss": "8.1.6",
"postcss": "8.1.8",
"postcss-color-rgb": "2.0.0",
"postcss-comment": "2.0.0",
"postcss-node-sass": "2.1.8",
"postcss-preset-env": "6.7.0",
"prettier": "2.2.1",
"rollup": "2.34.1",
"rollup-plugin-babel": "4.4.0",
Expand Down
81 changes: 44 additions & 37 deletions packages/autocomplete-js/src/__tests__/autocomplete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,8 @@ describe('autocomplete-js', () => {
role="search"
>
<div
class="aa-InputWrapper"
class="aa-InputWrapperPrefix"
>
<input
aria-autocomplete="both"
aria-labelledby="autocomplete-label"
autocapitalize="off"
autocomplete="off"
autocorrect="off"
class="aa-Input"
id="autocomplete-input"
maxlength="512"
placeholder=""
spellcheck="false"
type="search"
/>
<label
class="aa-Label"
for="autocomplete-input"
Expand Down Expand Up @@ -86,31 +73,8 @@ describe('autocomplete-js', () => {
</svg>
</button>
</label>
<button
class="aa-ResetButton"
hidden=""
type="reset"
>
<svg
class="aa-ResetIcon"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M10 10l5.09-5.09L10 10l5.09 5.09L10 10zm0 0L4.91 4.91 10 10l-5.09 5.09L10 10z"
fill="none"
fill-rule="evenodd"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.4"
/>
</svg>
</button>
<div
class="aa-LoadingIndicator"
hidden=""
>
<svg
class="aa-LoadingIcon"
Expand Down Expand Up @@ -143,6 +107,49 @@ describe('autocomplete-js', () => {
</svg>
</div>
</div>
<div
class="aa-InputWrapper"
>
<input
aria-autocomplete="both"
aria-labelledby="autocomplete-label"
autocapitalize="off"
autocomplete="off"
autocorrect="off"
class="aa-Input"
enterkeyhint="search"
id="autocomplete-input"
maxlength="512"
placeholder=""
spellcheck="false"
type="search"
/>
</div>
<div
class="aa-InputWrapperSuffix"
>
<button
class="aa-ResetButton"
type="reset"
>
<svg
class="aa-ResetIcon"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M10 10l5.09-5.09L10 10l5.09 5.09L10 10zm0 0L4.91 4.91 10 10l-5.09 5.09L10 10z"
fill="none"
fill-rule="evenodd"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.4"
/>
</svg>
</button>
</div>
</form>
</div>
</div>
Expand Down
76 changes: 54 additions & 22 deletions packages/autocomplete-js/src/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { createEffectWrapper } from './createEffectWrapper';
import { createReactiveWrapper } from './createReactiveWrapper';
import { getDefaultOptions } from './getDefaultOptions';
import { getPanelPositionStyle } from './getPanelPositionStyle';
import { render } from './render';
import { renderPanel, renderSearchBox } from './render';
import {
AutocompleteApi,
AutocompleteOptions,
Expand Down Expand Up @@ -50,6 +50,9 @@ export function autocomplete<TItem extends BaseItem>(
status: 'idle',
...props.value.core.initialState,
});
const isTouch = reactive(
() => window.matchMedia(props.value.renderer.touchMediaQuery).matches
);

const propGetters: AutocompletePropGetters<TItem> = {
getEnvironmentProps: props.value.renderer.getEnvironmentProps,
Expand All @@ -73,6 +76,8 @@ export function autocomplete<TItem extends BaseItem>(

const dom = reactive(() =>
createAutocompleteDom({
placeholder: props.value.core.placeholder,
isTouch: isTouch.value,
state: lastStateRef.current,
autocomplete: autocomplete.value,
classNames: props.value.renderer.classNames,
Expand All @@ -83,25 +88,33 @@ export function autocomplete<TItem extends BaseItem>(

function setPanelPosition() {
setProperties(dom.value.panel, {
style: getPanelPositionStyle({
panelPlacement: props.value.renderer.panelPlacement,
container: dom.value.root,
form: dom.value.form,
environment: props.value.core.environment,
}),
style: isTouch.value
? {}
: getPanelPositionStyle({
panelPlacement: props.value.renderer.panelPlacement,
container: dom.value.root,
form: dom.value.form,
environment: props.value.core.environment,
}),
});
}

function runRender() {
render(props.value.renderer.render, {
const renderProps = {
isTouch: isTouch.value,
state: lastStateRef.current,
autocomplete: autocomplete.value,
propGetters,
dom: dom.value,
classNames: props.value.renderer.classNames,
panelContainer: props.value.renderer.panelContainer,
panelContainer: isTouch.value
? dom.value.touchOverlay
: props.value.renderer.panelContainer,
autocompleteScopeApi,
});
};

renderSearchBox(renderProps);
renderPanel(props.value.renderer.render, renderProps);
}

function scheduleRender(state: AutocompleteState<TItem>) {
Expand Down Expand Up @@ -135,6 +148,27 @@ export function autocomplete<TItem extends BaseItem>(
};
});

runEffect(() => {
const panelContainerElement = isTouch.value
? document.body
: props.value.renderer.panelContainer;
const panelElement = isTouch.value
? dom.value.touchOverlay
: dom.value.panel;

if (isTouch.value && lastStateRef.current.isOpen) {
dom.value.openTouchOverlay();
}

scheduleRender(lastStateRef.current);

return () => {
if (panelContainerElement.contains(panelElement)) {
panelContainerElement.removeChild(panelElement);
}
};
});

runEffect(() => {
const containerElement = props.value.renderer.container;
invariant(
Expand All @@ -148,17 +182,6 @@ export function autocomplete<TItem extends BaseItem>(
};
});

runEffect(() => {
const panelContainerElement = props.value.renderer.panelContainer;
scheduleRender(lastStateRef.current);

return () => {
if (panelContainerElement.contains(dom.value.panel)) {
panelContainerElement.removeChild(dom.value.panel);
}
};
});

runEffect(() => {
const debouncedRender = debounce<{
state: AutocompleteState<TItem>;
Expand All @@ -185,7 +208,16 @@ export function autocomplete<TItem extends BaseItem>(

runEffect(() => {
const onResize = debounce<Event>(() => {
requestAnimationFrame(setPanelPosition);
const previousIsTouch = isTouch.value;
isTouch.value = window.matchMedia(
props.value.renderer.touchMediaQuery
).matches;

if (previousIsTouch !== isTouch.value) {
update({});
} else {
requestAnimationFrame(setPanelPosition);
}
}, 20);
window.addEventListener('resize', onResize);

Expand Down
20 changes: 20 additions & 0 deletions packages/autocomplete-js/src/components/Element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { setProperties } from '../utils';

type ElementProps = Record<string, unknown> & {
children?: Node[];
};

export function Element<KParam extends keyof HTMLElementTagNameMap>(
tagName: keyof HTMLElementTagNameMap | HTMLElement,
{ children = [], ...props }: ElementProps
): HTMLElementTagNameMap[KParam] {
const element =
typeof tagName === 'string'
? document.createElement<KParam>(tagName as any)
: tagName;
setProperties(element, props);

element.append(...children);

return element as any;
}
11 changes: 5 additions & 6 deletions packages/autocomplete-js/src/components/Form.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { AutocompleteApi as AutocompleteCoreApi } from '@algolia/autocomplete-core';

import { Component, WithClassNames } from '../types/Component';
import { concatClassNames, setProperties } from '../utils';
import { concatClassNames } from '../utils';

import { Element } from './Element';

type FormProps = WithClassNames<
ReturnType<AutocompleteCoreApi<any>['getFormProps']>
Expand All @@ -11,11 +13,8 @@ export const Form: Component<FormProps, HTMLFormElement> = ({
classNames,
...props
}) => {
const element = document.createElement('form');
setProperties(element, {
return Element<'form'>('form', {
...props,
class: concatClassNames(['aa-Form', classNames.form]),
class: concatClassNames('aa-Form', classNames.form),
});

return element;
};
21 changes: 16 additions & 5 deletions packages/autocomplete-js/src/components/Input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import {

import { AutocompletePropGetters, AutocompleteState } from '../types';
import { Component, WithClassNames } from '../types/Component';
import { concatClassNames, setProperties } from '../utils';
import { concatClassNames } from '../utils';

import { Element } from './Element';

type InputProps = WithClassNames<{
onTouchEscape?(): void;
state: AutocompleteState<any>;
getInputProps: AutocompletePropGetters<any>['getInputProps'];
getInputPropsCore: AutocompleteCoreApi<any>['getInputProps'];
Expand All @@ -20,6 +23,7 @@ export const Input: Component<InputProps, HTMLInputElement> = ({
getInputPropsCore,
state,
autocompleteScopeApi,
onTouchEscape,
}) => {
const element = document.createElement('input');
const inputProps = getInputProps({
Expand All @@ -28,10 +32,17 @@ export const Input: Component<InputProps, HTMLInputElement> = ({
inputElement: element,
...autocompleteScopeApi,
});
setProperties(element, {

return Element<'input'>(element, {
...inputProps,
class: concatClassNames(['aa-Input', classNames.input]),
});
onKeyDown(event: KeyboardEvent) {
if (onTouchEscape && event.key === 'Escape') {
onTouchEscape();
return;
}

return element;
inputProps.onKeyDown(event);
},
class: concatClassNames('aa-Input', classNames.input),
});
};

0 comments on commit 5cfbdf2

Please sign in to comment.