@neovici/cosmoz-autocomplete 11.0.0 → 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,34 +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';
8
+ import { AUTOCOMPLETE_DESELECT_LAST } from './autocomplete-keybindings';
9
9
  import { EMPTY, normalize, notify, search } from './util';
10
10
  const useNotify = (host, fn, name) => useCallback((val) => {
11
11
  fn?.(val);
12
12
  host.dispatchEvent(new CustomEvent(name, { detail: val }));
13
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, ...thru }) => {
15
- 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) => {
16
- _onChange?.(val, () => setClosed(true));
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));
17
19
  notify(host, 'value', val);
18
- }, [_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]);
19
23
  useEffect(() => source$.then(setOptions), [source$]);
20
- useKeys({
21
- focused,
22
- empty,
23
- limit,
24
- value,
25
- onChange,
26
- onText,
27
- });
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
28
37
  useEffect(() => {
29
- if (!focused && !keepQuery)
38
+ if (!opened && !keepQuery)
30
39
  onText('');
31
- }, [focused, keepQuery]);
40
+ }, [opened, keepQuery]);
32
41
  const meta = useMeta({
33
42
  onText,
34
43
  onChange,
@@ -37,19 +46,19 @@ export const useAutocomplete = ({ value: _value, text, onChange: _onChange, onTe
37
46
  min,
38
47
  keepQuery,
39
48
  keepOpened,
40
- setClosed,
49
+ setOpened,
41
50
  onSelect,
42
51
  });
43
52
  const [, , state] = usePromise(source$);
44
53
  return {
45
- active,
54
+ opened,
46
55
  query,
47
56
  textual,
48
57
  value,
49
58
  source$,
50
59
  loading: state === 'pending',
51
60
  items: useMemo(() => {
52
- if (!active)
61
+ if (!opened)
53
62
  return EMPTY;
54
63
  const items = preserveOrder
55
64
  ? options
@@ -57,7 +66,7 @@ export const useAutocomplete = ({ value: _value, text, onChange: _onChange, onTe
57
66
  return externalSearch ? items : search(items, query, textual);
58
67
  }, [
59
68
  options,
60
- active,
69
+ opened,
61
70
  query,
62
71
  textual,
63
72
  empty,
@@ -66,19 +75,21 @@ export const useAutocomplete = ({ value: _value, text, onChange: _onChange, onTe
66
75
  valueProperty,
67
76
  externalSearch,
68
77
  ]),
69
- onClick: useCallback(() => setClosed(false), []),
70
- onFocus,
78
+ // Sync opened state from cosmoz-dropdown-next's composed dropdown-toggle event
79
+ onToggle: useCallback((e) => {
80
+ setOpened(e.newState === 'open');
81
+ }, []),
71
82
  onText: useCallback((e) => {
72
83
  onText(e.target.value);
73
- setClosed(false);
74
- }, [onText, text, setClosed]),
84
+ setOpened(true);
85
+ }, [onText, text, setOpened]),
75
86
  onSelect: useCallback((newVal) => {
76
87
  meta.onSelect?.(newVal, meta);
77
- const { onChange, onText, limit, min, value: val, keepQuery, keepOpened, setClosed, } = meta;
88
+ const { onChange, onText, limit, min, value: val, keepQuery, keepOpened, setOpened, } = meta;
78
89
  if (!keepQuery)
79
90
  onText('');
80
91
  if (!keepOpened)
81
- setClosed(true);
92
+ setOpened(false);
82
93
  const value = array(val), deselect = value.includes(newVal);
83
94
  if (deselect && value.length === min)
84
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
  };
@@ -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": "11.0.0",
3
+ "version": "12.0.0",
4
4
  "type": "module",
5
5
  "description": "A autocomplete input web component",
6
6
  "keywords": [
@@ -72,13 +72,15 @@
72
72
  "exports": {
73
73
  ".": "./dist/index.js",
74
74
  "./autocomplete": "./dist/autocomplete/index.js",
75
+ "./keybindings": "./dist/autocomplete/autocomplete-keybindings.js",
75
76
  "./listbox": "./dist/listbox/index.js",
77
+ "./listbox/keybindings": "./dist/listbox/listbox-keybindings.js",
76
78
  "./item-renderer": "./dist/listbox/item-renderer.js"
77
79
  },
78
80
  "dependencies": {
79
81
  "@lit-labs/virtualizer": "^2.0.0",
80
- "@neovici/cosmoz-dropdown": "^6.0.0 || ^7.0.0",
81
- "@neovici/cosmoz-input": "^5.0.2",
82
+ "@neovici/cosmoz-dropdown": "^7.3.0",
83
+ "@neovici/cosmoz-input": "^5.6.0",
82
84
  "@neovici/cosmoz-utils": "^6.19.0",
83
85
  "@pionjs/pion": "^2.7.1",
84
86
  "i18next": "^23.16.8",
@@ -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
- };