@smilodon/core 1.4.7 → 1.4.10
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/README.md +28 -0
- package/dist/index.cjs +191 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +191 -44
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/index.umd.js +191 -44
- package/dist/index.umd.js.map +1 -1
- package/dist/index.umd.min.js +1 -1
- package/dist/index.umd.min.js.map +1 -1
- package/dist/types/src/components/enhanced-select.d.ts +10 -0
- package/dist/types/src/types.d.ts +5 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1873,7 +1873,22 @@ class EnhancedSelect extends HTMLElement {
|
|
|
1873
1873
|
}
|
|
1874
1874
|
set classMap(map) {
|
|
1875
1875
|
this._classMap = map;
|
|
1876
|
-
this._setGlobalStylesMirroring(Boolean(this._optionRenderer || map));
|
|
1876
|
+
this._setGlobalStylesMirroring(Boolean(this._optionRenderer || map || this._groupHeaderRenderer));
|
|
1877
|
+
if (!this.isConnected)
|
|
1878
|
+
return;
|
|
1879
|
+
this._renderOptions();
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* DOM-based renderer for group headers. When provided, the component will
|
|
1883
|
+
* call this function for each group during rendering. The returned element
|
|
1884
|
+
* will receive `.group-header` and `part="group-header"` automatically.
|
|
1885
|
+
*/
|
|
1886
|
+
get groupHeaderRenderer() {
|
|
1887
|
+
return this._groupHeaderRenderer;
|
|
1888
|
+
}
|
|
1889
|
+
set groupHeaderRenderer(renderer) {
|
|
1890
|
+
this._groupHeaderRenderer = renderer;
|
|
1891
|
+
this._setGlobalStylesMirroring(Boolean(this._optionRenderer || this._classMap || renderer));
|
|
1877
1892
|
if (!this.isConnected)
|
|
1878
1893
|
return;
|
|
1879
1894
|
this._renderOptions();
|
|
@@ -1932,6 +1947,8 @@ class EnhancedSelect extends HTMLElement {
|
|
|
1932
1947
|
this._initializeObservers();
|
|
1933
1948
|
}
|
|
1934
1949
|
connectedCallback() {
|
|
1950
|
+
// register instance
|
|
1951
|
+
EnhancedSelect._instances.add(this);
|
|
1935
1952
|
// WORKAROUND: Force display style on host element for Angular compatibility
|
|
1936
1953
|
// Angular's rendering seems to not apply :host styles correctly in some cases
|
|
1937
1954
|
// Must be done in connectedCallback when element is attached to DOM
|
|
@@ -1950,6 +1967,8 @@ class EnhancedSelect extends HTMLElement {
|
|
|
1950
1967
|
}
|
|
1951
1968
|
}
|
|
1952
1969
|
disconnectedCallback() {
|
|
1970
|
+
// unregister instance
|
|
1971
|
+
EnhancedSelect._instances.delete(this);
|
|
1953
1972
|
// Cleanup observers
|
|
1954
1973
|
this._resizeObserver?.disconnect();
|
|
1955
1974
|
this._intersectionObserver?.disconnect();
|
|
@@ -2235,6 +2254,8 @@ class EnhancedSelect extends HTMLElement {
|
|
|
2235
2254
|
right: 0;
|
|
2236
2255
|
bottom: 0;
|
|
2237
2256
|
width: var(--select-arrow-width, 40px);
|
|
2257
|
+
/* allow explicit height override even though container normally stretches */
|
|
2258
|
+
height: var(--select-arrow-height, auto);
|
|
2238
2259
|
display: flex;
|
|
2239
2260
|
align-items: center;
|
|
2240
2261
|
justify-content: center;
|
|
@@ -2300,9 +2321,14 @@ class EnhancedSelect extends HTMLElement {
|
|
|
2300
2321
|
background-color: var(--select-arrow-hover-bg, rgba(102, 126, 234, 0.08));
|
|
2301
2322
|
}
|
|
2302
2323
|
|
|
2324
|
+
.dropdown-arrow:hover {
|
|
2325
|
+
/* legacy alias --select-arrow-hover for icon color */
|
|
2326
|
+
color: var(--select-arrow-hover, var(--select-arrow-hover-color, #667eea));
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2303
2329
|
.dropdown-arrow {
|
|
2304
|
-
width: var(--select-arrow-size, 16px);
|
|
2305
|
-
height: var(--select-arrow-size, 16px);
|
|
2330
|
+
width: var(--select-arrow-width, var(--select-arrow-size, 16px));
|
|
2331
|
+
height: var(--select-arrow-height, var(--select-arrow-size, 16px));
|
|
2306
2332
|
color: var(--select-arrow-color, #667eea);
|
|
2307
2333
|
transition: transform 0.2s ease, color 0.2s ease;
|
|
2308
2334
|
transform: translateY(0);
|
|
@@ -2411,6 +2437,20 @@ class EnhancedSelect extends HTMLElement {
|
|
|
2411
2437
|
background: var(--select-options-bg, var(--select-dropdown-bg, var(--select-bg, white)));
|
|
2412
2438
|
}
|
|
2413
2439
|
|
|
2440
|
+
.group-header {
|
|
2441
|
+
padding: var(--select-group-header-padding, 8px 12px);
|
|
2442
|
+
font-weight: var(--select-group-header-weight, 600);
|
|
2443
|
+
color: var(--select-group-header-color, #6b7280);
|
|
2444
|
+
background-color: var(--select-group-header-bg, #f3f4f6);
|
|
2445
|
+
font-size: var(--select-group-header-font-size, 12px);
|
|
2446
|
+
text-transform: var(--select-group-header-text-transform, uppercase);
|
|
2447
|
+
letter-spacing: var(--select-group-header-letter-spacing, 0.05em);
|
|
2448
|
+
position: sticky;
|
|
2449
|
+
top: 0;
|
|
2450
|
+
z-index: 1;
|
|
2451
|
+
border-bottom: var(--select-group-header-border-bottom, 1px solid #e5e7eb);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2414
2454
|
.option {
|
|
2415
2455
|
padding: var(--select-option-padding, 8px 12px);
|
|
2416
2456
|
cursor: pointer;
|
|
@@ -2584,9 +2624,12 @@ class EnhancedSelect extends HTMLElement {
|
|
|
2584
2624
|
}
|
|
2585
2625
|
}
|
|
2586
2626
|
|
|
2587
|
-
/* Dark mode - Opt-in via class
|
|
2627
|
+
/* Dark mode - Opt-in via class, data attribute, or ancestor context */
|
|
2588
2628
|
:host(.dark-mode),
|
|
2589
|
-
:host([data-theme="dark"])
|
|
2629
|
+
:host([data-theme="dark"]),
|
|
2630
|
+
:host-context(.dark-mode),
|
|
2631
|
+
:host-context(.dark),
|
|
2632
|
+
:host-context([data-theme="dark"]) {
|
|
2590
2633
|
.input-container {
|
|
2591
2634
|
background: var(--select-dark-bg, #1f2937);
|
|
2592
2635
|
border-color: var(--select-dark-border, #4b5563);
|
|
@@ -2639,6 +2682,13 @@ class EnhancedSelect extends HTMLElement {
|
|
|
2639
2682
|
|
|
2640
2683
|
.option.active:not(.selected) {
|
|
2641
2684
|
background-color: var(--select-dark-option-active-bg, #374151);
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
/* Group header in dark mode */
|
|
2688
|
+
.group-header {
|
|
2689
|
+
color: var(--select-dark-group-header-color, var(--select-group-header-color, #6b7280));
|
|
2690
|
+
background-color: var(--select-dark-group-header-bg, var(--select-group-header-bg, #374151));
|
|
2691
|
+
}
|
|
2642
2692
|
color: var(--select-dark-option-active-color, #f9fafb);
|
|
2643
2693
|
outline: var(--select-dark-option-active-outline, 2px solid rgba(129, 140, 248, 0.55));
|
|
2644
2694
|
}
|
|
@@ -2721,19 +2771,14 @@ class EnhancedSelect extends HTMLElement {
|
|
|
2721
2771
|
this._boundArrowClick = (e) => {
|
|
2722
2772
|
e.stopPropagation();
|
|
2723
2773
|
e.preventDefault();
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
this.
|
|
2728
|
-
|
|
2729
|
-
this._config.callbacks.onOpen();
|
|
2730
|
-
}
|
|
2731
|
-
else if (!this._state.isOpen && this._config.callbacks.onClose) {
|
|
2732
|
-
this._config.callbacks.onClose();
|
|
2774
|
+
// delegate to the existing open/close helpers so we don't accidentally
|
|
2775
|
+
// drift out of sync with the logic in those methods (focus, events,
|
|
2776
|
+
// scroll-to-selected, etc.)
|
|
2777
|
+
if (this._state.isOpen) {
|
|
2778
|
+
this._handleClose();
|
|
2733
2779
|
}
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
setTimeout(() => this._scrollToSelected(), 50);
|
|
2780
|
+
else {
|
|
2781
|
+
this._handleOpen();
|
|
2737
2782
|
}
|
|
2738
2783
|
};
|
|
2739
2784
|
this._arrowContainer.addEventListener('click', this._boundArrowClick);
|
|
@@ -2750,7 +2795,12 @@ class EnhancedSelect extends HTMLElement {
|
|
|
2750
2795
|
});
|
|
2751
2796
|
}
|
|
2752
2797
|
// Input container click - focus input and open dropdown
|
|
2798
|
+
// Prevent the original pointer event from bubbling/causing default focus behavior
|
|
2799
|
+
// which can interfere with option click handling when opening the dropdown
|
|
2753
2800
|
this._inputContainer.addEventListener('pointerdown', (e) => {
|
|
2801
|
+
// Prevent propagation to document click listener but do NOT preventDefault.
|
|
2802
|
+
// Allow default so browser events (click) on newly opened options still fire.
|
|
2803
|
+
e.stopPropagation();
|
|
2754
2804
|
const target = e.target;
|
|
2755
2805
|
if (!this._config.enabled)
|
|
2756
2806
|
return;
|
|
@@ -2758,10 +2808,27 @@ class EnhancedSelect extends HTMLElement {
|
|
|
2758
2808
|
return;
|
|
2759
2809
|
if (target && target.closest('.clear-control-button'))
|
|
2760
2810
|
return;
|
|
2761
|
-
|
|
2811
|
+
const wasClosed = !this._state.isOpen;
|
|
2812
|
+
if (wasClosed) {
|
|
2762
2813
|
this._handleOpen();
|
|
2763
2814
|
}
|
|
2815
|
+
else {
|
|
2816
|
+
// clicking the input while open should close the dropdown too
|
|
2817
|
+
this._handleClose();
|
|
2818
|
+
}
|
|
2819
|
+
// Focus the input (do not prevent default behavior)
|
|
2764
2820
|
this._input.focus();
|
|
2821
|
+
// If we just opened the dropdown, transfer pointer capture to the
|
|
2822
|
+
// options container so the subsequent pointerup lands there instead of
|
|
2823
|
+
// staying with the input container (which would swallow the event).
|
|
2824
|
+
if (wasClosed && this._optionsContainer && typeof e.pointerId === 'number') {
|
|
2825
|
+
try {
|
|
2826
|
+
this._optionsContainer.setPointerCapture(e.pointerId);
|
|
2827
|
+
}
|
|
2828
|
+
catch (_err) {
|
|
2829
|
+
// Some browsers may throw if element is not yet "connected"; ignore
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2765
2832
|
});
|
|
2766
2833
|
// Input container click - prevent event from reaching document listener
|
|
2767
2834
|
this._container.addEventListener('click', (e) => {
|
|
@@ -2769,6 +2836,12 @@ class EnhancedSelect extends HTMLElement {
|
|
|
2769
2836
|
});
|
|
2770
2837
|
// Input focus/blur
|
|
2771
2838
|
this._input.addEventListener('focus', () => this._handleOpen());
|
|
2839
|
+
// When the input loses focus we normally close the dropdown, but
|
|
2840
|
+
// clicking an option will blur the input before the option's click
|
|
2841
|
+
// handler executes. To avoid the blur timer closing the dropdown
|
|
2842
|
+
// prematurely we use a short-lived flag that is set whenever we start
|
|
2843
|
+
// interacting with the options container. The close callback checks this
|
|
2844
|
+
// flag and skips closing if the user is about to click an option.
|
|
2772
2845
|
this._input.addEventListener('blur', (e) => {
|
|
2773
2846
|
const related = e.relatedTarget;
|
|
2774
2847
|
if (related && (this._shadow.contains(related) || this._container.contains(related))) {
|
|
@@ -2776,6 +2849,10 @@ class EnhancedSelect extends HTMLElement {
|
|
|
2776
2849
|
}
|
|
2777
2850
|
// Delay to allow option click/focus transitions
|
|
2778
2851
|
setTimeout(() => {
|
|
2852
|
+
if (this._suppressBlurClose) {
|
|
2853
|
+
// another pointerdown inside options is in progress; keep open
|
|
2854
|
+
return;
|
|
2855
|
+
}
|
|
2779
2856
|
const active = document.activeElement;
|
|
2780
2857
|
if (active && (this._shadow.contains(active) || this._container.contains(active))) {
|
|
2781
2858
|
return;
|
|
@@ -2790,11 +2867,33 @@ class EnhancedSelect extends HTMLElement {
|
|
|
2790
2867
|
const query = e.target.value;
|
|
2791
2868
|
this._handleSearch(query);
|
|
2792
2869
|
});
|
|
2793
|
-
//
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2870
|
+
// If the user presses down inside the options container we should
|
|
2871
|
+
// temporarily suppress blur-based closing until after the click has
|
|
2872
|
+
// been handled. Setting a flag that gets cleared in the next tick is
|
|
2873
|
+
// sufficient.
|
|
2874
|
+
this._optionsContainer.addEventListener('pointerdown', () => {
|
|
2875
|
+
this._suppressBlurClose = true;
|
|
2876
|
+
setTimeout(() => {
|
|
2877
|
+
this._suppressBlurClose = false;
|
|
2878
|
+
}, 0);
|
|
2879
|
+
});
|
|
2880
|
+
// Delegated click listener for improved event handling (robust across shadow DOM)
|
|
2881
|
+
const handleOptionEvent = (e) => {
|
|
2882
|
+
const path = (e.composedPath && e.composedPath()) || [e.target];
|
|
2883
|
+
let option = null;
|
|
2884
|
+
for (const node of path) {
|
|
2885
|
+
if (!(node instanceof Element))
|
|
2886
|
+
continue;
|
|
2887
|
+
try {
|
|
2888
|
+
if (node.matches('[data-sm-selectable], [data-selectable], [data-sm-state]')) {
|
|
2889
|
+
option = node;
|
|
2890
|
+
break;
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
catch (err) {
|
|
2894
|
+
continue;
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2798
2897
|
if (option && !option.hasAttribute('aria-disabled')) {
|
|
2799
2898
|
const indexStr = option.getAttribute('data-sm-index') ?? option.getAttribute('data-index');
|
|
2800
2899
|
const index = Number(indexStr);
|
|
@@ -2805,13 +2904,45 @@ class EnhancedSelect extends HTMLElement {
|
|
|
2805
2904
|
});
|
|
2806
2905
|
}
|
|
2807
2906
|
}
|
|
2808
|
-
}
|
|
2907
|
+
};
|
|
2908
|
+
this._optionsContainer.addEventListener('click', handleOptionEvent);
|
|
2909
|
+
// also watch pointerup to catch cases where the pointerdown started outside
|
|
2910
|
+
// (e.g. on the input) and the click never fires
|
|
2911
|
+
this._optionsContainer.addEventListener('pointerup', handleOptionEvent);
|
|
2809
2912
|
// Keyboard navigation
|
|
2810
2913
|
this._input.addEventListener('keydown', (e) => this._handleKeydown(e));
|
|
2811
|
-
// Click outside to close
|
|
2914
|
+
// Click outside to close — robust detection across shadow DOM and custom renderers
|
|
2812
2915
|
document.addEventListener('pointerdown', (e) => {
|
|
2813
2916
|
const path = (e.composedPath && e.composedPath()) || [];
|
|
2814
|
-
|
|
2917
|
+
let clickedInside = false;
|
|
2918
|
+
for (const node of path) {
|
|
2919
|
+
if (node === this || node === this._container) {
|
|
2920
|
+
clickedInside = true;
|
|
2921
|
+
break;
|
|
2922
|
+
}
|
|
2923
|
+
if (node instanceof Node) {
|
|
2924
|
+
try {
|
|
2925
|
+
if (this._shadow && this._shadow.contains(node)) {
|
|
2926
|
+
clickedInside = true;
|
|
2927
|
+
break;
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
catch (err) {
|
|
2931
|
+
// ignore
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
if (node instanceof Element) {
|
|
2935
|
+
try {
|
|
2936
|
+
if (node.matches('[data-sm-selectable], [data-selectable], [data-sm-state], .input-container, .select-container, .dropdown-arrow-container, .clear-control-button')) {
|
|
2937
|
+
clickedInside = true;
|
|
2938
|
+
break;
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
catch (err) {
|
|
2942
|
+
// ignore
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2815
2946
|
if (!clickedInside) {
|
|
2816
2947
|
this._handleClose();
|
|
2817
2948
|
}
|
|
@@ -2860,6 +2991,16 @@ class EnhancedSelect extends HTMLElement {
|
|
|
2860
2991
|
_handleOpen() {
|
|
2861
2992
|
if (!this._config.enabled || this._state.isOpen)
|
|
2862
2993
|
return;
|
|
2994
|
+
// close any other open selects before proceeding
|
|
2995
|
+
EnhancedSelect._instances.forEach(inst => {
|
|
2996
|
+
if (inst !== this)
|
|
2997
|
+
inst._handleClose();
|
|
2998
|
+
});
|
|
2999
|
+
// Always focus the input when opening so callers (arrow click,
|
|
3000
|
+
// programmatic `open()`, etc.) get the keyboard cursor. This was a
|
|
3001
|
+
// frequent source of confusion in #14 where people opened the dropdown
|
|
3002
|
+
// but the text field never received focus.
|
|
3003
|
+
this._input.focus();
|
|
2863
3004
|
this._markOpenStart();
|
|
2864
3005
|
this._state.isOpen = true;
|
|
2865
3006
|
this._dropdown.style.display = 'block';
|
|
@@ -3275,13 +3416,14 @@ class EnhancedSelect extends HTMLElement {
|
|
|
3275
3416
|
// FIX: Do not rely on this._optionsContainer.children[index] because filtering changes the children
|
|
3276
3417
|
// Instead, use the index to update state directly
|
|
3277
3418
|
const item = this._state.loadedItems[index];
|
|
3419
|
+
// Debug: log selection attempt
|
|
3278
3420
|
if (!item)
|
|
3279
3421
|
return;
|
|
3422
|
+
const isCurrentlySelected = this._state.selectedIndices.has(index);
|
|
3280
3423
|
// Keep active/focus styling aligned with the most recently interacted option.
|
|
3281
3424
|
// Without this, a previously selected item may retain active classes/styles
|
|
3282
3425
|
// after selecting a different option.
|
|
3283
3426
|
this._state.activeIndex = index;
|
|
3284
|
-
const isCurrentlySelected = this._state.selectedIndices.has(index);
|
|
3285
3427
|
if (this._config.selection.mode === 'single') {
|
|
3286
3428
|
// Single select: clear previous and select new
|
|
3287
3429
|
const wasSelected = this._state.selectedIndices.has(index);
|
|
@@ -3512,6 +3654,7 @@ class EnhancedSelect extends HTMLElement {
|
|
|
3512
3654
|
const getValue = this._config.serverSide.getValueFromItem || ((item) => item?.value ?? item);
|
|
3513
3655
|
const selectedValues = selectedItems.map(getValue);
|
|
3514
3656
|
const selectedIndices = Array.from(this._state.selectedIndices);
|
|
3657
|
+
// Debug: log change payload
|
|
3515
3658
|
this._emit('change', { selectedItems, selectedValues, selectedIndices });
|
|
3516
3659
|
this._config.callbacks.onChange?.(selectedItems, selectedValues);
|
|
3517
3660
|
}
|
|
@@ -3831,23 +3974,25 @@ class EnhancedSelect extends HTMLElement {
|
|
|
3831
3974
|
const query = this._state.searchQuery.toLowerCase();
|
|
3832
3975
|
// Handle Grouped Items Rendering (when no search query)
|
|
3833
3976
|
if (this._state.groupedItems.length > 0 && !query) {
|
|
3834
|
-
this._state.groupedItems.forEach(group => {
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
}
|
|
3977
|
+
this._state.groupedItems.forEach((group, groupIndex) => {
|
|
3978
|
+
let header;
|
|
3979
|
+
if (this.groupHeaderRenderer) {
|
|
3980
|
+
header = this.groupHeaderRenderer(group, groupIndex);
|
|
3981
|
+
// make sure the returned element has the correct semantics so
|
|
3982
|
+
// people can style it. we add the class/part even if the renderer
|
|
3983
|
+
// returned something else to ensure backward compatibility.
|
|
3984
|
+
if (!(header instanceof HTMLElement)) {
|
|
3985
|
+
// fall back to default if API is misused
|
|
3986
|
+
header = document.createElement('div');
|
|
3987
|
+
header.textContent = String(group.label);
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
else {
|
|
3991
|
+
header = document.createElement('div');
|
|
3992
|
+
header.textContent = group.label;
|
|
3993
|
+
}
|
|
3994
|
+
header.classList.add('group-header');
|
|
3995
|
+
header.setAttribute('part', 'group-header');
|
|
3851
3996
|
this._optionsContainer.appendChild(header);
|
|
3852
3997
|
group.options.forEach(item => {
|
|
3853
3998
|
// Find original index for correct ID generation and selection
|
|
@@ -4120,6 +4265,8 @@ class EnhancedSelect extends HTMLElement {
|
|
|
4120
4265
|
}
|
|
4121
4266
|
}
|
|
4122
4267
|
}
|
|
4268
|
+
/** live set of all connected instances; used to auto-close siblings */
|
|
4269
|
+
EnhancedSelect._instances = new Set();
|
|
4123
4270
|
// Register custom element
|
|
4124
4271
|
if (!customElements.get('enhanced-select')) {
|
|
4125
4272
|
customElements.define('enhanced-select', EnhancedSelect);
|