@nuralyui/select 0.0.5 → 0.1.1
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/bundle.js +788 -0
- package/index.d.ts +15 -0
- package/index.js +18 -0
- package/index.js.map +1 -1
- package/package.json +29 -3
- package/react.d.ts +12 -1
- package/react.js +13 -2
- package/react.js.map +1 -1
- package/select.component.d.ts +305 -20
- package/select.component.js +601 -137
- package/select.component.js.map +1 -1
- package/select.constant.d.ts +130 -0
- package/select.constant.js +133 -0
- package/select.constant.js.map +1 -1
- package/select.style.js +377 -158
- package/select.style.js.map +1 -1
- package/select.style.variables.d.ts +6 -0
- package/select.style.variables.js +93 -0
- package/select.style.variables.js.map +1 -0
- package/select.types.d.ts +86 -10
- package/select.types.js +66 -16
- package/select.types.js.map +1 -1
- package/demo/select-demo.d.ts +0 -25
- package/demo/select-demo.d.ts.map +0 -1
- package/demo/select-demo.js +0 -254
- package/demo/select-demo.js.map +0 -1
- package/index.d.ts.map +0 -1
- package/react.d.ts.map +0 -1
- package/select.component.d.ts.map +0 -1
- package/select.constant.d.ts.map +0 -1
- package/select.style.d.ts.map +0 -1
- package/select.types.d.ts.map +0 -1
- package/test/select_test.d.ts +0 -2
- package/test/select_test.d.ts.map +0 -1
- package/test/select_test.js +0 -132
- package/test/select_test.js.map +0 -1
package/select.component.js
CHANGED
|
@@ -1,209 +1,673 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2023 Nuraly, Laabidi Aymen
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
1
6
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
7
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
8
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
9
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
10
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
11
|
};
|
|
7
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
8
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
9
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
10
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
11
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
12
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
13
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
14
|
-
});
|
|
15
|
-
};
|
|
16
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
17
12
|
import { LitElement, html, nothing } from 'lit';
|
|
18
|
-
import { property,
|
|
13
|
+
import { property, customElement, query } from 'lit/decorators.js';
|
|
19
14
|
import { styles } from './select.style.js';
|
|
20
15
|
import { map } from 'lit/directives/map.js';
|
|
21
|
-
import { OptionSelectionMode, OptionSize, OptionStatus, OptionType } from './select.types.js';
|
|
22
16
|
import { choose } from 'lit/directives/choose.js';
|
|
23
|
-
import {
|
|
17
|
+
import { classMap } from 'lit/directives/class-map.js';
|
|
24
18
|
import { styleMap } from 'lit/directives/style-map.js';
|
|
25
|
-
|
|
19
|
+
import { NuralyUIBaseMixin } from '../../shared/base-mixin.js';
|
|
20
|
+
// Import types
|
|
21
|
+
import { SelectType, SelectSize, SelectStatus } from './select.types.js';
|
|
22
|
+
// Import controllers
|
|
23
|
+
import { SelectSelectionController, SelectKeyboardController, SelectDropdownController, SelectFocusController, SelectValidationController, SelectSearchController, SelectEventController } from './controllers/index.js';
|
|
24
|
+
/**
|
|
25
|
+
* Advanced select component with multiple selection modes, validation, and accessibility features.
|
|
26
|
+
*
|
|
27
|
+
* Supports single and multiple selection, custom rendering, validation states, keyboard navigation,
|
|
28
|
+
* and various display types including default, inline, button, and slot-based configurations.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```html
|
|
32
|
+
* <!-- Basic select -->
|
|
33
|
+
* <hy-select placeholder="Choose an option">
|
|
34
|
+
* <option value="1">Option 1</option>
|
|
35
|
+
* <option value="2">Option 2</option>
|
|
36
|
+
* </hy-select>
|
|
37
|
+
*
|
|
38
|
+
* <!-- Multiple selection -->
|
|
39
|
+
* <hy-select multiple placeholder="Choose multiple options"></hy-select>
|
|
40
|
+
*
|
|
41
|
+
* <!-- With validation -->
|
|
42
|
+
* <hy-select required status="error"></hy-select>
|
|
43
|
+
*
|
|
44
|
+
* <!-- Button style -->
|
|
45
|
+
* <hy-select type="button"></hy-select>
|
|
46
|
+
*
|
|
47
|
+
* <!-- With search functionality -->
|
|
48
|
+
* <hy-select searchable search-placeholder="Search options..."></hy-select>
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @fires nr-change - Selection changed
|
|
52
|
+
* @fires nr-focus - Component focused
|
|
53
|
+
* @fires nr-blur - Component blurred
|
|
54
|
+
* @fires nr-dropdown-open - Dropdown opened
|
|
55
|
+
* @fires nr-dropdown-close - Dropdown closed
|
|
56
|
+
* @fires nr-validation - Validation state changed
|
|
57
|
+
*
|
|
58
|
+
* @slot label - Select label content
|
|
59
|
+
* @slot helper-text - Helper text below select
|
|
60
|
+
* @slot trigger - Custom trigger content (slot type only)
|
|
61
|
+
*
|
|
62
|
+
* @cssproperty --select-border-color - Border color
|
|
63
|
+
* @cssproperty --select-background - Background color
|
|
64
|
+
* @cssproperty --select-text-color - Text color
|
|
65
|
+
* @cssproperty --select-focus-color - Focus indicator color
|
|
66
|
+
* @cssproperty --select-dropdown-shadow - Dropdown shadow
|
|
67
|
+
* @cssproperty --select-no-options-color - No options message text color
|
|
68
|
+
* @cssproperty --select-no-options-icon-color - No options icon color
|
|
69
|
+
* @cssproperty --select-no-options-padding - Padding for no options message
|
|
70
|
+
* @cssproperty --select-no-options-gap - Gap between icon and text
|
|
71
|
+
* @cssproperty --select-search-border - Search input border
|
|
72
|
+
* @cssproperty --select-search-background - Search input background
|
|
73
|
+
* @cssproperty --select-search-padding - Search input padding
|
|
74
|
+
*/
|
|
75
|
+
let HySelectComponent = class HySelectComponent extends NuralyUIBaseMixin(LitElement) {
|
|
26
76
|
constructor() {
|
|
27
77
|
super(...arguments);
|
|
28
|
-
this.
|
|
78
|
+
this.requiredComponents = ["nr-input", "hy-icon"];
|
|
79
|
+
/** Array of options to display in the select dropdown */
|
|
80
|
+
this.options = [];
|
|
81
|
+
/** Default selected values (for initialization) */
|
|
82
|
+
this.defaultValue = [];
|
|
83
|
+
/** Placeholder text shown when no option is selected */
|
|
29
84
|
this.placeholder = 'Select an option';
|
|
85
|
+
/** Disables the select component */
|
|
30
86
|
this.disabled = false;
|
|
31
|
-
|
|
32
|
-
this.
|
|
87
|
+
/** Select display type (default, inline, button, slot) */
|
|
88
|
+
this.type = SelectType.Default;
|
|
89
|
+
/** Enables multiple option selection */
|
|
90
|
+
this.multiple = false;
|
|
91
|
+
/** Controls dropdown visibility */
|
|
33
92
|
this.show = false;
|
|
34
|
-
|
|
35
|
-
this.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
93
|
+
/** Validation status (default, warning, error, success) */
|
|
94
|
+
this.status = SelectStatus.Default;
|
|
95
|
+
/** Select size (small, medium, large) */
|
|
96
|
+
this.size = SelectSize.Medium;
|
|
97
|
+
/** Makes the select required for form validation */
|
|
98
|
+
this.required = false;
|
|
99
|
+
/** Form field name */
|
|
100
|
+
this.name = '';
|
|
101
|
+
/** Current selected value(s) */
|
|
102
|
+
this.value = '';
|
|
103
|
+
/** Message to display when no options are available */
|
|
104
|
+
this.noOptionsMessage = 'No options available';
|
|
105
|
+
/** Icon to display with the no options message */
|
|
106
|
+
this.noOptionsIcon = 'circle-info';
|
|
107
|
+
/** Enable search/filter functionality */
|
|
108
|
+
this.searchable = false;
|
|
109
|
+
/** Placeholder text for the search input */
|
|
110
|
+
this.searchPlaceholder = 'Search options...';
|
|
111
|
+
/** Current search query */
|
|
112
|
+
this.searchQuery = '';
|
|
113
|
+
/** Handles option selection logic */
|
|
114
|
+
this.selectionController = new SelectSelectionController(this);
|
|
115
|
+
/** Manages dropdown visibility and positioning */
|
|
116
|
+
this.dropdownController = new SelectDropdownController(this);
|
|
117
|
+
/** Handles keyboard navigation */
|
|
118
|
+
this.keyboardController = new SelectKeyboardController(this, this.selectionController, this.dropdownController);
|
|
119
|
+
/** Manages focus states */
|
|
120
|
+
this.focusController = new SelectFocusController(this);
|
|
121
|
+
/** Handles validation logic */
|
|
122
|
+
this.validationController = new SelectValidationController(this, this.selectionController);
|
|
123
|
+
/** Handles search/filter functionality */
|
|
124
|
+
this.searchController = new SelectSearchController(this);
|
|
125
|
+
/** Handles all event management */
|
|
126
|
+
this.eventController = new SelectEventController(this);
|
|
127
|
+
/**
|
|
128
|
+
* Handles clicks on the select trigger element
|
|
129
|
+
*/
|
|
130
|
+
this.handleTriggerClick = (event) => {
|
|
131
|
+
this.eventController.handleTriggerClick(event);
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Handles clicks on individual options
|
|
135
|
+
*/
|
|
136
|
+
this.handleOptionClick = (event, option) => {
|
|
137
|
+
this.eventController.handleOptionClick(event, option);
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Handles removal of selected tags in multiple selection mode
|
|
141
|
+
*/
|
|
142
|
+
this.handleTagRemove = (event, option) => {
|
|
143
|
+
this.eventController.handleTagRemove(event, option);
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Handles the clear all selections button
|
|
147
|
+
*/
|
|
148
|
+
this.handleClearAll = (event) => {
|
|
149
|
+
this.eventController.handleClearAll(event);
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Handles keyboard navigation and interactions
|
|
153
|
+
*/
|
|
154
|
+
this.handleKeyDown = (event) => {
|
|
155
|
+
this.eventController.handleKeyDown(event);
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Handles focus events
|
|
159
|
+
*/
|
|
160
|
+
this.handleFocus = () => {
|
|
161
|
+
this.eventController.handleFocus();
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Handles blur events
|
|
165
|
+
*/
|
|
166
|
+
this.handleBlur = () => {
|
|
167
|
+
this.eventController.handleBlur();
|
|
168
|
+
};
|
|
48
169
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.calculateOptionsPosition();
|
|
55
|
-
else
|
|
56
|
-
this.initOptionsPosition();
|
|
57
|
-
});
|
|
170
|
+
/**
|
|
171
|
+
* Component connected to DOM - initialize base functionality
|
|
172
|
+
*/
|
|
173
|
+
connectedCallback() {
|
|
174
|
+
super.connectedCallback();
|
|
58
175
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Component disconnected from DOM - cleanup event listeners
|
|
178
|
+
*/
|
|
179
|
+
disconnectedCallback() {
|
|
180
|
+
super.disconnectedCallback();
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* First render complete - setup controllers and initialize state
|
|
184
|
+
*/
|
|
185
|
+
firstUpdated(changedProperties) {
|
|
186
|
+
super.firstUpdated(changedProperties);
|
|
187
|
+
// Configure dropdown controller with DOM element references
|
|
188
|
+
if (this.optionsElement && this.wrapper) {
|
|
189
|
+
this.dropdownController.setElements(this.optionsElement, this.wrapper);
|
|
70
190
|
}
|
|
71
191
|
else {
|
|
72
|
-
|
|
192
|
+
// Retry element setup if DOM isn't ready yet
|
|
193
|
+
setTimeout(() => {
|
|
194
|
+
if (this.optionsElement && this.wrapper) {
|
|
195
|
+
this.dropdownController.setElements(this.optionsElement, this.wrapper);
|
|
196
|
+
}
|
|
197
|
+
}, 100);
|
|
198
|
+
}
|
|
199
|
+
// Apply default selection if specified
|
|
200
|
+
if (this.defaultValue.length > 0) {
|
|
201
|
+
this.selectionController.initializeFromDefaultValue();
|
|
73
202
|
}
|
|
74
203
|
}
|
|
75
|
-
|
|
76
|
-
|
|
204
|
+
// === Public API Methods ===
|
|
205
|
+
/**
|
|
206
|
+
* Gets the currently selected options
|
|
207
|
+
* @returns Array of selected options
|
|
208
|
+
*/
|
|
209
|
+
get selectedOptions() {
|
|
210
|
+
return this.selectionController.getSelectedOptions();
|
|
77
211
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
else {
|
|
85
|
-
if (this.selected.includes(selectedOption)) {
|
|
86
|
-
this.selected = this.selected.filter((previousSelectedOption) => previousSelectedOption.label != selectedOption.label);
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
this.selected = [...this.selected, selectedOption];
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
this.dispatchChangeEvent();
|
|
212
|
+
/**
|
|
213
|
+
* Gets the first selected option (for single selection mode)
|
|
214
|
+
* @returns Selected option or undefined if none selected
|
|
215
|
+
*/
|
|
216
|
+
get selectedOption() {
|
|
217
|
+
return this.selectionController.getSelectedOption();
|
|
93
218
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Selects an option programmatically
|
|
221
|
+
* @param option - The option to select
|
|
222
|
+
*/
|
|
223
|
+
selectOption(option) {
|
|
224
|
+
this.selectionController.selectOption(option);
|
|
98
225
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
226
|
+
/**
|
|
227
|
+
* Unselects an option programmatically
|
|
228
|
+
* @param option - The option to unselect
|
|
229
|
+
*/
|
|
230
|
+
unselectOption(option) {
|
|
231
|
+
this.selectionController.unselectOption(option);
|
|
103
232
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
233
|
+
/**
|
|
234
|
+
* Clears all current selections
|
|
235
|
+
*/
|
|
236
|
+
clearSelection() {
|
|
237
|
+
this.selectionController.clearSelection();
|
|
107
238
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Checks if a specific option is currently selected
|
|
241
|
+
* @param option - The option to check
|
|
242
|
+
* @returns True if the option is selected
|
|
243
|
+
*/
|
|
244
|
+
isOptionSelected(option) {
|
|
245
|
+
return this.selectionController.isOptionSelected(option);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Toggles the dropdown visibility
|
|
249
|
+
*/
|
|
250
|
+
toggleDropdown() {
|
|
251
|
+
this.dropdownController.toggle();
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Opens the dropdown programmatically
|
|
255
|
+
*/
|
|
256
|
+
openDropdown() {
|
|
257
|
+
this.dropdownController.open();
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Closes the dropdown programmatically
|
|
261
|
+
*/
|
|
262
|
+
closeDropdown() {
|
|
263
|
+
this.dropdownController.close();
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Focuses the select component
|
|
267
|
+
*/
|
|
268
|
+
focus() {
|
|
269
|
+
this.focusController.focus();
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Removes focus from the select component
|
|
273
|
+
*/
|
|
274
|
+
blur() {
|
|
275
|
+
this.focusController.blur();
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Validates the current selection according to component rules
|
|
279
|
+
* @returns True if valid, false otherwise
|
|
280
|
+
*/
|
|
281
|
+
validate() {
|
|
282
|
+
return this.validationController.validate();
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Checks if the current selection is valid without showing validation UI
|
|
286
|
+
* @returns True if valid, false otherwise
|
|
287
|
+
*/
|
|
288
|
+
checkValidity() {
|
|
289
|
+
return this.validationController.checkValidity();
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Reports the current validation state and shows validation UI if invalid
|
|
293
|
+
* @returns True if valid, false otherwise
|
|
294
|
+
*/
|
|
295
|
+
reportValidity() {
|
|
296
|
+
return this.validationController.reportValidity();
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Sets a custom validation message
|
|
300
|
+
* @param message - Custom validation message (empty string to clear)
|
|
301
|
+
*/
|
|
302
|
+
setCustomValidity(message) {
|
|
303
|
+
this.validationController.setCustomValidity(message);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Searches for options with the given query
|
|
307
|
+
* @param query - Search query string
|
|
308
|
+
*/
|
|
309
|
+
searchOptions(query) {
|
|
310
|
+
this.searchController.search(query);
|
|
111
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* Clears the current search query
|
|
314
|
+
*/
|
|
315
|
+
clearSearch() {
|
|
316
|
+
this.searchController.clearSearch();
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Gets the filtered options based on current search
|
|
320
|
+
* @returns Array of filtered options
|
|
321
|
+
*/
|
|
322
|
+
getSearchFilteredOptions() {
|
|
323
|
+
return this.searchController.getFilteredOptions(this.options);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Gets the current search query
|
|
327
|
+
* @returns Current search query string
|
|
328
|
+
*/
|
|
329
|
+
getCurrentSearchQuery() {
|
|
330
|
+
return this.searchController.searchQuery;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Manually trigger setup of global event listeners
|
|
334
|
+
*/
|
|
335
|
+
setupGlobalEventListeners() {
|
|
336
|
+
this.eventController.setupEventListeners();
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Manually trigger removal of global event listeners
|
|
340
|
+
*/
|
|
341
|
+
removeGlobalEventListeners() {
|
|
342
|
+
this.eventController.removeEventListeners();
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Filters options based on search query
|
|
346
|
+
*/
|
|
347
|
+
getFilteredOptions() {
|
|
348
|
+
return this.searchController.getFilteredOptions(this.options);
|
|
349
|
+
}
|
|
350
|
+
;
|
|
351
|
+
/**
|
|
352
|
+
* Sets up global event listeners (called when dropdown opens)
|
|
353
|
+
*/
|
|
354
|
+
setupEventListeners() {
|
|
355
|
+
this.eventController.setupEventListeners();
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Removes global event listeners (called on disconnect or dropdown close)
|
|
359
|
+
*/
|
|
360
|
+
removeEventListeners() {
|
|
361
|
+
this.eventController.removeEventListeners();
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Main render method that delegates to specific type renderers
|
|
365
|
+
*/
|
|
112
366
|
render() {
|
|
367
|
+
return html `${choose(this.type, [
|
|
368
|
+
[SelectType.Default, () => this.renderDefault()],
|
|
369
|
+
[SelectType.Inline, () => this.renderInline()],
|
|
370
|
+
[SelectType.Button, () => this.renderButton()],
|
|
371
|
+
[SelectType.Slot, () => this.renderSlot()],
|
|
372
|
+
])}`;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Renders the default select appearance with full features
|
|
376
|
+
*/
|
|
377
|
+
renderDefault() {
|
|
378
|
+
const selectedOptions = this.selectedOptions;
|
|
379
|
+
const validationClasses = this.validationController.getValidationClasses();
|
|
113
380
|
return html `
|
|
114
381
|
<slot name="label"></slot>
|
|
115
|
-
<div
|
|
382
|
+
<div
|
|
383
|
+
class="${classMap(Object.assign({ 'wrapper': true }, validationClasses))}"
|
|
384
|
+
tabindex="0"
|
|
385
|
+
role="combobox"
|
|
386
|
+
aria-expanded="${this.show}"
|
|
387
|
+
aria-haspopup="listbox"
|
|
388
|
+
aria-labelledby="select-label"
|
|
389
|
+
|
|
390
|
+
@click=${this.handleTriggerClick}
|
|
391
|
+
@keydown=${this.handleKeyDown}
|
|
392
|
+
@focus=${this.handleFocus}
|
|
393
|
+
@blur=${this.handleBlur}
|
|
394
|
+
>
|
|
116
395
|
<div class="select">
|
|
117
396
|
<div class="select-trigger">
|
|
118
|
-
${
|
|
119
|
-
[
|
|
120
|
-
OptionSelectionMode.Single,
|
|
121
|
-
() => html `${this.selected.length ? this.selected[0].label : this.placeholder}`,
|
|
122
|
-
],
|
|
123
|
-
[
|
|
124
|
-
OptionSelectionMode.Multiple,
|
|
125
|
-
() => html `${this.selected.length
|
|
126
|
-
? map(this.selected, (option, index) => html `<span class="label">
|
|
127
|
-
<hy-icon
|
|
128
|
-
name="remove"
|
|
129
|
-
id="unselect-one"
|
|
130
|
-
@click=${(e) => this.unselectOne(e, index)}
|
|
131
|
-
></hy-icon
|
|
132
|
-
>${option.label}</span
|
|
133
|
-
>${this.selected.length - 1 != index ? MULTIPLE_OPTIONS_SEPARATOR : EMPTY_STRING}`)
|
|
134
|
-
: this.placeholder}`,
|
|
135
|
-
],
|
|
136
|
-
])}
|
|
397
|
+
${this.renderSelectedContent(selectedOptions)}
|
|
137
398
|
</div>
|
|
399
|
+
|
|
138
400
|
<div class="icons-container">
|
|
139
|
-
${
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
name="remove"
|
|
147
|
-
id="unselect-multiple"
|
|
148
|
-
@click=${(e) => this.unselectAll(e)}
|
|
149
|
-
></hy-icon>`
|
|
150
|
-
: nothing}
|
|
151
|
-
<hy-icon name="angle-down" id="arrow-icon"></hy-icon>
|
|
401
|
+
${this.renderStatusIcon()}
|
|
402
|
+
${this.renderClearButton(selectedOptions)}
|
|
403
|
+
<hy-icon
|
|
404
|
+
name="angle-down"
|
|
405
|
+
class="arrow-icon"
|
|
406
|
+
aria-hidden="true"
|
|
407
|
+
></hy-icon>
|
|
152
408
|
</div>
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
</div>`;
|
|
162
|
-
})}
|
|
409
|
+
|
|
410
|
+
<div
|
|
411
|
+
class="options"
|
|
412
|
+
role="listbox"
|
|
413
|
+
aria-multiselectable="${this.multiple}"
|
|
414
|
+
>
|
|
415
|
+
${this.searchable ? this.renderSearchInput() : nothing}
|
|
416
|
+
${this.renderSelectOptions()}
|
|
163
417
|
</div>
|
|
164
418
|
</div>
|
|
165
419
|
</div>
|
|
420
|
+
|
|
421
|
+
${this.renderValidationMessage()}
|
|
166
422
|
<slot name="helper-text"></slot>
|
|
167
423
|
`;
|
|
168
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Renders inline select with integrated label and helper text
|
|
427
|
+
*/
|
|
428
|
+
renderInline() {
|
|
429
|
+
return html `
|
|
430
|
+
<slot name="label"></slot>
|
|
431
|
+
${this.renderDefault()}
|
|
432
|
+
<slot name="helper-text"></slot>
|
|
433
|
+
`;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Renders select as a button-style component
|
|
437
|
+
*/
|
|
438
|
+
renderButton() {
|
|
439
|
+
const selectedOptions = this.selectedOptions;
|
|
440
|
+
return html `
|
|
441
|
+
<button
|
|
442
|
+
class="select-button"
|
|
443
|
+
?disabled=${this.disabled}
|
|
444
|
+
@click=${this.handleTriggerClick}
|
|
445
|
+
@keydown=${this.handleKeyDown}
|
|
446
|
+
>
|
|
447
|
+
${selectedOptions.length > 0 ? selectedOptions[0].label : this.placeholder}
|
|
448
|
+
<hy-icon name="angle-down" class="arrow-icon"></hy-icon>
|
|
449
|
+
</button>
|
|
450
|
+
|
|
451
|
+
<div class="options" role="listbox">
|
|
452
|
+
${this.searchable ? this.renderSearchInput() : nothing}
|
|
453
|
+
${this.renderSelectOptions()}
|
|
454
|
+
</div>
|
|
455
|
+
`;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Renders select with custom trigger content via slots
|
|
459
|
+
*/
|
|
460
|
+
renderSlot() {
|
|
461
|
+
return html `
|
|
462
|
+
<slot name="trigger" @click=${this.handleTriggerClick}></slot>
|
|
463
|
+
<div class="options" role="listbox">
|
|
464
|
+
${this.searchable ? this.renderSearchInput() : nothing}
|
|
465
|
+
${this.renderSelectOptions()}
|
|
466
|
+
</div>
|
|
467
|
+
`;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Renders the selected content in the trigger area
|
|
471
|
+
*/
|
|
472
|
+
renderSelectedContent(selectedOptions) {
|
|
473
|
+
if (selectedOptions.length === 0) {
|
|
474
|
+
return html `<span class="placeholder" aria-hidden="true">${this.placeholder}</span>`;
|
|
475
|
+
}
|
|
476
|
+
if (this.multiple) {
|
|
477
|
+
return map(selectedOptions, (option) => html `
|
|
478
|
+
<span class="tag">
|
|
479
|
+
<span class="tag-label">${option.label}</span>
|
|
480
|
+
<hy-icon
|
|
481
|
+
name="remove"
|
|
482
|
+
class="tag-close"
|
|
483
|
+
@click=${(e) => this.handleTagRemove(e, option)}
|
|
484
|
+
aria-label="Remove ${option.label}"
|
|
485
|
+
></hy-icon>
|
|
486
|
+
</span>
|
|
487
|
+
`);
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
return html `${selectedOptions[0].label}`;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Renders status/validation icons based on current status
|
|
495
|
+
*/
|
|
496
|
+
renderStatusIcon() {
|
|
497
|
+
switch (this.status) {
|
|
498
|
+
case SelectStatus.Warning:
|
|
499
|
+
return html `<hy-icon name="warning" class="status-icon warning"></hy-icon>`;
|
|
500
|
+
case SelectStatus.Error:
|
|
501
|
+
return html `<hy-icon name="exclamation-circle" class="status-icon error"></hy-icon>`;
|
|
502
|
+
case SelectStatus.Success:
|
|
503
|
+
return html `<hy-icon name="check-circle" class="status-icon success"></hy-icon>`;
|
|
504
|
+
default:
|
|
505
|
+
return nothing;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Renders the clear all selections button when applicable
|
|
510
|
+
*/
|
|
511
|
+
renderClearButton(selectedOptions) {
|
|
512
|
+
if (selectedOptions.length === 0 || this.disabled) {
|
|
513
|
+
return nothing;
|
|
514
|
+
}
|
|
515
|
+
return html `
|
|
516
|
+
<hy-icon
|
|
517
|
+
name="remove"
|
|
518
|
+
class="clear-icon"
|
|
519
|
+
@click=${this.handleClearAll}
|
|
520
|
+
aria-label="Clear selection"
|
|
521
|
+
tabindex="-1"
|
|
522
|
+
></hy-icon>
|
|
523
|
+
`;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Renders all available options in the dropdown
|
|
527
|
+
*/
|
|
528
|
+
renderSelectOptions() {
|
|
529
|
+
const filteredOptions = this.getFilteredOptions();
|
|
530
|
+
// Show "no options" message when no options are available (original array empty)
|
|
531
|
+
if (!this.options || this.options.length === 0) {
|
|
532
|
+
return html `
|
|
533
|
+
<div class="no-options" role="option" aria-disabled="true">
|
|
534
|
+
<div class="no-options-content">
|
|
535
|
+
<hy-icon
|
|
536
|
+
name="${this.noOptionsIcon}"
|
|
537
|
+
class="no-options-icon"
|
|
538
|
+
aria-hidden="true">
|
|
539
|
+
</hy-icon>
|
|
540
|
+
<span class="no-options-text">${this.noOptionsMessage}</span>
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
`;
|
|
544
|
+
}
|
|
545
|
+
// Show "no results" message when search returns no results
|
|
546
|
+
if (this.searchController.hasNoResults(this.options)) {
|
|
547
|
+
return this.searchController.renderNoResults();
|
|
548
|
+
}
|
|
549
|
+
// Cache the focused option to avoid multiple controller accesses
|
|
550
|
+
const focusedOption = this.keyboardController.focusedOption;
|
|
551
|
+
return map(filteredOptions, (option) => {
|
|
552
|
+
const isSelected = this.isOptionSelected(option);
|
|
553
|
+
const isFocused = focusedOption && focusedOption.value === option.value;
|
|
554
|
+
return html `
|
|
555
|
+
<div
|
|
556
|
+
class="${classMap({
|
|
557
|
+
'option': true,
|
|
558
|
+
'selected': isSelected,
|
|
559
|
+
'focused': isFocused,
|
|
560
|
+
'disabled': Boolean(option.disabled)
|
|
561
|
+
})}"
|
|
562
|
+
role="option"
|
|
563
|
+
aria-selected="${isSelected}"
|
|
564
|
+
aria-disabled="${Boolean(option.disabled)}"
|
|
565
|
+
data-value="${option.value}"
|
|
566
|
+
@click=${(e) => this.handleOptionClick(e, option)}
|
|
567
|
+
style=${styleMap(option.style ? { style: option.style } : {})}
|
|
568
|
+
title="${option.title || ''}"
|
|
569
|
+
>
|
|
570
|
+
<div class="option-content">
|
|
571
|
+
${option.icon ? html `<hy-icon name="${option.icon}" class="option-icon"></hy-icon>` : nothing}
|
|
572
|
+
<div class="option-text">
|
|
573
|
+
${option.htmlContent ? html `<div .innerHTML=${option.htmlContent}></div>` : option.label}
|
|
574
|
+
${option.description ? html `<div class="option-description">${option.description}</div>` : nothing}
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
${isSelected ? html `<hy-icon name="check" class="check-icon" aria-hidden="true"></hy-icon>` : nothing}
|
|
579
|
+
|
|
580
|
+
${option.state && option.message ? html `
|
|
581
|
+
<div class="option-message ${option.state}">
|
|
582
|
+
<hy-icon name="${option.state === 'error' ? 'exclamation-circle' : 'warning'}"></hy-icon>
|
|
583
|
+
<span>${option.message}</span>
|
|
584
|
+
</div>
|
|
585
|
+
` : nothing}
|
|
586
|
+
</div>
|
|
587
|
+
`;
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Renders the search input when searchable is enabled
|
|
592
|
+
*/
|
|
593
|
+
renderSearchInput() {
|
|
594
|
+
return this.searchController.renderSearchInput();
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Renders validation message when present
|
|
598
|
+
*/
|
|
599
|
+
renderValidationMessage() {
|
|
600
|
+
const validationMessage = this.validationController.validationMessage;
|
|
601
|
+
if (!validationMessage)
|
|
602
|
+
return nothing;
|
|
603
|
+
return html `
|
|
604
|
+
<div class="validation-message ${this.status}" id="validation-message">
|
|
605
|
+
${validationMessage}
|
|
606
|
+
</div>
|
|
607
|
+
`;
|
|
608
|
+
}
|
|
169
609
|
};
|
|
170
610
|
HySelectComponent.styles = styles;
|
|
171
611
|
__decorate([
|
|
172
|
-
property()
|
|
612
|
+
property({ type: Array })
|
|
173
613
|
], HySelectComponent.prototype, "options", void 0);
|
|
174
614
|
__decorate([
|
|
175
|
-
property()
|
|
176
|
-
], HySelectComponent.prototype, "
|
|
615
|
+
property({ type: Array, attribute: 'default-value' })
|
|
616
|
+
], HySelectComponent.prototype, "defaultValue", void 0);
|
|
177
617
|
__decorate([
|
|
178
|
-
property()
|
|
618
|
+
property({ type: String })
|
|
179
619
|
], HySelectComponent.prototype, "placeholder", void 0);
|
|
180
620
|
__decorate([
|
|
181
621
|
property({ type: Boolean, reflect: true })
|
|
182
622
|
], HySelectComponent.prototype, "disabled", void 0);
|
|
183
623
|
__decorate([
|
|
184
|
-
property({ reflect: true })
|
|
624
|
+
property({ type: String, reflect: true })
|
|
185
625
|
], HySelectComponent.prototype, "type", void 0);
|
|
186
626
|
__decorate([
|
|
187
|
-
property()
|
|
188
|
-
], HySelectComponent.prototype, "
|
|
627
|
+
property({ type: Boolean, attribute: 'multiple' })
|
|
628
|
+
], HySelectComponent.prototype, "multiple", void 0);
|
|
189
629
|
__decorate([
|
|
190
630
|
property({ type: Boolean, reflect: true })
|
|
191
631
|
], HySelectComponent.prototype, "show", void 0);
|
|
192
632
|
__decorate([
|
|
193
|
-
property({ reflect: true })
|
|
633
|
+
property({ type: String, reflect: true })
|
|
194
634
|
], HySelectComponent.prototype, "status", void 0);
|
|
195
635
|
__decorate([
|
|
196
|
-
property({ reflect: true })
|
|
636
|
+
property({ type: String, reflect: true })
|
|
197
637
|
], HySelectComponent.prototype, "size", void 0);
|
|
198
638
|
__decorate([
|
|
199
|
-
|
|
200
|
-
], HySelectComponent.prototype, "
|
|
639
|
+
property({ type: Boolean, reflect: true })
|
|
640
|
+
], HySelectComponent.prototype, "required", void 0);
|
|
641
|
+
__decorate([
|
|
642
|
+
property({ type: String })
|
|
643
|
+
], HySelectComponent.prototype, "name", void 0);
|
|
644
|
+
__decorate([
|
|
645
|
+
property({ type: String })
|
|
646
|
+
], HySelectComponent.prototype, "value", void 0);
|
|
647
|
+
__decorate([
|
|
648
|
+
property({ type: String, attribute: 'no-options-message' })
|
|
649
|
+
], HySelectComponent.prototype, "noOptionsMessage", void 0);
|
|
650
|
+
__decorate([
|
|
651
|
+
property({ type: String, attribute: 'no-options-icon' })
|
|
652
|
+
], HySelectComponent.prototype, "noOptionsIcon", void 0);
|
|
653
|
+
__decorate([
|
|
654
|
+
property({ type: Boolean, reflect: true })
|
|
655
|
+
], HySelectComponent.prototype, "searchable", void 0);
|
|
656
|
+
__decorate([
|
|
657
|
+
property({ type: String, attribute: 'search-placeholder' })
|
|
658
|
+
], HySelectComponent.prototype, "searchPlaceholder", void 0);
|
|
659
|
+
__decorate([
|
|
660
|
+
property({ type: String })
|
|
661
|
+
], HySelectComponent.prototype, "searchQuery", void 0);
|
|
201
662
|
__decorate([
|
|
202
663
|
query('.options')
|
|
203
664
|
], HySelectComponent.prototype, "optionsElement", void 0);
|
|
204
665
|
__decorate([
|
|
205
666
|
query('.wrapper')
|
|
206
667
|
], HySelectComponent.prototype, "wrapper", void 0);
|
|
668
|
+
__decorate([
|
|
669
|
+
query('.search-input')
|
|
670
|
+
], HySelectComponent.prototype, "searchInput", void 0);
|
|
207
671
|
HySelectComponent = __decorate([
|
|
208
672
|
customElement('hy-select')
|
|
209
673
|
], HySelectComponent);
|