@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.umd.js CHANGED
@@ -1561,16 +1561,55 @@
1561
1561
  this._shadow.appendChild(style);
1562
1562
  }
1563
1563
  _render() {
1564
- const { item, index, selected, disabled, active, render, showRemoveButton } = this._config;
1564
+ const { item, index, selected, disabled, active, render, showRemoveButton, classMap } = this._config;
1565
1565
  // Clear container
1566
1566
  this._container.innerHTML = '';
1567
- // Apply state classes
1568
- this._container.classList.toggle('selected', selected);
1569
- this._container.classList.toggle('disabled', disabled || false);
1570
- this._container.classList.toggle('active', active || false);
1567
+ // Add part attribute
1568
+ this._container.setAttribute('part', 'option');
1569
+ // Standard styling hook
1570
+ this._container.classList.add('smilodon-option');
1571
+ // Apply state classes using classMap or defaults
1572
+ const selectedClasses = (classMap?.selected ?? 'selected sm-selected').split(' ').filter(Boolean);
1573
+ const activeClasses = (classMap?.active ?? 'active sm-active').split(' ').filter(Boolean);
1574
+ const disabledClasses = (classMap?.disabled ?? 'disabled sm-disabled').split(' ').filter(Boolean);
1575
+ // Apply classes to both the container (internal styling) and the host (external styling/::part)
1576
+ // This ensures that utility classes are visible via ::part selectors
1577
+ const toggleClasses = (element, classes, add) => {
1578
+ if (add) {
1579
+ element.classList.add(...classes);
1580
+ }
1581
+ else {
1582
+ element.classList.remove(...classes);
1583
+ }
1584
+ };
1585
+ if (selected) {
1586
+ toggleClasses(this._container, [...selectedClasses, 'smilodon-option--selected'], true);
1587
+ toggleClasses(this, [...selectedClasses, 'smilodon-option--selected'], true);
1588
+ }
1589
+ else {
1590
+ toggleClasses(this._container, [...selectedClasses, 'smilodon-option--selected'], false);
1591
+ toggleClasses(this, [...selectedClasses, 'smilodon-option--selected'], false);
1592
+ }
1593
+ if (active) {
1594
+ toggleClasses(this._container, [...activeClasses, 'smilodon-option--active'], true);
1595
+ toggleClasses(this, [...activeClasses, 'smilodon-option--active'], true); // Make focus ring visible on host
1596
+ }
1597
+ else {
1598
+ toggleClasses(this._container, [...activeClasses, 'smilodon-option--active'], false);
1599
+ toggleClasses(this, [...activeClasses, 'smilodon-option--active'], false);
1600
+ }
1601
+ if (disabled) {
1602
+ toggleClasses(this._container, [...disabledClasses, 'smilodon-option--disabled'], true);
1603
+ toggleClasses(this, [...disabledClasses, 'smilodon-option--disabled'], true);
1604
+ }
1605
+ else {
1606
+ toggleClasses(this._container, [...disabledClasses, 'smilodon-option--disabled'], false);
1607
+ toggleClasses(this, [...disabledClasses, 'smilodon-option--disabled'], false);
1608
+ }
1571
1609
  // Custom class name
1572
1610
  if (this._config.className) {
1573
- this._container.className += ' ' + this._config.className;
1611
+ const classes = this._config.className.split(' ').filter(Boolean);
1612
+ this._container.classList.add(...classes);
1574
1613
  }
1575
1614
  // Apply custom styles
1576
1615
  if (this._config.style) {
@@ -1579,12 +1618,13 @@
1579
1618
  // Render content
1580
1619
  const contentDiv = document.createElement('div');
1581
1620
  contentDiv.className = 'option-content';
1621
+ // contentDiv.setAttribute('part', 'option-content'); // Optional
1582
1622
  if (render) {
1583
1623
  const rendered = render(item, index);
1584
1624
  if (typeof rendered === 'string') {
1585
1625
  contentDiv.innerHTML = rendered;
1586
1626
  }
1587
- else {
1627
+ else if (rendered instanceof HTMLElement) {
1588
1628
  contentDiv.appendChild(rendered);
1589
1629
  }
1590
1630
  }
@@ -1598,16 +1638,56 @@
1598
1638
  this._removeButton = document.createElement('button');
1599
1639
  this._removeButton.className = 'remove-button';
1600
1640
  this._removeButton.innerHTML = '×';
1641
+ this._removeButton.setAttribute('part', 'chip-remove');
1601
1642
  this._removeButton.setAttribute('aria-label', 'Remove option');
1602
1643
  this._removeButton.setAttribute('type', 'button');
1603
1644
  this._container.appendChild(this._removeButton);
1604
1645
  }
1605
- // Set ARIA attributes
1646
+ // Set ARIA attributes and State attributes on Host
1606
1647
  this.setAttribute('role', 'option');
1607
1648
  this.setAttribute('aria-selected', String(selected));
1608
1649
  if (disabled)
1609
1650
  this.setAttribute('aria-disabled', 'true');
1610
1651
  this.id = this._config.id || `select-option-${index}`;
1652
+ // Add checkmark (part="checkmark") - standard for object mode
1653
+ // Only show if NOT showing remove button (avoid clutter)
1654
+ if (!showRemoveButton) {
1655
+ const checkmark = document.createElement('div');
1656
+ checkmark.setAttribute('part', 'checkmark');
1657
+ checkmark.className = 'checkmark-icon';
1658
+ checkmark.innerHTML = `
1659
+ <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:1em;height:1em;">
1660
+ <path d="M4 8.5L6.5 11L12 5.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1661
+ </svg>
1662
+ `;
1663
+ // Visibility control via CSS or inline style
1664
+ // We set it to display: none unless selected.
1665
+ // User can override this behavior via part styling if they want transitions
1666
+ if (!selected) {
1667
+ checkmark.style.display = 'none';
1668
+ }
1669
+ else {
1670
+ checkmark.style.marginLeft = '8px';
1671
+ checkmark.style.color = 'currentColor';
1672
+ }
1673
+ this._container.appendChild(checkmark);
1674
+ }
1675
+ // Data Attributes Contract on Host
1676
+ const state = [];
1677
+ if (selected)
1678
+ state.push('selected');
1679
+ if (active)
1680
+ state.push('active');
1681
+ if (state.length) {
1682
+ this.dataset.smState = state.join(' ');
1683
+ }
1684
+ else {
1685
+ delete this.dataset.smState;
1686
+ }
1687
+ this.dataset.smIndex = String(index);
1688
+ if (!this.hasAttribute('data-sm-selectable')) {
1689
+ this.toggleAttribute('data-sm-selectable', true);
1690
+ }
1611
1691
  }
1612
1692
  _attachEventListeners() {
1613
1693
  // Click handler for selection
@@ -1752,6 +1832,7 @@
1752
1832
  this._pendingFirstRenderMark = false;
1753
1833
  this._pendingSearchRenderMark = false;
1754
1834
  this._rangeAnchorIndex = null;
1835
+ this._customOptionBoundElements = new WeakSet();
1755
1836
  this._shadow = this.attachShadow({ mode: 'open' });
1756
1837
  this._uniqueId = `enhanced-select-${Math.random().toString(36).substr(2, 9)}`;
1757
1838
  this._rendererHelpers = this._buildRendererHelpers();
@@ -1834,10 +1915,12 @@
1834
1915
  _createInputContainer() {
1835
1916
  const container = document.createElement('div');
1836
1917
  container.className = 'input-container';
1918
+ container.setAttribute('part', 'button');
1837
1919
  return container;
1838
1920
  }
1839
1921
  _createInput() {
1840
1922
  const input = document.createElement('input');
1923
+ input.setAttribute('part', 'input');
1841
1924
  input.type = 'text';
1842
1925
  input.className = 'select-input';
1843
1926
  input.id = `${this._uniqueId}-input`;
@@ -1865,6 +1948,7 @@
1865
1948
  _createDropdown() {
1866
1949
  const dropdown = document.createElement('div');
1867
1950
  dropdown.className = 'select-dropdown';
1951
+ dropdown.setAttribute('part', 'listbox');
1868
1952
  dropdown.style.display = 'none';
1869
1953
  if (this._config.styles.classNames?.dropdown) {
1870
1954
  dropdown.className += ' ' + this._config.styles.classNames.dropdown;
@@ -1896,7 +1980,7 @@
1896
1980
  const container = document.createElement('div');
1897
1981
  container.className = 'dropdown-arrow-container';
1898
1982
  container.innerHTML = `
1899
- <svg class="dropdown-arrow" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
1983
+ <svg class="dropdown-arrow" part="arrow" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
1900
1984
  <path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1901
1985
  </svg>
1902
1986
  `;
@@ -2418,6 +2502,22 @@
2418
2502
  const query = e.target.value;
2419
2503
  this._handleSearch(query);
2420
2504
  });
2505
+ // Delegated click listener for improved event handling (smart fallback)
2506
+ this._optionsContainer.addEventListener('click', (e) => {
2507
+ const target = e.target;
2508
+ // Handle option clicks
2509
+ const option = target.closest('[data-sm-selectable], [data-selectable], [data-sm-state]');
2510
+ if (option && !option.hasAttribute('aria-disabled')) {
2511
+ const indexStr = option.getAttribute('data-sm-index') ?? option.getAttribute('data-index');
2512
+ const index = Number(indexStr);
2513
+ if (!Number.isNaN(index)) {
2514
+ this._selectOption(index, {
2515
+ shiftKey: e.shiftKey,
2516
+ toggleKey: e.ctrlKey || e.metaKey,
2517
+ });
2518
+ }
2519
+ }
2520
+ });
2421
2521
  // Keyboard navigation
2422
2522
  this._input.addEventListener('keydown', (e) => this._handleKeydown(e));
2423
2523
  // Click outside to close
@@ -2990,10 +3090,12 @@
2990
3090
  selectedEntries.forEach(([index, item]) => {
2991
3091
  const badge = document.createElement('span');
2992
3092
  badge.className = 'selection-badge';
3093
+ badge.setAttribute('part', 'chip');
2993
3094
  badge.textContent = getLabel(item);
2994
3095
  // Add remove button to badge
2995
3096
  const removeBtn = document.createElement('button');
2996
3097
  removeBtn.className = 'badge-remove';
3098
+ removeBtn.setAttribute('part', 'chip-remove');
2997
3099
  removeBtn.innerHTML = '×';
2998
3100
  removeBtn.setAttribute('aria-label', `Remove ${getLabel(item)}`);
2999
3101
  removeBtn.addEventListener('click', (e) => {
@@ -3361,6 +3463,7 @@
3361
3463
  if (this._state.isSearching) {
3362
3464
  const searching = document.createElement('div');
3363
3465
  searching.className = 'searching-state';
3466
+ searching.setAttribute('part', 'loading');
3364
3467
  searching.textContent = 'Searching...';
3365
3468
  this._optionsContainer.appendChild(searching);
3366
3469
  return;
@@ -3418,6 +3521,7 @@
3418
3521
  });
3419
3522
  if (!hasRenderedItems && !this._state.isBusy) {
3420
3523
  const empty = document.createElement('div');
3524
+ empty.setAttribute('part', 'no-results');
3421
3525
  empty.className = 'empty-state';
3422
3526
  if (query) {
3423
3527
  empty.textContent = `No results found for "${this._state.searchQuery}"`;
@@ -3431,6 +3535,7 @@
3431
3535
  // Append Busy Indicator if busy
3432
3536
  if (this._state.isBusy && this._config.busyBucket.enabled) {
3433
3537
  const busyBucket = document.createElement('div');
3538
+ busyBucket.setAttribute('part', 'loading');
3434
3539
  busyBucket.className = 'busy-bucket';
3435
3540
  if (this._config.busyBucket.showSpinner) {
3436
3541
  const spinner = document.createElement('div');
@@ -3479,11 +3584,24 @@
3479
3584
  getValue,
3480
3585
  getLabel,
3481
3586
  showRemoveButton: this._config.selection.mode === 'multi' && this._config.selection.showRemoveButton,
3587
+ classMap: this.classMap,
3482
3588
  });
3589
+ // Valid part attribute on the web component host itself
3590
+ option.setAttribute('part', 'option');
3483
3591
  option.dataset.index = String(index);
3484
3592
  option.dataset.value = String(getValue(item));
3593
+ // New standard attributes on Host
3594
+ option.dataset.smIndex = String(index);
3595
+ if (!option.hasAttribute('data-sm-selectable')) {
3596
+ option.toggleAttribute('data-sm-selectable', true);
3597
+ }
3598
+ const val = getValue(item);
3599
+ if (val != null) {
3600
+ option.dataset.smValue = String(val);
3601
+ }
3485
3602
  option.id = option.id || optionId;
3486
3603
  option.addEventListener('click', (e) => {
3604
+ e.stopPropagation(); // Prevent duplicate handling by delegation
3487
3605
  const mouseEvent = e;
3488
3606
  this._selectOption(index, {
3489
3607
  shiftKey: mouseEvent.shiftKey,
@@ -3499,35 +3617,71 @@
3499
3617
  }
3500
3618
  _normalizeCustomOptionElement(element, meta) {
3501
3619
  const optionEl = element instanceof HTMLElement ? element : document.createElement('div');
3620
+ // Add part attribute for styling
3621
+ if (!optionEl.hasAttribute('part')) {
3622
+ optionEl.setAttribute('part', 'option');
3623
+ }
3502
3624
  // Add both semantic namespaced classes and the legacy internal classes that CSS uses
3503
3625
  optionEl.classList.add('smilodon-option', 'option');
3504
- // Toggle state classes
3626
+ // Toggle state classes using classMap if available
3505
3627
  const isSelected = meta.selected;
3506
3628
  const isActive = meta.active;
3507
3629
  const isDisabled = meta.disabled;
3630
+ // Resolve classes from classMap or defaults
3631
+ const selectedClasses = (this.classMap?.selected ?? 'selected sm-selected').split(' ').filter(Boolean);
3632
+ const activeClasses = (this.classMap?.active ?? 'active sm-active').split(' ').filter(Boolean);
3633
+ const disabledClasses = (this.classMap?.disabled ?? 'disabled sm-disabled').split(' ').filter(Boolean);
3508
3634
  if (isSelected) {
3509
- optionEl.classList.add('smilodon-option--selected', 'selected');
3635
+ optionEl.classList.add(...selectedClasses);
3636
+ optionEl.classList.add('smilodon-option--selected');
3510
3637
  }
3511
3638
  else {
3512
- optionEl.classList.remove('smilodon-option--selected', 'selected');
3639
+ optionEl.classList.remove(...selectedClasses);
3640
+ optionEl.classList.remove('smilodon-option--selected');
3513
3641
  }
3514
3642
  if (isActive) {
3515
- optionEl.classList.add('smilodon-option--active', 'active');
3643
+ optionEl.classList.add(...activeClasses);
3644
+ optionEl.classList.add('smilodon-option--active');
3516
3645
  }
3517
3646
  else {
3518
- optionEl.classList.remove('smilodon-option--active', 'active');
3647
+ optionEl.classList.remove(...activeClasses);
3648
+ optionEl.classList.remove('smilodon-option--active');
3519
3649
  }
3520
3650
  if (isDisabled) {
3521
- optionEl.classList.add('smilodon-option--disabled', 'disabled');
3651
+ optionEl.classList.add(...disabledClasses);
3652
+ optionEl.classList.add('smilodon-option--disabled');
3522
3653
  }
3523
3654
  else {
3524
- optionEl.classList.remove('smilodon-option--disabled', 'disabled');
3655
+ optionEl.classList.remove(...disabledClasses);
3656
+ optionEl.classList.remove('smilodon-option--disabled');
3525
3657
  }
3658
+ // Data Attributes Contract
3659
+ const state = [];
3660
+ if (isSelected)
3661
+ state.push('selected');
3662
+ if (isActive)
3663
+ state.push('active');
3664
+ if (state.length) {
3665
+ optionEl.dataset.smState = state.join(' ');
3666
+ }
3667
+ else {
3668
+ delete optionEl.dataset.smState;
3669
+ }
3670
+ // Legacy data attribute support
3526
3671
  if (!optionEl.hasAttribute('data-selectable')) {
3527
3672
  optionEl.setAttribute('data-selectable', '');
3528
3673
  }
3674
+ // New delegation attribute
3675
+ if (!optionEl.hasAttribute('data-sm-selectable')) {
3676
+ optionEl.setAttribute('data-sm-selectable', '');
3677
+ }
3529
3678
  optionEl.dataset.index = String(meta.index);
3530
3679
  optionEl.dataset.value = String(meta.value);
3680
+ // New standard attributes
3681
+ optionEl.dataset.smIndex = String(meta.index);
3682
+ if (meta.value != null) {
3683
+ optionEl.dataset.smValue = String(meta.value);
3684
+ }
3531
3685
  optionEl.id = optionEl.id || meta.id;
3532
3686
  if (!optionEl.getAttribute('role')) {
3533
3687
  optionEl.setAttribute('role', 'option');
@@ -3545,23 +3699,37 @@
3545
3699
  if (!optionEl.hasAttribute('tabindex')) {
3546
3700
  optionEl.tabIndex = -1;
3547
3701
  }
3548
- if (!meta.disabled) {
3702
+ if (!this._customOptionBoundElements.has(optionEl)) {
3549
3703
  optionEl.addEventListener('click', (e) => {
3704
+ e.stopPropagation();
3705
+ const current = e.currentTarget;
3706
+ if (current.getAttribute('aria-disabled') === 'true')
3707
+ return;
3708
+ const parsedIndex = Number(current.dataset.index);
3709
+ if (!Number.isFinite(parsedIndex))
3710
+ return;
3550
3711
  const mouseEvent = e;
3551
- this._selectOption(meta.index, {
3712
+ this._selectOption(parsedIndex, {
3552
3713
  shiftKey: mouseEvent.shiftKey,
3553
3714
  toggleKey: mouseEvent.ctrlKey || mouseEvent.metaKey,
3554
3715
  });
3555
3716
  });
3556
3717
  optionEl.addEventListener('keydown', (e) => {
3557
- if (e.key === 'Enter' || e.key === ' ') {
3558
- e.preventDefault();
3559
- this._selectOption(meta.index, {
3560
- shiftKey: e.shiftKey,
3561
- toggleKey: e.ctrlKey || e.metaKey,
3562
- });
3563
- }
3718
+ if (e.key !== 'Enter' && e.key !== ' ')
3719
+ return;
3720
+ const current = e.currentTarget;
3721
+ if (current.getAttribute('aria-disabled') === 'true')
3722
+ return;
3723
+ const parsedIndex = Number(current.dataset.index);
3724
+ if (!Number.isFinite(parsedIndex))
3725
+ return;
3726
+ e.preventDefault();
3727
+ this._selectOption(parsedIndex, {
3728
+ shiftKey: e.shiftKey,
3729
+ toggleKey: e.ctrlKey || e.metaKey,
3730
+ });
3564
3731
  });
3732
+ this._customOptionBoundElements.add(optionEl);
3565
3733
  }
3566
3734
  return optionEl;
3567
3735
  }