html-combobox-element 0.0.2 → 0.0.4

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.
Files changed (35) hide show
  1. package/README.md +123 -0
  2. package/dist/cjs/combobox/Boolean.attribute.value.normalizer.d.ts +2 -0
  3. package/dist/cjs/combobox/Combobox.markup.d.ts +35 -0
  4. package/dist/cjs/combobox/Combobox.markup.js +178 -102
  5. package/dist/cjs/combobox/HTML.combobox.element.d.ts +60 -0
  6. package/dist/cjs/combobox/HTML.combobox.element.js +262 -113
  7. package/dist/cjs/combobox/HTML.combobox.option.element.d.ts +24 -0
  8. package/dist/cjs/combobox/HTML.combobox.option.element.js +70 -5
  9. package/dist/cjs/combobox/HTML.combobox.tag.element.d.ts +12 -0
  10. package/dist/cjs/combobox/HTML.combobox.tag.element.js +26 -7
  11. package/dist/cjs/combobox/index.d.ts +1 -0
  12. package/dist/cjs/index.d.ts +253 -0
  13. package/dist/cjs/index.js +0 -66
  14. package/dist/cjs/package.json +3 -0
  15. package/dist/esm/combobox/Combobox.markup.js +178 -102
  16. package/dist/esm/combobox/HTML.combobox.element.js +262 -113
  17. package/dist/esm/combobox/HTML.combobox.option.element.js +70 -5
  18. package/dist/esm/combobox/HTML.combobox.tag.element.js +26 -7
  19. package/dist/esm/index.js +0 -66
  20. package/dist/types/combobox/Boolean.attribute.value.normalizer.d.ts +0 -1
  21. package/dist/types/combobox/Combobox.markup.d.ts +13 -9
  22. package/dist/types/combobox/HTML.combobox.element.d.ts +10 -5
  23. package/dist/types/combobox/HTML.combobox.option.element.d.ts +11 -1
  24. package/dist/types/combobox/HTML.combobox.tag.element.d.ts +9 -2
  25. package/dist/types/combobox/index.d.ts +0 -1
  26. package/dist/types/index.d.ts +101 -62
  27. package/package.json +17 -3
  28. package/dist/types/combobox/Boolean.attribute.value.normalizer.d.ts.map +0 -1
  29. package/dist/types/combobox/Combobox.markup.d.ts.map +0 -1
  30. package/dist/types/combobox/HTML.combobox.element.d.ts.map +0 -1
  31. package/dist/types/combobox/HTML.combobox.option.element.d.ts.map +0 -1
  32. package/dist/types/combobox/HTML.combobox.tag.element.d.ts.map +0 -1
  33. package/dist/types/combobox/index.d.ts.map +0 -1
  34. package/dist/types/index.d.ts.map +0 -1
  35. package/dist/types/types.d.ts +0 -198
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # html-combobox-element
2
+
3
+ The `<combo-box>` is a web component that provides dropdown/combobox functionality through `<select>` fully compatible API.
4
+
5
+ ## Install
6
+ ```sh
7
+ npm install html-combobox-element
8
+ ```
9
+
10
+ ```ts
11
+ // app entry point
12
+ import 'html-combobox-element';
13
+ ```
14
+ This registers the `<combo-box>`, `<box-option>` and `<box-tag>` custom elements with full TS support for plain DOM/vanilla-JS usage.
15
+
16
+ ### TypeScript + JSX
17
+ Using `<combo-box>` in JSX or a Vue template needs one extra step, due to a TypeScript limitation: JSX/template type-checking only picks up custom element typings from a
18
+ file in your own project - not from a package's shipped `.d.ts`.
19
+
20
+ This isn't specific to this package; it affects any library adding custom elements to `JSX.IntrinsicElements` under the modern automatic JSX runtime.
21
+
22
+ So, in addition to `import 'html-combobox-element'`, create and include one small file tsconfig.json, with the snippet for your framework:
23
+
24
+ <details>
25
+ <summary><strong>React</strong> (<code>jsx: "react-jsx"</code>)</summary>
26
+
27
+ ```ts
28
+ // combo-box.d.ts
29
+ declare module 'react/jsx-runtime' {
30
+ namespace JSX {
31
+ interface IntrinsicElements {
32
+ 'combo-box': ComboboxElementAttributes;
33
+ 'box-option': ComboboxOptionElementAttributes;
34
+ 'box-tag': ComboboxTagElementAttributes;
35
+ }
36
+ }
37
+ }
38
+ declare module 'react/jsx-dev-runtime' {
39
+ namespace JSX {
40
+ interface IntrinsicElements {
41
+ 'combo-box': ComboboxElementAttributes;
42
+ 'box-option': ComboboxOptionElementAttributes;
43
+ 'box-tag': ComboboxTagElementAttributes;
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ </details>
50
+
51
+ <details>
52
+ <summary><strong>Preact</strong> (<code>jsx: "react-jsx"</code>, <code>jsxImportSource: "preact"</code>)</summary>
53
+
54
+ ```ts
55
+ // combo-box.d.ts
56
+ declare module 'preact/jsx-runtime' {
57
+ namespace JSX {
58
+ interface IntrinsicElements {
59
+ 'combo-box': ComboboxElementAttributes;
60
+ 'box-option': ComboboxOptionElementAttributes;
61
+ 'box-tag': ComboboxTagElementAttributes;
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ </details>
68
+
69
+ <details>
70
+ <summary><strong>Solid</strong> (<code>jsx: "preserve"</code>, <code>jsxImportSource: "solid-js"</code>)</summary>
71
+
72
+ ```ts
73
+ // combo-box.d.ts
74
+ declare module 'solid-js/jsx-runtime' {
75
+ namespace JSX {
76
+ interface IntrinsicElements {
77
+ 'combo-box': ComboboxElementAttributes;
78
+ 'box-option': ComboboxOptionElementAttributes;
79
+ 'box-tag': ComboboxTagElementAttributes;
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ </details>
86
+
87
+ <details>
88
+ <summary><strong>Vue</strong> (SFC <code>&lt;template&gt;</code>, checked via <code>vue-tsc</code>)</summary>
89
+
90
+ Vue templates aren't JSX, so they're type-checked through `GlobalComponents` instead:
91
+
92
+ ```ts
93
+ // combo-box.d.ts
94
+ declare module 'vue' {
95
+ export interface GlobalComponents {
96
+ 'combo-box': new () => { $props: ComboboxElementAttributes };
97
+ 'box-option': new () => { $props: ComboboxOptionElementAttributes };
98
+ }
99
+ }
100
+
101
+ ```
102
+
103
+ This checks required props and value types for props you pass; like any native custom
104
+ element, Vue still allows extra/unrecognized attributes through (that's expected Vue
105
+ behavior for custom elements, not a gap in this typing). The trailing `export {};` may be
106
+ required here (without it the file is treated as a script, not a module, and the
107
+ augmentation breaks Vue's own types).
108
+
109
+ </details>
110
+
111
+ ### Exported types
112
+
113
+ - `HTMLComboboxElement`, `HTMLComboboxOptionElement`, `HTMLComboboxTagElement` - element
114
+ interfaces, also usable via `document.querySelector('combo-box')` etc.
115
+ - `ComboboxEvent`, `ComboboxInputEvent`, `ComboboxFocusEvent` - typed event objects
116
+ (`event.target` is a properly typed `HTMLComboboxElement`).
117
+ - `ComboboxJsxAttributes`, `ComboboxOptionJsxAttributes` - the component's own props
118
+ (`value`, `multiple`, `placeholder`, `clearable`, `searchable`, `filterable`,
119
+ `required`, `disabled`, etc.), framework-independent.
120
+ - `ComboboxElementAttributes`, `ComboboxOptionElementAttributes`,
121
+ `ComboboxTagElementAttributes` - the above plus common passthrough attributes
122
+ (`class`/`className`, `style`, `ref`, `children`, `data-*`, `aria-*`, `on*`). These
123
+ are what the snippets above use for `'combo-box'` / `'box-option'` / `'box-tag'`.
@@ -0,0 +1,2 @@
1
+ /** Transform invalid (nullish or stringed) values to correct boolean value */
2
+ export declare function toBoolean(value: any): boolean;
@@ -0,0 +1,35 @@
1
+ import { HTMLComboboxTagElement } from './HTML.combobox.tag.element.js';
2
+ import { HTMLComboboxOptionElement } from './HTML.combobox.option.element.js';
3
+ export declare class ComboboxMarkup {
4
+ #private;
5
+ tagsContainer: HTMLDivElement | null;
6
+ optionsContainer: HTMLDivElement | null;
7
+ clearAllButton: HTMLButtonElement | null;
8
+ dropdown: HTMLDivElement | null;
9
+ placeholder: HTMLDivElement | null;
10
+ searchInput: HTMLInputElement | null;
11
+ tagTemplate: HTMLComboboxTagElement | null;
12
+ constructor(shadowRoot: ShadowRoot, internals: ElementInternals);
13
+ connect(): void;
14
+ onHostConnected(): void;
15
+ disconnect(): void;
16
+ setPlaceholder(value: string): void;
17
+ setInputValue(value: string): void;
18
+ onClearAllFocusOut: (event: FocusEvent) => void;
19
+ onClickOutside: (ev: MouseEvent) => void;
20
+ sort(query: string): void;
21
+ get options(): NodeListOf<HTMLComboboxOptionElement>;
22
+ getTagControl(tag: HTMLComboboxTagElement): HTMLButtonElement;
23
+ setDropdownPosition(): void;
24
+ showDropdown(): void;
25
+ onFocusIn: () => void;
26
+ closeDropdown(): void;
27
+ onFocusOut: () => void;
28
+ createAndAppendTag(option: HTMLComboboxOptionElement): HTMLButtonElement;
29
+ getTagByValue(value: string): HTMLComboboxTagElement;
30
+ getOptionByValue(value: string): HTMLComboboxOptionElement;
31
+ get selectedOptions(): NodeListOf<HTMLComboboxOptionElement>;
32
+ static importCSS(urls: string[]): void;
33
+ static template: string;
34
+ static markup: HTMLTemplateElement;
35
+ }
@@ -12,36 +12,74 @@ class ComboboxMarkup {
12
12
  placeholder = null;
13
13
  searchInput = null;
14
14
  tagTemplate = null;
15
- options;
16
- connected = false;
15
+ #rso;
16
+ #hostWidth = 0;
17
+ #hostHeight = 0;
18
+ #hostTop = 0;
17
19
  constructor(shadowRoot, internals) {
18
20
  this.#shadowRoot = shadowRoot;
19
21
  this.#internals = internals;
20
- this.#shadowRoot.host.addEventListener('focus', this.showDropdown);
21
- this.#shadowRoot.host.addEventListener('blur', this.hideDropdown);
22
- this.#shadowRoot.host.addEventListener('click', this.showDropdown);
23
- document.addEventListener('click', this.hideDropdown);
24
- document.addEventListener('scroll', this.onPageScroll);
22
+ window.addEventListener('click', this.onClickOutside);
23
+ this.#shadowRoot.host.addEventListener('focusin', this.onFocusIn);
24
+ this.#shadowRoot.host.addEventListener('focusout', this.onFocusOut);
25
+ this.#rso = new ResizeObserver(this.#onDimensionsChanges);
25
26
  }
26
27
  connect() {
27
- const placeholder = this.#shadowRoot.host.getAttribute('placeholder') || '';
28
+ const placeholder = this.#shadowRoot.host.getAttribute('placeholder') || `Выбрать`;
28
29
  this.tagsContainer = this.#shadowRoot.querySelector('#tags');
30
+ this.#rso.observe(this.#shadowRoot.host, { box: 'border-box' });
29
31
  this.optionsContainer = this.#shadowRoot.querySelector('[part*="options"]');
30
32
  this.clearAllButton = this.#shadowRoot.querySelector('[part*="clear-all"]');
33
+ this.clearAllButton?.addEventListener('focusout', this.onClearAllFocusOut);
31
34
  this.dropdown = this.#shadowRoot.querySelector('#dropdown');
32
35
  this.placeholder = this.#shadowRoot.querySelector('#placeholder');
33
36
  this.placeholder.innerText = placeholder;
34
37
  this.searchInput = this.#shadowRoot.querySelector('[part*="search-input"]');
35
38
  this.searchInput.value = this.#shadowRoot.host.getAttribute('query');
36
- this.searchInput.placeholder = placeholder;
39
+ this.searchInput.style.setProperty('--search-input-placeholder-color', this.placeholder.style.color);
37
40
  const innerTemplate = this.#shadowRoot.querySelector('#tag-template');
38
41
  const doc = document.importNode(innerTemplate.content, true);
39
42
  this.tagTemplate = doc.querySelector('box-tag');
40
- this.connected = true;
41
43
  }
42
- invalidateOptionsCache() {
43
- this.options = this.optionsContainer.querySelectorAll('box-option');
44
+ onHostConnected() {
45
+ const host = this.#shadowRoot.host;
46
+ const clearable = host.hasAttribute('clearable') || host.hasAttribute('multiple');
47
+ this.tagTemplate.toggleAttribute('visible', clearable);
44
48
  }
49
+ disconnect() {
50
+ this.#shadowRoot.host.removeEventListener('focusin', this.onFocusIn);
51
+ this.#shadowRoot.host.removeEventListener('focusout', this.onFocusOut);
52
+ this.clearAllButton?.removeEventListener('focusout', this.onClearAllFocusOut);
53
+ window.removeEventListener('click', this.onClickOutside);
54
+ this.#rso.unobserve(this.#shadowRoot.host);
55
+ }
56
+ setPlaceholder(value) {
57
+ if (this.placeholder) {
58
+ this.placeholder.innerText = value.trim() || `Выбрать`;
59
+ }
60
+ }
61
+ setInputValue(value) {
62
+ if (this.searchInput) {
63
+ this.searchInput.value = value;
64
+ }
65
+ }
66
+ onClearAllFocusOut = (event) => {
67
+ event.preventDefault();
68
+ event.stopPropagation();
69
+ if (event.relatedTarget == null) {
70
+ const host = this.#shadowRoot.host;
71
+ if (!host.hasAttribute('searchable') && !host.hasAttribute('filterable')) {
72
+ this.closeDropdown();
73
+ }
74
+ }
75
+ };
76
+ onClickOutside = (ev) => {
77
+ if (this.#internals.ariaExpanded !== 'true')
78
+ return;
79
+ if (ev.target === this.#shadowRoot.host)
80
+ return;
81
+ this.closeDropdown();
82
+ };
45
83
  #timer = undefined;
46
84
  sort(query) {
47
85
  clearTimeout(this.#timer);
@@ -56,68 +94,63 @@ class ComboboxMarkup {
56
94
  }
57
95
  });
58
96
  this.dropdown.scrollTo({ top: 0, behavior: 'smooth' });
59
- }, 200);
97
+ }, 400);
60
98
  }
61
- disconnect() {
62
- this.#shadowRoot.host.removeEventListener('focus', this.showDropdown);
63
- this.#shadowRoot.host.removeEventListener('blur', this.hideDropdown);
64
- this.#shadowRoot.host.removeEventListener('click', this.showDropdown);
65
- document.removeEventListener('click', this.hideDropdown);
66
- document.removeEventListener('scroll', this.onPageScroll);
67
- this.connected = false;
99
+ get options() {
100
+ return this.optionsContainer.querySelectorAll('box-option');
101
+ }
102
+ getTagControl(tag) {
103
+ return tag.querySelector('[part*="clear-tag"]');
68
104
  }
69
- onPageScroll = () => {
70
- if (this.dropdown.matches(':popover-open')) {
71
- this.setDropdownPosition(this.#shadowRoot.host.getBoundingClientRect());
105
+ #onDimensionsChanges = (entries) => {
106
+ const entry = entries[0];
107
+ if (entry) {
108
+ this.#hostWidth = entry.borderBoxSize[0].inlineSize;
109
+ this.#hostHeight = entry.borderBoxSize[0].blockSize;
72
110
  }
111
+ this.setDropdownPosition();
73
112
  };
74
- setDropdownPosition(rect) {
113
+ setDropdownPosition() {
75
114
  const dropdown = this.dropdown;
76
115
  const vh = window.innerHeight;
77
- const sh = rect.height;
78
- const sy = rect.top;
79
- if (sy < (vh - vh / 3)) {
80
- dropdown.style.top = sh + sy + 'px';
81
- dropdown.style.bottom = 'unset';
82
- dropdown.style.transform = `unset`;
83
- }
84
- else {
85
- dropdown.style.top = sh + sy + 'px';
86
- dropdown.style.bottom = 'unset';
87
- dropdown.style.transform = `translateY(calc(-1 * calc(100% + ${sh}px)))`;
88
- }
89
- dropdown.style.left = rect.left + 'px';
90
- dropdown.style.width = rect.width + 'px';
91
- dropdown.style.maxHeight = '50vh';
116
+ const sh = this.#hostHeight;
117
+ const sy = this.#hostTop;
118
+ // Calculate available space below and above
119
+ const spaceBelow = vh - (sy + sh);
120
+ const spaceAbove = sy;
121
+ // Determine if dropdown should go below or above
122
+ const shouldGoBelow = spaceBelow >= spaceAbove;
123
+ // Set dropdown to max 50vh or available space, whichever is smaller
124
+ const maxHeight = Math.min(50 * vh / 100, shouldGoBelow ? spaceBelow - 10 : spaceAbove - 10);
125
+ dropdown.style.width = this.#hostWidth + 'px';
126
+ dropdown.style.maxHeight = `${Math.max(100, maxHeight)}px`;
127
+ }
128
+ showDropdown() {
129
+ this.onFocusIn();
92
130
  }
93
- showDropdown = () => {
131
+ onFocusIn = () => {
132
+ if (this.#internals.ariaExpanded === "true")
133
+ return;
134
+ this.#hostTop = this.#shadowRoot.host.getBoundingClientRect().top;
94
135
  try {
95
- this.setDropdownPosition(this.#shadowRoot.host.getBoundingClientRect());
96
- this.dropdown.style.display = 'flex';
97
136
  this.dropdown.showPopover();
98
137
  this.#internals.ariaExpanded = "true";
99
- if (this.tagsContainer?.children.length === 0) {
100
- this.searchInput?.focus();
101
- }
102
- this.placeholder.innerText = '';
103
138
  }
104
- catch {
139
+ catch (e) {
105
140
  this.#internals.ariaExpanded = "false";
106
141
  }
107
142
  };
108
143
  closeDropdown() {
109
144
  try {
110
145
  this.dropdown.hidePopover();
111
- this.dropdown.style.display = 'none';
112
146
  this.#internals.ariaExpanded = "false";
113
- this.placeholder.innerText = this.#shadowRoot.host.getAttribute('placeholder');
114
147
  }
115
148
  catch (e) {
116
149
  this.#internals.ariaExpanded = "true";
117
150
  }
118
151
  }
119
- hideDropdown = (event) => {
120
- if (event.composedPath().includes(this.#shadowRoot.host))
152
+ onFocusOut = () => {
153
+ if (this.#internals.ariaExpanded !== "true")
121
154
  return;
122
155
  this.closeDropdown();
123
156
  };
@@ -147,12 +180,13 @@ class ComboboxMarkup {
147
180
  const template = this.tagTemplate;
148
181
  tag = template.cloneNode(true);
149
182
  const label = tag.querySelector('[part="tag-label"]');
150
- label.textContent = option.textContent;
183
+ label.textContent = option.label || option.textContent;
151
184
  button = tag.querySelector('[part="clear-tag"]');
152
185
  }
153
186
  button?.setAttribute('value', value);
154
187
  tag.setAttribute('value', value);
155
188
  this.tagsContainer.appendChild(tag);
189
+ this.clearAllButton?.removeAttribute('disabled');
156
190
  return button;
157
191
  }
158
192
  getTagByValue(value) {
@@ -162,8 +196,7 @@ class ComboboxMarkup {
162
196
  return this.optionsContainer.querySelector(`box-option[value="${value}"]`);
163
197
  }
164
198
  get selectedOptions() {
165
- return this.optionsContainer
166
- .querySelectorAll('box-option[selected]');
199
+ return this.optionsContainer.querySelectorAll('box-option[selected]');
167
200
  }
168
201
  static importCSS(urls) {
169
202
  ComboboxMarkup.template = ComboboxMarkup.template.replace(':host {', `
@@ -177,35 +210,64 @@ ${urls.map(url => `@import "${url}";`)}
177
210
  :host {
178
211
  font-size: inherit;
179
212
  font-family: inherit;
213
+ display: flow-root;
214
+ position: relative;
215
+ anchor-name: --position;
216
+ }
217
+
218
+
219
+
220
+ #internal {
221
+ min-block-size: 1lh;
180
222
  display: grid;
181
223
  grid-template-columns: minmax(0, max-content) 1fr;
182
224
  align-items: center;
183
225
  gap: 1px;
184
- position: relative;
226
+ border-radius: inherit;
185
227
  }
186
228
 
187
- :host([multiple]) {
229
+ :host([multiple]) #internal {
188
230
  grid-template-columns: minmax(0, max-content) 1fr auto;
189
231
  }
190
232
 
191
233
  #dropdown {
234
+ contain: layout style paint;
192
235
  inset: unset;
193
236
  margin: 0;
237
+ /*position: absolute;*/
194
238
  box-sizing: border-box;
195
239
  overflow-y: scroll;
196
240
  flex-direction: column;
197
241
  border-radius: inherit;
198
242
  border-color: ButtonFace;
199
243
  border-width: inherit;
244
+ background-color: Canvas;
245
+ outline: none;
246
+ display: none;
247
+ top: calc(anchor(bottom) + 2px);
248
+ left: anchor(left);
249
+ position-anchor: --position;
250
+ position-try: normal flip-block;
200
251
  }
201
252
 
202
- [part="options"] {
253
+ #dropdown:popover-open {
254
+ display: flex;
255
+ }
256
+
257
+ #dropdown::-webkit-scrollbar {
258
+ display: none;
259
+ }
260
+
261
+ #options {
203
262
  display: flex;
204
263
  flex-direction: column;
205
264
  justify-content: start;
206
265
  gap: 2px;
207
- padding-block: .5rem;
208
- border-radius: inherit;
266
+ border-radius: inherit;
267
+ }
268
+
269
+ :host([filterable]) #options, :host([searchable]) #options {
270
+ padding-top: .5rem;
209
271
  }
210
272
 
211
273
  box-option {
@@ -213,8 +275,8 @@ ${urls.map(url => `@import "${url}";`)}
213
275
  border-radius: inherit;
214
276
  content-visibility: auto;
215
277
  cursor: pointer;
216
- padding-inline: 2px;
217
- padding-block: 1px;
278
+ padding: inherit;
279
+ padding-inline: .5lh;
218
280
  }
219
281
 
220
282
  box-option:hover {
@@ -227,29 +289,27 @@ ${urls.map(url => `@import "${url}";`)}
227
289
  pointer-events: none;
228
290
  }
229
291
 
230
- [part="search-input"] {
292
+ #dropdown > [part="search-input"] {
231
293
  display: none;
232
294
  position: sticky;
233
295
  top: 0;
234
296
  z-index: 2;
235
- border-radius: inherit;
236
- border-style: inherit;
237
- border-width: inherit;
238
- border-color: inherit;
239
- padding: inherit;
297
+ font-size: inherit;
298
+ outline: inherit;
240
299
  }
241
300
 
242
- :host([searchable]) [part="search-input"],
243
- :host([filterable]) [part="search-input"] {
244
- display: flex;
301
+ :host([searchable]) #dropdown > [part="search-input"],
302
+ :host([filterable]) #dropdown > [part="search-input"] {
303
+ display: flow-root;
245
304
  }
246
305
 
247
306
  #placeholder {
248
307
  text-align: left;
249
308
  overflow: hidden;
250
309
  padding-inline-start: 2px;
251
- font-size: smaller;
252
310
  color: dimgrey;
311
+ outline: none;
312
+ user-select: none;
253
313
  }
254
314
 
255
315
  #tags:not(:empty) + #placeholder {
@@ -266,7 +326,6 @@ ${urls.map(url => `@import "${url}";`)}
266
326
  flex-wrap: wrap;
267
327
  overflow: hidden;
268
328
  gap: 2px;
269
- border-radius: inherit;
270
329
  }
271
330
 
272
331
  box-tag {
@@ -275,13 +334,11 @@ ${urls.map(url => `@import "${url}";`)}
275
334
  box-sizing: border-box;
276
335
  display: flex;
277
336
  align-items: center;
278
- border-radius: inherit;
279
- padding-inline-start: 0.2lh;
280
- padding-inline-end: .2rem;
337
+ padding-inline-start: 0.4lh;
338
+ padding-inline-end: 0.2lh;
281
339
  background-color: transparent;
282
340
  gap: 5px;
283
- font-size: medium;
284
- text-transform: uppercase;
341
+ border-radius: 1lh;
285
342
  }
286
343
 
287
344
  :host([multiple]) box-tag {
@@ -290,80 +347,99 @@ ${urls.map(url => `@import "${url}";`)}
290
347
  max-width: 100%;
291
348
  }
292
349
 
293
- box-tag [part*="tag-label"] {
350
+ [part*="tag-label"] {
294
351
  white-space: nowrap;
295
352
  text-overflow: ellipsis;
296
353
  overflow: hidden;
297
354
  user-select: none;
298
- font-size: 95%;
299
355
  flex-grow: 1;
300
356
  }
301
357
 
302
- :host([multiple]) box-tag[part*="tag-label"] {
358
+ :host([multiple]) [part*="tag-label"] {
303
359
  flex-grow: unset;
304
360
  }
305
361
 
306
- [part*="clear-tag"], [part*="clear-all"] {
362
+ [part*="clear-tag"], #internal > [part*="clear-all"] {
307
363
  border-radius: 100%;
308
364
  border: none;
309
365
  aspect-ratio: 1;
310
- line-height: 0;
311
366
  padding: 0!important;
312
367
  user-select: none;
313
368
  background-color: transparent;
314
369
  }
315
370
 
316
371
  [part*="clear-tag"] {
317
- inline-size: 1em;
318
- block-size: 1em;
319
- font-size: 80%;
372
+ block-size: 1lh;
373
+ font-size: .7lh;
374
+ text-transform: uppercase;
320
375
  display: none;
321
376
  }
377
+
378
+ [part*="clear-tag"]:focus-visible, [part*="clear-tag"]:focus {
379
+ outline-style: solid;
380
+ outline-width: 1px;
381
+ }
322
382
 
323
- :host([multiple]) [part*="clear-all"] {
383
+ :host([multiple]) #internal > [part*="clear-all"] {
324
384
  display: block;
325
385
  }
326
386
 
327
- :host([clearable]) [part*="clear-tag"] {
328
- display: block;
387
+ :host([clearable]) [part*="clear-tag"],
388
+ :host([multiple]) [part*="clear-tag"] {
389
+ display: grid;
390
+ place-content: center;
329
391
  }
330
392
 
331
- [part*="clear-all"] {
393
+ #internal > [part*="clear-all"] {
332
394
  font-size: inherit;
333
- inline-size: 1.2em;
334
- block-size: 1.2em;
395
+ text-transform: uppercase;
396
+ block-size: 1lh;
335
397
  display: none;
336
398
  }
399
+
400
+ #tags:empty + [part*="clear-all"] {
401
+ outline: none;
402
+ }
337
403
 
338
- [part*="clear-all"]:hover,
404
+ #internal > [part*="clear-all"]:hover,
339
405
  [part*="clear-tag"]:hover {
340
406
  color: ActiveText;
341
407
  cursor: pointer;
342
408
  }
343
409
 
344
- [part*="clear-all"]:hover {
410
+ #internal >[part*="clear-all"]:hover {
345
411
  background-color: ButtonFace;
346
412
  }
347
413
 
348
- :host:has(#tags:empty) [part*="clear-all"] {
414
+ #tags:empty + [part*="clear-all"] {
349
415
  pointer-events: none;
350
- color: darkgrey;
416
+ outline: none;
351
417
  }
418
+
352
419
  </style>
420
+ <div id='internal' part='internal'>
421
+ <div id="tags" part='tags'></div>
422
+ <div id="placeholder" part='placeholder' tabindex='0'>Выбрать</div>
423
+ <button part="clear-all">✕</button>
424
+ </div>
353
425
 
354
- <div id="tags">&nbsp;</div>
355
- <div id="placeholder">&nbsp;</div>
356
- <button part="clear-all">✕</button>
357
- <div id="dropdown" popover="manual">
358
- <input name="search-input" part="search-input" />
359
- <div part="options"></div>
426
+ <div id="dropdown" popover="manual" part='dropdown' >
427
+ <input placeholder='Поиск' name="search-input" part="search-input" inputmode='search' autofocus />
428
+ <div part="options" id='options'></div>
360
429
  </div>
430
+
361
431
  <template id='tag-template'>
362
432
  <box-tag>
363
433
  <span part="tag-label"></span>
364
- <button part="clear-tag">✕</button>
434
+ <div part="clear-tag" role='button' tabindex='0'>✕</div>
365
435
  </box-tag>
366
436
  </template>
367
437
  `;
438
+ static markup;
439
+ static {
440
+ const template = document.createElement('template');
441
+ template.innerHTML = ComboboxMarkup.template;
442
+ ComboboxMarkup.markup = template;
443
+ }
368
444
  }
369
445
  exports.ComboboxMarkup = ComboboxMarkup;