@neovici/cosmoz-autocomplete 10.8.0 → 10.10.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.
@@ -1,8 +1,10 @@
1
+ import { Placement } from '@neovici/cosmoz-dropdown/use-floating';
1
2
  import '@neovici/cosmoz-input';
3
+ import { ItemRenderer } from '../listbox/item-renderer';
4
+ import { ChipRenderer } from './selection';
2
5
  import './skeleton-span';
3
6
  import style from './styles.css';
4
7
  import { Props as Base, RProps } from './use-autocomplete';
5
- import { Placement } from '@neovici/cosmoz-dropdown/use-floating';
6
8
  export interface Props<I> extends Base<I> {
7
9
  invalid?: boolean;
8
10
  errorMessage?: string;
@@ -17,6 +19,8 @@ export interface Props<I> extends Base<I> {
17
19
  defaultIndex?: number;
18
20
  externalSearch?: boolean;
19
21
  placement?: Placement;
22
+ itemRenderer?: ItemRenderer<I>;
23
+ chipRenderer?: ChipRenderer<I>;
20
24
  }
21
25
  type AProps<I> = Omit<Props<I>, keyof RProps<I>> & RProps<I> & {
22
26
  onInputRef?: (el?: Element) => void;
@@ -1,19 +1,19 @@
1
+ import { defaultMiddleware, size, useFloating, } from '@neovici/cosmoz-dropdown/use-floating';
1
2
  import '@neovici/cosmoz-input';
2
- import './skeleton-span';
3
3
  import { useHost } from '@neovici/cosmoz-utils/hooks/use-host';
4
4
  import { useImperativeApi } from '@neovici/cosmoz-utils/hooks/use-imperative-api';
5
+ import { useEffect } from '@pionjs/pion';
5
6
  import { html } from 'lit-html';
6
7
  import { live } from 'lit-html/directives/live.js';
8
+ import { ref } from 'lit-html/directives/ref.js';
7
9
  import { until } from 'lit-html/directives/until.js';
8
10
  import { when } from 'lit-html/directives/when.js';
9
11
  import { listbox } from '../listbox';
10
12
  import { selection } from './selection';
13
+ import './skeleton-span';
11
14
  import style from './styles.css';
12
15
  import { useAutocomplete } from './use-autocomplete';
13
16
  import { useOverflow } from './use-overflow';
14
- import { ref } from 'lit-html/directives/ref.js';
15
- import { useFloating, defaultMiddleware, size, } from '@neovici/cosmoz-dropdown/use-floating';
16
- import { useEffect } from '@pionjs/pion';
17
17
  const inputParts = ['input', 'control', 'label', 'line', 'error', 'wrap']
18
18
  .map((part) => `${part}: input-${part}`)
19
19
  .join();
@@ -34,7 +34,7 @@ const shouldShowDropdown = ({ active, isSingle, showSingle, }) => {
34
34
  return !disallowedSingle;
35
35
  };
36
36
  const autocomplete = (props) => {
37
- const { active, invalid, errorMessage, label, placeholder, disabled, noLabelFloat, alwaysFloatLabel, textual, text, onText, onFocus, onClick, onDeselect, value, limit, min, showSingle, items, source$, placement, loading, } = props, host = useHost(), isOne = limit == 1, // eslint-disable-line eqeqeq
37
+ 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
38
38
  isSingle = isOne && value?.[0] != null;
39
39
  const { styles, setReference, setFloating } = useFloating({
40
40
  placement,
@@ -81,6 +81,7 @@ const autocomplete = (props) => {
81
81
  onDeselect,
82
82
  textual,
83
83
  disabled,
84
+ chipRenderer,
84
85
  })}
85
86
  </cosmoz-input>
86
87
 
@@ -3,21 +3,25 @@ export default css `
3
3
  :host {
4
4
  border-radius: var(--cosmoz-autocomplete-chip-border-radius, 500px);
5
5
  background: var(--cosmoz-autocomplete-chip-bg-color, #cbcfdb);
6
- margin: 0px 0 2px 0;
6
+ margin-bottom: 2px;
7
7
  display: flex;
8
- flex-direction: row;
9
8
  align-items: center;
10
9
  flex: 0.0001 1 fit-content;
11
10
  max-width: 18ch;
12
11
  min-width: 40px;
13
- padding: 0 4px 0 8px;
12
+ padding-inline: 8px;
14
13
  gap: 4px;
15
14
  cursor: pointer;
16
15
  transform: translateY(var(--cosmoz-autocomplete-chip-translate-y, 0));
17
16
  }
17
+
18
18
  .content {
19
19
  color: var(--cosmoz-autocomplete-chip-color, #424242);
20
- font-family: var(--cosmoz-autocomplete-chip-text-font-family, 'Inter', sans-serif);
20
+ font-family: var(
21
+ --cosmoz-autocomplete-chip-text-font-family,
22
+ 'Inter',
23
+ sans-serif
24
+ );
21
25
  font-size: var(--cosmoz-autocomplete-chip-text-font-size, 12px);
22
26
  font-weight: var(--cosmoz-autocomplete-chip-text-font-weight, 400);
23
27
  line-height: var(--cosmoz-autocomplete-chip-text-line-height, 22px);
@@ -26,13 +30,16 @@ export default css `
26
30
  white-space: nowrap;
27
31
  flex: auto;
28
32
  min-width: 16px;
33
+ text-align: center;
29
34
  }
35
+
30
36
  .clear {
31
37
  background-color: var(--cosmoz-autocomplete-chip-clear-bg-color, #81899b);
32
38
  border-radius: 50%;
33
39
  cursor: pointer;
34
40
  width: 16px;
35
41
  height: 16px;
42
+ margin-right: -4px;
36
43
  stroke: var(
37
44
  --cosmoz-autocomplete-chip-clear-stroke,
38
45
  var(--cosmoz-autocomplete-chip-bg-color, #cbcfdb)
@@ -40,6 +47,7 @@ export default css `
40
47
  display: var(--cosmoz-autocomplete-chip-clear-display, block);
41
48
  flex: none;
42
49
  }
50
+
43
51
  .clear:hover {
44
52
  filter: brightness(90%);
45
53
  }
@@ -3,11 +3,12 @@ interface Props {
3
3
  disabled?: boolean;
4
4
  }
5
5
  export declare const Chip: ({ onClear, disabled }: Props) => import("lit-html").TemplateResult<1>;
6
- interface ChipProps extends Props {
6
+ export interface ChipProps<I> extends Props {
7
+ item: I | null;
7
8
  slot?: string;
8
9
  className?: string;
9
10
  content: unknown;
10
11
  hidden?: boolean;
11
12
  }
12
- export declare const chip: ({ content, onClear, disabled, hidden, className, slot, }: ChipProps) => import("lit-html").TemplateResult<1>;
13
+ export declare const chip: <I>({ content, onClear, disabled, hidden, className, slot, }: ChipProps<I>) => import("lit-html").TemplateResult<1>;
13
14
  export {};
@@ -1,5 +1,7 @@
1
+ import { chip as defaultChipRenderer } from './chip';
1
2
  import type { RProps } from './use-autocomplete';
2
3
  type Deselect<I> = RProps<I>['onDeselect'];
4
+ export type ChipRenderer<I> = typeof defaultChipRenderer<I>;
3
5
  interface Props<I> {
4
6
  value: I[];
5
7
  min?: number;
@@ -7,6 +9,7 @@ interface Props<I> {
7
9
  onDeselect: Deselect<I>;
8
10
  textual: (i: I) => string;
9
11
  disabled?: boolean;
12
+ chipRenderer?: ChipRenderer<I>;
10
13
  }
11
- export declare const selection: <I>({ value: values, min, onDeselect, textual, disabled, }: Props<I>) => import("lit-html").TemplateResult<1>[];
14
+ export declare const selection: <I>({ value: values, min, onDeselect, textual, disabled, chipRenderer, }: Props<I>) => import("lit-html").TemplateResult<1>[];
12
15
  export {};
@@ -1,13 +1,15 @@
1
1
  import { html } from 'lit-html';
2
- import { chip } from './chip';
3
- export const selection = ({ value: values, min = 0, onDeselect, textual, disabled, }) => [
4
- ...values.filter(Boolean).map((value) => chip({
2
+ import { chip as defaultChipRenderer } from './chip';
3
+ export const selection = ({ value: values, min = 0, onDeselect, textual, disabled, chipRenderer = defaultChipRenderer, }) => [
4
+ ...values.filter(Boolean).map((value) => chipRenderer({
5
+ item: value,
5
6
  content: textual(value),
6
7
  onClear: values.length > min && (() => onDeselect(value)),
7
8
  disabled,
8
9
  slot: 'control',
9
10
  })),
10
- chip({
11
+ chipRenderer({
12
+ item: null,
11
13
  content: html `<span></span>`,
12
14
  className: 'badge',
13
15
  disabled: true,
@@ -1,7 +1,7 @@
1
- type Source<I> = (opts: {
1
+ export type Source<I> = (opts: {
2
2
  query?: string;
3
3
  active?: boolean;
4
- }) => PromiseLike<I[]>;
4
+ }) => PromiseLike<I[]> | undefined;
5
5
  interface Base<I> {
6
6
  value?: I | I[];
7
7
  limit?: number;
@@ -84,6 +84,6 @@ export const useAutocomplete = ({ value: _value, text, onChange: _onChange, onTe
84
84
  }, [meta]),
85
85
  onDeselect: useCallback((val) => meta.onChange(without(val)(meta.value)), [meta]),
86
86
  // whenever there is a query, override defaultIndex to 0, so the first result gets selected
87
- defaultIndex: (query !== undefined && query?.length > 0) ? 0 : defaultIndex,
87
+ defaultIndex: query !== undefined && query?.length > 0 ? 0 : defaultIndex,
88
88
  };
89
89
  };
@@ -1,5 +1,5 @@
1
1
  export declare const search: <I>(source: I[], query: string | undefined, textual: (i: I) => string) => I[];
2
- export declare const normalize: <I>(source: I[] | false | null) => I[];
2
+ export declare const normalize: <I>(source: I[] | false | null | undefined) => I[];
3
3
  export declare const notify: <T>(host: EventTarget, name: string, detail: T) => boolean;
4
4
  export declare const useNotify: <V>(host: EventTarget, fn: undefined | ((v: V) => void), name: string) => (val: V) => void;
5
5
  export declare const EMPTY: never[];
@@ -0,0 +1 @@
1
+ import '../autocomplete';
@@ -0,0 +1,61 @@
1
+ import { component, html, useCallback, useMemo, useProperty, } from '@pionjs/pion';
2
+ import { nothing } from 'lit-html';
3
+ import { ifDefined } from 'lit-html/directives/if-defined.js';
4
+ import '../autocomplete';
5
+ import { Autocomplete, observedAttributes, style, } from '../autocomplete/autocomplete';
6
+ import { useExcludingSelection } from './use-excluding-selection';
7
+ import { unwrap } from './utils';
8
+ import excludingStyle from './style.css';
9
+ const isItemExcluded = (value, item) => value?.some((v) => v.item === item && v.excluded);
10
+ const excludedState = (value, item) => (item && isItemExcluded(value, item) ? 'excluded' : nothing);
11
+ const mkItemRenderer = (value) => (item, i, { highlight, select, textual, isSelected }) => {
12
+ const rendered = textual(item);
13
+ return html `<div
14
+ class="item"
15
+ role="option"
16
+ part="option ${excludedState(value, item)}"
17
+ ?aria-selected=${isSelected(item)}
18
+ data-index=${i}
19
+ @mouseenter=${() => highlight(i)}
20
+ @click=${() => select(item)}
21
+ @mousedown=${(e) => e.preventDefault()}
22
+ >
23
+ ${rendered}
24
+ </div>
25
+ <div class="sizer" virtualizer-sizer>${rendered}</div>`;
26
+ };
27
+ const mkChipRenderer = (value, onClear) => ({ item, content, disabled, hidden, className = 'chip', slot, }) => html `<cosmoz-autocomplete-chip
28
+ class=${ifDefined(className)}
29
+ slot=${ifDefined(slot)}
30
+ part="chip"
31
+ exportparts="chip-text, chip-clear"
32
+ data-state=${excludedState(value, item)}
33
+ ?disabled=${disabled}
34
+ ?hidden=${hidden}
35
+ .onClear=${() => onClear(item)}
36
+ title=${ifDefined(typeof content === 'string' ? content : undefined)}
37
+ >
38
+ ${content}
39
+ </cosmoz-autocomplete-chip>`;
40
+ const AutocompleteExcluding = (props) => {
41
+ const { value, setValue, setExcludingValue } = useExcludingSelection('value');
42
+ const [text, setText] = useProperty('text');
43
+ const onClear = useCallback((item) => setValue((values) => values?.filter((i) => i.item !== item)), []);
44
+ return Autocomplete({
45
+ ...props,
46
+ value: useMemo(() => value?.map(unwrap), [value]),
47
+ onChange: useCallback((value) => {
48
+ setExcludingValue(value);
49
+ }, []),
50
+ text,
51
+ onText: useCallback((text) => {
52
+ setText(text);
53
+ }, []),
54
+ itemRenderer: useMemo(() => mkItemRenderer(value), [value]),
55
+ chipRenderer: useMemo(() => mkChipRenderer(value, onClear), [value, onClear]),
56
+ });
57
+ };
58
+ customElements.define('cosmoz-autocomplete-excluding', component(AutocompleteExcluding, {
59
+ observedAttributes,
60
+ styleSheets: [style, excludingStyle],
61
+ }));
@@ -0,0 +1,2 @@
1
+ declare const excludingStyle: string;
2
+ export default excludingStyle;
@@ -0,0 +1,43 @@
1
+ import { css } from '@pionjs/pion';
2
+ const clearSVG =
3
+ /* eslint-disable quotes */
4
+ "data:image/svg+xml,%3Csvg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath " +
5
+ "d='M2.5 2.5L8.5 8.5M8.5 2.5L2.5 8.5' stroke='white' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E";
6
+ /* eslint-enable quotes */
7
+ const excludingStyle = css `
8
+ .chip[data-state='excluded'] {
9
+ background: var(--cosmoz-autocomplete-excluded-bg-color, rgb(244, 67, 54));
10
+ }
11
+
12
+ cosmoz-autocomplete-chip[data-state='excluded']::part(content) {
13
+ color: var(--cosmoz-autocomplete-excluded-chip-color, #fff);
14
+ }
15
+
16
+ cosmoz-autocomplete-chip[data-state='excluded']::part(clear) {
17
+ background-color: var(
18
+ --cosmoz-autocomplete-excluded-chip-clear-bg-color,
19
+ #fff
20
+ );
21
+ stroke: var(
22
+ --cosmoz-autocomplete-excluded-chip-clear-stroke,
23
+ var(--cosmoz-autocomplete-excluded-bg-color, rgb(244, 67, 54))
24
+ );
25
+ }
26
+
27
+ cosmoz-listbox::part(excluded)::before {
28
+ border-color: var(
29
+ --cosmoz-autocomplete-excluded-bg-color,
30
+ rgb(244, 67, 54)
31
+ );
32
+ /* prettier-ignore */
33
+ background: url("${clearSVG}") var(--cosmoz-autocomplete-excluded-bg-color, rgb(244, 67, 54)) no-repeat 50%;
34
+ }
35
+
36
+ cosmoz-listbox::part(excluded):hover {
37
+ background: var(
38
+ --cosmoz-listbox-excluded-active-color,
39
+ rgba(244, 67, 54, 0.1)
40
+ );
41
+ }
42
+ `;
43
+ export default excludingStyle;
@@ -0,0 +1,4 @@
1
+ export interface WrappedItem<I> {
2
+ item: I;
3
+ excluded: boolean;
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import { StateUpdater } from '@pionjs/pion';
2
+ import '../autocomplete';
3
+ import { WrappedItem } from './types';
4
+ export declare const useExcludingSelection: <I>(property: string) => {
5
+ readonly value: WrappedItem<I>[] | undefined;
6
+ readonly setExcludingValue: StateUpdater<I[] | undefined>;
7
+ readonly setValue: StateUpdater<WrappedItem<I>[] | undefined>;
8
+ };
@@ -0,0 +1,26 @@
1
+ import { invoke } from '@neovici/cosmoz-utils/function';
2
+ import { useCallback, useProperty } from '@pionjs/pion';
3
+ import '../autocomplete';
4
+ import { unwrap, wrap } from './utils';
5
+ export const useExcludingSelection = (property) => {
6
+ const [value, setValue] = useProperty(property);
7
+ const setExcludingValue = useCallback((next) => setValue((prev) => {
8
+ const _next = invoke(next, prev?.map(unwrap));
9
+ if (!_next)
10
+ return undefined;
11
+ if (!prev)
12
+ return _next.map(wrap);
13
+ const results = prev.reduce((nextState, item) => {
14
+ if (_next.includes(item.item))
15
+ return [...nextState, item];
16
+ else if (item.excluded)
17
+ return nextState;
18
+ return [...nextState, { ...item, excluded: true }];
19
+ }, []);
20
+ const newItems = _next
21
+ .filter((i) => !prev.some((p) => p.item === i))
22
+ .map(wrap);
23
+ return [...results, ...newItems];
24
+ }), []);
25
+ return { value, setExcludingValue, setValue };
26
+ };
@@ -0,0 +1,3 @@
1
+ import { WrappedItem } from './types';
2
+ export declare const wrap: <I>(item: I) => WrappedItem<I>;
3
+ export declare const unwrap: <I>(item: WrappedItem<I>) => I;
@@ -0,0 +1,2 @@
1
+ export const wrap = (item) => ({ item, excluded: false });
2
+ export const unwrap = (item) => item.item;
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  import './autocomplete/';
2
+ export type { Source } from './autocomplete/use-autocomplete';
@@ -1,6 +1,6 @@
1
1
  import { css } from '@pionjs/pion';
2
2
  import { when } from 'lit-html/directives/when.js';
3
- const svg =
3
+ const checkSVG =
4
4
  /* eslint-disable quotes */
5
5
  "data:image/svg+xml,%3Csvg width='11' height='8' viewBox='0 0 11 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath " +
6
6
  "d='M9.5 1L5.20039 7.04766L1.66348 3.46152' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E";
@@ -10,7 +10,10 @@ const style = css `
10
10
  position: fixed;
11
11
  z-index: 1000;
12
12
  font-family: var(--paper-font-subhead_-_font-family, inherit);
13
- background: var(--cosmoz-autocomplete-listbox-bg, rgba(255, 255, 255, 0.75));
13
+ background: var(
14
+ --cosmoz-autocomplete-listbox-bg,
15
+ rgba(255, 255, 255, 0.75)
16
+ );
14
17
  min-width: 50px;
15
18
  backdrop-filter: blur(16px) saturate(180%);
16
19
  -webkit-backdrop-filter: blur(16px) saturate(180%);
@@ -81,6 +84,7 @@ const style = css `
81
84
  :host([multi]) .item {
82
85
  padding-left: 0;
83
86
  }
87
+
84
88
  :host([multi]) .item::before {
85
89
  content: '';
86
90
  font-size: 0;
@@ -91,14 +95,17 @@ const style = css `
91
95
  border-radius: 4px;
92
96
  vertical-align: top;
93
97
  }
98
+
94
99
  :host([multi]) .item[aria-selected]::before {
95
100
  border-color: #5881f6;
96
101
  /* prettier-ignore */
97
- background: url("${svg}") #5881f6 no-repeat 50%;
102
+ background: url("${checkSVG}") #5881f6 no-repeat 50%;
98
103
  }
104
+
99
105
  :host([multi]) .sizer {
100
106
  padding-left: 33px;
101
107
  }
108
+
102
109
  .swatch {
103
110
  width: 18px;
104
111
  height: 18px;
@@ -107,6 +114,7 @@ const style = css `
107
114
  vertical-align: middle;
108
115
  border-radius: 50%;
109
116
  }
117
+
110
118
  [virtualizer-sizer]:not(.sizer) {
111
119
  line-height: 1;
112
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neovici/cosmoz-autocomplete",
3
- "version": "10.8.0",
3
+ "version": "10.10.0",
4
4
  "description": "A autocomplete input web component",
5
5
  "keywords": [
6
6
  "lit-html",