@duskmoon-dev/el-autocomplete 0.4.0 → 0.6.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,4 +1,4 @@
1
- import { BaseElement, css as cssTag } from '@duskmoon-dev/el-core';
1
+ import { BaseElement, css as cssTag } from '@duskmoon-dev/el-base';
2
2
  import { css } from '@duskmoon-dev/core/components/autocomplete';
3
3
 
4
4
  // Strip @layer components wrapper for Shadow DOM
@@ -20,6 +20,25 @@ const styles = cssTag`
20
20
  .autocomplete {
21
21
  width: 100%;
22
22
  }
23
+
24
+ /* Popover API styles for dropdown — override core's absolute positioning
25
+ and visibility rules since the top layer breaks parent-child CSS selectors */
26
+ .autocomplete-dropdown {
27
+ position: fixed;
28
+ margin: 0;
29
+ border: none;
30
+ padding: 0;
31
+ inset: unset;
32
+ /* Override core's hidden defaults — popover API controls visibility */
33
+ opacity: 1;
34
+ visibility: visible;
35
+ transform: none;
36
+ transition: none;
37
+ }
38
+
39
+ .autocomplete-dropdown:popover-open {
40
+ display: block;
41
+ }
23
42
  `;
24
43
 
25
44
  export type AutocompleteSize = 'sm' | 'md' | 'lg';
@@ -34,31 +53,32 @@ export interface AutocompleteOption {
34
53
 
35
54
  export class ElDmAutocomplete extends BaseElement {
36
55
  static properties = {
37
- value: { type: String, reflect: true, default: '' },
38
- options: { type: String, reflect: true, default: '[]' },
39
- multiple: { type: Boolean, reflect: true, default: false },
40
- disabled: { type: Boolean, reflect: true, default: false },
41
- clearable: { type: Boolean, reflect: true, default: false },
42
- placeholder: { type: String, reflect: true, default: '' },
43
- size: { type: String, reflect: true, default: 'md' },
44
- loading: { type: Boolean, reflect: true, default: false },
45
- noResultsText: { type: String, reflect: true, default: 'No results found' },
56
+ value: { type: String, reflect: true },
57
+ options: { type: String, reflect: true },
58
+ multiple: { type: Boolean, reflect: true },
59
+ disabled: { type: Boolean, reflect: true },
60
+ clearable: { type: Boolean, reflect: true },
61
+ placeholder: { type: String, reflect: true },
62
+ size: { type: String, reflect: true },
63
+ loading: { type: Boolean, reflect: true },
64
+ noResultsText: { type: String, reflect: true, attribute: 'no-results-text' },
46
65
  };
47
66
 
48
- value!: string;
49
- options!: string;
50
- multiple!: boolean;
51
- disabled!: boolean;
52
- clearable!: boolean;
53
- placeholder!: string;
54
- size!: AutocompleteSize;
55
- loading!: boolean;
56
- noResultsText!: string;
67
+ declare value: string;
68
+ declare options: string;
69
+ declare multiple: boolean;
70
+ declare disabled: boolean;
71
+ declare clearable: boolean;
72
+ declare placeholder: string;
73
+ declare size: AutocompleteSize;
74
+ declare loading: boolean;
75
+ declare noResultsText: string;
57
76
 
58
77
  private _isOpen = false;
59
78
  private _searchValue = '';
60
79
  private _highlightedIndex = -1;
61
80
  private _selectedValues: string[] = [];
81
+ private _scrollHandler: (() => void) | null = null;
62
82
 
63
83
  constructor() {
64
84
  super();
@@ -74,6 +94,11 @@ export class ElDmAutocomplete extends BaseElement {
74
94
  disconnectedCallback() {
75
95
  super.disconnectedCallback();
76
96
  document.removeEventListener('click', this._handleOutsideClick);
97
+ if (this._scrollHandler) {
98
+ window.removeEventListener('scroll', this._scrollHandler, true);
99
+ window.removeEventListener('resize', this._scrollHandler);
100
+ this._scrollHandler = null;
101
+ }
77
102
  }
78
103
 
79
104
  private _handleOutsideClick = (e: MouseEvent) => {
@@ -96,7 +121,7 @@ export class ElDmAutocomplete extends BaseElement {
96
121
 
97
122
  private _getOptions(): AutocompleteOption[] {
98
123
  try {
99
- return JSON.parse(this.options);
124
+ return JSON.parse(this.options || '[]');
100
125
  } catch {
101
126
  return [];
102
127
  }
@@ -120,15 +145,77 @@ export class ElDmAutocomplete extends BaseElement {
120
145
  this._isOpen = true;
121
146
  this._highlightedIndex = -1;
122
147
  this.update();
148
+
149
+ const dropdown = this.shadowRoot?.querySelector('.autocomplete-dropdown') as HTMLElement;
150
+ const trigger = this.shadowRoot?.querySelector('.autocomplete-input, .autocomplete-tags') as HTMLElement;
151
+ if (dropdown && trigger) {
152
+ try {
153
+ dropdown.showPopover();
154
+ this._positionDropdown(dropdown, trigger);
155
+ } catch {
156
+ // Ignore if already shown
157
+ }
158
+
159
+ this._scrollHandler = () => {
160
+ this._positionDropdown(dropdown, trigger);
161
+ };
162
+ window.addEventListener('scroll', this._scrollHandler, true);
163
+ window.addEventListener('resize', this._scrollHandler);
164
+ }
123
165
  }
124
166
 
125
167
  private _close() {
168
+ if (!this._isOpen) return;
126
169
  this._isOpen = false;
127
170
  this._searchValue = '';
128
171
  this._highlightedIndex = -1;
172
+
173
+ if (this._scrollHandler) {
174
+ window.removeEventListener('scroll', this._scrollHandler, true);
175
+ window.removeEventListener('resize', this._scrollHandler);
176
+ this._scrollHandler = null;
177
+ }
178
+
179
+ const dropdown = this.shadowRoot?.querySelector('.autocomplete-dropdown') as HTMLElement;
180
+ if (dropdown) {
181
+ try {
182
+ dropdown.hidePopover();
183
+ } catch {
184
+ // Ignore if already hidden
185
+ }
186
+ }
187
+
129
188
  this.update();
130
189
  }
131
190
 
191
+ private _positionDropdown(dropdown: HTMLElement, trigger: HTMLElement) {
192
+ const triggerRect = trigger.getBoundingClientRect();
193
+ const dropdownRect = dropdown.getBoundingClientRect();
194
+
195
+ // Position below the trigger
196
+ let top = triggerRect.bottom + 4;
197
+ let left = triggerRect.left;
198
+
199
+ // If dropdown would overflow the bottom, position above
200
+ if (top + dropdownRect.height > window.innerHeight) {
201
+ top = triggerRect.top - dropdownRect.height - 4;
202
+ }
203
+
204
+ // Clamp to viewport right edge
205
+ if (left + triggerRect.width > window.innerWidth) {
206
+ left = window.innerWidth - triggerRect.width - 8;
207
+ }
208
+
209
+ // Clamp to viewport left edge
210
+ if (left < 8) {
211
+ left = 8;
212
+ }
213
+
214
+ dropdown.style.top = `${top}px`;
215
+ dropdown.style.left = `${left}px`;
216
+ dropdown.style.width = `${triggerRect.width}px`;
217
+ }
218
+
132
219
  private _toggle() {
133
220
  if (this._isOpen) {
134
221
  this._close();
@@ -325,7 +412,7 @@ export class ElDmAutocomplete extends BaseElement {
325
412
 
326
413
  if (filteredOptions.length === 0) {
327
414
  return `
328
- <div class="autocomplete-no-results">${this.noResultsText}</div>
415
+ <div class="autocomplete-no-results">${this.noResultsText || 'No results found'}</div>
329
416
  `;
330
417
  }
331
418
 
@@ -402,12 +489,13 @@ export class ElDmAutocomplete extends BaseElement {
402
489
  }
403
490
 
404
491
  render() {
405
- const sizeClass =
406
- this.size !== 'md' ? `autocomplete-${this.size}` : '';
492
+ const size = this.size || 'md';
493
+ const sizeClass = size !== 'md' ? `autocomplete-${size}` : '';
407
494
  const openClass = this._isOpen ? 'autocomplete-open' : '';
408
495
  const clearableClass = this.clearable ? 'autocomplete-clearable' : '';
409
496
  const showClear =
410
497
  this.clearable && this._selectedValues.length > 0 && !this.disabled;
498
+ const placeholder = this.placeholder || '';
411
499
 
412
500
  if (this.multiple) {
413
501
  return `
@@ -417,7 +505,7 @@ export class ElDmAutocomplete extends BaseElement {
417
505
  <input
418
506
  type="text"
419
507
  class="autocomplete-tags-input"
420
- placeholder="${this._selectedValues.length === 0 ? this.placeholder : ''}"
508
+ placeholder="${this._selectedValues.length === 0 ? placeholder : ''}"
421
509
  value="${this._searchValue}"
422
510
  ${this.disabled ? 'disabled' : ''}
423
511
  role="combobox"
@@ -433,7 +521,7 @@ export class ElDmAutocomplete extends BaseElement {
433
521
  `
434
522
  : ''
435
523
  }
436
- <div class="autocomplete-dropdown" role="listbox">
524
+ <div class="autocomplete-dropdown" role="listbox" popover="manual">
437
525
  ${this._renderOptions()}
438
526
  </div>
439
527
  </div>
@@ -445,7 +533,7 @@ export class ElDmAutocomplete extends BaseElement {
445
533
  <input
446
534
  type="text"
447
535
  class="autocomplete-input"
448
- placeholder="${this.placeholder}"
536
+ placeholder="${placeholder}"
449
537
  value="${this._isOpen ? this._searchValue : this._getDisplayValue()}"
450
538
  ${this.disabled ? 'disabled' : ''}
451
539
  role="combobox"
@@ -460,7 +548,7 @@ export class ElDmAutocomplete extends BaseElement {
460
548
  `
461
549
  : ''
462
550
  }
463
- <div class="autocomplete-dropdown" role="listbox">
551
+ <div class="autocomplete-dropdown" role="listbox" popover="manual">
464
552
  ${this._renderOptions()}
465
553
  </div>
466
554
  </div>
@@ -470,6 +558,20 @@ export class ElDmAutocomplete extends BaseElement {
470
558
  update() {
471
559
  super.update();
472
560
  this._attachEventListeners();
561
+
562
+ // Re-show popover after DOM re-render (innerHTML replacement loses popover state)
563
+ if (this._isOpen) {
564
+ const dropdown = this.shadowRoot?.querySelector('.autocomplete-dropdown') as HTMLElement;
565
+ const trigger = this.shadowRoot?.querySelector('.autocomplete-input, .autocomplete-tags') as HTMLElement;
566
+ if (dropdown && trigger) {
567
+ try {
568
+ dropdown.showPopover();
569
+ this._positionDropdown(dropdown, trigger);
570
+ } catch {
571
+ // Ignore if already shown
572
+ }
573
+ }
574
+ }
473
575
  }
474
576
 
475
577
  private _attachEventListeners() {