@reliability-design/web 1.4.6 → 1.4.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.
- package/CHANGELOG.md +211 -49
- package/dist/components/rel-accordion/README.md +132 -132
- package/dist/components/rel-alert/README.md +145 -141
- package/dist/components/rel-autocomplete/README.md +156 -61
- package/dist/components/rel-avatar/README.md +116 -130
- package/dist/components/rel-badge/README.md +98 -93
- package/dist/components/rel-breadcrumb/README.md +96 -102
- package/dist/components/rel-button/README.md +160 -130
- package/dist/components/rel-calendar/README.md +109 -109
- package/dist/components/rel-card/README.md +75 -94
- package/dist/components/rel-cardmedia/README.md +100 -127
- package/dist/components/rel-carousel/README.md +184 -166
- package/dist/components/rel-checkbox/README.md +93 -75
- package/dist/components/rel-chip/README.md +132 -121
- package/dist/components/rel-container/README.md +107 -99
- package/dist/components/rel-date-picker/README.md +200 -289
- package/dist/components/rel-divider/README.md +78 -100
- package/dist/components/rel-drawer/README.md +187 -200
- package/dist/components/rel-kbd/README.md +141 -151
- package/dist/components/rel-menu/README.md +178 -151
- package/dist/components/rel-menubar/README.md +218 -329
- package/dist/components/rel-otp/README.md +170 -440
- package/dist/components/rel-pagination/README.md +100 -424
- package/dist/components/rel-popover/README.md +203 -568
- package/dist/components/rel-progress/README.md +88 -110
- package/dist/components/rel-radio/README.md +131 -138
- package/dist/components/rel-range-picker/README.md +162 -0
- package/dist/components/rel-resizable/README.md +199 -140
- package/dist/components/rel-segmented-button/README.md +155 -141
- package/dist/components/rel-select/README.md +154 -93
- package/dist/components/rel-sheet/README.md +168 -224
- package/dist/components/rel-skeleton/README.md +103 -0
- package/dist/components/rel-slider/README.md +130 -135
- package/dist/components/rel-snackbar/README.md +162 -134
- package/dist/components/rel-stepper/README.md +158 -160
- package/dist/components/rel-switch/README.md +108 -102
- package/dist/components/rel-tab/README.md +171 -163
- package/dist/components/rel-textfield/README.md +159 -69
- package/dist/components/rel-time-picker/README.md +122 -157
- package/dist/components/rel-toast/README.md +157 -148
- package/dist/components/rel-tooltip/README.md +108 -111
- package/dist/components/rel-upload/README.md +220 -0
- package/dist/index.d.mts +1147 -340
- package/dist/index.d.ts +1147 -340
- package/dist/index.js +3451 -3163
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3416 -3118
- package/dist/index.mjs.map +1 -1
- package/dist/styles/vars.css +877 -866
- package/dist/{vars-XNMTSRVN.css → vars-JHTUGNRG.css} +877 -866
- package/package.json +3 -2
- package/dist/components/rel-accordion/rel-accordion.ts +0 -336
- package/dist/components/rel-alert/rel-alert.ts +0 -305
- package/dist/components/rel-autocomplete/rel-autocomplete.ts +0 -720
- package/dist/components/rel-avatar/rel-avatar.ts +0 -142
- package/dist/components/rel-badge/rel-badge.ts +0 -175
- package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +0 -106
- package/dist/components/rel-button/rel-button.ts +0 -560
- package/dist/components/rel-calendar/index.ts +0 -1
- package/dist/components/rel-calendar/rel-calendar.ts +0 -565
- package/dist/components/rel-card/rel-card.ts +0 -88
- package/dist/components/rel-cardmedia/rel-cardmedia.ts +0 -88
- package/dist/components/rel-carousel/index.ts +0 -1
- package/dist/components/rel-carousel/rel-carousel.ts +0 -723
- package/dist/components/rel-chart/README.md +0 -286
- package/dist/components/rel-chart/index.ts +0 -1
- package/dist/components/rel-chart/rel-chart.ts +0 -754
- package/dist/components/rel-checkbox/rel-checkbox.ts +0 -382
- package/dist/components/rel-chip/rel-chip.ts +0 -483
- package/dist/components/rel-container/rel-container.ts +0 -95
- package/dist/components/rel-date-picker/date-format.test.ts +0 -43
- package/dist/components/rel-date-picker/date-format.ts +0 -47
- package/dist/components/rel-date-picker/date-parse.test.ts +0 -116
- package/dist/components/rel-date-picker/date-parse.ts +0 -178
- package/dist/components/rel-date-picker/index.ts +0 -1
- package/dist/components/rel-date-picker/rel-date-picker.ts +0 -1676
- package/dist/components/rel-divider/rel-divider.ts +0 -54
- package/dist/components/rel-drawer/index.ts +0 -1
- package/dist/components/rel-drawer/rel-drawer-footer.ts +0 -27
- package/dist/components/rel-drawer/rel-drawer.ts +0 -607
- package/dist/components/rel-kbd/rel-kbd.ts +0 -188
- package/dist/components/rel-layout/rel-layout.ts +0 -49
- package/dist/components/rel-menu/rel-menu.ts +0 -541
- package/dist/components/rel-menubar/index.ts +0 -2
- package/dist/components/rel-menubar/rel-menubar-menu.css +0 -3
- package/dist/components/rel-menubar/rel-menubar-menu.ts +0 -17
- package/dist/components/rel-menubar/rel-menubar.ts +0 -777
- package/dist/components/rel-nav-menu/README.md +0 -289
- package/dist/components/rel-nav-menu/index.ts +0 -3
- package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +0 -243
- package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +0 -72
- package/dist/components/rel-nav-menu/rel-nav-menu.ts +0 -270
- package/dist/components/rel-navbar/README.md +0 -126
- package/dist/components/rel-navbar/rel-navbar.ts +0 -105
- package/dist/components/rel-otp/index.ts +0 -1
- package/dist/components/rel-otp/rel-otp.ts +0 -527
- package/dist/components/rel-pagination/index.ts +0 -1
- package/dist/components/rel-pagination/rel-pagination.ts +0 -522
- package/dist/components/rel-popover/index.ts +0 -2
- package/dist/components/rel-popover/rel-popover-content.ts +0 -81
- package/dist/components/rel-popover/rel-popover.ts +0 -475
- package/dist/components/rel-progress/rel-progress.ts +0 -234
- package/dist/components/rel-radio/rel-radio.ts +0 -481
- package/dist/components/rel-range-picker/rel-range-picker.ts +0 -1688
- package/dist/components/rel-resizable/index.ts +0 -3
- package/dist/components/rel-resizable/rel-resizable-group.ts +0 -405
- package/dist/components/rel-resizable/rel-resizable-handle.ts +0 -150
- package/dist/components/rel-resizable/rel-resizable-panel.ts +0 -58
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +0 -217
- package/dist/components/rel-select/rel-select.ts +0 -1416
- package/dist/components/rel-sheet/rel-sheet.ts +0 -610
- package/dist/components/rel-sidebar/README.md +0 -209
- package/dist/components/rel-sidebar/rel-sidebar.ts +0 -1191
- package/dist/components/rel-skeleton/rel-skeleton.ts +0 -143
- package/dist/components/rel-slider/rel-slider.ts +0 -673
- package/dist/components/rel-snackbar/rel-snackbar.ts +0 -329
- package/dist/components/rel-stepper/rel-stepper.ts +0 -567
- package/dist/components/rel-switch/rel-switch.ts +0 -199
- package/dist/components/rel-tab/rel-tab.ts +0 -264
- package/dist/components/rel-table/README.md +0 -178
- package/dist/components/rel-table/rel-table-body.ts +0 -22
- package/dist/components/rel-table/rel-table-caption.ts +0 -30
- package/dist/components/rel-table/rel-table-cell.ts +0 -43
- package/dist/components/rel-table/rel-table-footer.ts +0 -24
- package/dist/components/rel-table/rel-table-head.ts +0 -47
- package/dist/components/rel-table/rel-table-header.ts +0 -22
- package/dist/components/rel-table/rel-table-row.ts +0 -37
- package/dist/components/rel-table/rel-table.ts +0 -62
- package/dist/components/rel-textfield/rel-textfield.ts +0 -1054
- package/dist/components/rel-time-picker/rel-time-picker.ts +0 -1482
- package/dist/components/rel-time-picker/time-parse.test.ts +0 -67
- package/dist/components/rel-time-picker/time-parse.ts +0 -85
- package/dist/components/rel-toast/rel-toast.ts +0 -206
- package/dist/components/rel-tooltip/rel-tooltip.ts +0 -193
- package/dist/components/rel-topbar/rel-topbar-actions.ts +0 -27
- package/dist/components/rel-topbar/rel-topbar-brand.ts +0 -54
- package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +0 -69
- package/dist/components/rel-topbar/rel-topbar-dropdown.ts +0 -31
- package/dist/components/rel-topbar/rel-topbar-item.ts +0 -324
- package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +0 -27
- package/dist/components/rel-topbar/rel-topbar-profile.ts +0 -243
- package/dist/components/rel-topbar/rel-topbar-utilities.ts +0 -27
- package/dist/components/rel-topbar/rel-topbar.ts +0 -446
- package/dist/components/rel-upload/UPLOAD-DESIGN-CHANGES.md +0 -134
- package/dist/components/rel-upload/rel-upload-actions.ts +0 -163
- package/dist/components/rel-upload/rel-upload.ts +0 -968
|
@@ -1,720 +0,0 @@
|
|
|
1
|
-
import { LitElement, html, css, nothing, PropertyValues, TemplateResult } from "lit";
|
|
2
|
-
import { customElement, property, state, query } from "lit/decorators.js";
|
|
3
|
-
import { repeat } from "lit/directives/repeat.js";
|
|
4
|
-
import "@reliability-design/icons/rel-icon";
|
|
5
|
-
import "../rel-textfield/rel-textfield.js";
|
|
6
|
-
import type {
|
|
7
|
-
RelTextfield,
|
|
8
|
-
TextFieldSize,
|
|
9
|
-
TextFieldVariant,
|
|
10
|
-
FieldLabelStyle,
|
|
11
|
-
} from "../rel-textfield/rel-textfield.js";
|
|
12
|
-
import { announce } from "../../utils/announce";
|
|
13
|
-
|
|
14
|
-
/** An item is a plain string or any object read through displayField/valueField. */
|
|
15
|
-
export type AutocompleteItem = string | Record<string, unknown>;
|
|
16
|
-
export type AutocompleteFilter = "contains" | "starts-with" | "none";
|
|
17
|
-
export type AutocompleteSearchFn = (query: string) => Promise<AutocompleteItem[]>;
|
|
18
|
-
|
|
19
|
-
interface NormalizedItem {
|
|
20
|
-
value: string;
|
|
21
|
-
label: string;
|
|
22
|
-
icon: string;
|
|
23
|
-
item: AutocompleteItem;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* `<rel-autocomplete>` — searchable single-select type-ahead field.
|
|
28
|
-
*
|
|
29
|
-
* Composes `<rel-textfield>` for all field chrome (label, sizes, variants,
|
|
30
|
-
* states, icons) and adds a suggestion panel on top. Two data modes:
|
|
31
|
-
* - local: set `items`; typing filters them (`filter` mode)
|
|
32
|
-
* - remote: set `searchFn`; input is debounced (`debounceMs`), queries are
|
|
33
|
-
* latest-wins (stale responses discarded), and `items` is ignored
|
|
34
|
-
*
|
|
35
|
-
* The committed `value` is always the selected item's value string (its
|
|
36
|
-
* `valueField`) — typing alone emits `rel-change` with the raw text but does
|
|
37
|
-
* not change `value` until an item is picked (or the text exactly matches an
|
|
38
|
-
* item's label/value on Enter/blur).
|
|
39
|
-
*/
|
|
40
|
-
@customElement("rel-autocomplete")
|
|
41
|
-
export class RelAutocomplete extends LitElement {
|
|
42
|
-
/** Local (or fallback initial) suggestion list. Ignored while searchFn drives results. */
|
|
43
|
-
@property({ type: Array }) items: AutocompleteItem[] = [];
|
|
44
|
-
|
|
45
|
-
/** Committed value — the selected item's valueField string ("" = none). */
|
|
46
|
-
@property({ type: String }) value: string = "";
|
|
47
|
-
|
|
48
|
-
/** Object key read for the row label when items are objects. */
|
|
49
|
-
@property({ type: String, attribute: "display-field" }) displayField: string = "label";
|
|
50
|
-
|
|
51
|
-
/** Object key read for the committed value when items are objects. */
|
|
52
|
-
@property({ type: String, attribute: "value-field" }) valueField: string = "value";
|
|
53
|
-
|
|
54
|
-
/** Local filtering mode. Ignored when searchFn is set (results arrive pre-filtered). */
|
|
55
|
-
@property({ type: String }) filter: AutocompleteFilter = "contains";
|
|
56
|
-
|
|
57
|
-
/** Async data source. When set, local filtering is bypassed entirely. */
|
|
58
|
-
@property({ attribute: false }) searchFn?: AutocompleteSearchFn;
|
|
59
|
-
|
|
60
|
-
/** Debounce for searchFn calls, in ms. */
|
|
61
|
-
@property({ type: Number, attribute: "debounce-ms" }) debounceMs: number = 300;
|
|
62
|
-
|
|
63
|
-
/** Minimum typed characters before the panel opens / searchFn fires. 0 = open on focus. */
|
|
64
|
-
@property({ type: Number, attribute: "min-search-length" }) minSearchLength: number = 1;
|
|
65
|
-
|
|
66
|
-
/** Optional cap on rendered suggestions (panel scrolls regardless). */
|
|
67
|
-
@property({ type: Number, attribute: "max-suggestions" }) maxSuggestions: number = Infinity;
|
|
68
|
-
|
|
69
|
-
/** Shows a trailing ✕ that clears the text and the committed value. */
|
|
70
|
-
@property({ type: Boolean }) clearable: boolean = false;
|
|
71
|
-
|
|
72
|
-
/** Externally-controlled loading state (internal searchFn loading is automatic). */
|
|
73
|
-
@property({ type: Boolean }) loading: boolean = false;
|
|
74
|
-
|
|
75
|
-
/** Row shown when a query yields nothing. */
|
|
76
|
-
@property({ type: String, attribute: "no-results-text" }) noResultsText: string = "No results found";
|
|
77
|
-
|
|
78
|
-
@property({ type: String, attribute: "loading-text" }) loadingText: string = "Loading…";
|
|
79
|
-
|
|
80
|
-
// ── Field chrome passthroughs (forwarded to the inner rel-textfield) ──
|
|
81
|
-
@property({ type: String }) label?: string;
|
|
82
|
-
@property({ type: String }) placeholder: string = "";
|
|
83
|
-
@property({ type: String }) size: TextFieldSize = "medium";
|
|
84
|
-
@property({ type: String }) variant: TextFieldVariant = "default";
|
|
85
|
-
/** See `FieldLabelStyle`. Left `undefined` the deprecated `floatingLabel` decides. */
|
|
86
|
-
@property({ type: String, attribute: "label-style", reflect: true })
|
|
87
|
-
labelStyle?: FieldLabelStyle;
|
|
88
|
-
/** @deprecated Use `labelStyle="floating"`. Kept permanently as an alias. */
|
|
89
|
-
@property({ type: Boolean, attribute: "floating-label" }) floatingLabel = false;
|
|
90
|
-
/** Accessible name for the input. Required when `labelStyle="none"`. */
|
|
91
|
-
@property({ type: String, attribute: "field-aria-label" }) fieldAriaLabel?: string;
|
|
92
|
-
/** IDREF list of external element(s) naming the input. Wins over `fieldAriaLabel`. */
|
|
93
|
-
@property({ type: String, attribute: "field-aria-labelledby" }) fieldAriaLabelledby?: string;
|
|
94
|
-
@property({ type: String, attribute: "prefix-icon" }) prefixIcon?: string;
|
|
95
|
-
@property({ type: Boolean }) disabled = false;
|
|
96
|
-
@property({ type: Boolean }) readonly = false;
|
|
97
|
-
@property({ type: Boolean }) required = false;
|
|
98
|
-
@property({ type: Boolean }) error = false;
|
|
99
|
-
@property({ type: String, attribute: "error-message" }) errorMessage?: string;
|
|
100
|
-
@property({ type: String, attribute: "helper-text" }) helperText?: string;
|
|
101
|
-
@property({ type: String }) name?: string;
|
|
102
|
-
@property({ type: Boolean, attribute: "full-width" }) fullWidth = true;
|
|
103
|
-
|
|
104
|
-
@state() private open = false;
|
|
105
|
-
@state() private inputText = "";
|
|
106
|
-
@state() private highlightedIndex = -1;
|
|
107
|
-
@state() private panelStyle = "";
|
|
108
|
-
@state() private searching = false;
|
|
109
|
-
/** searchFn results; null = idle/local mode (use items). */
|
|
110
|
-
@state() private remoteItems: AutocompleteItem[] | null = null;
|
|
111
|
-
|
|
112
|
-
@query("rel-textfield") private fieldEl!: RelTextfield;
|
|
113
|
-
@query(".ac-panel") private panelEl!: HTMLDivElement;
|
|
114
|
-
|
|
115
|
-
private uniqueId = `autocomplete-${Math.random().toString(36).slice(2)}`;
|
|
116
|
-
private debounceTimer?: ReturnType<typeof setTimeout>;
|
|
117
|
-
private searchSeq = 0;
|
|
118
|
-
/** Set while a selection/clear updates `value` internally, so willUpdate
|
|
119
|
-
* doesn't overwrite the input text the way it does for external writes. */
|
|
120
|
-
private internalValueWrite = false;
|
|
121
|
-
|
|
122
|
-
/** Name used for the suggestion panel — the visible label, or fieldAriaLabel
|
|
123
|
-
* once labelStyle="none" hides it. Mirrors rel-textfield's own resolution. */
|
|
124
|
-
private get accessibleName() {
|
|
125
|
-
return (this.labelStyle === "none" ? this.fieldAriaLabel : this.label) || "";
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
static styles = css`
|
|
129
|
-
:host {
|
|
130
|
-
color: var(--rel-text-default);
|
|
131
|
-
display: block;
|
|
132
|
-
font-family: var(--rel-fonts-font-family);
|
|
133
|
-
width: 100%;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
:host(:not([full-width])) {
|
|
137
|
-
display: inline-block;
|
|
138
|
-
width: auto;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/* ── Suggestion panel — mirrors rel-select's listbox so the two read as
|
|
142
|
-
one family (same tokens, radius, shadow, row styling). ── */
|
|
143
|
-
.ac-panel {
|
|
144
|
-
position: fixed;
|
|
145
|
-
z-index: 1001;
|
|
146
|
-
display: none;
|
|
147
|
-
/* Reset native [popover] UA styles (the panel is promoted to the
|
|
148
|
-
top layer so transformed ancestors — e.g. a centered rel-modal
|
|
149
|
-
panel — can't hijack its containing block or clip it). */
|
|
150
|
-
inset: auto;
|
|
151
|
-
margin: 0;
|
|
152
|
-
padding: 0;
|
|
153
|
-
width: auto;
|
|
154
|
-
height: auto;
|
|
155
|
-
color: inherit;
|
|
156
|
-
box-sizing: border-box;
|
|
157
|
-
background: var(--rel-autocomplete-bg, var(--rel-bg-neutral));
|
|
158
|
-
border: 1px solid var(--rel-autocomplete-menu-border, var(--rel-border-default));
|
|
159
|
-
border-radius: var(--rel-autocomplete-menu-radius, var(--rel-border-radius-sm));
|
|
160
|
-
box-shadow: var(--rel-autocomplete-menu-shadow, var(--rel-shadows-shadow-300));
|
|
161
|
-
max-height: 360px;
|
|
162
|
-
overflow-y: auto;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
.ac-panel[data-open] {
|
|
166
|
-
display: block;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
.ac-options {
|
|
170
|
-
padding: 4px 0;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
.ac-option {
|
|
174
|
-
display: flex;
|
|
175
|
-
align-items: center;
|
|
176
|
-
gap: var(--rel-spacing-xs);
|
|
177
|
-
padding: var(--rel-spacing-xs) 12px;
|
|
178
|
-
color: var(--rel-autocomplete-value-color, var(--rel-text-default));
|
|
179
|
-
cursor: pointer;
|
|
180
|
-
font-family: var(--rel-body-md-regular-font-family);
|
|
181
|
-
font-weight: var(--rel-body-md-regular-font-weight);
|
|
182
|
-
font-size: var(--rel-body-md-regular-font-size);
|
|
183
|
-
line-height: var(--rel-body-md-regular-line-height);
|
|
184
|
-
letter-spacing: var(--rel-body-md-regular-letter-spacing);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
.ac-option:hover,
|
|
188
|
-
.ac-option.highlighted {
|
|
189
|
-
background: var(--rel-autocomplete-option-hover, var(--rel-bg-neutral-elevated-1));
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
.ac-option-icon {
|
|
193
|
-
flex-shrink: 0;
|
|
194
|
-
color: var(--rel-autocomplete-icon-color, var(--rel-text-default));
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
.ac-option-label {
|
|
198
|
-
overflow: hidden;
|
|
199
|
-
text-overflow: ellipsis;
|
|
200
|
-
white-space: nowrap;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
.ac-option-label b {
|
|
204
|
-
font-weight: var(--rel-body-md-medium-font-weight);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/* Status rows (loading / no results) — informational, not selectable */
|
|
208
|
-
.ac-status {
|
|
209
|
-
display: flex;
|
|
210
|
-
align-items: center;
|
|
211
|
-
gap: var(--rel-spacing-xs);
|
|
212
|
-
padding: var(--rel-spacing-xs) 12px;
|
|
213
|
-
color: var(--rel-autocomplete-status-color, var(--rel-text-body));
|
|
214
|
-
font-family: var(--rel-body-sm-regular-font-family);
|
|
215
|
-
font-weight: var(--rel-body-sm-regular-font-weight);
|
|
216
|
-
font-size: var(--rel-body-sm-regular-font-size);
|
|
217
|
-
line-height: var(--rel-body-sm-regular-line-height);
|
|
218
|
-
letter-spacing: var(--rel-body-sm-regular-letter-spacing);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
.ac-spinner {
|
|
222
|
-
animation: ac-spin 0.9s linear infinite;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
@keyframes ac-spin {
|
|
226
|
-
to {
|
|
227
|
-
transform: rotate(360deg);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
`;
|
|
231
|
-
|
|
232
|
-
disconnectedCallback() {
|
|
233
|
-
super.disconnectedCallback();
|
|
234
|
-
this.removeGlobalListeners();
|
|
235
|
-
this.hideNativePopover();
|
|
236
|
-
clearTimeout(this.debounceTimer);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
protected willUpdate(changed: PropertyValues<this>) {
|
|
240
|
-
// External value writes (attribute, Angular writeValue) sync the visible
|
|
241
|
-
// text to the matching item's label — or the raw value when unknown
|
|
242
|
-
// (e.g. remote item not currently in the list).
|
|
243
|
-
if (changed.has("value") && !this.internalValueWrite) {
|
|
244
|
-
const match = this.allItems.find((i) => i.value === this.value);
|
|
245
|
-
this.inputText = match ? match.label : this.value ?? "";
|
|
246
|
-
}
|
|
247
|
-
this.internalValueWrite = false;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
private wasLoading = false;
|
|
251
|
-
|
|
252
|
-
protected updated() {
|
|
253
|
-
if (this.open && this.isLoading && !this.wasLoading) {
|
|
254
|
-
announce("Loading suggestions");
|
|
255
|
-
}
|
|
256
|
-
this.wasLoading = this.isLoading;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/** Full normalized source list (remote results while searchFn drives, else items). */
|
|
260
|
-
private get allItems(): NormalizedItem[] {
|
|
261
|
-
const source = this.searchFn && this.remoteItems !== null ? this.remoteItems : this.items;
|
|
262
|
-
return source.map((item) => this.normalizeItem(item));
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
private normalizeItem(item: AutocompleteItem): NormalizedItem {
|
|
266
|
-
if (typeof item === "string") return { value: item, label: item, icon: "", item };
|
|
267
|
-
const rec = item;
|
|
268
|
-
const rawLabel = rec[this.displayField] ?? rec[this.valueField];
|
|
269
|
-
const label = rawLabel == null ? "" : String(rawLabel);
|
|
270
|
-
const rawValue = rec[this.valueField];
|
|
271
|
-
const value = rawValue == null ? label : String(rawValue);
|
|
272
|
-
const icon = typeof rec["icon"] === "string" ? (rec["icon"] as string) : "";
|
|
273
|
-
return { value, label, icon, item };
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/** Suggestions for the current query — local filter only applies without searchFn. */
|
|
277
|
-
private get filteredItems(): NormalizedItem[] {
|
|
278
|
-
let list = this.allItems;
|
|
279
|
-
if (!this.searchFn && this.filter !== "none" && this.inputText) {
|
|
280
|
-
const q = this.inputText.toLowerCase();
|
|
281
|
-
list = list.filter((i) =>
|
|
282
|
-
this.filter === "starts-with"
|
|
283
|
-
? i.label.toLowerCase().startsWith(q)
|
|
284
|
-
: i.label.toLowerCase().includes(q)
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
return Number.isFinite(this.maxSuggestions) ? list.slice(0, this.maxSuggestions) : list;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
private get isLoading(): boolean {
|
|
291
|
-
return this.loading || this.searching;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
private get queryLongEnough(): boolean {
|
|
295
|
-
return this.inputText.length >= this.minSearchLength;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// ── Panel open/close + positioning (same proven mechanics as rel-select:
|
|
299
|
-
// fixed panel, flip above when short on space, close on outside
|
|
300
|
-
// pointerdown and on any outside scroll instead of re-anchoring) ──
|
|
301
|
-
|
|
302
|
-
private addGlobalListeners() {
|
|
303
|
-
// Capture phase so a pointerdown that another component stops in its own
|
|
304
|
-
// capture listener (e.g. a rel-popover trigger) still closes this panel.
|
|
305
|
-
document.addEventListener("pointerdown", this.onOutsidePointerDown, true);
|
|
306
|
-
window.addEventListener("resize", this.updatePosition);
|
|
307
|
-
window.addEventListener("scroll", this.onPageScroll, true);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
private removeGlobalListeners() {
|
|
311
|
-
document.removeEventListener("pointerdown", this.onOutsidePointerDown, true);
|
|
312
|
-
window.removeEventListener("resize", this.updatePosition);
|
|
313
|
-
window.removeEventListener("scroll", this.onPageScroll, true);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
private onOutsidePointerDown = (e: PointerEvent) => {
|
|
317
|
-
if (!this.open) return;
|
|
318
|
-
if (e.composedPath().includes(this)) return;
|
|
319
|
-
this.commitExactMatch();
|
|
320
|
-
this.closePanel();
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
private onPageScroll = (e: Event) => {
|
|
324
|
-
if (!this.open) return;
|
|
325
|
-
if (e.composedPath()[0] === this.panelEl) return;
|
|
326
|
-
this.closePanel();
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
private openPanel() {
|
|
330
|
-
if (this.disabled || this.readonly || this.open) return;
|
|
331
|
-
this.open = true;
|
|
332
|
-
this.highlightedIndex = -1;
|
|
333
|
-
this.addGlobalListeners();
|
|
334
|
-
requestAnimationFrame(() => {
|
|
335
|
-
this.showNativePopover();
|
|
336
|
-
this.updatePosition();
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
private closePanel() {
|
|
341
|
-
if (!this.open) return;
|
|
342
|
-
this.open = false;
|
|
343
|
-
this.highlightedIndex = -1;
|
|
344
|
-
this.removeGlobalListeners();
|
|
345
|
-
this.hideNativePopover();
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/* A transformed ancestor (e.g. rel-modal's centered panel with
|
|
349
|
-
* translate(-50%,-50%)) becomes the containing block for position:fixed
|
|
350
|
-
* descendants, breaking viewport-based coordinates and letting the
|
|
351
|
-
* panel's overflow:hidden clip the suggestions. Promoting the panel to
|
|
352
|
-
* the browser top layer sidesteps both — same fix as rel-popover.
|
|
353
|
-
* `manual` keeps light-dismiss and Escape handling under this
|
|
354
|
-
* component's own control. */
|
|
355
|
-
|
|
356
|
-
private showNativePopover() {
|
|
357
|
-
const p = this.panelEl;
|
|
358
|
-
if (!p || typeof p.showPopover !== "function") return;
|
|
359
|
-
try {
|
|
360
|
-
if (!p.matches(":popover-open")) p.showPopover();
|
|
361
|
-
} catch {
|
|
362
|
-
/* older engines / detached element — fixed-position fallback applies */
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
private hideNativePopover() {
|
|
367
|
-
const p = this.panelEl;
|
|
368
|
-
if (!p || typeof p.hidePopover !== "function") return;
|
|
369
|
-
try {
|
|
370
|
-
if (p.matches(":popover-open")) p.hidePopover();
|
|
371
|
-
} catch {
|
|
372
|
-
/* already hidden or unsupported — nothing to do */
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
private updatePosition = () => {
|
|
377
|
-
if (!this.fieldEl || !this.panelEl) return;
|
|
378
|
-
|
|
379
|
-
this.panelEl.setAttribute("data-open", "");
|
|
380
|
-
|
|
381
|
-
const t = this.fieldEl.getBoundingClientRect();
|
|
382
|
-
const c = this.panelEl.getBoundingClientRect();
|
|
383
|
-
|
|
384
|
-
let top = t.bottom + 4;
|
|
385
|
-
if (top + c.height > window.innerHeight - 8) {
|
|
386
|
-
top = Math.max(8, t.top - c.height - 4);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
let left = t.left;
|
|
390
|
-
if (left + t.width > window.innerWidth - 8) {
|
|
391
|
-
left = Math.max(8, window.innerWidth - t.width - 8);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Without top-layer support a transformed ancestor may be the containing
|
|
395
|
-
// block for position:fixed, shifting the panel away from the viewport
|
|
396
|
-
// coordinates computed above. Apply, re-measure, and cancel the offset.
|
|
397
|
-
this.panelEl.style.top = `${top}px`;
|
|
398
|
-
this.panelEl.style.left = `${left}px`;
|
|
399
|
-
this.panelEl.style.width = `${t.width}px`;
|
|
400
|
-
const applied = this.panelEl.getBoundingClientRect();
|
|
401
|
-
top -= applied.top - top;
|
|
402
|
-
left -= applied.left - left;
|
|
403
|
-
this.panelEl.style.top = `${top}px`;
|
|
404
|
-
this.panelEl.style.left = `${left}px`;
|
|
405
|
-
|
|
406
|
-
this.panelStyle = `top:${top}px;left:${left}px;width:${t.width}px;`;
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
/** Re-evaluates whether the panel should be showing for the current state. */
|
|
410
|
-
private syncPanel() {
|
|
411
|
-
const shouldShow =
|
|
412
|
-
this.queryLongEnough && (this.isLoading || this.filteredItems.length > 0 || !!this.inputText || this.minSearchLength === 0);
|
|
413
|
-
if (shouldShow) {
|
|
414
|
-
if (!this.open) this.openPanel();
|
|
415
|
-
else requestAnimationFrame(() => this.updatePosition());
|
|
416
|
-
} else {
|
|
417
|
-
this.closePanel();
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// ── Data flow ──
|
|
422
|
-
|
|
423
|
-
private onFieldInput(e: CustomEvent<string>) {
|
|
424
|
-
e.stopPropagation();
|
|
425
|
-
this.inputText = e.detail;
|
|
426
|
-
this.highlightedIndex = -1;
|
|
427
|
-
|
|
428
|
-
// Re-emit as our own rel-change so consumers listen on <rel-autocomplete>
|
|
429
|
-
// (the inner textfield's event is stopped above).
|
|
430
|
-
this.dispatchEvent(
|
|
431
|
-
new CustomEvent<string>("rel-change", { detail: this.inputText, bubbles: true, composed: true })
|
|
432
|
-
);
|
|
433
|
-
|
|
434
|
-
if (this.searchFn) {
|
|
435
|
-
clearTimeout(this.debounceTimer);
|
|
436
|
-
if (!this.queryLongEnough) {
|
|
437
|
-
this.searchSeq++; // invalidate any in-flight query
|
|
438
|
-
this.searching = false;
|
|
439
|
-
this.remoteItems = null;
|
|
440
|
-
this.closePanel();
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
this.searching = true;
|
|
444
|
-
this.syncPanel();
|
|
445
|
-
this.debounceTimer = setTimeout(() => this.runSearch(this.inputText), this.debounceMs);
|
|
446
|
-
} else {
|
|
447
|
-
this.syncPanel();
|
|
448
|
-
this.announceResults();
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
private announceResults() {
|
|
453
|
-
if (!this.open || this.isLoading) return;
|
|
454
|
-
const n = this.filteredItems.length;
|
|
455
|
-
if (n === 0) {
|
|
456
|
-
if (this.inputText) announce(this.noResultsText);
|
|
457
|
-
} else {
|
|
458
|
-
announce(`${n} result${n === 1 ? "" : "s"} available`);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
private async runSearch(query: string) {
|
|
463
|
-
const seq = ++this.searchSeq;
|
|
464
|
-
try {
|
|
465
|
-
const results = await this.searchFn!(query);
|
|
466
|
-
if (seq !== this.searchSeq) return; // stale — a newer query superseded this one
|
|
467
|
-
this.remoteItems = results ?? [];
|
|
468
|
-
} catch (err) {
|
|
469
|
-
if (seq !== this.searchSeq) return;
|
|
470
|
-
this.remoteItems = [];
|
|
471
|
-
this.dispatchEvent(
|
|
472
|
-
new CustomEvent("rel-search-error", { detail: { query, error: err }, bubbles: true, composed: true })
|
|
473
|
-
);
|
|
474
|
-
} finally {
|
|
475
|
-
if (seq === this.searchSeq) {
|
|
476
|
-
this.searching = false;
|
|
477
|
-
this.syncPanel();
|
|
478
|
-
this.announceResults();
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
private selectItem(item: NormalizedItem) {
|
|
484
|
-
this.internalValueWrite = true;
|
|
485
|
-
this.value = item.value;
|
|
486
|
-
this.inputText = item.label;
|
|
487
|
-
this.closePanel();
|
|
488
|
-
this.emitChanged(item);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/** Silent commit when typed text exactly matches an item (label or value, case-insensitive). */
|
|
492
|
-
private commitExactMatch() {
|
|
493
|
-
if (!this.inputText) return;
|
|
494
|
-
const q = this.inputText.toLowerCase();
|
|
495
|
-
const match = this.allItems.find(
|
|
496
|
-
(i) => i.label.toLowerCase() === q || i.value.toLowerCase() === q
|
|
497
|
-
);
|
|
498
|
-
if (match && match.value !== this.value) {
|
|
499
|
-
this.internalValueWrite = true;
|
|
500
|
-
this.value = match.value;
|
|
501
|
-
this.inputText = match.label;
|
|
502
|
-
this.emitChanged(match);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
private emitChanged(item: NormalizedItem | null) {
|
|
507
|
-
this.dispatchEvent(
|
|
508
|
-
new CustomEvent("changed", {
|
|
509
|
-
detail: { value: this.value, item: item ? item.item : null },
|
|
510
|
-
bubbles: true,
|
|
511
|
-
composed: true,
|
|
512
|
-
})
|
|
513
|
-
);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
private clear() {
|
|
517
|
-
if (this.disabled || this.readonly) return;
|
|
518
|
-
const hadValue = !!this.value || !!this.inputText;
|
|
519
|
-
this.internalValueWrite = true;
|
|
520
|
-
this.value = "";
|
|
521
|
-
this.inputText = "";
|
|
522
|
-
this.searchSeq++;
|
|
523
|
-
this.searching = false;
|
|
524
|
-
this.remoteItems = null;
|
|
525
|
-
this.closePanel();
|
|
526
|
-
if (hadValue) {
|
|
527
|
-
this.dispatchEvent(new CustomEvent("rel-clear", { bubbles: true, composed: true }));
|
|
528
|
-
this.emitChanged(null);
|
|
529
|
-
this.dispatchEvent(
|
|
530
|
-
new CustomEvent<string>("rel-change", { detail: "", bubbles: true, composed: true })
|
|
531
|
-
);
|
|
532
|
-
}
|
|
533
|
-
this.fieldEl?.focus();
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// ── Keyboard / focus ──
|
|
537
|
-
|
|
538
|
-
private onKeyDown(e: KeyboardEvent) {
|
|
539
|
-
if (this.disabled || this.readonly) return;
|
|
540
|
-
const filtered = this.filteredItems;
|
|
541
|
-
|
|
542
|
-
switch (e.key) {
|
|
543
|
-
case "ArrowDown":
|
|
544
|
-
e.preventDefault();
|
|
545
|
-
if (!this.open) {
|
|
546
|
-
if (this.queryLongEnough) this.openPanel();
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
this.highlightedIndex = Math.min(filtered.length - 1, this.highlightedIndex + 1);
|
|
550
|
-
this.announceHighlighted(filtered);
|
|
551
|
-
break;
|
|
552
|
-
case "ArrowUp":
|
|
553
|
-
e.preventDefault();
|
|
554
|
-
if (!this.open) return;
|
|
555
|
-
this.highlightedIndex = Math.max(0, this.highlightedIndex - 1);
|
|
556
|
-
this.announceHighlighted(filtered);
|
|
557
|
-
break;
|
|
558
|
-
case "Enter":
|
|
559
|
-
if (this.open && this.highlightedIndex >= 0 && filtered[this.highlightedIndex]) {
|
|
560
|
-
e.preventDefault();
|
|
561
|
-
this.selectItem(filtered[this.highlightedIndex]);
|
|
562
|
-
} else {
|
|
563
|
-
// Free text stays free text — but an exact match commits.
|
|
564
|
-
this.commitExactMatch();
|
|
565
|
-
this.closePanel();
|
|
566
|
-
}
|
|
567
|
-
break;
|
|
568
|
-
case "Escape":
|
|
569
|
-
if (this.open) {
|
|
570
|
-
e.preventDefault();
|
|
571
|
-
this.closePanel();
|
|
572
|
-
}
|
|
573
|
-
break;
|
|
574
|
-
case "Tab":
|
|
575
|
-
this.commitExactMatch();
|
|
576
|
-
this.closePanel();
|
|
577
|
-
break;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
private announceHighlighted(filtered: NormalizedItem[]) {
|
|
582
|
-
const item = filtered[this.highlightedIndex];
|
|
583
|
-
if (item) announce(`${item.label}, ${this.highlightedIndex + 1} of ${filtered.length}`);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
private onFocusIn() {
|
|
587
|
-
if (this.disabled || this.readonly) return;
|
|
588
|
-
if (this.queryLongEnough && (this.filteredItems.length > 0 || this.minSearchLength === 0)) {
|
|
589
|
-
this.openPanel();
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
private onFocusOut(e: FocusEvent) {
|
|
594
|
-
const next = e.relatedTarget as Node | null;
|
|
595
|
-
if (next && (this.contains(next) || this.renderRoot.contains(next))) return;
|
|
596
|
-
// Focus genuinely left the component (outside pointerdown handles the
|
|
597
|
-
// click-away case before blur; this covers programmatic focus moves).
|
|
598
|
-
this.commitExactMatch();
|
|
599
|
-
this.closePanel();
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
/** Delegates focus to the inner textfield. */
|
|
603
|
-
focus() {
|
|
604
|
-
this.fieldEl?.focus();
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
/** Bolds the first occurrence of the query inside a suggestion label. */
|
|
608
|
-
private renderLabel(label: string): TemplateResult | string {
|
|
609
|
-
const q = this.inputText;
|
|
610
|
-
if (!q) return label;
|
|
611
|
-
const idx = label.toLowerCase().indexOf(q.toLowerCase());
|
|
612
|
-
if (idx < 0) return label;
|
|
613
|
-
return html`${label.slice(0, idx)}<b>${label.slice(idx, idx + q.length)}</b>${label.slice(idx + q.length)}`;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
render() {
|
|
617
|
-
const filtered = this.filteredItems;
|
|
618
|
-
const panelId = `${this.uniqueId}-panel`;
|
|
619
|
-
const activeOptionId =
|
|
620
|
-
this.open && this.highlightedIndex >= 0 ? `${this.uniqueId}-option-${this.highlightedIndex}` : "";
|
|
621
|
-
const showClear = this.clearable && !!this.inputText && !this.disabled && !this.readonly;
|
|
622
|
-
const showNoResults = !this.isLoading && filtered.length === 0 && this.queryLongEnough && !!this.inputText;
|
|
623
|
-
|
|
624
|
-
// ARIA note: the real <input> lives in rel-textfield's shadow root, which
|
|
625
|
-
// aria-activedescendant can't reach across. The combobox role therefore
|
|
626
|
-
// sits on this wrapper (containing the field) in the same tree as the
|
|
627
|
-
// panel — the pragmatic pattern for nested shadow roots.
|
|
628
|
-
return html`
|
|
629
|
-
<div
|
|
630
|
-
class="ac-field"
|
|
631
|
-
role="combobox"
|
|
632
|
-
aria-haspopup="listbox"
|
|
633
|
-
aria-expanded="${this.open}"
|
|
634
|
-
aria-controls="${panelId}"
|
|
635
|
-
aria-activedescendant="${activeOptionId}"
|
|
636
|
-
@keydown="${this.onKeyDown}"
|
|
637
|
-
@focusin="${this.onFocusIn}"
|
|
638
|
-
@focusout="${this.onFocusOut}"
|
|
639
|
-
>
|
|
640
|
-
<rel-textfield
|
|
641
|
-
exportparts="label, control, input, helper, right-icon"
|
|
642
|
-
.label="${this.label}"
|
|
643
|
-
.placeholder="${this.placeholder}"
|
|
644
|
-
.value="${this.inputText}"
|
|
645
|
-
.size="${this.size}"
|
|
646
|
-
.variant="${this.variant}"
|
|
647
|
-
.labelStyle="${this.labelStyle}"
|
|
648
|
-
.floatingLabel="${this.floatingLabel}"
|
|
649
|
-
.fieldAriaLabel="${this.fieldAriaLabel}"
|
|
650
|
-
.fieldAriaLabelledby="${this.fieldAriaLabelledby}"
|
|
651
|
-
.prefixIcon="${this.prefixIcon}"
|
|
652
|
-
.disabled="${this.disabled}"
|
|
653
|
-
.readonly="${this.readonly}"
|
|
654
|
-
.required="${this.required}"
|
|
655
|
-
.error="${this.error}"
|
|
656
|
-
.errorMessage="${this.errorMessage}"
|
|
657
|
-
.helperText="${this.helperText}"
|
|
658
|
-
.name="${this.name}"
|
|
659
|
-
.rightIcon="${showClear ? "close" : undefined}"
|
|
660
|
-
.clickableRightIcon="${showClear}"
|
|
661
|
-
right-icon-aria-label="Clear"
|
|
662
|
-
@rel-change="${this.onFieldInput}"
|
|
663
|
-
@rel-right-icon-click="${this.clear}"
|
|
664
|
-
></rel-textfield>
|
|
665
|
-
</div>
|
|
666
|
-
|
|
667
|
-
<div
|
|
668
|
-
id="${panelId}"
|
|
669
|
-
part="panel list"
|
|
670
|
-
class="ac-panel"
|
|
671
|
-
popover="manual"
|
|
672
|
-
role="listbox"
|
|
673
|
-
aria-label="${this.accessibleName || "Suggestions"}"
|
|
674
|
-
style="${this.panelStyle}"
|
|
675
|
-
?data-open="${this.open}"
|
|
676
|
-
>
|
|
677
|
-
${this.isLoading
|
|
678
|
-
? html`<div class="ac-status" role="status">
|
|
679
|
-
<rel-icon name="status-loading" size="sm" class="ac-spinner"></rel-icon>
|
|
680
|
-
<span>${this.loadingText}</span>
|
|
681
|
-
</div>`
|
|
682
|
-
: nothing}
|
|
683
|
-
${showNoResults
|
|
684
|
-
? html`<div class="ac-status">${this.noResultsText}</div>`
|
|
685
|
-
: nothing}
|
|
686
|
-
${filtered.length > 0
|
|
687
|
-
? html`<div class="ac-options">
|
|
688
|
-
${repeat(
|
|
689
|
-
filtered,
|
|
690
|
-
(item) => item.value,
|
|
691
|
-
(item, index) => html`
|
|
692
|
-
<div
|
|
693
|
-
id="${this.uniqueId}-option-${index}"
|
|
694
|
-
part="option"
|
|
695
|
-
class="ac-option ${index === this.highlightedIndex ? "highlighted" : ""}"
|
|
696
|
-
role="option"
|
|
697
|
-
aria-selected="${item.value === this.value}"
|
|
698
|
-
@mousedown="${(e: Event) => e.preventDefault()}"
|
|
699
|
-
@click="${() => this.selectItem(item)}"
|
|
700
|
-
@pointerenter="${() => (this.highlightedIndex = index)}"
|
|
701
|
-
>
|
|
702
|
-
${item.icon
|
|
703
|
-
? html`<rel-icon name="${item.icon}" size="sm" class="ac-option-icon"></rel-icon>`
|
|
704
|
-
: nothing}
|
|
705
|
-
<span class="ac-option-label">${this.renderLabel(item.label)}</span>
|
|
706
|
-
</div>
|
|
707
|
-
`
|
|
708
|
-
)}
|
|
709
|
-
</div>`
|
|
710
|
-
: nothing}
|
|
711
|
-
</div>
|
|
712
|
-
`;
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
declare global {
|
|
717
|
-
interface HTMLElementTagNameMap {
|
|
718
|
-
"rel-autocomplete": RelAutocomplete;
|
|
719
|
-
}
|
|
720
|
-
}
|