@smilodon/core 1.4.4 → 1.4.5

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/dist/index.js CHANGED
@@ -1392,6 +1392,14 @@ const defaultConfig = {
1392
1392
  expandLabel: 'Show more',
1393
1393
  collapseLabel: 'Show less',
1394
1394
  },
1395
+ clearControl: {
1396
+ enabled: false,
1397
+ clearSelection: true,
1398
+ clearSearch: true,
1399
+ hideWhenEmpty: true,
1400
+ ariaLabel: 'Clear selection and search',
1401
+ icon: '×',
1402
+ },
1395
1403
  callbacks: {},
1396
1404
  enabled: true,
1397
1405
  searchable: false,
@@ -1888,6 +1896,7 @@ class EnhancedSelect extends HTMLElement {
1888
1896
  this._inputContainer = this._createInputContainer();
1889
1897
  this._input = this._createInput();
1890
1898
  this._arrowContainer = this._createArrowContainer();
1899
+ this._clearControl = this._createClearControl();
1891
1900
  this._dropdown = this._createDropdown();
1892
1901
  this._optionsContainer = this._createOptionsContainer();
1893
1902
  this._liveRegion = this._createLiveRegion();
@@ -2101,8 +2110,25 @@ class EnhancedSelect extends HTMLElement {
2101
2110
  `;
2102
2111
  return container;
2103
2112
  }
2113
+ _createClearControl() {
2114
+ const button = document.createElement('button');
2115
+ button.type = 'button';
2116
+ button.className = 'clear-control-button';
2117
+ button.setAttribute('part', 'clear-button');
2118
+ const icon = document.createElement('span');
2119
+ icon.className = 'clear-control-icon';
2120
+ icon.setAttribute('part', 'clear-icon');
2121
+ icon.textContent = this._config.clearControl.icon || '×';
2122
+ button.setAttribute('aria-label', this._config.clearControl.ariaLabel || 'Clear selection and search');
2123
+ button.appendChild(icon);
2124
+ this._clearControlIcon = icon;
2125
+ return button;
2126
+ }
2104
2127
  _assembleDOM() {
2105
2128
  this._inputContainer.appendChild(this._input);
2129
+ if (this._clearControl) {
2130
+ this._inputContainer.appendChild(this._clearControl);
2131
+ }
2106
2132
  if (this._arrowContainer) {
2107
2133
  this._inputContainer.appendChild(this._arrowContainer);
2108
2134
  }
@@ -2118,6 +2144,7 @@ class EnhancedSelect extends HTMLElement {
2118
2144
  this._dropdown.id = listboxId;
2119
2145
  this._input.setAttribute('aria-controls', listboxId);
2120
2146
  this._input.setAttribute('aria-owns', listboxId);
2147
+ this._syncClearControlState();
2121
2148
  }
2122
2149
  _initializeStyles() {
2123
2150
  const style = document.createElement('style');
@@ -2191,6 +2218,58 @@ class EnhancedSelect extends HTMLElement {
2191
2218
  border-radius: var(--select-arrow-border-radius, 0 4px 4px 0);
2192
2219
  z-index: 2;
2193
2220
  }
2221
+
2222
+ .input-container.has-clear-control {
2223
+ padding: var(--select-input-padding-with-clear, 6px 84px 6px 8px);
2224
+ }
2225
+
2226
+ .input-container.has-clear-control::after {
2227
+ right: var(--select-separator-position-with-clear, 72px);
2228
+ }
2229
+
2230
+ .dropdown-arrow-container.with-clear-control {
2231
+ right: var(--select-arrow-right-with-clear, 32px);
2232
+ }
2233
+
2234
+ .clear-control-button {
2235
+ position: absolute;
2236
+ top: 50%;
2237
+ right: var(--select-clear-button-right, 6px);
2238
+ transform: translateY(-50%);
2239
+ width: var(--select-clear-button-size, 24px);
2240
+ height: var(--select-clear-button-size, 24px);
2241
+ border: var(--select-clear-button-border, none);
2242
+ border-radius: var(--select-clear-button-radius, 999px);
2243
+ background: var(--select-clear-button-bg, transparent);
2244
+ color: var(--select-clear-button-color, #6b7280);
2245
+ display: inline-flex;
2246
+ align-items: center;
2247
+ justify-content: center;
2248
+ cursor: pointer;
2249
+ z-index: 3;
2250
+ transition: var(--select-clear-button-transition, all 0.2s ease);
2251
+ }
2252
+
2253
+ .clear-control-button:hover {
2254
+ background: var(--select-clear-button-hover-bg, rgba(0, 0, 0, 0.06));
2255
+ color: var(--select-clear-button-hover-color, #111827);
2256
+ }
2257
+
2258
+ .clear-control-button:focus-visible {
2259
+ outline: var(--select-clear-button-focus-outline, 2px solid rgba(102, 126, 234, 0.55));
2260
+ outline-offset: 1px;
2261
+ }
2262
+
2263
+ .clear-control-button[hidden] {
2264
+ display: none;
2265
+ }
2266
+
2267
+ .clear-control-icon {
2268
+ font-size: var(--select-clear-icon-size, 16px);
2269
+ line-height: 1;
2270
+ font-weight: var(--select-clear-icon-weight, 500);
2271
+ pointer-events: none;
2272
+ }
2194
2273
 
2195
2274
  .dropdown-arrow-container:hover {
2196
2275
  background-color: var(--select-arrow-hover-bg, rgba(102, 126, 234, 0.08));
@@ -2348,16 +2427,31 @@ class EnhancedSelect extends HTMLElement {
2348
2427
  transform: var(--select-option-selected-hover-transform, var(--select-option-selected-transform, var(--select-option-transform, none)));
2349
2428
  }
2350
2429
 
2351
- .option.active {
2430
+ .option.active:not(.selected) {
2352
2431
  background-color: var(--select-option-active-bg, #f3f4f6);
2353
2432
  color: var(--select-option-active-color, #1f2937);
2354
2433
  outline: var(--select-option-active-outline, 2px solid rgba(99, 102, 241, 0.45));
2355
2434
  outline-offset: -2px;
2356
2435
  }
2357
2436
 
2358
- .option:active {
2437
+ .option.selected.active {
2438
+ background-color: var(--select-option-selected-active-bg, var(--select-option-selected-bg, #e0e7ff));
2439
+ color: var(--select-option-selected-active-color, var(--select-option-selected-color, #4338ca));
2440
+ border: var(--select-option-selected-active-border, var(--select-option-selected-border, var(--select-option-border, none)));
2441
+ border-bottom: var(--select-option-selected-active-border-bottom, var(--select-option-selected-border-bottom, var(--select-option-border-bottom, none)));
2442
+ box-shadow: var(--select-option-selected-active-shadow, var(--select-option-selected-shadow, var(--select-option-shadow, none)));
2443
+ transform: var(--select-option-selected-active-transform, var(--select-option-selected-transform, var(--select-option-transform, none)));
2444
+ outline: var(--select-option-selected-active-outline, var(--select-option-active-outline, 2px solid rgba(99, 102, 241, 0.45)));
2445
+ outline-offset: -2px;
2446
+ }
2447
+
2448
+ .option:active:not(.selected) {
2359
2449
  background-color: var(--select-option-pressed-bg, #e5e7eb);
2360
2450
  }
2451
+
2452
+ .option.selected:active {
2453
+ background-color: var(--select-option-selected-pressed-bg, var(--select-option-selected-hover-bg, var(--select-option-selected-bg, #e0e7ff)));
2454
+ }
2361
2455
 
2362
2456
  .load-more-container {
2363
2457
  padding: var(--select-load-more-padding, 12px);
@@ -2518,12 +2612,23 @@ class EnhancedSelect extends HTMLElement {
2518
2612
  transform: var(--select-dark-option-selected-hover-transform, var(--select-dark-option-selected-transform, var(--select-option-selected-hover-transform, var(--select-option-selected-transform, var(--select-option-transform, none)))));
2519
2613
  }
2520
2614
 
2521
- .option.active {
2615
+ .option.active:not(.selected) {
2522
2616
  background-color: var(--select-dark-option-active-bg, #374151);
2523
2617
  color: var(--select-dark-option-active-color, #f9fafb);
2524
2618
  outline: var(--select-dark-option-active-outline, 2px solid rgba(129, 140, 248, 0.55));
2525
2619
  }
2526
2620
 
2621
+ .option.selected.active {
2622
+ background-color: var(--select-dark-option-selected-active-bg, var(--select-dark-option-selected-bg, #3730a3));
2623
+ color: var(--select-dark-option-selected-active-color, var(--select-dark-option-selected-text, #e0e7ff));
2624
+ border: var(--select-dark-option-selected-active-border, var(--select-dark-option-selected-border, var(--select-option-selected-active-border, var(--select-option-selected-border, var(--select-option-border, none)))));
2625
+ border-bottom: var(--select-dark-option-selected-active-border-bottom, var(--select-dark-option-selected-border-bottom, var(--select-option-selected-active-border-bottom, var(--select-option-selected-border-bottom, var(--select-option-border-bottom, none)))));
2626
+ box-shadow: var(--select-dark-option-selected-active-shadow, var(--select-dark-option-selected-shadow, var(--select-option-selected-active-shadow, var(--select-option-selected-shadow, var(--select-option-shadow, none)))));
2627
+ transform: var(--select-dark-option-selected-active-transform, var(--select-dark-option-selected-transform, var(--select-option-selected-active-transform, var(--select-option-selected-transform, var(--select-option-transform, none)))));
2628
+ outline: var(--select-dark-option-selected-active-outline, var(--select-dark-option-active-outline, var(--select-option-selected-active-outline, var(--select-option-active-outline, 2px solid rgba(129, 140, 248, 0.55)))));
2629
+ outline-offset: -2px;
2630
+ }
2631
+
2527
2632
  .selection-badge {
2528
2633
  background: var(--select-dark-badge-bg, #4f46e5);
2529
2634
  color: var(--select-dark-badge-color, #eef2ff);
@@ -2608,6 +2713,17 @@ class EnhancedSelect extends HTMLElement {
2608
2713
  };
2609
2714
  this._arrowContainer.addEventListener('click', this._boundArrowClick);
2610
2715
  }
2716
+ if (this._clearControl) {
2717
+ this._clearControl.addEventListener('pointerdown', (e) => {
2718
+ e.stopPropagation();
2719
+ e.preventDefault();
2720
+ });
2721
+ this._clearControl.addEventListener('click', (e) => {
2722
+ e.stopPropagation();
2723
+ e.preventDefault();
2724
+ this._handleClearControlClick();
2725
+ });
2726
+ }
2611
2727
  // Input container click - focus input and open dropdown
2612
2728
  this._inputContainer.addEventListener('pointerdown', (e) => {
2613
2729
  const target = e.target;
@@ -2615,6 +2731,8 @@ class EnhancedSelect extends HTMLElement {
2615
2731
  return;
2616
2732
  if (target && target.closest('.dropdown-arrow-container'))
2617
2733
  return;
2734
+ if (target && target.closest('.clear-control-button'))
2735
+ return;
2618
2736
  if (!this._state.isOpen) {
2619
2737
  this._handleOpen();
2620
2738
  }
@@ -2827,6 +2945,7 @@ class EnhancedSelect extends HTMLElement {
2827
2945
  }
2828
2946
  _handleSearch(query) {
2829
2947
  this._state.searchQuery = query;
2948
+ this._syncClearControlState();
2830
2949
  if (query.length > 0) {
2831
2950
  this._perfMark('smilodon-search-input-last');
2832
2951
  this._pendingSearchRenderMark = true;
@@ -3004,7 +3123,9 @@ class EnhancedSelect extends HTMLElement {
3004
3123
  option.classList.add('smilodon-option--active');
3005
3124
  option.setAttribute('aria-selected', 'true');
3006
3125
  }
3007
- option.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
3126
+ if (typeof option.scrollIntoView === 'function') {
3127
+ option.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
3128
+ }
3008
3129
  // Announce position for screen readers
3009
3130
  const total = options.length;
3010
3131
  this._announce(`Item ${index + 1} of ${total}`);
@@ -3259,6 +3380,7 @@ class EnhancedSelect extends HTMLElement {
3259
3380
  this._inputContainer.insertBefore(badge, this._input);
3260
3381
  });
3261
3382
  }
3383
+ this._syncClearControlState();
3262
3384
  }
3263
3385
  _renderOptionsWithAnimation() {
3264
3386
  // Add fade-out animation
@@ -3507,6 +3629,21 @@ class EnhancedSelect extends HTMLElement {
3507
3629
  this._renderOptions();
3508
3630
  this._updateInputDisplay();
3509
3631
  this._emitChange();
3632
+ this._syncClearControlState();
3633
+ }
3634
+ /**
3635
+ * Clear search query and restore full option list
3636
+ */
3637
+ clearSearch() {
3638
+ this._state.searchQuery = '';
3639
+ if (this._config.searchable) {
3640
+ this._input.value = '';
3641
+ if (this._state.selectedIndices.size > 0) {
3642
+ this._updateInputDisplay();
3643
+ }
3644
+ }
3645
+ this._renderOptions();
3646
+ this._syncClearControlState();
3510
3647
  }
3511
3648
  /**
3512
3649
  * Open dropdown
@@ -3533,6 +3670,12 @@ class EnhancedSelect extends HTMLElement {
3533
3670
  this._input.placeholder = this._config.placeholder || 'Select an option...';
3534
3671
  }
3535
3672
  }
3673
+ if (this._clearControl) {
3674
+ this._clearControl.setAttribute('aria-label', this._config.clearControl.ariaLabel || 'Clear selection and search');
3675
+ }
3676
+ if (this._clearControlIcon) {
3677
+ this._clearControlIcon.textContent = this._config.clearControl.icon || '×';
3678
+ }
3536
3679
  if (this._dropdown) {
3537
3680
  if (this._config.selection.mode === 'multi') {
3538
3681
  this._dropdown.setAttribute('aria-multiselectable', 'true');
@@ -3543,8 +3686,47 @@ class EnhancedSelect extends HTMLElement {
3543
3686
  }
3544
3687
  // Re-initialize observers in case infinite scroll was enabled/disabled
3545
3688
  this._initializeObservers();
3689
+ this._syncClearControlState();
3546
3690
  this._renderOptions();
3547
3691
  }
3692
+ _handleClearControlClick() {
3693
+ const shouldClearSelection = this._config.clearControl.clearSelection !== false;
3694
+ const shouldClearSearch = this._config.clearControl.clearSearch !== false;
3695
+ const hadSelection = this._state.selectedIndices.size > 0;
3696
+ const hadSearch = this._state.searchQuery.length > 0;
3697
+ if (shouldClearSelection && hadSelection) {
3698
+ this.clear();
3699
+ }
3700
+ if (shouldClearSearch && hadSearch) {
3701
+ this.clearSearch();
3702
+ }
3703
+ this._emit('clear', {
3704
+ clearedSelection: shouldClearSelection && hadSelection,
3705
+ clearedSearch: shouldClearSearch && hadSearch,
3706
+ });
3707
+ this._config.callbacks.onClear?.({
3708
+ clearedSelection: shouldClearSelection && hadSelection,
3709
+ clearedSearch: shouldClearSearch && hadSearch,
3710
+ });
3711
+ this._input.focus();
3712
+ }
3713
+ _syncClearControlState() {
3714
+ if (!this._clearControl || !this._inputContainer || !this._arrowContainer)
3715
+ return;
3716
+ const enabled = this._config.clearControl.enabled === true;
3717
+ const hasSelection = this._state.selectedIndices.size > 0;
3718
+ const hasSearch = this._state.searchQuery.length > 0;
3719
+ const clearSelection = this._config.clearControl.clearSelection !== false;
3720
+ const clearSearch = this._config.clearControl.clearSearch !== false;
3721
+ const hasSomethingToClear = (clearSelection && hasSelection) || (clearSearch && hasSearch);
3722
+ const hideWhenEmpty = this._config.clearControl.hideWhenEmpty !== false;
3723
+ const visible = enabled && (!hideWhenEmpty || hasSomethingToClear);
3724
+ this._inputContainer.classList.toggle('has-clear-control', enabled);
3725
+ this._arrowContainer.classList.toggle('with-clear-control', enabled);
3726
+ this._clearControl.hidden = !visible;
3727
+ this._clearControl.disabled = !hasSomethingToClear;
3728
+ this._clearControl.setAttribute('aria-hidden', String(!visible));
3729
+ }
3548
3730
  /**
3549
3731
  * Set error state
3550
3732
  */
@@ -4832,10 +5014,17 @@ const shadowDOMStyles = `
4832
5014
  color: var(--ns-item-selected-color, #0066cc);
4833
5015
  }
4834
5016
 
4835
- .ns-item[data-active="true"] {
5017
+ .ns-item[data-active="true"]:not([aria-selected="true"]) {
4836
5018
  background: var(--ns-item-active-bg, rgba(0, 102, 204, 0.2));
4837
5019
  }
4838
5020
 
5021
+ .ns-item[aria-selected="true"][data-active="true"] {
5022
+ background: var(--ns-item-selected-active-bg, var(--ns-item-selected-bg, rgba(0, 102, 204, 0.1)));
5023
+ color: var(--ns-item-selected-active-color, var(--ns-item-selected-color, #0066cc));
5024
+ outline: var(--ns-item-selected-active-outline, 2px solid #0066cc);
5025
+ outline-offset: -2px;
5026
+ }
5027
+
4839
5028
  .ns-item[aria-disabled="true"] {
4840
5029
  opacity: 0.5;
4841
5030
  cursor: not-allowed;