@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.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
|
-
//
|
|
1564
|
-
this._container.
|
|
1565
|
-
|
|
1566
|
-
this._container.classList.
|
|
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.
|
|
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
|
|
@@ -1830,10 +1910,12 @@ class EnhancedSelect extends HTMLElement {
|
|
|
1830
1910
|
_createInputContainer() {
|
|
1831
1911
|
const container = document.createElement('div');
|
|
1832
1912
|
container.className = 'input-container';
|
|
1913
|
+
container.setAttribute('part', 'button');
|
|
1833
1914
|
return container;
|
|
1834
1915
|
}
|
|
1835
1916
|
_createInput() {
|
|
1836
1917
|
const input = document.createElement('input');
|
|
1918
|
+
input.setAttribute('part', 'input');
|
|
1837
1919
|
input.type = 'text';
|
|
1838
1920
|
input.className = 'select-input';
|
|
1839
1921
|
input.id = `${this._uniqueId}-input`;
|
|
@@ -1861,6 +1943,7 @@ class EnhancedSelect extends HTMLElement {
|
|
|
1861
1943
|
_createDropdown() {
|
|
1862
1944
|
const dropdown = document.createElement('div');
|
|
1863
1945
|
dropdown.className = 'select-dropdown';
|
|
1946
|
+
dropdown.setAttribute('part', 'listbox');
|
|
1864
1947
|
dropdown.style.display = 'none';
|
|
1865
1948
|
if (this._config.styles.classNames?.dropdown) {
|
|
1866
1949
|
dropdown.className += ' ' + this._config.styles.classNames.dropdown;
|
|
@@ -1892,7 +1975,7 @@ class EnhancedSelect extends HTMLElement {
|
|
|
1892
1975
|
const container = document.createElement('div');
|
|
1893
1976
|
container.className = 'dropdown-arrow-container';
|
|
1894
1977
|
container.innerHTML = `
|
|
1895
|
-
<svg class="dropdown-arrow" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1978
|
+
<svg class="dropdown-arrow" part="arrow" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1896
1979
|
<path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
1897
1980
|
</svg>
|
|
1898
1981
|
`;
|
|
@@ -2414,6 +2497,22 @@ class EnhancedSelect extends HTMLElement {
|
|
|
2414
2497
|
const query = e.target.value;
|
|
2415
2498
|
this._handleSearch(query);
|
|
2416
2499
|
});
|
|
2500
|
+
// Delegated click listener for improved event handling (smart fallback)
|
|
2501
|
+
this._optionsContainer.addEventListener('click', (e) => {
|
|
2502
|
+
const target = e.target;
|
|
2503
|
+
// Handle option clicks
|
|
2504
|
+
const option = target.closest('[data-sm-selectable], [data-selectable], [data-sm-state]');
|
|
2505
|
+
if (option && !option.hasAttribute('aria-disabled')) {
|
|
2506
|
+
const indexStr = option.getAttribute('data-sm-index') ?? option.getAttribute('data-index');
|
|
2507
|
+
const index = Number(indexStr);
|
|
2508
|
+
if (!Number.isNaN(index)) {
|
|
2509
|
+
this._selectOption(index, {
|
|
2510
|
+
shiftKey: e.shiftKey,
|
|
2511
|
+
toggleKey: e.ctrlKey || e.metaKey,
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
});
|
|
2417
2516
|
// Keyboard navigation
|
|
2418
2517
|
this._input.addEventListener('keydown', (e) => this._handleKeydown(e));
|
|
2419
2518
|
// Click outside to close
|
|
@@ -2986,10 +3085,12 @@ class EnhancedSelect extends HTMLElement {
|
|
|
2986
3085
|
selectedEntries.forEach(([index, item]) => {
|
|
2987
3086
|
const badge = document.createElement('span');
|
|
2988
3087
|
badge.className = 'selection-badge';
|
|
3088
|
+
badge.setAttribute('part', 'chip');
|
|
2989
3089
|
badge.textContent = getLabel(item);
|
|
2990
3090
|
// Add remove button to badge
|
|
2991
3091
|
const removeBtn = document.createElement('button');
|
|
2992
3092
|
removeBtn.className = 'badge-remove';
|
|
3093
|
+
removeBtn.setAttribute('part', 'chip-remove');
|
|
2993
3094
|
removeBtn.innerHTML = '×';
|
|
2994
3095
|
removeBtn.setAttribute('aria-label', `Remove ${getLabel(item)}`);
|
|
2995
3096
|
removeBtn.addEventListener('click', (e) => {
|
|
@@ -3357,6 +3458,7 @@ class EnhancedSelect extends HTMLElement {
|
|
|
3357
3458
|
if (this._state.isSearching) {
|
|
3358
3459
|
const searching = document.createElement('div');
|
|
3359
3460
|
searching.className = 'searching-state';
|
|
3461
|
+
searching.setAttribute('part', 'loading');
|
|
3360
3462
|
searching.textContent = 'Searching...';
|
|
3361
3463
|
this._optionsContainer.appendChild(searching);
|
|
3362
3464
|
return;
|
|
@@ -3414,6 +3516,7 @@ class EnhancedSelect extends HTMLElement {
|
|
|
3414
3516
|
});
|
|
3415
3517
|
if (!hasRenderedItems && !this._state.isBusy) {
|
|
3416
3518
|
const empty = document.createElement('div');
|
|
3519
|
+
empty.setAttribute('part', 'no-results');
|
|
3417
3520
|
empty.className = 'empty-state';
|
|
3418
3521
|
if (query) {
|
|
3419
3522
|
empty.textContent = `No results found for "${this._state.searchQuery}"`;
|
|
@@ -3427,6 +3530,7 @@ class EnhancedSelect extends HTMLElement {
|
|
|
3427
3530
|
// Append Busy Indicator if busy
|
|
3428
3531
|
if (this._state.isBusy && this._config.busyBucket.enabled) {
|
|
3429
3532
|
const busyBucket = document.createElement('div');
|
|
3533
|
+
busyBucket.setAttribute('part', 'loading');
|
|
3430
3534
|
busyBucket.className = 'busy-bucket';
|
|
3431
3535
|
if (this._config.busyBucket.showSpinner) {
|
|
3432
3536
|
const spinner = document.createElement('div');
|
|
@@ -3475,11 +3579,24 @@ class EnhancedSelect extends HTMLElement {
|
|
|
3475
3579
|
getValue,
|
|
3476
3580
|
getLabel,
|
|
3477
3581
|
showRemoveButton: this._config.selection.mode === 'multi' && this._config.selection.showRemoveButton,
|
|
3582
|
+
classMap: this.classMap,
|
|
3478
3583
|
});
|
|
3584
|
+
// Valid part attribute on the web component host itself
|
|
3585
|
+
option.setAttribute('part', 'option');
|
|
3479
3586
|
option.dataset.index = String(index);
|
|
3480
3587
|
option.dataset.value = String(getValue(item));
|
|
3588
|
+
// New standard attributes on Host
|
|
3589
|
+
option.dataset.smIndex = String(index);
|
|
3590
|
+
if (!option.hasAttribute('data-sm-selectable')) {
|
|
3591
|
+
option.toggleAttribute('data-sm-selectable', true);
|
|
3592
|
+
}
|
|
3593
|
+
const val = getValue(item);
|
|
3594
|
+
if (val != null) {
|
|
3595
|
+
option.dataset.smValue = String(val);
|
|
3596
|
+
}
|
|
3481
3597
|
option.id = option.id || optionId;
|
|
3482
3598
|
option.addEventListener('click', (e) => {
|
|
3599
|
+
e.stopPropagation(); // Prevent duplicate handling by delegation
|
|
3483
3600
|
const mouseEvent = e;
|
|
3484
3601
|
this._selectOption(index, {
|
|
3485
3602
|
shiftKey: mouseEvent.shiftKey,
|
|
@@ -3495,35 +3612,71 @@ class EnhancedSelect extends HTMLElement {
|
|
|
3495
3612
|
}
|
|
3496
3613
|
_normalizeCustomOptionElement(element, meta) {
|
|
3497
3614
|
const optionEl = element instanceof HTMLElement ? element : document.createElement('div');
|
|
3615
|
+
// Add part attribute for styling
|
|
3616
|
+
if (!optionEl.hasAttribute('part')) {
|
|
3617
|
+
optionEl.setAttribute('part', 'option');
|
|
3618
|
+
}
|
|
3498
3619
|
// Add both semantic namespaced classes and the legacy internal classes that CSS uses
|
|
3499
3620
|
optionEl.classList.add('smilodon-option', 'option');
|
|
3500
|
-
// Toggle state classes
|
|
3621
|
+
// Toggle state classes using classMap if available
|
|
3501
3622
|
const isSelected = meta.selected;
|
|
3502
3623
|
const isActive = meta.active;
|
|
3503
3624
|
const isDisabled = meta.disabled;
|
|
3625
|
+
// Resolve classes from classMap or defaults
|
|
3626
|
+
const selectedClasses = (this.classMap?.selected ?? 'selected sm-selected').split(' ').filter(Boolean);
|
|
3627
|
+
const activeClasses = (this.classMap?.active ?? 'active sm-active').split(' ').filter(Boolean);
|
|
3628
|
+
const disabledClasses = (this.classMap?.disabled ?? 'disabled sm-disabled').split(' ').filter(Boolean);
|
|
3504
3629
|
if (isSelected) {
|
|
3505
|
-
optionEl.classList.add(
|
|
3630
|
+
optionEl.classList.add(...selectedClasses);
|
|
3631
|
+
optionEl.classList.add('smilodon-option--selected');
|
|
3506
3632
|
}
|
|
3507
3633
|
else {
|
|
3508
|
-
optionEl.classList.remove(
|
|
3634
|
+
optionEl.classList.remove(...selectedClasses);
|
|
3635
|
+
optionEl.classList.remove('smilodon-option--selected');
|
|
3509
3636
|
}
|
|
3510
3637
|
if (isActive) {
|
|
3511
|
-
optionEl.classList.add(
|
|
3638
|
+
optionEl.classList.add(...activeClasses);
|
|
3639
|
+
optionEl.classList.add('smilodon-option--active');
|
|
3512
3640
|
}
|
|
3513
3641
|
else {
|
|
3514
|
-
optionEl.classList.remove(
|
|
3642
|
+
optionEl.classList.remove(...activeClasses);
|
|
3643
|
+
optionEl.classList.remove('smilodon-option--active');
|
|
3515
3644
|
}
|
|
3516
3645
|
if (isDisabled) {
|
|
3517
|
-
optionEl.classList.add(
|
|
3646
|
+
optionEl.classList.add(...disabledClasses);
|
|
3647
|
+
optionEl.classList.add('smilodon-option--disabled');
|
|
3518
3648
|
}
|
|
3519
3649
|
else {
|
|
3520
|
-
optionEl.classList.remove(
|
|
3650
|
+
optionEl.classList.remove(...disabledClasses);
|
|
3651
|
+
optionEl.classList.remove('smilodon-option--disabled');
|
|
3652
|
+
}
|
|
3653
|
+
// Data Attributes Contract
|
|
3654
|
+
const state = [];
|
|
3655
|
+
if (isSelected)
|
|
3656
|
+
state.push('selected');
|
|
3657
|
+
if (isActive)
|
|
3658
|
+
state.push('active');
|
|
3659
|
+
if (state.length) {
|
|
3660
|
+
optionEl.dataset.smState = state.join(' ');
|
|
3521
3661
|
}
|
|
3662
|
+
else {
|
|
3663
|
+
delete optionEl.dataset.smState;
|
|
3664
|
+
}
|
|
3665
|
+
// Legacy data attribute support
|
|
3522
3666
|
if (!optionEl.hasAttribute('data-selectable')) {
|
|
3523
3667
|
optionEl.setAttribute('data-selectable', '');
|
|
3524
3668
|
}
|
|
3669
|
+
// New delegation attribute
|
|
3670
|
+
if (!optionEl.hasAttribute('data-sm-selectable')) {
|
|
3671
|
+
optionEl.setAttribute('data-sm-selectable', '');
|
|
3672
|
+
}
|
|
3525
3673
|
optionEl.dataset.index = String(meta.index);
|
|
3526
3674
|
optionEl.dataset.value = String(meta.value);
|
|
3675
|
+
// New standard attributes
|
|
3676
|
+
optionEl.dataset.smIndex = String(meta.index);
|
|
3677
|
+
if (meta.value != null) {
|
|
3678
|
+
optionEl.dataset.smValue = String(meta.value);
|
|
3679
|
+
}
|
|
3527
3680
|
optionEl.id = optionEl.id || meta.id;
|
|
3528
3681
|
if (!optionEl.getAttribute('role')) {
|
|
3529
3682
|
optionEl.setAttribute('role', 'option');
|
|
@@ -3543,6 +3696,7 @@ class EnhancedSelect extends HTMLElement {
|
|
|
3543
3696
|
}
|
|
3544
3697
|
if (!meta.disabled) {
|
|
3545
3698
|
optionEl.addEventListener('click', (e) => {
|
|
3699
|
+
e.stopPropagation(); // Prevent duplicate handling by delegation
|
|
3546
3700
|
const mouseEvent = e;
|
|
3547
3701
|
this._selectOption(meta.index, {
|
|
3548
3702
|
shiftKey: mouseEvent.shiftKey,
|