@sentropic/design-system-react 0.12.0 → 0.14.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.
Files changed (48) hide show
  1. package/dist/BoxPlotChart.d.ts +21 -0
  2. package/dist/BoxPlotChart.d.ts.map +1 -0
  3. package/dist/BoxPlotChart.js +77 -0
  4. package/dist/BoxPlotChart.js.map +1 -0
  5. package/dist/HeatmapChart.d.ts +18 -0
  6. package/dist/HeatmapChart.d.ts.map +1 -0
  7. package/dist/HeatmapChart.js +79 -0
  8. package/dist/HeatmapChart.js.map +1 -0
  9. package/dist/HistogramChart.d.ts +17 -0
  10. package/dist/HistogramChart.d.ts.map +1 -0
  11. package/dist/HistogramChart.js +94 -0
  12. package/dist/HistogramChart.js.map +1 -0
  13. package/dist/Popper.d.ts +18 -1
  14. package/dist/Popper.d.ts.map +1 -1
  15. package/dist/Popper.js +126 -3
  16. package/dist/Popper.js.map +1 -1
  17. package/dist/RadarChart.d.ts +21 -0
  18. package/dist/RadarChart.d.ts.map +1 -0
  19. package/dist/RadarChart.js +66 -0
  20. package/dist/RadarChart.js.map +1 -0
  21. package/dist/SankeyChart.d.ts +23 -0
  22. package/dist/SankeyChart.d.ts.map +1 -0
  23. package/dist/SankeyChart.js +126 -0
  24. package/dist/SankeyChart.js.map +1 -0
  25. package/dist/SelectableList.d.ts.map +1 -1
  26. package/dist/SelectableList.js +47 -15
  27. package/dist/SelectableList.js.map +1 -1
  28. package/dist/SelectableRow.d.ts +8 -6
  29. package/dist/SelectableRow.d.ts.map +1 -1
  30. package/dist/SelectableRow.js +16 -5
  31. package/dist/SelectableRow.js.map +1 -1
  32. package/dist/SunburstChart.d.ts +20 -0
  33. package/dist/SunburstChart.d.ts.map +1 -0
  34. package/dist/SunburstChart.js +125 -0
  35. package/dist/SunburstChart.js.map +1 -0
  36. package/dist/catalog.d.ts.map +1 -1
  37. package/dist/catalog.js +351 -44
  38. package/dist/catalog.js.map +1 -1
  39. package/dist/chartContrast.d.ts +0 -4
  40. package/dist/chartContrast.d.ts.map +1 -1
  41. package/dist/chartContrast.js +4 -56
  42. package/dist/chartContrast.js.map +1 -1
  43. package/dist/index.d.ts +12 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +6 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/styles.css +401 -0
  48. 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 onKeyDown(event) {
1534
+ function onRadioKeyDown(event) {
1531
1535
  if (readonly)
1532
1536
  return;
1533
- const step = allowHalf ? 0.5 : 1;
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
- return (_jsx("div", { ...rest, className: classNames("st-rating", `st-rating--${size}`, readonly && "st-rating--readonly", className), role: "radiogroup", "aria-label": label, "aria-readonly": readonly ? "true" : undefined, children: stars.map((star) => {
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": Math.ceil(current) === star ? "true" : "false", "aria-label": `${star} / ${max}`, tabIndex: !readonly && star === focusedStar ? 0 : -1, disabled: readonly, onClick: (event) => onStarClick(event, star), onKeyDown: onKeyDown, children: state === "half" ? (_jsx(StarHalfIcon, { size: iconSize })) : (_jsx(StarIcon, { size: iconSize, fill: state === "full" ? "currentColor" : "none" })) }, star));
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
- setOpen((prev) => !prev);
1702
+ if (open)
1703
+ closeList(true);
1704
+ else
1705
+ openList();
1614
1706
  }
1615
1707
  function pick(slot) {
1616
1708
  setCurrent(slot);
1617
- setOpen(false);
1709
+ closeList(true);
1618
1710
  }
1619
- useOutsideMouseDown(open, hostRef, () => setOpen(false));
1620
- 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", { 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", onClick: toggleOpen }), _jsx("button", { type: "button", className: "st-timepicker__trigger", "aria-label": "Ouvrir la liste des horaires", "aria-haspopup": "listbox", "aria-expanded": open ? "true" : "false", disabled: disabled, onClick: toggleOpen, children: _jsx(ClockIcon, { size: 16 }) })] })] }), open ? (_jsx("ul", { id: listId, className: "st-timepicker__list", role: "listbox", "aria-label": label ?? "Horaires", tabIndex: -1, onKeyDown: (event) => {
1621
- if (event.key === "Escape") {
1622
- event.preventDefault();
1623
- setOpen(false);
1624
- }
1625
- }, children: slots.map((slot) => (_jsx("li", { role: "presentation", children: _jsx("button", { type: "button", className: classNames("st-timepicker__option", slot === current && "st-timepicker__option--selected"), role: "option", "aria-selected": slot === current ? "true" : "false", onClick: () => pick(slot), children: display(slot) }) }, slot))) })) : null] }));
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
- function previousMonth() {
1734
- if (viewMonth === 0) {
1735
- setViewMonth(11);
1736
- setViewYear((y) => y - 1);
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
- else {
1739
- setViewMonth((m) => m - 1);
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 nextMonth() {
1743
- if (viewMonth === 11) {
1744
- setViewMonth(0);
1745
- setViewYear((y) => y + 1);
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
- else {
1748
- setViewMonth((m) => m + 1);
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", tabIndex: -1, "aria-label": monthLabel, onKeyDown: (event) => {
1770
- if (event.key === "PageUp") {
1771
- event.preventDefault();
1772
- previousMonth();
1773
- }
1774
- else if (event.key === "PageDown") {
1775
- event.preventDefault();
1776
- nextMonth();
1777
- }
1778
- }, 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: grid.map((cell, i) => {
1779
- const oob = isOutOfBounds(cell.date);
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: "tablist", "aria-label": label, children: items.map((index) => (_jsx("button", { type: "button", className: classNames("st-slideIndicator__dot", index === current && "st-slideIndicator__dot--current"), role: "tab", "aria-selected": index === current ? "true" : "false", "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))) }));
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");