gridsmith-ui 0.14.0 → 0.15.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.js CHANGED
@@ -1,20 +1,20 @@
1
- import { useAppShell, ControlSizeContext, Button, Spinner, OverlayCloseCtx, DrawerFooterCtx, useCompact, useElementWidth, Sheet, useCloseSettle, Drawer, useRememberedWidth, pxOf, ResizeEdge, useDirtyGuard, DiscardConfirm, defaultMaxWidth } from './chunk-JSQPVRHQ.js';
2
- export { AppShell, Button, ButtonSizeContext, ControlSizeContext, Drawer, Sheet, SkipLink, Spinner, useAppShell } from './chunk-JSQPVRHQ.js';
1
+ import { useAppShell, ControlSizeContext, Button, Spinner, OverlayCloseCtx, DrawerFooterCtx, useCompact, useElementWidth, Sheet, useCloseSettle, Drawer, useRememberedWidth, pxOf, ResizeEdge, useDirtyGuard, DiscardConfirm, defaultMaxWidth } from './chunk-ATONRIEB.js';
2
+ export { AppShell, Button, ButtonSizeContext, ControlSizeContext, Drawer, Sheet, SkipLink, Spinner, useAppShell } from './chunk-ATONRIEB.js';
3
3
  import { Skeleton } from './chunk-M3AARGLI.js';
4
4
  export { Skeleton } from './chunk-M3AARGLI.js';
5
- import { useOverlayLayer, useCloseOnFocusOut, focusInitial, rovingTabStop, OVERLAY_CLOSE_ATTR, useNonModalDialog, useFocusTrap } from './chunk-NHQCMOAN.js';
6
- import { Link } from './chunk-QJBCZ7BR.js';
7
- export { CodeBlock, Divider, Link } from './chunk-QJBCZ7BR.js';
8
- import { Badge } from './chunk-U2JT37GJ.js';
9
- export { Badge } from './chunk-U2JT37GJ.js';
10
- import { inferColumnKinds, CardValue } from './chunk-CR2KF73R.js';
5
+ import { usePopupFocus, focusInitial, focusAfterRemoval, useOverlayLayer, rovingTabStop, useGridKeyboard, OVERLAY_CLOSE_ATTR, useNonModalDialog, useFocusTrap, useFocusBeforeOpen, rememberFocus, useReturnFocus } from './chunk-VFAKME7Q.js';
6
+ import { Link } from './chunk-LU3YRNES.js';
7
+ export { CodeBlock, Divider, Link } from './chunk-LU3YRNES.js';
8
+ import { Badge } from './chunk-XCSTU6IJ.js';
9
+ export { Badge } from './chunk-XCSTU6IJ.js';
10
+ import { inferColumnKinds, CardValue } from './chunk-OENJCGTJ.js';
11
11
  import { SplitPaneCtx, useShellPreferences, TableCardCtx, PREFERENCE_HOUSE_DEFAULTS, preferenceActionKey } from './chunk-TKVTZWBN.js';
12
12
  export { PreferencesContext, preferenceActionKey, usePreferences } from './chunk-TKVTZWBN.js';
13
13
  import { useBreakpoint } from './chunk-CRQ6KCRD.js';
14
14
  export { BreakpointWidthProvider, useBreakpoint } from './chunk-CRQ6KCRD.js';
15
- import { useTextOverflow, Tooltip, usePosition } from './chunk-IYD6ZDM2.js';
16
- export { Tooltip, useTextOverflow } from './chunk-IYD6ZDM2.js';
17
- import { mergeRefs, Slot } from './chunk-PKLVP5ZB.js';
15
+ import { useTextOverflow, Tooltip, usePosition } from './chunk-EMCSARQO.js';
16
+ export { Tooltip, useTextOverflow } from './chunk-EMCSARQO.js';
17
+ import { mergeRefs, Slot } from './chunk-7Q46JXSK.js';
18
18
  import { useLatest } from './chunk-WH2FUFAC.js';
19
19
  import { Icon } from './chunk-X4CNZFWC.js';
20
20
  import { cn, GLASS_SURFACE, ACTIVE_RING, GLASS_BACKDROP, GLASS_DROPDOWN, ERROR_ACTIVE_RING, ERROR_FOCUS_RING, FOCUS_RING, ERROR_HAS_FOCUS_RING, HAS_FOCUS_RING, ERROR_FOCUS_WITHIN_RING, FOCUS_WITHIN_RING, lockBodyScroll, unlockBodyScroll, GLASS_OVERLAY } from './chunk-4KVFH5KC.js';
@@ -40,14 +40,13 @@ function DropdownMenu({ trigger, items, align = "end", className, container }) {
40
40
  setPrevOpen(open);
41
41
  if (!open) setFocusedIdx(-1);
42
42
  }
43
- const isTopLayer = useOverlayLayer(open);
44
- useCloseOnFocusOut(open, triggerRef, menuRef, () => setOpen(false), isTopLayer);
43
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: menuRef, close: () => setOpen(false), tabExits: "any" });
45
44
  useEffect(() => {
46
45
  if (!open) return;
47
46
  const onKeyDown = (e) => {
48
47
  if (e.key === "Escape" && isTopLayer()) {
48
+ returnFocus();
49
49
  setOpen(false);
50
- triggerRef.current?.focus();
51
50
  }
52
51
  };
53
52
  const onClick = (e) => {
@@ -61,11 +60,10 @@ function DropdownMenu({ trigger, items, align = "end", className, container }) {
61
60
  document.removeEventListener("keydown", onKeyDown);
62
61
  document.removeEventListener("mousedown", onClick);
63
62
  };
64
- }, [open, isTopLayer]);
63
+ }, [open, isTopLayer, returnFocus]);
65
64
  useEffect(() => {
66
65
  if (open && focusedIdx >= 0) {
67
- const buttons = menuRef.current?.querySelectorAll('[role="menuitem"]');
68
- buttons?.[focusedIdx]?.focus();
66
+ menuRef.current?.querySelector(`[data-index="${focusedIdx}"]`)?.focus();
69
67
  }
70
68
  }, [open, focusedIdx]);
71
69
  function handleMenuKeyDown(e) {
@@ -108,6 +106,7 @@ function DropdownMenu({ trigger, items, align = "end", className, container }) {
108
106
  className
109
107
  ),
110
108
  style: { top: pos.top, left: pos.left },
109
+ onClick: (e) => e.stopPropagation(),
111
110
  children: items.map((item, i) => {
112
111
  if (isDivider(item)) {
113
112
  return /* @__PURE__ */ jsx("div", { role: "separator", className: "my-ds-1 h-px bg-[var(--ds-border-default)]" }, i);
@@ -115,11 +114,13 @@ function DropdownMenu({ trigger, items, align = "end", className, container }) {
115
114
  return /* @__PURE__ */ jsxs(
116
115
  "button",
117
116
  {
117
+ "data-index": i,
118
118
  role: "menuitem",
119
119
  disabled: item.disabled,
120
120
  tabIndex: focusedIdx === i ? 0 : -1,
121
121
  onClick: () => {
122
122
  if (!item.disabled) {
123
+ returnFocus();
123
124
  item.onClick();
124
125
  setOpen(false);
125
126
  }
@@ -465,13 +466,7 @@ function Dropdown({
465
466
  useEffect(() => {
466
467
  if (!open) return;
467
468
  updatePos();
468
- requestAnimationFrame(() => {
469
- if (searchRef.current) {
470
- searchRef.current.focus();
471
- } else {
472
- const els = Array.from(menuRef.current?.querySelectorAll('[role="option"]') ?? []);
473
- (els.find((el) => el.getAttribute("aria-selected") === "true") ?? els[0])?.focus();
474
- }
469
+ const raf = requestAnimationFrame(() => {
475
470
  updatePos();
476
471
  setPositioned(true);
477
472
  });
@@ -479,10 +474,22 @@ function Dropdown({
479
474
  window.addEventListener("scroll", onScroll, true);
480
475
  window.addEventListener("resize", updatePos);
481
476
  return () => {
477
+ cancelAnimationFrame(raf);
482
478
  window.removeEventListener("scroll", onScroll, true);
483
479
  window.removeEventListener("resize", updatePos);
484
480
  };
485
481
  }, [open, updatePos]);
482
+ const moved = useRef(false);
483
+ useLayoutEffect(() => {
484
+ if (!open || !positioned) return;
485
+ moved.current = false;
486
+ if (searchRef.current) {
487
+ searchRef.current.focus();
488
+ return;
489
+ }
490
+ const els = Array.from(menuRef.current?.querySelectorAll('[role="option"]') ?? []);
491
+ (els.find((el) => el.getAttribute("aria-selected") === "true") ?? els[0])?.focus();
492
+ }, [open, positioned]);
486
493
  useEffect(() => {
487
494
  if (!open) return;
488
495
  const handler = (e) => {
@@ -493,7 +500,13 @@ function Dropdown({
493
500
  document.addEventListener("mousedown", handler);
494
501
  return () => document.removeEventListener("mousedown", handler);
495
502
  }, [open]);
496
- const isTopLayer = useOverlayLayer(open);
503
+ const commitMoved = () => {
504
+ if (multiple || !moved.current) return;
505
+ const el = document.activeElement;
506
+ const id = el instanceof HTMLElement && menuRef.current?.contains(el) ? el.getAttribute("data-option-id") : null;
507
+ if (id) onChange(id);
508
+ };
509
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: menuRef, close: () => setOpen(false), tabExits: "any", onTabOut: commitMoved });
497
510
  const typeahead = useRef({ buffer: "", at: 0 });
498
511
  useEffect(() => {
499
512
  if (!open) return;
@@ -502,6 +515,7 @@ function Dropdown({
502
515
  const els = optionButtons();
503
516
  if (els.length === 0) return;
504
517
  els[(idx % els.length + els.length) % els.length].focus();
518
+ moved.current = true;
505
519
  };
506
520
  const onKey = (e) => {
507
521
  const inMenu = menuRef.current?.contains(document.activeElement) || triggerRef.current === document.activeElement;
@@ -511,8 +525,8 @@ function Dropdown({
511
525
  if (e.key === "Escape") {
512
526
  if (!isTopLayer()) return;
513
527
  e.preventDefault();
528
+ returnFocus();
514
529
  setOpen(false);
515
- triggerRef.current?.focus();
516
530
  return;
517
531
  }
518
532
  if (e.key === "ArrowDown") {
@@ -536,10 +550,6 @@ function Dropdown({
536
550
  focusOption(els.length - 1);
537
551
  return;
538
552
  }
539
- if (e.key === "Tab") {
540
- setOpen(false);
541
- return;
542
- }
543
553
  if (!searchable && e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) {
544
554
  const now = Date.now();
545
555
  typeahead.current.buffer = now - typeahead.current.at > 700 ? e.key : typeahead.current.buffer + e.key;
@@ -554,7 +564,7 @@ function Dropdown({
554
564
  };
555
565
  document.addEventListener("keydown", onKey);
556
566
  return () => document.removeEventListener("keydown", onKey);
557
- }, [open, searchable, isTopLayer]);
567
+ }, [open, searchable, isTopLayer, returnFocus]);
558
568
  const filtered = searchable && search ? options.filter((o) => o.label.toLowerCase().includes(search.toLowerCase())) : options;
559
569
  const groups = /* @__PURE__ */ new Map();
560
570
  for (const opt of filtered) {
@@ -568,10 +578,21 @@ function Dropdown({
568
578
  const arr = selectedValues.includes(id) ? selectedValues.filter((v) => v !== id) : [...selectedValues, id];
569
579
  onChange(arr);
570
580
  } else {
581
+ returnFocus();
571
582
  onChange(id);
572
583
  setOpen(false);
573
584
  }
574
585
  };
586
+ const chipsRef = useRef(null);
587
+ const pendingChipFocus = useRef(null);
588
+ const chipCount = selectedValues.length;
589
+ useLayoutEffect(() => {
590
+ const index = pendingChipFocus.current;
591
+ if (index === null) return;
592
+ pendingChipFocus.current = null;
593
+ const chips = Array.from(chipsRef.current?.querySelectorAll("[data-chip-remove]") ?? []);
594
+ focusAfterRemoval(chips, index, triggerRef.current);
595
+ }, [chipCount]);
575
596
  const displayLabel = multiple ? selectedValues.length > 0 ? `${selectedValues.length} selected` : placeholder : options.find((o) => o.id === selectedValues[0])?.label ?? placeholder;
576
597
  const { ref: overflowRef, isTruncated } = useTextOverflow({ lines: 1, content: displayLabel });
577
598
  return /* @__PURE__ */ jsxs("div", { ref, className: cn("relative", className), children: [
@@ -620,6 +641,7 @@ function Dropdown({
620
641
  "div",
621
642
  {
622
643
  ref: menuRef,
644
+ onClick: (e) => e.stopPropagation(),
623
645
  className: cn(
624
646
  "fixed z-[var(--ds-z-popover)]",
625
647
  "bg-[var(--ds-bg-surface-elevated)]",
@@ -670,7 +692,7 @@ function Dropdown({
670
692
  ),
671
693
  container ?? document.body
672
694
  ),
673
- multiple && showChips && selectedValues.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-ds-1 mt-ds-1.5", children: selectedValues.map((v) => {
695
+ multiple && showChips && selectedValues.length > 0 && /* @__PURE__ */ jsx("div", { ref: chipsRef, className: "flex flex-wrap gap-ds-1 mt-ds-1.5", children: selectedValues.map((v, i) => {
674
696
  const opt = options.find((o) => o.id === v);
675
697
  return opt ? /* @__PURE__ */ jsxs(
676
698
  "span",
@@ -681,7 +703,11 @@ function Dropdown({
681
703
  /* @__PURE__ */ jsx(
682
704
  "button",
683
705
  {
684
- onClick: () => handleSelect(v),
706
+ "data-chip-remove": "",
707
+ onClick: () => {
708
+ pendingChipFocus.current = i;
709
+ handleSelect(v);
710
+ },
685
711
  className: "hover:text-[var(--ds-text-primary)] cursor-pointer",
686
712
  "aria-label": `Remove ${opt.label}`,
687
713
  children: /* @__PURE__ */ jsx(Icon, { name: "close", size: 10 })
@@ -706,6 +732,7 @@ function OptionButton({
706
732
  {
707
733
  type: "button",
708
734
  role: "option",
735
+ "data-option-id": opt.id,
709
736
  "aria-selected": isSelected,
710
737
  tabIndex: -1,
711
738
  onClick: () => onSelect(opt.id),
@@ -750,6 +777,7 @@ function Combobox({ value, onChange, options, placeholder = "Search\u2026", disa
750
777
  useOverlayLayer(open);
751
778
  const [query, setQuery] = useState("");
752
779
  const [highlightIdx, setHighlightIdx] = useState(0);
780
+ const [moved, setMoved] = useState(false);
753
781
  const inputRef = useRef(null);
754
782
  const triggerRef = useRef(null);
755
783
  const [triggerWidth, setTriggerWidth] = useState(void 0);
@@ -766,6 +794,14 @@ function Combobox({ value, onChange, options, placeholder = "Search\u2026", disa
766
794
  setPrevQuery(query);
767
795
  setHighlightIdx(0);
768
796
  }
797
+ const [prevOpen, setPrevOpen] = useState(open);
798
+ if (open !== prevOpen) {
799
+ setPrevOpen(open);
800
+ if (open && query === "") {
801
+ setHighlightIdx(Math.max(0, filtered.findIndex((o) => o.value === value)));
802
+ setMoved(false);
803
+ }
804
+ }
769
805
  useEffect(() => {
770
806
  if (!open) return;
771
807
  const handler = (e) => {
@@ -781,7 +817,6 @@ function Combobox({ value, onChange, options, placeholder = "Search\u2026", disa
781
817
  onChange(val);
782
818
  setQuery("");
783
819
  setOpen(false);
784
- inputRef.current?.blur();
785
820
  },
786
821
  [onChange]
787
822
  );
@@ -798,6 +833,7 @@ function Combobox({ value, onChange, options, placeholder = "Search\u2026", disa
798
833
  return;
799
834
  }
800
835
  setHighlightIdx((prev) => Math.min(prev + 1, filtered.length - 1));
836
+ setMoved(true);
801
837
  } else if (e.key === "ArrowUp") {
802
838
  e.preventDefault();
803
839
  if (!open) {
@@ -805,15 +841,18 @@ function Combobox({ value, onChange, options, placeholder = "Search\u2026", disa
805
841
  return;
806
842
  }
807
843
  setHighlightIdx((prev) => Math.max(prev - 1, 0));
844
+ setMoved(true);
808
845
  } else if (e.key === "Enter" && open && filtered[highlightIdx]) {
809
846
  e.preventDefault();
810
847
  handleSelect(filtered[highlightIdx].value);
848
+ } else if (e.key === "Tab" && open && moved && filtered[highlightIdx]) {
849
+ onChange(filtered[highlightIdx].value);
811
850
  } else if (e.key === "Escape") {
812
851
  setOpen(false);
813
852
  setQuery("");
814
853
  }
815
854
  },
816
- [open, filtered, highlightIdx, handleSelect, openList]
855
+ [open, moved, filtered, highlightIdx, handleSelect, openList, onChange]
817
856
  );
818
857
  const handleBlur = useCallback((e) => {
819
858
  const next = e.relatedTarget;
@@ -858,11 +897,13 @@ function Combobox({ value, onChange, options, placeholder = "Search\u2026", disa
858
897
  "aria-expanded": open,
859
898
  "aria-controls": listboxId,
860
899
  "aria-autocomplete": "list",
900
+ "aria-activedescendant": open && filtered[highlightIdx] ? `${listboxId}-opt-${highlightIdx}` : void 0,
861
901
  value: open ? query : selectedLabel,
862
902
  placeholder,
863
903
  disabled,
864
904
  onChange: (e) => {
865
905
  setQuery(e.target.value);
906
+ setMoved(true);
866
907
  if (!open) setOpen(true);
867
908
  },
868
909
  onFocus: (e) => e.currentTarget.select(),
@@ -917,8 +958,10 @@ function Combobox({ value, onChange, options, placeholder = "Search\u2026", disa
917
958
  children: /* @__PURE__ */ jsx("div", { style: { padding: "var(--ds-space-1)" }, children: loading ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-ds-4", children: /* @__PURE__ */ jsx(Spinner, { size: "sm", label: "Loading options" }) }) : filtered.length === 0 ? /* @__PURE__ */ jsx("div", { className: "px-ds-2 py-ds-3 text-sm text-[var(--ds-text-muted)] text-center", children: "No results" }) : filtered.map((opt, i) => /* @__PURE__ */ jsxs(
918
959
  "button",
919
960
  {
961
+ id: `${listboxId}-opt-${i}`,
920
962
  role: "option",
921
- "aria-selected": opt.value === value,
963
+ tabIndex: -1,
964
+ "aria-selected": i === highlightIdx,
922
965
  onMouseDown: (e) => e.preventDefault(),
923
966
  onClick: () => handleSelect(opt.value),
924
967
  onMouseEnter: () => setHighlightIdx(i),
@@ -940,10 +983,7 @@ function Combobox({ value, onChange, options, placeholder = "Search\u2026", disa
940
983
  container ?? document.body
941
984
  )
942
985
  ] });
943
- if (labelTruncated && !open) {
944
- return /* @__PURE__ */ jsx(Tooltip, { content: selectedLabel, children: triggerContent });
945
- }
946
- return triggerContent;
986
+ return /* @__PURE__ */ jsx(Tooltip, { content: selectedLabel, disabled: !labelTruncated || open, children: triggerContent });
947
987
  }
948
988
  var CHECKBOX_SIZES = {
949
989
  sm: { box: "h-[16px] w-[16px]", glyph: 10, label: "text-xs" },
@@ -1125,6 +1165,42 @@ function RadioGroup({ value, onChange, options, orientation = "vertical", classN
1125
1165
  ] });
1126
1166
  }
1127
1167
 
1168
+ // ../../showcase/src/lib/date-grid.ts
1169
+ function addDays(d, n) {
1170
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
1171
+ }
1172
+ function addMonthsClamped(d, n) {
1173
+ const first = new Date(d.getFullYear(), d.getMonth() + n, 1);
1174
+ const last = new Date(first.getFullYear(), first.getMonth() + 1, 0).getDate();
1175
+ return new Date(first.getFullYear(), first.getMonth(), Math.min(d.getDate(), last));
1176
+ }
1177
+ function dayKey(d) {
1178
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1179
+ }
1180
+ function moveDay(from, key, shift, weekStartsOn = 0) {
1181
+ const intoWeek = (from.getDay() - weekStartsOn + 7) % 7;
1182
+ switch (key) {
1183
+ case "ArrowLeft":
1184
+ return addDays(from, -1);
1185
+ case "ArrowRight":
1186
+ return addDays(from, 1);
1187
+ case "ArrowUp":
1188
+ return addDays(from, -7);
1189
+ case "ArrowDown":
1190
+ return addDays(from, 7);
1191
+ case "Home":
1192
+ return addDays(from, -intoWeek);
1193
+ case "End":
1194
+ return addDays(from, 6 - intoWeek);
1195
+ case "PageUp":
1196
+ return addMonthsClamped(from, shift ? -12 : -1);
1197
+ case "PageDown":
1198
+ return addMonthsClamped(from, shift ? 12 : 1);
1199
+ default:
1200
+ return null;
1201
+ }
1202
+ }
1203
+
1128
1204
  // ../../showcase/src/lib/segments.ts
1129
1205
  function stepValue(spec, current, delta) {
1130
1206
  if (current === null) return delta > 0 ? spec.min : spec.max;
@@ -1165,6 +1241,7 @@ var TEXT_SIZE2 = {
1165
1241
  lg: "text-base"
1166
1242
  };
1167
1243
  var DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
1244
+ var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
1168
1245
  var MONTHS = [
1169
1246
  "January",
1170
1247
  "February",
@@ -1236,7 +1313,9 @@ function DatePicker({ value, onChange, placeholder = "Pick a date", disabled, cl
1236
1313
  const [hover, setHover] = useState(null);
1237
1314
  const [viewDate, setViewDate] = useState(() => value ?? range?.start ?? range?.end ?? /* @__PURE__ */ new Date());
1238
1315
  const triggerRef = useRef(null);
1316
+ const buttonRef = useRef(null);
1239
1317
  const panelRef = useRef(null);
1318
+ const gridRef = useRef(null);
1240
1319
  const generatedId = useId();
1241
1320
  const inputId = id ?? generatedId;
1242
1321
  const pos = usePosition(triggerRef, panelRef, open, { side: "bottom", align: "start", offset: 4, flip: true });
@@ -1245,18 +1324,47 @@ function DatePicker({ value, onChange, placeholder = "Pick a date", disabled, cl
1245
1324
  const daysInMonth = getDaysInMonth(year, month);
1246
1325
  const firstDay = getFirstDayOfWeek(year, month);
1247
1326
  const today = useMemo(() => /* @__PURE__ */ new Date(), []);
1248
- const isTopLayer = useOverlayLayer(open);
1249
- const prevMonth = () => setViewDate(new Date(year, month - 1, 1));
1250
- const nextMonth = () => setViewDate(new Date(year, month + 1, 1));
1251
1327
  const startOf = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
1252
1328
  const isDisabledDay = (d) => !!min && d < startOf(min) || !!max && d > startOf(max);
1253
- useCloseOnFocusOut(open, triggerRef, panelRef, () => setOpen(false), isTopLayer);
1329
+ const [focusDate, setFocusDate] = useState(() => startOf(value ?? range?.start ?? range?.end ?? /* @__PURE__ */ new Date()));
1330
+ const [prevOpen, setPrevOpen] = useState(open);
1331
+ if (open !== prevOpen) {
1332
+ setPrevOpen(open);
1333
+ if (open) {
1334
+ const start = startOf(value ?? range?.start ?? range?.end ?? today);
1335
+ setFocusDate(start);
1336
+ setViewDate(new Date(start.getFullYear(), start.getMonth(), 1));
1337
+ }
1338
+ }
1339
+ const showDay = (d) => {
1340
+ setFocusDate(d);
1341
+ if (d.getFullYear() !== year || d.getMonth() !== month) setViewDate(new Date(d.getFullYear(), d.getMonth(), 1));
1342
+ };
1343
+ const grid = useGridKeyboard({
1344
+ container: gridRef,
1345
+ active: focusDate,
1346
+ setActive: showDay,
1347
+ keyOf: dayKey,
1348
+ move: (from, e) => moveDay(from, e.key, e.shiftKey, 0)
1349
+ });
1350
+ const prevMonth = () => {
1351
+ setViewDate(new Date(year, month - 1, 1));
1352
+ setFocusDate(addMonthsClamped(focusDate, -1));
1353
+ };
1354
+ const nextMonth = () => {
1355
+ setViewDate(new Date(year, month + 1, 1));
1356
+ setFocusDate(addMonthsClamped(focusDate, 1));
1357
+ };
1358
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: buttonRef, panel: panelRef, close: () => setOpen(false), tabExits: "edges" });
1359
+ const focusActiveDay = useLatest(grid.focusActive);
1254
1360
  useEffect(() => {
1255
- if (open) requestAnimationFrame(() => focusInitial(panelRef.current));
1256
- }, [open]);
1361
+ if (!open) return;
1362
+ const raf = requestAnimationFrame(() => focusActiveDay.current());
1363
+ return () => cancelAnimationFrame(raf);
1364
+ }, [open, focusActiveDay]);
1257
1365
  const closeAndRefocus = () => {
1366
+ returnFocus();
1258
1367
  setOpen(false);
1259
- focusSegment(0);
1260
1368
  };
1261
1369
  const dayFirst = useMemo(() => localeDayFirst(), []);
1262
1370
  const [segs, setSegs] = useState(() => value ? { day: value.getDate(), month: value.getMonth() + 1, year: value.getFullYear() } : { day: null, month: null, year: null });
@@ -1272,7 +1380,6 @@ function DatePicker({ value, onChange, placeholder = "Pick a date", disabled, cl
1272
1380
  const focusSegment = (i) => {
1273
1381
  segmentRefs.current[Math.max(0, Math.min(specs.length - 1, i))]?.focus();
1274
1382
  };
1275
- const focusSegmentRef = useLatest(focusSegment);
1276
1383
  useEffect(() => {
1277
1384
  if (!open) return;
1278
1385
  const handler = (e) => {
@@ -1282,8 +1389,8 @@ function DatePicker({ value, onChange, placeholder = "Pick a date", disabled, cl
1282
1389
  };
1283
1390
  const keyHandler = (e) => {
1284
1391
  if (e.key === "Escape" && isTopLayer()) {
1392
+ returnFocus();
1285
1393
  setOpen(false);
1286
- focusSegmentRef.current(0);
1287
1394
  }
1288
1395
  };
1289
1396
  document.addEventListener("mousedown", handler);
@@ -1292,7 +1399,7 @@ function DatePicker({ value, onChange, placeholder = "Pick a date", disabled, cl
1292
1399
  document.removeEventListener("mousedown", handler);
1293
1400
  document.removeEventListener("keydown", keyHandler);
1294
1401
  };
1295
- }, [open, isTopLayer, focusSegmentRef]);
1402
+ }, [open, isTopLayer, returnFocus]);
1296
1403
  const [typedInvalid, setTypedInvalid] = useState(false);
1297
1404
  const commitIfComplete = (next) => {
1298
1405
  if (next.day === null || next.month === null || next.year === null) {
@@ -1390,6 +1497,9 @@ function DatePicker({ value, onChange, placeholder = "Pick a date", disabled, cl
1390
1497
  const cells = [];
1391
1498
  for (let i = 0; i < firstDay; i++) cells.push(null);
1392
1499
  for (let d = 1; d <= daysInMonth; d++) cells.push(d);
1500
+ while (cells.length % 7) cells.push(null);
1501
+ const weeks = [];
1502
+ for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
1393
1503
  return /* @__PURE__ */ jsxs("div", { ref, className: cn(className), children: [
1394
1504
  label && /* @__PURE__ */ jsx("label", { htmlFor: inputId, className: "block text-sm font-medium text-[var(--ds-text-primary)] mb-ds-1", children: label }),
1395
1505
  /* @__PURE__ */ jsxs(
@@ -1455,6 +1565,7 @@ function DatePicker({ value, onChange, placeholder = "Pick a date", disabled, cl
1455
1565
  /* @__PURE__ */ jsx(
1456
1566
  "button",
1457
1567
  {
1568
+ ref: buttonRef,
1458
1569
  type: "button",
1459
1570
  "aria-label": "Choose a date",
1460
1571
  "aria-haspopup": "dialog",
@@ -1501,38 +1612,56 @@ function DatePicker({ value, onChange, placeholder = "Pick a date", disabled, cl
1501
1612
  ]
1502
1613
  }
1503
1614
  ),
1504
- /* @__PURE__ */ jsxs("div", { style: { padding: "var(--ds-space-2) var(--ds-space-3)" }, children: [
1505
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-7 gap-ds-0.5 mb-ds-1 tabular-nums", children: DAYS.map((d) => /* @__PURE__ */ jsx("div", { className: "text-center text-2xs font-medium text-[var(--ds-text-muted)] py-ds-1", children: d }, d)) }),
1506
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-7 gap-ds-0.5 tabular-nums", onMouseLeave: () => setHover(null), children: cells.map((day, i) => {
1507
- if (day === null) return /* @__PURE__ */ jsx("div", {}, `empty-${i}`);
1508
- const dateObj = new Date(year, month, day);
1509
- const isSelected = !!value && isSameDay(dateObj, value);
1510
- const isOtherEnd = !!otherEnd && isSameDay(dateObj, otherEnd);
1511
- const isToday2 = isSameDay(dateObj, today);
1512
- const between = inBand(dateObj);
1513
- const dis = isDisabledDay(dateObj);
1514
- return /* @__PURE__ */ jsx(
1515
- "button",
1516
- {
1517
- type: "button",
1518
- disabled: dis,
1519
- onClick: () => selectDay(day),
1520
- onMouseEnter: () => setHover(dateObj),
1521
- "aria-pressed": isSelected,
1522
- "aria-label": formatDate(dateObj),
1523
- className: cn(
1524
- "w-full aspect-square flex items-center justify-center text-xs",
1525
- "transition-colors cursor-pointer",
1526
- "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
1527
- isSelected ? "bg-[var(--ds-accent-primary)] text-[var(--ds-text-on-primary)] font-bold rounded-[var(--ds-radius-sm)]" : isOtherEnd ? "bg-[var(--ds-accent-muted)] text-[var(--ds-accent-subtle-text)] font-semibold rounded-[var(--ds-radius-sm)]" : between ? "bg-[var(--ds-accent-subtle)] text-[var(--ds-accent-subtle-text)] rounded-none" : isToday2 ? "border-[length:var(--ds-border-width)] border-[var(--ds-accent-primary)] text-[var(--ds-accent-primary)] font-medium rounded-[var(--ds-radius-sm)]" : "text-[var(--ds-text-secondary)] hover:bg-[var(--ds-bg-emphasis)] rounded-[var(--ds-radius-sm)]",
1528
- dis && "opacity-disabled cursor-not-allowed hover:bg-transparent"
1529
- ),
1530
- children: day
1531
- },
1532
- day
1533
- );
1534
- }) })
1535
- ] }),
1615
+ /* @__PURE__ */ jsxs(
1616
+ "div",
1617
+ {
1618
+ ref: gridRef,
1619
+ role: "grid",
1620
+ "aria-label": `${MONTHS[month]} ${year}`,
1621
+ onKeyDown: grid.onKeyDown,
1622
+ className: "flex flex-col gap-ds-0.5 tabular-nums",
1623
+ style: { padding: "var(--ds-space-2) var(--ds-space-3)" },
1624
+ children: [
1625
+ /* @__PURE__ */ jsx("div", { role: "row", className: "grid grid-cols-7 gap-ds-0.5 mb-ds-0.5", children: DAYS.map((d, i) => /* @__PURE__ */ jsx("div", { role: "columnheader", "aria-label": DAY_NAMES[i], className: "text-center text-2xs font-medium text-[var(--ds-text-muted)] py-ds-1", children: d }, d)) }),
1626
+ weeks.map((week, w) => /* @__PURE__ */ jsx("div", { role: "row", className: "grid grid-cols-7 gap-ds-0.5", onMouseLeave: () => setHover(null), children: week.map((day, i) => {
1627
+ if (day === null) return /* @__PURE__ */ jsx("div", { role: "gridcell" }, `empty-${i}`);
1628
+ const dateObj = new Date(year, month, day);
1629
+ const isSelected = !!value && isSameDay(dateObj, value);
1630
+ const isOtherEnd = !!otherEnd && isSameDay(dateObj, otherEnd);
1631
+ const isToday2 = isSameDay(dateObj, today);
1632
+ const between = inBand(dateObj);
1633
+ const dis = isDisabledDay(dateObj);
1634
+ return /* @__PURE__ */ jsx(
1635
+ "button",
1636
+ {
1637
+ type: "button",
1638
+ role: "gridcell",
1639
+ "data-grid-key": dayKey(dateObj),
1640
+ tabIndex: grid.tabIndexOf(dateObj),
1641
+ "aria-disabled": dis || void 0,
1642
+ onClick: () => selectDay(day),
1643
+ onFocus: () => {
1644
+ if (!isSameDay(dateObj, focusDate)) setFocusDate(dateObj);
1645
+ },
1646
+ onMouseEnter: () => setHover(dateObj),
1647
+ "aria-selected": isSelected,
1648
+ "aria-current": isToday2 ? "date" : void 0,
1649
+ "aria-label": formatDate(dateObj),
1650
+ className: cn(
1651
+ "w-full aspect-square flex items-center justify-center text-xs",
1652
+ "transition-colors cursor-pointer",
1653
+ "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
1654
+ isSelected ? "bg-[var(--ds-accent-primary)] text-[var(--ds-text-on-primary)] font-bold rounded-[var(--ds-radius-sm)]" : isOtherEnd ? "bg-[var(--ds-accent-muted)] text-[var(--ds-accent-subtle-text)] font-semibold rounded-[var(--ds-radius-sm)]" : between ? "bg-[var(--ds-accent-subtle)] text-[var(--ds-accent-subtle-text)] rounded-none" : isToday2 ? "border-[length:var(--ds-border-width)] border-[var(--ds-accent-primary)] text-[var(--ds-accent-primary)] font-medium rounded-[var(--ds-radius-sm)]" : "text-[var(--ds-text-secondary)] hover:bg-[var(--ds-bg-emphasis)] rounded-[var(--ds-radius-sm)]",
1655
+ dis && "opacity-disabled cursor-not-allowed hover:bg-transparent"
1656
+ ),
1657
+ children: day
1658
+ },
1659
+ day
1660
+ );
1661
+ }) }, w))
1662
+ ]
1663
+ }
1664
+ ),
1536
1665
  /* @__PURE__ */ jsxs(
1537
1666
  "div",
1538
1667
  {
@@ -1645,7 +1774,7 @@ function endOfWeek(d, weekStartsOn = 0) {
1645
1774
  end.setHours(23, 59, 59, 999);
1646
1775
  return end;
1647
1776
  }
1648
- function addDays(d, n) {
1777
+ function addDays2(d, n) {
1649
1778
  const result = new Date(d);
1650
1779
  result.setDate(result.getDate() + n);
1651
1780
  return result;
@@ -1674,7 +1803,7 @@ function getWeekDays(date, weekStartsOn = 0) {
1674
1803
  const start = startOfWeek(date, weekStartsOn);
1675
1804
  const days = [];
1676
1805
  for (let i = 0; i < 7; i++) {
1677
- days.push(addDays(start, i));
1806
+ days.push(addDays2(start, i));
1678
1807
  }
1679
1808
  return days;
1680
1809
  }
@@ -1764,11 +1893,11 @@ var DEFAULT_DATE_RANGE_PRESETS = [
1764
1893
  } },
1765
1894
  { label: "Last 7 days", range: () => {
1766
1895
  const t = startOfDay(/* @__PURE__ */ new Date());
1767
- return { start: addDays(t, -6), end: t };
1896
+ return { start: addDays2(t, -6), end: t };
1768
1897
  } },
1769
1898
  { label: "Last 30 days", range: () => {
1770
1899
  const t = startOfDay(/* @__PURE__ */ new Date());
1771
- return { start: addDays(t, -29), end: t };
1900
+ return { start: addDays2(t, -29), end: t };
1772
1901
  } },
1773
1902
  { label: "This month", range: () => {
1774
1903
  const t = /* @__PURE__ */ new Date();
@@ -1808,11 +1937,38 @@ function DateRangePicker({
1808
1937
  const pos = usePosition(triggerRef, panelRef, open, { side: "bottom", align: "start", offset: 4, flip: true });
1809
1938
  const today = useMemo(() => startOfDay(/* @__PURE__ */ new Date()), []);
1810
1939
  const dayNames = getOrderedDayNamesShort(weekStartsOn);
1940
+ const gridRef = useRef(null);
1941
+ const [focusDate, setFocusDate] = useState(() => startOfDay(value.start ?? /* @__PURE__ */ new Date()));
1942
+ const [prevOpen, setPrevOpen] = useState(open);
1943
+ if (open !== prevOpen) {
1944
+ setPrevOpen(open);
1945
+ if (open) {
1946
+ const start = startOfDay(value.start ?? today);
1947
+ setFocusDate(start);
1948
+ setViewDate(new Date(start.getFullYear(), start.getMonth(), 1));
1949
+ }
1950
+ }
1951
+ const showDay = (d) => {
1952
+ setFocusDate(d);
1953
+ const monthIndex = (x) => x.getFullYear() * 12 + x.getMonth();
1954
+ const offset = monthIndex(d) - monthIndex(viewDate);
1955
+ if (offset < 0) setViewDate(new Date(d.getFullYear(), d.getMonth(), 1));
1956
+ else if (offset > 1) setViewDate(new Date(d.getFullYear(), d.getMonth() - 1, 1));
1957
+ };
1958
+ const grid = useGridKeyboard({
1959
+ container: gridRef,
1960
+ active: focusDate,
1961
+ setActive: showDay,
1962
+ keyOf: dayKey,
1963
+ move: (from, e) => moveDay(from, e.key, e.shiftKey, weekStartsOn)
1964
+ });
1965
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: panelRef, close: () => setOpen(false), tabExits: "edges" });
1966
+ const focusActiveDay = useLatest(grid.focusActive);
1811
1967
  useEffect(() => {
1812
- if (open) requestAnimationFrame(() => focusInitial(panelRef.current));
1813
- }, [open]);
1814
- const isTopLayer = useOverlayLayer(open);
1815
- useCloseOnFocusOut(open, triggerRef, panelRef, () => setOpen(false), isTopLayer);
1968
+ if (!open) return;
1969
+ const raf = requestAnimationFrame(() => focusActiveDay.current());
1970
+ return () => cancelAnimationFrame(raf);
1971
+ }, [open, focusActiveDay]);
1816
1972
  useEffect(() => {
1817
1973
  if (!open) return;
1818
1974
  const onMouse = (e) => {
@@ -1822,8 +1978,8 @@ function DateRangePicker({
1822
1978
  };
1823
1979
  const onKey = (e) => {
1824
1980
  if (e.key === "Escape" && isTopLayer()) {
1981
+ returnFocus();
1825
1982
  setOpen(false);
1826
- triggerRef.current?.focus();
1827
1983
  }
1828
1984
  };
1829
1985
  document.addEventListener("mousedown", onMouse);
@@ -1832,7 +1988,7 @@ function DateRangePicker({
1832
1988
  document.removeEventListener("mousedown", onMouse);
1833
1989
  document.removeEventListener("keydown", onKey);
1834
1990
  };
1835
- }, [open, isTopLayer]);
1991
+ }, [open, isTopLayer, returnFocus]);
1836
1992
  const isDisabledDay = (d) => min && d < startOfDay(min) || max && d > startOfDay(max) || false;
1837
1993
  const selectDay = (d) => {
1838
1994
  if (isDisabledDay(d)) return;
@@ -1844,9 +2000,9 @@ function DateRangePicker({
1844
2000
  onChange({ start: d, end: null });
1845
2001
  return;
1846
2002
  }
2003
+ returnFocus();
1847
2004
  onChange({ start: value.start, end: d });
1848
2005
  setOpen(false);
1849
- triggerRef.current?.focus();
1850
2006
  };
1851
2007
  const previewEnd = value.start && !value.end && hover && hover >= value.start ? hover : null;
1852
2008
  const rangeStart = value.start;
@@ -1908,10 +2064,10 @@ function DateRangePicker({
1908
2064
  children: [
1909
2065
  presets.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-ds-0.5 p-ds-2 border-r-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)] min-w-[140px]", children: presets.map((p) => /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", className: "justify-start", onClick: () => {
1910
2066
  const r = p.range();
2067
+ returnFocus();
1911
2068
  onChange(r);
1912
2069
  setViewDate(r.start ?? /* @__PURE__ */ new Date());
1913
2070
  setOpen(false);
1914
- triggerRef.current?.focus();
1915
2071
  }, children: p.label }, p.label)) }),
1916
2072
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
1917
2073
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between bg-[var(--ds-bg-muted)] rounded-tr-[var(--ds-radius-md)] px-ds-3 py-ds-2", children: [
@@ -1927,10 +2083,10 @@ function DateRangePicker({
1927
2083
  ] }),
1928
2084
  /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", onClick: () => setViewDate(addMonths(viewDate, 1)), "aria-label": "Next month", children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 14 }) })
1929
2085
  ] }),
1930
- /* @__PURE__ */ jsx("div", { className: "flex gap-ds-4 px-ds-3 py-ds-2", onMouseLeave: () => setHover(null), children: months.map((m) => /* @__PURE__ */ jsxs("div", { className: "w-[224px]", children: [
1931
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-7 gap-ds-0.5 mb-ds-1", children: dayNames.map((d) => /* @__PURE__ */ jsx("div", { className: "text-center text-2xs font-medium text-[var(--ds-text-muted)] py-ds-1", children: d }, d)) }),
1932
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-7 gap-ds-0.5", children: getMonthGrid(m.getFullYear(), m.getMonth(), weekStartsOn).flat().map((d) => {
1933
- if (!isSameMonth(d, m)) return /* @__PURE__ */ jsx("div", {}, d.toISOString());
2086
+ /* @__PURE__ */ jsx("div", { ref: gridRef, onKeyDown: grid.onKeyDown, className: "flex gap-ds-4 px-ds-3 py-ds-2", onMouseLeave: () => setHover(null), children: months.map((m) => /* @__PURE__ */ jsxs("div", { role: "grid", "aria-label": `${MONTHS2[m.getMonth()]} ${m.getFullYear()}`, className: "w-[224px] flex flex-col gap-ds-0.5", children: [
2087
+ /* @__PURE__ */ jsx("div", { role: "row", className: "grid grid-cols-7 gap-ds-0.5 mb-ds-0.5", children: dayNames.map((d) => /* @__PURE__ */ jsx("div", { role: "columnheader", className: "text-center text-2xs font-medium text-[var(--ds-text-muted)] py-ds-1", children: d }, d)) }),
2088
+ getMonthGrid(m.getFullYear(), m.getMonth(), weekStartsOn).map((week, w) => /* @__PURE__ */ jsx("div", { role: "row", className: "grid grid-cols-7 gap-ds-0.5", children: week.map((d) => {
2089
+ if (!isSameMonth(d, m)) return /* @__PURE__ */ jsx("div", { role: "gridcell" }, d.toISOString());
1934
2090
  const isStart = !!rangeStart && isSameDay2(d, rangeStart);
1935
2091
  const isEnd = !!rangeEnd && isSameDay2(d, rangeEnd);
1936
2092
  const between = inRange(d);
@@ -1939,10 +2095,17 @@ function DateRangePicker({
1939
2095
  "button",
1940
2096
  {
1941
2097
  type: "button",
1942
- disabled: dis,
2098
+ role: "gridcell",
2099
+ "data-grid-key": dayKey(d),
2100
+ tabIndex: grid.tabIndexOf(d),
2101
+ "aria-disabled": dis || void 0,
1943
2102
  onClick: () => selectDay(d),
2103
+ onFocus: () => {
2104
+ if (!isSameDay2(d, focusDate)) setFocusDate(d);
2105
+ },
1944
2106
  onMouseEnter: () => setHover(d),
1945
- "aria-pressed": isStart || isEnd,
2107
+ "aria-selected": isStart || isEnd || between,
2108
+ "aria-current": isSameDay2(d, today) ? "date" : void 0,
1946
2109
  "aria-label": fmt(d),
1947
2110
  className: cn(
1948
2111
  "w-full aspect-square flex items-center justify-center text-xs",
@@ -1955,7 +2118,7 @@ function DateRangePicker({
1955
2118
  },
1956
2119
  d.toISOString()
1957
2120
  );
1958
- }) })
2121
+ }) }, w))
1959
2122
  ] }, `${m.getFullYear()}-${m.getMonth()}`)) }),
1960
2123
  /* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center bg-[var(--ds-bg-muted)] border-t-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)] rounded-br-[var(--ds-radius-md)] px-ds-3 py-ds-2", children: [
1961
2124
  /* @__PURE__ */ jsx("span", { className: "text-xs text-[var(--ds-text-muted)]", children: value.start && !value.end ? "Now pick an end date" : summary ?? "Pick a start date" }),
@@ -2365,14 +2528,25 @@ function TimePicker({
2365
2528
  const size = sizeProp ?? contextSize ?? "md";
2366
2529
  const [open, setOpen] = useState(false);
2367
2530
  const triggerRef = useRef(null);
2531
+ const buttonRef = useRef(null);
2368
2532
  const panelRef = useRef(null);
2369
2533
  const listboxId = useId();
2370
2534
  const pos = usePosition(triggerRef, panelRef, open, { side: "bottom", align: "start", offset: 4, flip: true });
2371
2535
  const parsed = parseTime(value);
2372
2536
  const hours = generateHours(use24Hour);
2373
2537
  const minutes = generateMinutes(minuteStep);
2374
- const isTopLayer = useOverlayLayer(open);
2375
- useCloseOnFocusOut(open, triggerRef, panelRef, () => setOpen(false), isTopLayer);
2538
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: buttonRef, panel: panelRef, close: () => setOpen(false), tabExits: "edges" });
2539
+ const [columnStop, setColumnStop] = useState({});
2540
+ const [prevOpen, setPrevOpen] = useState(open);
2541
+ if (open !== prevOpen) {
2542
+ setPrevOpen(open);
2543
+ if (open) setColumnStop({});
2544
+ }
2545
+ const stopOf = (column, chosen) => columnStop[column] ?? Math.max(0, chosen);
2546
+ const holdStop = (column, index) => setColumnStop((s) => s[column] === index ? s : { ...s, [column]: index });
2547
+ const chosenHour = hours.findIndex((h) => use24Hour ? h === parseInt(value) : h === parsed.hour);
2548
+ const chosenMinute = minutes.findIndex((m) => m === parsed.minute);
2549
+ const chosenPeriod = parsed.period === "PM" ? 1 : 0;
2376
2550
  useEffect(() => {
2377
2551
  if (!open) return;
2378
2552
  const id2 = requestAnimationFrame(() => {
@@ -2424,7 +2598,6 @@ function TimePicker({
2424
2598
  const el = segmentRefs.current[Math.max(0, Math.min(segmentCount - 1, i))];
2425
2599
  el?.focus();
2426
2600
  };
2427
- const focusSegmentRef = useLatest(focusSegment);
2428
2601
  useEffect(() => {
2429
2602
  if (!open) return;
2430
2603
  const handler = (e) => {
@@ -2433,8 +2606,8 @@ function TimePicker({
2433
2606
  };
2434
2607
  const keyHandler = (e) => {
2435
2608
  if (e.key === "Escape" && isTopLayer()) {
2609
+ returnFocus();
2436
2610
  setOpen(false);
2437
- focusSegmentRef.current(0);
2438
2611
  }
2439
2612
  };
2440
2613
  document.addEventListener("mousedown", handler);
@@ -2443,7 +2616,7 @@ function TimePicker({
2443
2616
  document.removeEventListener("mousedown", handler);
2444
2617
  document.removeEventListener("keydown", keyHandler);
2445
2618
  };
2446
- }, [open, isTopLayer, focusSegmentRef]);
2619
+ }, [open, isTopLayer, returnFocus]);
2447
2620
  const commitIfComplete = (next) => {
2448
2621
  if (next.hour === null || next.minute === null) return;
2449
2622
  let h24 = next.hour;
@@ -2619,6 +2792,7 @@ function TimePicker({
2619
2792
  /* @__PURE__ */ jsx(
2620
2793
  "button",
2621
2794
  {
2795
+ ref: buttonRef,
2622
2796
  type: "button",
2623
2797
  "aria-label": "Choose a time",
2624
2798
  "aria-haspopup": "dialog",
@@ -2651,10 +2825,12 @@ function TimePicker({
2651
2825
  ),
2652
2826
  style: { top: pos.top, left: pos.left },
2653
2827
  children: /* @__PURE__ */ jsxs("div", { className: "inline-flex p-ds-1 gap-ds-1 tabular-nums", children: [
2654
- /* @__PURE__ */ jsx("div", { "data-column": true, className: "max-h-48 overflow-y-auto w-ds-10", children: hours.map((h) => /* @__PURE__ */ jsx(
2828
+ /* @__PURE__ */ jsx("div", { "data-column": true, className: "max-h-48 overflow-y-auto w-ds-10", children: hours.map((h, i) => /* @__PURE__ */ jsx(
2655
2829
  "button",
2656
2830
  {
2657
2831
  type: "button",
2832
+ tabIndex: i === stopOf("hour", chosenHour) ? 0 : -1,
2833
+ onFocus: () => holdStop("hour", i),
2658
2834
  "data-selected": use24Hour ? h === parseInt(value) : h === parsed.hour,
2659
2835
  onMouseDown: (e) => e.preventDefault(),
2660
2836
  onClick: () => emitChange(h, parsed.minute, parsed.period),
@@ -2668,10 +2844,12 @@ function TimePicker({
2668
2844
  h
2669
2845
  )) }),
2670
2846
  /* @__PURE__ */ jsx("div", { className: "w-px bg-[var(--ds-border-subtle)]" }),
2671
- /* @__PURE__ */ jsx("div", { "data-column": true, className: "max-h-48 overflow-y-auto w-ds-10", children: minutes.map((m) => /* @__PURE__ */ jsx(
2847
+ /* @__PURE__ */ jsx("div", { "data-column": true, className: "max-h-48 overflow-y-auto w-ds-10", children: minutes.map((m, i) => /* @__PURE__ */ jsx(
2672
2848
  "button",
2673
2849
  {
2674
2850
  type: "button",
2851
+ tabIndex: i === stopOf("minute", chosenMinute) ? 0 : -1,
2852
+ onFocus: () => holdStop("minute", i),
2675
2853
  "data-selected": m === parsed.minute,
2676
2854
  onMouseDown: (e) => e.preventDefault(),
2677
2855
  onClick: () => emitChange(parsed.hour, m, parsed.period),
@@ -2686,10 +2864,12 @@ function TimePicker({
2686
2864
  )) }),
2687
2865
  !use24Hour && /* @__PURE__ */ jsxs(Fragment, { children: [
2688
2866
  /* @__PURE__ */ jsx("div", { className: "w-px bg-[var(--ds-border-subtle)]" }),
2689
- /* @__PURE__ */ jsx("div", { "data-column": true, className: "flex flex-col gap-ds-1 w-ds-10", children: ["AM", "PM"].map((p) => /* @__PURE__ */ jsx(
2867
+ /* @__PURE__ */ jsx("div", { "data-column": true, className: "flex flex-col gap-ds-1 w-ds-10", children: ["AM", "PM"].map((p, i) => /* @__PURE__ */ jsx(
2690
2868
  "button",
2691
2869
  {
2692
2870
  type: "button",
2871
+ tabIndex: i === stopOf("period", chosenPeriod) ? 0 : -1,
2872
+ onFocus: () => holdStop("period", i),
2693
2873
  "data-selected": p === parsed.period,
2694
2874
  onMouseDown: (e) => e.preventDefault(),
2695
2875
  onClick: () => emitChange(parsed.hour, parsed.minute, p),
@@ -3016,7 +3196,8 @@ var Tag = forwardRef(function Tag2({
3016
3196
  {
3017
3197
  type: "button",
3018
3198
  onClick: handleRemove,
3019
- "aria-label": "Remove",
3199
+ "data-tag-remove": "",
3200
+ "aria-label": typeof children === "string" ? `Remove ${children}` : "Remove",
3020
3201
  className: cn(
3021
3202
  "inline-flex items-center justify-center shrink-0",
3022
3203
  "rounded-[var(--ds-radius-sm)]",
@@ -3063,6 +3244,16 @@ function TagInput({
3063
3244
  },
3064
3245
  [value, onChange]
3065
3246
  );
3247
+ const containerRef = useRef(null);
3248
+ const pendingFocus = useRef(null);
3249
+ const tagCount = value.length;
3250
+ useLayoutEffect(() => {
3251
+ const index = pendingFocus.current;
3252
+ if (index === null) return;
3253
+ pendingFocus.current = null;
3254
+ const buttons = Array.from(containerRef.current?.querySelectorAll("[data-tag-remove]") ?? []);
3255
+ focusAfterRemoval(buttons, index, inputRef.current);
3256
+ }, [tagCount]);
3066
3257
  const handleKeyDown = useCallback(
3067
3258
  (e) => {
3068
3259
  if ((e.key === "Enter" || e.key === ",") && input.trim()) {
@@ -3095,7 +3286,10 @@ function TagInput({
3095
3286
  return /* @__PURE__ */ jsxs(
3096
3287
  "div",
3097
3288
  {
3098
- onClick: () => inputRef.current?.focus(),
3289
+ ref: containerRef,
3290
+ onClick: (e) => {
3291
+ if (!(e.target instanceof Element && e.target.closest("button"))) inputRef.current?.focus();
3292
+ },
3099
3293
  className: cn(
3100
3294
  "flex flex-wrap items-center gap-[calc(var(--ds-space-1)*1.5)] min-h-[calc(var(--ds-space-1)*10.5)] px-ds-2 py-[calc(var(--ds-space-1)*1.5)]",
3101
3295
  "bg-[var(--ds-input-bg)] text-[var(--ds-text-primary)]",
@@ -3110,10 +3304,13 @@ function TagInput({
3110
3304
  className
3111
3305
  ),
3112
3306
  children: [
3113
- value.map((tag) => /* @__PURE__ */ jsx(
3307
+ value.map((tag, i) => /* @__PURE__ */ jsx(
3114
3308
  Tag,
3115
3309
  {
3116
- onRemove: disabled ? void 0 : () => removeTag(tag),
3310
+ onRemove: disabled ? void 0 : () => {
3311
+ pendingFocus.current = i;
3312
+ removeTag(tag);
3313
+ },
3117
3314
  disabled,
3118
3315
  children: tag
3119
3316
  },
@@ -4683,17 +4880,30 @@ var Card = forwardRef(function Card2({
4683
4880
  onSelectedChange?.(!selected);
4684
4881
  onClick?.(e);
4685
4882
  } : onClick;
4883
+ const isAction = !isSelectable && variant === "interactive" && !!onClick;
4686
4884
  const handleKeyDown = isSelectable && !disabled ? (e) => {
4687
4885
  if (e.key === "Enter" || e.key === " ") {
4688
4886
  e.preventDefault();
4689
4887
  onSelectedChange?.(!selected);
4690
4888
  }
4889
+ } : isAction && !disabled ? (e) => {
4890
+ if (e.target !== e.currentTarget) return;
4891
+ if (e.key === "Enter") {
4892
+ e.preventDefault();
4893
+ e.currentTarget.click();
4894
+ } else if (e.key === " ") e.preventDefault();
4895
+ } : void 0;
4896
+ const handleKeyUp = isAction && !disabled ? (e) => {
4897
+ if (e.target === e.currentTarget && e.key === " ") {
4898
+ e.preventDefault();
4899
+ e.currentTarget.click();
4900
+ }
4691
4901
  } : void 0;
4692
4902
  const handleCheckableClick = (e) => {
4693
4903
  e.stopPropagation();
4694
4904
  onSelectedChange?.(!selected);
4695
4905
  };
4696
- const role = selectType === "radio" ? "radio" : selectType === "checkbox" ? "checkbox" : selectType === "toggle" ? "switch" : isSelectable ? "button" : void 0;
4906
+ const role = selectType === "radio" ? "radio" : selectType === "checkbox" ? "checkbox" : selectType === "toggle" ? "switch" : isSelectable || isAction ? "button" : void 0;
4697
4907
  const ariaAttr = selectType === "radio" || selectType === "checkbox" || selectType === "toggle" ? { "aria-checked": !!selected } : isSelectable ? { "aria-pressed": !!selected } : {};
4698
4908
  const showRing = isSelectable && !isToggle && !hasSelectType && selected;
4699
4909
  const indicator = selectType === "radio" ? /* @__PURE__ */ jsx(RadioIndicator, { checked: !!selected, disabled }) : selectType === "checkbox" ? /* @__PURE__ */ jsx(CheckboxIndicator, { checked: !!selected, indeterminate, disabled }) : selectType === "toggle" ? /* @__PURE__ */ jsx(ToggleIndicator, { checked: !!selected, disabled }) : null;
@@ -4704,8 +4914,8 @@ var Card = forwardRef(function Card2({
4704
4914
  ref,
4705
4915
  "data-card": "",
4706
4916
  role,
4707
- tabIndex: isSelectable ? 0 : void 0,
4708
- "aria-disabled": isSelectable && disabled ? true : void 0,
4917
+ tabIndex: isSelectable || isAction && !disabled ? 0 : void 0,
4918
+ "aria-disabled": (isSelectable || isAction) && disabled ? true : void 0,
4709
4919
  ...name && selectType === "radio" ? { "data-radio-group": name } : {},
4710
4920
  style: variant === "interactive" ? { ...INTERACTIVE_SCALE_CAP, ...style } : style,
4711
4921
  className: cn(
@@ -4713,7 +4923,7 @@ var Card = forwardRef(function Card2({
4713
4923
  variantStyles3[variant],
4714
4924
  isSelectable && !disabled && "cursor-pointer select-none transition-all duration-[var(--ds-transition-speed)] ease-[var(--ds-transition-ease)]",
4715
4925
  isSelectable && !disabled && !selected && "hover:border-[var(--ds-accent-primary)]",
4716
- isSelectable && "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
4926
+ (isSelectable || isAction) && "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
4717
4927
  (showRing || hasSelectType && selected) && ACTIVE_RING,
4718
4928
  hasSelectType && selected && "bg-[var(--ds-accent-subtle)]",
4719
4929
  disabled && "opacity-disabled cursor-not-allowed",
@@ -4721,6 +4931,7 @@ var Card = forwardRef(function Card2({
4721
4931
  ),
4722
4932
  onClick: handleClick,
4723
4933
  onKeyDown: handleKeyDown,
4934
+ onKeyUp: handleKeyUp,
4724
4935
  ...ariaAttr,
4725
4936
  ...props,
4726
4937
  children: [
@@ -5278,6 +5489,7 @@ function Table({
5278
5489
  return /* @__PURE__ */ jsxs(
5279
5490
  "div",
5280
5491
  {
5492
+ "data-row": "",
5281
5493
  className: cn(
5282
5494
  "border-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
5283
5495
  "rounded-[var(--ds-radius-md)] bg-[var(--ds-bg-surface)]",
@@ -5830,11 +6042,34 @@ function OverflowPopover({
5830
6042
  offset: 4,
5831
6043
  flip: true
5832
6044
  });
6045
+ const { isTop, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: floatingRef, close: () => setOpen(false), tabExits: "edges" });
6046
+ useEffect(() => {
6047
+ if (!open) return;
6048
+ const raf = requestAnimationFrame(() => focusInitial(floatingRef.current));
6049
+ const onKey = (e) => {
6050
+ if (e.key === "Escape" && isTop()) {
6051
+ returnFocus();
6052
+ setOpen(false);
6053
+ }
6054
+ };
6055
+ const onDown = (e) => {
6056
+ if (!triggerRef.current?.contains(e.target)) setOpen(false);
6057
+ };
6058
+ document.addEventListener("keydown", onKey);
6059
+ document.addEventListener("mousedown", onDown);
6060
+ return () => {
6061
+ cancelAnimationFrame(raf);
6062
+ document.removeEventListener("keydown", onKey);
6063
+ document.removeEventListener("mousedown", onDown);
6064
+ };
6065
+ }, [open, isTop, returnFocus]);
5833
6066
  return /* @__PURE__ */ jsxs(Fragment, { children: [
5834
6067
  /* @__PURE__ */ jsxs(
5835
6068
  "button",
5836
6069
  {
5837
6070
  ref: triggerRef,
6071
+ "aria-haspopup": "dialog",
6072
+ "aria-expanded": open,
5838
6073
  className: "text-2xs text-[var(--ds-accent-primary)] font-medium cursor-pointer hover:underline px-ds-1.5",
5839
6074
  onClick: (e) => {
5840
6075
  e.stopPropagation();
@@ -5852,6 +6087,8 @@ function OverflowPopover({
5852
6087
  "div",
5853
6088
  {
5854
6089
  ref: floatingRef,
6090
+ role: "dialog",
6091
+ "aria-label": date.toLocaleDateString(void 0, { weekday: "long", month: "long", day: "numeric" }),
5855
6092
  className: cn(
5856
6093
  "fixed z-[var(--ds-z-popover)] w-56 max-h-48 overflow-y-auto",
5857
6094
  "bg-[var(--ds-bg-surface-elevated)]",
@@ -6663,10 +6900,18 @@ function MonthDropdown({
6663
6900
  setPrevBrowseSyncKey(browseSyncKey);
6664
6901
  if (open) setBrowseYear(currentDate.getFullYear());
6665
6902
  }
6903
+ const { isTop, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: floatingRef, close: () => setOpen(false), tabExits: "edges" });
6666
6904
  useEffect(() => {
6667
6905
  if (!open) return;
6906
+ const raf = requestAnimationFrame(() => {
6907
+ const panel = floatingRef.current;
6908
+ (panel?.querySelector("[data-current]") ?? panel?.querySelector("button"))?.focus();
6909
+ });
6668
6910
  const onKey = (e) => {
6669
- if (e.key === "Escape") setOpen(false);
6911
+ if (e.key === "Escape" && isTop()) {
6912
+ returnFocus();
6913
+ setOpen(false);
6914
+ }
6670
6915
  };
6671
6916
  const onClick = (e) => {
6672
6917
  const t = e.target;
@@ -6676,10 +6921,11 @@ function MonthDropdown({
6676
6921
  document.addEventListener("keydown", onKey);
6677
6922
  document.addEventListener("mousedown", onClick);
6678
6923
  return () => {
6924
+ cancelAnimationFrame(raf);
6679
6925
  document.removeEventListener("keydown", onKey);
6680
6926
  document.removeEventListener("mousedown", onClick);
6681
6927
  };
6682
- }, [open]);
6928
+ }, [open, isTop, returnFocus]);
6683
6929
  const currentMonth = currentDate.getMonth();
6684
6930
  const currentYear = currentDate.getFullYear();
6685
6931
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -6749,7 +6995,9 @@ function MonthDropdown({
6749
6995
  "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
6750
6996
  isActive ? "bg-[var(--ds-accent-primary)] text-[var(--ds-text-on-primary)]" : "text-[var(--ds-text-secondary)] hover:bg-[var(--ds-bg-emphasis)] hover:text-[var(--ds-text-primary)]"
6751
6997
  ),
6998
+ "data-current": isActive || void 0,
6752
6999
  onClick: () => {
7000
+ returnFocus();
6753
7001
  onSelect(new Date(browseYear, i, 1));
6754
7002
  setOpen(false);
6755
7003
  },
@@ -6786,10 +7034,18 @@ function YearDropdown({
6786
7034
  setPrevRangeSyncKey(rangeSyncKey);
6787
7035
  if (open) setRangeStart(currentYear - 4);
6788
7036
  }
7037
+ const { isTop, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: floatingRef, close: () => setOpen(false), tabExits: "edges" });
6789
7038
  useEffect(() => {
6790
7039
  if (!open) return;
7040
+ const raf = requestAnimationFrame(() => {
7041
+ const panel = floatingRef.current;
7042
+ (panel?.querySelector("[data-current]") ?? panel?.querySelector("button"))?.focus();
7043
+ });
6791
7044
  const onKey = (e) => {
6792
- if (e.key === "Escape") setOpen(false);
7045
+ if (e.key === "Escape" && isTop()) {
7046
+ returnFocus();
7047
+ setOpen(false);
7048
+ }
6793
7049
  };
6794
7050
  const onClick = (e) => {
6795
7051
  const t = e.target;
@@ -6799,10 +7055,11 @@ function YearDropdown({
6799
7055
  document.addEventListener("keydown", onKey);
6800
7056
  document.addEventListener("mousedown", onClick);
6801
7057
  return () => {
7058
+ cancelAnimationFrame(raf);
6802
7059
  document.removeEventListener("keydown", onKey);
6803
7060
  document.removeEventListener("mousedown", onClick);
6804
7061
  };
6805
- }, [open]);
7062
+ }, [open, isTop, returnFocus]);
6806
7063
  const years = Array.from({ length: 9 }, (_, i) => rangeStart + i);
6807
7064
  return /* @__PURE__ */ jsxs(Fragment, { children: [
6808
7065
  /* @__PURE__ */ jsxs(
@@ -6871,7 +7128,9 @@ function YearDropdown({
6871
7128
  "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
6872
7129
  y === currentYear ? "bg-[var(--ds-accent-primary)] text-[var(--ds-text-on-primary)]" : "text-[var(--ds-text-secondary)] hover:bg-[var(--ds-bg-emphasis)] hover:text-[var(--ds-text-primary)]"
6873
7130
  ),
7131
+ "data-current": y === currentYear || void 0,
6874
7132
  onClick: () => {
7133
+ returnFocus();
6875
7134
  onSelect(new Date(y, currentDate.getMonth(), 1));
6876
7135
  setOpen(false);
6877
7136
  },
@@ -6947,7 +7206,7 @@ function Calendar({
6947
7206
  if (currentView === "month") {
6948
7207
  setCurrentDate(addMonths(currentDate, direction));
6949
7208
  } else if (currentView === "week") {
6950
- setCurrentDate(addDays(currentDate, direction * 7));
7209
+ setCurrentDate(addDays2(currentDate, direction * 7));
6951
7210
  } else {
6952
7211
  setCurrentDate(new Date(currentDate.getFullYear() + direction, currentDate.getMonth(), 1));
6953
7212
  }
@@ -9600,8 +9859,11 @@ var Breadcrumbs = forwardRef(function Breadcrumbs2({
9600
9859
  container
9601
9860
  }, ref) {
9602
9861
  const [dropdownOpen, setDropdownOpen] = useState(false);
9862
+ const [activeIdx, setActiveIdx] = useState(0);
9603
9863
  const ellipsisBtnRef = useRef(null);
9604
9864
  const dropdownRef = useRef(null);
9865
+ const closeDropdown = useCallback(() => setDropdownOpen(false), []);
9866
+ const { isTop, returnFocus } = usePopupFocus(dropdownOpen, { trigger: ellipsisBtnRef, panel: dropdownRef, close: closeDropdown, tabExits: "any" });
9605
9867
  const pos = usePosition(ellipsisBtnRef, dropdownRef, dropdownOpen, { side: "bottom", align: "start", offset: 4, flip: true });
9606
9868
  const effectiveMaxItems = maxItems ? Math.max(maxItems, 2) : void 0;
9607
9869
  const shouldCollapse = effectiveMaxItems && items.length > effectiveMaxItems;
@@ -9614,9 +9876,31 @@ var Breadcrumbs = forwardRef(function Breadcrumbs2({
9614
9876
  if (ellipsisBtnRef.current?.contains(target) || dropdownRef.current?.contains(target)) return;
9615
9877
  setDropdownOpen(false);
9616
9878
  };
9879
+ const onKeyDown = (e) => {
9880
+ if (e.key === "Escape" && isTop()) {
9881
+ returnFocus();
9882
+ setDropdownOpen(false);
9883
+ }
9884
+ };
9617
9885
  document.addEventListener("mousedown", handler);
9618
- return () => document.removeEventListener("mousedown", handler);
9619
- }, [dropdownOpen]);
9886
+ document.addEventListener("keydown", onKeyDown);
9887
+ return () => {
9888
+ document.removeEventListener("mousedown", handler);
9889
+ document.removeEventListener("keydown", onKeyDown);
9890
+ };
9891
+ }, [dropdownOpen, isTop, returnFocus]);
9892
+ useEffect(() => {
9893
+ if (!dropdownOpen) return;
9894
+ const id = requestAnimationFrame(() => dropdownRef.current?.querySelector(`[data-index="${activeIdx}"]`)?.focus());
9895
+ return () => cancelAnimationFrame(id);
9896
+ }, [dropdownOpen, activeIdx]);
9897
+ function onMenuKeyDown(e) {
9898
+ const last = collapsedItems.length - 1;
9899
+ const next = e.key === "ArrowDown" ? activeIdx >= last ? 0 : activeIdx + 1 : e.key === "ArrowUp" ? activeIdx <= 0 ? last : activeIdx - 1 : e.key === "Home" ? 0 : e.key === "End" ? last : null;
9900
+ if (next === null) return;
9901
+ e.preventDefault();
9902
+ setActiveIdx(next);
9903
+ }
9620
9904
  const textClass = size === "sm" ? "text-xs" : "text-sm";
9621
9905
  const gapClass = size === "sm" ? "gap-ds-1" : "gap-ds-1.5";
9622
9906
  const iconSize = size === "sm" ? 12 : 14;
@@ -9681,7 +9965,10 @@ var Breadcrumbs = forwardRef(function Breadcrumbs2({
9681
9965
  "button",
9682
9966
  {
9683
9967
  ref: ellipsisBtnRef,
9684
- onClick: () => setDropdownOpen((v) => !v),
9968
+ onClick: () => {
9969
+ if (!dropdownOpen) setActiveIdx(0);
9970
+ setDropdownOpen(!dropdownOpen);
9971
+ },
9685
9972
  "aria-label": "Show hidden breadcrumbs",
9686
9973
  "aria-expanded": dropdownOpen,
9687
9974
  "aria-haspopup": "menu",
@@ -9712,6 +9999,8 @@ var Breadcrumbs = forwardRef(function Breadcrumbs2({
9712
9999
  {
9713
10000
  ref: dropdownRef,
9714
10001
  role: "menu",
10002
+ "aria-label": "Hidden breadcrumbs",
10003
+ onKeyDown: onMenuKeyDown,
9715
10004
  className: cn(
9716
10005
  "fixed z-[var(--ds-z-popover)] min-w-[160px] max-h-60 overflow-auto",
9717
10006
  "bg-[var(--ds-bg-surface-elevated)]",
@@ -9725,12 +10014,15 @@ var Breadcrumbs = forwardRef(function Breadcrumbs2({
9725
10014
  children: collapsedItems.map((ci, idx) => /* @__PURE__ */ jsxs(
9726
10015
  "button",
9727
10016
  {
10017
+ "data-index": idx,
9728
10018
  role: "menuitem",
10019
+ tabIndex: idx === activeIdx ? 0 : -1,
9729
10020
  className: cn(
9730
10021
  "flex items-center w-full text-left text-sm cursor-pointer",
9731
10022
  "px-ds-3 py-ds-1.5 transition-colors",
9732
10023
  "text-[var(--ds-text-secondary)]",
9733
10024
  "hover:bg-[var(--ds-bg-muted)] hover:text-[var(--ds-text-primary)]",
10025
+ "focus-visible:outline-none focus-visible:bg-[var(--ds-bg-muted)] focus-visible:text-[var(--ds-text-primary)] focus-visible:shadow-[inset_var(--ds-shadow-focus)]",
9734
10026
  gapClass
9735
10027
  ),
9736
10028
  onClick: () => {
@@ -9788,14 +10080,13 @@ function Popover({ open, onOpenChange, trigger, children, side = "bottom", align
9788
10080
  setTriggerWidth(node?.offsetWidth);
9789
10081
  }, []);
9790
10082
  const pos = usePosition(triggerRef, floatingRef, open, { side, align, offset: 8, flip: true });
9791
- const isTopLayer = useOverlayLayer(open);
9792
- useCloseOnFocusOut(open, triggerRef, floatingRef, () => onOpenChange(false), isTopLayer);
10083
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: floatingRef, close: () => onOpenChange(false), tabExits: "edges" });
9793
10084
  useEffect(() => {
9794
10085
  if (!open) return;
9795
10086
  const onKeyDown = (e) => {
9796
10087
  if (e.key === "Escape" && isTopLayer()) {
10088
+ returnFocus();
9797
10089
  onOpenChange(false);
9798
- triggerRef.current?.focus();
9799
10090
  }
9800
10091
  };
9801
10092
  const onClick = (e) => {
@@ -9809,7 +10100,7 @@ function Popover({ open, onOpenChange, trigger, children, side = "bottom", align
9809
10100
  document.removeEventListener("keydown", onKeyDown);
9810
10101
  document.removeEventListener("mousedown", onClick);
9811
10102
  };
9812
- }, [open, onOpenChange, isTopLayer]);
10103
+ }, [open, onOpenChange, isTopLayer, returnFocus]);
9813
10104
  useEffect(() => {
9814
10105
  if (open) focusInitial(floatingRef.current);
9815
10106
  }, [open]);
@@ -9831,6 +10122,7 @@ function Popover({ open, onOpenChange, trigger, children, side = "bottom", align
9831
10122
  {
9832
10123
  ref: floatingRef,
9833
10124
  "data-gs": "popover",
10125
+ onClick: (e) => e.stopPropagation(),
9834
10126
  className: cn(
9835
10127
  "fixed z-[var(--ds-z-popover)]",
9836
10128
  "bg-[var(--ds-bg-surface-elevated)]",
@@ -10510,17 +10802,39 @@ function CompactFlyout({
10510
10802
  onItemClick,
10511
10803
  onClose,
10512
10804
  onMouseEnter,
10513
- onMouseLeave
10805
+ onMouseLeave,
10806
+ focusIn = false
10514
10807
  }) {
10515
10808
  const pos = useMemo(() => computeCompactFlyoutPos(anchorEl, items.length), [anchorEl, items.length]);
10516
- const isTopLayer = useOverlayLayer(true);
10809
+ const anchorRef = useRef(anchorEl);
10810
+ const panelRef = useRef(null);
10811
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(true, { trigger: anchorRef, panel: panelRef, close: onClose, tabExits: "edges" });
10812
+ useEffect(() => {
10813
+ if (!focusIn) return;
10814
+ const raf = requestAnimationFrame(() => panelRef.current?.querySelector('[role="menuitem"]')?.focus());
10815
+ return () => cancelAnimationFrame(raf);
10816
+ }, [focusIn]);
10517
10817
  useEffect(() => {
10518
10818
  function handler(e) {
10519
- if (e.key === "Escape" && isTopLayer()) onClose();
10819
+ if (!isTopLayer()) return;
10820
+ const inside = !!panelRef.current?.contains(document.activeElement);
10821
+ if (e.key === "Escape" || e.key === "ArrowLeft" && inside) {
10822
+ e.preventDefault();
10823
+ if (inside) returnFocus();
10824
+ onClose();
10825
+ } else if (inside && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
10826
+ const links = Array.from(panelRef.current?.querySelectorAll('[role="menuitem"]') ?? []);
10827
+ const at = links.indexOf(document.activeElement);
10828
+ const next = links[(at + (e.key === "ArrowDown" ? 1 : -1) + links.length) % links.length];
10829
+ if (next) {
10830
+ e.preventDefault();
10831
+ next.focus();
10832
+ }
10833
+ }
10520
10834
  }
10521
10835
  document.addEventListener("keydown", handler);
10522
10836
  return () => document.removeEventListener("keydown", handler);
10523
- }, [onClose, isTopLayer]);
10837
+ }, [onClose, isTopLayer, returnFocus]);
10524
10838
  const content = /* @__PURE__ */ jsx(
10525
10839
  "div",
10526
10840
  {
@@ -10531,7 +10845,9 @@ function CompactFlyout({
10531
10845
  children: /* @__PURE__ */ jsxs(
10532
10846
  "div",
10533
10847
  {
10848
+ ref: panelRef,
10534
10849
  role: "menu",
10850
+ "aria-label": parentLabel,
10535
10851
  className: cn(
10536
10852
  "min-w-[160px] py-ds-1",
10537
10853
  "bg-[var(--ds-sidebar-bg)] rounded-[var(--ds-radius-md)]",
@@ -10908,6 +11224,8 @@ function CompactNavItem({
10908
11224
  onToggleExpand
10909
11225
  }) {
10910
11226
  const [triggerEl, setTriggerEl] = useState(null);
11227
+ const [keyboardOpen, setKeyboardOpen] = useState(false);
11228
+ if (!isOpen && keyboardOpen) setKeyboardOpen(false);
10911
11229
  const handleMouseEnter = useCallback(() => {
10912
11230
  onCancelClose();
10913
11231
  if (hasChildren && !isOpen) onOpen(flyoutKey);
@@ -10930,6 +11248,14 @@ function CompactNavItem({
10930
11248
  onMouseEnter: !mobile && hasChildren ? handleMouseEnter : void 0,
10931
11249
  onMouseLeave: !mobile && hasChildren && isOpen ? handleMouseLeave : void 0,
10932
11250
  onClick: handleClick,
11251
+ onKeyDown: !mobile && hasChildren ? (e) => {
11252
+ if (e.key !== "ArrowRight") return;
11253
+ e.preventDefault();
11254
+ onCancelClose();
11255
+ setKeyboardOpen(true);
11256
+ onOpen(flyoutKey);
11257
+ } : void 0,
11258
+ "aria-haspopup": !mobile && hasChildren ? "menu" : void 0,
10933
11259
  "aria-expanded": hasChildren ? mobile ? expanded : isOpen : void 0,
10934
11260
  className: cn(
10935
11261
  "relative flex flex-col items-center justify-center gap-[3px] w-full py-ds-3 px-ds-1 rounded-[var(--ds-radius-md)] transition-colors cursor-pointer",
@@ -10972,7 +11298,8 @@ function CompactNavItem({
10972
11298
  onItemClick,
10973
11299
  onClose,
10974
11300
  onMouseEnter: onCancelClose,
10975
- onMouseLeave: handleMouseLeave
11301
+ onMouseLeave: handleMouseLeave,
11302
+ focusIn: keyboardOpen
10976
11303
  }
10977
11304
  ),
10978
11305
  mobile && expanded && hasChildren && item.children && /* @__PURE__ */ jsx("div", { className: "w-full flex flex-col gap-ds-1 pb-ds-1", children: item.children.map((child, ci) => /* @__PURE__ */ jsxs(
@@ -12326,12 +12653,11 @@ function NavMenu({
12326
12653
  className
12327
12654
  }) {
12328
12655
  const [open, setOpen] = useState(false);
12329
- const isTopLayer = useOverlayLayer(open);
12330
12656
  const triggerRef = useRef(null);
12331
12657
  const panelRef = useRef(null);
12332
12658
  const [pos, setPos] = useState({ top: 0, left: 0, minWidth: 0 });
12333
12659
  const close = useCallback(() => setOpen(false), []);
12334
- useCloseOnFocusOut(open, triggerRef, panelRef, close, isTopLayer);
12660
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: panelRef, close, tabExits: "edges" });
12335
12661
  useEffect(() => {
12336
12662
  if (!open) return;
12337
12663
  const id = requestAnimationFrame(() => focusInitial(panelRef.current));
@@ -12355,8 +12681,8 @@ function NavMenu({
12355
12681
  }
12356
12682
  function handleKey(e) {
12357
12683
  if (e.key === "Escape" && isTopLayer()) {
12684
+ returnFocus();
12358
12685
  close();
12359
- triggerRef.current?.focus();
12360
12686
  }
12361
12687
  }
12362
12688
  document.addEventListener("mousedown", handleClick);
@@ -12365,7 +12691,7 @@ function NavMenu({
12365
12691
  document.removeEventListener("mousedown", handleClick);
12366
12692
  document.removeEventListener("keydown", handleKey);
12367
12693
  };
12368
- }, [open, close, isTopLayer]);
12694
+ }, [open, close, isTopLayer, returnFocus]);
12369
12695
  const hasActiveChild = active || items.some(
12370
12696
  (it) => it.active || it.children?.some((c) => c.active)
12371
12697
  );
@@ -12932,15 +13258,20 @@ function Dialog({ open, onClose: onCloseProp, title, description, children, foot
12932
13258
  const isMobile = useBreakpoint("sm");
12933
13259
  const overlayRef = useRef(null);
12934
13260
  const dialogRef = useRef(null);
12935
- const triggerRef = useRef(null);
13261
+ const opener = useRef(null);
12936
13262
  const defaultButtonRef = useRef(null);
12937
13263
  const onCloseRef = useLatest(onClose);
12938
13264
  const onSubmitRef = useLatest(onSubmit);
12939
13265
  const isTopLayer = useOverlayLayer(open);
12940
13266
  useFocusTrap(dialogRef, open, isTopLayer);
13267
+ const before = useFocusBeforeOpen(open);
13268
+ useEffect(() => {
13269
+ if (!open) return;
13270
+ const active = before.current;
13271
+ opener.current = active instanceof HTMLElement && active !== document.body && active.isConnected ? rememberFocus(active) : null;
13272
+ }, [open, before]);
12941
13273
  useEffect(() => {
12942
13274
  if (!open) return;
12943
- triggerRef.current = document.activeElement;
12944
13275
  requestAnimationFrame(() => {
12945
13276
  focusInitial(dialogRef.current, { destructive: variant === "destructive" });
12946
13277
  });
@@ -12960,7 +13291,10 @@ function Dialog({ open, onClose: onCloseProp, title, description, children, foot
12960
13291
  lockBodyScroll();
12961
13292
  return () => {
12962
13293
  unlockBodyScroll();
12963
- triggerRef.current?.focus();
13294
+ const r = opener.current;
13295
+ opener.current = null;
13296
+ const active = document.activeElement;
13297
+ if (!active || active === document.body || !active.isConnected) r?.restore();
12964
13298
  };
12965
13299
  }, [open]);
12966
13300
  if (!open) return null;
@@ -13078,6 +13412,7 @@ function CommandPalette({ open, onOpenChange, commands, placeholder = "Type a co
13078
13412
  const [selectedIdx, setSelectedIdx] = useState(0);
13079
13413
  const inputRef = useRef(null);
13080
13414
  const listRef = useRef(null);
13415
+ const dialogRef = useRef(null);
13081
13416
  const listboxId = useId();
13082
13417
  const filtered = commands.filter(
13083
13418
  (c) => c.label.toLowerCase().includes(query.toLowerCase()) || c.group?.toLowerCase().includes(query.toLowerCase())
@@ -13106,6 +13441,8 @@ function CommandPalette({ open, onOpenChange, commands, placeholder = "Type a co
13106
13441
  if (open) requestAnimationFrame(() => inputRef.current?.focus());
13107
13442
  }, [open]);
13108
13443
  const isTopLayer = useOverlayLayer(open);
13444
+ useFocusTrap(dialogRef, open, isTopLayer);
13445
+ const returnFocus = useReturnFocus(open, dialogRef);
13109
13446
  useEffect(() => {
13110
13447
  if (!open) return;
13111
13448
  const handler = (e) => {
@@ -13116,10 +13453,11 @@ function CommandPalette({ open, onOpenChange, commands, placeholder = "Type a co
13116
13453
  }, [open, onOpenChange, isTopLayer]);
13117
13454
  const handleSelect = useCallback(
13118
13455
  (cmd) => {
13456
+ returnFocus();
13119
13457
  cmd.onSelect();
13120
13458
  onOpenChange(false);
13121
13459
  },
13122
- [onOpenChange]
13460
+ [onOpenChange, returnFocus]
13123
13461
  );
13124
13462
  const handleKeyDown = (e) => {
13125
13463
  if (e.key === "ArrowDown") {
@@ -13150,8 +13488,10 @@ function CommandPalette({ open, onOpenChange, commands, placeholder = "Type a co
13150
13488
  /* @__PURE__ */ jsxs(
13151
13489
  "div",
13152
13490
  {
13491
+ ref: dialogRef,
13153
13492
  role: "dialog",
13154
13493
  "aria-modal": "true",
13494
+ "aria-label": "Command palette",
13155
13495
  className: cn(
13156
13496
  "relative z-10 w-full max-w-lg",
13157
13497
  "bg-[var(--ds-bg-surface)] text-[var(--ds-text-primary)]",
@@ -13177,6 +13517,7 @@ function CommandPalette({ open, onOpenChange, commands, placeholder = "Type a co
13177
13517
  "aria-expanded": true,
13178
13518
  "aria-controls": listboxId,
13179
13519
  "aria-autocomplete": "list",
13520
+ "aria-activedescendant": flatFiltered[selectedIdx] ? `${listboxId}-opt-${selectedIdx}` : void 0,
13180
13521
  value: query,
13181
13522
  onChange: (e) => setQuery(e.target.value),
13182
13523
  onKeyDown: handleKeyDown,
@@ -13210,7 +13551,9 @@ function CommandPalette({ open, onOpenChange, commands, placeholder = "Type a co
13210
13551
  return /* @__PURE__ */ jsxs(
13211
13552
  "button",
13212
13553
  {
13554
+ id: `${listboxId}-opt-${myIdx}`,
13213
13555
  role: "option",
13556
+ tabIndex: -1,
13214
13557
  "aria-selected": myIdx === selectedIdx,
13215
13558
  onClick: () => handleSelect(cmd),
13216
13559
  onMouseEnter: () => setSelectedIdx(myIdx),
@@ -13469,8 +13812,7 @@ function NotificationCenter({
13469
13812
  const setFilter = onFilterChange ?? setInternalFilter;
13470
13813
  const unreadCount = notifications.filter((n) => !n.read).length;
13471
13814
  const filteredNotifications = filter === "unread" ? notifications.filter((n) => !n.read) : notifications;
13472
- const isTopLayer = useOverlayLayer(open);
13473
- useCloseOnFocusOut(open, triggerRef, panelRef, () => setOpen(false), isTopLayer);
13815
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: panelRef, close: () => setOpen(false), tabExits: "edges" });
13474
13816
  useEffect(() => {
13475
13817
  if (!open) return;
13476
13818
  const id = requestAnimationFrame(() => focusInitial(panelRef.current));
@@ -13480,8 +13822,8 @@ function NotificationCenter({
13480
13822
  if (!open) return;
13481
13823
  const onKey = (e) => {
13482
13824
  if (e.key === "Escape" && isTopLayer()) {
13825
+ returnFocus();
13483
13826
  setOpen(false);
13484
- triggerRef.current?.focus();
13485
13827
  }
13486
13828
  };
13487
13829
  const onClick = (e) => {
@@ -13495,7 +13837,7 @@ function NotificationCenter({
13495
13837
  document.removeEventListener("keydown", onKey);
13496
13838
  document.removeEventListener("mousedown", onClick);
13497
13839
  };
13498
- }, [open, setOpen, isTopLayer]);
13840
+ }, [open, setOpen, isTopLayer, returnFocus]);
13499
13841
  const grouped = groupByDate(filteredNotifications);
13500
13842
  if (variant === "page") {
13501
13843
  return /* @__PURE__ */ jsxs("div", { ref, className: cn("w-full", className), children: [
@@ -13768,6 +14110,15 @@ function Tour({ steps, open, onClose, onComplete, container }) {
13768
14110
  if (current > 0) setCurrent((c) => c - 1);
13769
14111
  }, [current]);
13770
14112
  const isTopLayer = useOverlayLayer(open);
14113
+ const nextRef = useRef(null);
14114
+ const titleId = useId();
14115
+ useFocusTrap(popoverRef, open, isTopLayer);
14116
+ useReturnFocus(open, popoverRef);
14117
+ useEffect(() => {
14118
+ if (!open) return;
14119
+ const raf = requestAnimationFrame(() => nextRef.current?.focus());
14120
+ return () => cancelAnimationFrame(raf);
14121
+ }, [open, current]);
13771
14122
  useEffect(() => {
13772
14123
  if (!open) return;
13773
14124
  const onKey = (e) => {
@@ -13820,6 +14171,9 @@ function Tour({ steps, open, onClose, onComplete, container }) {
13820
14171
  "div",
13821
14172
  {
13822
14173
  ref: popoverRef,
14174
+ role: "dialog",
14175
+ "aria-modal": "true",
14176
+ "aria-labelledby": titleId,
13823
14177
  className: cn(
13824
14178
  "fixed z-[2] w-[300px]",
13825
14179
  "bg-[var(--ds-bg-surface-elevated)]",
@@ -13832,7 +14186,7 @@ function Tour({ steps, open, onClose, onComplete, container }) {
13832
14186
  children: [
13833
14187
  /* @__PURE__ */ jsxs("div", { className: "px-ds-4 pt-ds-4 pb-ds-3", children: [
13834
14188
  /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-ds-2 mb-ds-1", children: [
13835
- /* @__PURE__ */ jsx("h3", { className: "text-sm font-[var(--ds-weight-heading)] text-[var(--ds-text-primary)]", children: step.title }),
14189
+ /* @__PURE__ */ jsx("h3", { id: titleId, className: "text-sm font-[var(--ds-weight-heading)] text-[var(--ds-text-primary)]", children: step.title }),
13836
14190
  /* @__PURE__ */ jsx(
13837
14191
  "button",
13838
14192
  {
@@ -13868,6 +14222,7 @@ function Tour({ steps, open, onClose, onComplete, container }) {
13868
14222
  /* @__PURE__ */ jsx(
13869
14223
  "button",
13870
14224
  {
14225
+ ref: nextRef,
13871
14226
  onClick: handleNext,
13872
14227
  className: cn(
13873
14228
  "px-ds-3 py-ds-1.5 text-xs font-medium rounded-[var(--ds-radius-sm)]",