@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.umd.js CHANGED
@@ -2417,6 +2417,20 @@
2417
2417
  background: var(--select-options-bg, var(--select-dropdown-bg, var(--select-bg, white)));
2418
2418
  }
2419
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
+
2420
2434
  .option {
2421
2435
  padding: var(--select-option-padding, 8px 12px);
2422
2436
  cursor: pointer;
@@ -2590,9 +2604,12 @@
2590
2604
  }
2591
2605
  }
2592
2606
 
2593
- /* Dark mode - Opt-in via class or data attribute */
2607
+ /* Dark mode - Opt-in via class, data attribute, or ancestor context */
2594
2608
  :host(.dark-mode),
2595
- :host([data-theme="dark"]) {
2609
+ :host([data-theme="dark"]),
2610
+ :host-context(.dark-mode),
2611
+ :host-context(.dark),
2612
+ :host-context([data-theme="dark"]) {
2596
2613
  .input-container {
2597
2614
  background: var(--select-dark-bg, #1f2937);
2598
2615
  border-color: var(--select-dark-border, #4b5563);
@@ -2756,7 +2773,12 @@
2756
2773
  });
2757
2774
  }
2758
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
2759
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();
2760
2782
  const target = e.target;
2761
2783
  if (!this._config.enabled)
2762
2784
  return;
@@ -2764,10 +2786,23 @@
2764
2786
  return;
2765
2787
  if (target && target.closest('.clear-control-button'))
2766
2788
  return;
2767
- if (!this._state.isOpen) {
2789
+ const wasClosed = !this._state.isOpen;
2790
+ if (wasClosed) {
2768
2791
  this._handleOpen();
2769
2792
  }
2793
+ // Focus the input (do not prevent default behavior)
2770
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
+ }
2771
2806
  });
2772
2807
  // Input container click - prevent event from reaching document listener
2773
2808
  this._container.addEventListener('click', (e) => {
@@ -2775,6 +2810,12 @@
2775
2810
  });
2776
2811
  // Input focus/blur
2777
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.
2778
2819
  this._input.addEventListener('blur', (e) => {
2779
2820
  const related = e.relatedTarget;
2780
2821
  if (related && (this._shadow.contains(related) || this._container.contains(related))) {
@@ -2782,6 +2823,10 @@
2782
2823
  }
2783
2824
  // Delay to allow option click/focus transitions
2784
2825
  setTimeout(() => {
2826
+ if (this._suppressBlurClose) {
2827
+ // another pointerdown inside options is in progress; keep open
2828
+ return;
2829
+ }
2785
2830
  const active = document.activeElement;
2786
2831
  if (active && (this._shadow.contains(active) || this._container.contains(active))) {
2787
2832
  return;
@@ -2796,11 +2841,33 @@
2796
2841
  const query = e.target.value;
2797
2842
  this._handleSearch(query);
2798
2843
  });
2799
- // Delegated click listener for improved event handling (smart fallback)
2800
- this._optionsContainer.addEventListener('click', (e) => {
2801
- const target = e.target;
2802
- // Handle option clicks
2803
- 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
+ }
2804
2871
  if (option && !option.hasAttribute('aria-disabled')) {
2805
2872
  const indexStr = option.getAttribute('data-sm-index') ?? option.getAttribute('data-index');
2806
2873
  const index = Number(indexStr);
@@ -2811,13 +2878,45 @@
2811
2878
  });
2812
2879
  }
2813
2880
  }
2814
- });
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);
2815
2886
  // Keyboard navigation
2816
2887
  this._input.addEventListener('keydown', (e) => this._handleKeydown(e));
2817
- // Click outside to close
2888
+ // Click outside to close — robust detection across shadow DOM and custom renderers
2818
2889
  document.addEventListener('pointerdown', (e) => {
2819
2890
  const path = (e.composedPath && e.composedPath()) || [];
2820
- 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
+ }
2821
2920
  if (!clickedInside) {
2822
2921
  this._handleClose();
2823
2922
  }
@@ -3281,13 +3380,14 @@
3281
3380
  // FIX: Do not rely on this._optionsContainer.children[index] because filtering changes the children
3282
3381
  // Instead, use the index to update state directly
3283
3382
  const item = this._state.loadedItems[index];
3383
+ // Debug: log selection attempt
3284
3384
  if (!item)
3285
3385
  return;
3386
+ const isCurrentlySelected = this._state.selectedIndices.has(index);
3286
3387
  // Keep active/focus styling aligned with the most recently interacted option.
3287
3388
  // Without this, a previously selected item may retain active classes/styles
3288
3389
  // after selecting a different option.
3289
3390
  this._state.activeIndex = index;
3290
- const isCurrentlySelected = this._state.selectedIndices.has(index);
3291
3391
  if (this._config.selection.mode === 'single') {
3292
3392
  // Single select: clear previous and select new
3293
3393
  const wasSelected = this._state.selectedIndices.has(index);
@@ -3518,6 +3618,7 @@
3518
3618
  const getValue = this._config.serverSide.getValueFromItem || ((item) => item?.value ?? item);
3519
3619
  const selectedValues = selectedItems.map(getValue);
3520
3620
  const selectedIndices = Array.from(this._state.selectedIndices);
3621
+ // Debug: log change payload
3521
3622
  this._emit('change', { selectedItems, selectedValues, selectedIndices });
3522
3623
  this._config.callbacks.onChange?.(selectedItems, selectedValues);
3523
3624
  }
@@ -3841,19 +3942,6 @@
3841
3942
  const header = document.createElement('div');
3842
3943
  header.className = 'group-header';
3843
3944
  header.textContent = group.label;
3844
- Object.assign(header.style, {
3845
- padding: '8px 12px',
3846
- fontWeight: '600',
3847
- color: '#6b7280',
3848
- backgroundColor: '#f3f4f6',
3849
- fontSize: '12px',
3850
- textTransform: 'uppercase',
3851
- letterSpacing: '0.05em',
3852
- position: 'sticky',
3853
- top: '0',
3854
- zIndex: '1',
3855
- borderBottom: '1px solid #e5e7eb'
3856
- });
3857
3945
  this._optionsContainer.appendChild(header);
3858
3946
  group.options.forEach(item => {
3859
3947
  // Find original index for correct ID generation and selection