@neovici/cosmoz-autocomplete 2.17.0 → 3.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/cosmoz-listbox.js CHANGED
@@ -33,24 +33,24 @@ const Listbox = host => {
33
33
  height: 100%;
34
34
  }
35
35
  .item {
36
+ font-size: 14px;
36
37
  line-height: ${ itemHeight }px;
37
38
  height: ${ itemHeight }px;
38
- padding: 0 16px;
39
+ padding: 0 20px;
39
40
  box-sizing: border-box;
40
41
  width: 100%;
41
42
  cursor: pointer;
42
43
  overflow: hidden;
43
44
  text-overflow: ellipsis;
44
45
  white-space: nowrap;
46
+ transition: background 0.2s;
47
+ color: var(--cosmoz-listbox-color, #101010);
45
48
  }
46
- .item[data-index="${ index }"] {
47
- background: #eee;
48
- color: #333;
49
- }
50
- .item[aria-selected] {
51
- background: #dadada;
52
- color: #333;
49
+
50
+ .item[data-index="${ index }"]{
51
+ background: var(--cosmoz-listbox-active-color, var(--cosmoz-selection-color, rgba(58, 145, 226, 0.1)));
53
52
  }
53
+
54
54
  .sizer {
55
55
  position: relative;
56
56
  visibility: hidden;
@@ -63,6 +63,8 @@ const Listbox = host => {
63
63
  overflow: hidden;
64
64
  max-width: inherit;
65
65
  }
66
+ .items > :first-child { line-height: 1;}
67
+ ${ host.styles || '' }
66
68
  </style>
67
69
  <div class="items">${ scroll({ items, renderItem }) }</div>`;
68
70
  };
@@ -0,0 +1,107 @@
1
+ import { tagged as css } from '@neovici/cosmoz-utils';
2
+ const svg = '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 '
3
+ + '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';
4
+
5
+ export const listboxStyle = css`
6
+ .item {
7
+ padding-left: 0;
8
+ }
9
+ .item::before {
10
+ content: "";
11
+ font-size: 0;
12
+ padding: 7.5px;
13
+ margin: 0 8px;
14
+ background: #fff;
15
+ border: 1px solid #d6d6d5;
16
+ border-radius: 4px;
17
+ vertical-align: top;
18
+ }
19
+
20
+ .item[aria-selected]::before {
21
+ border-color: #5881f6;
22
+ background: url("${ svg }") #5881f6 no-repeat 50%;
23
+ }
24
+ `;
25
+
26
+ export default css`
27
+ :host {
28
+ display: block;
29
+ position: relative;
30
+ }
31
+ .chip {
32
+ border-radius: 500px;
33
+ background: var(--cosmoz-autocomplete-chip-bg-color, #cbcfdb);
34
+ margin: 1px 4px 1px 0;
35
+ white-space: nowrap;
36
+ overflow: hidden;
37
+ display: flex;
38
+ flex-direction: row;
39
+ align-items: center;
40
+
41
+ position: absolute;
42
+ left: 0;
43
+ top: 1px;
44
+ bottom: 1px;
45
+ right: 0;
46
+ margin: 0;
47
+ z-index: 2;
48
+ pointer-events: auto;
49
+ }
50
+ .chip-text {
51
+ color: var(--cosmoz-autocomplete-chip-color, #424242);
52
+ margin: 0 10px;
53
+ font-size: 12px;
54
+ line-height: 22px;
55
+ overflow: hidden;
56
+ text-overflow: ellipsis;
57
+ flex: 1;
58
+ }
59
+ .chip-clear {
60
+ margin: 2px 4px 2px -6px;
61
+ background-color: var(--cosmoz-autocomplete-chip-clear-bg-color, #81899B);
62
+ border-radius: 500px;
63
+ cursor: pointer;
64
+ width: 16px;
65
+ height: 16px;
66
+ stroke: var(--cosmoz-autocomplete-chip-bg-color, #cbcfdb);
67
+ display: block;
68
+ }
69
+ .chip-clear svg {
70
+ display: block;
71
+ transform: translate(3.5px, 3.5px);
72
+ }
73
+
74
+ .badge {
75
+ background: var(--cosmoz-autocomplete-chip-bg-color, #cbcfdb);
76
+ color: var(--cosmoz-autocomplete-chip-color, #424242);
77
+ font-weight: 500;
78
+ font-size: 12px;
79
+ line-height: 22px;
80
+ padding: 0 2px;
81
+ border-radius: 90px;
82
+ text-align: center;
83
+ box-sizing: border-box;
84
+ flex: 0.01 2 30px;
85
+ cursor: pointer;
86
+ }
87
+
88
+ slot {
89
+ display: contents !important;
90
+ }
91
+
92
+ @keyframes rotating {
93
+ 100% {
94
+ transform: rotate(360deg);
95
+ }
96
+ }
97
+ .spinner {
98
+ border-radius: 50%;
99
+ width: 22px;
100
+ height: 22px;
101
+ border: 2px solid rgba(0, 0, 0, 0.1);
102
+ border-top: 2px solid #5f5a92;
103
+ animation: rotating 1.2s infinite cubic-bezier(0.785, 0.135, 0.15, 0.86);
104
+ box-sizing: border-box;
105
+ margin-top: -3px;
106
+ }
107
+ `;
@@ -7,96 +7,30 @@ import { array } from '@neovici/cosmoz-utils/lib/array';
7
7
  import '@neovici/cosmoz-input';
8
8
  import { useAutocomplete } from './use-autocomplete';
9
9
  import { listbox } from '../cosmoz-listbox';
10
+ import style, { listboxStyle } from './autocomplete.css';
10
11
 
11
12
  const
12
13
  blank = () => nothing,
13
14
  renderChip = (text, onClear, disabled) => html`
14
- <div class="chip" part="chip" title=${ text }>
15
+ <div class="chip" part="chip" slot="suffix" title=${ text }>
15
16
  <span class="chip-text" part="chip-text">${ text }</span>
16
17
  ${ !disabled && html`
17
18
  <span class="chip-clear" part="chip-clear" @click=${ onClear }>
18
- <svg viewBox="0 0 24 24" preserveAspectRatio="xMidYMid meet" focusable="false">
19
- <g><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path></g>
19
+ <svg width="9" height="8" viewBox="0 0 9 8" fill="none" xmlns="http://www.w3.org/2000/svg">
20
+ <line x1="7.53033" y1="0.994808" x2="1.16637" y2="7.35877" stroke-width="1.5"/>
21
+ <line x1="7.46967" y1="7.35882" x2="1.10571" y2="0.99486" stroke-width="1.5"/>
20
22
  </svg>
21
23
  </span>` || nothing }
22
24
  </div>
23
25
  `,
24
- renderChips = (value, textual, onDeselect, disabled) => array(value).map(value =>
25
- renderChip(textual(value), () => onDeselect(value), disabled)
26
- ),
27
- style = `
28
- :host {
29
- display: block;
30
- position: relative;
26
+ renderBadge = len => len > 0 ? html`<div class="badge" slot="suffix" part="badge">${ len }</div>` : nothing,
27
+ renderSelection = (value, isOne, onDeselect, textual, disabled) => {
28
+ if (isOne) {
29
+ const val = value[0];
30
+ return val ? renderChip(textual(val), () => onDeselect(val), disabled) : nothing;
31
31
  }
32
- .chips {
33
- display: flex;
34
- flex-direction: row;
35
- flex-wrap: wrap;
36
- align-items: flex-end;
37
- }
38
- .chip {
39
- border-radius: 500px;
40
- background-color: #e0e0e0;
41
- margin: 1px 4px 1px 0;
42
- white-space: nowrap;
43
- overflow: hidden;
44
- display: flex;
45
- flex-direction: row;
46
- align-items: center;
47
- }
48
- .chip-text {
49
- color: #424242;
50
- margin: 0 10px;
51
- font-size: 13px;
52
- overflow: hidden;
53
- text-overflow: ellipsis;
54
- flex: 1;
55
- }
56
- .chip-clear {
57
- margin: 2px 4px 2px -6px;
58
- background-color: #a6a6a6;
59
- border-radius: 500px;
60
- cursor: pointer;
61
- width: 16px;
62
- height: 16px;
63
- fill: #cdcdcd;
64
- display: block;
65
- }
66
- .chip-clear svg { display: block; }
67
-
68
- cosmoz-input .chip {
69
- position: absolute;
70
- left: 0;
71
- top: 1px;
72
- bottom: 1px;
73
- right: 0;
74
- margin: 0;
75
- z-index: 2;
76
- pointer-events: auto;
77
- }
78
-
79
- slot {
80
- display: contents !important;
81
- }
82
-
83
- @keyframes rotating {
84
- 100% {
85
- transform: rotate(360deg);
86
- }
87
- }
88
- .spinner {
89
- border-radius: 50%;
90
- width: 22px;
91
- height: 22px;
92
- border: 2px solid rgba(0, 0, 0, 0.1);
93
- border-top: 2px solid #5f5a92;
94
- animation: rotating 1.2s infinite cubic-bezier(0.785, 0.135, 0.15, 0.86);
95
- box-sizing: border-box;
96
- margin-top: -3px;
97
- }
98
- `,
99
-
32
+ return renderBadge(value.length);
33
+ },
100
34
  inputParts = ['input', 'label', 'line', 'error'].map(part => `${ part }: input-${ part }`).join(),
101
35
 
102
36
  autocomplete = props => {
@@ -123,21 +57,18 @@ const
123
57
  } = props,
124
58
  host = useHost(),
125
59
 
126
- chips = renderChips(value, textual, onDeselect, disabled),
127
- isOne = limit == 1, //eslint-disable-line eqeqeq
60
+ isOne = limit == 1, // eslint-disable-line eqeqeq
128
61
  isSingle = isOne && array(value)?.[0] != null,
129
62
  anchor = useCallback(() => host.shadowRoot.querySelector('#input'), [host, value]),
130
63
  suggestions = until(items$.then(items => (!isSingle || showSingle) && items.length && listbox({
131
64
  ...props,
132
65
  anchor,
133
- items
66
+ items,
67
+ styles: !isOne ? listboxStyle : undefined
134
68
  }) || nothing));
135
69
 
136
70
  return html`
137
71
  <style>${ style }</style>
138
-
139
- ${ !isOne && html`<div class="chips">${ chips }</div>` || '' }
140
-
141
72
  <cosmoz-input
142
73
  id="input" part="input"
143
74
  .label=${ label }
@@ -157,9 +88,10 @@ const
157
88
  >
158
89
  <slot name="prefix" slot="prefix"></slot>
159
90
  <slot name="suffix" slot="suffix"></slot>
160
- ${ isOne && html`<div class="chips" slot="prefix">${ chips }</div>` || '' }
91
+ ${ renderSelection(array(value), isOne, onDeselect, textual, disabled) }
161
92
  ${ until(values$.then(blank, blank), html`<div slot="suffix" class="spinner"></div>`) }
162
93
  </cosmoz-input>
94
+
163
95
  ${ suggestions }`;
164
96
  },
165
97
  Autocomplete = props => autocomplete({
@@ -174,7 +106,8 @@ const
174
106
  'text-property',
175
107
  'limit',
176
108
  'show-single',
177
- 'show-selection',
109
+ 'keep-query',
110
+ 'keep-opened',
178
111
  'hide-empty',
179
112
  'default-index',
180
113
  'item-height',
@@ -2,11 +2,12 @@ import {
2
2
  useCallback, useMemo
3
3
  } from 'haunted';
4
4
  import {
5
- without, search, strProp, prop
5
+ without, search, strProp, prop, array
6
6
  } from './utils';
7
7
  import { useHost } from '@neovici/cosmoz-utils/lib/hooks/use-host';
8
+ import { useMeta } from '@neovici/cosmoz-utils/lib/hooks/use-meta';
9
+ import { useFocus } from '@neovici/cosmoz-dropdown/src/use-focus.js';
8
10
  import { useKeys } from './use-keys';
9
- import { useFocus } from './use-focus';
10
11
 
11
12
  const
12
13
  EMPTY = [],
@@ -27,7 +28,7 @@ const
27
28
  valueProperty,
28
29
  external,
29
30
  hideEmpty,
30
- showSelection,
31
+ keepQuery,
31
32
  ...thru
32
33
  }) => {
33
34
  const
@@ -45,7 +46,7 @@ const
45
46
  ? source({ query, active })
46
47
  : source
47
48
  ), [source, active, query]),
48
- values$ = useMemo(() => showSelection ? source$ : source$.then(without(value, prop(valueProperty))), [source$, showSelection, value, valueProperty]);
49
+ values$ = useMemo(() => source$.then(source => [...array(value), ...without(value, prop(valueProperty))(source)]), [source$, value, valueProperty]);
49
50
 
50
51
  useKeys({
51
52
  active,
@@ -57,6 +58,8 @@ const
57
58
  onText
58
59
  });
59
60
 
61
+ const meta = useMeta({ onText, onChange, value, limit, keepQuery, setClosed });
62
+
60
63
  return {
61
64
  query,
62
65
  textual,
@@ -76,10 +79,17 @@ const
76
79
  setClosed(false);
77
80
  }, [onText, text, setClosed]),
78
81
  onSelect: useCallback(newVal => {
79
- onText('');
80
- onChange([...without(newVal)(value), newVal].slice(-limit));
81
- }, [onText, onChange, value, limit]),
82
- onDeselect: useCallback(val => onChange(without(val)(value)), [onChange, value])
82
+ const { onChange, onText, limit, value: val, keepQuery, keepOpened } = meta;
83
+ if (!keepQuery) {
84
+ onText('');
85
+ }
86
+ if (!keepOpened) {
87
+ setClosed(true);
88
+ }
89
+ const value = array(val);
90
+ onChange((value.includes(newVal) ? without(newVal)(value) : [...value, newVal]).slice(-limit));
91
+ }, [meta]),
92
+ onDeselect: useCallback(val => meta.onChange(without(val)(meta.value)), [meta])
83
93
  };
84
94
  };
85
95
 
@@ -1,9 +1,9 @@
1
1
  import { useCallback, useEffect, useState, useMemo } from 'haunted';
2
2
  import { useMeta } from '@neovici/cosmoz-utils/lib/hooks/use-meta';
3
+ import { usePosition } from '@neovici/cosmoz-dropdown/src/use-position.js';
3
4
  import { useKeyboard } from './use-keyboard';
4
5
  import defaultItemRenderer from './item-renderer';
5
6
  import { byValue } from './utils';
6
- import { usePosition } from './use-position';
7
7
 
8
8
  const useItems = ({
9
9
  items,
@@ -50,7 +50,7 @@ const useItems = ({
50
50
  query,
51
51
  textual,
52
52
  itemRenderer,
53
- itemHeight = 36,
53
+ itemHeight = 40,
54
54
  itemLimit = 5,
55
55
  anchor,
56
56
  confinement,
@@ -72,7 +72,8 @@ const useItems = ({
72
72
  anchor,
73
73
  host,
74
74
  confinement,
75
- placement
75
+ placement,
76
+ limit: true
76
77
  });
77
78
 
78
79
  return {
@@ -92,7 +93,7 @@ const useItems = ({
92
93
  })
93
94
  };
94
95
  },
95
- properties = ['query', 'items', 'onSelect', 'textual', 'anchor', 'confinement', 'placement', 'itemHeight', 'itemLimit', 'defaultIndex', 'value'];
96
+ properties = ['query', 'items', 'onSelect', 'textual', 'anchor', 'confinement', 'placement', 'itemHeight', 'itemLimit', 'defaultIndex', 'value', 'styles'];
96
97
 
97
98
  export {
98
99
  useListbox,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neovici/cosmoz-autocomplete",
3
- "version": "2.17.0",
3
+ "version": "3.0.0",
4
4
  "description": "A autocomplete input web component",
5
5
  "keywords": [
6
6
  "lit-html",
@@ -56,7 +56,7 @@
56
56
  ]
57
57
  },
58
58
  "dependencies": {
59
- "@neovici/cosmoz-input": "^1.4.0",
59
+ "@neovici/cosmoz-input": "^1.5.0",
60
60
  "@neovici/cosmoz-utils": "^3.20.0",
61
61
  "@open-wc/lit-helpers": "^0.3.12",
62
62
  "haunted": "^4.7.0",
@@ -65,20 +65,18 @@
65
65
  "position.js": "^0.3.0"
66
66
  },
67
67
  "devDependencies": {
68
- "@commitlint/cli": "^13.0.0",
69
- "@commitlint/config-conventional": "^13.0.0",
70
- "@neovici/eslint-config": "^1.2.0",
68
+ "@commitlint/cli": "^16.0.0",
69
+ "@commitlint/config-conventional": "^16.0.0",
70
+ "@neovici/cfg": "^1.13.0",
71
+ "@neovici/cosmoz-dropdown": "^1.8.0",
71
72
  "@open-wc/demoing-storybook": "^2.1.0",
72
73
  "@open-wc/testing": "^2.5.32",
73
- "@semantic-release/changelog": "^5.0.1",
74
- "@semantic-release/git": "^9.0.0",
74
+ "@semantic-release/changelog": "^6.0.0",
75
+ "@semantic-release/git": "^10.0.0",
75
76
  "@storybook/storybook-deployer": "^2.8.5",
76
- "@web/test-runner": "^0.13.0",
77
- "@web/test-runner-selenium": "^0.5.0",
78
- "eslint": "^7.0.0",
79
77
  "husky": "^7.0.0",
80
- "semantic-release": "^17.4.0",
81
- "sinon": "^11.0.0",
82
- "typescript": "^4.0.0"
78
+ "semantic-release": "^19.0.0",
79
+ "sinon": "^13.0.0",
80
+ "typescript": "^4.6.0"
83
81
  }
84
82
  }
package/lib/use-focus.js DELETED
@@ -1,53 +0,0 @@
1
- import { useEffect, useState, useCallback } from 'haunted';
2
- import { useMeta } from '@neovici/cosmoz-utils/lib/hooks/use-meta';
3
-
4
- export const useFocus = ({ disabled, onFocus }) => {
5
- const [{ focused, closed } = {}, setState] = useState(),
6
- active = focused && !disabled,
7
- meta = useMeta({ closed, onFocus }),
8
- setClosed = useCallback(closed => setState(p => ({ ...p, closed })), []),
9
- onToggle = useCallback(e => {
10
- const target = e.currentTarget,
11
- focusing = target._focusing;
12
- target._focusing = false;
13
- if (focusing) {
14
- return;
15
- }
16
- setState(p => ({ ...p, closed: !p.closed }));
17
- }, []);
18
-
19
- useEffect(() => {
20
- if (!active) {
21
- return;
22
- }
23
- const handler = e => {
24
- if (e.defaultPrevented) {
25
- return;
26
- }
27
- const { closed } = meta;
28
- if (e.key === 'Escape' && !closed) {
29
- e.preventDefault();
30
- setClosed(true);
31
- } else if (['ArrowUp', 'Up'].includes(e.key) && closed) {
32
- e.preventDefault();
33
- setClosed(false);
34
- }
35
- };
36
- document.addEventListener('keydown', handler, true);
37
- return () => document.removeEventListener('keydown', handler, true);
38
- }, [active]);
39
-
40
- return {
41
- active: active && !closed,
42
- setClosed,
43
- onToggle,
44
- onFocus: useCallback(e => {
45
- const target = e.currentTarget,
46
- focused = target.matches(':focus-within')
47
- || e.type === 'focusout' && e.relatedTarget?.closest('[data-dropdown]');
48
- target._focusing = focused;
49
- setState({ focused });
50
- meta.onFocus?.(focused);
51
- }, [meta])
52
- };
53
- };
@@ -1,49 +0,0 @@
1
- import { useEffect } from 'haunted';
2
- import getPosition from 'position.js';
3
- import { onScrolled } from './on-scrolled';
4
-
5
- const defaultPlacement = ['bottom-left', 'bottom-right', 'bottom', 'top-left', 'top-right', 'top'],
6
- position = (host, anchor, placement = defaultPlacement, confinement) => {
7
- const anchorBounds = anchor.getBoundingClientRect(),
8
- hostBounds = host.getBoundingClientRect(),
9
- { popupOffset: offset } = getPosition(hostBounds, anchorBounds, placement, {
10
- fixed: true,
11
- adjustXY: 'both',
12
- offsetParent: confinement
13
- }),
14
- { style } = host;
15
- style.left = offset.left + 'px';
16
- style.top = offset.top + 'px';
17
- style.minWidth = Math.max(anchorBounds.width, hostBounds.width) + 'px';
18
- },
19
- usePosition = ({ anchor: anchorage, host, confinement, placement }) => {
20
- useEffect(() => {
21
- const anchor = typeof anchorage === 'function' ? anchorage() : anchorage;
22
- if (anchor == null) {
23
- return;
24
- }
25
- let rid;
26
- const reposition = () => position(host, anchor, placement, confinement),
27
- ro = new ResizeObserver(reposition);
28
-
29
- ro.observe(host);
30
- ro.observe(anchor);
31
-
32
- const onReposition = () => {
33
- cancelAnimationFrame(rid);
34
- rid = requestAnimationFrame(reposition);
35
- },
36
- offScroll = onScrolled(anchor, onReposition);
37
- window.addEventListener('resize', onReposition, true);
38
-
39
- return () => {
40
- ro.unobserve(host);
41
- ro.unobserve(anchor);
42
- offScroll();
43
- window.removeEventListener('resize', onReposition, true);
44
- cancelAnimationFrame(rid);
45
- };
46
- }, [anchorage, confinement, placement]);
47
- };
48
-
49
- export { usePosition };