@smilodon/core 1.4.9 → 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/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);
@@ -2662,6 +2688,13 @@
2662
2688
 
2663
2689
  .option.active:not(.selected) {
2664
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
+ }
2665
2698
  color: var(--select-dark-option-active-color, #f9fafb);
2666
2699
  outline: var(--select-dark-option-active-outline, 2px solid rgba(129, 140, 248, 0.55));
2667
2700
  }
@@ -2744,19 +2777,14 @@
2744
2777
  this._boundArrowClick = (e) => {
2745
2778
  e.stopPropagation();
2746
2779
  e.preventDefault();
2747
- const wasOpen = this._state.isOpen;
2748
- this._state.isOpen = !this._state.isOpen;
2749
- this._updateDropdownVisibility();
2750
- this._updateArrowRotation();
2751
- if (this._state.isOpen && this._config.callbacks.onOpen) {
2752
- this._config.callbacks.onOpen();
2753
- }
2754
- else if (!this._state.isOpen && this._config.callbacks.onClose) {
2755
- 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();
2756
2785
  }
2757
- // Scroll to selected when opening
2758
- if (!wasOpen && this._state.isOpen && this._state.selectedIndices.size > 0) {
2759
- setTimeout(() => this._scrollToSelected(), 50);
2786
+ else {
2787
+ this._handleOpen();
2760
2788
  }
2761
2789
  };
2762
2790
  this._arrowContainer.addEventListener('click', this._boundArrowClick);
@@ -2790,6 +2818,10 @@
2790
2818
  if (wasClosed) {
2791
2819
  this._handleOpen();
2792
2820
  }
2821
+ else {
2822
+ // clicking the input while open should close the dropdown too
2823
+ this._handleClose();
2824
+ }
2793
2825
  // Focus the input (do not prevent default behavior)
2794
2826
  this._input.focus();
2795
2827
  // If we just opened the dropdown, transfer pointer capture to the
@@ -2965,6 +2997,16 @@
2965
2997
  _handleOpen() {
2966
2998
  if (!this._config.enabled || this._state.isOpen)
2967
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();
2968
3010
  this._markOpenStart();
2969
3011
  this._state.isOpen = true;
2970
3012
  this._dropdown.style.display = 'block';
@@ -3938,10 +3980,25 @@
3938
3980
  const query = this._state.searchQuery.toLowerCase();
3939
3981
  // Handle Grouped Items Rendering (when no search query)
3940
3982
  if (this._state.groupedItems.length > 0 && !query) {
3941
- this._state.groupedItems.forEach(group => {
3942
- const header = document.createElement('div');
3943
- header.className = 'group-header';
3944
- header.textContent = group.label;
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');
3945
4002
  this._optionsContainer.appendChild(header);
3946
4003
  group.options.forEach(item => {
3947
4004
  // Find original index for correct ID generation and selection
@@ -4214,6 +4271,8 @@
4214
4271
  }
4215
4272
  }
4216
4273
  }
4274
+ /** live set of all connected instances; used to auto-close siblings */
4275
+ EnhancedSelect._instances = new Set();
4217
4276
  // Register custom element
4218
4277
  if (!customElements.get('enhanced-select')) {
4219
4278
  customElements.define('enhanced-select', EnhancedSelect);