@hjmds/react 1.1.0 → 1.2.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 (199) hide show
  1. package/dist/actions.d.ts.map +1 -1
  2. package/dist/actions.js +12 -6
  3. package/dist/actions.js.map +1 -1
  4. package/dist/advanced-display.d.ts +21 -0
  5. package/dist/advanced-display.d.ts.map +1 -1
  6. package/dist/advanced-display.js +11 -0
  7. package/dist/advanced-display.js.map +1 -1
  8. package/dist/advanced-navigation.d.ts +2 -23
  9. package/dist/advanced-navigation.d.ts.map +1 -1
  10. package/dist/advanced-navigation.js +3 -24
  11. package/dist/advanced-navigation.js.map +1 -1
  12. package/dist/agreement.d.ts +20 -0
  13. package/dist/agreement.d.ts.map +1 -0
  14. package/dist/agreement.js +29 -0
  15. package/dist/agreement.js.map +1 -0
  16. package/dist/anchor.d.ts +24 -0
  17. package/dist/anchor.d.ts.map +1 -0
  18. package/dist/anchor.js +110 -0
  19. package/dist/anchor.js.map +1 -0
  20. package/dist/asset.d.ts +40 -0
  21. package/dist/asset.d.ts.map +1 -0
  22. package/dist/asset.js +26 -0
  23. package/dist/asset.js.map +1 -0
  24. package/dist/bottom-cta.d.ts +32 -0
  25. package/dist/bottom-cta.d.ts.map +1 -0
  26. package/dist/bottom-cta.js +30 -0
  27. package/dist/bottom-cta.js.map +1 -0
  28. package/dist/bottom-info.d.ts +16 -0
  29. package/dist/bottom-info.d.ts.map +1 -0
  30. package/dist/bottom-info.js +12 -0
  31. package/dist/bottom-info.js.map +1 -0
  32. package/dist/breadcrumb.d.ts +9 -0
  33. package/dist/breadcrumb.d.ts.map +1 -0
  34. package/dist/breadcrumb.js +12 -0
  35. package/dist/breadcrumb.js.map +1 -0
  36. package/dist/calendar.d.ts +30 -0
  37. package/dist/calendar.d.ts.map +1 -0
  38. package/dist/calendar.js +69 -0
  39. package/dist/calendar.js.map +1 -0
  40. package/dist/carousel.d.ts +20 -0
  41. package/dist/carousel.d.ts.map +1 -0
  42. package/dist/carousel.js +70 -0
  43. package/dist/carousel.js.map +1 -0
  44. package/dist/clipboard.d.ts +33 -0
  45. package/dist/clipboard.d.ts.map +1 -0
  46. package/dist/clipboard.js +40 -0
  47. package/dist/clipboard.js.map +1 -0
  48. package/dist/collapsible.d.ts +11 -0
  49. package/dist/collapsible.d.ts.map +1 -0
  50. package/dist/collapsible.js +18 -0
  51. package/dist/collapsible.js.map +1 -0
  52. package/dist/command-palette.d.ts +25 -0
  53. package/dist/command-palette.d.ts.map +1 -0
  54. package/dist/command-palette.js +82 -0
  55. package/dist/command-palette.js.map +1 -0
  56. package/dist/context-menu.d.ts +12 -0
  57. package/dist/context-menu.d.ts.map +1 -0
  58. package/dist/context-menu.js +111 -0
  59. package/dist/context-menu.js.map +1 -0
  60. package/dist/data-table.d.ts +30 -0
  61. package/dist/data-table.d.ts.map +1 -0
  62. package/dist/data-table.js +57 -0
  63. package/dist/data-table.js.map +1 -0
  64. package/dist/date-picker.d.ts +3 -1
  65. package/dist/date-picker.d.ts.map +1 -1
  66. package/dist/date-picker.js +16 -31
  67. package/dist/date-picker.js.map +1 -1
  68. package/dist/date-range.d.ts +24 -0
  69. package/dist/date-range.d.ts.map +1 -0
  70. package/dist/date-range.js +29 -0
  71. package/dist/date-range.js.map +1 -0
  72. package/dist/display.d.ts +16 -0
  73. package/dist/display.d.ts.map +1 -1
  74. package/dist/display.js +13 -1
  75. package/dist/display.js.map +1 -1
  76. package/dist/evidence.d.ts +481 -1
  77. package/dist/evidence.d.ts.map +1 -1
  78. package/dist/evidence.js +47 -2
  79. package/dist/evidence.js.map +1 -1
  80. package/dist/feedback.d.ts +18 -2
  81. package/dist/feedback.d.ts.map +1 -1
  82. package/dist/feedback.js +10 -2
  83. package/dist/feedback.js.map +1 -1
  84. package/dist/floating-action-button.d.ts +32 -0
  85. package/dist/floating-action-button.d.ts.map +1 -0
  86. package/dist/floating-action-button.js +58 -0
  87. package/dist/floating-action-button.js.map +1 -0
  88. package/dist/forms.d.ts +3 -3
  89. package/dist/forms.d.ts.map +1 -1
  90. package/dist/forms.js +18 -7
  91. package/dist/forms.js.map +1 -1
  92. package/dist/heading.d.ts +12 -0
  93. package/dist/heading.d.ts.map +1 -0
  94. package/dist/heading.js +25 -0
  95. package/dist/heading.js.map +1 -0
  96. package/dist/index.d.ts +35 -2
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +35 -2
  99. package/dist/index.js.map +1 -1
  100. package/dist/layout.d.ts +1 -1
  101. package/dist/mentions.d.ts +28 -0
  102. package/dist/mentions.d.ts.map +1 -0
  103. package/dist/mentions.js +84 -0
  104. package/dist/mentions.js.map +1 -0
  105. package/dist/menubar.d.ts +11 -0
  106. package/dist/menubar.d.ts.map +1 -0
  107. package/dist/menubar.js +137 -0
  108. package/dist/menubar.js.map +1 -0
  109. package/dist/modal.d.ts +67 -0
  110. package/dist/modal.d.ts.map +1 -0
  111. package/dist/modal.js +307 -0
  112. package/dist/modal.js.map +1 -0
  113. package/dist/overlay-stack.d.ts +28 -0
  114. package/dist/overlay-stack.d.ts.map +1 -0
  115. package/dist/overlay-stack.js +47 -0
  116. package/dist/overlay-stack.js.map +1 -0
  117. package/dist/overlays.d.ts +26 -43
  118. package/dist/overlays.d.ts.map +1 -1
  119. package/dist/overlays.js +62 -302
  120. package/dist/overlays.js.map +1 -1
  121. package/dist/pagination.d.ts +17 -0
  122. package/dist/pagination.d.ts.map +1 -0
  123. package/dist/pagination.js +21 -0
  124. package/dist/pagination.js.map +1 -0
  125. package/dist/popover.d.ts +25 -0
  126. package/dist/popover.d.ts.map +1 -0
  127. package/dist/popover.js +226 -0
  128. package/dist/popover.js.map +1 -0
  129. package/dist/portal.d.ts +5 -1
  130. package/dist/portal.d.ts.map +1 -1
  131. package/dist/portal.js +24 -1
  132. package/dist/portal.js.map +1 -1
  133. package/dist/provider-button.d.ts +21 -0
  134. package/dist/provider-button.d.ts.map +1 -0
  135. package/dist/provider-button.js +26 -0
  136. package/dist/provider-button.js.map +1 -0
  137. package/dist/provider.d.ts +13 -1
  138. package/dist/provider.d.ts.map +1 -1
  139. package/dist/provider.js +12 -2
  140. package/dist/provider.js.map +1 -1
  141. package/dist/selection.d.ts +5 -1
  142. package/dist/selection.d.ts.map +1 -1
  143. package/dist/selection.js +2 -2
  144. package/dist/selection.js.map +1 -1
  145. package/dist/side-panel.d.ts +34 -0
  146. package/dist/side-panel.d.ts.map +1 -0
  147. package/dist/side-panel.js +95 -0
  148. package/dist/side-panel.js.map +1 -0
  149. package/dist/sidebar.d.ts +22 -0
  150. package/dist/sidebar.d.ts.map +1 -0
  151. package/dist/sidebar.js +29 -0
  152. package/dist/sidebar.js.map +1 -0
  153. package/dist/skip-nav.d.ts +12 -0
  154. package/dist/skip-nav.d.ts.map +1 -0
  155. package/dist/skip-nav.js +29 -0
  156. package/dist/skip-nav.js.map +1 -0
  157. package/dist/splitter.d.ts +45 -0
  158. package/dist/splitter.d.ts.map +1 -0
  159. package/dist/splitter.js +116 -0
  160. package/dist/splitter.js.map +1 -0
  161. package/dist/styles.css +1068 -26
  162. package/dist/supplemental-display.d.ts +12 -0
  163. package/dist/supplemental-display.d.ts.map +1 -1
  164. package/dist/supplemental-display.js +6 -2
  165. package/dist/supplemental-display.js.map +1 -1
  166. package/dist/tags-input.d.ts +60 -0
  167. package/dist/tags-input.d.ts.map +1 -0
  168. package/dist/tags-input.js +90 -0
  169. package/dist/tags-input.js.map +1 -0
  170. package/dist/text-formats.d.ts +16 -0
  171. package/dist/text-formats.d.ts.map +1 -0
  172. package/dist/text-formats.js +24 -0
  173. package/dist/text-formats.js.map +1 -0
  174. package/dist/toast.d.ts +1 -1
  175. package/dist/toggle-group.d.ts +13 -0
  176. package/dist/toggle-group.d.ts.map +1 -0
  177. package/dist/toggle-group.js +21 -0
  178. package/dist/toggle-group.js.map +1 -0
  179. package/dist/top-bar.d.ts +27 -0
  180. package/dist/top-bar.d.ts.map +1 -0
  181. package/dist/top-bar.js +30 -0
  182. package/dist/top-bar.js.map +1 -0
  183. package/dist/top.d.ts +15 -0
  184. package/dist/top.d.ts.map +1 -0
  185. package/dist/top.js +18 -0
  186. package/dist/top.js.map +1 -0
  187. package/dist/tour.d.ts +21 -0
  188. package/dist/tour.d.ts.map +1 -0
  189. package/dist/tour.js +122 -0
  190. package/dist/tour.js.map +1 -0
  191. package/dist/transfer-list.d.ts +24 -0
  192. package/dist/transfer-list.d.ts.map +1 -0
  193. package/dist/transfer-list.js +109 -0
  194. package/dist/transfer-list.js.map +1 -0
  195. package/dist/tree.d.ts +29 -0
  196. package/dist/tree.d.ts.map +1 -0
  197. package/dist/tree.js +120 -0
  198. package/dist/tree.js.map +1 -0
  199. package/package.json +173 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"top.js","sourceRoot":"","sources":["../src/top.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,WAAW,EACX,SAAS,EACT,qBAAqB,GAEtB,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,aAAa,EAAsC,MAAM,OAAO,CAAC;AACtF,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAS3C,MAAM,CAAC,MAAM,GAAG,GAAG,UAAU,CAAwB,SAAS,GAAG,CAC/D,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,EACnC,YAAY;IAEZ,qBAAqB,CAAC,UAAU,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,IAAI,WAAW,CAAC,IAAI,CAAC;IACjD,MAAM,KAAK,GAAG,UAAU,CAAC,YAAY,IAAI,WAAW,CAAC,YAAY,CAAC;IAClE,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACtC,OAAO,CACL,kBACE,GAAG,EAAE,YAAY,EACjB,SAAS,EAAE,UAAU,CAAC,SAAS,EAAE,SAAS,CAAC,eAChC,IAAI,EACf,KAAK,EAAE;YACL,sBAAsB,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,IAAI;YACrD,6BAA6B,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,UAAU,IAAI;YAC9D,wBAAwB,EAAE,OAAO,CAAC,KAAK,CAAC,UAAU;YAClD,uBAAuB,EAAE,GAAG,OAAO,CAAC,UAAU,IAAI;YAClD,0BAA0B,EAAE,GAAG,OAAO,CAAC,aAAa,IAAI;SACxC,aAEjB,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,YAAG,SAAS,EAAC,kBAAkB,YAAE,UAAU,CAAC,OAAO,GAAK,CAAC,CAAC,CAAC,IAAI,EACrF,eAAK,SAAS,EAAC,cAAc,aAK1B,aAAa,CAAC,IAAI,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,gBAAgB,EAAE,EAAE,UAAU,CAAC,KAAK,CAAC,EAC7E,QAAQ,CAAC,CAAC,CAAC,cAAK,SAAS,EAAC,mBAAmB,YAAE,QAAQ,GAAO,CAAC,CAAC,CAAC,IAAI,IAClE,EACL,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,YAAG,SAAS,EAAC,sBAAsB,YAAE,UAAU,CAAC,WAAW,GAAK,CAAC,CAAC,CAAC,IAAI,IAC1F,CACV,CAAC;AACJ,CAAC,CAAC,CAAC","sourcesContent":["import {\n topDefaults,\n topRecipe,\n validateTopDescriptor,\n type TopDescriptor,\n} from \"@hjmds/design-contracts/components/top\";\nimport { forwardRef, createElement, type CSSProperties, type ReactNode } from \"react\";\nimport { classNames } from \"./internal.js\";\n\nexport type TopProps = Readonly<{\n descriptor: TopDescriptor;\n /** Secondary action sharing the title row; drops below it when space runs out. */\n trailing?: ReactNode;\n className?: string;\n}>;\n\nexport const Top = forwardRef<HTMLElement, TopProps>(function Top(\n { descriptor, trailing, className },\n forwardedRef,\n) {\n validateTopDescriptor(descriptor);\n const size = descriptor.size ?? topDefaults.size;\n const level = descriptor.headingLevel ?? topDefaults.headingLevel;\n const metrics = topRecipe.sizes[size];\n return (\n <header\n ref={forwardedRef}\n className={classNames(\"hjm-top\", className)}\n data-size={size}\n style={{\n \"--hjm-top-title-size\": `${metrics.title.fontSize}px`,\n \"--hjm-top-title-line-height\": `${metrics.title.lineHeight}px`,\n \"--hjm-top-title-weight\": metrics.title.fontWeight,\n \"--hjm-top-padding-top\": `${metrics.paddingTop}px`,\n \"--hjm-top-padding-bottom\": `${metrics.paddingBottom}px`,\n } as CSSProperties}\n >\n {descriptor.eyebrow ? <p className=\"hjm-top__eyebrow\">{descriptor.eyebrow}</p> : null}\n <div className=\"hjm-top__row\">\n {/*\n A real heading element at the declared level, not styled text: this\n block is what a screen reader lands on when it skips to the content.\n */}\n {createElement(`h${level}`, { className: \"hjm-top__title\" }, descriptor.title)}\n {trailing ? <div className=\"hjm-top__trailing\">{trailing}</div> : null}\n </div>\n {descriptor.description ? <p className=\"hjm-top__description\">{descriptor.description}</p> : null}\n </header>\n );\n});\n"]}
package/dist/tour.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { type ComposeTourStepAnnouncement, type TourDescriptor, type TourOpenReason, type TourStepChangeHandler } from "@hjmds/design-contracts/components/tour";
2
+ import { type OverlayTrigger } from "./modal.js";
3
+ export type TourProps<Id extends string = string> = Readonly<{
4
+ descriptor: TourDescriptor<Id>;
5
+ /** Resolves the product-owned anchor key to the element to point at. */
6
+ resolveAnchor: (anchorId: string) => HTMLElement | null;
7
+ composeAnnouncement: ComposeTourStepAnnouncement;
8
+ onStepChange: TourStepChangeHandler<Id>;
9
+ open?: boolean;
10
+ defaultOpen?: boolean;
11
+ onOpenChange?: (open: boolean, detail: Readonly<{
12
+ reason: TourOpenReason;
13
+ }>) => void;
14
+ trigger?: OverlayTrigger;
15
+ portalContainer?: HTMLElement;
16
+ className?: string;
17
+ }>;
18
+ export declare const Tour: <Id extends string = string>(props: TourProps<Id> & {
19
+ ref?: React.Ref<HTMLDivElement>;
20
+ }) => React.ReactElement | null;
21
+ //# sourceMappingURL=tour.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tour.d.ts","sourceRoot":"","sources":["../src/tour.tsx"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,2BAA2B,EAGhC,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC3B,MAAM,yCAAyC,CAAC;AAajD,OAAO,EAA0D,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAEzG,MAAM,MAAM,SAAS,CAAC,EAAE,SAAS,MAAM,GAAG,MAAM,IAAI,QAAQ,CAAC;IAC3D,UAAU,EAAE,cAAc,CAAC,EAAE,CAAC,CAAC;IAC/B,wEAAwE;IACxE,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,WAAW,GAAG,IAAI,CAAC;IACxD,mBAAmB,EAAE,2BAA2B,CAAC;IACjD,YAAY,EAAE,qBAAqB,CAAC,EAAE,CAAC,CAAC;IACxC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC;QAAE,MAAM,EAAE,cAAc,CAAA;KAAE,CAAC,KAAK,IAAI,CAAC;IACrF,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,eAAe,CAAC,EAAE,WAAW,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC,CAAC;AA2BH,eAAO,MAAM,IAAI,EA4JX,CAAC,EAAE,SAAS,MAAM,GAAG,MAAM,EAC/B,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;CAAE,KACvD,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC"}
package/dist/tour.js ADDED
@@ -0,0 +1,122 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { resolveTourAdvance, resolveTourDescriptor, tourRecipe, validateTourOpenState, } from "@hjmds/design-contracts/components/tour";
3
+ import { forwardRef, useCallback, useEffect, useId, useRef, useState, } from "react";
4
+ import { Button } from "./actions.js";
5
+ import { classNames, composeRefs } from "./internal.js";
6
+ import { AnchoredPortal, useAnchoredPopup } from "./portal.js";
7
+ import { getModalLayer, HjmPortal, renderTrigger, useModalFocus } from "./modal.js";
8
+ /*
9
+ The contract hands renderers an opaque `anchorId`, never a node, so the
10
+ product owns what each step points at. This component therefore keeps its own
11
+ element box: it re-reads the anchor on every step and on layout changes rather
12
+ than storing a rect the product could invalidate by re-rendering the page.
13
+ */
14
+ function useAnchorRect(anchor, active) {
15
+ const [rect, setRect] = useState(null);
16
+ useEffect(() => {
17
+ if (!active || !anchor) {
18
+ setRect(null);
19
+ return;
20
+ }
21
+ const measure = () => setRect(anchor.getBoundingClientRect());
22
+ measure();
23
+ const observer = new ResizeObserver(measure);
24
+ observer.observe(anchor);
25
+ window.addEventListener("scroll", measure, true);
26
+ window.addEventListener("resize", measure);
27
+ return () => {
28
+ observer.disconnect();
29
+ window.removeEventListener("scroll", measure, true);
30
+ window.removeEventListener("resize", measure);
31
+ };
32
+ }, [anchor, active]);
33
+ return rect;
34
+ }
35
+ export const Tour = forwardRef(function Tour({ descriptor, resolveAnchor, composeAnnouncement, onStepChange, open: openProp, defaultOpen, onOpenChange, trigger, portalContainer, className, }, forwardedRef) {
36
+ validateTourOpenState({
37
+ ...(openProp === undefined ? {} : { open: openProp }),
38
+ ...(defaultOpen === undefined ? {} : { defaultOpen }),
39
+ ...(onOpenChange === undefined ? {} : { onOpenChange }),
40
+ });
41
+ const [internalOpen, setInternalOpen] = useState(defaultOpen ?? false);
42
+ const open = openProp ?? internalOpen;
43
+ const resolved = resolveTourDescriptor(descriptor, { composeAnnouncement });
44
+ const step = resolved.currentStep;
45
+ const cardRef = useRef(null);
46
+ const triggerRef = useRef(null);
47
+ const anchorRef = useRef(null);
48
+ const [card, setCard] = useState(null);
49
+ const id = `${useId().replaceAll(":", "")}-tour`;
50
+ const anchor = open ? resolveAnchor(step.anchorId) : null;
51
+ anchorRef.current = anchor;
52
+ const anchorRect = useAnchorRect(anchor, open);
53
+ const change = useCallback((next, reason) => {
54
+ if (openProp === undefined)
55
+ setInternalOpen(next);
56
+ onOpenChange?.(next, { reason });
57
+ }, [openProp, onOpenChange]);
58
+ const close = (reason) => change(false, reason);
59
+ const advance = (reason) => {
60
+ const outcome = resolveTourAdvance(descriptor, reason);
61
+ // `no-op` is Previous on the first step: the contract makes that a decision,
62
+ // not a disabled button, so the control stays focusable and simply does nothing.
63
+ if (outcome.type === "step")
64
+ onStepChange(outcome.stepId, reason);
65
+ else if (outcome.type === "close")
66
+ close(outcome.reason);
67
+ };
68
+ const openRef = useRef(open);
69
+ openRef.current = open;
70
+ const settleRef = useRef(close);
71
+ settleRef.current = close;
72
+ const epochRef = useRef(0);
73
+ useEffect(() => {
74
+ const epoch = epochRef.current + 1;
75
+ epochRef.current = epoch;
76
+ return () => {
77
+ queueMicrotask(() => {
78
+ // StrictMode re-runs setup right after its probe cleanup; only a real
79
+ // unmount has no later epoch. An open tour torn down that way was
80
+ // interrupted — the user never skipped, escaped, or completed it.
81
+ if (epochRef.current !== epoch || !openRef.current)
82
+ return;
83
+ settleRef.current("interrupted");
84
+ });
85
+ };
86
+ }, []);
87
+ useModalFocus({
88
+ active: open,
89
+ contentRef: cardRef,
90
+ // The card itself, not its first button: it carries the step announcement,
91
+ // which is the whole substitute for a sighted user's visual pointer.
92
+ initialFocusRef: cardRef,
93
+ ...(trigger === undefined ? {} : { fallbackReturnRef: triggerRef }),
94
+ onEscape: () => close("escape"),
95
+ });
96
+ useEffect(() => {
97
+ // Focus follows the card on every step change, never the anchor: the anchor
98
+ // is inert background while the tour runs.
99
+ if (open)
100
+ cardRef.current?.focus({ preventScroll: true });
101
+ }, [open, step.id]);
102
+ const position = useAnchoredPopup(anchorRef, open ? card : null, {
103
+ ...(step.placement === undefined ? {} : { placement: step.placement }),
104
+ ...(step.align === undefined ? {} : { align: step.align }),
105
+ gap: tourRecipe.sideOffset,
106
+ zIndex: getModalLayer(0) + 1,
107
+ fallbackAxis: true,
108
+ });
109
+ const cardRefs = useCallback(composeRefs(setCard, cardRef, forwardedRef), [forwardedRef]);
110
+ const renderedTrigger = trigger === undefined
111
+ ? null
112
+ : renderTrigger(trigger, triggerRef, open, id, "dialog", () => change(true, "trigger"));
113
+ if (!open)
114
+ return renderedTrigger;
115
+ return (_jsxs(_Fragment, { children: [renderedTrigger, _jsx(HjmPortal, { ...(portalContainer === undefined ? {} : { container: portalContainer }), children: _jsx("div", { className: "hjm-tour-backdrop", "data-state": "open", style: { zIndex: getModalLayer(0) }, children: anchorRect ? (_jsx("div", { className: "hjm-tour-highlight", "aria-hidden": "true", style: {
116
+ insetBlockStart: anchorRect.top,
117
+ insetInlineStart: anchorRect.left,
118
+ inlineSize: anchorRect.width,
119
+ blockSize: anchorRect.height,
120
+ } })) : null }) }), _jsx(AnchoredPortal, { anchorRef: anchorRef, ...(portalContainer ? { container: portalContainer } : {}), children: _jsxs("div", { ref: cardRefs, id: id, role: "dialog", "aria-modal": "true", "aria-label": resolved.accessibilityLabel, "aria-describedby": `${id}-announcement`, tabIndex: -1, "data-hjm-modal-content": "", "data-placement": position.placement, "data-align": position.align, className: classNames("hjm-tour", className), style: { ...position.style, maxInlineSize: tourRecipe.maxWidth }, children: [_jsx("span", { id: `${id}-announcement`, className: "hjm-visually-hidden", children: step.announcement }), _jsxs("div", { "aria-hidden": "true", children: [_jsxs("p", { className: "hjm-tour__counter", children: [step.position, " / ", step.total] }), _jsx("h2", { className: "hjm-tour__title", children: step.title }), _jsx("p", { className: "hjm-tour__description", children: step.description })] }), _jsxs("div", { className: "hjm-tour__actions", children: [_jsx(Button, { tone: "ghost", onClick: () => close("skip"), children: descriptor.labels.skip }), _jsx(Button, { tone: "secondary", disabled: resolved.isFirstStep, onClick: () => advance("previous"), children: descriptor.labels.previous }), _jsx(Button, { onClick: () => advance("next"), children: resolved.isLastStep ? descriptor.labels.done : descriptor.labels.next })] })] }) })] }));
121
+ });
122
+ //# sourceMappingURL=tour.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tour.js","sourceRoot":"","sources":["../src/tour.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n resolveTourAdvance,\n resolveTourDescriptor,\n tourRecipe,\n validateTourOpenState,\n type ComposeTourStepAnnouncement,\n type TourAdvanceReason,\n type TourCloseReason,\n type TourDescriptor,\n type TourOpenReason,\n type TourStepChangeHandler,\n} from \"@hjmds/design-contracts/components/tour\";\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n type CSSProperties,\n} from \"react\";\nimport { Button } from \"./actions.js\";\nimport { classNames, composeRefs } from \"./internal.js\";\nimport { AnchoredPortal, useAnchoredPopup } from \"./portal.js\";\nimport { getModalLayer, HjmPortal, renderTrigger, useModalFocus, type OverlayTrigger } from \"./modal.js\";\n\nexport type TourProps<Id extends string = string> = Readonly<{\n descriptor: TourDescriptor<Id>;\n /** Resolves the product-owned anchor key to the element to point at. */\n resolveAnchor: (anchorId: string) => HTMLElement | null;\n composeAnnouncement: ComposeTourStepAnnouncement;\n onStepChange: TourStepChangeHandler<Id>;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean, detail: Readonly<{ reason: TourOpenReason }>) => void;\n trigger?: OverlayTrigger;\n portalContainer?: HTMLElement;\n className?: string;\n}>;\n\n/*\n The contract hands renderers an opaque `anchorId`, never a node, so the\n product owns what each step points at. This component therefore keeps its own\n element box: it re-reads the anchor on every step and on layout changes rather\n than storing a rect the product could invalidate by re-rendering the page.\n*/\nfunction useAnchorRect(anchor: HTMLElement | null, active: boolean): DOMRect | null {\n const [rect, setRect] = useState<DOMRect | null>(null);\n useEffect(() => {\n if (!active || !anchor) { setRect(null); return; }\n const measure = () => setRect(anchor.getBoundingClientRect());\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(anchor);\n window.addEventListener(\"scroll\", measure, true);\n window.addEventListener(\"resize\", measure);\n return () => {\n observer.disconnect();\n window.removeEventListener(\"scroll\", measure, true);\n window.removeEventListener(\"resize\", measure);\n };\n }, [anchor, active]);\n return rect;\n}\n\nexport const Tour = forwardRef(function Tour<Id extends string = string>(\n {\n descriptor,\n resolveAnchor,\n composeAnnouncement,\n onStepChange,\n open: openProp,\n defaultOpen,\n onOpenChange,\n trigger,\n portalContainer,\n className,\n }: TourProps<Id>,\n forwardedRef: React.Ref<HTMLDivElement>,\n) {\n validateTourOpenState({\n ...(openProp === undefined ? {} : { open: openProp }),\n ...(defaultOpen === undefined ? {} : { defaultOpen }),\n ...(onOpenChange === undefined ? {} : { onOpenChange }),\n } as Parameters<typeof validateTourOpenState>[0]);\n const [internalOpen, setInternalOpen] = useState(defaultOpen ?? false);\n const open = openProp ?? internalOpen;\n const resolved = resolveTourDescriptor(descriptor, { composeAnnouncement });\n const step = resolved.currentStep;\n\n const cardRef = useRef<HTMLDivElement>(null);\n const triggerRef = useRef<HTMLElement>(null);\n const anchorRef = useRef<HTMLElement | null>(null);\n const [card, setCard] = useState<HTMLDivElement | null>(null);\n const id = `${useId().replaceAll(\":\", \"\")}-tour`;\n const anchor = open ? resolveAnchor(step.anchorId) : null;\n anchorRef.current = anchor;\n const anchorRect = useAnchorRect(anchor, open);\n\n const change = useCallback((next: boolean, reason: TourOpenReason) => {\n if (openProp === undefined) setInternalOpen(next);\n onOpenChange?.(next, { reason });\n }, [openProp, onOpenChange]);\n const close = (reason: TourCloseReason) => change(false, reason);\n const advance = (reason: TourAdvanceReason) => {\n const outcome = resolveTourAdvance(descriptor, reason);\n // `no-op` is Previous on the first step: the contract makes that a decision,\n // not a disabled button, so the control stays focusable and simply does nothing.\n if (outcome.type === \"step\") onStepChange(outcome.stepId, reason);\n else if (outcome.type === \"close\") close(outcome.reason);\n };\n\n const openRef = useRef(open);\n openRef.current = open;\n const settleRef = useRef(close);\n settleRef.current = close;\n const epochRef = useRef(0);\n useEffect(() => {\n const epoch = epochRef.current + 1;\n epochRef.current = epoch;\n return () => {\n queueMicrotask(() => {\n // StrictMode re-runs setup right after its probe cleanup; only a real\n // unmount has no later epoch. An open tour torn down that way was\n // interrupted — the user never skipped, escaped, or completed it.\n if (epochRef.current !== epoch || !openRef.current) return;\n settleRef.current(\"interrupted\");\n });\n };\n }, []);\n\n useModalFocus({\n active: open,\n contentRef: cardRef,\n // The card itself, not its first button: it carries the step announcement,\n // which is the whole substitute for a sighted user's visual pointer.\n initialFocusRef: cardRef,\n ...(trigger === undefined ? {} : { fallbackReturnRef: triggerRef }),\n onEscape: () => close(\"escape\"),\n });\n useEffect(() => {\n // Focus follows the card on every step change, never the anchor: the anchor\n // is inert background while the tour runs.\n if (open) cardRef.current?.focus({ preventScroll: true });\n }, [open, step.id]);\n\n const position = useAnchoredPopup(anchorRef, open ? card : null, {\n ...(step.placement === undefined ? {} : { placement: step.placement }),\n ...(step.align === undefined ? {} : { align: step.align }),\n gap: tourRecipe.sideOffset,\n zIndex: getModalLayer(0) + 1,\n fallbackAxis: true,\n });\n const cardRefs = useCallback(composeRefs(setCard, cardRef, forwardedRef), [forwardedRef]);\n\n const renderedTrigger = trigger === undefined\n ? null\n : renderTrigger(trigger, triggerRef, open, id, \"dialog\", () => change(true, \"trigger\"));\n if (!open) return renderedTrigger;\n return (\n <>\n {renderedTrigger}\n <HjmPortal {...(portalContainer === undefined ? {} : { container: portalContainer })}>\n {/*\n No dismiss handler on the veil: the contract has no `outside` close\n reason, so an accidental pointer must not end a walkthrough.\n */}\n <div className=\"hjm-tour-backdrop\" data-state=\"open\" style={{ zIndex: getModalLayer(0) }}>\n {anchorRect ? (\n <div\n className=\"hjm-tour-highlight\"\n aria-hidden=\"true\"\n style={{\n insetBlockStart: anchorRect.top,\n insetInlineStart: anchorRect.left,\n inlineSize: anchorRect.width,\n blockSize: anchorRect.height,\n } as CSSProperties}\n />\n ) : null}\n </div>\n </HjmPortal>\n <AnchoredPortal anchorRef={anchorRef} {...(portalContainer ? { container: portalContainer } : {})}>\n <div\n ref={cardRefs}\n id={id}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={resolved.accessibilityLabel}\n aria-describedby={`${id}-announcement`}\n tabIndex={-1}\n data-hjm-modal-content=\"\"\n data-placement={position.placement}\n data-align={position.align}\n className={classNames(\"hjm-tour\", className)}\n style={{ ...position.style, maxInlineSize: tourRecipe.maxWidth } as CSSProperties}\n >\n {/*\n The composed announcement already carries title, description, and\n position, so the visible copy is hidden from assistive technology\n instead of being read twice.\n */}\n <span id={`${id}-announcement`} className=\"hjm-visually-hidden\">{step.announcement}</span>\n <div aria-hidden=\"true\">\n <p className=\"hjm-tour__counter\">{step.position} / {step.total}</p>\n <h2 className=\"hjm-tour__title\">{step.title}</h2>\n <p className=\"hjm-tour__description\">{step.description}</p>\n </div>\n <div className=\"hjm-tour__actions\">\n <Button tone=\"ghost\" onClick={() => close(\"skip\")}>{descriptor.labels.skip}</Button>\n <Button tone=\"secondary\" disabled={resolved.isFirstStep} onClick={() => advance(\"previous\")}>\n {descriptor.labels.previous}\n </Button>\n <Button onClick={() => advance(\"next\")}>\n {resolved.isLastStep ? descriptor.labels.done : descriptor.labels.next}\n </Button>\n </div>\n </div>\n </AnchoredPortal>\n </>\n );\n}) as <Id extends string = string>(\n props: TourProps<Id> & { ref?: React.Ref<HTMLDivElement> },\n) => React.ReactElement | null;\n"]}
@@ -0,0 +1,24 @@
1
+ import { type TransferListMoveDirection } from "@hjmds/design-contracts/components/transfer-list";
2
+ import type { SelectItemDescriptor } from "@hjmds/design-contracts/behaviors";
3
+ export type TransferListLabels = Readonly<{
4
+ source: string;
5
+ target: string;
6
+ toTarget: string;
7
+ toSource: string;
8
+ selectAll: string;
9
+ empty: string;
10
+ }>;
11
+ export type TransferListProps<Id extends string = string> = Readonly<{
12
+ items: readonly SelectItemDescriptor<Id>[];
13
+ labels: TransferListLabels;
14
+ targetKeys?: ReadonlySet<Id>;
15
+ defaultTargetKeys?: ReadonlySet<Id>;
16
+ onTargetKeysChange?: (keys: ReadonlySet<Id>) => void;
17
+ /** Receives which ids moved, in origin-panel order, so the product announces it. */
18
+ onMove?: (movedIds: readonly Id[], direction: TransferListMoveDirection) => void;
19
+ className?: string;
20
+ }>;
21
+ export declare const TransferList: <Id extends string = string>(props: TransferListProps<Id> & {
22
+ ref?: React.Ref<HTMLDivElement>;
23
+ }) => React.ReactElement | null;
24
+ //# sourceMappingURL=transfer-list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transfer-list.d.ts","sourceRoot":"","sources":["../src/transfer-list.tsx"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,yBAAyB,EAG/B,MAAM,kDAAkD,CAAC;AAC1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAW9E,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC,CAAC;AAEH,MAAM,MAAM,iBAAiB,CAAC,EAAE,SAAS,MAAM,GAAG,MAAM,IAAI,QAAQ,CAAC;IACnE,KAAK,EAAE,SAAS,oBAAoB,CAAC,EAAE,CAAC,EAAE,CAAC;IAC3C,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,CAAC,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC;IAC7B,iBAAiB,CAAC,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC;IACpC,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,CAAC,EAAE,CAAC,KAAK,IAAI,CAAC;IACrD,oFAAoF;IACpF,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,EAAE,EAAE,SAAS,EAAE,yBAAyB,KAAK,IAAI,CAAC;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC,CAAC;AAOH,eAAO,MAAM,YAAY,EA0JnB,CAAC,EAAE,SAAS,MAAM,GAAG,MAAM,EAC/B,KAAK,EAAE,iBAAiB,CAAC,EAAE,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;CAAE,KAC/D,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC"}
@@ -0,0 +1,109 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { moveTransferListSelection, reconcileTransferListSelection, resolveTransferListFocusAfterMove, resolveTransferListPanels, resolveTransferListSelectAllState, toggleTransferListSelectAll, toggleTransferListSelection, } from "@hjmds/design-contracts/components/transfer-list";
3
+ import { forwardRef, useMemo, useRef, useState, } from "react";
4
+ import { Button } from "./actions.js";
5
+ import { classNames, composeRefs, useControllableState } from "./internal.js";
6
+ const emptySelection = () => ({
7
+ source: new Set(),
8
+ target: new Set(),
9
+ });
10
+ export const TransferList = forwardRef(function TransferList({ items, labels, targetKeys: controlledTargetKeys, defaultTargetKeys, onTargetKeysChange, onMove, className, }, forwardedRef) {
11
+ const [targetKeys, setTargetKeys] = useControllableState({
12
+ ...(controlledTargetKeys === undefined ? {} : { value: controlledTargetKeys }),
13
+ defaultValue: defaultTargetKeys ?? new Set(),
14
+ ...(onTargetKeysChange === undefined ? {} : { onChange: onTargetKeysChange }),
15
+ });
16
+ const [rawSelection, setSelection] = useState((emptySelection));
17
+ const descriptor = useMemo(() => ({ items, targetKeys }), [items, targetKeys]);
18
+ // A row that left the list entirely must not keep a pending check mark.
19
+ const selection = useMemo(() => reconcileTransferListSelection(descriptor, rawSelection), [descriptor, rawSelection]);
20
+ const panels = resolveTransferListPanels(descriptor);
21
+ const rootRef = useRef(null);
22
+ const [focused, setFocused] = useState({ source: null, target: null });
23
+ const focusRow = (panel, id) => {
24
+ setFocused((previous) => ({ ...previous, [panel]: id }));
25
+ if (id === null)
26
+ return;
27
+ rootRef.current?.querySelector(`[data-hjm-transfer-row="${panel}:${CSS.escape(id)}"]`)?.focus();
28
+ };
29
+ const move = (direction) => {
30
+ const from = direction === "toTarget" ? "source" : "target";
31
+ const before = panels[from];
32
+ const result = moveTransferListSelection(descriptor, selection, direction);
33
+ if (result.movedIds.length === 0)
34
+ return;
35
+ const removedIndex = before.findIndex((item) => item.id === result.movedIds[0]);
36
+ const remaining = before.filter((item) => !result.movedIds.includes(item.id));
37
+ setTargetKeys(result.targetKeys);
38
+ setSelection(result.selection);
39
+ onMove?.(result.movedIds, direction);
40
+ // Focus lands on the row that slid into the first removed position, or on
41
+ // the panel's empty state — never on the document body.
42
+ const next = resolveTransferListFocusAfterMove(remaining, removedIndex);
43
+ queueMicrotask(() => {
44
+ if (next !== null)
45
+ focusRow(from, next);
46
+ else
47
+ rootRef.current?.querySelector(`[data-hjm-transfer-empty="${from}"]`)?.focus();
48
+ });
49
+ };
50
+ const moveSingle = (panel, id) => {
51
+ // A focused row moves on its own: a single move must not require first
52
+ // building a multi-selection.
53
+ const single = { ...emptySelection(), [panel]: new Set([id]) };
54
+ const direction = panel === "source" ? "toTarget" : "toSource";
55
+ const result = moveTransferListSelection(descriptor, single, direction);
56
+ if (result.movedIds.length === 0)
57
+ return;
58
+ const before = panels[panel];
59
+ const removedIndex = before.findIndex((item) => item.id === id);
60
+ const remaining = before.filter((item) => item.id !== id);
61
+ setTargetKeys(result.targetKeys);
62
+ setSelection((current) => ({ ...current, [panel]: new Set([...current[panel]].filter((key) => key !== id)) }));
63
+ onMove?.(result.movedIds, direction);
64
+ const next = resolveTransferListFocusAfterMove(remaining, removedIndex);
65
+ queueMicrotask(() => {
66
+ if (next !== null)
67
+ focusRow(panel, next);
68
+ else
69
+ rootRef.current?.querySelector(`[data-hjm-transfer-empty="${panel}"]`)?.focus();
70
+ });
71
+ };
72
+ const renderPanel = (panel) => {
73
+ const panelItems = panels[panel];
74
+ const selected = selection[panel];
75
+ const selectAllState = resolveTransferListSelectAllState(descriptor, selection, panel);
76
+ const activeId = panelItems.some((item) => item.id === focused[panel])
77
+ ? focused[panel]
78
+ : panelItems.find((item) => !item.disabled)?.id ?? panelItems[0]?.id ?? null;
79
+ const onKeyDown = (event, item) => {
80
+ const index = panelItems.findIndex((candidate) => candidate.id === item.id);
81
+ const goto = (nextIndex) => {
82
+ const target = panelItems[Math.min(Math.max(nextIndex, 0), panelItems.length - 1)];
83
+ if (target) {
84
+ event.preventDefault();
85
+ focusRow(panel, target.id);
86
+ }
87
+ };
88
+ if (event.key === "ArrowDown")
89
+ goto(index + 1);
90
+ else if (event.key === "ArrowUp")
91
+ goto(index - 1);
92
+ else if (event.key === "Home")
93
+ goto(0);
94
+ else if (event.key === "End")
95
+ goto(panelItems.length - 1);
96
+ else if (event.key === " ") {
97
+ event.preventDefault();
98
+ setSelection(toggleTransferListSelection(descriptor, selection, panel, item.id));
99
+ }
100
+ else if (event.key === "Enter") {
101
+ event.preventDefault();
102
+ moveSingle(panel, item.id);
103
+ }
104
+ };
105
+ return (_jsxs("div", { className: "hjm-transfer-list__panel", role: "group", "aria-label": labels[panel], children: [_jsxs("div", { className: "hjm-transfer-list__header", children: [_jsx("span", { className: "hjm-transfer-list__title", children: labels[panel] }), _jsx("button", { type: "button", role: "checkbox", "aria-checked": selectAllState === "mixed" ? "mixed" : String(selectAllState === true), "aria-label": `${labels[panel]} ${labels.selectAll}`, className: "hjm-transfer-list__select-all", onClick: () => setSelection(toggleTransferListSelectAll(descriptor, selection, panel)), children: selectAllState === true ? "✓" : selectAllState === "mixed" ? "–" : "" })] }), panelItems.length === 0 ? (_jsx("p", { className: "hjm-transfer-list__empty", "data-hjm-transfer-empty": panel, tabIndex: -1, children: labels.empty })) : (_jsx("div", { role: "listbox", "aria-multiselectable": "true", "aria-label": labels[panel], className: "hjm-transfer-list__options", children: panelItems.map((item) => (_jsxs("div", { role: "option", "data-hjm-transfer-row": `${panel}:${item.id}`, "aria-selected": selected.has(item.id), "aria-disabled": item.disabled || undefined, tabIndex: item.id === activeId ? 0 : -1, className: "hjm-transfer-list__option", onFocus: () => setFocused((previous) => ({ ...previous, [panel]: item.id })), onClick: () => setSelection(toggleTransferListSelection(descriptor, selection, panel, item.id)), onKeyDown: (event) => onKeyDown(event, item), children: [_jsx("span", { "aria-hidden": "true", className: "hjm-transfer-list__mark", "data-checked": selected.has(item.id) || undefined, children: selected.has(item.id) ? "✓" : "" }), _jsx("span", { children: item.label })] }, item.id))) }))] }));
106
+ };
107
+ return (_jsxs("div", { ref: composeRefs(rootRef, forwardedRef), className: classNames("hjm-transfer-list", className), children: [renderPanel("source"), _jsxs("div", { className: "hjm-transfer-list__actions", children: [_jsx(Button, { tone: "secondary", disabled: selection.source.size === 0, onClick: () => move("toTarget"), children: labels.toTarget }), _jsx(Button, { tone: "secondary", disabled: selection.target.size === 0, onClick: () => move("toSource"), children: labels.toSource })] }), renderPanel("target")] }));
108
+ });
109
+ //# sourceMappingURL=transfer-list.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transfer-list.js","sourceRoot":"","sources":["../src/transfer-list.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n moveTransferListSelection,\n reconcileTransferListSelection,\n resolveTransferListFocusAfterMove,\n resolveTransferListPanels,\n resolveTransferListSelectAllState,\n toggleTransferListSelectAll,\n toggleTransferListSelection,\n type TransferListMoveDirection,\n type TransferListPanel,\n type TransferListSelection,\n} from \"@hjmds/design-contracts/components/transfer-list\";\nimport type { SelectItemDescriptor } from \"@hjmds/design-contracts/behaviors\";\nimport {\n forwardRef,\n useMemo,\n useRef,\n useState,\n type KeyboardEvent,\n} from \"react\";\nimport { Button } from \"./actions.js\";\nimport { classNames, composeRefs, useControllableState } from \"./internal.js\";\n\nexport type TransferListLabels = Readonly<{\n source: string;\n target: string;\n toTarget: string;\n toSource: string;\n selectAll: string;\n empty: string;\n}>;\n\nexport type TransferListProps<Id extends string = string> = Readonly<{\n items: readonly SelectItemDescriptor<Id>[];\n labels: TransferListLabels;\n targetKeys?: ReadonlySet<Id>;\n defaultTargetKeys?: ReadonlySet<Id>;\n onTargetKeysChange?: (keys: ReadonlySet<Id>) => void;\n /** Receives which ids moved, in origin-panel order, so the product announces it. */\n onMove?: (movedIds: readonly Id[], direction: TransferListMoveDirection) => void;\n className?: string;\n}>;\n\nconst emptySelection = <Id extends string>(): TransferListSelection<Id> => ({\n source: new Set<Id>(),\n target: new Set<Id>(),\n});\n\nexport const TransferList = forwardRef(function TransferList<Id extends string = string>(\n {\n items,\n labels,\n targetKeys: controlledTargetKeys,\n defaultTargetKeys,\n onTargetKeysChange,\n onMove,\n className,\n }: TransferListProps<Id>,\n forwardedRef: React.Ref<HTMLDivElement>,\n) {\n const [targetKeys, setTargetKeys] = useControllableState<ReadonlySet<Id>>({\n ...(controlledTargetKeys === undefined ? {} : { value: controlledTargetKeys }),\n defaultValue: defaultTargetKeys ?? new Set<Id>(),\n ...(onTargetKeysChange === undefined ? {} : { onChange: onTargetKeysChange }),\n });\n const [rawSelection, setSelection] = useState<TransferListSelection<Id>>(emptySelection<Id>);\n const descriptor = useMemo(() => ({ items, targetKeys }), [items, targetKeys]);\n // A row that left the list entirely must not keep a pending check mark.\n const selection = useMemo(\n () => reconcileTransferListSelection(descriptor, rawSelection),\n [descriptor, rawSelection],\n );\n const panels = resolveTransferListPanels(descriptor);\n const rootRef = useRef<HTMLDivElement>(null);\n const [focused, setFocused] = useState<Readonly<Record<TransferListPanel, Id | null>>>({ source: null, target: null });\n\n const focusRow = (panel: TransferListPanel, id: Id | null) => {\n setFocused((previous) => ({ ...previous, [panel]: id }));\n if (id === null) return;\n rootRef.current?.querySelector<HTMLElement>(`[data-hjm-transfer-row=\"${panel}:${CSS.escape(id)}\"]`)?.focus();\n };\n const move = (direction: TransferListMoveDirection) => {\n const from: TransferListPanel = direction === \"toTarget\" ? \"source\" : \"target\";\n const before = panels[from];\n const result = moveTransferListSelection(descriptor, selection, direction);\n if (result.movedIds.length === 0) return;\n const removedIndex = before.findIndex((item) => item.id === result.movedIds[0]);\n const remaining = before.filter((item) => !result.movedIds.includes(item.id));\n setTargetKeys(result.targetKeys);\n setSelection(result.selection);\n onMove?.(result.movedIds, direction);\n // Focus lands on the row that slid into the first removed position, or on\n // the panel's empty state — never on the document body.\n const next = resolveTransferListFocusAfterMove(remaining, removedIndex);\n queueMicrotask(() => {\n if (next !== null) focusRow(from, next);\n else rootRef.current?.querySelector<HTMLElement>(`[data-hjm-transfer-empty=\"${from}\"]`)?.focus();\n });\n };\n const moveSingle = (panel: TransferListPanel, id: Id) => {\n // A focused row moves on its own: a single move must not require first\n // building a multi-selection.\n const single = { ...emptySelection<Id>(), [panel]: new Set<Id>([id]) } as TransferListSelection<Id>;\n const direction: TransferListMoveDirection = panel === \"source\" ? \"toTarget\" : \"toSource\";\n const result = moveTransferListSelection(descriptor, single, direction);\n if (result.movedIds.length === 0) return;\n const before = panels[panel];\n const removedIndex = before.findIndex((item) => item.id === id);\n const remaining = before.filter((item) => item.id !== id);\n setTargetKeys(result.targetKeys);\n setSelection((current) => ({ ...current, [panel]: new Set([...current[panel]].filter((key) => key !== id)) }));\n onMove?.(result.movedIds, direction);\n const next = resolveTransferListFocusAfterMove(remaining, removedIndex);\n queueMicrotask(() => {\n if (next !== null) focusRow(panel, next);\n else rootRef.current?.querySelector<HTMLElement>(`[data-hjm-transfer-empty=\"${panel}\"]`)?.focus();\n });\n };\n\n const renderPanel = (panel: TransferListPanel) => {\n const panelItems = panels[panel];\n const selected = selection[panel];\n const selectAllState = resolveTransferListSelectAllState(descriptor, selection, panel);\n const activeId = panelItems.some((item) => item.id === focused[panel])\n ? focused[panel]\n : panelItems.find((item) => !item.disabled)?.id ?? panelItems[0]?.id ?? null;\n const onKeyDown = (event: KeyboardEvent<HTMLElement>, item: SelectItemDescriptor<Id>) => {\n const index = panelItems.findIndex((candidate) => candidate.id === item.id);\n const goto = (nextIndex: number) => {\n const target = panelItems[Math.min(Math.max(nextIndex, 0), panelItems.length - 1)];\n if (target) { event.preventDefault(); focusRow(panel, target.id); }\n };\n if (event.key === \"ArrowDown\") goto(index + 1);\n else if (event.key === \"ArrowUp\") goto(index - 1);\n else if (event.key === \"Home\") goto(0);\n else if (event.key === \"End\") goto(panelItems.length - 1);\n else if (event.key === \" \") {\n event.preventDefault();\n setSelection(toggleTransferListSelection(descriptor, selection, panel, item.id));\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n moveSingle(panel, item.id);\n }\n };\n return (\n <div className=\"hjm-transfer-list__panel\" role=\"group\" aria-label={labels[panel]}>\n <div className=\"hjm-transfer-list__header\">\n <span className=\"hjm-transfer-list__title\">{labels[panel]}</span>\n <button\n type=\"button\"\n role=\"checkbox\"\n aria-checked={selectAllState === \"mixed\" ? \"mixed\" : String(selectAllState === true) as \"true\" | \"false\"}\n aria-label={`${labels[panel]} ${labels.selectAll}`}\n className=\"hjm-transfer-list__select-all\"\n onClick={() => setSelection(toggleTransferListSelectAll(descriptor, selection, panel))}\n >\n {selectAllState === true ? \"✓\" : selectAllState === \"mixed\" ? \"–\" : \"\"}\n </button>\n </div>\n {panelItems.length === 0 ? (\n <p className=\"hjm-transfer-list__empty\" data-hjm-transfer-empty={panel} tabIndex={-1}>{labels.empty}</p>\n ) : (\n <div role=\"listbox\" aria-multiselectable=\"true\" aria-label={labels[panel]} className=\"hjm-transfer-list__options\">\n {panelItems.map((item) => (\n <div\n key={item.id}\n role=\"option\"\n data-hjm-transfer-row={`${panel}:${item.id}`}\n aria-selected={selected.has(item.id)}\n aria-disabled={item.disabled || undefined}\n tabIndex={item.id === activeId ? 0 : -1}\n className=\"hjm-transfer-list__option\"\n onFocus={() => setFocused((previous) => ({ ...previous, [panel]: item.id }))}\n onClick={() => setSelection(toggleTransferListSelection(descriptor, selection, panel, item.id))}\n onKeyDown={(event) => onKeyDown(event, item)}\n >\n <span aria-hidden=\"true\" className=\"hjm-transfer-list__mark\" data-checked={selected.has(item.id) || undefined}>\n {selected.has(item.id) ? \"✓\" : \"\"}\n </span>\n <span>{item.label}</span>\n </div>\n ))}\n </div>\n )}\n </div>\n );\n };\n\n return (\n <div ref={composeRefs(rootRef, forwardedRef)} className={classNames(\"hjm-transfer-list\", className)}>\n {renderPanel(\"source\")}\n <div className=\"hjm-transfer-list__actions\">\n <Button tone=\"secondary\" disabled={selection.source.size === 0} onClick={() => move(\"toTarget\")}>\n {labels.toTarget}\n </Button>\n <Button tone=\"secondary\" disabled={selection.target.size === 0} onClick={() => move(\"toSource\")}>\n {labels.toSource}\n </Button>\n </div>\n {renderPanel(\"target\")}\n </div>\n );\n}) as <Id extends string = string>(\n props: TransferListProps<Id> & { ref?: React.Ref<HTMLDivElement> },\n) => React.ReactElement | null;\n"]}
package/dist/tree.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { type ComposeTreeAccessibleName, type TreeAsyncState, type TreeNodeDescriptor, type TreeSelectionModel } from "@hjmds/design-contracts/components/tree";
2
+ import type { CheckboxState } from "@hjmds/design-contracts/behaviors";
3
+ import { type ReactNode } from "react";
4
+ export type TreeProps<Id extends string = string> = Readonly<{
5
+ label: string;
6
+ nodes: readonly TreeNodeDescriptor<Id>[];
7
+ composeAccessibleName: ComposeTreeAccessibleName;
8
+ expandedKeys?: ReadonlySet<Id>;
9
+ defaultExpandedKeys?: ReadonlySet<Id>;
10
+ onExpandedKeysChange?: (keys: ReadonlySet<Id>) => void;
11
+ selection?: TreeSelectionModel<Id>;
12
+ /**
13
+ * Tri-state check marks per node, as `resolveTreeCheckedStates` derives them.
14
+ * Checkboxes are not nested controls in a tree: the state rides on the node
15
+ * itself as `aria-checked`, keeping the contract's one-tab-stop-per-node rule.
16
+ */
17
+ checkedStates?: ReadonlyMap<Id, CheckboxState>;
18
+ onCheckedToggle?: (id: Id) => void;
19
+ asyncState?: TreeAsyncState;
20
+ /** Product-owned glyph for the expand/collapse affordance; decorative by contract. */
21
+ renderToggle?: (state: Readonly<{
22
+ expanded: boolean;
23
+ }>) => ReactNode;
24
+ className?: string;
25
+ }>;
26
+ export declare const Tree: <Id extends string = string>(props: TreeProps<Id> & {
27
+ ref?: React.Ref<HTMLDivElement>;
28
+ }) => React.ReactElement | null;
29
+ //# sourceMappingURL=tree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../src/tree.tsx"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,yBAAyB,EAE9B,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACxB,MAAM,yCAAyC,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,mCAAmC,CAAC;AACvF,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAIf,MAAM,MAAM,SAAS,CAAC,EAAE,SAAS,MAAM,GAAG,MAAM,IAAI,QAAQ,CAAC;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,kBAAkB,CAAC,EAAE,CAAC,EAAE,CAAC;IACzC,qBAAqB,EAAE,yBAAyB,CAAC;IACjD,YAAY,CAAC,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC;IAC/B,mBAAmB,CAAC,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC;IACtC,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,CAAC,EAAE,CAAC,KAAK,IAAI,CAAC;IACvD,SAAS,CAAC,EAAE,kBAAkB,CAAC,EAAE,CAAC,CAAC;IACnC;;;;OAIG;IACH,aAAa,CAAC,EAAE,WAAW,CAAC,EAAE,EAAE,aAAa,CAAC,CAAC;IAC/C,eAAe,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,IAAI,CAAC;IACnC,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,sFAAsF;IACtF,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC,KAAK,SAAS,CAAC;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC,CAAC;AAaH,eAAO,MAAM,IAAI,EAuJX,CAAC,EAAE,SAAS,MAAM,GAAG,MAAM,EAC/B,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;CAAE,KACvD,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC"}
package/dist/tree.js ADDED
@@ -0,0 +1,120 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { getTreeArrowKeyIntent, getTreeArrowResult, getVisibleTreeNavigationTarget, getVisibleTreeTypeaheadMatch, reconcileTreeExpansion, resolveTreeDescriptor, treeRecipe, } from "@hjmds/design-contracts/components/tree";
3
+ import { forwardRef, useMemo, useRef, useState, } from "react";
4
+ import { classNames, composeRefs, useControllableState } from "./internal.js";
5
+ import { useOptionalHjmTheme } from "./provider.js";
6
+ const typeaheadResetMs = 500;
7
+ function selectedIds(selection) {
8
+ if (!selection || selection.mode === "none")
9
+ return new Set();
10
+ if (selection.mode === "single") {
11
+ const key = selection.selectedKey ?? selection.defaultSelectedKey ?? null;
12
+ return key === null ? new Set() : new Set([key]);
13
+ }
14
+ return selection.selectedKeys ?? selection.defaultSelectedKeys ?? new Set();
15
+ }
16
+ export const Tree = forwardRef(function Tree({ label, nodes, composeAccessibleName, expandedKeys: controlledExpanded, defaultExpandedKeys, onExpandedKeysChange, selection, checkedStates, onCheckedToggle, asyncState = { status: "idle" }, renderToggle, className, }, forwardedRef) {
17
+ const [expandedKeys, setExpandedKeys] = useControllableState({
18
+ ...(controlledExpanded === undefined ? {} : { value: controlledExpanded }),
19
+ defaultValue: defaultExpandedKeys ?? new Set(),
20
+ ...(onExpandedKeysChange === undefined ? {} : { onChange: onExpandedKeysChange }),
21
+ });
22
+ const direction = useOptionalHjmTheme()?.environment.direction ?? "ltr";
23
+ // Expansion keys survive a node list change only where the node still exists
24
+ // and can still hold children; the contract owns that reconciliation.
25
+ const liveExpanded = useMemo(() => reconcileTreeExpansion(nodes, expandedKeys), [nodes, expandedKeys]);
26
+ const resolved = useMemo(() => resolveTreeDescriptor(nodes, liveExpanded, { composeAccessibleName }), [nodes, liveExpanded, composeAccessibleName]);
27
+ const visible = useMemo(() => resolved.filter((node) => node.visible), [resolved]);
28
+ const selected = selectedIds(selection);
29
+ const [focusedId, setFocusedId] = useState(null);
30
+ // Roving tab stop: one node is tabbable, and it must stay a real visible node
31
+ // after a collapse removed the previously focused descendant.
32
+ const activeId = visible.some((node) => node.id === focusedId) ? focusedId : visible[0]?.id ?? null;
33
+ const rootRef = useRef(null);
34
+ const typeahead = useRef({ query: "", at: 0 });
35
+ const focusNode = (id) => {
36
+ setFocusedId(id);
37
+ rootRef.current?.querySelector(`[data-hjm-tree-node="${CSS.escape(id)}"]`)?.focus();
38
+ };
39
+ const setExpanded = (id, expanded) => {
40
+ const next = new Set(liveExpanded);
41
+ if (expanded)
42
+ next.add(id);
43
+ else
44
+ next.delete(id);
45
+ setExpandedKeys(next);
46
+ };
47
+ const toggleSelection = (node) => {
48
+ if (node.disabled || !selection || selection.mode === "none")
49
+ return;
50
+ if (selection.mode === "single") {
51
+ const current = selection.selectedKey ?? selection.defaultSelectedKey ?? null;
52
+ const next = current === node.id && selection.disallowEmptySelection !== true ? null : node.id;
53
+ selection.onSelectionChange?.(next);
54
+ return;
55
+ }
56
+ const next = new Set(selection.selectedKeys ?? selection.defaultSelectedKeys ?? []);
57
+ if (next.has(node.id))
58
+ next.delete(node.id);
59
+ else
60
+ next.add(node.id);
61
+ selection.onSelectionChange?.(next);
62
+ };
63
+ const onKeyDown = (event, node) => {
64
+ const key = event.key;
65
+ const arrow = getTreeArrowKeyIntent(key, direction);
66
+ if (arrow) {
67
+ const result = getTreeArrowResult(visible, node.id, arrow);
68
+ if (result.action === "none")
69
+ return;
70
+ event.preventDefault();
71
+ if (result.action === "moveFocus")
72
+ focusNode(result.targetId);
73
+ else
74
+ setExpanded(node.id, result.action === "expand");
75
+ return;
76
+ }
77
+ if (key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End") {
78
+ const intent = key === "ArrowDown" ? "next" : key === "ArrowUp" ? "previous" : key === "Home" ? "first" : "last";
79
+ const target = getVisibleTreeNavigationTarget(visible, node.id, intent);
80
+ if (target === undefined)
81
+ return;
82
+ event.preventDefault();
83
+ focusNode(target);
84
+ return;
85
+ }
86
+ if (key === "Enter" || event.key === " ") {
87
+ event.preventDefault();
88
+ if (checkedStates && onCheckedToggle) {
89
+ if (!node.disabled)
90
+ onCheckedToggle(node.id);
91
+ }
92
+ else
93
+ toggleSelection(node);
94
+ return;
95
+ }
96
+ if (event.key.length !== 1 || event.metaKey || event.ctrlKey || event.altKey)
97
+ return;
98
+ const now = Date.now();
99
+ const query = now - typeahead.current.at > typeaheadResetMs ? event.key : typeahead.current.query + event.key;
100
+ typeahead.current = { query, at: now };
101
+ const match = getVisibleTreeTypeaheadMatch(visible, query, { startsAfterKey: node.id });
102
+ if (match !== undefined) {
103
+ event.preventDefault();
104
+ focusNode(match);
105
+ }
106
+ };
107
+ return (_jsxs("div", { ref: composeRefs(rootRef, forwardedRef), role: "tree", "aria-label": label, "aria-busy": asyncState.status === "loading" || asyncState.status === "loadingMore" || undefined, "aria-multiselectable": selection?.mode === "multiple" || undefined, className: classNames("hjm-tree", className), style: { "--hjm-tree-indent": `${treeRecipe.indentPerLevel}px` }, children: [asyncState.status === "empty" || asyncState.status === "error" || asyncState.status === "loading" ? (_jsx("p", { className: "hjm-tree__state", role: asyncState.status === "error" ? "alert" : "status", children: asyncState.message })) : null, visible.map((node) => (_jsxs("div", { role: "treeitem", "data-hjm-tree-node": node.id, "aria-level": node.depth, "aria-posinset": node.position, "aria-setsize": node.siblingCount, "aria-expanded": node.hasChildren ? node.expanded : undefined, "aria-selected": selection && selection.mode !== "none" ? selected.has(node.id) : undefined, "aria-checked": checkedStates ? (checkedStates.get(node.id) === "mixed" ? "mixed" : String(checkedStates.get(node.id) === true)) : undefined, "aria-disabled": node.disabled || undefined, "aria-label": node.accessibleName,
108
+ // One tab stop for the whole tree; the glyph is decorative, not a
109
+ // nested control, so a node is the only focusable thing in a row.
110
+ tabIndex: node.id === activeId ? 0 : -1, className: "hjm-tree__node", style: { "--hjm-tree-depth": node.depth - 1 }, onClick: () => {
111
+ setFocusedId(node.id);
112
+ if (checkedStates && onCheckedToggle) {
113
+ if (!node.disabled)
114
+ onCheckedToggle(node.id);
115
+ }
116
+ else
117
+ toggleSelection(node);
118
+ }, onFocus: () => setFocusedId(node.id), onKeyDown: (event) => onKeyDown(event, node), children: [_jsx("span", { "aria-hidden": "true", className: "hjm-tree__indent" }), checkedStates ? (_jsx("span", { "aria-hidden": "true", className: "hjm-tree__check", "data-state": String(checkedStates.get(node.id) ?? false), children: checkedStates.get(node.id) === true ? "✓" : checkedStates.get(node.id) === "mixed" ? "–" : "" })) : null, _jsx("span", { "aria-hidden": "true", className: "hjm-tree__toggle", "data-visible": node.hasChildren || undefined, children: node.hasChildren ? renderToggle?.({ expanded: node.expanded }) ?? (node.expanded ? "▾" : "▸") : null }), _jsxs("span", { "aria-hidden": "true", className: "hjm-tree__label", children: [node.label, node.description ? _jsx("span", { className: "hjm-tree__description", children: node.description }) : null] })] }, node.id))), asyncState.status === "loadingMore" ? _jsx("p", { className: "hjm-tree__state", role: "status", children: asyncState.message }) : null] }));
119
+ });
120
+ //# sourceMappingURL=tree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree.js","sourceRoot":"","sources":["../src/tree.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n getTreeArrowKeyIntent,\n getTreeArrowResult,\n getVisibleTreeNavigationTarget,\n getVisibleTreeTypeaheadMatch,\n reconcileTreeExpansion,\n resolveTreeDescriptor,\n treeRecipe,\n type ComposeTreeAccessibleName,\n type ResolvedTreeNodeDescriptor,\n type TreeAsyncState,\n type TreeNodeDescriptor,\n type TreeSelectionModel,\n} from \"@hjmds/design-contracts/components/tree\";\nimport type { CheckboxState, WebKeyboardKey } from \"@hjmds/design-contracts/behaviors\";\nimport {\n forwardRef,\n useMemo,\n useRef,\n useState,\n type CSSProperties,\n type KeyboardEvent,\n type ReactNode,\n} from \"react\";\nimport { classNames, composeRefs, useControllableState } from \"./internal.js\";\nimport { useOptionalHjmTheme } from \"./provider.js\";\n\nexport type TreeProps<Id extends string = string> = Readonly<{\n label: string;\n nodes: readonly TreeNodeDescriptor<Id>[];\n composeAccessibleName: ComposeTreeAccessibleName;\n expandedKeys?: ReadonlySet<Id>;\n defaultExpandedKeys?: ReadonlySet<Id>;\n onExpandedKeysChange?: (keys: ReadonlySet<Id>) => void;\n selection?: TreeSelectionModel<Id>;\n /**\n * Tri-state check marks per node, as `resolveTreeCheckedStates` derives them.\n * Checkboxes are not nested controls in a tree: the state rides on the node\n * itself as `aria-checked`, keeping the contract's one-tab-stop-per-node rule.\n */\n checkedStates?: ReadonlyMap<Id, CheckboxState>;\n onCheckedToggle?: (id: Id) => void;\n asyncState?: TreeAsyncState;\n /** Product-owned glyph for the expand/collapse affordance; decorative by contract. */\n renderToggle?: (state: Readonly<{ expanded: boolean }>) => ReactNode;\n className?: string;\n}>;\n\nconst typeaheadResetMs = 500;\n\nfunction selectedIds<Id extends string>(selection: TreeSelectionModel<Id> | undefined): ReadonlySet<Id> {\n if (!selection || selection.mode === \"none\") return new Set<Id>();\n if (selection.mode === \"single\") {\n const key = selection.selectedKey ?? selection.defaultSelectedKey ?? null;\n return key === null ? new Set<Id>() : new Set<Id>([key]);\n }\n return selection.selectedKeys ?? selection.defaultSelectedKeys ?? new Set<Id>();\n}\n\nexport const Tree = forwardRef(function Tree<Id extends string = string>(\n {\n label,\n nodes,\n composeAccessibleName,\n expandedKeys: controlledExpanded,\n defaultExpandedKeys,\n onExpandedKeysChange,\n selection,\n checkedStates,\n onCheckedToggle,\n asyncState = { status: \"idle\" },\n renderToggle,\n className,\n }: TreeProps<Id>,\n forwardedRef: React.Ref<HTMLDivElement>,\n) {\n const [expandedKeys, setExpandedKeys] = useControllableState<ReadonlySet<Id>>({\n ...(controlledExpanded === undefined ? {} : { value: controlledExpanded }),\n defaultValue: defaultExpandedKeys ?? new Set<Id>(),\n ...(onExpandedKeysChange === undefined ? {} : { onChange: onExpandedKeysChange }),\n });\n const direction = useOptionalHjmTheme()?.environment.direction ?? \"ltr\";\n // Expansion keys survive a node list change only where the node still exists\n // and can still hold children; the contract owns that reconciliation.\n const liveExpanded = useMemo(() => reconcileTreeExpansion(nodes, expandedKeys), [nodes, expandedKeys]);\n const resolved = useMemo(\n () => resolveTreeDescriptor(nodes, liveExpanded, { composeAccessibleName }),\n [nodes, liveExpanded, composeAccessibleName],\n );\n const visible = useMemo(() => resolved.filter((node) => node.visible), [resolved]);\n const selected = selectedIds(selection);\n const [focusedId, setFocusedId] = useState<Id | null>(null);\n // Roving tab stop: one node is tabbable, and it must stay a real visible node\n // after a collapse removed the previously focused descendant.\n const activeId = visible.some((node) => node.id === focusedId) ? focusedId : visible[0]?.id ?? null;\n const rootRef = useRef<HTMLDivElement>(null);\n const typeahead = useRef<Readonly<{ query: string; at: number }>>({ query: \"\", at: 0 });\n\n const focusNode = (id: Id) => {\n setFocusedId(id);\n rootRef.current?.querySelector<HTMLElement>(`[data-hjm-tree-node=\"${CSS.escape(id)}\"]`)?.focus();\n };\n const setExpanded = (id: Id, expanded: boolean) => {\n const next = new Set(liveExpanded);\n if (expanded) next.add(id); else next.delete(id);\n setExpandedKeys(next);\n };\n const toggleSelection = (node: ResolvedTreeNodeDescriptor<Id>) => {\n if (node.disabled || !selection || selection.mode === \"none\") return;\n if (selection.mode === \"single\") {\n const current = selection.selectedKey ?? selection.defaultSelectedKey ?? null;\n const next = current === node.id && selection.disallowEmptySelection !== true ? null : node.id;\n selection.onSelectionChange?.(next);\n return;\n }\n const next = new Set(selection.selectedKeys ?? selection.defaultSelectedKeys ?? []);\n if (next.has(node.id)) next.delete(node.id); else next.add(node.id);\n selection.onSelectionChange?.(next);\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>, node: ResolvedTreeNodeDescriptor<Id>) => {\n const key = event.key as WebKeyboardKey;\n const arrow = getTreeArrowKeyIntent(key, direction);\n if (arrow) {\n const result = getTreeArrowResult(visible, node.id, arrow);\n if (result.action === \"none\") return;\n event.preventDefault();\n if (result.action === \"moveFocus\") focusNode(result.targetId);\n else setExpanded(node.id, result.action === \"expand\");\n return;\n }\n if (key === \"ArrowDown\" || key === \"ArrowUp\" || key === \"Home\" || key === \"End\") {\n const intent = key === \"ArrowDown\" ? \"next\" : key === \"ArrowUp\" ? \"previous\" : key === \"Home\" ? \"first\" : \"last\";\n const target = getVisibleTreeNavigationTarget(visible, node.id, intent);\n if (target === undefined) return;\n event.preventDefault();\n focusNode(target);\n return;\n }\n if (key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n if (checkedStates && onCheckedToggle) { if (!node.disabled) onCheckedToggle(node.id); }\n else toggleSelection(node);\n return;\n }\n if (event.key.length !== 1 || event.metaKey || event.ctrlKey || event.altKey) return;\n const now = Date.now();\n const query = now - typeahead.current.at > typeaheadResetMs ? event.key : typeahead.current.query + event.key;\n typeahead.current = { query, at: now };\n const match = getVisibleTreeTypeaheadMatch(visible, query, { startsAfterKey: node.id });\n if (match !== undefined) { event.preventDefault(); focusNode(match); }\n };\n\n return (\n <div\n ref={composeRefs(rootRef, forwardedRef)}\n role=\"tree\"\n aria-label={label}\n aria-busy={asyncState.status === \"loading\" || asyncState.status === \"loadingMore\" || undefined}\n aria-multiselectable={selection?.mode === \"multiple\" || undefined}\n className={classNames(\"hjm-tree\", className)}\n style={{ \"--hjm-tree-indent\": `${treeRecipe.indentPerLevel}px` } as CSSProperties}\n >\n {asyncState.status === \"empty\" || asyncState.status === \"error\" || asyncState.status === \"loading\" ? (\n <p className=\"hjm-tree__state\" role={asyncState.status === \"error\" ? \"alert\" : \"status\"}>{asyncState.message}</p>\n ) : null}\n {visible.map((node) => (\n <div\n key={node.id}\n role=\"treeitem\"\n data-hjm-tree-node={node.id}\n aria-level={node.depth}\n aria-posinset={node.position}\n aria-setsize={node.siblingCount}\n aria-expanded={node.hasChildren ? node.expanded : undefined}\n aria-selected={selection && selection.mode !== \"none\" ? selected.has(node.id) : undefined}\n aria-checked={checkedStates ? (checkedStates.get(node.id) === \"mixed\" ? \"mixed\" : String(checkedStates.get(node.id) === true)) as \"true\" | \"false\" | \"mixed\" : undefined}\n aria-disabled={node.disabled || undefined}\n aria-label={node.accessibleName}\n // One tab stop for the whole tree; the glyph is decorative, not a\n // nested control, so a node is the only focusable thing in a row.\n tabIndex={node.id === activeId ? 0 : -1}\n className=\"hjm-tree__node\"\n style={{ \"--hjm-tree-depth\": node.depth - 1 } as CSSProperties}\n onClick={() => {\n setFocusedId(node.id);\n if (checkedStates && onCheckedToggle) { if (!node.disabled) onCheckedToggle(node.id); }\n else toggleSelection(node);\n }}\n onFocus={() => setFocusedId(node.id)}\n onKeyDown={(event) => onKeyDown(event, node)}\n >\n <span aria-hidden=\"true\" className=\"hjm-tree__indent\" />\n {checkedStates ? (\n <span aria-hidden=\"true\" className=\"hjm-tree__check\" data-state={String(checkedStates.get(node.id) ?? false)}>\n {checkedStates.get(node.id) === true ? \"✓\" : checkedStates.get(node.id) === \"mixed\" ? \"–\" : \"\"}\n </span>\n ) : null}\n <span aria-hidden=\"true\" className=\"hjm-tree__toggle\" data-visible={node.hasChildren || undefined}>\n {node.hasChildren ? renderToggle?.({ expanded: node.expanded }) ?? (node.expanded ? \"▾\" : \"▸\") : null}\n </span>\n <span aria-hidden=\"true\" className=\"hjm-tree__label\">\n {node.label}\n {node.description ? <span className=\"hjm-tree__description\">{node.description}</span> : null}\n </span>\n </div>\n ))}\n {asyncState.status === \"loadingMore\" ? <p className=\"hjm-tree__state\" role=\"status\">{asyncState.message}</p> : null}\n </div>\n );\n}) as <Id extends string = string>(\n props: TreeProps<Id> & { ref?: React.Ref<HTMLDivElement> },\n) => React.ReactElement | null;\n"]}