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