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