@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.
package/dist/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/el-dm-autocomplete.ts
2
- import { BaseElement, css as cssTag } from "@duskmoon-dev/el-core";
2
+ import { BaseElement, css as cssTag } from "@duskmoon-dev/el-base";
3
3
  import { css } from "@duskmoon-dev/core/components/autocomplete";
4
4
  var strippedCss = css.replace(/@layer\s+components\s*\{/, "").replace(/\}[\s]*$/, "");
5
5
  var styles = cssTag`
@@ -16,33 +16,44 @@ var styles = cssTag`
16
16
  .autocomplete {
17
17
  width: 100%;
18
18
  }
19
+
20
+ /* Popover API styles for dropdown — override core's absolute positioning
21
+ and visibility rules since the top layer breaks parent-child CSS selectors */
22
+ .autocomplete-dropdown {
23
+ position: fixed;
24
+ margin: 0;
25
+ border: none;
26
+ padding: 0;
27
+ inset: unset;
28
+ /* Override core's hidden defaults — popover API controls visibility */
29
+ opacity: 1;
30
+ visibility: visible;
31
+ transform: none;
32
+ transition: none;
33
+ }
34
+
35
+ .autocomplete-dropdown:popover-open {
36
+ display: block;
37
+ }
19
38
  `;
20
39
 
21
40
  class ElDmAutocomplete extends BaseElement {
22
41
  static properties = {
23
- value: { type: String, reflect: true, default: "" },
24
- options: { type: String, reflect: true, default: "[]" },
25
- multiple: { type: Boolean, reflect: true, default: false },
26
- disabled: { type: Boolean, reflect: true, default: false },
27
- clearable: { type: Boolean, reflect: true, default: false },
28
- placeholder: { type: String, reflect: true, default: "" },
29
- size: { type: String, reflect: true, default: "md" },
30
- loading: { type: Boolean, reflect: true, default: false },
31
- noResultsText: { type: String, reflect: true, default: "No results found" }
42
+ value: { type: String, reflect: true },
43
+ options: { type: String, reflect: true },
44
+ multiple: { type: Boolean, reflect: true },
45
+ disabled: { type: Boolean, reflect: true },
46
+ clearable: { type: Boolean, reflect: true },
47
+ placeholder: { type: String, reflect: true },
48
+ size: { type: String, reflect: true },
49
+ loading: { type: Boolean, reflect: true },
50
+ noResultsText: { type: String, reflect: true, attribute: "no-results-text" }
32
51
  };
33
- value;
34
- options;
35
- multiple;
36
- disabled;
37
- clearable;
38
- placeholder;
39
- size;
40
- loading;
41
- noResultsText;
42
52
  _isOpen = false;
43
53
  _searchValue = "";
44
54
  _highlightedIndex = -1;
45
55
  _selectedValues = [];
56
+ _scrollHandler = null;
46
57
  constructor() {
47
58
  super();
48
59
  this.attachStyles(styles);
@@ -55,6 +66,11 @@ class ElDmAutocomplete extends BaseElement {
55
66
  disconnectedCallback() {
56
67
  super.disconnectedCallback();
57
68
  document.removeEventListener("click", this._handleOutsideClick);
69
+ if (this._scrollHandler) {
70
+ window.removeEventListener("scroll", this._scrollHandler, true);
71
+ window.removeEventListener("resize", this._scrollHandler);
72
+ this._scrollHandler = null;
73
+ }
58
74
  }
59
75
  _handleOutsideClick = (e) => {
60
76
  if (!this.contains(e.target)) {
@@ -74,7 +90,7 @@ class ElDmAutocomplete extends BaseElement {
74
90
  }
75
91
  _getOptions() {
76
92
  try {
77
- return JSON.parse(this.options);
93
+ return JSON.parse(this.options || "[]");
78
94
  } catch {
79
95
  return [];
80
96
  }
@@ -92,13 +108,57 @@ class ElDmAutocomplete extends BaseElement {
92
108
  this._isOpen = true;
93
109
  this._highlightedIndex = -1;
94
110
  this.update();
111
+ const dropdown = this.shadowRoot?.querySelector(".autocomplete-dropdown");
112
+ const trigger = this.shadowRoot?.querySelector(".autocomplete-input, .autocomplete-tags");
113
+ if (dropdown && trigger) {
114
+ try {
115
+ dropdown.showPopover();
116
+ this._positionDropdown(dropdown, trigger);
117
+ } catch {}
118
+ this._scrollHandler = () => {
119
+ this._positionDropdown(dropdown, trigger);
120
+ };
121
+ window.addEventListener("scroll", this._scrollHandler, true);
122
+ window.addEventListener("resize", this._scrollHandler);
123
+ }
95
124
  }
96
125
  _close() {
126
+ if (!this._isOpen)
127
+ return;
97
128
  this._isOpen = false;
98
129
  this._searchValue = "";
99
130
  this._highlightedIndex = -1;
131
+ if (this._scrollHandler) {
132
+ window.removeEventListener("scroll", this._scrollHandler, true);
133
+ window.removeEventListener("resize", this._scrollHandler);
134
+ this._scrollHandler = null;
135
+ }
136
+ const dropdown = this.shadowRoot?.querySelector(".autocomplete-dropdown");
137
+ if (dropdown) {
138
+ try {
139
+ dropdown.hidePopover();
140
+ } catch {}
141
+ }
100
142
  this.update();
101
143
  }
144
+ _positionDropdown(dropdown, trigger) {
145
+ const triggerRect = trigger.getBoundingClientRect();
146
+ const dropdownRect = dropdown.getBoundingClientRect();
147
+ let top = triggerRect.bottom + 4;
148
+ let left = triggerRect.left;
149
+ if (top + dropdownRect.height > window.innerHeight) {
150
+ top = triggerRect.top - dropdownRect.height - 4;
151
+ }
152
+ if (left + triggerRect.width > window.innerWidth) {
153
+ left = window.innerWidth - triggerRect.width - 8;
154
+ }
155
+ if (left < 8) {
156
+ left = 8;
157
+ }
158
+ dropdown.style.top = `${top}px`;
159
+ dropdown.style.left = `${left}px`;
160
+ dropdown.style.width = `${triggerRect.width}px`;
161
+ }
102
162
  _toggle() {
103
163
  if (this._isOpen) {
104
164
  this._close();
@@ -246,7 +306,7 @@ class ElDmAutocomplete extends BaseElement {
246
306
  }
247
307
  if (filteredOptions.length === 0) {
248
308
  return `
249
- <div class="autocomplete-no-results">${this.noResultsText}</div>
309
+ <div class="autocomplete-no-results">${this.noResultsText || "No results found"}</div>
250
310
  `;
251
311
  }
252
312
  const groups = new Map;
@@ -304,10 +364,12 @@ class ElDmAutocomplete extends BaseElement {
304
364
  `;
305
365
  }
306
366
  render() {
307
- const sizeClass = this.size !== "md" ? `autocomplete-${this.size}` : "";
367
+ const size = this.size || "md";
368
+ const sizeClass = size !== "md" ? `autocomplete-${size}` : "";
308
369
  const openClass = this._isOpen ? "autocomplete-open" : "";
309
370
  const clearableClass = this.clearable ? "autocomplete-clearable" : "";
310
371
  const showClear = this.clearable && this._selectedValues.length > 0 && !this.disabled;
372
+ const placeholder = this.placeholder || "";
311
373
  if (this.multiple) {
312
374
  return `
313
375
  <div class="autocomplete ${sizeClass} ${openClass} ${clearableClass}">
@@ -316,7 +378,7 @@ class ElDmAutocomplete extends BaseElement {
316
378
  <input
317
379
  type="text"
318
380
  class="autocomplete-tags-input"
319
- placeholder="${this._selectedValues.length === 0 ? this.placeholder : ""}"
381
+ placeholder="${this._selectedValues.length === 0 ? placeholder : ""}"
320
382
  value="${this._searchValue}"
321
383
  ${this.disabled ? "disabled" : ""}
322
384
  role="combobox"
@@ -328,7 +390,7 @@ class ElDmAutocomplete extends BaseElement {
328
390
  ${showClear ? `
329
391
  <button type="button" class="autocomplete-clear" aria-label="Clear selection">&times;</button>
330
392
  ` : ""}
331
- <div class="autocomplete-dropdown" role="listbox">
393
+ <div class="autocomplete-dropdown" role="listbox" popover="manual">
332
394
  ${this._renderOptions()}
333
395
  </div>
334
396
  </div>
@@ -339,7 +401,7 @@ class ElDmAutocomplete extends BaseElement {
339
401
  <input
340
402
  type="text"
341
403
  class="autocomplete-input"
342
- placeholder="${this.placeholder}"
404
+ placeholder="${placeholder}"
343
405
  value="${this._isOpen ? this._searchValue : this._getDisplayValue()}"
344
406
  ${this.disabled ? "disabled" : ""}
345
407
  role="combobox"
@@ -350,7 +412,7 @@ class ElDmAutocomplete extends BaseElement {
350
412
  ${showClear ? `
351
413
  <button type="button" class="autocomplete-clear" aria-label="Clear selection">&times;</button>
352
414
  ` : ""}
353
- <div class="autocomplete-dropdown" role="listbox">
415
+ <div class="autocomplete-dropdown" role="listbox" popover="manual">
354
416
  ${this._renderOptions()}
355
417
  </div>
356
418
  </div>
@@ -359,6 +421,16 @@ class ElDmAutocomplete extends BaseElement {
359
421
  update() {
360
422
  super.update();
361
423
  this._attachEventListeners();
424
+ if (this._isOpen) {
425
+ const dropdown = this.shadowRoot?.querySelector(".autocomplete-dropdown");
426
+ const trigger = this.shadowRoot?.querySelector(".autocomplete-input, .autocomplete-tags");
427
+ if (dropdown && trigger) {
428
+ try {
429
+ dropdown.showPopover();
430
+ this._positionDropdown(dropdown, trigger);
431
+ } catch {}
432
+ }
433
+ }
362
434
  }
363
435
  _attachEventListeners() {
364
436
  const input = this.shadowRoot?.querySelector(".autocomplete-input, .autocomplete-tags-input");
@@ -1,5 +1,5 @@
1
1
  // src/el-dm-autocomplete.ts
2
- import { BaseElement, css as cssTag } from "@duskmoon-dev/el-core";
2
+ import { BaseElement, css as cssTag } from "@duskmoon-dev/el-base";
3
3
  import { css } from "@duskmoon-dev/core/components/autocomplete";
4
4
  var strippedCss = css.replace(/@layer\s+components\s*\{/, "").replace(/\}[\s]*$/, "");
5
5
  var styles = cssTag`
@@ -16,33 +16,44 @@ var styles = cssTag`
16
16
  .autocomplete {
17
17
  width: 100%;
18
18
  }
19
+
20
+ /* Popover API styles for dropdown — override core's absolute positioning
21
+ and visibility rules since the top layer breaks parent-child CSS selectors */
22
+ .autocomplete-dropdown {
23
+ position: fixed;
24
+ margin: 0;
25
+ border: none;
26
+ padding: 0;
27
+ inset: unset;
28
+ /* Override core's hidden defaults — popover API controls visibility */
29
+ opacity: 1;
30
+ visibility: visible;
31
+ transform: none;
32
+ transition: none;
33
+ }
34
+
35
+ .autocomplete-dropdown:popover-open {
36
+ display: block;
37
+ }
19
38
  `;
20
39
 
21
40
  class ElDmAutocomplete extends BaseElement {
22
41
  static properties = {
23
- value: { type: String, reflect: true, default: "" },
24
- options: { type: String, reflect: true, default: "[]" },
25
- multiple: { type: Boolean, reflect: true, default: false },
26
- disabled: { type: Boolean, reflect: true, default: false },
27
- clearable: { type: Boolean, reflect: true, default: false },
28
- placeholder: { type: String, reflect: true, default: "" },
29
- size: { type: String, reflect: true, default: "md" },
30
- loading: { type: Boolean, reflect: true, default: false },
31
- noResultsText: { type: String, reflect: true, default: "No results found" }
42
+ value: { type: String, reflect: true },
43
+ options: { type: String, reflect: true },
44
+ multiple: { type: Boolean, reflect: true },
45
+ disabled: { type: Boolean, reflect: true },
46
+ clearable: { type: Boolean, reflect: true },
47
+ placeholder: { type: String, reflect: true },
48
+ size: { type: String, reflect: true },
49
+ loading: { type: Boolean, reflect: true },
50
+ noResultsText: { type: String, reflect: true, attribute: "no-results-text" }
32
51
  };
33
- value;
34
- options;
35
- multiple;
36
- disabled;
37
- clearable;
38
- placeholder;
39
- size;
40
- loading;
41
- noResultsText;
42
52
  _isOpen = false;
43
53
  _searchValue = "";
44
54
  _highlightedIndex = -1;
45
55
  _selectedValues = [];
56
+ _scrollHandler = null;
46
57
  constructor() {
47
58
  super();
48
59
  this.attachStyles(styles);
@@ -55,6 +66,11 @@ class ElDmAutocomplete extends BaseElement {
55
66
  disconnectedCallback() {
56
67
  super.disconnectedCallback();
57
68
  document.removeEventListener("click", this._handleOutsideClick);
69
+ if (this._scrollHandler) {
70
+ window.removeEventListener("scroll", this._scrollHandler, true);
71
+ window.removeEventListener("resize", this._scrollHandler);
72
+ this._scrollHandler = null;
73
+ }
58
74
  }
59
75
  _handleOutsideClick = (e) => {
60
76
  if (!this.contains(e.target)) {
@@ -74,7 +90,7 @@ class ElDmAutocomplete extends BaseElement {
74
90
  }
75
91
  _getOptions() {
76
92
  try {
77
- return JSON.parse(this.options);
93
+ return JSON.parse(this.options || "[]");
78
94
  } catch {
79
95
  return [];
80
96
  }
@@ -92,13 +108,57 @@ class ElDmAutocomplete extends BaseElement {
92
108
  this._isOpen = true;
93
109
  this._highlightedIndex = -1;
94
110
  this.update();
111
+ const dropdown = this.shadowRoot?.querySelector(".autocomplete-dropdown");
112
+ const trigger = this.shadowRoot?.querySelector(".autocomplete-input, .autocomplete-tags");
113
+ if (dropdown && trigger) {
114
+ try {
115
+ dropdown.showPopover();
116
+ this._positionDropdown(dropdown, trigger);
117
+ } catch {}
118
+ this._scrollHandler = () => {
119
+ this._positionDropdown(dropdown, trigger);
120
+ };
121
+ window.addEventListener("scroll", this._scrollHandler, true);
122
+ window.addEventListener("resize", this._scrollHandler);
123
+ }
95
124
  }
96
125
  _close() {
126
+ if (!this._isOpen)
127
+ return;
97
128
  this._isOpen = false;
98
129
  this._searchValue = "";
99
130
  this._highlightedIndex = -1;
131
+ if (this._scrollHandler) {
132
+ window.removeEventListener("scroll", this._scrollHandler, true);
133
+ window.removeEventListener("resize", this._scrollHandler);
134
+ this._scrollHandler = null;
135
+ }
136
+ const dropdown = this.shadowRoot?.querySelector(".autocomplete-dropdown");
137
+ if (dropdown) {
138
+ try {
139
+ dropdown.hidePopover();
140
+ } catch {}
141
+ }
100
142
  this.update();
101
143
  }
144
+ _positionDropdown(dropdown, trigger) {
145
+ const triggerRect = trigger.getBoundingClientRect();
146
+ const dropdownRect = dropdown.getBoundingClientRect();
147
+ let top = triggerRect.bottom + 4;
148
+ let left = triggerRect.left;
149
+ if (top + dropdownRect.height > window.innerHeight) {
150
+ top = triggerRect.top - dropdownRect.height - 4;
151
+ }
152
+ if (left + triggerRect.width > window.innerWidth) {
153
+ left = window.innerWidth - triggerRect.width - 8;
154
+ }
155
+ if (left < 8) {
156
+ left = 8;
157
+ }
158
+ dropdown.style.top = `${top}px`;
159
+ dropdown.style.left = `${left}px`;
160
+ dropdown.style.width = `${triggerRect.width}px`;
161
+ }
102
162
  _toggle() {
103
163
  if (this._isOpen) {
104
164
  this._close();
@@ -246,7 +306,7 @@ class ElDmAutocomplete extends BaseElement {
246
306
  }
247
307
  if (filteredOptions.length === 0) {
248
308
  return `
249
- <div class="autocomplete-no-results">${this.noResultsText}</div>
309
+ <div class="autocomplete-no-results">${this.noResultsText || "No results found"}</div>
250
310
  `;
251
311
  }
252
312
  const groups = new Map;
@@ -304,10 +364,12 @@ class ElDmAutocomplete extends BaseElement {
304
364
  `;
305
365
  }
306
366
  render() {
307
- const sizeClass = this.size !== "md" ? `autocomplete-${this.size}` : "";
367
+ const size = this.size || "md";
368
+ const sizeClass = size !== "md" ? `autocomplete-${size}` : "";
308
369
  const openClass = this._isOpen ? "autocomplete-open" : "";
309
370
  const clearableClass = this.clearable ? "autocomplete-clearable" : "";
310
371
  const showClear = this.clearable && this._selectedValues.length > 0 && !this.disabled;
372
+ const placeholder = this.placeholder || "";
311
373
  if (this.multiple) {
312
374
  return `
313
375
  <div class="autocomplete ${sizeClass} ${openClass} ${clearableClass}">
@@ -316,7 +378,7 @@ class ElDmAutocomplete extends BaseElement {
316
378
  <input
317
379
  type="text"
318
380
  class="autocomplete-tags-input"
319
- placeholder="${this._selectedValues.length === 0 ? this.placeholder : ""}"
381
+ placeholder="${this._selectedValues.length === 0 ? placeholder : ""}"
320
382
  value="${this._searchValue}"
321
383
  ${this.disabled ? "disabled" : ""}
322
384
  role="combobox"
@@ -328,7 +390,7 @@ class ElDmAutocomplete extends BaseElement {
328
390
  ${showClear ? `
329
391
  <button type="button" class="autocomplete-clear" aria-label="Clear selection">&times;</button>
330
392
  ` : ""}
331
- <div class="autocomplete-dropdown" role="listbox">
393
+ <div class="autocomplete-dropdown" role="listbox" popover="manual">
332
394
  ${this._renderOptions()}
333
395
  </div>
334
396
  </div>
@@ -339,7 +401,7 @@ class ElDmAutocomplete extends BaseElement {
339
401
  <input
340
402
  type="text"
341
403
  class="autocomplete-input"
342
- placeholder="${this.placeholder}"
404
+ placeholder="${placeholder}"
343
405
  value="${this._isOpen ? this._searchValue : this._getDisplayValue()}"
344
406
  ${this.disabled ? "disabled" : ""}
345
407
  role="combobox"
@@ -350,7 +412,7 @@ class ElDmAutocomplete extends BaseElement {
350
412
  ${showClear ? `
351
413
  <button type="button" class="autocomplete-clear" aria-label="Clear selection">&times;</button>
352
414
  ` : ""}
353
- <div class="autocomplete-dropdown" role="listbox">
415
+ <div class="autocomplete-dropdown" role="listbox" popover="manual">
354
416
  ${this._renderOptions()}
355
417
  </div>
356
418
  </div>
@@ -359,6 +421,16 @@ class ElDmAutocomplete extends BaseElement {
359
421
  update() {
360
422
  super.update();
361
423
  this._attachEventListeners();
424
+ if (this._isOpen) {
425
+ const dropdown = this.shadowRoot?.querySelector(".autocomplete-dropdown");
426
+ const trigger = this.shadowRoot?.querySelector(".autocomplete-input, .autocomplete-tags");
427
+ if (dropdown && trigger) {
428
+ try {
429
+ dropdown.showPopover();
430
+ this._positionDropdown(dropdown, trigger);
431
+ } catch {}
432
+ }
433
+ }
362
434
  }
363
435
  _attachEventListeners() {
364
436
  const input = this.shadowRoot?.querySelector(".autocomplete-input, .autocomplete-tags-input");