html-combobox-element 0.0.2 → 0.0.3

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/dist/cjs/index.js CHANGED
@@ -1,8 +1,4 @@
1
1
  "use strict";
2
- /// <reference types="solid-js" />
3
- /// <reference types="preact" />
4
- /// <reference types="react" />
5
- /// <reference types="vue" />
6
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
3
  if (k2 === undefined) k2 = k;
8
4
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -19,65 +15,3 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
19
15
  };
20
16
  Object.defineProperty(exports, "__esModule", { value: true });
21
17
  __exportStar(require("./combobox/index.js"), exports);
22
- // declare global {
23
- // namespace React {
24
- // namespace JSX {
25
- // interface IntrinsicElements {
26
- // 'combo-box': React.DetailedHTMLProps<Omit<React.HTMLAttributes<HTMLComboboxElement>, 'defaultValue'>,
27
- // HTMLComboboxElement
28
- // > & ComboboxJsxAttributes;
29
- //
30
- // 'box-option': React.DetailedHTMLProps<
31
- // React.HTMLAttributes<HTMLComboboxOptionElement>,
32
- // HTMLComboboxOptionElement>
33
- // & ComboboxOptionJsxAttributes;
34
- //
35
- // 'box-tag': React.DetailedHTMLProps<React.HTMLAttributes<HTMLComboboxTagElement>, HTMLComboboxTagElement>;
36
- // }
37
- // }
38
- // }
39
- //
40
- //
41
- // namespace preact {
42
- // namespace JSX {
43
- // interface IntrinsicElements {
44
- // 'combo-box': preact.HTMLAttributes<HTMLComboboxElement> & ComboboxJsxAttributes;
45
- // 'box-option': preact.HTMLAttributes<HTMLComboboxOptionElement> & ComboboxOptionJsxAttributes;
46
- // 'box-tag': preact.HTMLAttributes<HTMLComboboxTagElement>;
47
- // }
48
- // }
49
- // }
50
- //
51
- // }
52
- //
53
- // declare module 'preact' {
54
- // namespace JSX {
55
- // interface IntrinsicElements {
56
- // 'combo-box': preact.HTMLAttributes<HTMLComboboxElement> & ComboboxJsxAttributes;
57
- // 'box-option': preact.HTMLAttributes<HTMLComboboxOptionElement> & ComboboxOptionJsxAttributes;
58
- // 'box-tag': preact.HTMLAttributes<HTMLComboboxTagElement>;
59
- // }
60
- // }
61
- // }
62
- //
63
- // // Solid
64
- // declare module 'solid-js' {
65
- // namespace JSX {
66
- // interface IntrinsicElements {
67
- // 'combo-box': HTMLAttributes<HTMLComboboxElement> & ComboboxJsxAttributes
68
- // 'box-option': HTMLAttributes<HTMLComboboxOptionElement> & ComboboxOptionJsxAttributes
69
- // 'box-tag': HTMLAttributes<HTMLElement>
70
- // }
71
- // }
72
- // }
73
- //
74
- //
75
- // declare global {
76
- // namespace JSX {
77
- // interface IntrinsicElements {
78
- // 'combo-box': ComboboxJsxAttributes;
79
- // 'box-option': ComboboxOptionJsxAttributes;
80
- // 'box-tag': HTMLElement;
81
- // }
82
- // }
83
- // }
@@ -9,36 +9,74 @@ export class ComboboxMarkup {
9
9
  placeholder = null;
10
10
  searchInput = null;
11
11
  tagTemplate = null;
12
- options;
13
- connected = false;
12
+ #rso;
13
+ #hostWidth = 0;
14
+ #hostHeight = 0;
15
+ #hostTop = 0;
14
16
  constructor(shadowRoot, internals) {
15
17
  this.#shadowRoot = shadowRoot;
16
18
  this.#internals = internals;
17
- this.#shadowRoot.host.addEventListener('focus', this.showDropdown);
18
- this.#shadowRoot.host.addEventListener('blur', this.hideDropdown);
19
- this.#shadowRoot.host.addEventListener('click', this.showDropdown);
20
- document.addEventListener('click', this.hideDropdown);
21
- document.addEventListener('scroll', this.onPageScroll);
19
+ window.addEventListener('click', this.onClickOutside);
20
+ this.#shadowRoot.host.addEventListener('focusin', this.onFocusIn);
21
+ this.#shadowRoot.host.addEventListener('focusout', this.onFocusOut);
22
+ this.#rso = new ResizeObserver(this.#onDimensionsChanges);
22
23
  }
23
24
  connect() {
24
- const placeholder = this.#shadowRoot.host.getAttribute('placeholder') || '';
25
+ const placeholder = this.#shadowRoot.host.getAttribute('placeholder') || `Выбрать`;
25
26
  this.tagsContainer = this.#shadowRoot.querySelector('#tags');
27
+ this.#rso.observe(this.#shadowRoot.host, { box: 'border-box' });
26
28
  this.optionsContainer = this.#shadowRoot.querySelector('[part*="options"]');
27
29
  this.clearAllButton = this.#shadowRoot.querySelector('[part*="clear-all"]');
30
+ this.clearAllButton?.addEventListener('focusout', this.onClearAllFocusOut);
28
31
  this.dropdown = this.#shadowRoot.querySelector('#dropdown');
29
32
  this.placeholder = this.#shadowRoot.querySelector('#placeholder');
30
33
  this.placeholder.innerText = placeholder;
31
34
  this.searchInput = this.#shadowRoot.querySelector('[part*="search-input"]');
32
35
  this.searchInput.value = this.#shadowRoot.host.getAttribute('query');
33
- this.searchInput.placeholder = placeholder;
36
+ this.searchInput.style.setProperty('--search-input-placeholder-color', this.placeholder.style.color);
34
37
  const innerTemplate = this.#shadowRoot.querySelector('#tag-template');
35
38
  const doc = document.importNode(innerTemplate.content, true);
36
39
  this.tagTemplate = doc.querySelector('box-tag');
37
- this.connected = true;
38
40
  }
39
- invalidateOptionsCache() {
40
- this.options = this.optionsContainer.querySelectorAll('box-option');
41
+ onHostConnected() {
42
+ const host = this.#shadowRoot.host;
43
+ const clearable = host.hasAttribute('clearable') || host.hasAttribute('multiple');
44
+ this.tagTemplate.toggleAttribute('visible', clearable);
41
45
  }
46
+ disconnect() {
47
+ this.#shadowRoot.host.removeEventListener('focusin', this.onFocusIn);
48
+ this.#shadowRoot.host.removeEventListener('focusout', this.onFocusOut);
49
+ this.clearAllButton?.removeEventListener('focusout', this.onClearAllFocusOut);
50
+ window.removeEventListener('click', this.onClickOutside);
51
+ this.#rso.unobserve(this.#shadowRoot.host);
52
+ }
53
+ setPlaceholder(value) {
54
+ if (this.placeholder) {
55
+ this.placeholder.innerText = value.trim() || `Выбрать`;
56
+ }
57
+ }
58
+ setInputValue(value) {
59
+ if (this.searchInput) {
60
+ this.searchInput.value = value;
61
+ }
62
+ }
63
+ onClearAllFocusOut = (event) => {
64
+ event.preventDefault();
65
+ event.stopPropagation();
66
+ if (event.relatedTarget == null) {
67
+ const host = this.#shadowRoot.host;
68
+ if (!host.hasAttribute('searchable') && !host.hasAttribute('filterable')) {
69
+ this.closeDropdown();
70
+ }
71
+ }
72
+ };
73
+ onClickOutside = (ev) => {
74
+ if (this.#internals.ariaExpanded !== 'true')
75
+ return;
76
+ if (ev.target === this.#shadowRoot.host)
77
+ return;
78
+ this.closeDropdown();
79
+ };
42
80
  #timer = undefined;
43
81
  sort(query) {
44
82
  clearTimeout(this.#timer);
@@ -53,68 +91,63 @@ export class ComboboxMarkup {
53
91
  }
54
92
  });
55
93
  this.dropdown.scrollTo({ top: 0, behavior: 'smooth' });
56
- }, 200);
94
+ }, 400);
57
95
  }
58
- disconnect() {
59
- this.#shadowRoot.host.removeEventListener('focus', this.showDropdown);
60
- this.#shadowRoot.host.removeEventListener('blur', this.hideDropdown);
61
- this.#shadowRoot.host.removeEventListener('click', this.showDropdown);
62
- document.removeEventListener('click', this.hideDropdown);
63
- document.removeEventListener('scroll', this.onPageScroll);
64
- this.connected = false;
96
+ get options() {
97
+ return this.optionsContainer.querySelectorAll('box-option');
98
+ }
99
+ getTagControl(tag) {
100
+ return tag.querySelector('[part*="clear-tag"]');
65
101
  }
66
- onPageScroll = () => {
67
- if (this.dropdown.matches(':popover-open')) {
68
- this.setDropdownPosition(this.#shadowRoot.host.getBoundingClientRect());
102
+ #onDimensionsChanges = (entries) => {
103
+ const entry = entries[0];
104
+ if (entry) {
105
+ this.#hostWidth = entry.borderBoxSize[0].inlineSize;
106
+ this.#hostHeight = entry.borderBoxSize[0].blockSize;
69
107
  }
108
+ this.setDropdownPosition();
70
109
  };
71
- setDropdownPosition(rect) {
110
+ setDropdownPosition() {
72
111
  const dropdown = this.dropdown;
73
112
  const vh = window.innerHeight;
74
- const sh = rect.height;
75
- const sy = rect.top;
76
- if (sy < (vh - vh / 3)) {
77
- dropdown.style.top = sh + sy + 'px';
78
- dropdown.style.bottom = 'unset';
79
- dropdown.style.transform = `unset`;
80
- }
81
- else {
82
- dropdown.style.top = sh + sy + 'px';
83
- dropdown.style.bottom = 'unset';
84
- dropdown.style.transform = `translateY(calc(-1 * calc(100% + ${sh}px)))`;
85
- }
86
- dropdown.style.left = rect.left + 'px';
87
- dropdown.style.width = rect.width + 'px';
88
- dropdown.style.maxHeight = '50vh';
113
+ const sh = this.#hostHeight;
114
+ const sy = this.#hostTop;
115
+ // Calculate available space below and above
116
+ const spaceBelow = vh - (sy + sh);
117
+ const spaceAbove = sy;
118
+ // Determine if dropdown should go below or above
119
+ const shouldGoBelow = spaceBelow >= spaceAbove;
120
+ // Set dropdown to max 50vh or available space, whichever is smaller
121
+ const maxHeight = Math.min(50 * vh / 100, shouldGoBelow ? spaceBelow - 10 : spaceAbove - 10);
122
+ dropdown.style.width = this.#hostWidth + 'px';
123
+ dropdown.style.maxHeight = `${Math.max(100, maxHeight)}px`;
124
+ }
125
+ showDropdown() {
126
+ this.onFocusIn();
89
127
  }
90
- showDropdown = () => {
128
+ onFocusIn = () => {
129
+ if (this.#internals.ariaExpanded === "true")
130
+ return;
131
+ this.#hostTop = this.#shadowRoot.host.getBoundingClientRect().top;
91
132
  try {
92
- this.setDropdownPosition(this.#shadowRoot.host.getBoundingClientRect());
93
- this.dropdown.style.display = 'flex';
94
133
  this.dropdown.showPopover();
95
134
  this.#internals.ariaExpanded = "true";
96
- if (this.tagsContainer?.children.length === 0) {
97
- this.searchInput?.focus();
98
- }
99
- this.placeholder.innerText = '';
100
135
  }
101
- catch {
136
+ catch (e) {
102
137
  this.#internals.ariaExpanded = "false";
103
138
  }
104
139
  };
105
140
  closeDropdown() {
106
141
  try {
107
142
  this.dropdown.hidePopover();
108
- this.dropdown.style.display = 'none';
109
143
  this.#internals.ariaExpanded = "false";
110
- this.placeholder.innerText = this.#shadowRoot.host.getAttribute('placeholder');
111
144
  }
112
145
  catch (e) {
113
146
  this.#internals.ariaExpanded = "true";
114
147
  }
115
148
  }
116
- hideDropdown = (event) => {
117
- if (event.composedPath().includes(this.#shadowRoot.host))
149
+ onFocusOut = () => {
150
+ if (this.#internals.ariaExpanded !== "true")
118
151
  return;
119
152
  this.closeDropdown();
120
153
  };
@@ -144,12 +177,13 @@ export class ComboboxMarkup {
144
177
  const template = this.tagTemplate;
145
178
  tag = template.cloneNode(true);
146
179
  const label = tag.querySelector('[part="tag-label"]');
147
- label.textContent = option.textContent;
180
+ label.textContent = option.label || option.textContent;
148
181
  button = tag.querySelector('[part="clear-tag"]');
149
182
  }
150
183
  button?.setAttribute('value', value);
151
184
  tag.setAttribute('value', value);
152
185
  this.tagsContainer.appendChild(tag);
186
+ this.clearAllButton?.removeAttribute('disabled');
153
187
  return button;
154
188
  }
155
189
  getTagByValue(value) {
@@ -159,8 +193,7 @@ export class ComboboxMarkup {
159
193
  return this.optionsContainer.querySelector(`box-option[value="${value}"]`);
160
194
  }
161
195
  get selectedOptions() {
162
- return this.optionsContainer
163
- .querySelectorAll('box-option[selected]');
196
+ return this.optionsContainer.querySelectorAll('box-option[selected]');
164
197
  }
165
198
  static importCSS(urls) {
166
199
  ComboboxMarkup.template = ComboboxMarkup.template.replace(':host {', `
@@ -174,35 +207,64 @@ ${urls.map(url => `@import "${url}";`)}
174
207
  :host {
175
208
  font-size: inherit;
176
209
  font-family: inherit;
210
+ display: flow-root;
211
+ position: relative;
212
+ anchor-name: --position;
213
+ }
214
+
215
+
216
+
217
+ #internal {
218
+ min-block-size: 1lh;
177
219
  display: grid;
178
220
  grid-template-columns: minmax(0, max-content) 1fr;
179
221
  align-items: center;
180
222
  gap: 1px;
181
- position: relative;
223
+ border-radius: inherit;
182
224
  }
183
225
 
184
- :host([multiple]) {
226
+ :host([multiple]) #internal {
185
227
  grid-template-columns: minmax(0, max-content) 1fr auto;
186
228
  }
187
229
 
188
230
  #dropdown {
231
+ contain: layout style paint;
189
232
  inset: unset;
190
233
  margin: 0;
234
+ /*position: absolute;*/
191
235
  box-sizing: border-box;
192
236
  overflow-y: scroll;
193
237
  flex-direction: column;
194
238
  border-radius: inherit;
195
239
  border-color: ButtonFace;
196
240
  border-width: inherit;
241
+ background-color: Canvas;
242
+ outline: none;
243
+ display: none;
244
+ top: calc(anchor(bottom) + 2px);
245
+ left: anchor(left);
246
+ position-anchor: --position;
247
+ position-try: normal flip-block;
197
248
  }
198
249
 
199
- [part="options"] {
250
+ #dropdown:popover-open {
251
+ display: flex;
252
+ }
253
+
254
+ #dropdown::-webkit-scrollbar {
255
+ display: none;
256
+ }
257
+
258
+ #options {
200
259
  display: flex;
201
260
  flex-direction: column;
202
261
  justify-content: start;
203
262
  gap: 2px;
204
- padding-block: .5rem;
205
- border-radius: inherit;
263
+ border-radius: inherit;
264
+ }
265
+
266
+ :host([filterable]) #options, :host([searchable]) #options {
267
+ padding-top: .5rem;
206
268
  }
207
269
 
208
270
  box-option {
@@ -210,8 +272,8 @@ ${urls.map(url => `@import "${url}";`)}
210
272
  border-radius: inherit;
211
273
  content-visibility: auto;
212
274
  cursor: pointer;
213
- padding-inline: 2px;
214
- padding-block: 1px;
275
+ padding: inherit;
276
+ padding-inline: .5lh;
215
277
  }
216
278
 
217
279
  box-option:hover {
@@ -224,29 +286,27 @@ ${urls.map(url => `@import "${url}";`)}
224
286
  pointer-events: none;
225
287
  }
226
288
 
227
- [part="search-input"] {
289
+ #dropdown > [part="search-input"] {
228
290
  display: none;
229
291
  position: sticky;
230
292
  top: 0;
231
293
  z-index: 2;
232
- border-radius: inherit;
233
- border-style: inherit;
234
- border-width: inherit;
235
- border-color: inherit;
236
- padding: inherit;
294
+ font-size: inherit;
295
+ outline: inherit;
237
296
  }
238
297
 
239
- :host([searchable]) [part="search-input"],
240
- :host([filterable]) [part="search-input"] {
241
- display: flex;
298
+ :host([searchable]) #dropdown > [part="search-input"],
299
+ :host([filterable]) #dropdown > [part="search-input"] {
300
+ display: flow-root;
242
301
  }
243
302
 
244
303
  #placeholder {
245
304
  text-align: left;
246
305
  overflow: hidden;
247
306
  padding-inline-start: 2px;
248
- font-size: smaller;
249
307
  color: dimgrey;
308
+ outline: none;
309
+ user-select: none;
250
310
  }
251
311
 
252
312
  #tags:not(:empty) + #placeholder {
@@ -263,7 +323,6 @@ ${urls.map(url => `@import "${url}";`)}
263
323
  flex-wrap: wrap;
264
324
  overflow: hidden;
265
325
  gap: 2px;
266
- border-radius: inherit;
267
326
  }
268
327
 
269
328
  box-tag {
@@ -272,13 +331,11 @@ ${urls.map(url => `@import "${url}";`)}
272
331
  box-sizing: border-box;
273
332
  display: flex;
274
333
  align-items: center;
275
- border-radius: inherit;
276
- padding-inline-start: 0.2lh;
277
- padding-inline-end: .2rem;
334
+ padding-inline-start: 0.4lh;
335
+ padding-inline-end: 0.2lh;
278
336
  background-color: transparent;
279
337
  gap: 5px;
280
- font-size: medium;
281
- text-transform: uppercase;
338
+ border-radius: 1lh;
282
339
  }
283
340
 
284
341
  :host([multiple]) box-tag {
@@ -287,79 +344,98 @@ ${urls.map(url => `@import "${url}";`)}
287
344
  max-width: 100%;
288
345
  }
289
346
 
290
- box-tag [part*="tag-label"] {
347
+ [part*="tag-label"] {
291
348
  white-space: nowrap;
292
349
  text-overflow: ellipsis;
293
350
  overflow: hidden;
294
351
  user-select: none;
295
- font-size: 95%;
296
352
  flex-grow: 1;
297
353
  }
298
354
 
299
- :host([multiple]) box-tag[part*="tag-label"] {
355
+ :host([multiple]) [part*="tag-label"] {
300
356
  flex-grow: unset;
301
357
  }
302
358
 
303
- [part*="clear-tag"], [part*="clear-all"] {
359
+ [part*="clear-tag"], #internal > [part*="clear-all"] {
304
360
  border-radius: 100%;
305
361
  border: none;
306
362
  aspect-ratio: 1;
307
- line-height: 0;
308
363
  padding: 0!important;
309
364
  user-select: none;
310
365
  background-color: transparent;
311
366
  }
312
367
 
313
368
  [part*="clear-tag"] {
314
- inline-size: 1em;
315
- block-size: 1em;
316
- font-size: 80%;
369
+ block-size: 1lh;
370
+ font-size: .7lh;
371
+ text-transform: uppercase;
317
372
  display: none;
318
373
  }
374
+
375
+ [part*="clear-tag"]:focus-visible, [part*="clear-tag"]:focus {
376
+ outline-style: solid;
377
+ outline-width: 1px;
378
+ }
319
379
 
320
- :host([multiple]) [part*="clear-all"] {
380
+ :host([multiple]) #internal > [part*="clear-all"] {
321
381
  display: block;
322
382
  }
323
383
 
324
- :host([clearable]) [part*="clear-tag"] {
325
- display: block;
384
+ :host([clearable]) [part*="clear-tag"],
385
+ :host([multiple]) [part*="clear-tag"] {
386
+ display: grid;
387
+ place-content: center;
326
388
  }
327
389
 
328
- [part*="clear-all"] {
390
+ #internal > [part*="clear-all"] {
329
391
  font-size: inherit;
330
- inline-size: 1.2em;
331
- block-size: 1.2em;
392
+ text-transform: uppercase;
393
+ block-size: 1lh;
332
394
  display: none;
333
395
  }
396
+
397
+ #tags:empty + [part*="clear-all"] {
398
+ outline: none;
399
+ }
334
400
 
335
- [part*="clear-all"]:hover,
401
+ #internal > [part*="clear-all"]:hover,
336
402
  [part*="clear-tag"]:hover {
337
403
  color: ActiveText;
338
404
  cursor: pointer;
339
405
  }
340
406
 
341
- [part*="clear-all"]:hover {
407
+ #internal >[part*="clear-all"]:hover {
342
408
  background-color: ButtonFace;
343
409
  }
344
410
 
345
- :host:has(#tags:empty) [part*="clear-all"] {
411
+ #tags:empty + [part*="clear-all"] {
346
412
  pointer-events: none;
347
- color: darkgrey;
413
+ outline: none;
348
414
  }
415
+
349
416
  </style>
417
+ <div id='internal' part='internal'>
418
+ <div id="tags" part='tags'></div>
419
+ <div id="placeholder" part='placeholder' tabindex='0'>Выбрать</div>
420
+ <button part="clear-all">✕</button>
421
+ </div>
350
422
 
351
- <div id="tags">&nbsp;</div>
352
- <div id="placeholder">&nbsp;</div>
353
- <button part="clear-all">✕</button>
354
- <div id="dropdown" popover="manual">
355
- <input name="search-input" part="search-input" />
356
- <div part="options"></div>
423
+ <div id="dropdown" popover="manual" part='dropdown' >
424
+ <input placeholder='Поиск' name="search-input" part="search-input" inputmode='search' autofocus />
425
+ <div part="options" id='options'></div>
357
426
  </div>
427
+
358
428
  <template id='tag-template'>
359
429
  <box-tag>
360
430
  <span part="tag-label"></span>
361
- <button part="clear-tag">✕</button>
431
+ <div part="clear-tag" role='button' tabindex='0'>✕</div>
362
432
  </box-tag>
363
433
  </template>
364
434
  `;
435
+ static markup;
436
+ static {
437
+ const template = document.createElement('template');
438
+ template.innerHTML = ComboboxMarkup.template;
439
+ ComboboxMarkup.markup = template;
440
+ }
365
441
  }