@metropolle/design-system 1.0.0-beta.2025.12.14.1932.93a1534 → 1.0.0-beta.2025.12.29.1122.b256706
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/css/components.css +366 -0
- package/dist/react/components/react/Select/Select.d.ts +61 -10
- package/dist/react/components/react/Select/Select.d.ts.map +1 -1
- package/dist/react/components/react/index.d.ts +1 -1
- package/dist/react/components/react/index.d.ts.map +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.esm.js +235 -16
- package/dist/react/index.esm.js.map +1 -1
- package/dist/react/index.js +234 -15
- package/dist/react/index.js.map +1 -1
- package/package.json +1 -1
package/dist/react/index.esm.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import require$$0, { forwardRef, useState, useEffect, useMemo,
|
|
1
|
+
import require$$0, { forwardRef, useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
|
|
4
4
|
var jsxRuntime = {exports: {}};
|
|
@@ -1520,22 +1520,241 @@ const LoadingSpinner = () => (jsxRuntimeExports.jsxs("svg", { className: "mds-sp
|
|
|
1520
1520
|
/**
|
|
1521
1521
|
* Select Component (Design System)
|
|
1522
1522
|
*
|
|
1523
|
-
*
|
|
1524
|
-
*
|
|
1525
|
-
*
|
|
1526
|
-
*
|
|
1523
|
+
* Custom dropdown select that renders consistently across all browsers.
|
|
1524
|
+
* Unlike native <select>, this component renders the dropdown via JavaScript,
|
|
1525
|
+
* ensuring proper theming support on Edge/Chrome Windows.
|
|
1526
|
+
*
|
|
1527
|
+
* @example
|
|
1528
|
+
* ```tsx
|
|
1529
|
+
* <Select
|
|
1530
|
+
* options={[
|
|
1531
|
+
* { label: 'Option 1', value: '1' },
|
|
1532
|
+
* { label: 'Option 2', value: '2' },
|
|
1533
|
+
* ]}
|
|
1534
|
+
* value={selectedValue}
|
|
1535
|
+
* onChange={setSelectedValue}
|
|
1536
|
+
* placeholder="Select an option..."
|
|
1537
|
+
* />
|
|
1538
|
+
* ```
|
|
1527
1539
|
*/
|
|
1528
|
-
const Select = forwardRef(({ options,
|
|
1529
|
-
const
|
|
1530
|
-
const
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1540
|
+
const Select = forwardRef(({ options, value, onChange, placeholder = 'Select...', variant = 'themed', size = 'md', disabled = false, loading = false, error = false, className, dropdownClassName, id, name, 'aria-label': ariaLabel, fullWidth = false, searchable = false, searchPlaceholder = 'Search...', maxHeight = 300, zIndex = 1050, }, ref) => {
|
|
1541
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
1542
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
1543
|
+
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
1544
|
+
const [mounted, setMounted] = useState(false);
|
|
1545
|
+
const triggerRef = useRef(null);
|
|
1546
|
+
const dropdownRef = useRef(null);
|
|
1547
|
+
const searchInputRef = useRef(null);
|
|
1548
|
+
const listRef = useRef(null);
|
|
1549
|
+
// Combine refs
|
|
1550
|
+
const combinedRef = (el) => {
|
|
1551
|
+
triggerRef.current = el;
|
|
1552
|
+
if (typeof ref === 'function') {
|
|
1553
|
+
ref(el);
|
|
1554
|
+
}
|
|
1555
|
+
else if (ref) {
|
|
1556
|
+
ref.current = el;
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
// Client-side only
|
|
1560
|
+
useEffect(() => {
|
|
1561
|
+
setMounted(true);
|
|
1562
|
+
}, []);
|
|
1563
|
+
// Filter options based on search
|
|
1564
|
+
const filteredOptions = useMemo(() => {
|
|
1565
|
+
if (!searchTerm)
|
|
1566
|
+
return options;
|
|
1567
|
+
const term = searchTerm.toLowerCase();
|
|
1568
|
+
return options.filter(opt => {
|
|
1569
|
+
const label = typeof opt.label === 'string' ? opt.label : String(opt.value);
|
|
1570
|
+
return label.toLowerCase().includes(term);
|
|
1571
|
+
});
|
|
1572
|
+
}, [options, searchTerm]);
|
|
1573
|
+
// Get selected option label
|
|
1574
|
+
const selectedOption = useMemo(() => {
|
|
1575
|
+
return options.find(opt => opt.value === value);
|
|
1576
|
+
}, [options, value]);
|
|
1577
|
+
// Handle dropdown positioning
|
|
1578
|
+
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 });
|
|
1579
|
+
const updateDropdownPosition = useCallback(() => {
|
|
1580
|
+
if (!triggerRef.current)
|
|
1581
|
+
return;
|
|
1582
|
+
const rect = triggerRef.current.getBoundingClientRect();
|
|
1583
|
+
const viewportHeight = window.innerHeight;
|
|
1584
|
+
const spaceBelow = viewportHeight - rect.bottom;
|
|
1585
|
+
const spaceAbove = rect.top;
|
|
1586
|
+
// Determine if dropdown should open above or below
|
|
1587
|
+
const dropdownHeight = Math.min(maxHeight, filteredOptions.length * 40 + (searchable ? 48 : 0));
|
|
1588
|
+
const openAbove = spaceBelow < dropdownHeight && spaceAbove > spaceBelow;
|
|
1589
|
+
setDropdownPosition({
|
|
1590
|
+
top: openAbove ? rect.top - dropdownHeight : rect.bottom + 4,
|
|
1591
|
+
left: rect.left,
|
|
1592
|
+
width: rect.width,
|
|
1593
|
+
});
|
|
1594
|
+
}, [maxHeight, filteredOptions.length, searchable]);
|
|
1595
|
+
// Open dropdown
|
|
1596
|
+
const openDropdown = useCallback(() => {
|
|
1597
|
+
if (disabled || loading)
|
|
1598
|
+
return;
|
|
1599
|
+
updateDropdownPosition();
|
|
1600
|
+
setIsOpen(true);
|
|
1601
|
+
setSearchTerm('');
|
|
1602
|
+
setHighlightedIndex(value ? filteredOptions.findIndex(opt => opt.value === value) : 0);
|
|
1603
|
+
}, [disabled, loading, updateDropdownPosition, value, filteredOptions]);
|
|
1604
|
+
// Close dropdown
|
|
1605
|
+
const closeDropdown = useCallback(() => {
|
|
1606
|
+
setIsOpen(false);
|
|
1607
|
+
setSearchTerm('');
|
|
1608
|
+
setHighlightedIndex(-1);
|
|
1609
|
+
triggerRef.current?.focus();
|
|
1610
|
+
}, []);
|
|
1611
|
+
// Handle option select
|
|
1612
|
+
const handleSelect = useCallback((optionValue) => {
|
|
1613
|
+
// Safety check: ensure we're passing a string, not an object
|
|
1614
|
+
const safeValue = typeof optionValue === 'string' ? optionValue : String(optionValue);
|
|
1615
|
+
onChange?.(safeValue);
|
|
1616
|
+
closeDropdown();
|
|
1617
|
+
}, [onChange, closeDropdown]);
|
|
1618
|
+
// Keyboard navigation
|
|
1619
|
+
const handleKeyDown = useCallback((e) => {
|
|
1620
|
+
if (disabled || loading)
|
|
1621
|
+
return;
|
|
1622
|
+
switch (e.key) {
|
|
1623
|
+
case 'Enter':
|
|
1624
|
+
case ' ':
|
|
1625
|
+
e.preventDefault();
|
|
1626
|
+
if (isOpen && highlightedIndex >= 0 && filteredOptions[highlightedIndex]) {
|
|
1627
|
+
const opt = filteredOptions[highlightedIndex];
|
|
1628
|
+
if (!opt.disabled) {
|
|
1629
|
+
handleSelect(opt.value);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
else if (!isOpen) {
|
|
1633
|
+
openDropdown();
|
|
1634
|
+
}
|
|
1635
|
+
break;
|
|
1636
|
+
case 'ArrowDown':
|
|
1637
|
+
e.preventDefault();
|
|
1638
|
+
if (!isOpen) {
|
|
1639
|
+
openDropdown();
|
|
1640
|
+
}
|
|
1641
|
+
else {
|
|
1642
|
+
setHighlightedIndex(prev => {
|
|
1643
|
+
const next = prev + 1;
|
|
1644
|
+
return next >= filteredOptions.length ? 0 : next;
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
break;
|
|
1648
|
+
case 'ArrowUp':
|
|
1649
|
+
e.preventDefault();
|
|
1650
|
+
if (isOpen) {
|
|
1651
|
+
setHighlightedIndex(prev => {
|
|
1652
|
+
const next = prev - 1;
|
|
1653
|
+
return next < 0 ? filteredOptions.length - 1 : next;
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
break;
|
|
1657
|
+
case 'Escape':
|
|
1658
|
+
e.preventDefault();
|
|
1659
|
+
closeDropdown();
|
|
1660
|
+
break;
|
|
1661
|
+
case 'Tab':
|
|
1662
|
+
if (isOpen) {
|
|
1663
|
+
closeDropdown();
|
|
1664
|
+
}
|
|
1665
|
+
break;
|
|
1666
|
+
case 'Home':
|
|
1667
|
+
if (isOpen) {
|
|
1668
|
+
e.preventDefault();
|
|
1669
|
+
setHighlightedIndex(0);
|
|
1670
|
+
}
|
|
1671
|
+
break;
|
|
1672
|
+
case 'End':
|
|
1673
|
+
if (isOpen) {
|
|
1674
|
+
e.preventDefault();
|
|
1675
|
+
setHighlightedIndex(filteredOptions.length - 1);
|
|
1676
|
+
}
|
|
1677
|
+
break;
|
|
1678
|
+
}
|
|
1679
|
+
}, [disabled, loading, isOpen, highlightedIndex, filteredOptions, handleSelect, openDropdown, closeDropdown]);
|
|
1680
|
+
// Click outside to close
|
|
1681
|
+
useEffect(() => {
|
|
1682
|
+
if (!isOpen)
|
|
1683
|
+
return;
|
|
1684
|
+
const handleClickOutside = (e) => {
|
|
1685
|
+
if (triggerRef.current?.contains(e.target) ||
|
|
1686
|
+
dropdownRef.current?.contains(e.target)) {
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
closeDropdown();
|
|
1690
|
+
};
|
|
1691
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
1692
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
1693
|
+
}, [isOpen, closeDropdown]);
|
|
1694
|
+
// Update position on scroll/resize
|
|
1695
|
+
useEffect(() => {
|
|
1696
|
+
if (!isOpen)
|
|
1697
|
+
return;
|
|
1698
|
+
const handleUpdate = () => updateDropdownPosition();
|
|
1699
|
+
window.addEventListener('scroll', handleUpdate, true);
|
|
1700
|
+
window.addEventListener('resize', handleUpdate);
|
|
1701
|
+
return () => {
|
|
1702
|
+
window.removeEventListener('scroll', handleUpdate, true);
|
|
1703
|
+
window.removeEventListener('resize', handleUpdate);
|
|
1704
|
+
};
|
|
1705
|
+
}, [isOpen, updateDropdownPosition]);
|
|
1706
|
+
// Focus search input when dropdown opens
|
|
1707
|
+
useEffect(() => {
|
|
1708
|
+
if (isOpen && searchable && searchInputRef.current) {
|
|
1709
|
+
searchInputRef.current.focus();
|
|
1710
|
+
}
|
|
1711
|
+
}, [isOpen, searchable]);
|
|
1712
|
+
// Scroll highlighted option into view
|
|
1713
|
+
useEffect(() => {
|
|
1714
|
+
if (!isOpen || highlightedIndex < 0 || !listRef.current)
|
|
1715
|
+
return;
|
|
1716
|
+
const highlighted = listRef.current.children[highlightedIndex];
|
|
1717
|
+
if (highlighted) {
|
|
1718
|
+
highlighted.scrollIntoView({ block: 'nearest' });
|
|
1719
|
+
}
|
|
1720
|
+
}, [isOpen, highlightedIndex]);
|
|
1721
|
+
// Size classes
|
|
1722
|
+
const sizeClasses = {
|
|
1723
|
+
sm: 'mds-select--sm',
|
|
1724
|
+
md: 'mds-select--md',
|
|
1725
|
+
lg: 'mds-select--lg',
|
|
1726
|
+
};
|
|
1727
|
+
// Variant classes
|
|
1728
|
+
const variantClasses = {
|
|
1729
|
+
base: 'mds-select--base',
|
|
1730
|
+
themed: 'mds-select--themed',
|
|
1731
|
+
dashboard: 'mds-select--themed',
|
|
1732
|
+
};
|
|
1733
|
+
const triggerClasses = cn('mds-select-trigger', sizeClasses[size], variantClasses[variant], isOpen && 'mds-select-trigger--open', disabled && 'mds-select-trigger--disabled', loading && 'mds-select-trigger--loading', error && 'mds-select-trigger--error', fullWidth && 'mds-select-trigger--full-width', className);
|
|
1734
|
+
const dropdownClasses = cn('mds-select-dropdown', variantClasses[variant], dropdownClassName);
|
|
1735
|
+
// Hidden input for form submission
|
|
1736
|
+
const hiddenInput = name ? (jsxRuntimeExports.jsx("input", { type: "hidden", name: name, value: value || '' })) : null;
|
|
1737
|
+
// Dropdown portal content
|
|
1738
|
+
const dropdownContent = isOpen && mounted ? createPortal(jsxRuntimeExports.jsxs("div", { ref: dropdownRef, className: dropdownClasses, style: {
|
|
1739
|
+
position: 'fixed',
|
|
1740
|
+
top: dropdownPosition.top,
|
|
1741
|
+
left: dropdownPosition.left,
|
|
1742
|
+
width: dropdownPosition.width,
|
|
1743
|
+
maxHeight,
|
|
1744
|
+
zIndex,
|
|
1745
|
+
}, role: "listbox", "aria-label": ariaLabel || placeholder, children: [searchable && (jsxRuntimeExports.jsxs("div", { className: "mds-select-search", children: [jsxRuntimeExports.jsx("input", { ref: searchInputRef, type: "text", className: "mds-select-search__input", placeholder: searchPlaceholder, value: searchTerm, onChange: (e) => {
|
|
1746
|
+
setSearchTerm(e.target.value);
|
|
1747
|
+
setHighlightedIndex(0);
|
|
1748
|
+
}, onKeyDown: handleKeyDown, "aria-label": "Search options" }), jsxRuntimeExports.jsx("svg", { className: "mds-select-search__icon", viewBox: "0 0 20 20", fill: "currentColor", children: jsxRuntimeExports.jsx("path", { fillRule: "evenodd", d: "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", clipRule: "evenodd" }) })] })), jsxRuntimeExports.jsx("ul", { ref: listRef, className: "mds-select-options", children: filteredOptions.length === 0 ? (jsxRuntimeExports.jsx("li", { className: "mds-select-option mds-select-option--empty", children: "No options found" })) : (filteredOptions.map((option, index) => (jsxRuntimeExports.jsxs("li", { className: cn('mds-select-option', option.value === value && 'mds-select-option--selected', index === highlightedIndex && 'mds-select-option--highlighted', option.disabled && 'mds-select-option--disabled'), role: "option", "aria-selected": option.value === value, "aria-disabled": option.disabled, onClick: () => {
|
|
1749
|
+
if (!option.disabled) {
|
|
1750
|
+
handleSelect(option.value);
|
|
1751
|
+
}
|
|
1752
|
+
}, onMouseEnter: () => {
|
|
1753
|
+
if (!option.disabled) {
|
|
1754
|
+
setHighlightedIndex(index);
|
|
1755
|
+
}
|
|
1756
|
+
}, children: [jsxRuntimeExports.jsx("span", { className: "mds-select-option__label", children: option.label }), option.value === value && (jsxRuntimeExports.jsx("svg", { className: "mds-select-option__check", viewBox: "0 0 20 20", fill: "currentColor", children: jsxRuntimeExports.jsx("path", { fillRule: "evenodd", d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", clipRule: "evenodd" }) }))] }, option.value)))) })] }), document.body) : null;
|
|
1757
|
+
return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [hiddenInput, jsxRuntimeExports.jsxs("button", { ref: combinedRef, type: "button", id: id, className: triggerClasses, onClick: () => isOpen ? closeDropdown() : openDropdown(), onKeyDown: handleKeyDown, disabled: disabled || loading, "aria-haspopup": "listbox", "aria-expanded": isOpen, "aria-label": ariaLabel, "aria-invalid": error, children: [jsxRuntimeExports.jsx("span", { className: cn('mds-select-trigger__value', !selectedOption && 'mds-select-trigger__placeholder'), children: loading ? 'Loading...' : (selectedOption?.label || placeholder) }), jsxRuntimeExports.jsx("span", { className: "mds-select-trigger__icon", children: loading ? (jsxRuntimeExports.jsx("svg", { className: "mds-select-spinner", viewBox: "0 0 24 24", fill: "none", children: jsxRuntimeExports.jsx("circle", { cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeDasharray: "32", strokeDashoffset: "32", children: jsxRuntimeExports.jsx("animate", { attributeName: "stroke-dashoffset", values: "32;0", dur: "1s", repeatCount: "indefinite" }) }) })) : (jsxRuntimeExports.jsx("svg", { className: "mds-select-chevron", viewBox: "0 0 20 20", fill: "currentColor", children: jsxRuntimeExports.jsx("path", { fillRule: "evenodd", d: "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z", clipRule: "evenodd" }) })) })] }), dropdownContent] }));
|
|
1539
1758
|
});
|
|
1540
1759
|
Select.displayName = 'Select';
|
|
1541
1760
|
|