@smilodon/core 1.4.6 → 1.4.9

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
@@ -1506,6 +1506,31 @@
1506
1506
  position: relative;
1507
1507
  }
1508
1508
 
1509
+ /* Allow authors to style selected state from outside the shadow root
1510
+ by setting attributes/classes on the host element. This mirrors the
1511
+ internal .option-container.selected rules but reads the same CSS
1512
+ custom properties so themes can fully control selected appearance. */
1513
+ :host([aria-selected="true"]) .option-container,
1514
+ :host(.smilodon-option--selected) .option-container {
1515
+ background: var(--select-option-selected-bg, #e3f2fd);
1516
+ color: var(--select-option-selected-color, #1976d2);
1517
+ border: var(--select-option-selected-border, var(--select-option-border, none));
1518
+ border-bottom: var(--select-option-selected-border-bottom, var(--select-option-border-bottom, none));
1519
+ border-radius: var(--select-option-selected-border-radius, var(--select-option-border-radius, 0));
1520
+ box-shadow: var(--select-option-selected-shadow, var(--select-option-shadow, none));
1521
+ transform: var(--select-option-selected-transform, var(--select-option-transform, none));
1522
+ }
1523
+
1524
+ :host([aria-selected="true"]) .option-container:hover,
1525
+ :host(.smilodon-option--selected) .option-container:hover {
1526
+ background: var(--select-option-selected-hover-bg, var(--select-option-selected-bg, #e3f2fd));
1527
+ color: var(--select-option-selected-hover-color, var(--select-option-selected-color, #1976d2));
1528
+ border: var(--select-option-selected-hover-border, var(--select-option-selected-border, var(--select-option-border, none)));
1529
+ border-bottom: var(--select-option-selected-hover-border-bottom, var(--select-option-selected-border-bottom, var(--select-option-border-bottom, none)));
1530
+ box-shadow: var(--select-option-selected-hover-shadow, var(--select-option-selected-shadow, var(--select-option-shadow, none)));
1531
+ transform: var(--select-option-selected-hover-transform, var(--select-option-selected-transform, var(--select-option-transform, none)));
1532
+ }
1533
+
1509
1534
  .option-container {
1510
1535
  display: flex;
1511
1536
  align-items: center;
@@ -2392,6 +2417,20 @@
2392
2417
  background: var(--select-options-bg, var(--select-dropdown-bg, var(--select-bg, white)));
2393
2418
  }
2394
2419
 
2420
+ .group-header {
2421
+ padding: var(--select-group-header-padding, 8px 12px);
2422
+ font-weight: var(--select-group-header-weight, 600);
2423
+ color: var(--select-group-header-color, #6b7280);
2424
+ background-color: var(--select-group-header-bg, #f3f4f6);
2425
+ font-size: var(--select-group-header-font-size, 12px);
2426
+ text-transform: var(--select-group-header-text-transform, uppercase);
2427
+ letter-spacing: var(--select-group-header-letter-spacing, 0.05em);
2428
+ position: sticky;
2429
+ top: 0;
2430
+ z-index: 1;
2431
+ border-bottom: var(--select-group-header-border-bottom, 1px solid #e5e7eb);
2432
+ }
2433
+
2395
2434
  .option {
2396
2435
  padding: var(--select-option-padding, 8px 12px);
2397
2436
  cursor: pointer;
@@ -2565,9 +2604,12 @@
2565
2604
  }
2566
2605
  }
2567
2606
 
2568
- /* Dark mode - Opt-in via class or data attribute */
2607
+ /* Dark mode - Opt-in via class, data attribute, or ancestor context */
2569
2608
  :host(.dark-mode),
2570
- :host([data-theme="dark"]) {
2609
+ :host([data-theme="dark"]),
2610
+ :host-context(.dark-mode),
2611
+ :host-context(.dark),
2612
+ :host-context([data-theme="dark"]) {
2571
2613
  .input-container {
2572
2614
  background: var(--select-dark-bg, #1f2937);
2573
2615
  border-color: var(--select-dark-border, #4b5563);
@@ -2596,12 +2638,12 @@
2596
2638
  }
2597
2639
 
2598
2640
  .option:hover {
2599
- background-color: var(--select-dark-option-hover-bg, #374151);
2641
+ background: var(--select-dark-option-hover-bg, #374151);
2600
2642
  color: var(--select-dark-option-hover-color, #f9fafb);
2601
2643
  }
2602
2644
 
2603
2645
  .option.selected {
2604
- background-color: var(--select-dark-option-selected-bg, #3730a3);
2646
+ background: var(--select-dark-option-selected-bg, #3730a3);
2605
2647
  color: var(--select-dark-option-selected-text, #e0e7ff);
2606
2648
  border: var(--select-dark-option-selected-border, var(--select-option-selected-border, var(--select-option-border, none)));
2607
2649
  border-bottom: var(--select-dark-option-selected-border-bottom, var(--select-option-selected-border-bottom, var(--select-option-border-bottom, none)));
@@ -2610,7 +2652,7 @@
2610
2652
  }
2611
2653
 
2612
2654
  .option.selected:hover {
2613
- background-color: var(--select-dark-option-selected-hover-bg, var(--select-dark-option-selected-bg, #3730a3));
2655
+ background: var(--select-dark-option-selected-hover-bg, var(--select-dark-option-selected-bg, #3730a3));
2614
2656
  color: var(--select-dark-option-selected-hover-color, var(--select-dark-option-selected-text, #e0e7ff));
2615
2657
  border: var(--select-dark-option-selected-hover-border, var(--select-dark-option-selected-border, var(--select-option-selected-hover-border, var(--select-option-selected-border, var(--select-option-border, none)))));
2616
2658
  border-bottom: var(--select-dark-option-selected-hover-border-bottom, var(--select-dark-option-selected-border-bottom, var(--select-option-selected-hover-border-bottom, var(--select-option-selected-border-bottom, var(--select-option-border-bottom, none)))));
@@ -2731,7 +2773,12 @@
2731
2773
  });
2732
2774
  }
2733
2775
  // Input container click - focus input and open dropdown
2776
+ // Prevent the original pointer event from bubbling/causing default focus behavior
2777
+ // which can interfere with option click handling when opening the dropdown
2734
2778
  this._inputContainer.addEventListener('pointerdown', (e) => {
2779
+ // Prevent propagation to document click listener but do NOT preventDefault.
2780
+ // Allow default so browser events (click) on newly opened options still fire.
2781
+ e.stopPropagation();
2735
2782
  const target = e.target;
2736
2783
  if (!this._config.enabled)
2737
2784
  return;
@@ -2739,10 +2786,23 @@
2739
2786
  return;
2740
2787
  if (target && target.closest('.clear-control-button'))
2741
2788
  return;
2742
- if (!this._state.isOpen) {
2789
+ const wasClosed = !this._state.isOpen;
2790
+ if (wasClosed) {
2743
2791
  this._handleOpen();
2744
2792
  }
2793
+ // Focus the input (do not prevent default behavior)
2745
2794
  this._input.focus();
2795
+ // If we just opened the dropdown, transfer pointer capture to the
2796
+ // options container so the subsequent pointerup lands there instead of
2797
+ // staying with the input container (which would swallow the event).
2798
+ if (wasClosed && this._optionsContainer && typeof e.pointerId === 'number') {
2799
+ try {
2800
+ this._optionsContainer.setPointerCapture(e.pointerId);
2801
+ }
2802
+ catch (_err) {
2803
+ // Some browsers may throw if element is not yet "connected"; ignore
2804
+ }
2805
+ }
2746
2806
  });
2747
2807
  // Input container click - prevent event from reaching document listener
2748
2808
  this._container.addEventListener('click', (e) => {
@@ -2750,6 +2810,12 @@
2750
2810
  });
2751
2811
  // Input focus/blur
2752
2812
  this._input.addEventListener('focus', () => this._handleOpen());
2813
+ // When the input loses focus we normally close the dropdown, but
2814
+ // clicking an option will blur the input before the option's click
2815
+ // handler executes. To avoid the blur timer closing the dropdown
2816
+ // prematurely we use a short-lived flag that is set whenever we start
2817
+ // interacting with the options container. The close callback checks this
2818
+ // flag and skips closing if the user is about to click an option.
2753
2819
  this._input.addEventListener('blur', (e) => {
2754
2820
  const related = e.relatedTarget;
2755
2821
  if (related && (this._shadow.contains(related) || this._container.contains(related))) {
@@ -2757,6 +2823,10 @@
2757
2823
  }
2758
2824
  // Delay to allow option click/focus transitions
2759
2825
  setTimeout(() => {
2826
+ if (this._suppressBlurClose) {
2827
+ // another pointerdown inside options is in progress; keep open
2828
+ return;
2829
+ }
2760
2830
  const active = document.activeElement;
2761
2831
  if (active && (this._shadow.contains(active) || this._container.contains(active))) {
2762
2832
  return;
@@ -2771,11 +2841,33 @@
2771
2841
  const query = e.target.value;
2772
2842
  this._handleSearch(query);
2773
2843
  });
2774
- // Delegated click listener for improved event handling (smart fallback)
2775
- this._optionsContainer.addEventListener('click', (e) => {
2776
- const target = e.target;
2777
- // Handle option clicks
2778
- const option = target.closest('[data-sm-selectable], [data-selectable], [data-sm-state]');
2844
+ // If the user presses down inside the options container we should
2845
+ // temporarily suppress blur-based closing until after the click has
2846
+ // been handled. Setting a flag that gets cleared in the next tick is
2847
+ // sufficient.
2848
+ this._optionsContainer.addEventListener('pointerdown', () => {
2849
+ this._suppressBlurClose = true;
2850
+ setTimeout(() => {
2851
+ this._suppressBlurClose = false;
2852
+ }, 0);
2853
+ });
2854
+ // Delegated click listener for improved event handling (robust across shadow DOM)
2855
+ const handleOptionEvent = (e) => {
2856
+ const path = (e.composedPath && e.composedPath()) || [e.target];
2857
+ let option = null;
2858
+ for (const node of path) {
2859
+ if (!(node instanceof Element))
2860
+ continue;
2861
+ try {
2862
+ if (node.matches('[data-sm-selectable], [data-selectable], [data-sm-state]')) {
2863
+ option = node;
2864
+ break;
2865
+ }
2866
+ }
2867
+ catch (err) {
2868
+ continue;
2869
+ }
2870
+ }
2779
2871
  if (option && !option.hasAttribute('aria-disabled')) {
2780
2872
  const indexStr = option.getAttribute('data-sm-index') ?? option.getAttribute('data-index');
2781
2873
  const index = Number(indexStr);
@@ -2786,13 +2878,45 @@
2786
2878
  });
2787
2879
  }
2788
2880
  }
2789
- });
2881
+ };
2882
+ this._optionsContainer.addEventListener('click', handleOptionEvent);
2883
+ // also watch pointerup to catch cases where the pointerdown started outside
2884
+ // (e.g. on the input) and the click never fires
2885
+ this._optionsContainer.addEventListener('pointerup', handleOptionEvent);
2790
2886
  // Keyboard navigation
2791
2887
  this._input.addEventListener('keydown', (e) => this._handleKeydown(e));
2792
- // Click outside to close
2888
+ // Click outside to close — robust detection across shadow DOM and custom renderers
2793
2889
  document.addEventListener('pointerdown', (e) => {
2794
2890
  const path = (e.composedPath && e.composedPath()) || [];
2795
- const clickedInside = path.includes(this) || path.includes(this._container) || this._shadow.contains(e.target);
2891
+ let clickedInside = false;
2892
+ for (const node of path) {
2893
+ if (node === this || node === this._container) {
2894
+ clickedInside = true;
2895
+ break;
2896
+ }
2897
+ if (node instanceof Node) {
2898
+ try {
2899
+ if (this._shadow && this._shadow.contains(node)) {
2900
+ clickedInside = true;
2901
+ break;
2902
+ }
2903
+ }
2904
+ catch (err) {
2905
+ // ignore
2906
+ }
2907
+ }
2908
+ if (node instanceof Element) {
2909
+ try {
2910
+ if (node.matches('[data-sm-selectable], [data-selectable], [data-sm-state], .input-container, .select-container, .dropdown-arrow-container, .clear-control-button')) {
2911
+ clickedInside = true;
2912
+ break;
2913
+ }
2914
+ }
2915
+ catch (err) {
2916
+ // ignore
2917
+ }
2918
+ }
2919
+ }
2796
2920
  if (!clickedInside) {
2797
2921
  this._handleClose();
2798
2922
  }
@@ -3256,13 +3380,14 @@
3256
3380
  // FIX: Do not rely on this._optionsContainer.children[index] because filtering changes the children
3257
3381
  // Instead, use the index to update state directly
3258
3382
  const item = this._state.loadedItems[index];
3383
+ // Debug: log selection attempt
3259
3384
  if (!item)
3260
3385
  return;
3386
+ const isCurrentlySelected = this._state.selectedIndices.has(index);
3261
3387
  // Keep active/focus styling aligned with the most recently interacted option.
3262
3388
  // Without this, a previously selected item may retain active classes/styles
3263
3389
  // after selecting a different option.
3264
3390
  this._state.activeIndex = index;
3265
- const isCurrentlySelected = this._state.selectedIndices.has(index);
3266
3391
  if (this._config.selection.mode === 'single') {
3267
3392
  // Single select: clear previous and select new
3268
3393
  const wasSelected = this._state.selectedIndices.has(index);
@@ -3493,6 +3618,7 @@
3493
3618
  const getValue = this._config.serverSide.getValueFromItem || ((item) => item?.value ?? item);
3494
3619
  const selectedValues = selectedItems.map(getValue);
3495
3620
  const selectedIndices = Array.from(this._state.selectedIndices);
3621
+ // Debug: log change payload
3496
3622
  this._emit('change', { selectedItems, selectedValues, selectedIndices });
3497
3623
  this._config.callbacks.onChange?.(selectedItems, selectedValues);
3498
3624
  }
@@ -3816,19 +3942,6 @@
3816
3942
  const header = document.createElement('div');
3817
3943
  header.className = 'group-header';
3818
3944
  header.textContent = group.label;
3819
- Object.assign(header.style, {
3820
- padding: '8px 12px',
3821
- fontWeight: '600',
3822
- color: '#6b7280',
3823
- backgroundColor: '#f3f4f6',
3824
- fontSize: '12px',
3825
- textTransform: 'uppercase',
3826
- letterSpacing: '0.05em',
3827
- position: 'sticky',
3828
- top: '0',
3829
- zIndex: '1',
3830
- borderBottom: '1px solid #e5e7eb'
3831
- });
3832
3945
  this._optionsContainer.appendChild(header);
3833
3946
  group.options.forEach(item => {
3834
3947
  // Find original index for correct ID generation and selection