@smilodon/core 1.4.4 → 1.4.6

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/README.md CHANGED
@@ -136,6 +136,31 @@ See the [main documentation](https://github.com/navidrezadoost/smilodon#readme)
136
136
 
137
137
  ## API Reference
138
138
 
139
+ ### Clear Control (Selection/Search Reset)
140
+
141
+ ```javascript
142
+ const select = document.querySelector('enhanced-select');
143
+
144
+ select.updateConfig({
145
+ clearControl: {
146
+ enabled: true,
147
+ clearSelection: true,
148
+ clearSearch: true,
149
+ hideWhenEmpty: true,
150
+ ariaLabel: 'Clear selected and searched values',
151
+ icon: '✕',
152
+ }
153
+ });
154
+
155
+ select.addEventListener('clear', (e) => {
156
+ console.log(e.detail); // { clearedSelection: boolean, clearedSearch: boolean }
157
+ });
158
+ ```
159
+
160
+ Style hooks:
161
+ - Parts: `::part(clear-button)`, `::part(clear-icon)`
162
+ - Tokens: `--select-clear-button-*`, `--select-clear-icon-*`, `--select-input-padding-with-clear`
163
+
139
164
  ### Properties
140
165
 
141
166
  ```typescript
package/dist/index.cjs CHANGED
@@ -1394,6 +1394,14 @@ const defaultConfig = {
1394
1394
  expandLabel: 'Show more',
1395
1395
  collapseLabel: 'Show less',
1396
1396
  },
1397
+ clearControl: {
1398
+ enabled: false,
1399
+ clearSelection: true,
1400
+ clearSearch: true,
1401
+ hideWhenEmpty: true,
1402
+ ariaLabel: 'Clear selection and search',
1403
+ icon: '×',
1404
+ },
1397
1405
  callbacks: {},
1398
1406
  enabled: true,
1399
1407
  searchable: false,
@@ -1510,11 +1518,11 @@ class SelectOption extends HTMLElement {
1510
1518
  }
1511
1519
 
1512
1520
  .option-container:hover {
1513
- background-color: var(--select-option-hover-bg, #f0f0f0);
1521
+ background: var(--select-option-hover-bg, #f0f0f0);
1514
1522
  }
1515
1523
 
1516
1524
  .option-container.selected {
1517
- background-color: var(--select-option-selected-bg, #e3f2fd);
1525
+ background: var(--select-option-selected-bg, #e3f2fd);
1518
1526
  color: var(--select-option-selected-color, #1976d2);
1519
1527
  border: var(--select-option-selected-border, var(--select-option-border, none));
1520
1528
  border-bottom: var(--select-option-selected-border-bottom, var(--select-option-border-bottom, none));
@@ -1524,7 +1532,7 @@ class SelectOption extends HTMLElement {
1524
1532
  }
1525
1533
 
1526
1534
  .option-container.selected:hover {
1527
- background-color: var(--select-option-selected-hover-bg, var(--select-option-selected-bg, #e3f2fd));
1535
+ background: var(--select-option-selected-hover-bg, var(--select-option-selected-bg, #e3f2fd));
1528
1536
  color: var(--select-option-selected-hover-color, var(--select-option-selected-color, #1976d2));
1529
1537
  border: var(--select-option-selected-hover-border, var(--select-option-selected-border, var(--select-option-border, none)));
1530
1538
  border-bottom: var(--select-option-selected-hover-border-bottom, var(--select-option-selected-border-bottom, var(--select-option-border-bottom, none)));
@@ -1890,6 +1898,7 @@ class EnhancedSelect extends HTMLElement {
1890
1898
  this._inputContainer = this._createInputContainer();
1891
1899
  this._input = this._createInput();
1892
1900
  this._arrowContainer = this._createArrowContainer();
1901
+ this._clearControl = this._createClearControl();
1893
1902
  this._dropdown = this._createDropdown();
1894
1903
  this._optionsContainer = this._createOptionsContainer();
1895
1904
  this._liveRegion = this._createLiveRegion();
@@ -2103,8 +2112,25 @@ class EnhancedSelect extends HTMLElement {
2103
2112
  `;
2104
2113
  return container;
2105
2114
  }
2115
+ _createClearControl() {
2116
+ const button = document.createElement('button');
2117
+ button.type = 'button';
2118
+ button.className = 'clear-control-button';
2119
+ button.setAttribute('part', 'clear-button');
2120
+ const icon = document.createElement('span');
2121
+ icon.className = 'clear-control-icon';
2122
+ icon.setAttribute('part', 'clear-icon');
2123
+ icon.textContent = this._config.clearControl.icon || '×';
2124
+ button.setAttribute('aria-label', this._config.clearControl.ariaLabel || 'Clear selection and search');
2125
+ button.appendChild(icon);
2126
+ this._clearControlIcon = icon;
2127
+ return button;
2128
+ }
2106
2129
  _assembleDOM() {
2107
2130
  this._inputContainer.appendChild(this._input);
2131
+ if (this._clearControl) {
2132
+ this._inputContainer.appendChild(this._clearControl);
2133
+ }
2108
2134
  if (this._arrowContainer) {
2109
2135
  this._inputContainer.appendChild(this._arrowContainer);
2110
2136
  }
@@ -2120,6 +2146,7 @@ class EnhancedSelect extends HTMLElement {
2120
2146
  this._dropdown.id = listboxId;
2121
2147
  this._input.setAttribute('aria-controls', listboxId);
2122
2148
  this._input.setAttribute('aria-owns', listboxId);
2149
+ this._syncClearControlState();
2123
2150
  }
2124
2151
  _initializeStyles() {
2125
2152
  const style = document.createElement('style');
@@ -2193,6 +2220,58 @@ class EnhancedSelect extends HTMLElement {
2193
2220
  border-radius: var(--select-arrow-border-radius, 0 4px 4px 0);
2194
2221
  z-index: 2;
2195
2222
  }
2223
+
2224
+ .input-container.has-clear-control {
2225
+ padding: var(--select-input-padding-with-clear, 6px 84px 6px 8px);
2226
+ }
2227
+
2228
+ .input-container.has-clear-control::after {
2229
+ right: var(--select-separator-position-with-clear, 72px);
2230
+ }
2231
+
2232
+ .dropdown-arrow-container.with-clear-control {
2233
+ right: var(--select-arrow-right-with-clear, 32px);
2234
+ }
2235
+
2236
+ .clear-control-button {
2237
+ position: absolute;
2238
+ top: 50%;
2239
+ right: var(--select-clear-button-right, 6px);
2240
+ transform: translateY(-50%);
2241
+ width: var(--select-clear-button-size, 24px);
2242
+ height: var(--select-clear-button-size, 24px);
2243
+ border: var(--select-clear-button-border, none);
2244
+ border-radius: var(--select-clear-button-radius, 999px);
2245
+ background: var(--select-clear-button-bg, transparent);
2246
+ color: var(--select-clear-button-color, #6b7280);
2247
+ display: inline-flex;
2248
+ align-items: center;
2249
+ justify-content: center;
2250
+ cursor: pointer;
2251
+ z-index: 3;
2252
+ transition: var(--select-clear-button-transition, all 0.2s ease);
2253
+ }
2254
+
2255
+ .clear-control-button:hover {
2256
+ background: var(--select-clear-button-hover-bg, rgba(0, 0, 0, 0.06));
2257
+ color: var(--select-clear-button-hover-color, #111827);
2258
+ }
2259
+
2260
+ .clear-control-button:focus-visible {
2261
+ outline: var(--select-clear-button-focus-outline, 2px solid rgba(102, 126, 234, 0.55));
2262
+ outline-offset: 1px;
2263
+ }
2264
+
2265
+ .clear-control-button[hidden] {
2266
+ display: none;
2267
+ }
2268
+
2269
+ .clear-control-icon {
2270
+ font-size: var(--select-clear-icon-size, 16px);
2271
+ line-height: 1;
2272
+ font-weight: var(--select-clear-icon-weight, 500);
2273
+ pointer-events: none;
2274
+ }
2196
2275
 
2197
2276
  .dropdown-arrow-container:hover {
2198
2277
  background-color: var(--select-arrow-hover-bg, rgba(102, 126, 234, 0.08));
@@ -2326,12 +2405,12 @@ class EnhancedSelect extends HTMLElement {
2326
2405
  }
2327
2406
 
2328
2407
  .option:hover {
2329
- background-color: var(--select-option-hover-bg, #f3f4f6);
2408
+ background: var(--select-option-hover-bg, #f3f4f6);
2330
2409
  color: var(--select-option-hover-color, #1f2937);
2331
2410
  }
2332
2411
 
2333
2412
  .option.selected {
2334
- background-color: var(--select-option-selected-bg, #e0e7ff);
2413
+ background: var(--select-option-selected-bg, #e0e7ff);
2335
2414
  color: var(--select-option-selected-color, #4338ca);
2336
2415
  font-weight: var(--select-option-selected-weight, 500);
2337
2416
  border: var(--select-option-selected-border, var(--select-option-border, none));
@@ -2342,7 +2421,7 @@ class EnhancedSelect extends HTMLElement {
2342
2421
  }
2343
2422
 
2344
2423
  .option.selected:hover {
2345
- background-color: var(--select-option-selected-hover-bg, var(--select-option-selected-bg, #e0e7ff));
2424
+ background: var(--select-option-selected-hover-bg, var(--select-option-selected-bg, #e0e7ff));
2346
2425
  color: var(--select-option-selected-hover-color, var(--select-option-selected-color, #4338ca));
2347
2426
  border: var(--select-option-selected-hover-border, var(--select-option-selected-border, var(--select-option-border, none)));
2348
2427
  border-bottom: var(--select-option-selected-hover-border-bottom, var(--select-option-selected-border-bottom, var(--select-option-border-bottom, none)));
@@ -2350,15 +2429,30 @@ class EnhancedSelect extends HTMLElement {
2350
2429
  transform: var(--select-option-selected-hover-transform, var(--select-option-selected-transform, var(--select-option-transform, none)));
2351
2430
  }
2352
2431
 
2353
- .option.active {
2354
- background-color: var(--select-option-active-bg, #f3f4f6);
2432
+ .option.active:not(.selected) {
2433
+ background: var(--select-option-active-bg, #f3f4f6);
2355
2434
  color: var(--select-option-active-color, #1f2937);
2356
2435
  outline: var(--select-option-active-outline, 2px solid rgba(99, 102, 241, 0.45));
2357
2436
  outline-offset: -2px;
2358
2437
  }
2359
2438
 
2360
- .option:active {
2361
- background-color: var(--select-option-pressed-bg, #e5e7eb);
2439
+ .option.selected.active {
2440
+ background: var(--select-option-selected-active-bg, var(--select-option-selected-bg, #e0e7ff));
2441
+ color: var(--select-option-selected-active-color, var(--select-option-selected-color, #4338ca));
2442
+ border: var(--select-option-selected-active-border, var(--select-option-selected-border, var(--select-option-border, none)));
2443
+ border-bottom: var(--select-option-selected-active-border-bottom, var(--select-option-selected-border-bottom, var(--select-option-border-bottom, none)));
2444
+ box-shadow: var(--select-option-selected-active-shadow, var(--select-option-selected-shadow, var(--select-option-shadow, none)));
2445
+ transform: var(--select-option-selected-active-transform, var(--select-option-selected-transform, var(--select-option-transform, none)));
2446
+ outline: var(--select-option-selected-active-outline, var(--select-option-active-outline, 2px solid rgba(99, 102, 241, 0.45)));
2447
+ outline-offset: -2px;
2448
+ }
2449
+
2450
+ .option:active:not(.selected) {
2451
+ background: var(--select-option-pressed-bg, #e5e7eb);
2452
+ }
2453
+
2454
+ .option.selected:active {
2455
+ background: var(--select-option-selected-pressed-bg, var(--select-option-selected-hover-bg, var(--select-option-selected-bg, #e0e7ff)));
2362
2456
  }
2363
2457
 
2364
2458
  .load-more-container {
@@ -2520,12 +2614,23 @@ class EnhancedSelect extends HTMLElement {
2520
2614
  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)))));
2521
2615
  }
2522
2616
 
2523
- .option.active {
2617
+ .option.active:not(.selected) {
2524
2618
  background-color: var(--select-dark-option-active-bg, #374151);
2525
2619
  color: var(--select-dark-option-active-color, #f9fafb);
2526
2620
  outline: var(--select-dark-option-active-outline, 2px solid rgba(129, 140, 248, 0.55));
2527
2621
  }
2528
2622
 
2623
+ .option.selected.active {
2624
+ background-color: var(--select-dark-option-selected-active-bg, var(--select-dark-option-selected-bg, #3730a3));
2625
+ color: var(--select-dark-option-selected-active-color, var(--select-dark-option-selected-text, #e0e7ff));
2626
+ 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)))));
2627
+ 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)))));
2628
+ 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)))));
2629
+ 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)))));
2630
+ 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)))));
2631
+ outline-offset: -2px;
2632
+ }
2633
+
2529
2634
  .selection-badge {
2530
2635
  background: var(--select-dark-badge-bg, #4f46e5);
2531
2636
  color: var(--select-dark-badge-color, #eef2ff);
@@ -2610,6 +2715,17 @@ class EnhancedSelect extends HTMLElement {
2610
2715
  };
2611
2716
  this._arrowContainer.addEventListener('click', this._boundArrowClick);
2612
2717
  }
2718
+ if (this._clearControl) {
2719
+ this._clearControl.addEventListener('pointerdown', (e) => {
2720
+ e.stopPropagation();
2721
+ e.preventDefault();
2722
+ });
2723
+ this._clearControl.addEventListener('click', (e) => {
2724
+ e.stopPropagation();
2725
+ e.preventDefault();
2726
+ this._handleClearControlClick();
2727
+ });
2728
+ }
2613
2729
  // Input container click - focus input and open dropdown
2614
2730
  this._inputContainer.addEventListener('pointerdown', (e) => {
2615
2731
  const target = e.target;
@@ -2617,6 +2733,8 @@ class EnhancedSelect extends HTMLElement {
2617
2733
  return;
2618
2734
  if (target && target.closest('.dropdown-arrow-container'))
2619
2735
  return;
2736
+ if (target && target.closest('.clear-control-button'))
2737
+ return;
2620
2738
  if (!this._state.isOpen) {
2621
2739
  this._handleOpen();
2622
2740
  }
@@ -2829,6 +2947,7 @@ class EnhancedSelect extends HTMLElement {
2829
2947
  }
2830
2948
  _handleSearch(query) {
2831
2949
  this._state.searchQuery = query;
2950
+ this._syncClearControlState();
2832
2951
  if (query.length > 0) {
2833
2952
  this._perfMark('smilodon-search-input-last');
2834
2953
  this._pendingSearchRenderMark = true;
@@ -3006,7 +3125,9 @@ class EnhancedSelect extends HTMLElement {
3006
3125
  option.classList.add('smilodon-option--active');
3007
3126
  option.setAttribute('aria-selected', 'true');
3008
3127
  }
3009
- option.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
3128
+ if (typeof option.scrollIntoView === 'function') {
3129
+ option.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
3130
+ }
3010
3131
  // Announce position for screen readers
3011
3132
  const total = options.length;
3012
3133
  this._announce(`Item ${index + 1} of ${total}`);
@@ -3261,6 +3382,7 @@ class EnhancedSelect extends HTMLElement {
3261
3382
  this._inputContainer.insertBefore(badge, this._input);
3262
3383
  });
3263
3384
  }
3385
+ this._syncClearControlState();
3264
3386
  }
3265
3387
  _renderOptionsWithAnimation() {
3266
3388
  // Add fade-out animation
@@ -3509,6 +3631,21 @@ class EnhancedSelect extends HTMLElement {
3509
3631
  this._renderOptions();
3510
3632
  this._updateInputDisplay();
3511
3633
  this._emitChange();
3634
+ this._syncClearControlState();
3635
+ }
3636
+ /**
3637
+ * Clear search query and restore full option list
3638
+ */
3639
+ clearSearch() {
3640
+ this._state.searchQuery = '';
3641
+ if (this._config.searchable) {
3642
+ this._input.value = '';
3643
+ if (this._state.selectedIndices.size > 0) {
3644
+ this._updateInputDisplay();
3645
+ }
3646
+ }
3647
+ this._renderOptions();
3648
+ this._syncClearControlState();
3512
3649
  }
3513
3650
  /**
3514
3651
  * Open dropdown
@@ -3535,6 +3672,12 @@ class EnhancedSelect extends HTMLElement {
3535
3672
  this._input.placeholder = this._config.placeholder || 'Select an option...';
3536
3673
  }
3537
3674
  }
3675
+ if (this._clearControl) {
3676
+ this._clearControl.setAttribute('aria-label', this._config.clearControl.ariaLabel || 'Clear selection and search');
3677
+ }
3678
+ if (this._clearControlIcon) {
3679
+ this._clearControlIcon.textContent = this._config.clearControl.icon || '×';
3680
+ }
3538
3681
  if (this._dropdown) {
3539
3682
  if (this._config.selection.mode === 'multi') {
3540
3683
  this._dropdown.setAttribute('aria-multiselectable', 'true');
@@ -3545,8 +3688,47 @@ class EnhancedSelect extends HTMLElement {
3545
3688
  }
3546
3689
  // Re-initialize observers in case infinite scroll was enabled/disabled
3547
3690
  this._initializeObservers();
3691
+ this._syncClearControlState();
3548
3692
  this._renderOptions();
3549
3693
  }
3694
+ _handleClearControlClick() {
3695
+ const shouldClearSelection = this._config.clearControl.clearSelection !== false;
3696
+ const shouldClearSearch = this._config.clearControl.clearSearch !== false;
3697
+ const hadSelection = this._state.selectedIndices.size > 0;
3698
+ const hadSearch = this._state.searchQuery.length > 0;
3699
+ if (shouldClearSelection && hadSelection) {
3700
+ this.clear();
3701
+ }
3702
+ if (shouldClearSearch && hadSearch) {
3703
+ this.clearSearch();
3704
+ }
3705
+ this._emit('clear', {
3706
+ clearedSelection: shouldClearSelection && hadSelection,
3707
+ clearedSearch: shouldClearSearch && hadSearch,
3708
+ });
3709
+ this._config.callbacks.onClear?.({
3710
+ clearedSelection: shouldClearSelection && hadSelection,
3711
+ clearedSearch: shouldClearSearch && hadSearch,
3712
+ });
3713
+ this._input.focus();
3714
+ }
3715
+ _syncClearControlState() {
3716
+ if (!this._clearControl || !this._inputContainer || !this._arrowContainer)
3717
+ return;
3718
+ const enabled = this._config.clearControl.enabled === true;
3719
+ const hasSelection = this._state.selectedIndices.size > 0;
3720
+ const hasSearch = this._state.searchQuery.length > 0;
3721
+ const clearSelection = this._config.clearControl.clearSelection !== false;
3722
+ const clearSearch = this._config.clearControl.clearSearch !== false;
3723
+ const hasSomethingToClear = (clearSelection && hasSelection) || (clearSearch && hasSearch);
3724
+ const hideWhenEmpty = this._config.clearControl.hideWhenEmpty !== false;
3725
+ const visible = enabled && (!hideWhenEmpty || hasSomethingToClear);
3726
+ this._inputContainer.classList.toggle('has-clear-control', enabled);
3727
+ this._arrowContainer.classList.toggle('with-clear-control', enabled);
3728
+ this._clearControl.hidden = !visible;
3729
+ this._clearControl.disabled = !hasSomethingToClear;
3730
+ this._clearControl.setAttribute('aria-hidden', String(!visible));
3731
+ }
3550
3732
  /**
3551
3733
  * Set error state
3552
3734
  */
@@ -4834,10 +5016,17 @@ const shadowDOMStyles = `
4834
5016
  color: var(--ns-item-selected-color, #0066cc);
4835
5017
  }
4836
5018
 
4837
- .ns-item[data-active="true"] {
5019
+ .ns-item[data-active="true"]:not([aria-selected="true"]) {
4838
5020
  background: var(--ns-item-active-bg, rgba(0, 102, 204, 0.2));
4839
5021
  }
4840
5022
 
5023
+ .ns-item[aria-selected="true"][data-active="true"] {
5024
+ background: var(--ns-item-selected-active-bg, var(--ns-item-selected-bg, rgba(0, 102, 204, 0.1)));
5025
+ color: var(--ns-item-selected-active-color, var(--ns-item-selected-color, #0066cc));
5026
+ outline: var(--ns-item-selected-active-outline, 2px solid #0066cc);
5027
+ outline-offset: -2px;
5028
+ }
5029
+
4841
5030
  .ns-item[aria-disabled="true"] {
4842
5031
  opacity: 0.5;
4843
5032
  cursor: not-allowed;