@neovici/cosmoz-autocomplete 10.12.1 → 12.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/autocomplete/autocomplete-keybindings.d.ts +31 -0
- package/dist/autocomplete/autocomplete-keybindings.js +14 -0
- package/dist/autocomplete/autocomplete.d.ts +1 -2
- package/dist/autocomplete/autocomplete.js +17 -47
- package/dist/autocomplete/index.js +3 -2
- package/dist/autocomplete/styles.css.js +4 -0
- package/dist/autocomplete/use-autocomplete.d.ts +4 -6
- package/dist/autocomplete/use-autocomplete.js +42 -27
- package/dist/autocomplete/use-overflow.d.ts +2 -2
- package/dist/autocomplete/use-overflow.js +2 -2
- package/dist/autocomplete/util.d.ts +23 -1
- package/dist/autocomplete/util.js +17 -8
- package/dist/excluding/index.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/listbox/index.d.ts +1 -4
- package/dist/listbox/index.js +2 -8
- package/dist/listbox/listbox-keybindings.d.ts +26 -0
- package/dist/listbox/listbox-keybindings.js +23 -0
- package/dist/listbox/style.css.js +2 -32
- package/dist/listbox/use-keyboard.d.ts +1 -1
- package/dist/listbox/use-keyboard.js +20 -31
- package/package.json +17 -8
- package/dist/autocomplete/use-keys.d.ts +0 -9
- package/dist/autocomplete/use-keys.js +0 -23
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Activity } from '@neovici/cosmoz-utils/keybindings';
|
|
2
|
+
export declare const AUTOCOMPLETE_DESELECT_LAST: Activity;
|
|
3
|
+
export declare const autocompleteKeybindings: readonly [readonly [{
|
|
4
|
+
readonly key: "ArrowUp";
|
|
5
|
+
}, [symbol], {
|
|
6
|
+
readonly title: "Navigate up";
|
|
7
|
+
readonly description: "Move to previous listbox item";
|
|
8
|
+
}, {
|
|
9
|
+
readonly allowInEditable: true;
|
|
10
|
+
}], readonly [{
|
|
11
|
+
readonly key: "ArrowDown";
|
|
12
|
+
}, [symbol], {
|
|
13
|
+
readonly title: "Navigate down";
|
|
14
|
+
readonly description: "Move to next listbox item";
|
|
15
|
+
}, {
|
|
16
|
+
readonly allowInEditable: true;
|
|
17
|
+
}], readonly [{
|
|
18
|
+
readonly key: "Enter";
|
|
19
|
+
}, [symbol], {
|
|
20
|
+
readonly title: "Select";
|
|
21
|
+
readonly description: "Select current listbox item";
|
|
22
|
+
}, {
|
|
23
|
+
readonly allowInEditable: true;
|
|
24
|
+
}], readonly [{
|
|
25
|
+
readonly key: "Backspace";
|
|
26
|
+
}, [symbol], {
|
|
27
|
+
readonly title: "Deselect last";
|
|
28
|
+
readonly description: "Remove the last selected item";
|
|
29
|
+
}, {
|
|
30
|
+
readonly allowInEditable: true;
|
|
31
|
+
}]];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { listboxKeybindings } from '../listbox/listbox-keybindings';
|
|
2
|
+
export const AUTOCOMPLETE_DESELECT_LAST = Symbol('autocomplete.deselect.last');
|
|
3
|
+
export const autocompleteKeybindings = [
|
|
4
|
+
...listboxKeybindings,
|
|
5
|
+
[
|
|
6
|
+
{ key: 'Backspace' },
|
|
7
|
+
[AUTOCOMPLETE_DESELECT_LAST],
|
|
8
|
+
{
|
|
9
|
+
title: 'Deselect last',
|
|
10
|
+
description: 'Remove the last selected item',
|
|
11
|
+
},
|
|
12
|
+
{ allowInEditable: true },
|
|
13
|
+
],
|
|
14
|
+
];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import '@neovici/cosmoz-dropdown/cosmoz-dropdown-next';
|
|
2
2
|
import '@neovici/cosmoz-input';
|
|
3
3
|
import { ItemRenderer } from '../listbox/item-renderer';
|
|
4
4
|
import { ChipRenderer } from './selection';
|
|
@@ -18,7 +18,6 @@ export interface Props<I> extends Base<I> {
|
|
|
18
18
|
wrap?: boolean;
|
|
19
19
|
defaultIndex?: number;
|
|
20
20
|
externalSearch?: boolean;
|
|
21
|
-
placement?: Placement;
|
|
22
21
|
itemRenderer?: ItemRenderer<I>;
|
|
23
22
|
chipRenderer?: ChipRenderer<I>;
|
|
24
23
|
}
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import '@neovici/cosmoz-dropdown/cosmoz-dropdown-next';
|
|
2
2
|
import '@neovici/cosmoz-input';
|
|
3
|
-
import { useHost } from '@neovici/cosmoz-utils/hooks/use-host';
|
|
4
|
-
import { useImperativeApi } from '@neovici/cosmoz-utils/hooks/use-imperative-api';
|
|
5
|
-
import { useEffect } from '@pionjs/pion';
|
|
6
3
|
import { t } from 'i18next';
|
|
7
4
|
import { html } from 'lit-html';
|
|
8
5
|
import { live } from 'lit-html/directives/live.js';
|
|
9
|
-
import { ref } from 'lit-html/directives/ref.js';
|
|
10
6
|
import { until } from 'lit-html/directives/until.js';
|
|
11
7
|
import { when } from 'lit-html/directives/when.js';
|
|
12
8
|
import { listbox } from '../listbox';
|
|
@@ -18,45 +14,25 @@ import { useOverflow } from './use-overflow';
|
|
|
18
14
|
const inputParts = ['input', 'control', 'label', 'line', 'error', 'wrap']
|
|
19
15
|
.map((part) => `${part}: input-${part}`)
|
|
20
16
|
.join();
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
minWidth: `${Math.max(rects.reference.width, rects.floating.width)}px`,
|
|
26
|
-
});
|
|
27
|
-
},
|
|
28
|
-
}),
|
|
29
|
-
...defaultMiddleware,
|
|
30
|
-
];
|
|
31
|
-
const shouldShowDropdown = ({ active, isSingle, showSingle, }) => {
|
|
32
|
-
if (!active)
|
|
17
|
+
const shouldShowDropdown = ({ opened, isSingle, showSingle, hasResultsOrQuery, }) => {
|
|
18
|
+
if (!opened)
|
|
19
|
+
return false;
|
|
20
|
+
if (isSingle && !showSingle)
|
|
33
21
|
return false;
|
|
34
|
-
|
|
35
|
-
return !disallowedSingle;
|
|
22
|
+
return hasResultsOrQuery;
|
|
36
23
|
};
|
|
37
24
|
const autocomplete = (props) => {
|
|
38
|
-
const {
|
|
25
|
+
const { opened, invalid, errorMessage, label, placeholder, disabled, noLabelFloat, alwaysFloatLabel, textual, text, onText, onToggle, onDeselect, value, limit, min, showSingle, items, source$, loading, chipRenderer, } = props, isOne = limit == 1, // eslint-disable-line eqeqeq
|
|
39
26
|
isSingle = isOne && value?.[0] != null;
|
|
40
|
-
const { styles, setReference, setFloating } = useFloating({
|
|
41
|
-
placement,
|
|
42
|
-
middleware,
|
|
43
|
-
});
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
host.addEventListener('focusin', onFocus);
|
|
46
|
-
host.addEventListener('focusout', onFocus);
|
|
47
|
-
return () => {
|
|
48
|
-
host.removeEventListener('focusin', onFocus);
|
|
49
|
-
host.removeEventListener('focusout', onFocus);
|
|
50
|
-
};
|
|
51
|
-
}, [onFocus]);
|
|
52
|
-
useImperativeApi({
|
|
53
|
-
focus: () => host.shadowRoot?.querySelector('#input')?.focus(),
|
|
54
|
-
}, []);
|
|
55
27
|
const hasResultsOrQuery = loading || items.length > 0 || (text != null && text.length > 0);
|
|
56
|
-
return html `<cosmoz-
|
|
28
|
+
return html `<cosmoz-dropdown-next
|
|
29
|
+
open-on-focus
|
|
30
|
+
@dropdown-toggle=${onToggle}
|
|
31
|
+
>
|
|
32
|
+
<cosmoz-input
|
|
33
|
+
slot="button"
|
|
57
34
|
id="input"
|
|
58
35
|
part="input"
|
|
59
|
-
${ref(setReference)}
|
|
60
36
|
.label=${label}
|
|
61
37
|
.placeholder=${isSingle ? undefined : placeholder}
|
|
62
38
|
?no-label-float=${noLabelFloat}
|
|
@@ -67,7 +43,6 @@ const autocomplete = (props) => {
|
|
|
67
43
|
.errorMessage=${until(source$.then(() => errorMessage, (e) => e.message), errorMessage)}
|
|
68
44
|
.value=${live(text)}
|
|
69
45
|
@value-changed=${onText}
|
|
70
|
-
@click=${onClick}
|
|
71
46
|
autocomplete="off"
|
|
72
47
|
exportparts=${inputParts}
|
|
73
48
|
?data-one=${isOne}
|
|
@@ -87,23 +62,18 @@ const autocomplete = (props) => {
|
|
|
87
62
|
</cosmoz-input>
|
|
88
63
|
|
|
89
64
|
${when(shouldShowDropdown({
|
|
90
|
-
|
|
65
|
+
opened,
|
|
91
66
|
isSingle,
|
|
92
67
|
showSingle,
|
|
68
|
+
hasResultsOrQuery,
|
|
93
69
|
}), () => listbox({
|
|
94
70
|
...props,
|
|
95
71
|
items,
|
|
96
72
|
multi: !isOne,
|
|
97
|
-
setFloating,
|
|
98
|
-
styles: {
|
|
99
|
-
...styles,
|
|
100
|
-
// WORKAROUND: hide the listbox if there are no results, don't remove it from DOM
|
|
101
|
-
// TODO: revert https://github.com/Neovici/cosmoz-autocomplete/pull/206 after https://github.com/pionjs/pion/issues/64 is fixed
|
|
102
|
-
display: hasResultsOrQuery ? undefined : 'none',
|
|
103
|
-
},
|
|
104
73
|
}, when(loading, () => html `<cosmoz-autocomplete-skeleton-span></cosmoz-autocomplete-skeleton-span>`, () => when(text != null && text.length > 0 && items.length === 0, () => html `<slot name="no-result">
|
|
105
74
|
<p class="no-result">${t('No results found')}</p>
|
|
106
|
-
</slot>`))))}
|
|
75
|
+
</slot>`))))}
|
|
76
|
+
</cosmoz-dropdown-next>`;
|
|
107
77
|
}, Autocomplete = (props) => {
|
|
108
78
|
const thru = {
|
|
109
79
|
...props,
|
|
@@ -24,5 +24,6 @@ const Standalone = (host) => {
|
|
|
24
24
|
});
|
|
25
25
|
};
|
|
26
26
|
const styleSheets = [sheet(style)];
|
|
27
|
-
|
|
28
|
-
customElements.define('cosmoz-autocomplete', component(
|
|
27
|
+
const shadowRootInit = { mode: 'open', delegatesFocus: true };
|
|
28
|
+
customElements.define('cosmoz-autocomplete-ui', component(Autocomplete, { observedAttributes, styleSheets, shadowRootInit }));
|
|
29
|
+
customElements.define('cosmoz-autocomplete', component(Standalone, { observedAttributes, styleSheets, shadowRootInit }));
|
|
@@ -13,7 +13,7 @@ interface Base<I> {
|
|
|
13
13
|
onSelect: (value: I, meta: Meta<I>) => void;
|
|
14
14
|
}
|
|
15
15
|
interface Meta<I> extends Omit<Base<I>, 'value'> {
|
|
16
|
-
|
|
16
|
+
setOpened: (opened: boolean) => void;
|
|
17
17
|
value: I[];
|
|
18
18
|
}
|
|
19
19
|
export interface Props<I> extends Base<I> {
|
|
@@ -23,21 +23,19 @@ export interface Props<I> extends Base<I> {
|
|
|
23
23
|
textual?: (prop?: string) => (i: I) => string;
|
|
24
24
|
valueProperty?: string;
|
|
25
25
|
disabled?: boolean;
|
|
26
|
-
onFocus?: (focused?: boolean) => void;
|
|
27
26
|
preserveOrder?: boolean;
|
|
28
27
|
defaultIndex?: number;
|
|
29
28
|
externalSearch?: boolean;
|
|
30
29
|
}
|
|
31
|
-
export declare const useAutocomplete: <I>({ value: _value, text, onChange: _onChange, onText: _onText, onSelect, limit, min, source, textProperty, textual: _textual, valueProperty, keepOpened, keepQuery, preserveOrder, defaultIndex, externalSearch,
|
|
32
|
-
|
|
30
|
+
export declare const useAutocomplete: <I>({ value: _value, text, onChange: _onChange, onText: _onText, onSelect, limit, min, source, textProperty, textual: _textual, valueProperty, keepOpened, keepQuery, preserveOrder, defaultIndex, externalSearch, }: Props<I>) => {
|
|
31
|
+
opened: boolean;
|
|
33
32
|
query: string | undefined;
|
|
34
33
|
textual: (i: I) => string;
|
|
35
34
|
value: I[];
|
|
36
35
|
source$: Promise<I[]>;
|
|
37
36
|
loading: boolean;
|
|
38
37
|
items: I[];
|
|
39
|
-
|
|
40
|
-
onFocus: (e: FocusEvent) => void;
|
|
38
|
+
onToggle: (e: Event) => void;
|
|
41
39
|
onText: (e: InputEvent) => void;
|
|
42
40
|
onSelect: (newVal: I) => void;
|
|
43
41
|
onDeselect: (val: I | I[]) => void;
|
|
@@ -1,30 +1,43 @@
|
|
|
1
|
-
import { useFocus } from '@neovici/cosmoz-dropdown/use-focus';
|
|
2
1
|
import { array, without } from '@neovici/cosmoz-utils/array';
|
|
3
2
|
import { useHost } from '@neovici/cosmoz-utils/hooks/use-host';
|
|
4
3
|
import { useMeta } from '@neovici/cosmoz-utils/hooks/use-meta';
|
|
5
4
|
import { usePromise } from '@neovici/cosmoz-utils/hooks/use-promise';
|
|
5
|
+
import { useActivity } from '@neovici/cosmoz-utils/keybindings';
|
|
6
6
|
import { prop, strProp } from '@neovici/cosmoz-utils/object';
|
|
7
7
|
import { useCallback, useEffect, useMemo, useState } from '@pionjs/pion';
|
|
8
|
-
import {
|
|
9
|
-
import { EMPTY, normalize, notify, search
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
import { AUTOCOMPLETE_DESELECT_LAST } from './autocomplete-keybindings';
|
|
9
|
+
import { EMPTY, normalize, notify, search } from './util';
|
|
10
|
+
const useNotify = (host, fn, name) => useCallback((val) => {
|
|
11
|
+
fn?.(val);
|
|
12
|
+
host.dispatchEvent(new CustomEvent(name, { detail: val }));
|
|
13
|
+
}, [fn]);
|
|
14
|
+
export const useAutocomplete = ({ value: _value, text, onChange: _onChange, onText: _onText, onSelect, limit, min, source, textProperty, textual: _textual, valueProperty, keepOpened, keepQuery, preserveOrder, defaultIndex, externalSearch, }) => {
|
|
15
|
+
const textual = useMemo(() => (_textual ?? strProp)(textProperty), [_textual, textProperty]), host = useHost(),
|
|
16
|
+
// Popover open/close state, synced from cosmoz-dropdown-next's dropdown-toggle event
|
|
17
|
+
[opened, setOpened] = useState(false), empty = !text, query = useMemo(() => text?.trim(), [text]), onText = useNotify(host, _onText, 'text'), onChange = useCallback((val) => {
|
|
18
|
+
_onChange?.(val, () => setOpened(false));
|
|
13
19
|
notify(host, 'value', val);
|
|
14
|
-
}, [_onChange]), [options, setOptions] = useState([]), source$ = useMemo(() => Promise.resolve(typeof source === 'function'
|
|
20
|
+
}, [_onChange]), [options, setOptions] = useState([]), source$ = useMemo(() => Promise.resolve(typeof source === 'function'
|
|
21
|
+
? source({ query, active: opened })
|
|
22
|
+
: source).then(normalize), [source, opened, query]), value = useMemo(() => array(_value), [_value]);
|
|
15
23
|
useEffect(() => source$.then(setOptions), [source$]);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
// Backspace to deselect last chip (when input is empty and multi-select)
|
|
25
|
+
useActivity({
|
|
26
|
+
activity: AUTOCOMPLETE_DESELECT_LAST,
|
|
27
|
+
callback: () => {
|
|
28
|
+
const values = array(value);
|
|
29
|
+
if (values.length > 0) {
|
|
30
|
+
onChange(values.slice(0, -1));
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
check: () => empty && limit !== 1 && host.matches(':focus-within'),
|
|
34
|
+
element: () => host,
|
|
35
|
+
}, []);
|
|
36
|
+
// Clear query when popover closes
|
|
24
37
|
useEffect(() => {
|
|
25
|
-
if (!
|
|
38
|
+
if (!opened && !keepQuery)
|
|
26
39
|
onText('');
|
|
27
|
-
}, [
|
|
40
|
+
}, [opened, keepQuery]);
|
|
28
41
|
const meta = useMeta({
|
|
29
42
|
onText,
|
|
30
43
|
onChange,
|
|
@@ -33,19 +46,19 @@ export const useAutocomplete = ({ value: _value, text, onChange: _onChange, onTe
|
|
|
33
46
|
min,
|
|
34
47
|
keepQuery,
|
|
35
48
|
keepOpened,
|
|
36
|
-
|
|
49
|
+
setOpened,
|
|
37
50
|
onSelect,
|
|
38
51
|
});
|
|
39
52
|
const [, , state] = usePromise(source$);
|
|
40
53
|
return {
|
|
41
|
-
|
|
54
|
+
opened,
|
|
42
55
|
query,
|
|
43
56
|
textual,
|
|
44
57
|
value,
|
|
45
58
|
source$,
|
|
46
59
|
loading: state === 'pending',
|
|
47
60
|
items: useMemo(() => {
|
|
48
|
-
if (!
|
|
61
|
+
if (!opened)
|
|
49
62
|
return EMPTY;
|
|
50
63
|
const items = preserveOrder
|
|
51
64
|
? options
|
|
@@ -53,7 +66,7 @@ export const useAutocomplete = ({ value: _value, text, onChange: _onChange, onTe
|
|
|
53
66
|
return externalSearch ? items : search(items, query, textual);
|
|
54
67
|
}, [
|
|
55
68
|
options,
|
|
56
|
-
|
|
69
|
+
opened,
|
|
57
70
|
query,
|
|
58
71
|
textual,
|
|
59
72
|
empty,
|
|
@@ -62,19 +75,21 @@ export const useAutocomplete = ({ value: _value, text, onChange: _onChange, onTe
|
|
|
62
75
|
valueProperty,
|
|
63
76
|
externalSearch,
|
|
64
77
|
]),
|
|
65
|
-
|
|
66
|
-
|
|
78
|
+
// Sync opened state from cosmoz-dropdown-next's composed dropdown-toggle event
|
|
79
|
+
onToggle: useCallback((e) => {
|
|
80
|
+
setOpened(e.newState === 'open');
|
|
81
|
+
}, []),
|
|
67
82
|
onText: useCallback((e) => {
|
|
68
83
|
onText(e.target.value);
|
|
69
|
-
|
|
70
|
-
}, [onText, text,
|
|
84
|
+
setOpened(true);
|
|
85
|
+
}, [onText, text, setOpened]),
|
|
71
86
|
onSelect: useCallback((newVal) => {
|
|
72
87
|
meta.onSelect?.(newVal, meta);
|
|
73
|
-
const { onChange, onText, limit, min, value: val, keepQuery, keepOpened,
|
|
88
|
+
const { onChange, onText, limit, min, value: val, keepQuery, keepOpened, setOpened, } = meta;
|
|
74
89
|
if (!keepQuery)
|
|
75
90
|
onText('');
|
|
76
91
|
if (!keepOpened)
|
|
77
|
-
|
|
92
|
+
setOpened(false);
|
|
78
93
|
const value = array(val), deselect = value.includes(newVal);
|
|
79
94
|
if (deselect && value.length === min)
|
|
80
95
|
return;
|
|
@@ -29,7 +29,7 @@ const overflow = (host) => {
|
|
|
29
29
|
chips[i].hidden = true;
|
|
30
30
|
}
|
|
31
31
|
};
|
|
32
|
-
export const useOverflow = ({ value,
|
|
32
|
+
export const useOverflow = ({ value, opened, wrap, limit, }) => {
|
|
33
33
|
const host = useHost();
|
|
34
34
|
// eslint-disable-next-line eqeqeq
|
|
35
35
|
const enabled = !(wrap || limit == 1);
|
|
@@ -45,5 +45,5 @@ export const useOverflow = ({ value, active, wrap, limit, }) => {
|
|
|
45
45
|
observer.observe(input);
|
|
46
46
|
return () => observer.disconnect();
|
|
47
47
|
}, [enabled]);
|
|
48
|
-
useLayoutEffect(() => (enabled ? doRaf() : undefined), [enabled, width,
|
|
48
|
+
useLayoutEffect(() => (enabled ? doRaf() : undefined), [enabled, width, opened, value]);
|
|
49
49
|
};
|
|
@@ -1,9 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove accents/diacritics from a string.
|
|
3
|
+
* @see https://stackoverflow.com/questions/990904/remove-accents-diacritics-in-a-string-in-javascript
|
|
4
|
+
* @see https://medium.com/nerd-for-tech/basics-of-javascript-string-normalize-method-f3241174c2d0
|
|
5
|
+
* @see https://www.codu.co/articles/remove-accents-from-a-javascript-string-skgp1inb
|
|
6
|
+
*
|
|
7
|
+
* @param str string from which to remove accents
|
|
8
|
+
* @returns string w/o accents
|
|
9
|
+
*/
|
|
10
|
+
export declare const removeAccents: (str: string) => string;
|
|
11
|
+
/**
|
|
12
|
+
* Search items by query string, returning matches sorted by position.
|
|
13
|
+
* Accent-insensitive search.
|
|
14
|
+
*/
|
|
1
15
|
export declare const search: <I>(source: I[], query: string | undefined, textual: (i: I) => string) => I[];
|
|
16
|
+
/**
|
|
17
|
+
* Normalize source to an array.
|
|
18
|
+
*/
|
|
2
19
|
export declare const normalize: <I>(source: I[] | false | null | undefined) => I[];
|
|
20
|
+
/**
|
|
21
|
+
* Dispatch a custom event with detail.
|
|
22
|
+
*/
|
|
3
23
|
export declare const notify: <T>(host: EventTarget, name: string, detail: T) => boolean;
|
|
4
|
-
export declare const useNotify: <V>(host: EventTarget, fn: undefined | ((v: V) => void), name: string) => (val: V) => void;
|
|
5
24
|
export declare const EMPTY: never[];
|
|
6
25
|
type Arr = unknown[];
|
|
7
26
|
type ArrFn<T extends Arr> = (...args: T) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Request animation frame wrapper with cleanup.
|
|
29
|
+
*/
|
|
8
30
|
export declare const raf: <A extends Arr, F extends ArrFn<A> = ArrFn<A>>(fn: F) => (...args: A) => () => void;
|
|
9
31
|
export {};
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { useCallback } from '@pionjs/pion';
|
|
2
1
|
/**
|
|
2
|
+
* Remove accents/diacritics from a string.
|
|
3
3
|
* @see https://stackoverflow.com/questions/990904/remove-accents-diacritics-in-a-string-in-javascript
|
|
4
4
|
* @see https://medium.com/nerd-for-tech/basics-of-javascript-string-normalize-method-f3241174c2d0
|
|
5
5
|
* @see https://www.codu.co/articles/remove-accents-from-a-javascript-string-skgp1inb
|
|
6
6
|
*
|
|
7
|
-
* @param
|
|
8
|
-
* @returns
|
|
7
|
+
* @param str string from which to remove accents
|
|
8
|
+
* @returns string w/o accents
|
|
9
9
|
*/
|
|
10
|
-
const removeAccents = (str) => {
|
|
10
|
+
export const removeAccents = (str) => {
|
|
11
11
|
return str.normalize('NFD').replace(/[\u0300-\u036f]/gu, '');
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Search items by query string, returning matches sorted by position.
|
|
15
|
+
* Accent-insensitive search.
|
|
16
|
+
*/
|
|
13
17
|
export const search = (source, query, textual) => {
|
|
14
18
|
if (!query)
|
|
15
19
|
return source;
|
|
@@ -28,17 +32,22 @@ export const search = (source, query, textual) => {
|
|
|
28
32
|
}
|
|
29
33
|
return matches.sort((a, b) => a.index - b.index).map(({ item }) => item);
|
|
30
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* Normalize source to an array.
|
|
37
|
+
*/
|
|
31
38
|
export const normalize = (source) => {
|
|
32
39
|
if (source === false || source == null)
|
|
33
40
|
return [];
|
|
34
41
|
return source;
|
|
35
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* Dispatch a custom event with detail.
|
|
45
|
+
*/
|
|
36
46
|
export const notify = (host, name, detail) => host.dispatchEvent(new CustomEvent(name, { detail }));
|
|
37
|
-
export const useNotify = (host, fn, name) => useCallback((val) => {
|
|
38
|
-
fn?.(val);
|
|
39
|
-
notify(host, name, val);
|
|
40
|
-
}, [fn]);
|
|
41
47
|
export const EMPTY = [];
|
|
48
|
+
/**
|
|
49
|
+
* Request animation frame wrapper with cleanup.
|
|
50
|
+
*/
|
|
42
51
|
export const raf = (fn) => (...args) => {
|
|
43
52
|
let id;
|
|
44
53
|
const cleanup = () => {
|
package/dist/excluding/index.js
CHANGED
|
@@ -55,7 +55,9 @@ const AutocompleteExcluding = (props) => {
|
|
|
55
55
|
chipRenderer: useMemo(() => mkChipRenderer(value, onClear), [value, onClear]),
|
|
56
56
|
});
|
|
57
57
|
};
|
|
58
|
+
const shadowRootInit = { mode: 'open', delegatesFocus: true };
|
|
58
59
|
customElements.define('cosmoz-autocomplete-excluding', component(AutocompleteExcluding, {
|
|
59
60
|
observedAttributes,
|
|
60
61
|
styleSheets: [style, excludingStyle],
|
|
62
|
+
shadowRootInit,
|
|
61
63
|
}));
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import './autocomplete/';
|
|
2
|
+
export { AUTOCOMPLETE_DESELECT_LAST, autocompleteKeybindings, } from './autocomplete/autocomplete-keybindings';
|
|
2
3
|
export type { Source } from './autocomplete/use-autocomplete';
|
|
4
|
+
export { LISTBOX_NAVIGATE_DOWN, LISTBOX_NAVIGATE_UP, LISTBOX_SELECT, listboxKeybindings, } from './listbox/listbox-keybindings';
|
package/dist/index.js
CHANGED
package/dist/listbox/index.d.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { StyleInfo } from 'lit-html/directives/style-map.js';
|
|
2
1
|
import { Props } from './use-listbox';
|
|
3
2
|
interface ListboxProps<I> extends Props<I> {
|
|
4
3
|
multi?: boolean;
|
|
5
|
-
setFloating: (el?: Element) => void;
|
|
6
|
-
styles: StyleInfo;
|
|
7
4
|
}
|
|
8
|
-
export declare const listbox: <I>({ multi,
|
|
5
|
+
export declare const listbox: <I>({ multi, ...thru }: ListboxProps<I>, content: unknown) => import("lit-html").TemplateResult<1>;
|
|
9
6
|
export {};
|
package/dist/listbox/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { virtualize, virtualizerRef, } from '@lit-labs/virtualizer/virtualize.js';
|
|
2
|
-
import { connectable } from '@neovici/cosmoz-utils/connectable';
|
|
3
2
|
import { sheet } from '@neovici/cosmoz-utils';
|
|
4
3
|
import { spreadProps } from '@neovici/cosmoz-utils/directives/spread-props';
|
|
5
4
|
import { noop } from '@neovici/cosmoz-utils/function';
|
|
@@ -7,7 +6,6 @@ import { useStyleSheet } from '@neovici/cosmoz-utils/hooks/use-stylesheet';
|
|
|
7
6
|
import { props } from '@neovici/cosmoz-utils/object';
|
|
8
7
|
import { component, html, useEffect, useRef } from '@pionjs/pion';
|
|
9
8
|
import { ref } from 'lit-html/directives/ref.js';
|
|
10
|
-
import { styleMap } from 'lit-html/directives/style-map.js';
|
|
11
9
|
import style, { styles } from './style.css';
|
|
12
10
|
import { properties, useListbox } from './use-listbox';
|
|
13
11
|
const ignore = noop;
|
|
@@ -51,14 +49,10 @@ const Listbox = (host) => {
|
|
|
51
49
|
</div>
|
|
52
50
|
<slot></slot>`;
|
|
53
51
|
};
|
|
54
|
-
customElements.define('cosmoz-listbox',
|
|
55
|
-
export const listbox = ({ multi,
|
|
56
|
-
style="${styleMap(styles)}"
|
|
57
|
-
@connected="${(e) => e.target.showPopover?.()}"
|
|
58
|
-
popover="manual"
|
|
52
|
+
customElements.define('cosmoz-listbox', component(Listbox, { styleSheets: [sheet(style)] }));
|
|
53
|
+
export const listbox = ({ multi, ...thru }, content) => html `<cosmoz-listbox
|
|
59
54
|
part="listbox"
|
|
60
55
|
?multi=${multi}
|
|
61
|
-
${ref(setFloating)}
|
|
62
56
|
...=${spreadProps(props(properties)(thru))}
|
|
63
57
|
>${content}</cosmoz-listbox
|
|
64
58
|
>`;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Activity } from '@neovici/cosmoz-utils/keybindings';
|
|
2
|
+
export declare const LISTBOX_NAVIGATE_UP: Activity;
|
|
3
|
+
export declare const LISTBOX_NAVIGATE_DOWN: Activity;
|
|
4
|
+
export declare const LISTBOX_SELECT: Activity;
|
|
5
|
+
export declare const listboxKeybindings: readonly [readonly [{
|
|
6
|
+
readonly key: "ArrowUp";
|
|
7
|
+
}, [symbol], {
|
|
8
|
+
readonly title: "Navigate up";
|
|
9
|
+
readonly description: "Move to previous listbox item";
|
|
10
|
+
}, {
|
|
11
|
+
readonly allowInEditable: true;
|
|
12
|
+
}], readonly [{
|
|
13
|
+
readonly key: "ArrowDown";
|
|
14
|
+
}, [symbol], {
|
|
15
|
+
readonly title: "Navigate down";
|
|
16
|
+
readonly description: "Move to next listbox item";
|
|
17
|
+
}, {
|
|
18
|
+
readonly allowInEditable: true;
|
|
19
|
+
}], readonly [{
|
|
20
|
+
readonly key: "Enter";
|
|
21
|
+
}, [symbol], {
|
|
22
|
+
readonly title: "Select";
|
|
23
|
+
readonly description: "Select current listbox item";
|
|
24
|
+
}, {
|
|
25
|
+
readonly allowInEditable: true;
|
|
26
|
+
}]];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const LISTBOX_NAVIGATE_UP = Symbol('listbox.navigate.up');
|
|
2
|
+
export const LISTBOX_NAVIGATE_DOWN = Symbol('listbox.navigate.down');
|
|
3
|
+
export const LISTBOX_SELECT = Symbol('listbox.select');
|
|
4
|
+
export const listboxKeybindings = [
|
|
5
|
+
[
|
|
6
|
+
{ key: 'ArrowUp' },
|
|
7
|
+
[LISTBOX_NAVIGATE_UP],
|
|
8
|
+
{ title: 'Navigate up', description: 'Move to previous listbox item' },
|
|
9
|
+
{ allowInEditable: true },
|
|
10
|
+
],
|
|
11
|
+
[
|
|
12
|
+
{ key: 'ArrowDown' },
|
|
13
|
+
[LISTBOX_NAVIGATE_DOWN],
|
|
14
|
+
{ title: 'Navigate down', description: 'Move to next listbox item' },
|
|
15
|
+
{ allowInEditable: true },
|
|
16
|
+
],
|
|
17
|
+
[
|
|
18
|
+
{ key: 'Enter' },
|
|
19
|
+
[LISTBOX_SELECT],
|
|
20
|
+
{ title: 'Select', description: 'Select current listbox item' },
|
|
21
|
+
{ allowInEditable: true },
|
|
22
|
+
],
|
|
23
|
+
];
|
|
@@ -7,42 +7,21 @@ const checkSVG =
|
|
|
7
7
|
/* eslint-enable quotes */
|
|
8
8
|
const style = css `
|
|
9
9
|
:host {
|
|
10
|
-
|
|
11
|
-
z-index: 1000;
|
|
10
|
+
display: block;
|
|
12
11
|
font-family: var(--paper-font-subhead_-_font-family, inherit);
|
|
13
12
|
background: var(
|
|
14
13
|
--cosmoz-autocomplete-listbox-bg,
|
|
15
14
|
rgba(255, 255, 255, 0.75)
|
|
16
15
|
);
|
|
17
|
-
min-width: 50px;
|
|
18
16
|
backdrop-filter: blur(16px) saturate(180%);
|
|
19
17
|
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
|
20
18
|
border-radius: 6px;
|
|
21
19
|
box-shadow:
|
|
22
20
|
0 4px 24px 0 rgba(0, 0, 0, 0.18),
|
|
23
21
|
0 1.5px 6px 0 rgba(0, 0, 0, 0.1);
|
|
22
|
+
border: 1px solid rgba(200, 200, 200, 0.25);
|
|
24
23
|
text-transform: var(--cosmoz-autocomplete-listbox-text-transform, initial);
|
|
25
24
|
overflow: hidden;
|
|
26
|
-
transition:
|
|
27
|
-
opacity 150ms ease-in-out,
|
|
28
|
-
transform 100ms ease-in-out;
|
|
29
|
-
}
|
|
30
|
-
:host(:popover-open) {
|
|
31
|
-
box-sizing: border-box;
|
|
32
|
-
display: block;
|
|
33
|
-
margin: 0;
|
|
34
|
-
border: 1px solid rgba(200, 200, 200, 0.25);
|
|
35
|
-
|
|
36
|
-
opacity: 1;
|
|
37
|
-
transform: translateY(0);
|
|
38
|
-
|
|
39
|
-
@starting-style {
|
|
40
|
-
opacity: 0;
|
|
41
|
-
transform: translateY(-50px);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
:host([popover]) {
|
|
45
|
-
padding: 0;
|
|
46
25
|
}
|
|
47
26
|
.items {
|
|
48
27
|
position: relative;
|
|
@@ -106,15 +85,6 @@ const style = css `
|
|
|
106
85
|
padding-left: 33px;
|
|
107
86
|
}
|
|
108
87
|
|
|
109
|
-
.swatch {
|
|
110
|
-
width: 18px;
|
|
111
|
-
height: 18px;
|
|
112
|
-
display: inline-block;
|
|
113
|
-
box-sizing: border-box;
|
|
114
|
-
vertical-align: middle;
|
|
115
|
-
border-radius: 50%;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
88
|
[virtualizer-sizer]:not(.sizer) {
|
|
119
89
|
line-height: 1;
|
|
120
90
|
}
|
|
@@ -1,32 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
case 'Enter':
|
|
22
|
-
e.preventDefault();
|
|
23
|
-
listeners.onEnter();
|
|
24
|
-
break;
|
|
25
|
-
default:
|
|
26
|
-
break;
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
document.addEventListener('keydown', handler, true);
|
|
30
|
-
return () => document.removeEventListener('keydown', handler, true);
|
|
31
|
-
}, [listeners]);
|
|
1
|
+
import { useHost } from '@neovici/cosmoz-utils/hooks/use-host';
|
|
2
|
+
import { useActivity } from '@neovici/cosmoz-utils/keybindings';
|
|
3
|
+
import { LISTBOX_NAVIGATE_DOWN, LISTBOX_NAVIGATE_UP, LISTBOX_SELECT, } from './listbox-keybindings';
|
|
4
|
+
export const useKeyboard = ({ onUp, onDown, onEnter }) => {
|
|
5
|
+
const host = useHost();
|
|
6
|
+
useActivity({
|
|
7
|
+
activity: LISTBOX_NAVIGATE_UP,
|
|
8
|
+
callback: onUp,
|
|
9
|
+
element: () => host,
|
|
10
|
+
}, []);
|
|
11
|
+
useActivity({
|
|
12
|
+
activity: LISTBOX_NAVIGATE_DOWN,
|
|
13
|
+
callback: onDown,
|
|
14
|
+
element: () => host,
|
|
15
|
+
}, []);
|
|
16
|
+
useActivity({
|
|
17
|
+
activity: LISTBOX_SELECT,
|
|
18
|
+
callback: onEnter,
|
|
19
|
+
element: () => host,
|
|
20
|
+
}, []);
|
|
32
21
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neovici/cosmoz-autocomplete",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "12.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A autocomplete input web component",
|
|
6
6
|
"keywords": [
|
|
@@ -26,8 +26,10 @@
|
|
|
26
26
|
"build": "tsc -p tsconfig.build.json",
|
|
27
27
|
"check:duplicates": "check-duplicate-components",
|
|
28
28
|
"start": "npm run storybook:start",
|
|
29
|
-
"test": "
|
|
30
|
-
"test:
|
|
29
|
+
"test": "vitest --run && npm run e2e",
|
|
30
|
+
"test:unit": "vitest --project=unit --run",
|
|
31
|
+
"test:storybook": "vitest --project=storybook --run",
|
|
32
|
+
"test:watch": "vitest",
|
|
31
33
|
"e2e": "playwright test",
|
|
32
34
|
"prepare": "husky",
|
|
33
35
|
"prettier": "prettier --write \"**/*.{js,ts,json,css,md}\"",
|
|
@@ -70,13 +72,15 @@
|
|
|
70
72
|
"exports": {
|
|
71
73
|
".": "./dist/index.js",
|
|
72
74
|
"./autocomplete": "./dist/autocomplete/index.js",
|
|
75
|
+
"./keybindings": "./dist/autocomplete/autocomplete-keybindings.js",
|
|
73
76
|
"./listbox": "./dist/listbox/index.js",
|
|
77
|
+
"./listbox/keybindings": "./dist/listbox/listbox-keybindings.js",
|
|
74
78
|
"./item-renderer": "./dist/listbox/item-renderer.js"
|
|
75
79
|
},
|
|
76
80
|
"dependencies": {
|
|
77
81
|
"@lit-labs/virtualizer": "^2.0.0",
|
|
78
|
-
"@neovici/cosmoz-dropdown": "^
|
|
79
|
-
"@neovici/cosmoz-input": "^5.0
|
|
82
|
+
"@neovici/cosmoz-dropdown": "^7.3.0",
|
|
83
|
+
"@neovici/cosmoz-input": "^5.6.0",
|
|
80
84
|
"@neovici/cosmoz-utils": "^6.19.0",
|
|
81
85
|
"@pionjs/pion": "^2.7.1",
|
|
82
86
|
"i18next": "^23.16.8",
|
|
@@ -86,23 +90,28 @@
|
|
|
86
90
|
"@commitlint/cli": "^20.0.0",
|
|
87
91
|
"@commitlint/config-conventional": "^20.0.0",
|
|
88
92
|
"@neovici/cfg": "^2.8.0",
|
|
89
|
-
"@open-wc/testing": "^4.0.0",
|
|
90
93
|
"@playwright/test": "^1.43.1",
|
|
91
94
|
"@semantic-release/changelog": "^6.0.0",
|
|
92
95
|
"@semantic-release/git": "^10.0.0",
|
|
96
|
+
"@storybook/addon-docs": "^10.0.0",
|
|
97
|
+
"@storybook/addon-vitest": "^10.0.0",
|
|
93
98
|
"@storybook/web-components-vite": "^10.0.0",
|
|
94
|
-
"@types/mocha": "^10.0.0",
|
|
95
99
|
"@types/node": "24.0.0",
|
|
100
|
+
"@vitest/browser": "^4.0.0",
|
|
101
|
+
"@vitest/browser-playwright": "^4.0.0",
|
|
96
102
|
"esbuild": "^0.24.0",
|
|
97
103
|
"eslint": "^9.0.0",
|
|
98
104
|
"http-server": "^14.1.1",
|
|
99
105
|
"husky": "^9.0.0",
|
|
106
|
+
"jsdom": "^26.1.0",
|
|
100
107
|
"lint-staged": "^16.2.7",
|
|
101
108
|
"rollup-plugin-esbuild": "^6.1.1",
|
|
102
109
|
"rollup-plugin-terser": "^7.0.2",
|
|
103
110
|
"semantic-release": "^25.0.0",
|
|
111
|
+
"shadow-dom-testing-library": "^1.13.1",
|
|
104
112
|
"sinon": "^19.0.0",
|
|
105
|
-
"storybook": "^10.0.0"
|
|
113
|
+
"storybook": "^10.0.0",
|
|
114
|
+
"vitest": "^4.0.0"
|
|
106
115
|
},
|
|
107
116
|
"overrides": {
|
|
108
117
|
"conventional-changelog-conventionalcommits": ">= 8.0.0"
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { array } from '@neovici/cosmoz-utils/array';
|
|
2
|
-
import { useMeta } from '@neovici/cosmoz-utils/hooks/use-meta';
|
|
3
|
-
import { useEffect } from '@pionjs/pion';
|
|
4
|
-
export const useKeys = ({ focused, empty, ...rest }) => {
|
|
5
|
-
const enabled = focused && empty && rest.limit !== 1, meta = useMeta(rest);
|
|
6
|
-
useEffect(() => {
|
|
7
|
-
if (!enabled) {
|
|
8
|
-
return;
|
|
9
|
-
}
|
|
10
|
-
const handler = (e) => {
|
|
11
|
-
if (e.defaultPrevented) {
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
const { key } = e, values = array(meta.value), isOne = meta.limit == 1; //eslint-disable-line eqeqeq
|
|
15
|
-
if (values.length > 0 &&
|
|
16
|
-
(key === 'Backspace' || (isOne && key.length === 1))) {
|
|
17
|
-
return meta.onChange(values.slice(0, -1));
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
document.addEventListener('keydown', handler, true);
|
|
21
|
-
return () => document.removeEventListener('keydown', handler, true);
|
|
22
|
-
}, [enabled, meta]);
|
|
23
|
-
};
|