@smilodon/core 1.4.7 → 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
@@ -2413,6 +2413,20 @@ class EnhancedSelect extends HTMLElement {
2413
2413
  background: var(--select-options-bg, var(--select-dropdown-bg, var(--select-bg, white)));
2414
2414
  }
2415
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
+
2416
2430
  .option {
2417
2431
  padding: var(--select-option-padding, 8px 12px);
2418
2432
  cursor: pointer;
@@ -2586,9 +2600,12 @@ class EnhancedSelect extends HTMLElement {
2586
2600
  }
2587
2601
  }
2588
2602
 
2589
- /* Dark mode - Opt-in via class or data attribute */
2603
+ /* Dark mode - Opt-in via class, data attribute, or ancestor context */
2590
2604
  :host(.dark-mode),
2591
- :host([data-theme="dark"]) {
2605
+ :host([data-theme="dark"]),
2606
+ :host-context(.dark-mode),
2607
+ :host-context(.dark),
2608
+ :host-context([data-theme="dark"]) {
2592
2609
  .input-container {
2593
2610
  background: var(--select-dark-bg, #1f2937);
2594
2611
  border-color: var(--select-dark-border, #4b5563);
@@ -2752,7 +2769,12 @@ class EnhancedSelect extends HTMLElement {
2752
2769
  });
2753
2770
  }
2754
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
2755
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();
2756
2778
  const target = e.target;
2757
2779
  if (!this._config.enabled)
2758
2780
  return;
@@ -2760,10 +2782,23 @@ class EnhancedSelect extends HTMLElement {
2760
2782
  return;
2761
2783
  if (target && target.closest('.clear-control-button'))
2762
2784
  return;
2763
- if (!this._state.isOpen) {
2785
+ const wasClosed = !this._state.isOpen;
2786
+ if (wasClosed) {
2764
2787
  this._handleOpen();
2765
2788
  }
2789
+ // Focus the input (do not prevent default behavior)
2766
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
+ }
2767
2802
  });
2768
2803
  // Input container click - prevent event from reaching document listener
2769
2804
  this._container.addEventListener('click', (e) => {
@@ -2771,6 +2806,12 @@ class EnhancedSelect extends HTMLElement {
2771
2806
  });
2772
2807
  // Input focus/blur
2773
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.
2774
2815
  this._input.addEventListener('blur', (e) => {
2775
2816
  const related = e.relatedTarget;
2776
2817
  if (related && (this._shadow.contains(related) || this._container.contains(related))) {
@@ -2778,6 +2819,10 @@ class EnhancedSelect extends HTMLElement {
2778
2819
  }
2779
2820
  // Delay to allow option click/focus transitions
2780
2821
  setTimeout(() => {
2822
+ if (this._suppressBlurClose) {
2823
+ // another pointerdown inside options is in progress; keep open
2824
+ return;
2825
+ }
2781
2826
  const active = document.activeElement;
2782
2827
  if (active && (this._shadow.contains(active) || this._container.contains(active))) {
2783
2828
  return;
@@ -2792,11 +2837,33 @@ class EnhancedSelect extends HTMLElement {
2792
2837
  const query = e.target.value;
2793
2838
  this._handleSearch(query);
2794
2839
  });
2795
- // Delegated click listener for improved event handling (smart fallback)
2796
- this._optionsContainer.addEventListener('click', (e) => {
2797
- const target = e.target;
2798
- // Handle option clicks
2799
- 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
+ }
2800
2867
  if (option && !option.hasAttribute('aria-disabled')) {
2801
2868
  const indexStr = option.getAttribute('data-sm-index') ?? option.getAttribute('data-index');
2802
2869
  const index = Number(indexStr);
@@ -2807,13 +2874,45 @@ class EnhancedSelect extends HTMLElement {
2807
2874
  });
2808
2875
  }
2809
2876
  }
2810
- });
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);
2811
2882
  // Keyboard navigation
2812
2883
  this._input.addEventListener('keydown', (e) => this._handleKeydown(e));
2813
- // Click outside to close
2884
+ // Click outside to close — robust detection across shadow DOM and custom renderers
2814
2885
  document.addEventListener('pointerdown', (e) => {
2815
2886
  const path = (e.composedPath && e.composedPath()) || [];
2816
- 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
+ }
2817
2916
  if (!clickedInside) {
2818
2917
  this._handleClose();
2819
2918
  }
@@ -3277,13 +3376,14 @@ class EnhancedSelect extends HTMLElement {
3277
3376
  // FIX: Do not rely on this._optionsContainer.children[index] because filtering changes the children
3278
3377
  // Instead, use the index to update state directly
3279
3378
  const item = this._state.loadedItems[index];
3379
+ // Debug: log selection attempt
3280
3380
  if (!item)
3281
3381
  return;
3382
+ const isCurrentlySelected = this._state.selectedIndices.has(index);
3282
3383
  // Keep active/focus styling aligned with the most recently interacted option.
3283
3384
  // Without this, a previously selected item may retain active classes/styles
3284
3385
  // after selecting a different option.
3285
3386
  this._state.activeIndex = index;
3286
- const isCurrentlySelected = this._state.selectedIndices.has(index);
3287
3387
  if (this._config.selection.mode === 'single') {
3288
3388
  // Single select: clear previous and select new
3289
3389
  const wasSelected = this._state.selectedIndices.has(index);
@@ -3514,6 +3614,7 @@ class EnhancedSelect extends HTMLElement {
3514
3614
  const getValue = this._config.serverSide.getValueFromItem || ((item) => item?.value ?? item);
3515
3615
  const selectedValues = selectedItems.map(getValue);
3516
3616
  const selectedIndices = Array.from(this._state.selectedIndices);
3617
+ // Debug: log change payload
3517
3618
  this._emit('change', { selectedItems, selectedValues, selectedIndices });
3518
3619
  this._config.callbacks.onChange?.(selectedItems, selectedValues);
3519
3620
  }
@@ -3837,19 +3938,6 @@ class EnhancedSelect extends HTMLElement {
3837
3938
  const header = document.createElement('div');
3838
3939
  header.className = 'group-header';
3839
3940
  header.textContent = group.label;
3840
- Object.assign(header.style, {
3841
- padding: '8px 12px',
3842
- fontWeight: '600',
3843
- color: '#6b7280',
3844
- backgroundColor: '#f3f4f6',
3845
- fontSize: '12px',
3846
- textTransform: 'uppercase',
3847
- letterSpacing: '0.05em',
3848
- position: 'sticky',
3849
- top: '0',
3850
- zIndex: '1',
3851
- borderBottom: '1px solid #e5e7eb'
3852
- });
3853
3941
  this._optionsContainer.appendChild(header);
3854
3942
  group.options.forEach(item => {
3855
3943
  // Find original index for correct ID generation and selection