@smilodon/core 1.3.13 → 1.4.1

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.cjs CHANGED
@@ -1557,16 +1557,55 @@ class SelectOption extends HTMLElement {
1557
1557
  this._shadow.appendChild(style);
1558
1558
  }
1559
1559
  _render() {
1560
- const { item, index, selected, disabled, active, render, showRemoveButton } = this._config;
1560
+ const { item, index, selected, disabled, active, render, showRemoveButton, classMap } = this._config;
1561
1561
  // Clear container
1562
1562
  this._container.innerHTML = '';
1563
- // Apply state classes
1564
- this._container.classList.toggle('selected', selected);
1565
- this._container.classList.toggle('disabled', disabled || false);
1566
- this._container.classList.toggle('active', active || false);
1563
+ // Add part attribute
1564
+ this._container.setAttribute('part', 'option');
1565
+ // Standard styling hook
1566
+ this._container.classList.add('smilodon-option');
1567
+ // Apply state classes using classMap or defaults
1568
+ const selectedClasses = (classMap?.selected ?? 'selected sm-selected').split(' ').filter(Boolean);
1569
+ const activeClasses = (classMap?.active ?? 'active sm-active').split(' ').filter(Boolean);
1570
+ const disabledClasses = (classMap?.disabled ?? 'disabled sm-disabled').split(' ').filter(Boolean);
1571
+ // Apply classes to both the container (internal styling) and the host (external styling/::part)
1572
+ // This ensures that utility classes are visible via ::part selectors
1573
+ const toggleClasses = (element, classes, add) => {
1574
+ if (add) {
1575
+ element.classList.add(...classes);
1576
+ }
1577
+ else {
1578
+ element.classList.remove(...classes);
1579
+ }
1580
+ };
1581
+ if (selected) {
1582
+ toggleClasses(this._container, [...selectedClasses, 'smilodon-option--selected'], true);
1583
+ toggleClasses(this, [...selectedClasses, 'smilodon-option--selected'], true);
1584
+ }
1585
+ else {
1586
+ toggleClasses(this._container, [...selectedClasses, 'smilodon-option--selected'], false);
1587
+ toggleClasses(this, [...selectedClasses, 'smilodon-option--selected'], false);
1588
+ }
1589
+ if (active) {
1590
+ toggleClasses(this._container, [...activeClasses, 'smilodon-option--active'], true);
1591
+ toggleClasses(this, [...activeClasses, 'smilodon-option--active'], true); // Make focus ring visible on host
1592
+ }
1593
+ else {
1594
+ toggleClasses(this._container, [...activeClasses, 'smilodon-option--active'], false);
1595
+ toggleClasses(this, [...activeClasses, 'smilodon-option--active'], false);
1596
+ }
1597
+ if (disabled) {
1598
+ toggleClasses(this._container, [...disabledClasses, 'smilodon-option--disabled'], true);
1599
+ toggleClasses(this, [...disabledClasses, 'smilodon-option--disabled'], true);
1600
+ }
1601
+ else {
1602
+ toggleClasses(this._container, [...disabledClasses, 'smilodon-option--disabled'], false);
1603
+ toggleClasses(this, [...disabledClasses, 'smilodon-option--disabled'], false);
1604
+ }
1567
1605
  // Custom class name
1568
1606
  if (this._config.className) {
1569
- this._container.className += ' ' + this._config.className;
1607
+ const classes = this._config.className.split(' ').filter(Boolean);
1608
+ this._container.classList.add(...classes);
1570
1609
  }
1571
1610
  // Apply custom styles
1572
1611
  if (this._config.style) {
@@ -1575,12 +1614,13 @@ class SelectOption extends HTMLElement {
1575
1614
  // Render content
1576
1615
  const contentDiv = document.createElement('div');
1577
1616
  contentDiv.className = 'option-content';
1617
+ // contentDiv.setAttribute('part', 'option-content'); // Optional
1578
1618
  if (render) {
1579
1619
  const rendered = render(item, index);
1580
1620
  if (typeof rendered === 'string') {
1581
1621
  contentDiv.innerHTML = rendered;
1582
1622
  }
1583
- else {
1623
+ else if (rendered instanceof HTMLElement) {
1584
1624
  contentDiv.appendChild(rendered);
1585
1625
  }
1586
1626
  }
@@ -1594,16 +1634,56 @@ class SelectOption extends HTMLElement {
1594
1634
  this._removeButton = document.createElement('button');
1595
1635
  this._removeButton.className = 'remove-button';
1596
1636
  this._removeButton.innerHTML = '×';
1637
+ this._removeButton.setAttribute('part', 'chip-remove');
1597
1638
  this._removeButton.setAttribute('aria-label', 'Remove option');
1598
1639
  this._removeButton.setAttribute('type', 'button');
1599
1640
  this._container.appendChild(this._removeButton);
1600
1641
  }
1601
- // Set ARIA attributes
1642
+ // Set ARIA attributes and State attributes on Host
1602
1643
  this.setAttribute('role', 'option');
1603
1644
  this.setAttribute('aria-selected', String(selected));
1604
1645
  if (disabled)
1605
1646
  this.setAttribute('aria-disabled', 'true');
1606
1647
  this.id = this._config.id || `select-option-${index}`;
1648
+ // Add checkmark (part="checkmark") - standard for object mode
1649
+ // Only show if NOT showing remove button (avoid clutter)
1650
+ if (!showRemoveButton) {
1651
+ const checkmark = document.createElement('div');
1652
+ checkmark.setAttribute('part', 'checkmark');
1653
+ checkmark.className = 'checkmark-icon';
1654
+ checkmark.innerHTML = `
1655
+ <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:1em;height:1em;">
1656
+ <path d="M4 8.5L6.5 11L12 5.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1657
+ </svg>
1658
+ `;
1659
+ // Visibility control via CSS or inline style
1660
+ // We set it to display: none unless selected.
1661
+ // User can override this behavior via part styling if they want transitions
1662
+ if (!selected) {
1663
+ checkmark.style.display = 'none';
1664
+ }
1665
+ else {
1666
+ checkmark.style.marginLeft = '8px';
1667
+ checkmark.style.color = 'currentColor';
1668
+ }
1669
+ this._container.appendChild(checkmark);
1670
+ }
1671
+ // Data Attributes Contract on Host
1672
+ const state = [];
1673
+ if (selected)
1674
+ state.push('selected');
1675
+ if (active)
1676
+ state.push('active');
1677
+ if (state.length) {
1678
+ this.dataset.smState = state.join(' ');
1679
+ }
1680
+ else {
1681
+ delete this.dataset.smState;
1682
+ }
1683
+ this.dataset.smIndex = String(index);
1684
+ if (!this.hasAttribute('data-sm-selectable')) {
1685
+ this.toggleAttribute('data-sm-selectable', true);
1686
+ }
1607
1687
  }
1608
1688
  _attachEventListeners() {
1609
1689
  // Click handler for selection
@@ -1748,6 +1828,7 @@ class EnhancedSelect extends HTMLElement {
1748
1828
  this._pendingFirstRenderMark = false;
1749
1829
  this._pendingSearchRenderMark = false;
1750
1830
  this._rangeAnchorIndex = null;
1831
+ this._customOptionBoundElements = new WeakSet();
1751
1832
  this._shadow = this.attachShadow({ mode: 'open' });
1752
1833
  this._uniqueId = `enhanced-select-${Math.random().toString(36).substr(2, 9)}`;
1753
1834
  this._rendererHelpers = this._buildRendererHelpers();
@@ -1830,10 +1911,12 @@ class EnhancedSelect extends HTMLElement {
1830
1911
  _createInputContainer() {
1831
1912
  const container = document.createElement('div');
1832
1913
  container.className = 'input-container';
1914
+ container.setAttribute('part', 'button');
1833
1915
  return container;
1834
1916
  }
1835
1917
  _createInput() {
1836
1918
  const input = document.createElement('input');
1919
+ input.setAttribute('part', 'input');
1837
1920
  input.type = 'text';
1838
1921
  input.className = 'select-input';
1839
1922
  input.id = `${this._uniqueId}-input`;
@@ -1861,6 +1944,7 @@ class EnhancedSelect extends HTMLElement {
1861
1944
  _createDropdown() {
1862
1945
  const dropdown = document.createElement('div');
1863
1946
  dropdown.className = 'select-dropdown';
1947
+ dropdown.setAttribute('part', 'listbox');
1864
1948
  dropdown.style.display = 'none';
1865
1949
  if (this._config.styles.classNames?.dropdown) {
1866
1950
  dropdown.className += ' ' + this._config.styles.classNames.dropdown;
@@ -1892,7 +1976,7 @@ class EnhancedSelect extends HTMLElement {
1892
1976
  const container = document.createElement('div');
1893
1977
  container.className = 'dropdown-arrow-container';
1894
1978
  container.innerHTML = `
1895
- <svg class="dropdown-arrow" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
1979
+ <svg class="dropdown-arrow" part="arrow" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
1896
1980
  <path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1897
1981
  </svg>
1898
1982
  `;
@@ -2414,6 +2498,22 @@ class EnhancedSelect extends HTMLElement {
2414
2498
  const query = e.target.value;
2415
2499
  this._handleSearch(query);
2416
2500
  });
2501
+ // Delegated click listener for improved event handling (smart fallback)
2502
+ this._optionsContainer.addEventListener('click', (e) => {
2503
+ const target = e.target;
2504
+ // Handle option clicks
2505
+ const option = target.closest('[data-sm-selectable], [data-selectable], [data-sm-state]');
2506
+ if (option && !option.hasAttribute('aria-disabled')) {
2507
+ const indexStr = option.getAttribute('data-sm-index') ?? option.getAttribute('data-index');
2508
+ const index = Number(indexStr);
2509
+ if (!Number.isNaN(index)) {
2510
+ this._selectOption(index, {
2511
+ shiftKey: e.shiftKey,
2512
+ toggleKey: e.ctrlKey || e.metaKey,
2513
+ });
2514
+ }
2515
+ }
2516
+ });
2417
2517
  // Keyboard navigation
2418
2518
  this._input.addEventListener('keydown', (e) => this._handleKeydown(e));
2419
2519
  // Click outside to close
@@ -2986,10 +3086,12 @@ class EnhancedSelect extends HTMLElement {
2986
3086
  selectedEntries.forEach(([index, item]) => {
2987
3087
  const badge = document.createElement('span');
2988
3088
  badge.className = 'selection-badge';
3089
+ badge.setAttribute('part', 'chip');
2989
3090
  badge.textContent = getLabel(item);
2990
3091
  // Add remove button to badge
2991
3092
  const removeBtn = document.createElement('button');
2992
3093
  removeBtn.className = 'badge-remove';
3094
+ removeBtn.setAttribute('part', 'chip-remove');
2993
3095
  removeBtn.innerHTML = '×';
2994
3096
  removeBtn.setAttribute('aria-label', `Remove ${getLabel(item)}`);
2995
3097
  removeBtn.addEventListener('click', (e) => {
@@ -3357,6 +3459,7 @@ class EnhancedSelect extends HTMLElement {
3357
3459
  if (this._state.isSearching) {
3358
3460
  const searching = document.createElement('div');
3359
3461
  searching.className = 'searching-state';
3462
+ searching.setAttribute('part', 'loading');
3360
3463
  searching.textContent = 'Searching...';
3361
3464
  this._optionsContainer.appendChild(searching);
3362
3465
  return;
@@ -3414,6 +3517,7 @@ class EnhancedSelect extends HTMLElement {
3414
3517
  });
3415
3518
  if (!hasRenderedItems && !this._state.isBusy) {
3416
3519
  const empty = document.createElement('div');
3520
+ empty.setAttribute('part', 'no-results');
3417
3521
  empty.className = 'empty-state';
3418
3522
  if (query) {
3419
3523
  empty.textContent = `No results found for "${this._state.searchQuery}"`;
@@ -3427,6 +3531,7 @@ class EnhancedSelect extends HTMLElement {
3427
3531
  // Append Busy Indicator if busy
3428
3532
  if (this._state.isBusy && this._config.busyBucket.enabled) {
3429
3533
  const busyBucket = document.createElement('div');
3534
+ busyBucket.setAttribute('part', 'loading');
3430
3535
  busyBucket.className = 'busy-bucket';
3431
3536
  if (this._config.busyBucket.showSpinner) {
3432
3537
  const spinner = document.createElement('div');
@@ -3475,11 +3580,24 @@ class EnhancedSelect extends HTMLElement {
3475
3580
  getValue,
3476
3581
  getLabel,
3477
3582
  showRemoveButton: this._config.selection.mode === 'multi' && this._config.selection.showRemoveButton,
3583
+ classMap: this.classMap,
3478
3584
  });
3585
+ // Valid part attribute on the web component host itself
3586
+ option.setAttribute('part', 'option');
3479
3587
  option.dataset.index = String(index);
3480
3588
  option.dataset.value = String(getValue(item));
3589
+ // New standard attributes on Host
3590
+ option.dataset.smIndex = String(index);
3591
+ if (!option.hasAttribute('data-sm-selectable')) {
3592
+ option.toggleAttribute('data-sm-selectable', true);
3593
+ }
3594
+ const val = getValue(item);
3595
+ if (val != null) {
3596
+ option.dataset.smValue = String(val);
3597
+ }
3481
3598
  option.id = option.id || optionId;
3482
3599
  option.addEventListener('click', (e) => {
3600
+ e.stopPropagation(); // Prevent duplicate handling by delegation
3483
3601
  const mouseEvent = e;
3484
3602
  this._selectOption(index, {
3485
3603
  shiftKey: mouseEvent.shiftKey,
@@ -3495,35 +3613,71 @@ class EnhancedSelect extends HTMLElement {
3495
3613
  }
3496
3614
  _normalizeCustomOptionElement(element, meta) {
3497
3615
  const optionEl = element instanceof HTMLElement ? element : document.createElement('div');
3616
+ // Add part attribute for styling
3617
+ if (!optionEl.hasAttribute('part')) {
3618
+ optionEl.setAttribute('part', 'option');
3619
+ }
3498
3620
  // Add both semantic namespaced classes and the legacy internal classes that CSS uses
3499
3621
  optionEl.classList.add('smilodon-option', 'option');
3500
- // Toggle state classes
3622
+ // Toggle state classes using classMap if available
3501
3623
  const isSelected = meta.selected;
3502
3624
  const isActive = meta.active;
3503
3625
  const isDisabled = meta.disabled;
3626
+ // Resolve classes from classMap or defaults
3627
+ const selectedClasses = (this.classMap?.selected ?? 'selected sm-selected').split(' ').filter(Boolean);
3628
+ const activeClasses = (this.classMap?.active ?? 'active sm-active').split(' ').filter(Boolean);
3629
+ const disabledClasses = (this.classMap?.disabled ?? 'disabled sm-disabled').split(' ').filter(Boolean);
3504
3630
  if (isSelected) {
3505
- optionEl.classList.add('smilodon-option--selected', 'selected');
3631
+ optionEl.classList.add(...selectedClasses);
3632
+ optionEl.classList.add('smilodon-option--selected');
3506
3633
  }
3507
3634
  else {
3508
- optionEl.classList.remove('smilodon-option--selected', 'selected');
3635
+ optionEl.classList.remove(...selectedClasses);
3636
+ optionEl.classList.remove('smilodon-option--selected');
3509
3637
  }
3510
3638
  if (isActive) {
3511
- optionEl.classList.add('smilodon-option--active', 'active');
3639
+ optionEl.classList.add(...activeClasses);
3640
+ optionEl.classList.add('smilodon-option--active');
3512
3641
  }
3513
3642
  else {
3514
- optionEl.classList.remove('smilodon-option--active', 'active');
3643
+ optionEl.classList.remove(...activeClasses);
3644
+ optionEl.classList.remove('smilodon-option--active');
3515
3645
  }
3516
3646
  if (isDisabled) {
3517
- optionEl.classList.add('smilodon-option--disabled', 'disabled');
3647
+ optionEl.classList.add(...disabledClasses);
3648
+ optionEl.classList.add('smilodon-option--disabled');
3518
3649
  }
3519
3650
  else {
3520
- optionEl.classList.remove('smilodon-option--disabled', 'disabled');
3651
+ optionEl.classList.remove(...disabledClasses);
3652
+ optionEl.classList.remove('smilodon-option--disabled');
3521
3653
  }
3654
+ // Data Attributes Contract
3655
+ const state = [];
3656
+ if (isSelected)
3657
+ state.push('selected');
3658
+ if (isActive)
3659
+ state.push('active');
3660
+ if (state.length) {
3661
+ optionEl.dataset.smState = state.join(' ');
3662
+ }
3663
+ else {
3664
+ delete optionEl.dataset.smState;
3665
+ }
3666
+ // Legacy data attribute support
3522
3667
  if (!optionEl.hasAttribute('data-selectable')) {
3523
3668
  optionEl.setAttribute('data-selectable', '');
3524
3669
  }
3670
+ // New delegation attribute
3671
+ if (!optionEl.hasAttribute('data-sm-selectable')) {
3672
+ optionEl.setAttribute('data-sm-selectable', '');
3673
+ }
3525
3674
  optionEl.dataset.index = String(meta.index);
3526
3675
  optionEl.dataset.value = String(meta.value);
3676
+ // New standard attributes
3677
+ optionEl.dataset.smIndex = String(meta.index);
3678
+ if (meta.value != null) {
3679
+ optionEl.dataset.smValue = String(meta.value);
3680
+ }
3527
3681
  optionEl.id = optionEl.id || meta.id;
3528
3682
  if (!optionEl.getAttribute('role')) {
3529
3683
  optionEl.setAttribute('role', 'option');
@@ -3541,23 +3695,37 @@ class EnhancedSelect extends HTMLElement {
3541
3695
  if (!optionEl.hasAttribute('tabindex')) {
3542
3696
  optionEl.tabIndex = -1;
3543
3697
  }
3544
- if (!meta.disabled) {
3698
+ if (!this._customOptionBoundElements.has(optionEl)) {
3545
3699
  optionEl.addEventListener('click', (e) => {
3700
+ e.stopPropagation();
3701
+ const current = e.currentTarget;
3702
+ if (current.getAttribute('aria-disabled') === 'true')
3703
+ return;
3704
+ const parsedIndex = Number(current.dataset.index);
3705
+ if (!Number.isFinite(parsedIndex))
3706
+ return;
3546
3707
  const mouseEvent = e;
3547
- this._selectOption(meta.index, {
3708
+ this._selectOption(parsedIndex, {
3548
3709
  shiftKey: mouseEvent.shiftKey,
3549
3710
  toggleKey: mouseEvent.ctrlKey || mouseEvent.metaKey,
3550
3711
  });
3551
3712
  });
3552
3713
  optionEl.addEventListener('keydown', (e) => {
3553
- if (e.key === 'Enter' || e.key === ' ') {
3554
- e.preventDefault();
3555
- this._selectOption(meta.index, {
3556
- shiftKey: e.shiftKey,
3557
- toggleKey: e.ctrlKey || e.metaKey,
3558
- });
3559
- }
3714
+ if (e.key !== 'Enter' && e.key !== ' ')
3715
+ return;
3716
+ const current = e.currentTarget;
3717
+ if (current.getAttribute('aria-disabled') === 'true')
3718
+ return;
3719
+ const parsedIndex = Number(current.dataset.index);
3720
+ if (!Number.isFinite(parsedIndex))
3721
+ return;
3722
+ e.preventDefault();
3723
+ this._selectOption(parsedIndex, {
3724
+ shiftKey: e.shiftKey,
3725
+ toggleKey: e.ctrlKey || e.metaKey,
3726
+ });
3560
3727
  });
3728
+ this._customOptionBoundElements.add(optionEl);
3561
3729
  }
3562
3730
  return optionEl;
3563
3731
  }