@duskmoon-dev/el-autocomplete 0.5.0 → 0.7.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/README.md +123 -0
- package/dist/cjs/index.js +100 -28
- package/dist/cjs/register.js +100 -28
- package/dist/esm/index.js +98 -26
- package/dist/esm/register.js +98 -26
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/el-dm-autocomplete.d.ts +4 -10
- package/dist/types/el-dm-autocomplete.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/el-dm-autocomplete.test.ts +216 -0
- package/src/el-dm-autocomplete.ts +129 -27
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseElement, css as cssTag } from '@duskmoon-dev/el-
|
|
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
|
|
38
|
-
options: { type: String, reflect: true
|
|
39
|
-
multiple: { type: Boolean, reflect: true
|
|
40
|
-
disabled: { type: Boolean, reflect: true
|
|
41
|
-
clearable: { type: Boolean, reflect: true
|
|
42
|
-
placeholder: { type: String, reflect: true
|
|
43
|
-
size: { type: String, reflect: true
|
|
44
|
-
loading: { type: Boolean, reflect: true
|
|
45
|
-
noResultsText: { type: String, reflect: true,
|
|
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
|
|
49
|
-
options
|
|
50
|
-
multiple
|
|
51
|
-
disabled
|
|
52
|
-
clearable
|
|
53
|
-
placeholder
|
|
54
|
-
size
|
|
55
|
-
loading
|
|
56
|
-
noResultsText
|
|
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
|
|
406
|
-
|
|
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 ?
|
|
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="${
|
|
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() {
|