@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.
@@ -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, state, customElement, query } from 'lit/decorators.js';
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 { EMPTY_STRING, MULTIPLE_OPTIONS_SEPARATOR } from './select.constant.js';
17
+ import { classMap } from 'lit/directives/class-map.js';
24
18
  import { styleMap } from 'lit/directives/style-map.js';
25
- let HySelectComponent = class HySelectComponent extends LitElement {
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.defaultSelected = [];
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
- this.type = OptionType.Default;
32
- this.selectionMode = OptionSelectionMode.Single;
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
- this.status = OptionStatus.Default;
35
- this.size = OptionSize.Medium;
36
- this.selected = [];
37
- }
38
- updated(_changedProperties) {
39
- if (_changedProperties.has('defaultSelected') && JSON.stringify(_changedProperties.get('defaultSelected')) != JSON.stringify(this.defaultSelected)) {
40
- let defaultOptions = [];
41
- this.defaultSelected.forEach((value) => {
42
- const option = this.options.find((option) => option.value == value);
43
- if (option)
44
- defaultOptions.push(option);
45
- });
46
- this.selected = [...defaultOptions];
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
- toggleOptions() {
50
- return __awaiter(this, void 0, void 0, function* () {
51
- this.show = !this.show;
52
- yield this.updateComplete;
53
- if (this.show)
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
- calculateOptionsPosition() {
60
- const wrapperBorderBottomWidth = +getComputedStyle(this.wrapper).borderBottomWidth.split('px')[0];
61
- const wrapperBorderTopWidth = +getComputedStyle(this.wrapper).borderTopWidth.split('px')[0];
62
- const clientRect = this.optionsElement.getBoundingClientRect();
63
- const availableBottomSpace = window.visualViewport.height -
64
- clientRect.bottom +
65
- clientRect.height -
66
- this.wrapper.getBoundingClientRect().height;
67
- const availableTopSpace = clientRect.top;
68
- if (clientRect.height > availableBottomSpace && availableTopSpace > clientRect.height) {
69
- this.optionsElement.style.top = `${-clientRect.height - wrapperBorderTopWidth}px`;
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
- this.optionsElement.style.top = `calc(100% + ${wrapperBorderBottomWidth}px)`;
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
- initOptionsPosition() {
76
- this.optionsElement.style.removeProperty('top');
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
- selectOption(selectOptionEvent, selectedOption) {
79
- selectOptionEvent.stopPropagation();
80
- if (this.selectionMode == OptionSelectionMode.Single) {
81
- this.selected = this.selected.length && this.selected[0].label == selectedOption.label ? [] : [selectedOption];
82
- this.toggleOptions();
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
- unselectAll(unselectAllEvent) {
95
- unselectAllEvent.stopPropagation();
96
- this.selected = [];
97
- this.dispatchChangeEvent();
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
- unselectOne(unselectOneEvent, selectedIndex) {
100
- unselectOneEvent.stopPropagation();
101
- this.selected = this.selected.filter((_, index) => index != selectedIndex);
102
- this.dispatchChangeEvent();
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
- dispatchChangeEvent() {
105
- let result = this.selectionMode == OptionSelectionMode.Single ? this.selected[0] : this.selected;
106
- this.dispatchEvent(new CustomEvent('changed', { detail: { value: result }, bubbles: true, composed: true }));
233
+ /**
234
+ * Clears all current selections
235
+ */
236
+ clearSelection() {
237
+ this.selectionController.clearSelection();
107
238
  }
108
- onBlur() {
109
- this.show = false;
110
- this.initOptionsPosition();
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 class="wrapper" tabindex="0" @click="${!this.disabled ? this.toggleOptions : nothing}" @blur=${this.onBlur}>
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
- ${choose(this.selectionMode, [
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
- ${choose(this.status, [
140
- [OptionStatus.Default, () => undefined],
141
- [OptionStatus.Warning, () => html `<hy-icon name="warning" id="warning-icon"></hy-icon>`],
142
- [OptionStatus.Error, () => html `<hy-icon name="exclamation-circle" id="error-icon"></hy-icon>`],
143
- ])}
144
- ${this.selected.length
145
- ? html `<hy-icon
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
- <div class="options">
154
- ${map(this.options, (option) => {
155
- var _a;
156
- return html `<div class="option" @click="${(e) => this.selectOption(e, option)}">
157
- ${this.selected.includes(option) ? html `<hy-icon name="check" id="check-icon"></hy-icon>` : nothing}
158
- <span class="option-text"
159
- style=${styleMap(Object.assign({}, (_a = option.additionalStyle) !== null && _a !== void 0 ? _a : []))}
160
- >${option.label}</span>
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, "defaultSelected", void 0);
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, "selectionMode", void 0);
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
- state()
200
- ], HySelectComponent.prototype, "selected", void 0);
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);