@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.
@@ -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 { Placement } from '@neovici/cosmoz-dropdown/use-floating';
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 { defaultMiddleware, size, useFloating, } from '@neovici/cosmoz-dropdown/use-floating';
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 middleware = [
22
- size({
23
- apply({ rects, elements }) {
24
- Object.assign(elements.floating.style, {
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
- const disallowedSingle = isSingle && !showSingle;
35
- return !disallowedSingle;
22
+ return hasResultsOrQuery;
36
23
  };
37
24
  const autocomplete = (props) => {
38
- const { active, invalid, errorMessage, label, placeholder, disabled, noLabelFloat, alwaysFloatLabel, textual, text, onText, onFocus, onClick, onDeselect, value, limit, min, showSingle, items, source$, placement, loading, chipRenderer, } = props, host = useHost(), isOne = limit == 1, // eslint-disable-line eqeqeq
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-input
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
- active,
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
- customElements.define('cosmoz-autocomplete-ui', component(Autocomplete, { observedAttributes, styleSheets }));
28
- customElements.define('cosmoz-autocomplete', component(Standalone, { observedAttributes, styleSheets }));
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 }));
@@ -6,6 +6,10 @@ export default css `
6
6
  min-width: 35px;
7
7
  }
8
8
 
9
+ cosmoz-dropdown-next {
10
+ display: block;
11
+ }
12
+
9
13
  cosmoz-input::part(control) {
10
14
  display: flex;
11
15
  gap: 4px;
@@ -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
- setClosed: (closed: boolean) => void;
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, ...thru }: Props<I>) => {
32
- active: boolean | undefined;
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
- onClick: () => void;
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 { useKeys } from './use-keys';
9
- import { EMPTY, normalize, notify, search, useNotify } from './util';
10
- export const useAutocomplete = ({ value: _value, text, onChange: _onChange, onText: _onText, onSelect, limit, min, source, textProperty, textual: _textual, valueProperty, keepOpened, keepQuery, preserveOrder, defaultIndex, externalSearch, ...thru }) => {
11
- const textual = useMemo(() => (_textual ?? strProp)(textProperty), [_textual, textProperty]), { active, focused, onFocus, setClosed } = useFocus(thru), empty = !text, query = useMemo(() => text?.trim(), [text]), host = useHost(), onText = useNotify(host, _onText, 'text'), onChange = useCallback((val) => {
12
- _onChange?.(val, () => setClosed(true));
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' ? source({ query, active }) : source).then(normalize), [source, active, query]), value = useMemo(() => array(_value), [_value]);
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
- useKeys({
17
- focused,
18
- empty,
19
- limit,
20
- value,
21
- onChange,
22
- onText,
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 (!focused && !keepQuery)
38
+ if (!opened && !keepQuery)
26
39
  onText('');
27
- }, [focused, keepQuery]);
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
- setClosed,
49
+ setOpened,
37
50
  onSelect,
38
51
  });
39
52
  const [, , state] = usePromise(source$);
40
53
  return {
41
- active,
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 (!active)
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
- active,
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
- onClick: useCallback(() => setClosed(false), []),
66
- onFocus,
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
- setClosed(false);
70
- }, [onText, text, setClosed]),
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, setClosed, } = meta;
88
+ const { onChange, onText, limit, min, value: val, keepQuery, keepOpened, setOpened, } = meta;
74
89
  if (!keepQuery)
75
90
  onText('');
76
91
  if (!keepOpened)
77
- setClosed(true);
92
+ setOpened(false);
78
93
  const value = array(val), deselect = value.includes(newVal);
79
94
  if (deselect && value.length === min)
80
95
  return;
@@ -1,6 +1,6 @@
1
- export declare const useOverflow: <I>({ value, active, wrap, limit, }: {
1
+ export declare const useOverflow: <I>({ value, opened, wrap, limit, }: {
2
2
  value: I[];
3
- active?: boolean;
3
+ opened?: boolean;
4
4
  wrap?: boolean;
5
5
  limit?: number;
6
6
  }) => void;
@@ -29,7 +29,7 @@ const overflow = (host) => {
29
29
  chips[i].hidden = true;
30
30
  }
31
31
  };
32
- export const useOverflow = ({ value, active, wrap, limit, }) => {
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, active, value]);
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 {string} str string from which to remove accents
8
- * @returns {string} string w/o accents
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 = () => {
@@ -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
@@ -1 +1,3 @@
1
1
  import './autocomplete/';
2
+ export { AUTOCOMPLETE_DESELECT_LAST, autocompleteKeybindings, } from './autocomplete/autocomplete-keybindings';
3
+ export { LISTBOX_NAVIGATE_DOWN, LISTBOX_NAVIGATE_UP, LISTBOX_SELECT, listboxKeybindings, } from './listbox/listbox-keybindings';
@@ -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, setFloating, styles, ...thru }: ListboxProps<I>, content: unknown) => import("lit-html").TemplateResult<1>;
5
+ export declare const listbox: <I>({ multi, ...thru }: ListboxProps<I>, content: unknown) => import("lit-html").TemplateResult<1>;
9
6
  export {};
@@ -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', connectable(component(Listbox, { styleSheets: [sheet(style)] })));
55
- export const listbox = ({ multi, setFloating, styles, ...thru }, content) => html `<cosmoz-listbox
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
- position: fixed;
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
  }
@@ -4,5 +4,5 @@ export interface Handlers {
4
4
  onDown: Handler;
5
5
  onEnter: Handler;
6
6
  }
7
- export declare const useKeyboard: (handlers: Handlers) => void;
7
+ export declare const useKeyboard: ({ onUp, onDown, onEnter }: Handlers) => void;
8
8
  export {};
@@ -1,32 +1,21 @@
1
- import { useMeta } from '@neovici/cosmoz-utils/hooks/use-meta';
2
- import { useEffect } from '@pionjs/pion';
3
- export const useKeyboard = (handlers) => {
4
- const listeners = useMeta(handlers);
5
- useEffect(() => {
6
- const handler = (e) => {
7
- if ((e.ctrlKey && e.altKey) || e.defaultPrevented) {
8
- return;
9
- }
10
- switch (e.key) {
11
- case 'Up':
12
- case 'ArrowUp':
13
- e.preventDefault();
14
- listeners.onUp();
15
- break;
16
- case 'Down':
17
- case 'ArrowDown':
18
- e.preventDefault();
19
- listeners.onDown();
20
- break;
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": "10.12.1",
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": "wtr --coverage",
30
- "test:watch": "wtr --watch",
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": "^6.0.0 || ^7.0.0",
79
- "@neovici/cosmoz-input": "^5.0.2",
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,9 +0,0 @@
1
- export interface Props<I> {
2
- focused?: boolean;
3
- empty?: boolean;
4
- value: I | I[];
5
- limit?: number;
6
- onChange: (v: I[]) => void;
7
- onText: (s: string) => void;
8
- }
9
- export declare const useKeys: <I>({ focused, empty, ...rest }: Props<I>) => void;
@@ -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
- };