@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.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
|
-
//
|
|
1568
|
-
this._container.
|
|
1569
|
-
|
|
1570
|
-
this._container.classList.
|
|
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.
|
|
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(
|
|
3634
|
+
optionEl.classList.add(...selectedClasses);
|
|
3635
|
+
optionEl.classList.add('smilodon-option--selected');
|
|
3510
3636
|
}
|
|
3511
3637
|
else {
|
|
3512
|
-
optionEl.classList.remove(
|
|
3638
|
+
optionEl.classList.remove(...selectedClasses);
|
|
3639
|
+
optionEl.classList.remove('smilodon-option--selected');
|
|
3513
3640
|
}
|
|
3514
3641
|
if (isActive) {
|
|
3515
|
-
optionEl.classList.add(
|
|
3642
|
+
optionEl.classList.add(...activeClasses);
|
|
3643
|
+
optionEl.classList.add('smilodon-option--active');
|
|
3516
3644
|
}
|
|
3517
3645
|
else {
|
|
3518
|
-
optionEl.classList.remove(
|
|
3646
|
+
optionEl.classList.remove(...activeClasses);
|
|
3647
|
+
optionEl.classList.remove('smilodon-option--active');
|
|
3519
3648
|
}
|
|
3520
3649
|
if (isDisabled) {
|
|
3521
|
-
optionEl.classList.add(
|
|
3650
|
+
optionEl.classList.add(...disabledClasses);
|
|
3651
|
+
optionEl.classList.add('smilodon-option--disabled');
|
|
3522
3652
|
}
|
|
3523
3653
|
else {
|
|
3524
|
-
optionEl.classList.remove(
|
|
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,
|