@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.umd.js CHANGED
@@ -1398,6 +1398,14 @@
1398
1398
  expandLabel: 'Show more',
1399
1399
  collapseLabel: 'Show less',
1400
1400
  },
1401
+ clearControl: {
1402
+ enabled: false,
1403
+ clearSelection: true,
1404
+ clearSearch: true,
1405
+ hideWhenEmpty: true,
1406
+ ariaLabel: 'Clear selection and search',
1407
+ icon: '×',
1408
+ },
1401
1409
  callbacks: {},
1402
1410
  enabled: true,
1403
1411
  searchable: false,
@@ -1894,6 +1902,7 @@
1894
1902
  this._inputContainer = this._createInputContainer();
1895
1903
  this._input = this._createInput();
1896
1904
  this._arrowContainer = this._createArrowContainer();
1905
+ this._clearControl = this._createClearControl();
1897
1906
  this._dropdown = this._createDropdown();
1898
1907
  this._optionsContainer = this._createOptionsContainer();
1899
1908
  this._liveRegion = this._createLiveRegion();
@@ -2107,8 +2116,25 @@
2107
2116
  `;
2108
2117
  return container;
2109
2118
  }
2119
+ _createClearControl() {
2120
+ const button = document.createElement('button');
2121
+ button.type = 'button';
2122
+ button.className = 'clear-control-button';
2123
+ button.setAttribute('part', 'clear-button');
2124
+ const icon = document.createElement('span');
2125
+ icon.className = 'clear-control-icon';
2126
+ icon.setAttribute('part', 'clear-icon');
2127
+ icon.textContent = this._config.clearControl.icon || '×';
2128
+ button.setAttribute('aria-label', this._config.clearControl.ariaLabel || 'Clear selection and search');
2129
+ button.appendChild(icon);
2130
+ this._clearControlIcon = icon;
2131
+ return button;
2132
+ }
2110
2133
  _assembleDOM() {
2111
2134
  this._inputContainer.appendChild(this._input);
2135
+ if (this._clearControl) {
2136
+ this._inputContainer.appendChild(this._clearControl);
2137
+ }
2112
2138
  if (this._arrowContainer) {
2113
2139
  this._inputContainer.appendChild(this._arrowContainer);
2114
2140
  }
@@ -2124,6 +2150,7 @@
2124
2150
  this._dropdown.id = listboxId;
2125
2151
  this._input.setAttribute('aria-controls', listboxId);
2126
2152
  this._input.setAttribute('aria-owns', listboxId);
2153
+ this._syncClearControlState();
2127
2154
  }
2128
2155
  _initializeStyles() {
2129
2156
  const style = document.createElement('style');
@@ -2197,6 +2224,58 @@
2197
2224
  border-radius: var(--select-arrow-border-radius, 0 4px 4px 0);
2198
2225
  z-index: 2;
2199
2226
  }
2227
+
2228
+ .input-container.has-clear-control {
2229
+ padding: var(--select-input-padding-with-clear, 6px 84px 6px 8px);
2230
+ }
2231
+
2232
+ .input-container.has-clear-control::after {
2233
+ right: var(--select-separator-position-with-clear, 72px);
2234
+ }
2235
+
2236
+ .dropdown-arrow-container.with-clear-control {
2237
+ right: var(--select-arrow-right-with-clear, 32px);
2238
+ }
2239
+
2240
+ .clear-control-button {
2241
+ position: absolute;
2242
+ top: 50%;
2243
+ right: var(--select-clear-button-right, 6px);
2244
+ transform: translateY(-50%);
2245
+ width: var(--select-clear-button-size, 24px);
2246
+ height: var(--select-clear-button-size, 24px);
2247
+ border: var(--select-clear-button-border, none);
2248
+ border-radius: var(--select-clear-button-radius, 999px);
2249
+ background: var(--select-clear-button-bg, transparent);
2250
+ color: var(--select-clear-button-color, #6b7280);
2251
+ display: inline-flex;
2252
+ align-items: center;
2253
+ justify-content: center;
2254
+ cursor: pointer;
2255
+ z-index: 3;
2256
+ transition: var(--select-clear-button-transition, all 0.2s ease);
2257
+ }
2258
+
2259
+ .clear-control-button:hover {
2260
+ background: var(--select-clear-button-hover-bg, rgba(0, 0, 0, 0.06));
2261
+ color: var(--select-clear-button-hover-color, #111827);
2262
+ }
2263
+
2264
+ .clear-control-button:focus-visible {
2265
+ outline: var(--select-clear-button-focus-outline, 2px solid rgba(102, 126, 234, 0.55));
2266
+ outline-offset: 1px;
2267
+ }
2268
+
2269
+ .clear-control-button[hidden] {
2270
+ display: none;
2271
+ }
2272
+
2273
+ .clear-control-icon {
2274
+ font-size: var(--select-clear-icon-size, 16px);
2275
+ line-height: 1;
2276
+ font-weight: var(--select-clear-icon-weight, 500);
2277
+ pointer-events: none;
2278
+ }
2200
2279
 
2201
2280
  .dropdown-arrow-container:hover {
2202
2281
  background-color: var(--select-arrow-hover-bg, rgba(102, 126, 234, 0.08));
@@ -2354,16 +2433,31 @@
2354
2433
  transform: var(--select-option-selected-hover-transform, var(--select-option-selected-transform, var(--select-option-transform, none)));
2355
2434
  }
2356
2435
 
2357
- .option.active {
2436
+ .option.active:not(.selected) {
2358
2437
  background-color: var(--select-option-active-bg, #f3f4f6);
2359
2438
  color: var(--select-option-active-color, #1f2937);
2360
2439
  outline: var(--select-option-active-outline, 2px solid rgba(99, 102, 241, 0.45));
2361
2440
  outline-offset: -2px;
2362
2441
  }
2363
2442
 
2364
- .option:active {
2443
+ .option.selected.active {
2444
+ background-color: var(--select-option-selected-active-bg, var(--select-option-selected-bg, #e0e7ff));
2445
+ color: var(--select-option-selected-active-color, var(--select-option-selected-color, #4338ca));
2446
+ border: var(--select-option-selected-active-border, var(--select-option-selected-border, var(--select-option-border, none)));
2447
+ border-bottom: var(--select-option-selected-active-border-bottom, var(--select-option-selected-border-bottom, var(--select-option-border-bottom, none)));
2448
+ box-shadow: var(--select-option-selected-active-shadow, var(--select-option-selected-shadow, var(--select-option-shadow, none)));
2449
+ transform: var(--select-option-selected-active-transform, var(--select-option-selected-transform, var(--select-option-transform, none)));
2450
+ outline: var(--select-option-selected-active-outline, var(--select-option-active-outline, 2px solid rgba(99, 102, 241, 0.45)));
2451
+ outline-offset: -2px;
2452
+ }
2453
+
2454
+ .option:active:not(.selected) {
2365
2455
  background-color: var(--select-option-pressed-bg, #e5e7eb);
2366
2456
  }
2457
+
2458
+ .option.selected:active {
2459
+ background-color: var(--select-option-selected-pressed-bg, var(--select-option-selected-hover-bg, var(--select-option-selected-bg, #e0e7ff)));
2460
+ }
2367
2461
 
2368
2462
  .load-more-container {
2369
2463
  padding: var(--select-load-more-padding, 12px);
@@ -2524,12 +2618,23 @@
2524
2618
  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)))));
2525
2619
  }
2526
2620
 
2527
- .option.active {
2621
+ .option.active:not(.selected) {
2528
2622
  background-color: var(--select-dark-option-active-bg, #374151);
2529
2623
  color: var(--select-dark-option-active-color, #f9fafb);
2530
2624
  outline: var(--select-dark-option-active-outline, 2px solid rgba(129, 140, 248, 0.55));
2531
2625
  }
2532
2626
 
2627
+ .option.selected.active {
2628
+ background-color: var(--select-dark-option-selected-active-bg, var(--select-dark-option-selected-bg, #3730a3));
2629
+ color: var(--select-dark-option-selected-active-color, var(--select-dark-option-selected-text, #e0e7ff));
2630
+ 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)))));
2631
+ 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)))));
2632
+ 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)))));
2633
+ 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)))));
2634
+ 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)))));
2635
+ outline-offset: -2px;
2636
+ }
2637
+
2533
2638
  .selection-badge {
2534
2639
  background: var(--select-dark-badge-bg, #4f46e5);
2535
2640
  color: var(--select-dark-badge-color, #eef2ff);
@@ -2614,6 +2719,17 @@
2614
2719
  };
2615
2720
  this._arrowContainer.addEventListener('click', this._boundArrowClick);
2616
2721
  }
2722
+ if (this._clearControl) {
2723
+ this._clearControl.addEventListener('pointerdown', (e) => {
2724
+ e.stopPropagation();
2725
+ e.preventDefault();
2726
+ });
2727
+ this._clearControl.addEventListener('click', (e) => {
2728
+ e.stopPropagation();
2729
+ e.preventDefault();
2730
+ this._handleClearControlClick();
2731
+ });
2732
+ }
2617
2733
  // Input container click - focus input and open dropdown
2618
2734
  this._inputContainer.addEventListener('pointerdown', (e) => {
2619
2735
  const target = e.target;
@@ -2621,6 +2737,8 @@
2621
2737
  return;
2622
2738
  if (target && target.closest('.dropdown-arrow-container'))
2623
2739
  return;
2740
+ if (target && target.closest('.clear-control-button'))
2741
+ return;
2624
2742
  if (!this._state.isOpen) {
2625
2743
  this._handleOpen();
2626
2744
  }
@@ -2833,6 +2951,7 @@
2833
2951
  }
2834
2952
  _handleSearch(query) {
2835
2953
  this._state.searchQuery = query;
2954
+ this._syncClearControlState();
2836
2955
  if (query.length > 0) {
2837
2956
  this._perfMark('smilodon-search-input-last');
2838
2957
  this._pendingSearchRenderMark = true;
@@ -3010,7 +3129,9 @@
3010
3129
  option.classList.add('smilodon-option--active');
3011
3130
  option.setAttribute('aria-selected', 'true');
3012
3131
  }
3013
- option.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
3132
+ if (typeof option.scrollIntoView === 'function') {
3133
+ option.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
3134
+ }
3014
3135
  // Announce position for screen readers
3015
3136
  const total = options.length;
3016
3137
  this._announce(`Item ${index + 1} of ${total}`);
@@ -3265,6 +3386,7 @@
3265
3386
  this._inputContainer.insertBefore(badge, this._input);
3266
3387
  });
3267
3388
  }
3389
+ this._syncClearControlState();
3268
3390
  }
3269
3391
  _renderOptionsWithAnimation() {
3270
3392
  // Add fade-out animation
@@ -3513,6 +3635,21 @@
3513
3635
  this._renderOptions();
3514
3636
  this._updateInputDisplay();
3515
3637
  this._emitChange();
3638
+ this._syncClearControlState();
3639
+ }
3640
+ /**
3641
+ * Clear search query and restore full option list
3642
+ */
3643
+ clearSearch() {
3644
+ this._state.searchQuery = '';
3645
+ if (this._config.searchable) {
3646
+ this._input.value = '';
3647
+ if (this._state.selectedIndices.size > 0) {
3648
+ this._updateInputDisplay();
3649
+ }
3650
+ }
3651
+ this._renderOptions();
3652
+ this._syncClearControlState();
3516
3653
  }
3517
3654
  /**
3518
3655
  * Open dropdown
@@ -3539,6 +3676,12 @@
3539
3676
  this._input.placeholder = this._config.placeholder || 'Select an option...';
3540
3677
  }
3541
3678
  }
3679
+ if (this._clearControl) {
3680
+ this._clearControl.setAttribute('aria-label', this._config.clearControl.ariaLabel || 'Clear selection and search');
3681
+ }
3682
+ if (this._clearControlIcon) {
3683
+ this._clearControlIcon.textContent = this._config.clearControl.icon || '×';
3684
+ }
3542
3685
  if (this._dropdown) {
3543
3686
  if (this._config.selection.mode === 'multi') {
3544
3687
  this._dropdown.setAttribute('aria-multiselectable', 'true');
@@ -3549,8 +3692,47 @@
3549
3692
  }
3550
3693
  // Re-initialize observers in case infinite scroll was enabled/disabled
3551
3694
  this._initializeObservers();
3695
+ this._syncClearControlState();
3552
3696
  this._renderOptions();
3553
3697
  }
3698
+ _handleClearControlClick() {
3699
+ const shouldClearSelection = this._config.clearControl.clearSelection !== false;
3700
+ const shouldClearSearch = this._config.clearControl.clearSearch !== false;
3701
+ const hadSelection = this._state.selectedIndices.size > 0;
3702
+ const hadSearch = this._state.searchQuery.length > 0;
3703
+ if (shouldClearSelection && hadSelection) {
3704
+ this.clear();
3705
+ }
3706
+ if (shouldClearSearch && hadSearch) {
3707
+ this.clearSearch();
3708
+ }
3709
+ this._emit('clear', {
3710
+ clearedSelection: shouldClearSelection && hadSelection,
3711
+ clearedSearch: shouldClearSearch && hadSearch,
3712
+ });
3713
+ this._config.callbacks.onClear?.({
3714
+ clearedSelection: shouldClearSelection && hadSelection,
3715
+ clearedSearch: shouldClearSearch && hadSearch,
3716
+ });
3717
+ this._input.focus();
3718
+ }
3719
+ _syncClearControlState() {
3720
+ if (!this._clearControl || !this._inputContainer || !this._arrowContainer)
3721
+ return;
3722
+ const enabled = this._config.clearControl.enabled === true;
3723
+ const hasSelection = this._state.selectedIndices.size > 0;
3724
+ const hasSearch = this._state.searchQuery.length > 0;
3725
+ const clearSelection = this._config.clearControl.clearSelection !== false;
3726
+ const clearSearch = this._config.clearControl.clearSearch !== false;
3727
+ const hasSomethingToClear = (clearSelection && hasSelection) || (clearSearch && hasSearch);
3728
+ const hideWhenEmpty = this._config.clearControl.hideWhenEmpty !== false;
3729
+ const visible = enabled && (!hideWhenEmpty || hasSomethingToClear);
3730
+ this._inputContainer.classList.toggle('has-clear-control', enabled);
3731
+ this._arrowContainer.classList.toggle('with-clear-control', enabled);
3732
+ this._clearControl.hidden = !visible;
3733
+ this._clearControl.disabled = !hasSomethingToClear;
3734
+ this._clearControl.setAttribute('aria-hidden', String(!visible));
3735
+ }
3554
3736
  /**
3555
3737
  * Set error state
3556
3738
  */
@@ -4838,10 +5020,17 @@
4838
5020
  color: var(--ns-item-selected-color, #0066cc);
4839
5021
  }
4840
5022
 
4841
- .ns-item[data-active="true"] {
5023
+ .ns-item[data-active="true"]:not([aria-selected="true"]) {
4842
5024
  background: var(--ns-item-active-bg, rgba(0, 102, 204, 0.2));
4843
5025
  }
4844
5026
 
5027
+ .ns-item[aria-selected="true"][data-active="true"] {
5028
+ background: var(--ns-item-selected-active-bg, var(--ns-item-selected-bg, rgba(0, 102, 204, 0.1)));
5029
+ color: var(--ns-item-selected-active-color, var(--ns-item-selected-color, #0066cc));
5030
+ outline: var(--ns-item-selected-active-outline, 2px solid #0066cc);
5031
+ outline-offset: -2px;
5032
+ }
5033
+
4845
5034
  .ns-item[aria-disabled="true"] {
4846
5035
  opacity: 0.5;
4847
5036
  cursor: not-allowed;