Skip to content

Commit 5cfbdf2

Browse files
feat(js): introduce Autocomplete Touch (#379)
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.
1 parent 3e2f87b commit 5cfbdf2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+733
-290
lines changed

.stylelintrc.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@
1414
"selector-class-pattern": ["^aa-[A-Za-z0-9-]*$"],
1515
"prettier/prettier": true,
1616
"max-nesting-depth": [
17-
1,
17+
2,
1818
{
1919
"ignore": ["pseudo-classes"],
2020
"ignoreAtRules": ["media"]
2121
}
2222
],
23-
"rule-empty-line-before": "always",
23+
"rule-empty-line-before": [
24+
"always",
25+
{ "ignore": ["after-comment", "first-nested", "inside-block"] }
26+
],
2427
"plugin/no-unsupported-browser-features": [
2528
null,
2629
{

bundlesize.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
},
77
{
88
"path": "packages/autocomplete-js/dist/umd/index.production.js",
9-
"maxSize": "9.75 kB"
9+
"maxSize": "10 kB"
1010
},
1111
{
1212
"path": "packages/autocomplete-preset-algolia/dist/umd/index.production.js",

examples/js/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const querySuggestionsPlugin = createQuerySuggestionsPlugin({
2929

3030
autocomplete({
3131
container: '#autocomplete',
32+
placeholder: 'Search',
3233
openOnFocus: true,
3334
plugins: [
3435
algoliaInsightsPlugin,

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,11 @@
6868
"jest-diff": "26.6.2",
6969
"jest-watch-typeahead": "0.6.1",
7070
"lerna": "3.22.1",
71-
"postcss": "8.1.6",
71+
"postcss": "8.1.8",
72+
"postcss-color-rgb": "2.0.0",
73+
"postcss-comment": "2.0.0",
7274
"postcss-node-sass": "2.1.8",
75+
"postcss-preset-env": "6.7.0",
7376
"prettier": "2.2.1",
7477
"rollup": "2.34.1",
7578
"rollup-plugin-babel": "4.4.0",

packages/autocomplete-js/src/__tests__/autocomplete.test.ts

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,8 @@ describe('autocomplete-js', () => {
4444
role="search"
4545
>
4646
<div
47-
class="aa-InputWrapper"
47+
class="aa-InputWrapperPrefix"
4848
>
49-
<input
50-
aria-autocomplete="both"
51-
aria-labelledby="autocomplete-label"
52-
autocapitalize="off"
53-
autocomplete="off"
54-
autocorrect="off"
55-
class="aa-Input"
56-
id="autocomplete-input"
57-
maxlength="512"
58-
placeholder=""
59-
spellcheck="false"
60-
type="search"
61-
/>
6249
<label
6350
class="aa-Label"
6451
for="autocomplete-input"
@@ -86,31 +73,8 @@ describe('autocomplete-js', () => {
8673
</svg>
8774
</button>
8875
</label>
89-
<button
90-
class="aa-ResetButton"
91-
hidden=""
92-
type="reset"
93-
>
94-
<svg
95-
class="aa-ResetIcon"
96-
height="20"
97-
viewBox="0 0 20 20"
98-
width="20"
99-
>
100-
<path
101-
d="M10 10l5.09-5.09L10 10l5.09 5.09L10 10zm0 0L4.91 4.91 10 10l-5.09 5.09L10 10z"
102-
fill="none"
103-
fill-rule="evenodd"
104-
stroke="currentColor"
105-
stroke-linecap="round"
106-
stroke-linejoin="round"
107-
stroke-width="1.4"
108-
/>
109-
</svg>
110-
</button>
11176
<div
11277
class="aa-LoadingIndicator"
113-
hidden=""
11478
>
11579
<svg
11680
class="aa-LoadingIcon"
@@ -143,6 +107,49 @@ describe('autocomplete-js', () => {
143107
</svg>
144108
</div>
145109
</div>
110+
<div
111+
class="aa-InputWrapper"
112+
>
113+
<input
114+
aria-autocomplete="both"
115+
aria-labelledby="autocomplete-label"
116+
autocapitalize="off"
117+
autocomplete="off"
118+
autocorrect="off"
119+
class="aa-Input"
120+
enterkeyhint="search"
121+
id="autocomplete-input"
122+
maxlength="512"
123+
placeholder=""
124+
spellcheck="false"
125+
type="search"
126+
/>
127+
</div>
128+
<div
129+
class="aa-InputWrapperSuffix"
130+
>
131+
<button
132+
class="aa-ResetButton"
133+
type="reset"
134+
>
135+
<svg
136+
class="aa-ResetIcon"
137+
height="20"
138+
viewBox="0 0 20 20"
139+
width="20"
140+
>
141+
<path
142+
d="M10 10l5.09-5.09L10 10l5.09 5.09L10 10zm0 0L4.91 4.91 10 10l-5.09 5.09L10 10z"
143+
fill="none"
144+
fill-rule="evenodd"
145+
stroke="currentColor"
146+
stroke-linecap="round"
147+
stroke-linejoin="round"
148+
stroke-width="1.4"
149+
/>
150+
</svg>
151+
</button>
152+
</div>
146153
</form>
147154
</div>
148155
</div>

packages/autocomplete-js/src/autocomplete.ts

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { createEffectWrapper } from './createEffectWrapper';
1010
import { createReactiveWrapper } from './createReactiveWrapper';
1111
import { getDefaultOptions } from './getDefaultOptions';
1212
import { getPanelPositionStyle } from './getPanelPositionStyle';
13-
import { render } from './render';
13+
import { renderPanel, renderSearchBox } from './render';
1414
import {
1515
AutocompleteApi,
1616
AutocompleteOptions,
@@ -50,6 +50,9 @@ export function autocomplete<TItem extends BaseItem>(
5050
status: 'idle',
5151
...props.value.core.initialState,
5252
});
53+
const isTouch = reactive(
54+
() => window.matchMedia(props.value.renderer.touchMediaQuery).matches
55+
);
5356

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

7477
const dom = reactive(() =>
7578
createAutocompleteDom({
79+
placeholder: props.value.core.placeholder,
80+
isTouch: isTouch.value,
7681
state: lastStateRef.current,
7782
autocomplete: autocomplete.value,
7883
classNames: props.value.renderer.classNames,
@@ -83,25 +88,33 @@ export function autocomplete<TItem extends BaseItem>(
8388

8489
function setPanelPosition() {
8590
setProperties(dom.value.panel, {
86-
style: getPanelPositionStyle({
87-
panelPlacement: props.value.renderer.panelPlacement,
88-
container: dom.value.root,
89-
form: dom.value.form,
90-
environment: props.value.core.environment,
91-
}),
91+
style: isTouch.value
92+
? {}
93+
: getPanelPositionStyle({
94+
panelPlacement: props.value.renderer.panelPlacement,
95+
container: dom.value.root,
96+
form: dom.value.form,
97+
environment: props.value.core.environment,
98+
}),
9299
});
93100
}
94101

95102
function runRender() {
96-
render(props.value.renderer.render, {
103+
const renderProps = {
104+
isTouch: isTouch.value,
97105
state: lastStateRef.current,
98106
autocomplete: autocomplete.value,
99107
propGetters,
100108
dom: dom.value,
101109
classNames: props.value.renderer.classNames,
102-
panelContainer: props.value.renderer.panelContainer,
110+
panelContainer: isTouch.value
111+
? dom.value.touchOverlay
112+
: props.value.renderer.panelContainer,
103113
autocompleteScopeApi,
104-
});
114+
};
115+
116+
renderSearchBox(renderProps);
117+
renderPanel(props.value.renderer.render, renderProps);
105118
}
106119

107120
function scheduleRender(state: AutocompleteState<TItem>) {
@@ -135,6 +148,27 @@ export function autocomplete<TItem extends BaseItem>(
135148
};
136149
});
137150

151+
runEffect(() => {
152+
const panelContainerElement = isTouch.value
153+
? document.body
154+
: props.value.renderer.panelContainer;
155+
const panelElement = isTouch.value
156+
? dom.value.touchOverlay
157+
: dom.value.panel;
158+
159+
if (isTouch.value && lastStateRef.current.isOpen) {
160+
dom.value.openTouchOverlay();
161+
}
162+
163+
scheduleRender(lastStateRef.current);
164+
165+
return () => {
166+
if (panelContainerElement.contains(panelElement)) {
167+
panelContainerElement.removeChild(panelElement);
168+
}
169+
};
170+
});
171+
138172
runEffect(() => {
139173
const containerElement = props.value.renderer.container;
140174
invariant(
@@ -148,17 +182,6 @@ export function autocomplete<TItem extends BaseItem>(
148182
};
149183
});
150184

151-
runEffect(() => {
152-
const panelContainerElement = props.value.renderer.panelContainer;
153-
scheduleRender(lastStateRef.current);
154-
155-
return () => {
156-
if (panelContainerElement.contains(dom.value.panel)) {
157-
panelContainerElement.removeChild(dom.value.panel);
158-
}
159-
};
160-
});
161-
162185
runEffect(() => {
163186
const debouncedRender = debounce<{
164187
state: AutocompleteState<TItem>;
@@ -185,7 +208,16 @@ export function autocomplete<TItem extends BaseItem>(
185208

186209
runEffect(() => {
187210
const onResize = debounce<Event>(() => {
188-
requestAnimationFrame(setPanelPosition);
211+
const previousIsTouch = isTouch.value;
212+
isTouch.value = window.matchMedia(
213+
props.value.renderer.touchMediaQuery
214+
).matches;
215+
216+
if (previousIsTouch !== isTouch.value) {
217+
update({});
218+
} else {
219+
requestAnimationFrame(setPanelPosition);
220+
}
189221
}, 20);
190222
window.addEventListener('resize', onResize);
191223

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { setProperties } from '../utils';
2+
3+
type ElementProps = Record<string, unknown> & {
4+
children?: Node[];
5+
};
6+
7+
export function Element<KParam extends keyof HTMLElementTagNameMap>(
8+
tagName: keyof HTMLElementTagNameMap | HTMLElement,
9+
{ children = [], ...props }: ElementProps
10+
): HTMLElementTagNameMap[KParam] {
11+
const element =
12+
typeof tagName === 'string'
13+
? document.createElement<KParam>(tagName as any)
14+
: tagName;
15+
setProperties(element, props);
16+
17+
element.append(...children);
18+
19+
return element as any;
20+
}
Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { AutocompleteApi as AutocompleteCoreApi } from '@algolia/autocomplete-core';
22

33
import { Component, WithClassNames } from '../types/Component';
4-
import { concatClassNames, setProperties } from '../utils';
4+
import { concatClassNames } from '../utils';
5+
6+
import { Element } from './Element';
57

68
type FormProps = WithClassNames<
79
ReturnType<AutocompleteCoreApi<any>['getFormProps']>
@@ -11,11 +13,8 @@ export const Form: Component<FormProps, HTMLFormElement> = ({
1113
classNames,
1214
...props
1315
}) => {
14-
const element = document.createElement('form');
15-
setProperties(element, {
16+
return Element<'form'>('form', {
1617
...props,
17-
class: concatClassNames(['aa-Form', classNames.form]),
18+
class: concatClassNames('aa-Form', classNames.form),
1819
});
19-
20-
return element;
2120
};

packages/autocomplete-js/src/components/Input.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import {
55

66
import { AutocompletePropGetters, AutocompleteState } from '../types';
77
import { Component, WithClassNames } from '../types/Component';
8-
import { concatClassNames, setProperties } from '../utils';
8+
import { concatClassNames } from '../utils';
9+
10+
import { Element } from './Element';
911

1012
type InputProps = WithClassNames<{
13+
onTouchEscape?(): void;
1114
state: AutocompleteState<any>;
1215
getInputProps: AutocompletePropGetters<any>['getInputProps'];
1316
getInputPropsCore: AutocompleteCoreApi<any>['getInputProps'];
@@ -20,6 +23,7 @@ export const Input: Component<InputProps, HTMLInputElement> = ({
2023
getInputPropsCore,
2124
state,
2225
autocompleteScopeApi,
26+
onTouchEscape,
2327
}) => {
2428
const element = document.createElement('input');
2529
const inputProps = getInputProps({
@@ -28,10 +32,17 @@ export const Input: Component<InputProps, HTMLInputElement> = ({
2832
inputElement: element,
2933
...autocompleteScopeApi,
3034
});
31-
setProperties(element, {
35+
36+
return Element<'input'>(element, {
3237
...inputProps,
33-
class: concatClassNames(['aa-Input', classNames.input]),
34-
});
38+
onKeyDown(event: KeyboardEvent) {
39+
if (onTouchEscape && event.key === 'Escape') {
40+
onTouchEscape();
41+
return;
42+
}
3543

36-
return element;
44+
inputProps.onKeyDown(event);
45+
},
46+
class: concatClassNames('aa-Input', classNames.input),
47+
});
3748
};

0 commit comments

Comments
 (0)