html-combobox-element 0.0.2-beta.6 → 0.0.2-beta.7

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