@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.umd.js
CHANGED
|
@@ -1879,7 +1879,22 @@
|
|
|
1879
1879
|
}
|
|
1880
1880
|
set classMap(map) {
|
|
1881
1881
|
this._classMap = map;
|
|
1882
|
-
this._setGlobalStylesMirroring(Boolean(this._optionRenderer || map));
|
|
1882
|
+
this._setGlobalStylesMirroring(Boolean(this._optionRenderer || map || this._groupHeaderRenderer));
|
|
1883
|
+
if (!this.isConnected)
|
|
1884
|
+
return;
|
|
1885
|
+
this._renderOptions();
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* DOM-based renderer for group headers. When provided, the component will
|
|
1889
|
+
* call this function for each group during rendering. The returned element
|
|
1890
|
+
* will receive `.group-header` and `part="group-header"` automatically.
|
|
1891
|
+
*/
|
|
1892
|
+
get groupHeaderRenderer() {
|
|
1893
|
+
return this._groupHeaderRenderer;
|
|
1894
|
+
}
|
|
1895
|
+
set groupHeaderRenderer(renderer) {
|
|
1896
|
+
this._groupHeaderRenderer = renderer;
|
|
1897
|
+
this._setGlobalStylesMirroring(Boolean(this._optionRenderer || this._classMap || renderer));
|
|
1883
1898
|
if (!this.isConnected)
|
|
1884
1899
|
return;
|
|
1885
1900
|
this._renderOptions();
|
|
@@ -1938,6 +1953,8 @@
|
|
|
1938
1953
|
this._initializeObservers();
|
|
1939
1954
|
}
|
|
1940
1955
|
connectedCallback() {
|
|
1956
|
+
// register instance
|
|
1957
|
+
EnhancedSelect._instances.add(this);
|
|
1941
1958
|
// WORKAROUND: Force display style on host element for Angular compatibility
|
|
1942
1959
|
// Angular's rendering seems to not apply :host styles correctly in some cases
|
|
1943
1960
|
// Must be done in connectedCallback when element is attached to DOM
|
|
@@ -1956,6 +1973,8 @@
|
|
|
1956
1973
|
}
|
|
1957
1974
|
}
|
|
1958
1975
|
disconnectedCallback() {
|
|
1976
|
+
// unregister instance
|
|
1977
|
+
EnhancedSelect._instances.delete(this);
|
|
1959
1978
|
// Cleanup observers
|
|
1960
1979
|
this._resizeObserver?.disconnect();
|
|
1961
1980
|
this._intersectionObserver?.disconnect();
|
|
@@ -2241,6 +2260,8 @@
|
|
|
2241
2260
|
right: 0;
|
|
2242
2261
|
bottom: 0;
|
|
2243
2262
|
width: var(--select-arrow-width, 40px);
|
|
2263
|
+
/* allow explicit height override even though container normally stretches */
|
|
2264
|
+
height: var(--select-arrow-height, auto);
|
|
2244
2265
|
display: flex;
|
|
2245
2266
|
align-items: center;
|
|
2246
2267
|
justify-content: center;
|
|
@@ -2306,9 +2327,14 @@
|
|
|
2306
2327
|
background-color: var(--select-arrow-hover-bg, rgba(102, 126, 234, 0.08));
|
|
2307
2328
|
}
|
|
2308
2329
|
|
|
2330
|
+
.dropdown-arrow:hover {
|
|
2331
|
+
/* legacy alias --select-arrow-hover for icon color */
|
|
2332
|
+
color: var(--select-arrow-hover, var(--select-arrow-hover-color, #667eea));
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2309
2335
|
.dropdown-arrow {
|
|
2310
|
-
width: var(--select-arrow-size, 16px);
|
|
2311
|
-
height: var(--select-arrow-size, 16px);
|
|
2336
|
+
width: var(--select-arrow-width, var(--select-arrow-size, 16px));
|
|
2337
|
+
height: var(--select-arrow-height, var(--select-arrow-size, 16px));
|
|
2312
2338
|
color: var(--select-arrow-color, #667eea);
|
|
2313
2339
|
transition: transform 0.2s ease, color 0.2s ease;
|
|
2314
2340
|
transform: translateY(0);
|
|
@@ -2417,6 +2443,20 @@
|
|
|
2417
2443
|
background: var(--select-options-bg, var(--select-dropdown-bg, var(--select-bg, white)));
|
|
2418
2444
|
}
|
|
2419
2445
|
|
|
2446
|
+
.group-header {
|
|
2447
|
+
padding: var(--select-group-header-padding, 8px 12px);
|
|
2448
|
+
font-weight: var(--select-group-header-weight, 600);
|
|
2449
|
+
color: var(--select-group-header-color, #6b7280);
|
|
2450
|
+
background-color: var(--select-group-header-bg, #f3f4f6);
|
|
2451
|
+
font-size: var(--select-group-header-font-size, 12px);
|
|
2452
|
+
text-transform: var(--select-group-header-text-transform, uppercase);
|
|
2453
|
+
letter-spacing: var(--select-group-header-letter-spacing, 0.05em);
|
|
2454
|
+
position: sticky;
|
|
2455
|
+
top: 0;
|
|
2456
|
+
z-index: 1;
|
|
2457
|
+
border-bottom: var(--select-group-header-border-bottom, 1px solid #e5e7eb);
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2420
2460
|
.option {
|
|
2421
2461
|
padding: var(--select-option-padding, 8px 12px);
|
|
2422
2462
|
cursor: pointer;
|
|
@@ -2590,9 +2630,12 @@
|
|
|
2590
2630
|
}
|
|
2591
2631
|
}
|
|
2592
2632
|
|
|
2593
|
-
/* Dark mode - Opt-in via class
|
|
2633
|
+
/* Dark mode - Opt-in via class, data attribute, or ancestor context */
|
|
2594
2634
|
:host(.dark-mode),
|
|
2595
|
-
:host([data-theme="dark"])
|
|
2635
|
+
:host([data-theme="dark"]),
|
|
2636
|
+
:host-context(.dark-mode),
|
|
2637
|
+
:host-context(.dark),
|
|
2638
|
+
:host-context([data-theme="dark"]) {
|
|
2596
2639
|
.input-container {
|
|
2597
2640
|
background: var(--select-dark-bg, #1f2937);
|
|
2598
2641
|
border-color: var(--select-dark-border, #4b5563);
|
|
@@ -2645,6 +2688,13 @@
|
|
|
2645
2688
|
|
|
2646
2689
|
.option.active:not(.selected) {
|
|
2647
2690
|
background-color: var(--select-dark-option-active-bg, #374151);
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
/* Group header in dark mode */
|
|
2694
|
+
.group-header {
|
|
2695
|
+
color: var(--select-dark-group-header-color, var(--select-group-header-color, #6b7280));
|
|
2696
|
+
background-color: var(--select-dark-group-header-bg, var(--select-group-header-bg, #374151));
|
|
2697
|
+
}
|
|
2648
2698
|
color: var(--select-dark-option-active-color, #f9fafb);
|
|
2649
2699
|
outline: var(--select-dark-option-active-outline, 2px solid rgba(129, 140, 248, 0.55));
|
|
2650
2700
|
}
|
|
@@ -2727,19 +2777,14 @@
|
|
|
2727
2777
|
this._boundArrowClick = (e) => {
|
|
2728
2778
|
e.stopPropagation();
|
|
2729
2779
|
e.preventDefault();
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
this.
|
|
2734
|
-
|
|
2735
|
-
this._config.callbacks.onOpen();
|
|
2736
|
-
}
|
|
2737
|
-
else if (!this._state.isOpen && this._config.callbacks.onClose) {
|
|
2738
|
-
this._config.callbacks.onClose();
|
|
2780
|
+
// delegate to the existing open/close helpers so we don't accidentally
|
|
2781
|
+
// drift out of sync with the logic in those methods (focus, events,
|
|
2782
|
+
// scroll-to-selected, etc.)
|
|
2783
|
+
if (this._state.isOpen) {
|
|
2784
|
+
this._handleClose();
|
|
2739
2785
|
}
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
setTimeout(() => this._scrollToSelected(), 50);
|
|
2786
|
+
else {
|
|
2787
|
+
this._handleOpen();
|
|
2743
2788
|
}
|
|
2744
2789
|
};
|
|
2745
2790
|
this._arrowContainer.addEventListener('click', this._boundArrowClick);
|
|
@@ -2756,7 +2801,12 @@
|
|
|
2756
2801
|
});
|
|
2757
2802
|
}
|
|
2758
2803
|
// Input container click - focus input and open dropdown
|
|
2804
|
+
// Prevent the original pointer event from bubbling/causing default focus behavior
|
|
2805
|
+
// which can interfere with option click handling when opening the dropdown
|
|
2759
2806
|
this._inputContainer.addEventListener('pointerdown', (e) => {
|
|
2807
|
+
// Prevent propagation to document click listener but do NOT preventDefault.
|
|
2808
|
+
// Allow default so browser events (click) on newly opened options still fire.
|
|
2809
|
+
e.stopPropagation();
|
|
2760
2810
|
const target = e.target;
|
|
2761
2811
|
if (!this._config.enabled)
|
|
2762
2812
|
return;
|
|
@@ -2764,10 +2814,27 @@
|
|
|
2764
2814
|
return;
|
|
2765
2815
|
if (target && target.closest('.clear-control-button'))
|
|
2766
2816
|
return;
|
|
2767
|
-
|
|
2817
|
+
const wasClosed = !this._state.isOpen;
|
|
2818
|
+
if (wasClosed) {
|
|
2768
2819
|
this._handleOpen();
|
|
2769
2820
|
}
|
|
2821
|
+
else {
|
|
2822
|
+
// clicking the input while open should close the dropdown too
|
|
2823
|
+
this._handleClose();
|
|
2824
|
+
}
|
|
2825
|
+
// Focus the input (do not prevent default behavior)
|
|
2770
2826
|
this._input.focus();
|
|
2827
|
+
// If we just opened the dropdown, transfer pointer capture to the
|
|
2828
|
+
// options container so the subsequent pointerup lands there instead of
|
|
2829
|
+
// staying with the input container (which would swallow the event).
|
|
2830
|
+
if (wasClosed && this._optionsContainer && typeof e.pointerId === 'number') {
|
|
2831
|
+
try {
|
|
2832
|
+
this._optionsContainer.setPointerCapture(e.pointerId);
|
|
2833
|
+
}
|
|
2834
|
+
catch (_err) {
|
|
2835
|
+
// Some browsers may throw if element is not yet "connected"; ignore
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2771
2838
|
});
|
|
2772
2839
|
// Input container click - prevent event from reaching document listener
|
|
2773
2840
|
this._container.addEventListener('click', (e) => {
|
|
@@ -2775,6 +2842,12 @@
|
|
|
2775
2842
|
});
|
|
2776
2843
|
// Input focus/blur
|
|
2777
2844
|
this._input.addEventListener('focus', () => this._handleOpen());
|
|
2845
|
+
// When the input loses focus we normally close the dropdown, but
|
|
2846
|
+
// clicking an option will blur the input before the option's click
|
|
2847
|
+
// handler executes. To avoid the blur timer closing the dropdown
|
|
2848
|
+
// prematurely we use a short-lived flag that is set whenever we start
|
|
2849
|
+
// interacting with the options container. The close callback checks this
|
|
2850
|
+
// flag and skips closing if the user is about to click an option.
|
|
2778
2851
|
this._input.addEventListener('blur', (e) => {
|
|
2779
2852
|
const related = e.relatedTarget;
|
|
2780
2853
|
if (related && (this._shadow.contains(related) || this._container.contains(related))) {
|
|
@@ -2782,6 +2855,10 @@
|
|
|
2782
2855
|
}
|
|
2783
2856
|
// Delay to allow option click/focus transitions
|
|
2784
2857
|
setTimeout(() => {
|
|
2858
|
+
if (this._suppressBlurClose) {
|
|
2859
|
+
// another pointerdown inside options is in progress; keep open
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2785
2862
|
const active = document.activeElement;
|
|
2786
2863
|
if (active && (this._shadow.contains(active) || this._container.contains(active))) {
|
|
2787
2864
|
return;
|
|
@@ -2796,11 +2873,33 @@
|
|
|
2796
2873
|
const query = e.target.value;
|
|
2797
2874
|
this._handleSearch(query);
|
|
2798
2875
|
});
|
|
2799
|
-
//
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2876
|
+
// If the user presses down inside the options container we should
|
|
2877
|
+
// temporarily suppress blur-based closing until after the click has
|
|
2878
|
+
// been handled. Setting a flag that gets cleared in the next tick is
|
|
2879
|
+
// sufficient.
|
|
2880
|
+
this._optionsContainer.addEventListener('pointerdown', () => {
|
|
2881
|
+
this._suppressBlurClose = true;
|
|
2882
|
+
setTimeout(() => {
|
|
2883
|
+
this._suppressBlurClose = false;
|
|
2884
|
+
}, 0);
|
|
2885
|
+
});
|
|
2886
|
+
// Delegated click listener for improved event handling (robust across shadow DOM)
|
|
2887
|
+
const handleOptionEvent = (e) => {
|
|
2888
|
+
const path = (e.composedPath && e.composedPath()) || [e.target];
|
|
2889
|
+
let option = null;
|
|
2890
|
+
for (const node of path) {
|
|
2891
|
+
if (!(node instanceof Element))
|
|
2892
|
+
continue;
|
|
2893
|
+
try {
|
|
2894
|
+
if (node.matches('[data-sm-selectable], [data-selectable], [data-sm-state]')) {
|
|
2895
|
+
option = node;
|
|
2896
|
+
break;
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
catch (err) {
|
|
2900
|
+
continue;
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2804
2903
|
if (option && !option.hasAttribute('aria-disabled')) {
|
|
2805
2904
|
const indexStr = option.getAttribute('data-sm-index') ?? option.getAttribute('data-index');
|
|
2806
2905
|
const index = Number(indexStr);
|
|
@@ -2811,13 +2910,45 @@
|
|
|
2811
2910
|
});
|
|
2812
2911
|
}
|
|
2813
2912
|
}
|
|
2814
|
-
}
|
|
2913
|
+
};
|
|
2914
|
+
this._optionsContainer.addEventListener('click', handleOptionEvent);
|
|
2915
|
+
// also watch pointerup to catch cases where the pointerdown started outside
|
|
2916
|
+
// (e.g. on the input) and the click never fires
|
|
2917
|
+
this._optionsContainer.addEventListener('pointerup', handleOptionEvent);
|
|
2815
2918
|
// Keyboard navigation
|
|
2816
2919
|
this._input.addEventListener('keydown', (e) => this._handleKeydown(e));
|
|
2817
|
-
// Click outside to close
|
|
2920
|
+
// Click outside to close — robust detection across shadow DOM and custom renderers
|
|
2818
2921
|
document.addEventListener('pointerdown', (e) => {
|
|
2819
2922
|
const path = (e.composedPath && e.composedPath()) || [];
|
|
2820
|
-
|
|
2923
|
+
let clickedInside = false;
|
|
2924
|
+
for (const node of path) {
|
|
2925
|
+
if (node === this || node === this._container) {
|
|
2926
|
+
clickedInside = true;
|
|
2927
|
+
break;
|
|
2928
|
+
}
|
|
2929
|
+
if (node instanceof Node) {
|
|
2930
|
+
try {
|
|
2931
|
+
if (this._shadow && this._shadow.contains(node)) {
|
|
2932
|
+
clickedInside = true;
|
|
2933
|
+
break;
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
catch (err) {
|
|
2937
|
+
// ignore
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
if (node instanceof Element) {
|
|
2941
|
+
try {
|
|
2942
|
+
if (node.matches('[data-sm-selectable], [data-selectable], [data-sm-state], .input-container, .select-container, .dropdown-arrow-container, .clear-control-button')) {
|
|
2943
|
+
clickedInside = true;
|
|
2944
|
+
break;
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
catch (err) {
|
|
2948
|
+
// ignore
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2821
2952
|
if (!clickedInside) {
|
|
2822
2953
|
this._handleClose();
|
|
2823
2954
|
}
|
|
@@ -2866,6 +2997,16 @@
|
|
|
2866
2997
|
_handleOpen() {
|
|
2867
2998
|
if (!this._config.enabled || this._state.isOpen)
|
|
2868
2999
|
return;
|
|
3000
|
+
// close any other open selects before proceeding
|
|
3001
|
+
EnhancedSelect._instances.forEach(inst => {
|
|
3002
|
+
if (inst !== this)
|
|
3003
|
+
inst._handleClose();
|
|
3004
|
+
});
|
|
3005
|
+
// Always focus the input when opening so callers (arrow click,
|
|
3006
|
+
// programmatic `open()`, etc.) get the keyboard cursor. This was a
|
|
3007
|
+
// frequent source of confusion in #14 where people opened the dropdown
|
|
3008
|
+
// but the text field never received focus.
|
|
3009
|
+
this._input.focus();
|
|
2869
3010
|
this._markOpenStart();
|
|
2870
3011
|
this._state.isOpen = true;
|
|
2871
3012
|
this._dropdown.style.display = 'block';
|
|
@@ -3281,13 +3422,14 @@
|
|
|
3281
3422
|
// FIX: Do not rely on this._optionsContainer.children[index] because filtering changes the children
|
|
3282
3423
|
// Instead, use the index to update state directly
|
|
3283
3424
|
const item = this._state.loadedItems[index];
|
|
3425
|
+
// Debug: log selection attempt
|
|
3284
3426
|
if (!item)
|
|
3285
3427
|
return;
|
|
3428
|
+
const isCurrentlySelected = this._state.selectedIndices.has(index);
|
|
3286
3429
|
// Keep active/focus styling aligned with the most recently interacted option.
|
|
3287
3430
|
// Without this, a previously selected item may retain active classes/styles
|
|
3288
3431
|
// after selecting a different option.
|
|
3289
3432
|
this._state.activeIndex = index;
|
|
3290
|
-
const isCurrentlySelected = this._state.selectedIndices.has(index);
|
|
3291
3433
|
if (this._config.selection.mode === 'single') {
|
|
3292
3434
|
// Single select: clear previous and select new
|
|
3293
3435
|
const wasSelected = this._state.selectedIndices.has(index);
|
|
@@ -3518,6 +3660,7 @@
|
|
|
3518
3660
|
const getValue = this._config.serverSide.getValueFromItem || ((item) => item?.value ?? item);
|
|
3519
3661
|
const selectedValues = selectedItems.map(getValue);
|
|
3520
3662
|
const selectedIndices = Array.from(this._state.selectedIndices);
|
|
3663
|
+
// Debug: log change payload
|
|
3521
3664
|
this._emit('change', { selectedItems, selectedValues, selectedIndices });
|
|
3522
3665
|
this._config.callbacks.onChange?.(selectedItems, selectedValues);
|
|
3523
3666
|
}
|
|
@@ -3837,23 +3980,25 @@
|
|
|
3837
3980
|
const query = this._state.searchQuery.toLowerCase();
|
|
3838
3981
|
// Handle Grouped Items Rendering (when no search query)
|
|
3839
3982
|
if (this._state.groupedItems.length > 0 && !query) {
|
|
3840
|
-
this._state.groupedItems.forEach(group => {
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
}
|
|
3983
|
+
this._state.groupedItems.forEach((group, groupIndex) => {
|
|
3984
|
+
let header;
|
|
3985
|
+
if (this.groupHeaderRenderer) {
|
|
3986
|
+
header = this.groupHeaderRenderer(group, groupIndex);
|
|
3987
|
+
// make sure the returned element has the correct semantics so
|
|
3988
|
+
// people can style it. we add the class/part even if the renderer
|
|
3989
|
+
// returned something else to ensure backward compatibility.
|
|
3990
|
+
if (!(header instanceof HTMLElement)) {
|
|
3991
|
+
// fall back to default if API is misused
|
|
3992
|
+
header = document.createElement('div');
|
|
3993
|
+
header.textContent = String(group.label);
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
else {
|
|
3997
|
+
header = document.createElement('div');
|
|
3998
|
+
header.textContent = group.label;
|
|
3999
|
+
}
|
|
4000
|
+
header.classList.add('group-header');
|
|
4001
|
+
header.setAttribute('part', 'group-header');
|
|
3857
4002
|
this._optionsContainer.appendChild(header);
|
|
3858
4003
|
group.options.forEach(item => {
|
|
3859
4004
|
// Find original index for correct ID generation and selection
|
|
@@ -4126,6 +4271,8 @@
|
|
|
4126
4271
|
}
|
|
4127
4272
|
}
|
|
4128
4273
|
}
|
|
4274
|
+
/** live set of all connected instances; used to auto-close siblings */
|
|
4275
|
+
EnhancedSelect._instances = new Set();
|
|
4129
4276
|
// Register custom element
|
|
4130
4277
|
if (!customElements.get('enhanced-select')) {
|
|
4131
4278
|
customElements.define('enhanced-select', EnhancedSelect);
|