@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.
@@ -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, 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
+ // 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
- this.type = OptionType.Default;
32
- this.selectionMode = OptionSelectionMode.Single;
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
- 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
- }
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
- 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
- });
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
- 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`;
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
- this.optionsElement.style.top = `calc(100% + ${wrapperBorderBottomWidth}px)`;
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
- initOptionsPosition() {
76
- this.optionsElement.style.removeProperty('top');
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
- 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();
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
- unselectAll(unselectAllEvent) {
95
- unselectAllEvent.stopPropagation();
96
- this.selected = [];
97
- this.dispatchChangeEvent();
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
- unselectOne(unselectOneEvent, selectedIndex) {
100
- unselectOneEvent.stopPropagation();
101
- this.selected = this.selected.filter((_, index) => index != selectedIndex);
102
- this.dispatchChangeEvent();
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
- 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 }));
242
+ /**
243
+ * Clears all current selections
244
+ */
245
+ clearSelection() {
246
+ this.selectionController.clearSelection();
107
247
  }
108
- onBlur() {
109
- this.show = false;
110
- this.initOptionsPosition();
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 class="wrapper" tabindex="0" @click="${!this.disabled ? this.toggleOptions : nothing}" @blur=${this.onBlur}>
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
- ${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
- ])}
409
+ ${this.renderSelectedContent(selectedOptions)}
137
410
  </div>
411
+
138
412
  <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>
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
- <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
- })}
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, "defaultSelected", void 0);
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, "selectionMode", void 0);
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
- state()
200
- ], HySelectComponent.prototype, "selected", void 0);
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);