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