@sentropic/design-system-react 0.12.0 → 0.13.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/Popper.d.ts +18 -1
- package/dist/Popper.d.ts.map +1 -1
- package/dist/Popper.js +126 -3
- package/dist/Popper.js.map +1 -1
- package/dist/SelectableList.d.ts.map +1 -1
- package/dist/SelectableList.js +47 -15
- package/dist/SelectableList.js.map +1 -1
- package/dist/SelectableRow.d.ts +8 -6
- package/dist/SelectableRow.d.ts.map +1 -1
- package/dist/SelectableRow.js +16 -5
- package/dist/SelectableRow.js.map +1 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +351 -44
- package/dist/catalog.js.map +1 -1
- package/package.json +1 -1
package/dist/catalog.js
CHANGED
|
@@ -1498,7 +1498,11 @@ export function Rating({ value, max = 5, onChange, readonly = false, allowHalf =
|
|
|
1498
1498
|
const iconSize = size === "sm" ? 16 : size === "lg" ? 28 : 22;
|
|
1499
1499
|
const stars = Array.from({ length: max }, (_, i) => i + 1);
|
|
1500
1500
|
// L'étoile « focusable » (tabindex 0) suit la valeur ; à 0 c'est la première.
|
|
1501
|
+
// En mode entier, focusedStar est toujours >= 1 (pas de radio "0").
|
|
1501
1502
|
const focusedStar = current > 0 ? Math.ceil(current) : 1;
|
|
1503
|
+
// Refs des boutons radio pour déplacer le focus programmatiquement (mode entier).
|
|
1504
|
+
const radioRefs = React.useRef({});
|
|
1505
|
+
const valueText = `${current} / ${max}`;
|
|
1502
1506
|
function fill(star) {
|
|
1503
1507
|
if (current >= star)
|
|
1504
1508
|
return "full";
|
|
@@ -1527,10 +1531,47 @@ export function Rating({ value, max = 5, onChange, readonly = false, allowHalf =
|
|
|
1527
1531
|
}
|
|
1528
1532
|
commit(next);
|
|
1529
1533
|
}
|
|
1530
|
-
function
|
|
1534
|
+
function onRadioKeyDown(event) {
|
|
1531
1535
|
if (readonly)
|
|
1532
1536
|
return;
|
|
1533
|
-
const step =
|
|
1537
|
+
const step = 1;
|
|
1538
|
+
let handled = true;
|
|
1539
|
+
let next = null;
|
|
1540
|
+
switch (event.key) {
|
|
1541
|
+
case "ArrowRight":
|
|
1542
|
+
case "ArrowUp":
|
|
1543
|
+
next = Math.min(max, current + step);
|
|
1544
|
+
break;
|
|
1545
|
+
case "ArrowLeft":
|
|
1546
|
+
case "ArrowDown":
|
|
1547
|
+
// En mode entier, ne pas descendre sous 1 (pas de radio "0").
|
|
1548
|
+
next = Math.max(1, current - step);
|
|
1549
|
+
break;
|
|
1550
|
+
case "Home":
|
|
1551
|
+
// Home → première étoile (1), pas 0 (aucun radio "0" n'existe).
|
|
1552
|
+
next = 1;
|
|
1553
|
+
break;
|
|
1554
|
+
case "End":
|
|
1555
|
+
next = max;
|
|
1556
|
+
break;
|
|
1557
|
+
default:
|
|
1558
|
+
handled = false;
|
|
1559
|
+
}
|
|
1560
|
+
if (handled) {
|
|
1561
|
+
event.preventDefault();
|
|
1562
|
+
if (next !== null) {
|
|
1563
|
+
commit(next);
|
|
1564
|
+
// Déplacer le focus DOM vers le radio cible.
|
|
1565
|
+
const targetEl = radioRefs.current[next > 0 ? Math.ceil(next) : 1];
|
|
1566
|
+
if (targetEl)
|
|
1567
|
+
targetEl.focus();
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
function onSliderKeyDown(event) {
|
|
1572
|
+
if (readonly)
|
|
1573
|
+
return;
|
|
1574
|
+
const step = 0.5;
|
|
1534
1575
|
let handled = true;
|
|
1535
1576
|
switch (event.key) {
|
|
1536
1577
|
case "ArrowRight":
|
|
@@ -1553,9 +1594,24 @@ export function Rating({ value, max = 5, onChange, readonly = false, allowHalf =
|
|
|
1553
1594
|
if (handled)
|
|
1554
1595
|
event.preventDefault();
|
|
1555
1596
|
}
|
|
1556
|
-
|
|
1597
|
+
// Readonly : rendu non interactif avec role="img" et aria-label global.
|
|
1598
|
+
if (readonly) {
|
|
1599
|
+
return (_jsx("div", { ...rest, className: classNames("st-rating", `st-rating--${size}`, "st-rating--readonly", className), role: "img", "aria-label": label ? `${label} : ${valueText}` : valueText, children: stars.map((star) => {
|
|
1600
|
+
const state = fill(star);
|
|
1601
|
+
return (_jsx("span", { className: classNames("st-rating__star", state === "full" && "st-rating__star--full", state === "half" && "st-rating__star--half"), "aria-hidden": "true", children: state === "half" ? (_jsx(StarHalfIcon, { size: iconSize })) : (_jsx(StarIcon, { size: iconSize, fill: state === "full" ? "currentColor" : "none" })) }, star));
|
|
1602
|
+
}) }));
|
|
1603
|
+
}
|
|
1604
|
+
// allowHalf : slider ARIA — valeurs fractionnaires (0.5 step).
|
|
1605
|
+
if (allowHalf) {
|
|
1606
|
+
return (_jsx("div", { ...rest, className: classNames("st-rating", `st-rating--${size}`, className), role: "slider", "aria-label": label, "aria-valuemin": 0, "aria-valuemax": max, "aria-valuenow": current, "aria-valuetext": valueText, tabIndex: 0, onKeyDown: onSliderKeyDown, children: stars.map((star) => {
|
|
1607
|
+
const state = fill(star);
|
|
1608
|
+
return (_jsx("span", { className: classNames("st-rating__star", state === "full" && "st-rating__star--full", state === "half" && "st-rating__star--half"), "aria-hidden": "true", onClick: (event) => onStarClick(event, star), children: state === "half" ? (_jsx(StarHalfIcon, { size: iconSize })) : (_jsx(StarIcon, { size: iconSize, fill: state === "full" ? "currentColor" : "none" })) }, star));
|
|
1609
|
+
}) }));
|
|
1610
|
+
}
|
|
1611
|
+
// Mode entier : radiogroup / radio. aria-checked=true uniquement sur l'étoile == value.
|
|
1612
|
+
return (_jsx("div", { ...rest, className: classNames("st-rating", `st-rating--${size}`, className), role: "radiogroup", "aria-label": label, children: stars.map((star) => {
|
|
1557
1613
|
const state = fill(star);
|
|
1558
|
-
return (_jsx("button", { type: "button", className: classNames("st-rating__star", state === "full" && "st-rating__star--full", state === "half" && "st-rating__star--half"), role: "radio", name: name, "aria-checked":
|
|
1614
|
+
return (_jsx("button", { ref: (el) => { radioRefs.current[star] = el; }, type: "button", className: classNames("st-rating__star", state === "full" && "st-rating__star--full", state === "half" && "st-rating__star--half"), role: "radio", name: name, "aria-checked": current === star ? "true" : "false", "aria-label": `${star} / ${max}`, tabIndex: star === focusedStar ? 0 : -1, onClick: (event) => onStarClick(event, star), onKeyDown: onRadioKeyDown, children: _jsx(StarIcon, { size: iconSize, fill: state === "full" ? "currentColor" : "none" }) }, star));
|
|
1559
1615
|
}) }));
|
|
1560
1616
|
}
|
|
1561
1617
|
function timeToMinutes(hhmm) {
|
|
@@ -1580,8 +1636,12 @@ export function TimePicker({ value, onChange, step = 15, min, max, format = "24"
|
|
|
1580
1636
|
const fieldId = id ?? `st-timepicker-${reactId}`;
|
|
1581
1637
|
const listId = `${fieldId}-list`;
|
|
1582
1638
|
const hostRef = React.useRef(null);
|
|
1639
|
+
const inputRef = React.useRef(null);
|
|
1640
|
+
const listRef = React.useRef(null);
|
|
1583
1641
|
const [open, setOpen] = React.useState(false);
|
|
1584
1642
|
const [current, setCurrent] = useControlled(value, value ?? "", onChange);
|
|
1643
|
+
/** Index de l'option mise en évidence dans la listbox (-1 = aucune). */
|
|
1644
|
+
const [activeIndex, setActiveIndex] = React.useState(-1);
|
|
1585
1645
|
function display(hhmm) {
|
|
1586
1646
|
if (format === "24")
|
|
1587
1647
|
return hhmm;
|
|
@@ -1607,22 +1667,120 @@ export function TimePicker({ value, onChange, step = 15, min, max, format = "24"
|
|
|
1607
1667
|
return result;
|
|
1608
1668
|
}, [step, min, max]);
|
|
1609
1669
|
const displayValue = current ? display(current) : "";
|
|
1670
|
+
/** Id de l'option active, consommé par aria-activedescendant. */
|
|
1671
|
+
const activeDescendant = open && activeIndex >= 0 ? `${listId}-opt-${activeIndex}` : undefined;
|
|
1672
|
+
function scrollActiveIntoView(idx) {
|
|
1673
|
+
const list = listRef.current;
|
|
1674
|
+
if (!list || idx < 0)
|
|
1675
|
+
return;
|
|
1676
|
+
const optEl = list.querySelector(`#${listId}-opt-${idx}`);
|
|
1677
|
+
if (optEl && typeof optEl.scrollIntoView === "function") {
|
|
1678
|
+
optEl.scrollIntoView({ block: "nearest" });
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
function openList() {
|
|
1682
|
+
if (disabled)
|
|
1683
|
+
return;
|
|
1684
|
+
const idx = current ? slots.indexOf(current) : -1;
|
|
1685
|
+
const initIdx = idx >= 0 ? idx : 0;
|
|
1686
|
+
setActiveIndex(initIdx);
|
|
1687
|
+
setOpen(true);
|
|
1688
|
+
// Le focus reste sur l'input (pattern aria-activedescendant).
|
|
1689
|
+
// Scroll après rendu.
|
|
1690
|
+
setTimeout(() => scrollActiveIntoView(initIdx), 0);
|
|
1691
|
+
}
|
|
1692
|
+
function closeList(returnFocus = true) {
|
|
1693
|
+
setOpen(false);
|
|
1694
|
+
setActiveIndex(-1);
|
|
1695
|
+
if (returnFocus && inputRef.current) {
|
|
1696
|
+
inputRef.current.focus();
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1610
1699
|
function toggleOpen() {
|
|
1611
1700
|
if (disabled)
|
|
1612
1701
|
return;
|
|
1613
|
-
|
|
1702
|
+
if (open)
|
|
1703
|
+
closeList(true);
|
|
1704
|
+
else
|
|
1705
|
+
openList();
|
|
1614
1706
|
}
|
|
1615
1707
|
function pick(slot) {
|
|
1616
1708
|
setCurrent(slot);
|
|
1617
|
-
|
|
1709
|
+
closeList(true);
|
|
1618
1710
|
}
|
|
1619
|
-
useOutsideMouseDown(open, hostRef, () =>
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1711
|
+
useOutsideMouseDown(open, hostRef, () => closeList(false));
|
|
1712
|
+
function onInputKeyDown(event) {
|
|
1713
|
+
if (disabled)
|
|
1714
|
+
return;
|
|
1715
|
+
switch (event.key) {
|
|
1716
|
+
case "ArrowDown": {
|
|
1717
|
+
event.preventDefault();
|
|
1718
|
+
if (!open) {
|
|
1719
|
+
openList();
|
|
1720
|
+
}
|
|
1721
|
+
else {
|
|
1722
|
+
const next = Math.min(activeIndex + 1, slots.length - 1);
|
|
1723
|
+
setActiveIndex(next);
|
|
1724
|
+
scrollActiveIntoView(next);
|
|
1725
|
+
}
|
|
1726
|
+
break;
|
|
1727
|
+
}
|
|
1728
|
+
case "ArrowUp": {
|
|
1729
|
+
event.preventDefault();
|
|
1730
|
+
if (!open) {
|
|
1731
|
+
openList();
|
|
1732
|
+
}
|
|
1733
|
+
else {
|
|
1734
|
+
const next = Math.max(activeIndex - 1, 0);
|
|
1735
|
+
setActiveIndex(next);
|
|
1736
|
+
scrollActiveIntoView(next);
|
|
1737
|
+
}
|
|
1738
|
+
break;
|
|
1739
|
+
}
|
|
1740
|
+
case "Home": {
|
|
1741
|
+
event.preventDefault();
|
|
1742
|
+
if (!open) {
|
|
1743
|
+
openList();
|
|
1744
|
+
}
|
|
1745
|
+
else {
|
|
1746
|
+
setActiveIndex(0);
|
|
1747
|
+
scrollActiveIntoView(0);
|
|
1748
|
+
}
|
|
1749
|
+
break;
|
|
1750
|
+
}
|
|
1751
|
+
case "End": {
|
|
1752
|
+
event.preventDefault();
|
|
1753
|
+
if (!open) {
|
|
1754
|
+
openList();
|
|
1755
|
+
}
|
|
1756
|
+
else {
|
|
1757
|
+
const last = slots.length - 1;
|
|
1758
|
+
setActiveIndex(last);
|
|
1759
|
+
scrollActiveIntoView(last);
|
|
1760
|
+
}
|
|
1761
|
+
break;
|
|
1762
|
+
}
|
|
1763
|
+
case "Enter":
|
|
1764
|
+
case " ": {
|
|
1765
|
+
event.preventDefault();
|
|
1766
|
+
if (!open) {
|
|
1767
|
+
openList();
|
|
1768
|
+
}
|
|
1769
|
+
else if (activeIndex >= 0 && activeIndex < slots.length) {
|
|
1770
|
+
pick(slots[activeIndex]);
|
|
1771
|
+
}
|
|
1772
|
+
break;
|
|
1773
|
+
}
|
|
1774
|
+
case "Escape": {
|
|
1775
|
+
if (open) {
|
|
1776
|
+
event.preventDefault();
|
|
1777
|
+
closeList(true);
|
|
1778
|
+
}
|
|
1779
|
+
break;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
return (_jsxs("div", { className: classNames("st-field", className), ref: hostRef, ...rest, children: [_jsxs("div", { className: "st-field__control", children: [label ? (_jsx("label", { className: "st-field__label", htmlFor: fieldId, children: label })) : null, _jsxs("span", { className: classNames("st-timepicker", `st-timepicker--${size}`), children: [_jsx("input", { ref: inputRef, id: fieldId, type: "text", readOnly: true, className: "st-timepicker__control", value: displayValue, placeholder: format === "24" ? "HH:mm" : "hh:mm AM", disabled: disabled, role: "combobox", "aria-haspopup": "listbox", "aria-controls": listId, "aria-expanded": open ? "true" : "false", "aria-activedescendant": activeDescendant, "aria-autocomplete": "none", onClick: toggleOpen, onKeyDown: onInputKeyDown }), _jsx("button", { type: "button", className: "st-timepicker__trigger", "aria-label": "Ouvrir la liste des horaires", "aria-haspopup": "listbox", "aria-expanded": open ? "true" : "false", tabIndex: -1, disabled: disabled, onClick: toggleOpen, children: _jsx(ClockIcon, { size: 16 }) })] })] }), open ? (_jsx("ul", { ref: listRef, id: listId, className: "st-timepicker__list", role: "listbox", "aria-label": label ?? "Horaires", tabIndex: -1, children: slots.map((slot, i) => (_jsx("li", { role: "presentation", children: _jsx("div", { id: `${listId}-opt-${i}`, className: classNames("st-timepicker__option", slot === current && "st-timepicker__option--selected", i === activeIndex && "st-timepicker__option--active"), role: "option", "aria-selected": slot === current ? "true" : "false", tabIndex: -1, onMouseDown: (e) => e.preventDefault(), onClick: () => pick(slot), onMouseEnter: () => setActiveIndex(i), children: display(slot) }) }, slot))) })) : null] }));
|
|
1626
1784
|
}
|
|
1627
1785
|
function ClockIcon({ size }) {
|
|
1628
1786
|
return (_jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("polyline", { points: "12 6 12 12 16 14" })] }));
|
|
@@ -1709,8 +1867,8 @@ export function Calendar({ value, onChange, min, max, range = false, weekStartsO
|
|
|
1709
1867
|
}
|
|
1710
1868
|
return cells;
|
|
1711
1869
|
}, [viewYear, viewMonth, weekStartsOn]);
|
|
1712
|
-
const minDate = calParseISO(min);
|
|
1713
|
-
const maxDate = calParseISO(max);
|
|
1870
|
+
const minDate = React.useMemo(() => calParseISO(min), [min]);
|
|
1871
|
+
const maxDate = React.useMemo(() => calParseISO(max), [max]);
|
|
1714
1872
|
function isOutOfBounds(date) {
|
|
1715
1873
|
const d = calStartOfDay(date).getTime();
|
|
1716
1874
|
if (minDate && d < minDate.getTime())
|
|
@@ -1730,23 +1888,106 @@ export function Calendar({ value, onChange, min, max, range = false, weekStartsO
|
|
|
1730
1888
|
const d = calStartOfDay(date).getTime();
|
|
1731
1889
|
return d > rangeStart.getTime() && d < rangeEnd.getTime();
|
|
1732
1890
|
}
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1891
|
+
// --- Roving tabindex : date active dans la grille -------------------------
|
|
1892
|
+
// INVARIANT : focusDate est toujours dans le mois affiché ET non-disabled.
|
|
1893
|
+
function calClampToMonth(preferred, y, m) {
|
|
1894
|
+
if (preferred.getFullYear() === y &&
|
|
1895
|
+
preferred.getMonth() === m &&
|
|
1896
|
+
!isOutOfBounds(preferred)) {
|
|
1897
|
+
return preferred;
|
|
1737
1898
|
}
|
|
1738
|
-
|
|
1739
|
-
|
|
1899
|
+
// Chercher le jour sélectionné dans ce mois en priorité.
|
|
1900
|
+
const sel = !range ? single : rangeStart;
|
|
1901
|
+
if (sel && sel.getFullYear() === y && sel.getMonth() === m && !isOutOfBounds(sel)) {
|
|
1902
|
+
return sel;
|
|
1740
1903
|
}
|
|
1904
|
+
const lastDay = new Date(y, m + 1, 0).getDate();
|
|
1905
|
+
for (let dd = 1; dd <= lastDay; dd++) {
|
|
1906
|
+
const candidate = calStartOfDay(new Date(y, m, dd));
|
|
1907
|
+
if (!isOutOfBounds(candidate))
|
|
1908
|
+
return candidate;
|
|
1909
|
+
}
|
|
1910
|
+
return null;
|
|
1741
1911
|
}
|
|
1742
|
-
function
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1912
|
+
function calInitialFocusDate() {
|
|
1913
|
+
const sel = !range ? single : rangeStart;
|
|
1914
|
+
if (sel && sel.getFullYear() === viewYear && sel.getMonth() === viewMonth && !isOutOfBounds(sel)) {
|
|
1915
|
+
return sel;
|
|
1746
1916
|
}
|
|
1747
|
-
|
|
1748
|
-
|
|
1917
|
+
const lastDay = new Date(viewYear, viewMonth + 1, 0).getDate();
|
|
1918
|
+
for (let dd = 1; dd <= lastDay; dd++) {
|
|
1919
|
+
const candidate = calStartOfDay(new Date(viewYear, viewMonth, dd));
|
|
1920
|
+
if (!isOutOfBounds(candidate))
|
|
1921
|
+
return candidate;
|
|
1749
1922
|
}
|
|
1923
|
+
return calStartOfDay(new Date(viewYear, viewMonth, 1));
|
|
1924
|
+
}
|
|
1925
|
+
const [focusDate, setFocusDate] = React.useState(calInitialFocusDate);
|
|
1926
|
+
const gridRef = React.useRef(null);
|
|
1927
|
+
// Resynchronise focusDate quand viewYear/viewMonth change.
|
|
1928
|
+
// Utilise une ref pour éviter les dépendances circulaires dans l'effet.
|
|
1929
|
+
const isOutOfBoundsRef = React.useRef(isOutOfBounds);
|
|
1930
|
+
isOutOfBoundsRef.current = isOutOfBounds;
|
|
1931
|
+
const calClampToMonthRef = React.useRef(calClampToMonth);
|
|
1932
|
+
calClampToMonthRef.current = calClampToMonth;
|
|
1933
|
+
React.useEffect(() => {
|
|
1934
|
+
setFocusDate((prev) => {
|
|
1935
|
+
const clamped = calClampToMonthRef.current(prev, viewYear, viewMonth);
|
|
1936
|
+
return clamped ?? prev;
|
|
1937
|
+
});
|
|
1938
|
+
}, [viewYear, viewMonth]);
|
|
1939
|
+
function focusActiveCell() {
|
|
1940
|
+
const grid = gridRef.current;
|
|
1941
|
+
if (!grid)
|
|
1942
|
+
return;
|
|
1943
|
+
const iso = calToISO(focusDate);
|
|
1944
|
+
const btn = grid.querySelector(`[data-date="${iso}"]`);
|
|
1945
|
+
btn?.focus();
|
|
1946
|
+
}
|
|
1947
|
+
function moveFocus(deltaDays) {
|
|
1948
|
+
const next = calStartOfDay(new Date(focusDate));
|
|
1949
|
+
next.setDate(next.getDate() + deltaDays);
|
|
1950
|
+
let nextYear = viewYear;
|
|
1951
|
+
let nextMonth = viewMonth;
|
|
1952
|
+
if (next.getFullYear() !== viewYear || next.getMonth() !== viewMonth) {
|
|
1953
|
+
nextYear = next.getFullYear();
|
|
1954
|
+
nextMonth = next.getMonth();
|
|
1955
|
+
if (nextMonth > viewMonth || nextYear > viewYear) {
|
|
1956
|
+
// Next month
|
|
1957
|
+
if (viewMonth === 11) {
|
|
1958
|
+
setViewMonth(0);
|
|
1959
|
+
setViewYear((y) => y + 1);
|
|
1960
|
+
}
|
|
1961
|
+
else {
|
|
1962
|
+
setViewMonth((m) => m + 1);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
else {
|
|
1966
|
+
// Previous month
|
|
1967
|
+
if (viewMonth === 0) {
|
|
1968
|
+
setViewMonth(11);
|
|
1969
|
+
setViewYear((y) => y - 1);
|
|
1970
|
+
}
|
|
1971
|
+
else {
|
|
1972
|
+
setViewMonth((m) => m - 1);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
setFocusDate(next);
|
|
1977
|
+
setTimeout(focusActiveCell, 0);
|
|
1978
|
+
}
|
|
1979
|
+
function previousMonth() {
|
|
1980
|
+
const targetMonth = viewMonth === 0 ? 11 : viewMonth - 1;
|
|
1981
|
+
const targetYear = viewMonth === 0 ? viewYear - 1 : viewYear;
|
|
1982
|
+
setViewMonth(targetMonth);
|
|
1983
|
+
setViewYear(targetYear);
|
|
1984
|
+
// focusDate will be re-clamped by the viewYear/viewMonth effect
|
|
1985
|
+
}
|
|
1986
|
+
function nextMonth() {
|
|
1987
|
+
const targetMonth = viewMonth === 11 ? 0 : viewMonth + 1;
|
|
1988
|
+
const targetYear = viewMonth === 11 ? viewYear + 1 : viewYear;
|
|
1989
|
+
setViewMonth(targetMonth);
|
|
1990
|
+
setViewYear(targetYear);
|
|
1750
1991
|
}
|
|
1751
1992
|
function pickDate(date) {
|
|
1752
1993
|
if (isOutOfBounds(date))
|
|
@@ -1765,23 +2006,83 @@ export function Calendar({ value, onChange, min, max, range = false, weekStartsO
|
|
|
1765
2006
|
}
|
|
1766
2007
|
setCurrent([calToISO(rangeStart), iso]);
|
|
1767
2008
|
}
|
|
2009
|
+
function onKeyDown(event) {
|
|
2010
|
+
switch (event.key) {
|
|
2011
|
+
case "ArrowLeft":
|
|
2012
|
+
event.preventDefault();
|
|
2013
|
+
moveFocus(-1);
|
|
2014
|
+
break;
|
|
2015
|
+
case "ArrowRight":
|
|
2016
|
+
event.preventDefault();
|
|
2017
|
+
moveFocus(1);
|
|
2018
|
+
break;
|
|
2019
|
+
case "ArrowUp":
|
|
2020
|
+
event.preventDefault();
|
|
2021
|
+
moveFocus(-7);
|
|
2022
|
+
break;
|
|
2023
|
+
case "ArrowDown":
|
|
2024
|
+
event.preventDefault();
|
|
2025
|
+
moveFocus(7);
|
|
2026
|
+
break;
|
|
2027
|
+
case "Home": {
|
|
2028
|
+
event.preventDefault();
|
|
2029
|
+
const dayOfWeek = focusDate.getDay();
|
|
2030
|
+
const offset = (dayOfWeek - weekStartsOn + 7) % 7;
|
|
2031
|
+
moveFocus(-offset);
|
|
2032
|
+
break;
|
|
2033
|
+
}
|
|
2034
|
+
case "End": {
|
|
2035
|
+
event.preventDefault();
|
|
2036
|
+
const dayOfWeek = focusDate.getDay();
|
|
2037
|
+
const offset = 6 - ((dayOfWeek - weekStartsOn + 7) % 7);
|
|
2038
|
+
moveFocus(offset);
|
|
2039
|
+
break;
|
|
2040
|
+
}
|
|
2041
|
+
case "PageUp": {
|
|
2042
|
+
event.preventDefault();
|
|
2043
|
+
const puDay = focusDate.getDate();
|
|
2044
|
+
const puTargetMonth = viewMonth === 0 ? 11 : viewMonth - 1;
|
|
2045
|
+
const puTargetYear = viewMonth === 0 ? viewYear - 1 : viewYear;
|
|
2046
|
+
const puLastDay = new Date(puTargetYear, puTargetMonth + 1, 0).getDate();
|
|
2047
|
+
const puNext = calStartOfDay(new Date(puTargetYear, puTargetMonth, Math.min(puDay, puLastDay)));
|
|
2048
|
+
setFocusDate(puNext);
|
|
2049
|
+
previousMonth();
|
|
2050
|
+
setTimeout(focusActiveCell, 0);
|
|
2051
|
+
break;
|
|
2052
|
+
}
|
|
2053
|
+
case "PageDown": {
|
|
2054
|
+
event.preventDefault();
|
|
2055
|
+
const pdDay = focusDate.getDate();
|
|
2056
|
+
const pdTargetMonth = viewMonth === 11 ? 0 : viewMonth + 1;
|
|
2057
|
+
const pdTargetYear = viewMonth === 11 ? viewYear + 1 : viewYear;
|
|
2058
|
+
const pdLastDay = new Date(pdTargetYear, pdTargetMonth + 1, 0).getDate();
|
|
2059
|
+
const pdNext = calStartOfDay(new Date(pdTargetYear, pdTargetMonth, Math.min(pdDay, pdLastDay)));
|
|
2060
|
+
setFocusDate(pdNext);
|
|
2061
|
+
nextMonth();
|
|
2062
|
+
setTimeout(focusActiveCell, 0);
|
|
2063
|
+
break;
|
|
2064
|
+
}
|
|
2065
|
+
case "Enter":
|
|
2066
|
+
case " ": {
|
|
2067
|
+
event.preventDefault();
|
|
2068
|
+
if (!isOutOfBounds(focusDate))
|
|
2069
|
+
pickDate(focusDate);
|
|
2070
|
+
break;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
1768
2074
|
const monthLabel = monthFormatter.format(new Date(viewYear, viewMonth, 1));
|
|
1769
|
-
return (_jsxs("div", { className: classNames("st-calendar", className), ...rest, children: [_jsxs("div", { className: "st-calendar__nav", children: [_jsx("button", { type: "button", className: "st-calendar__navBtn", "aria-label": resolvedPrevLabel, onClick: previousMonth, children: _jsx(ChevronLeftIcon, { size: 18 }) }), _jsx("span", { className: "st-calendar__monthLabel", "aria-live": "polite", children: monthLabel }), _jsx("button", { type: "button", className: "st-calendar__navBtn", "aria-label": resolvedNextLabel, onClick: nextMonth, children: _jsx(ChevronRightIcon, { size: 18 }) })] }), _jsxs("div", { className: "st-calendar__grid", role: "grid",
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
const selected = isSelected(cell.date);
|
|
1781
|
-
const inRange = isInRange(cell.date);
|
|
1782
|
-
const isToday = calIsSameDay(cell.date, today);
|
|
1783
|
-
return (_jsx("button", { type: "button", className: classNames("st-calendar__day", !cell.inMonth && "st-calendar__day--outside", selected && "st-calendar__day--selected", inRange && "st-calendar__day--inRange", isToday && "st-calendar__day--today"), role: "gridcell", "aria-label": cellFormatter.format(cell.date), "aria-selected": selected ? "true" : "false", "aria-current": isToday ? "date" : undefined, "aria-disabled": oob ? "true" : undefined, disabled: oob, onClick: () => pickDate(cell.date), children: cell.date.getDate() }, i));
|
|
1784
|
-
}) })] })] }));
|
|
2075
|
+
return (_jsxs("div", { className: classNames("st-calendar", className), ...rest, children: [_jsxs("div", { className: "st-calendar__nav", children: [_jsx("button", { type: "button", className: "st-calendar__navBtn", "aria-label": resolvedPrevLabel, onClick: previousMonth, children: _jsx(ChevronLeftIcon, { size: 18 }) }), _jsx("span", { className: "st-calendar__monthLabel", "aria-live": "polite", children: monthLabel }), _jsx("button", { type: "button", className: "st-calendar__navBtn", "aria-label": resolvedNextLabel, onClick: nextMonth, children: _jsx(ChevronRightIcon, { size: 18 }) })] }), _jsxs("div", { ref: gridRef, className: "st-calendar__grid", role: "grid", "aria-label": monthLabel, onKeyDown: onKeyDown, children: [_jsx("div", { className: "st-calendar__weekdays", role: "row", children: weekdayLabels.map((wd, i) => (_jsx("span", { className: "st-calendar__weekday", role: "columnheader", children: wd }, `${wd}-${i}`))) }), _jsx("div", { className: "st-calendar__days", children: Array.from({ length: 6 }, (_, rowIdx) => (_jsx("div", { className: "st-calendar__week", role: "row", children: grid.slice(rowIdx * 7, rowIdx * 7 + 7).map((cell, colIdx) => {
|
|
2076
|
+
const oob = isOutOfBounds(cell.date);
|
|
2077
|
+
const selected = isSelected(cell.date);
|
|
2078
|
+
const inRange = isInRange(cell.date);
|
|
2079
|
+
const isToday = calIsSameDay(cell.date, today);
|
|
2080
|
+
const isActive = calIsSameDay(cell.date, focusDate);
|
|
2081
|
+
return (_jsx("button", { type: "button", className: classNames("st-calendar__day", !cell.inMonth && "st-calendar__day--outside", selected && "st-calendar__day--selected", inRange && "st-calendar__day--inRange", isToday && "st-calendar__day--today"), role: "gridcell", "aria-label": cellFormatter.format(cell.date), "aria-selected": selected ? "true" : "false", "aria-current": isToday ? "date" : undefined, "aria-disabled": oob ? "true" : undefined, disabled: oob, tabIndex: isActive && !oob ? 0 : -1, "data-date": calToISO(cell.date), onClick: () => {
|
|
2082
|
+
setFocusDate(calStartOfDay(cell.date));
|
|
2083
|
+
pickDate(cell.date);
|
|
2084
|
+
}, children: cell.date.getDate() }, rowIdx * 7 + colIdx));
|
|
2085
|
+
}) }, rowIdx))) })] })] }));
|
|
1785
2086
|
}
|
|
1786
2087
|
function ChevronLeftIcon({ size }) {
|
|
1787
2088
|
return (_jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("polyline", { points: "15 18 9 12 15 6" }) }));
|
|
@@ -1791,6 +2092,8 @@ function ChevronRightIcon({ size }) {
|
|
|
1791
2092
|
}
|
|
1792
2093
|
export function SlideIndicator({ count, current = 0, onChange, size = "md", variant = "dots", label = "Diapositive", className, ...rest }) {
|
|
1793
2094
|
const items = Array.from({ length: Math.max(0, count) }, (_, i) => i);
|
|
2095
|
+
// Refs des boutons pour déplacer le focus programmatiquement lors de la navigation clavier.
|
|
2096
|
+
const buttonRefs = React.useRef({});
|
|
1794
2097
|
function select(index) {
|
|
1795
2098
|
if (index < 0 || index >= count || index === current)
|
|
1796
2099
|
return;
|
|
@@ -1817,9 +2120,13 @@ export function SlideIndicator({ count, current = 0, onChange, size = "md", vari
|
|
|
1817
2120
|
return;
|
|
1818
2121
|
}
|
|
1819
2122
|
event.preventDefault();
|
|
2123
|
+
// Déplacer le focus DOM vers le bouton cible (roving tabindex correct).
|
|
2124
|
+
const targetEl = buttonRefs.current[target];
|
|
2125
|
+
if (targetEl)
|
|
2126
|
+
targetEl.focus();
|
|
1820
2127
|
select(target);
|
|
1821
2128
|
}
|
|
1822
|
-
return (_jsx("div", { ...rest, className: classNames("st-slideIndicator", `st-slideIndicator--${size}`, `st-slideIndicator--${variant}`, className), role: "
|
|
2129
|
+
return (_jsx("div", { ...rest, className: classNames("st-slideIndicator", `st-slideIndicator--${size}`, `st-slideIndicator--${variant}`, className), role: "group", "aria-label": label, children: items.map((index) => (_jsx("button", { ref: (el) => { buttonRefs.current[index] = el; }, type: "button", className: classNames("st-slideIndicator__dot", index === current && "st-slideIndicator__dot--current"), "aria-current": index === current ? "true" : undefined, "aria-label": `${label} ${index + 1}`, tabIndex: index === current ? 0 : -1, onClick: () => select(index), onKeyDown: (event) => onKeyDown(event, index) }, index))) }));
|
|
1823
2130
|
}
|
|
1824
2131
|
export function Autosave({ status = "idle", lastSaved, onRetry, labels, retryLabel, locale = "fr-FR", className, ...rest }) {
|
|
1825
2132
|
const isFr = (locale ?? "fr-FR").toLowerCase().startsWith("fr");
|