@smilodon/core 1.3.13 → 1.4.0

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
@@ -1834,10 +1914,12 @@
1834
1914
  _createInputContainer() {
1835
1915
  const container = document.createElement('div');
1836
1916
  container.className = 'input-container';
1917
+ container.setAttribute('part', 'button');
1837
1918
  return container;
1838
1919
  }
1839
1920
  _createInput() {
1840
1921
  const input = document.createElement('input');
1922
+ input.setAttribute('part', 'input');
1841
1923
  input.type = 'text';
1842
1924
  input.className = 'select-input';
1843
1925
  input.id = `${this._uniqueId}-input`;
@@ -1865,6 +1947,7 @@
1865
1947
  _createDropdown() {
1866
1948
  const dropdown = document.createElement('div');
1867
1949
  dropdown.className = 'select-dropdown';
1950
+ dropdown.setAttribute('part', 'listbox');
1868
1951
  dropdown.style.display = 'none';
1869
1952
  if (this._config.styles.classNames?.dropdown) {
1870
1953
  dropdown.className += ' ' + this._config.styles.classNames.dropdown;
@@ -1896,7 +1979,7 @@
1896
1979
  const container = document.createElement('div');
1897
1980
  container.className = 'dropdown-arrow-container';
1898
1981
  container.innerHTML = `
1899
- <svg class="dropdown-arrow" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
1982
+ <svg class="dropdown-arrow" part="arrow" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
1900
1983
  <path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
1901
1984
  </svg>
1902
1985
  `;
@@ -2418,6 +2501,22 @@
2418
2501
  const query = e.target.value;
2419
2502
  this._handleSearch(query);
2420
2503
  });
2504
+ // Delegated click listener for improved event handling (smart fallback)
2505
+ this._optionsContainer.addEventListener('click', (e) => {
2506
+ const target = e.target;
2507
+ // Handle option clicks
2508
+ const option = target.closest('[data-sm-selectable], [data-selectable], [data-sm-state]');
2509
+ if (option && !option.hasAttribute('aria-disabled')) {
2510
+ const indexStr = option.getAttribute('data-sm-index') ?? option.getAttribute('data-index');
2511
+ const index = Number(indexStr);
2512
+ if (!Number.isNaN(index)) {
2513
+ this._selectOption(index, {
2514
+ shiftKey: e.shiftKey,
2515
+ toggleKey: e.ctrlKey || e.metaKey,
2516
+ });
2517
+ }
2518
+ }
2519
+ });
2421
2520
  // Keyboard navigation
2422
2521
  this._input.addEventListener('keydown', (e) => this._handleKeydown(e));
2423
2522
  // Click outside to close
@@ -2990,10 +3089,12 @@
2990
3089
  selectedEntries.forEach(([index, item]) => {
2991
3090
  const badge = document.createElement('span');
2992
3091
  badge.className = 'selection-badge';
3092
+ badge.setAttribute('part', 'chip');
2993
3093
  badge.textContent = getLabel(item);
2994
3094
  // Add remove button to badge
2995
3095
  const removeBtn = document.createElement('button');
2996
3096
  removeBtn.className = 'badge-remove';
3097
+ removeBtn.setAttribute('part', 'chip-remove');
2997
3098
  removeBtn.innerHTML = '×';
2998
3099
  removeBtn.setAttribute('aria-label', `Remove ${getLabel(item)}`);
2999
3100
  removeBtn.addEventListener('click', (e) => {
@@ -3361,6 +3462,7 @@
3361
3462
  if (this._state.isSearching) {
3362
3463
  const searching = document.createElement('div');
3363
3464
  searching.className = 'searching-state';
3465
+ searching.setAttribute('part', 'loading');
3364
3466
  searching.textContent = 'Searching...';
3365
3467
  this._optionsContainer.appendChild(searching);
3366
3468
  return;
@@ -3418,6 +3520,7 @@
3418
3520
  });
3419
3521
  if (!hasRenderedItems && !this._state.isBusy) {
3420
3522
  const empty = document.createElement('div');
3523
+ empty.setAttribute('part', 'no-results');
3421
3524
  empty.className = 'empty-state';
3422
3525
  if (query) {
3423
3526
  empty.textContent = `No results found for "${this._state.searchQuery}"`;
@@ -3431,6 +3534,7 @@
3431
3534
  // Append Busy Indicator if busy
3432
3535
  if (this._state.isBusy && this._config.busyBucket.enabled) {
3433
3536
  const busyBucket = document.createElement('div');
3537
+ busyBucket.setAttribute('part', 'loading');
3434
3538
  busyBucket.className = 'busy-bucket';
3435
3539
  if (this._config.busyBucket.showSpinner) {
3436
3540
  const spinner = document.createElement('div');
@@ -3479,11 +3583,24 @@
3479
3583
  getValue,
3480
3584
  getLabel,
3481
3585
  showRemoveButton: this._config.selection.mode === 'multi' && this._config.selection.showRemoveButton,
3586
+ classMap: this.classMap,
3482
3587
  });
3588
+ // Valid part attribute on the web component host itself
3589
+ option.setAttribute('part', 'option');
3483
3590
  option.dataset.index = String(index);
3484
3591
  option.dataset.value = String(getValue(item));
3592
+ // New standard attributes on Host
3593
+ option.dataset.smIndex = String(index);
3594
+ if (!option.hasAttribute('data-sm-selectable')) {
3595
+ option.toggleAttribute('data-sm-selectable', true);
3596
+ }
3597
+ const val = getValue(item);
3598
+ if (val != null) {
3599
+ option.dataset.smValue = String(val);
3600
+ }
3485
3601
  option.id = option.id || optionId;
3486
3602
  option.addEventListener('click', (e) => {
3603
+ e.stopPropagation(); // Prevent duplicate handling by delegation
3487
3604
  const mouseEvent = e;
3488
3605
  this._selectOption(index, {
3489
3606
  shiftKey: mouseEvent.shiftKey,
@@ -3499,35 +3616,71 @@
3499
3616
  }
3500
3617
  _normalizeCustomOptionElement(element, meta) {
3501
3618
  const optionEl = element instanceof HTMLElement ? element : document.createElement('div');
3619
+ // Add part attribute for styling
3620
+ if (!optionEl.hasAttribute('part')) {
3621
+ optionEl.setAttribute('part', 'option');
3622
+ }
3502
3623
  // Add both semantic namespaced classes and the legacy internal classes that CSS uses
3503
3624
  optionEl.classList.add('smilodon-option', 'option');
3504
- // Toggle state classes
3625
+ // Toggle state classes using classMap if available
3505
3626
  const isSelected = meta.selected;
3506
3627
  const isActive = meta.active;
3507
3628
  const isDisabled = meta.disabled;
3629
+ // Resolve classes from classMap or defaults
3630
+ const selectedClasses = (this.classMap?.selected ?? 'selected sm-selected').split(' ').filter(Boolean);
3631
+ const activeClasses = (this.classMap?.active ?? 'active sm-active').split(' ').filter(Boolean);
3632
+ const disabledClasses = (this.classMap?.disabled ?? 'disabled sm-disabled').split(' ').filter(Boolean);
3508
3633
  if (isSelected) {
3509
- optionEl.classList.add('smilodon-option--selected', 'selected');
3634
+ optionEl.classList.add(...selectedClasses);
3635
+ optionEl.classList.add('smilodon-option--selected');
3510
3636
  }
3511
3637
  else {
3512
- optionEl.classList.remove('smilodon-option--selected', 'selected');
3638
+ optionEl.classList.remove(...selectedClasses);
3639
+ optionEl.classList.remove('smilodon-option--selected');
3513
3640
  }
3514
3641
  if (isActive) {
3515
- optionEl.classList.add('smilodon-option--active', 'active');
3642
+ optionEl.classList.add(...activeClasses);
3643
+ optionEl.classList.add('smilodon-option--active');
3516
3644
  }
3517
3645
  else {
3518
- optionEl.classList.remove('smilodon-option--active', 'active');
3646
+ optionEl.classList.remove(...activeClasses);
3647
+ optionEl.classList.remove('smilodon-option--active');
3519
3648
  }
3520
3649
  if (isDisabled) {
3521
- optionEl.classList.add('smilodon-option--disabled', 'disabled');
3650
+ optionEl.classList.add(...disabledClasses);
3651
+ optionEl.classList.add('smilodon-option--disabled');
3522
3652
  }
3523
3653
  else {
3524
- optionEl.classList.remove('smilodon-option--disabled', 'disabled');
3654
+ optionEl.classList.remove(...disabledClasses);
3655
+ optionEl.classList.remove('smilodon-option--disabled');
3656
+ }
3657
+ // Data Attributes Contract
3658
+ const state = [];
3659
+ if (isSelected)
3660
+ state.push('selected');
3661
+ if (isActive)
3662
+ state.push('active');
3663
+ if (state.length) {
3664
+ optionEl.dataset.smState = state.join(' ');
3525
3665
  }
3666
+ else {
3667
+ delete optionEl.dataset.smState;
3668
+ }
3669
+ // Legacy data attribute support
3526
3670
  if (!optionEl.hasAttribute('data-selectable')) {
3527
3671
  optionEl.setAttribute('data-selectable', '');
3528
3672
  }
3673
+ // New delegation attribute
3674
+ if (!optionEl.hasAttribute('data-sm-selectable')) {
3675
+ optionEl.setAttribute('data-sm-selectable', '');
3676
+ }
3529
3677
  optionEl.dataset.index = String(meta.index);
3530
3678
  optionEl.dataset.value = String(meta.value);
3679
+ // New standard attributes
3680
+ optionEl.dataset.smIndex = String(meta.index);
3681
+ if (meta.value != null) {
3682
+ optionEl.dataset.smValue = String(meta.value);
3683
+ }
3531
3684
  optionEl.id = optionEl.id || meta.id;
3532
3685
  if (!optionEl.getAttribute('role')) {
3533
3686
  optionEl.setAttribute('role', 'option');
@@ -3547,6 +3700,7 @@
3547
3700
  }
3548
3701
  if (!meta.disabled) {
3549
3702
  optionEl.addEventListener('click', (e) => {
3703
+ e.stopPropagation(); // Prevent duplicate handling by delegation
3550
3704
  const mouseEvent = e;
3551
3705
  this._selectOption(meta.index, {
3552
3706
  shiftKey: mouseEvent.shiftKey,