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