@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.cjs +170 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +170 -16
- 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 +170 -16
- 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 +2 -1
- package/dist/types/src/components/select-option.d.ts +3 -0
- package/dist/types/src/types.d.ts +6 -0
- package/dist/types/tests/styling-contract.spec.d.ts +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1555,16 +1555,55 @@ class SelectOption extends HTMLElement {
|
|
|
1555
1555
|
this._shadow.appendChild(style);
|
|
1556
1556
|
}
|
|
1557
1557
|
_render() {
|
|
1558
|
-
const { item, index, selected, disabled, active, render, showRemoveButton } = this._config;
|
|
1558
|
+
const { item, index, selected, disabled, active, render, showRemoveButton, classMap } = this._config;
|
|
1559
1559
|
// Clear container
|
|
1560
1560
|
this._container.innerHTML = '';
|
|
1561
|
-
//
|
|
1562
|
-
this._container.
|
|
1563
|
-
|
|
1564
|
-
this._container.classList.
|
|
1561
|
+
// Add part attribute
|
|
1562
|
+
this._container.setAttribute('part', 'option');
|
|
1563
|
+
// Standard styling hook
|
|
1564
|
+
this._container.classList.add('smilodon-option');
|
|
1565
|
+
// Apply state classes using classMap or defaults
|
|
1566
|
+
const selectedClasses = (classMap?.selected ?? 'selected sm-selected').split(' ').filter(Boolean);
|
|
1567
|
+
const activeClasses = (classMap?.active ?? 'active sm-active').split(' ').filter(Boolean);
|
|
1568
|
+
const disabledClasses = (classMap?.disabled ?? 'disabled sm-disabled').split(' ').filter(Boolean);
|
|
1569
|
+
// Apply classes to both the container (internal styling) and the host (external styling/::part)
|
|
1570
|
+
// This ensures that utility classes are visible via ::part selectors
|
|
1571
|
+
const toggleClasses = (element, classes, add) => {
|
|
1572
|
+
if (add) {
|
|
1573
|
+
element.classList.add(...classes);
|
|
1574
|
+
}
|
|
1575
|
+
else {
|
|
1576
|
+
element.classList.remove(...classes);
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
if (selected) {
|
|
1580
|
+
toggleClasses(this._container, [...selectedClasses, 'smilodon-option--selected'], true);
|
|
1581
|
+
toggleClasses(this, [...selectedClasses, 'smilodon-option--selected'], true);
|
|
1582
|
+
}
|
|
1583
|
+
else {
|
|
1584
|
+
toggleClasses(this._container, [...selectedClasses, 'smilodon-option--selected'], false);
|
|
1585
|
+
toggleClasses(this, [...selectedClasses, 'smilodon-option--selected'], false);
|
|
1586
|
+
}
|
|
1587
|
+
if (active) {
|
|
1588
|
+
toggleClasses(this._container, [...activeClasses, 'smilodon-option--active'], true);
|
|
1589
|
+
toggleClasses(this, [...activeClasses, 'smilodon-option--active'], true); // Make focus ring visible on host
|
|
1590
|
+
}
|
|
1591
|
+
else {
|
|
1592
|
+
toggleClasses(this._container, [...activeClasses, 'smilodon-option--active'], false);
|
|
1593
|
+
toggleClasses(this, [...activeClasses, 'smilodon-option--active'], false);
|
|
1594
|
+
}
|
|
1595
|
+
if (disabled) {
|
|
1596
|
+
toggleClasses(this._container, [...disabledClasses, 'smilodon-option--disabled'], true);
|
|
1597
|
+
toggleClasses(this, [...disabledClasses, 'smilodon-option--disabled'], true);
|
|
1598
|
+
}
|
|
1599
|
+
else {
|
|
1600
|
+
toggleClasses(this._container, [...disabledClasses, 'smilodon-option--disabled'], false);
|
|
1601
|
+
toggleClasses(this, [...disabledClasses, 'smilodon-option--disabled'], false);
|
|
1602
|
+
}
|
|
1565
1603
|
// Custom class name
|
|
1566
1604
|
if (this._config.className) {
|
|
1567
|
-
this.
|
|
1605
|
+
const classes = this._config.className.split(' ').filter(Boolean);
|
|
1606
|
+
this._container.classList.add(...classes);
|
|
1568
1607
|
}
|
|
1569
1608
|
// Apply custom styles
|
|
1570
1609
|
if (this._config.style) {
|
|
@@ -1573,12 +1612,13 @@ class SelectOption extends HTMLElement {
|
|
|
1573
1612
|
// Render content
|
|
1574
1613
|
const contentDiv = document.createElement('div');
|
|
1575
1614
|
contentDiv.className = 'option-content';
|
|
1615
|
+
// contentDiv.setAttribute('part', 'option-content'); // Optional
|
|
1576
1616
|
if (render) {
|
|
1577
1617
|
const rendered = render(item, index);
|
|
1578
1618
|
if (typeof rendered === 'string') {
|
|
1579
1619
|
contentDiv.innerHTML = rendered;
|
|
1580
1620
|
}
|
|
1581
|
-
else {
|
|
1621
|
+
else if (rendered instanceof HTMLElement) {
|
|
1582
1622
|
contentDiv.appendChild(rendered);
|
|
1583
1623
|
}
|
|
1584
1624
|
}
|
|
@@ -1592,16 +1632,56 @@ class SelectOption extends HTMLElement {
|
|
|
1592
1632
|
this._removeButton = document.createElement('button');
|
|
1593
1633
|
this._removeButton.className = 'remove-button';
|
|
1594
1634
|
this._removeButton.innerHTML = '×';
|
|
1635
|
+
this._removeButton.setAttribute('part', 'chip-remove');
|
|
1595
1636
|
this._removeButton.setAttribute('aria-label', 'Remove option');
|
|
1596
1637
|
this._removeButton.setAttribute('type', 'button');
|
|
1597
1638
|
this._container.appendChild(this._removeButton);
|
|
1598
1639
|
}
|
|
1599
|
-
// Set ARIA attributes
|
|
1640
|
+
// Set ARIA attributes and State attributes on Host
|
|
1600
1641
|
this.setAttribute('role', 'option');
|
|
1601
1642
|
this.setAttribute('aria-selected', String(selected));
|
|
1602
1643
|
if (disabled)
|
|
1603
1644
|
this.setAttribute('aria-disabled', 'true');
|
|
1604
1645
|
this.id = this._config.id || `select-option-${index}`;
|
|
1646
|
+
// Add checkmark (part="checkmark") - standard for object mode
|
|
1647
|
+
// Only show if NOT showing remove button (avoid clutter)
|
|
1648
|
+
if (!showRemoveButton) {
|
|
1649
|
+
const checkmark = document.createElement('div');
|
|
1650
|
+
checkmark.setAttribute('part', 'checkmark');
|
|
1651
|
+
checkmark.className = 'checkmark-icon';
|
|
1652
|
+
checkmark.innerHTML = `
|
|
1653
|
+
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:1em;height:1em;">
|
|
1654
|
+
<path d="M4 8.5L6.5 11L12 5.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
1655
|
+
</svg>
|
|
1656
|
+
`;
|
|
1657
|
+
// Visibility control via CSS or inline style
|
|
1658
|
+
// We set it to display: none unless selected.
|
|
1659
|
+
// User can override this behavior via part styling if they want transitions
|
|
1660
|
+
if (!selected) {
|
|
1661
|
+
checkmark.style.display = 'none';
|
|
1662
|
+
}
|
|
1663
|
+
else {
|
|
1664
|
+
checkmark.style.marginLeft = '8px';
|
|
1665
|
+
checkmark.style.color = 'currentColor';
|
|
1666
|
+
}
|
|
1667
|
+
this._container.appendChild(checkmark);
|
|
1668
|
+
}
|
|
1669
|
+
// Data Attributes Contract on Host
|
|
1670
|
+
const state = [];
|
|
1671
|
+
if (selected)
|
|
1672
|
+
state.push('selected');
|
|
1673
|
+
if (active)
|
|
1674
|
+
state.push('active');
|
|
1675
|
+
if (state.length) {
|
|
1676
|
+
this.dataset.smState = state.join(' ');
|
|
1677
|
+
}
|
|
1678
|
+
else {
|
|
1679
|
+
delete this.dataset.smState;
|
|
1680
|
+
}
|
|
1681
|
+
this.dataset.smIndex = String(index);
|
|
1682
|
+
if (!this.hasAttribute('data-sm-selectable')) {
|
|
1683
|
+
this.toggleAttribute('data-sm-selectable', true);
|
|
1684
|
+
}
|
|
1605
1685
|
}
|
|
1606
1686
|
_attachEventListeners() {
|
|
1607
1687
|
// Click handler for selection
|
|
@@ -1828,10 +1908,12 @@ class EnhancedSelect extends HTMLElement {
|
|
|
1828
1908
|
_createInputContainer() {
|
|
1829
1909
|
const container = document.createElement('div');
|
|
1830
1910
|
container.className = 'input-container';
|
|
1911
|
+
container.setAttribute('part', 'button');
|
|
1831
1912
|
return container;
|
|
1832
1913
|
}
|
|
1833
1914
|
_createInput() {
|
|
1834
1915
|
const input = document.createElement('input');
|
|
1916
|
+
input.setAttribute('part', 'input');
|
|
1835
1917
|
input.type = 'text';
|
|
1836
1918
|
input.className = 'select-input';
|
|
1837
1919
|
input.id = `${this._uniqueId}-input`;
|
|
@@ -1859,6 +1941,7 @@ class EnhancedSelect extends HTMLElement {
|
|
|
1859
1941
|
_createDropdown() {
|
|
1860
1942
|
const dropdown = document.createElement('div');
|
|
1861
1943
|
dropdown.className = 'select-dropdown';
|
|
1944
|
+
dropdown.setAttribute('part', 'listbox');
|
|
1862
1945
|
dropdown.style.display = 'none';
|
|
1863
1946
|
if (this._config.styles.classNames?.dropdown) {
|
|
1864
1947
|
dropdown.className += ' ' + this._config.styles.classNames.dropdown;
|
|
@@ -1890,7 +1973,7 @@ class EnhancedSelect extends HTMLElement {
|
|
|
1890
1973
|
const container = document.createElement('div');
|
|
1891
1974
|
container.className = 'dropdown-arrow-container';
|
|
1892
1975
|
container.innerHTML = `
|
|
1893
|
-
<svg class="dropdown-arrow" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1976
|
+
<svg class="dropdown-arrow" part="arrow" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1894
1977
|
<path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
1895
1978
|
</svg>
|
|
1896
1979
|
`;
|
|
@@ -2412,6 +2495,22 @@ class EnhancedSelect extends HTMLElement {
|
|
|
2412
2495
|
const query = e.target.value;
|
|
2413
2496
|
this._handleSearch(query);
|
|
2414
2497
|
});
|
|
2498
|
+
// Delegated click listener for improved event handling (smart fallback)
|
|
2499
|
+
this._optionsContainer.addEventListener('click', (e) => {
|
|
2500
|
+
const target = e.target;
|
|
2501
|
+
// Handle option clicks
|
|
2502
|
+
const option = target.closest('[data-sm-selectable], [data-selectable], [data-sm-state]');
|
|
2503
|
+
if (option && !option.hasAttribute('aria-disabled')) {
|
|
2504
|
+
const indexStr = option.getAttribute('data-sm-index') ?? option.getAttribute('data-index');
|
|
2505
|
+
const index = Number(indexStr);
|
|
2506
|
+
if (!Number.isNaN(index)) {
|
|
2507
|
+
this._selectOption(index, {
|
|
2508
|
+
shiftKey: e.shiftKey,
|
|
2509
|
+
toggleKey: e.ctrlKey || e.metaKey,
|
|
2510
|
+
});
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
});
|
|
2415
2514
|
// Keyboard navigation
|
|
2416
2515
|
this._input.addEventListener('keydown', (e) => this._handleKeydown(e));
|
|
2417
2516
|
// Click outside to close
|
|
@@ -2984,10 +3083,12 @@ class EnhancedSelect extends HTMLElement {
|
|
|
2984
3083
|
selectedEntries.forEach(([index, item]) => {
|
|
2985
3084
|
const badge = document.createElement('span');
|
|
2986
3085
|
badge.className = 'selection-badge';
|
|
3086
|
+
badge.setAttribute('part', 'chip');
|
|
2987
3087
|
badge.textContent = getLabel(item);
|
|
2988
3088
|
// Add remove button to badge
|
|
2989
3089
|
const removeBtn = document.createElement('button');
|
|
2990
3090
|
removeBtn.className = 'badge-remove';
|
|
3091
|
+
removeBtn.setAttribute('part', 'chip-remove');
|
|
2991
3092
|
removeBtn.innerHTML = '×';
|
|
2992
3093
|
removeBtn.setAttribute('aria-label', `Remove ${getLabel(item)}`);
|
|
2993
3094
|
removeBtn.addEventListener('click', (e) => {
|
|
@@ -3355,6 +3456,7 @@ class EnhancedSelect extends HTMLElement {
|
|
|
3355
3456
|
if (this._state.isSearching) {
|
|
3356
3457
|
const searching = document.createElement('div');
|
|
3357
3458
|
searching.className = 'searching-state';
|
|
3459
|
+
searching.setAttribute('part', 'loading');
|
|
3358
3460
|
searching.textContent = 'Searching...';
|
|
3359
3461
|
this._optionsContainer.appendChild(searching);
|
|
3360
3462
|
return;
|
|
@@ -3412,6 +3514,7 @@ class EnhancedSelect extends HTMLElement {
|
|
|
3412
3514
|
});
|
|
3413
3515
|
if (!hasRenderedItems && !this._state.isBusy) {
|
|
3414
3516
|
const empty = document.createElement('div');
|
|
3517
|
+
empty.setAttribute('part', 'no-results');
|
|
3415
3518
|
empty.className = 'empty-state';
|
|
3416
3519
|
if (query) {
|
|
3417
3520
|
empty.textContent = `No results found for "${this._state.searchQuery}"`;
|
|
@@ -3425,6 +3528,7 @@ class EnhancedSelect extends HTMLElement {
|
|
|
3425
3528
|
// Append Busy Indicator if busy
|
|
3426
3529
|
if (this._state.isBusy && this._config.busyBucket.enabled) {
|
|
3427
3530
|
const busyBucket = document.createElement('div');
|
|
3531
|
+
busyBucket.setAttribute('part', 'loading');
|
|
3428
3532
|
busyBucket.className = 'busy-bucket';
|
|
3429
3533
|
if (this._config.busyBucket.showSpinner) {
|
|
3430
3534
|
const spinner = document.createElement('div');
|
|
@@ -3473,11 +3577,24 @@ class EnhancedSelect extends HTMLElement {
|
|
|
3473
3577
|
getValue,
|
|
3474
3578
|
getLabel,
|
|
3475
3579
|
showRemoveButton: this._config.selection.mode === 'multi' && this._config.selection.showRemoveButton,
|
|
3580
|
+
classMap: this.classMap,
|
|
3476
3581
|
});
|
|
3582
|
+
// Valid part attribute on the web component host itself
|
|
3583
|
+
option.setAttribute('part', 'option');
|
|
3477
3584
|
option.dataset.index = String(index);
|
|
3478
3585
|
option.dataset.value = String(getValue(item));
|
|
3586
|
+
// New standard attributes on Host
|
|
3587
|
+
option.dataset.smIndex = String(index);
|
|
3588
|
+
if (!option.hasAttribute('data-sm-selectable')) {
|
|
3589
|
+
option.toggleAttribute('data-sm-selectable', true);
|
|
3590
|
+
}
|
|
3591
|
+
const val = getValue(item);
|
|
3592
|
+
if (val != null) {
|
|
3593
|
+
option.dataset.smValue = String(val);
|
|
3594
|
+
}
|
|
3479
3595
|
option.id = option.id || optionId;
|
|
3480
3596
|
option.addEventListener('click', (e) => {
|
|
3597
|
+
e.stopPropagation(); // Prevent duplicate handling by delegation
|
|
3481
3598
|
const mouseEvent = e;
|
|
3482
3599
|
this._selectOption(index, {
|
|
3483
3600
|
shiftKey: mouseEvent.shiftKey,
|
|
@@ -3493,35 +3610,71 @@ class EnhancedSelect extends HTMLElement {
|
|
|
3493
3610
|
}
|
|
3494
3611
|
_normalizeCustomOptionElement(element, meta) {
|
|
3495
3612
|
const optionEl = element instanceof HTMLElement ? element : document.createElement('div');
|
|
3613
|
+
// Add part attribute for styling
|
|
3614
|
+
if (!optionEl.hasAttribute('part')) {
|
|
3615
|
+
optionEl.setAttribute('part', 'option');
|
|
3616
|
+
}
|
|
3496
3617
|
// Add both semantic namespaced classes and the legacy internal classes that CSS uses
|
|
3497
3618
|
optionEl.classList.add('smilodon-option', 'option');
|
|
3498
|
-
// Toggle state classes
|
|
3619
|
+
// Toggle state classes using classMap if available
|
|
3499
3620
|
const isSelected = meta.selected;
|
|
3500
3621
|
const isActive = meta.active;
|
|
3501
3622
|
const isDisabled = meta.disabled;
|
|
3623
|
+
// Resolve classes from classMap or defaults
|
|
3624
|
+
const selectedClasses = (this.classMap?.selected ?? 'selected sm-selected').split(' ').filter(Boolean);
|
|
3625
|
+
const activeClasses = (this.classMap?.active ?? 'active sm-active').split(' ').filter(Boolean);
|
|
3626
|
+
const disabledClasses = (this.classMap?.disabled ?? 'disabled sm-disabled').split(' ').filter(Boolean);
|
|
3502
3627
|
if (isSelected) {
|
|
3503
|
-
optionEl.classList.add(
|
|
3628
|
+
optionEl.classList.add(...selectedClasses);
|
|
3629
|
+
optionEl.classList.add('smilodon-option--selected');
|
|
3504
3630
|
}
|
|
3505
3631
|
else {
|
|
3506
|
-
optionEl.classList.remove(
|
|
3632
|
+
optionEl.classList.remove(...selectedClasses);
|
|
3633
|
+
optionEl.classList.remove('smilodon-option--selected');
|
|
3507
3634
|
}
|
|
3508
3635
|
if (isActive) {
|
|
3509
|
-
optionEl.classList.add(
|
|
3636
|
+
optionEl.classList.add(...activeClasses);
|
|
3637
|
+
optionEl.classList.add('smilodon-option--active');
|
|
3510
3638
|
}
|
|
3511
3639
|
else {
|
|
3512
|
-
optionEl.classList.remove(
|
|
3640
|
+
optionEl.classList.remove(...activeClasses);
|
|
3641
|
+
optionEl.classList.remove('smilodon-option--active');
|
|
3513
3642
|
}
|
|
3514
3643
|
if (isDisabled) {
|
|
3515
|
-
optionEl.classList.add(
|
|
3644
|
+
optionEl.classList.add(...disabledClasses);
|
|
3645
|
+
optionEl.classList.add('smilodon-option--disabled');
|
|
3516
3646
|
}
|
|
3517
3647
|
else {
|
|
3518
|
-
optionEl.classList.remove(
|
|
3648
|
+
optionEl.classList.remove(...disabledClasses);
|
|
3649
|
+
optionEl.classList.remove('smilodon-option--disabled');
|
|
3650
|
+
}
|
|
3651
|
+
// Data Attributes Contract
|
|
3652
|
+
const state = [];
|
|
3653
|
+
if (isSelected)
|
|
3654
|
+
state.push('selected');
|
|
3655
|
+
if (isActive)
|
|
3656
|
+
state.push('active');
|
|
3657
|
+
if (state.length) {
|
|
3658
|
+
optionEl.dataset.smState = state.join(' ');
|
|
3519
3659
|
}
|
|
3660
|
+
else {
|
|
3661
|
+
delete optionEl.dataset.smState;
|
|
3662
|
+
}
|
|
3663
|
+
// Legacy data attribute support
|
|
3520
3664
|
if (!optionEl.hasAttribute('data-selectable')) {
|
|
3521
3665
|
optionEl.setAttribute('data-selectable', '');
|
|
3522
3666
|
}
|
|
3667
|
+
// New delegation attribute
|
|
3668
|
+
if (!optionEl.hasAttribute('data-sm-selectable')) {
|
|
3669
|
+
optionEl.setAttribute('data-sm-selectable', '');
|
|
3670
|
+
}
|
|
3523
3671
|
optionEl.dataset.index = String(meta.index);
|
|
3524
3672
|
optionEl.dataset.value = String(meta.value);
|
|
3673
|
+
// New standard attributes
|
|
3674
|
+
optionEl.dataset.smIndex = String(meta.index);
|
|
3675
|
+
if (meta.value != null) {
|
|
3676
|
+
optionEl.dataset.smValue = String(meta.value);
|
|
3677
|
+
}
|
|
3525
3678
|
optionEl.id = optionEl.id || meta.id;
|
|
3526
3679
|
if (!optionEl.getAttribute('role')) {
|
|
3527
3680
|
optionEl.setAttribute('role', 'option');
|
|
@@ -3541,6 +3694,7 @@ class EnhancedSelect extends HTMLElement {
|
|
|
3541
3694
|
}
|
|
3542
3695
|
if (!meta.disabled) {
|
|
3543
3696
|
optionEl.addEventListener('click', (e) => {
|
|
3697
|
+
e.stopPropagation(); // Prevent duplicate handling by delegation
|
|
3544
3698
|
const mouseEvent = e;
|
|
3545
3699
|
this._selectOption(meta.index, {
|
|
3546
3700
|
shiftKey: mouseEvent.shiftKey,
|