@hjmds/react 0.8.2

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 (135) hide show
  1. package/README.md +146 -0
  2. package/dist/actions.d.ts +61 -0
  3. package/dist/actions.d.ts.map +1 -0
  4. package/dist/actions.js +54 -0
  5. package/dist/actions.js.map +1 -0
  6. package/dist/advanced-display.d.ts +133 -0
  7. package/dist/advanced-display.d.ts.map +1 -0
  8. package/dist/advanced-display.js +201 -0
  9. package/dist/advanced-display.js.map +1 -0
  10. package/dist/advanced-forms.d.ts +116 -0
  11. package/dist/advanced-forms.d.ts.map +1 -0
  12. package/dist/advanced-forms.js +234 -0
  13. package/dist/advanced-forms.js.map +1 -0
  14. package/dist/advanced-navigation.d.ts +24 -0
  15. package/dist/advanced-navigation.d.ts.map +1 -0
  16. package/dist/advanced-navigation.js +25 -0
  17. package/dist/advanced-navigation.js.map +1 -0
  18. package/dist/bottom-navigation.d.ts +37 -0
  19. package/dist/bottom-navigation.d.ts.map +1 -0
  20. package/dist/bottom-navigation.js +132 -0
  21. package/dist/bottom-navigation.js.map +1 -0
  22. package/dist/date-picker.d.ts +24 -0
  23. package/dist/date-picker.d.ts.map +1 -0
  24. package/dist/date-picker.js +81 -0
  25. package/dist/date-picker.js.map +1 -0
  26. package/dist/display-public.d.ts +5 -0
  27. package/dist/display-public.d.ts.map +1 -0
  28. package/dist/display-public.js +5 -0
  29. package/dist/display-public.js.map +1 -0
  30. package/dist/display.d.ts +77 -0
  31. package/dist/display.d.ts.map +1 -0
  32. package/dist/display.js +51 -0
  33. package/dist/display.js.map +1 -0
  34. package/dist/evidence.d.ts +940 -0
  35. package/dist/evidence.d.ts.map +1 -0
  36. package/dist/evidence.js +98 -0
  37. package/dist/evidence.js.map +1 -0
  38. package/dist/feedback.d.ts +89 -0
  39. package/dist/feedback.d.ts.map +1 -0
  40. package/dist/feedback.js +41 -0
  41. package/dist/feedback.js.map +1 -0
  42. package/dist/file-picker.d.ts +30 -0
  43. package/dist/file-picker.d.ts.map +1 -0
  44. package/dist/file-picker.js +40 -0
  45. package/dist/file-picker.js.map +1 -0
  46. package/dist/forms-public.d.ts +8 -0
  47. package/dist/forms-public.d.ts.map +1 -0
  48. package/dist/forms-public.js +8 -0
  49. package/dist/forms-public.js.map +1 -0
  50. package/dist/forms.d.ts +160 -0
  51. package/dist/forms.d.ts.map +1 -0
  52. package/dist/forms.js +178 -0
  53. package/dist/forms.js.map +1 -0
  54. package/dist/index.d.ts +24 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +24 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/internal.d.ts +17 -0
  59. package/dist/internal.d.ts.map +1 -0
  60. package/dist/internal.js +68 -0
  61. package/dist/internal.js.map +1 -0
  62. package/dist/layout.d.ts +134 -0
  63. package/dist/layout.d.ts.map +1 -0
  64. package/dist/layout.js +167 -0
  65. package/dist/layout.js.map +1 -0
  66. package/dist/navigation-public.d.ts +6 -0
  67. package/dist/navigation-public.d.ts.map +1 -0
  68. package/dist/navigation-public.js +6 -0
  69. package/dist/navigation-public.js.map +1 -0
  70. package/dist/navigation.d.ts +65 -0
  71. package/dist/navigation.d.ts.map +1 -0
  72. package/dist/navigation.js +196 -0
  73. package/dist/navigation.js.map +1 -0
  74. package/dist/number-field.d.ts +55 -0
  75. package/dist/number-field.d.ts.map +1 -0
  76. package/dist/number-field.js +107 -0
  77. package/dist/number-field.js.map +1 -0
  78. package/dist/otp-field.d.ts +2 -0
  79. package/dist/otp-field.d.ts.map +1 -0
  80. package/dist/otp-field.js +2 -0
  81. package/dist/otp-field.js.map +1 -0
  82. package/dist/overlays.d.ts +192 -0
  83. package/dist/overlays.d.ts.map +1 -0
  84. package/dist/overlays.js +949 -0
  85. package/dist/overlays.js.map +1 -0
  86. package/dist/password-field.d.ts +2 -0
  87. package/dist/password-field.d.ts.map +1 -0
  88. package/dist/password-field.js +2 -0
  89. package/dist/password-field.js.map +1 -0
  90. package/dist/portal.d.ts +32 -0
  91. package/dist/portal.d.ts.map +1 -0
  92. package/dist/portal.js +264 -0
  93. package/dist/portal.js.map +1 -0
  94. package/dist/provider.d.ts +38 -0
  95. package/dist/provider.d.ts.map +1 -0
  96. package/dist/provider.js +87 -0
  97. package/dist/provider.js.map +1 -0
  98. package/dist/select.d.ts +72 -0
  99. package/dist/select.d.ts.map +1 -0
  100. package/dist/select.js +353 -0
  101. package/dist/select.js.map +1 -0
  102. package/dist/selection.d.ts +160 -0
  103. package/dist/selection.d.ts.map +1 -0
  104. package/dist/selection.js +225 -0
  105. package/dist/selection.js.map +1 -0
  106. package/dist/slider.d.ts +35 -0
  107. package/dist/slider.d.ts.map +1 -0
  108. package/dist/slider.js +150 -0
  109. package/dist/slider.js.map +1 -0
  110. package/dist/steps.d.ts +29 -0
  111. package/dist/steps.d.ts.map +1 -0
  112. package/dist/steps.js +10 -0
  113. package/dist/steps.js.map +1 -0
  114. package/dist/styles.css +1349 -0
  115. package/dist/supplemental-display.d.ts +59 -0
  116. package/dist/supplemental-display.d.ts.map +1 -0
  117. package/dist/supplemental-display.js +136 -0
  118. package/dist/supplemental-display.js.map +1 -0
  119. package/dist/supplemental-navigation.d.ts +28 -0
  120. package/dist/supplemental-navigation.d.ts.map +1 -0
  121. package/dist/supplemental-navigation.js +106 -0
  122. package/dist/supplemental-navigation.js.map +1 -0
  123. package/dist/theme.d.ts +5 -0
  124. package/dist/theme.d.ts.map +1 -0
  125. package/dist/theme.js +55 -0
  126. package/dist/theme.js.map +1 -0
  127. package/dist/toast.d.ts +41 -0
  128. package/dist/toast.d.ts.map +1 -0
  129. package/dist/toast.js +250 -0
  130. package/dist/toast.js.map +1 -0
  131. package/dist/upload-item.d.ts +18 -0
  132. package/dist/upload-item.d.ts.map +1 -0
  133. package/dist/upload-item.js +19 -0
  134. package/dist/upload-item.js.map +1 -0
  135. package/package.json +151 -0
@@ -0,0 +1,949 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { createAlertDialogSession, getAlertDialogInitialFocus, validateAlertDialogRequest, } from "@hjmds/design-contracts/components/alert-dialog";
3
+ import { createSheetLifecycle, sheetBehaviorDefaults, } from "@hjmds/design-contracts/components/sheet";
4
+ import { resolveTooltipDescriptor, tooltipBehaviorDefaults, } from "@hjmds/design-contracts/components/tooltip";
5
+ import { dialogRecipe, menuRecipe, sheetRecipe, } from "@hjmds/design-contracts/recipes";
6
+ import { cloneElement, forwardRef, useCallback, useEffect, useId, useRef, useState, useSyncExternalStore, } from "react";
7
+ import { createPortal } from "react-dom";
8
+ import { Button } from "./actions.js";
9
+ import { classNames, composeRefs, useControllableState } from "./internal.js";
10
+ import { AnchoredPortal, useAnchoredPopup, } from "./portal.js";
11
+ import { useOptionalHjmTheme, useTooltipCoordinator } from "./provider.js";
12
+ import { createHjmThemeStyle } from "./theme.js";
13
+ function HjmPortal({ children, container }) {
14
+ const [mounted, setMounted] = useState(false);
15
+ const theme = useOptionalHjmTheme();
16
+ useEffect(() => setMounted(true), []);
17
+ if (!mounted)
18
+ return null;
19
+ return createPortal(theme ? (_jsx("div", { className: "hjm-root hjm-portal", "data-hjm-portal": "", "data-motion": theme.environment.reducedMotion ? "reduced" : "full", "data-theme": theme.environment.theme, "data-text-scale": theme.environment.textScale, dir: theme.environment.direction, style: createHjmThemeStyle(theme), children: children })) : children, container ?? document.body);
20
+ }
21
+ function useOpenState({ open, defaultOpen = false, onOpenChange, }) {
22
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
23
+ const controlled = open !== undefined;
24
+ const currentOpen = controlled ? open : internalOpen;
25
+ const pendingRequestRef = useRef(undefined);
26
+ useEffect(() => {
27
+ if (pendingRequestRef.current === currentOpen) {
28
+ pendingRequestRef.current = undefined;
29
+ }
30
+ }, [currentOpen]);
31
+ const changeOpen = useCallback((nextOpen, detail) => {
32
+ if (nextOpen === currentOpen ||
33
+ pendingRequestRef.current === nextOpen)
34
+ return;
35
+ pendingRequestRef.current = nextOpen;
36
+ if (!controlled)
37
+ setInternalOpen(nextOpen);
38
+ onOpenChange?.(nextOpen, detail);
39
+ if (controlled && nextOpen) {
40
+ queueMicrotask(() => {
41
+ if (pendingRequestRef.current === true) {
42
+ pendingRequestRef.current = undefined;
43
+ }
44
+ });
45
+ }
46
+ }, [controlled, currentOpen, onOpenChange]);
47
+ return [currentOpen, changeOpen];
48
+ }
49
+ const focusableSelector = [
50
+ "a[href]",
51
+ "button:not([disabled])",
52
+ "input:not([disabled])",
53
+ "select:not([disabled])",
54
+ "textarea:not([disabled])",
55
+ "[tabindex]:not([tabindex='-1'])",
56
+ ].join(",");
57
+ function getFocusable(container) {
58
+ return [...container.querySelectorAll(focusableSelector)].filter((element) => !element.hidden && element.getAttribute("aria-hidden") !== "true");
59
+ }
60
+ let bodyLockCount = 0;
61
+ let previousBodyOverflow = "";
62
+ const activeModalStack = [];
63
+ let activeModalOrder = 0;
64
+ const isolatedModalBackground = new Map();
65
+ let modalIsolationObserver = null;
66
+ function getOwnedPopupHosts(modal) {
67
+ const ownerId = modal.element.id;
68
+ if (ownerId.length === 0)
69
+ return [];
70
+ return [...document.querySelectorAll("[data-hjm-popup-owner]")]
71
+ .filter((host) => host.getAttribute("data-hjm-popup-owner") === ownerId);
72
+ }
73
+ function modalContainsNode(modal, node) {
74
+ return modal.element.contains(node) ||
75
+ getOwnedPopupHosts(modal).some((host) => host.contains(node));
76
+ }
77
+ function getModalFocusable(modal) {
78
+ return [
79
+ ...getFocusable(modal.element),
80
+ ...getOwnedPopupHosts(modal).flatMap((host) => getFocusable(host)),
81
+ ];
82
+ }
83
+ function getModalLayer(priority) {
84
+ if (!Number.isSafeInteger(priority)) {
85
+ throw new TypeError("modalPriority must be a safe integer");
86
+ }
87
+ return 1000 + priority;
88
+ }
89
+ function getTopModal() {
90
+ let top;
91
+ for (const modal of activeModalStack) {
92
+ if (!modal.element.isConnected)
93
+ continue;
94
+ if (top === undefined ||
95
+ modal.priority > top.priority ||
96
+ (modal.priority === top.priority && modal.order > top.order))
97
+ top = modal;
98
+ }
99
+ return top;
100
+ }
101
+ function modalRanksAbove(candidate, reference) {
102
+ return candidate.priority > reference.priority ||
103
+ (candidate.priority === reference.priority && candidate.order > reference.order);
104
+ }
105
+ function restoreModalBackground() {
106
+ for (const [element, previous] of isolatedModalBackground) {
107
+ element.inert = previous.inert;
108
+ if (previous.ariaHidden === null)
109
+ element.removeAttribute("aria-hidden");
110
+ else
111
+ element.setAttribute("aria-hidden", previous.ariaHidden);
112
+ }
113
+ isolatedModalBackground.clear();
114
+ }
115
+ function isolateModalBackgroundElement(element) {
116
+ if (isolatedModalBackground.has(element))
117
+ return;
118
+ isolatedModalBackground.set(element, {
119
+ ariaHidden: element.getAttribute("aria-hidden"),
120
+ inert: element.inert,
121
+ });
122
+ element.inert = true;
123
+ element.setAttribute("aria-hidden", "true");
124
+ }
125
+ /** Keeps only the top modal's ancestor path interactive, including late portals. */
126
+ function synchronizeModalBackgroundIsolation() {
127
+ restoreModalBackground();
128
+ for (let index = activeModalStack.length - 1; index >= 0; index -= 1) {
129
+ if (!activeModalStack[index]?.element.isConnected)
130
+ activeModalStack.splice(index, 1);
131
+ }
132
+ const top = getTopModal();
133
+ if (!top) {
134
+ modalIsolationObserver?.disconnect();
135
+ modalIsolationObserver = null;
136
+ return;
137
+ }
138
+ const interactivePath = new Set();
139
+ for (const root of [top.element, ...getOwnedPopupHosts(top)]) {
140
+ let pathNode = root;
141
+ while (pathNode) {
142
+ if (pathNode === document.body)
143
+ break;
144
+ interactivePath.add(pathNode);
145
+ pathNode = pathNode.parentElement;
146
+ }
147
+ }
148
+ const inspectedParents = new Set();
149
+ for (const pathNode of interactivePath) {
150
+ const parent = pathNode.parentElement;
151
+ if (!parent || inspectedParents.has(parent))
152
+ continue;
153
+ inspectedParents.add(parent);
154
+ for (const sibling of parent.children) {
155
+ if (sibling instanceof HTMLElement && !interactivePath.has(sibling)) {
156
+ isolateModalBackgroundElement(sibling);
157
+ }
158
+ }
159
+ }
160
+ if (modalIsolationObserver === null) {
161
+ modalIsolationObserver = new MutationObserver(() => {
162
+ synchronizeModalBackgroundIsolation();
163
+ });
164
+ modalIsolationObserver.observe(document.body, {
165
+ attributes: true,
166
+ attributeFilter: ["data-hjm-popup-owner"],
167
+ childList: true,
168
+ subtree: true,
169
+ });
170
+ }
171
+ }
172
+ function lockBodyScroll() {
173
+ if (bodyLockCount === 0) {
174
+ previousBodyOverflow = document.body.style.overflow;
175
+ document.body.style.overflow = "hidden";
176
+ }
177
+ bodyLockCount += 1;
178
+ return () => {
179
+ bodyLockCount = Math.max(0, bodyLockCount - 1);
180
+ if (bodyLockCount === 0)
181
+ document.body.style.overflow = previousBodyOverflow;
182
+ };
183
+ }
184
+ function useModalFocus({ active, priority = 0, contentRef, initialFocusRef, returnFocusRef, fallbackReturnRef, onEscape, }) {
185
+ const escapeRef = useRef(onEscape);
186
+ escapeRef.current = onEscape;
187
+ useEffect(() => {
188
+ if (!active)
189
+ return;
190
+ let retryTimer;
191
+ let release;
192
+ let cancelled = false;
193
+ const activate = () => {
194
+ const content = contentRef.current;
195
+ if (!content) {
196
+ retryTimer = setTimeout(activate, 0);
197
+ return;
198
+ }
199
+ const previouslyFocused = document.activeElement instanceof HTMLElement
200
+ ? document.activeElement
201
+ : null;
202
+ const releaseScroll = lockBodyScroll();
203
+ const modal = {
204
+ element: content,
205
+ order: activeModalOrder += 1,
206
+ priority,
207
+ };
208
+ activeModalStack.push(modal);
209
+ synchronizeModalBackgroundIsolation();
210
+ const initial = initialFocusRef?.current ?? getFocusable(content)[0] ?? content;
211
+ if (getTopModal() === modal)
212
+ initial.focus();
213
+ const handleKeyDown = (event) => {
214
+ if (getTopModal() !== modal)
215
+ return;
216
+ if (event.key === "Escape") {
217
+ event.preventDefault();
218
+ escapeRef.current();
219
+ return;
220
+ }
221
+ if (event.key !== "Tab")
222
+ return;
223
+ const focusable = getModalFocusable(modal);
224
+ if (focusable.length === 0) {
225
+ event.preventDefault();
226
+ content.focus();
227
+ return;
228
+ }
229
+ const first = focusable[0];
230
+ const last = focusable.at(-1);
231
+ const current = document.activeElement;
232
+ if (event.shiftKey && (current === first || !(current instanceof Node) || !modalContainsNode(modal, current))) {
233
+ event.preventDefault();
234
+ last.focus();
235
+ }
236
+ else if (!event.shiftKey && (current === last || !(current instanceof Node) || !modalContainsNode(modal, current))) {
237
+ event.preventDefault();
238
+ first.focus();
239
+ }
240
+ };
241
+ const handleFocusIn = (event) => {
242
+ if (getTopModal() !== modal)
243
+ return;
244
+ if (event.target instanceof Node && !modalContainsNode(modal, event.target)) {
245
+ (initialFocusRef?.current ?? getFocusable(content)[0] ?? content).focus();
246
+ }
247
+ };
248
+ document.addEventListener("keydown", handleKeyDown, true);
249
+ document.addEventListener("focusin", handleFocusIn, true);
250
+ release = () => {
251
+ const wasTop = !activeModalStack.some((candidate) => candidate !== modal && candidate.element.isConnected && modalRanksAbove(candidate, modal));
252
+ document.removeEventListener("keydown", handleKeyDown, true);
253
+ document.removeEventListener("focusin", handleFocusIn, true);
254
+ const stackIndex = activeModalStack.lastIndexOf(modal);
255
+ if (stackIndex >= 0)
256
+ activeModalStack.splice(stackIndex, 1);
257
+ synchronizeModalBackgroundIsolation();
258
+ releaseScroll();
259
+ const returnTarget = returnFocusRef?.current ?? fallbackReturnRef?.current ?? previouslyFocused;
260
+ if (wasTop) {
261
+ queueMicrotask(() => {
262
+ const nextTop = getTopModal();
263
+ if (!nextTop) {
264
+ returnTarget?.focus();
265
+ return;
266
+ }
267
+ if (returnTarget && modalContainsNode(nextTop, returnTarget))
268
+ returnTarget.focus();
269
+ else
270
+ (getModalFocusable(nextTop)[0] ?? nextTop.element).focus();
271
+ });
272
+ }
273
+ };
274
+ if (cancelled)
275
+ release();
276
+ };
277
+ activate();
278
+ return () => {
279
+ cancelled = true;
280
+ if (retryTimer !== undefined)
281
+ clearTimeout(retryTimer);
282
+ release?.();
283
+ };
284
+ }, [active, contentRef, fallbackReturnRef, initialFocusRef, priority, returnFocusRef]);
285
+ }
286
+ function renderTrigger(trigger, triggerRef, open, contentId, popup, onOpen) {
287
+ const props = trigger.props;
288
+ return cloneElement(trigger, {
289
+ ref: composeRefs(props.ref, triggerRef),
290
+ "aria-controls": contentId,
291
+ "aria-expanded": open,
292
+ "aria-haspopup": popup,
293
+ onClick: (event) => {
294
+ props.onClick?.(event);
295
+ if (!event.defaultPrevented && !props.disabled)
296
+ onOpen();
297
+ },
298
+ });
299
+ }
300
+ export const Dialog = forwardRef(function Dialog({ trigger, title, description, children, footer, size = dialogRecipe.defaults.size, dismissible = dialogRecipe.defaults.dismissible, busy = false, closeLabel, initialFocusRef, returnFocusRef, modalPriority = 0, portalContainer, open: openProp, defaultOpen, onOpenChange, className, }, forwardedRef) {
301
+ const modalLayer = getModalLayer(modalPriority);
302
+ const [open, changeOpen] = useOpenState({
303
+ ...(openProp === undefined ? {} : { open: openProp }),
304
+ ...(defaultOpen === undefined ? {} : { defaultOpen }),
305
+ ...(onOpenChange === undefined ? {} : { onOpenChange }),
306
+ });
307
+ const triggerRef = useRef(null);
308
+ const contentRef = useRef(null);
309
+ const mergedContentRef = composeRefs(contentRef, forwardedRef);
310
+ const id = useId().replaceAll(":", "");
311
+ const contentId = `${id}-dialog`;
312
+ const titleId = `${id}-title`;
313
+ const descriptionId = `${id}-description`;
314
+ const requestClose = (reason) => {
315
+ if (!dismissible || busy)
316
+ return;
317
+ changeOpen(false, { reason });
318
+ };
319
+ useModalFocus({
320
+ active: open,
321
+ priority: modalPriority,
322
+ contentRef,
323
+ ...(initialFocusRef === undefined ? {} : { initialFocusRef }),
324
+ ...(returnFocusRef === undefined ? {} : { returnFocusRef }),
325
+ ...(trigger === undefined ? {} : { fallbackReturnRef: triggerRef }),
326
+ onEscape: () => requestClose("escape"),
327
+ });
328
+ return (_jsxs(_Fragment, { children: [trigger === undefined ? null : renderTrigger(trigger, triggerRef, open, contentId, "dialog", () => changeOpen(true, { reason: "trigger" })), open ? (_jsx(HjmPortal, { ...(portalContainer === undefined ? {} : { container: portalContainer }), children: _jsx("div", { className: "hjm-overlay", "data-kind": "dialog", "data-modal-priority": modalPriority, "data-state": "open", style: { zIndex: modalLayer }, onMouseDown: (event) => {
329
+ if (event.target === event.currentTarget)
330
+ requestClose("outside");
331
+ }, children: _jsxs("div", { ref: mergedContentRef, id: contentId, role: "dialog", "aria-modal": "true", "aria-labelledby": titleId, "aria-describedby": description ? descriptionId : undefined, "aria-busy": busy || undefined, tabIndex: -1, className: classNames("hjm-dialog", className), "data-hjm-modal-content": "", "data-size": size, "data-state": busy ? "busy" : "idle", children: [_jsxs("header", { className: "hjm-dialog__header", children: [_jsx("h2", { id: titleId, className: "hjm-dialog__title", children: title }), dismissible ? (_jsx("button", { type: "button", className: "hjm-dialog__close", "aria-label": closeLabel, disabled: busy, onClick: () => requestClose("close-action"), children: "\u00D7" })) : null] }), description ? _jsx("p", { id: descriptionId, className: "hjm-dialog__description", children: description }) : null, children ? _jsx("div", { className: "hjm-dialog__body", children: children }) : null, footer ? _jsx("footer", { className: "hjm-dialog__footer", children: footer }) : null] }) }) })) : null] }));
332
+ });
333
+ export const AlertDialog = forwardRef(function AlertDialog({ trigger, request, icon, size = dialogRecipe.defaults.size, returnFocusRef, modalPriority = 0, portalContainer, open: openProp, defaultOpen, onOpenChange, className, }, forwardedRef) {
334
+ const modalLayer = getModalLayer(modalPriority);
335
+ validateAlertDialogRequest(request);
336
+ const [open, changeOpen] = useOpenState({
337
+ ...(openProp === undefined ? {} : { open: openProp }),
338
+ ...(defaultOpen === undefined ? {} : { defaultOpen }),
339
+ ...(onOpenChange === undefined ? {} : { onOpenChange }),
340
+ });
341
+ const sessionRef = useRef(null);
342
+ const [sessionGeneration, setSessionGeneration] = useState(0);
343
+ if (open && sessionRef.current === null) {
344
+ sessionRef.current = createAlertDialogSession(request);
345
+ }
346
+ const idleSnapshot = useRef({ status: "idle" });
347
+ const phase = useSyncExternalStore((listener) => sessionRef.current?.subscribe(listener) ?? (() => undefined), () => sessionRef.current?.getSnapshot() ?? idleSnapshot.current, () => idleSnapshot.current);
348
+ const busy = phase.status === "busy";
349
+ const closing = phase.status === "closing" || phase.status === "closed";
350
+ const error = phase.status === "error" ? phase.message : undefined;
351
+ const triggerRef = useRef(null);
352
+ const contentRef = useRef(null);
353
+ const cancelRef = useRef(null);
354
+ const confirmRef = useRef(null);
355
+ const initialFocusRef = getAlertDialogInitialFocus(request.mode) === "cancel"
356
+ ? cancelRef
357
+ : confirmRef;
358
+ const id = useId().replaceAll(":", "");
359
+ const contentId = `${id}-alert-dialog`;
360
+ const titleId = `${id}-title`;
361
+ const descriptionId = `${id}-description`;
362
+ const errorId = `${id}-error`;
363
+ const startSession = () => {
364
+ sessionRef.current?.interrupt();
365
+ const session = createAlertDialogSession(request);
366
+ sessionRef.current = session;
367
+ setSessionGeneration((current) => current + 1);
368
+ return session;
369
+ };
370
+ const cancel = (reason) => {
371
+ const session = sessionRef.current;
372
+ if (!session?.cancel(reason))
373
+ return;
374
+ changeOpen(false, { reason });
375
+ };
376
+ useModalFocus({
377
+ active: open,
378
+ priority: modalPriority,
379
+ contentRef,
380
+ initialFocusRef,
381
+ ...(returnFocusRef === undefined ? {} : { returnFocusRef }),
382
+ ...(trigger === undefined ? {} : { fallbackReturnRef: triggerRef }),
383
+ onEscape: () => cancel("escape"),
384
+ });
385
+ const confirm = async () => {
386
+ const session = sessionRef.current;
387
+ if (!session)
388
+ return;
389
+ if (await session.confirm())
390
+ changeOpen(false, { reason: "confirm" });
391
+ };
392
+ useEffect(() => {
393
+ if (open)
394
+ return;
395
+ const session = sessionRef.current;
396
+ if (!session)
397
+ return;
398
+ const snapshot = session.getSnapshot();
399
+ if (snapshot.status === "idle" || snapshot.status === "error") {
400
+ session.cancel("programmatic");
401
+ }
402
+ if (session.getSnapshot().status === "closing")
403
+ session.completeExit();
404
+ else if (session.getSnapshot().status !== "closed")
405
+ session.interrupt();
406
+ sessionRef.current = null;
407
+ }, [open, sessionGeneration]);
408
+ useEffect(() => () => {
409
+ sessionRef.current?.interrupt();
410
+ }, []);
411
+ const tone = request.tone ?? "attention";
412
+ return (_jsxs(_Fragment, { children: [trigger === undefined ? null : renderTrigger(trigger, triggerRef, open, contentId, "dialog", () => {
413
+ startSession();
414
+ changeOpen(true, { reason: "trigger" });
415
+ }), open ? (_jsx(HjmPortal, { ...(portalContainer === undefined ? {} : { container: portalContainer }), children: _jsx("div", { className: "hjm-overlay", "data-kind": "alert-dialog", "data-modal-priority": modalPriority, "data-state": "open", style: { zIndex: modalLayer }, children: _jsxs("div", { ref: composeRefs(contentRef, forwardedRef), id: contentId, role: "alertdialog", "aria-modal": "true", "aria-labelledby": titleId, "aria-describedby": `${descriptionId}${error ? ` ${errorId}` : ""}`, "aria-busy": busy || undefined, tabIndex: -1, className: classNames("hjm-alert-dialog", className), "data-hjm-modal-content": "", "data-size": size, "data-tone": tone, "data-state": busy ? "busy" : error ? "error" : "idle", children: [icon ? _jsx("div", { className: "hjm-alert-dialog__icon", "aria-hidden": "true", children: icon }) : null, _jsx("h2", { id: titleId, className: "hjm-alert-dialog__title", children: request.title }), _jsx("p", { id: descriptionId, className: "hjm-alert-dialog__description", children: request.description }), error ? _jsx("p", { id: errorId, className: "hjm-alert-dialog__error", role: "alert", children: error }) : null, _jsxs("div", { className: "hjm-alert-dialog__actions", children: [request.mode === "confirm" ? (_jsx(Button, { ref: cancelRef, tone: "secondary", disabled: busy || closing, onClick: () => cancel("cancel-action"), children: request.cancelLabel })) : null, _jsx(Button, { ref: confirmRef, tone: tone === "danger" ? "danger" : "primary", loading: busy, disabled: closing, onClick: () => void confirm(), children: request.confirmLabel })] })] }) }) })) : null] }));
416
+ });
417
+ export const Sheet = forwardRef(function Sheet({ trigger, title, description, children, footer, placement = sheetRecipe.defaults.placement, busy = false, dismissPolicy, closeLabel, initialFocusRef, returnFocusRef, onDismissComplete, modalPriority = 0, portalContainer, open: openProp, defaultOpen, onOpenChange, className, }, forwardedRef) {
418
+ const modalLayer = getModalLayer(modalPriority);
419
+ const [open, changeOpen] = useOpenState({
420
+ ...(openProp === undefined ? {} : { open: openProp }),
421
+ ...(defaultOpen === undefined ? {} : { defaultOpen }),
422
+ ...(onOpenChange === undefined ? {} : { onOpenChange }),
423
+ });
424
+ const policy = { ...sheetBehaviorDefaults, ...dismissPolicy };
425
+ const lifecycleRef = useRef(createSheetLifecycle(open));
426
+ const previousOpenRef = useRef(open);
427
+ const currentOpenRef = useRef(open);
428
+ currentOpenRef.current = open;
429
+ const dismissReasonRef = useRef(undefined);
430
+ const dismissCompleteRef = useRef(onDismissComplete);
431
+ dismissCompleteRef.current = onDismissComplete;
432
+ const settleDismissRef = useRef(() => undefined);
433
+ settleDismissRef.current = (reason) => {
434
+ const cycle = lifecycleRef.current.beginDismiss();
435
+ if (cycle !== null && lifecycleRef.current.completeDismiss(cycle)) {
436
+ dismissReasonRef.current = undefined;
437
+ previousOpenRef.current = false;
438
+ dismissCompleteRef.current?.({ reason });
439
+ }
440
+ };
441
+ const triggerRef = useRef(null);
442
+ const contentRef = useRef(null);
443
+ const id = useId().replaceAll(":", "");
444
+ const contentId = `${id}-sheet`;
445
+ const titleId = `${id}-title`;
446
+ const descriptionId = `${id}-description`;
447
+ const requestClose = (reason) => {
448
+ if (lifecycleRef.current.requestClose(reason, busy, policy)) {
449
+ dismissReasonRef.current = reason;
450
+ changeOpen(false, { reason });
451
+ }
452
+ };
453
+ useEffect(() => {
454
+ if (open) {
455
+ lifecycleRef.current.open();
456
+ previousOpenRef.current = true;
457
+ dismissReasonRef.current = undefined;
458
+ return;
459
+ }
460
+ if (!previousOpenRef.current)
461
+ return;
462
+ settleDismissRef.current(dismissReasonRef.current ?? "programmatic");
463
+ }, [open]);
464
+ const mountEpochRef = useRef(0);
465
+ useEffect(() => {
466
+ const epoch = mountEpochRef.current + 1;
467
+ mountEpochRef.current = epoch;
468
+ return () => {
469
+ queueMicrotask(() => {
470
+ // React StrictMode immediately runs the setup again after its probe
471
+ // cleanup. A real unmount has no later epoch and is settled here,
472
+ // after the portal and focus-lock cleanups have completed.
473
+ if (mountEpochRef.current !== epoch || !currentOpenRef.current)
474
+ return;
475
+ settleDismissRef.current(dismissReasonRef.current ?? "programmatic");
476
+ });
477
+ };
478
+ }, []);
479
+ useModalFocus({
480
+ active: open,
481
+ priority: modalPriority,
482
+ contentRef,
483
+ ...(initialFocusRef === undefined ? {} : { initialFocusRef }),
484
+ ...(returnFocusRef === undefined ? {} : { returnFocusRef }),
485
+ ...(trigger === undefined ? {} : { fallbackReturnRef: triggerRef }),
486
+ onEscape: () => requestClose("escape"),
487
+ });
488
+ return (_jsxs(_Fragment, { children: [trigger === undefined ? null : renderTrigger(trigger, triggerRef, open, contentId, "dialog", () => changeOpen(true, { reason: "trigger" })), open ? (_jsx(HjmPortal, { ...(portalContainer === undefined ? {} : { container: portalContainer }), children: _jsx("div", { className: "hjm-overlay hjm-sheet-positioner", "data-kind": "sheet", "data-modal-priority": modalPriority, "data-placement": placement, "data-state": "open", style: { zIndex: modalLayer }, onMouseDown: (event) => {
489
+ if (event.target === event.currentTarget)
490
+ requestClose("outside");
491
+ }, children: _jsxs("div", { ref: composeRefs(contentRef, forwardedRef), id: contentId, role: "dialog", "aria-modal": "true", "aria-labelledby": titleId, "aria-describedby": description ? descriptionId : undefined, "aria-busy": busy || undefined, tabIndex: -1, className: classNames("hjm-sheet", className), "data-hjm-modal-content": "", "data-placement": placement, "data-has-footer": footer ? true : undefined, "data-state": busy ? "busy" : "idle", children: [_jsxs("header", { className: "hjm-sheet__header", children: [_jsxs("div", { children: [_jsx("h2", { id: titleId, className: "hjm-sheet__title", children: title }), description ? _jsx("p", { id: descriptionId, className: "hjm-sheet__description", children: description }) : null] }), policy.dismissible ? (_jsx("button", { type: "button", className: "hjm-dialog__close", "aria-label": closeLabel, disabled: busy && !policy.dismissWhileBusy, onClick: () => requestClose("close-action"), children: "\u00D7" })) : null] }), children ? _jsx("div", { className: "hjm-sheet__body", children: children }) : null, footer ? _jsx("footer", { className: "hjm-sheet__footer", children: footer }) : null] }) }) })) : null] }));
492
+ });
493
+ export const Tooltip = forwardRef(function Tooltip({ trigger, content, placement, align, pointerOpenDelayMs = tooltipBehaviorDefaults.pointerOpenDelayMs, focusOpenDelayMs = tooltipBehaviorDefaults.focusOpenDelayMs, portalContainer, open: openProp, defaultOpen, onOpenChange, className, }, ref) {
494
+ const descriptor = resolveTooltipDescriptor({
495
+ content,
496
+ ...(placement === undefined ? {} : { placement }),
497
+ ...(align === undefined ? {} : { align }),
498
+ });
499
+ const [open, changeOpen] = useOpenState({
500
+ ...(openProp === undefined ? {} : { open: openProp }),
501
+ ...(defaultOpen === undefined ? {} : { defaultOpen }),
502
+ ...(onOpenChange === undefined ? {} : { onOpenChange }),
503
+ });
504
+ const timerRef = useRef(undefined);
505
+ const suppressedRef = useRef(false);
506
+ const triggerRef = useRef(null);
507
+ const [tooltipNode, setTooltipNode] = useState(null);
508
+ const id = `${useId().replaceAll(":", "")}-tooltip`;
509
+ const coordinator = useTooltipCoordinator();
510
+ const activeTooltipId = coordinator?.activeId ?? null;
511
+ const activateTooltip = coordinator?.activate;
512
+ const deactivateTooltip = coordinator?.deactivate;
513
+ const shouldSkipTooltipDelay = coordinator?.shouldSkipDelay;
514
+ const visible = open &&
515
+ (coordinator === null ||
516
+ activeTooltipId === null ||
517
+ activeTooltipId === id);
518
+ const clearTimer = () => {
519
+ if (timerRef.current !== undefined)
520
+ clearTimeout(timerRef.current);
521
+ timerRef.current = undefined;
522
+ };
523
+ useEffect(() => clearTimer, []);
524
+ const schedule = (nextOpen, delay, detail) => {
525
+ clearTimer();
526
+ if (nextOpen && suppressedRef.current)
527
+ return;
528
+ timerRef.current = setTimeout(() => {
529
+ if (nextOpen)
530
+ activateTooltip?.(id);
531
+ changeOpen(nextOpen, detail);
532
+ }, delay);
533
+ };
534
+ useEffect(() => {
535
+ if (open)
536
+ activateTooltip?.(id);
537
+ else
538
+ deactivateTooltip?.(id);
539
+ }, [activateTooltip, deactivateTooltip, id, open]);
540
+ useEffect(() => {
541
+ if (open &&
542
+ activeTooltipId !== null &&
543
+ activeTooltipId !== id) {
544
+ clearTimer();
545
+ changeOpen(false, { reason: "another-tooltip" });
546
+ }
547
+ }, [activeTooltipId, changeOpen, id, open]);
548
+ useEffect(() => () => deactivateTooltip?.(id), [deactivateTooltip, id]);
549
+ const triggerProps = trigger.props;
550
+ const describedBy = visible
551
+ ? [triggerProps["aria-describedby"], id].filter(Boolean).join(" ")
552
+ : triggerProps["aria-describedby"];
553
+ const renderedTrigger = cloneElement(trigger, {
554
+ ref: composeRefs(triggerProps.ref, triggerRef),
555
+ ...(describedBy ? { "aria-describedby": describedBy } : {}),
556
+ onPointerEnter: (event) => {
557
+ triggerProps.onPointerEnter?.(event);
558
+ if (event.pointerType === "touch")
559
+ return;
560
+ schedule(true, shouldSkipTooltipDelay?.() ? 0 : pointerOpenDelayMs, { reason: "pointer" });
561
+ },
562
+ onPointerLeave: (event) => {
563
+ triggerProps.onPointerLeave?.(event);
564
+ if (event.pointerType === "touch")
565
+ return;
566
+ suppressedRef.current = false;
567
+ schedule(false, 80, { reason: "pointer-leave" });
568
+ },
569
+ onFocus: (event) => {
570
+ triggerProps.onFocus?.(event);
571
+ schedule(true, focusOpenDelayMs, { reason: "focus" });
572
+ },
573
+ onBlur: (event) => {
574
+ triggerProps.onBlur?.(event);
575
+ suppressedRef.current = false;
576
+ schedule(false, 0, { reason: "blur" });
577
+ },
578
+ onKeyDown: (event) => {
579
+ triggerProps.onKeyDown?.(event);
580
+ if (event.key === "Escape" && visible) {
581
+ event.preventDefault();
582
+ clearTimer();
583
+ suppressedRef.current = true;
584
+ changeOpen(false, { reason: "escape" });
585
+ }
586
+ },
587
+ onClick: (event) => {
588
+ triggerProps.onClick?.(event);
589
+ if (visible && !event.defaultPrevented) {
590
+ changeOpen(false, { reason: "trigger-activation" });
591
+ }
592
+ },
593
+ });
594
+ const popupPosition = useAnchoredPopup(triggerRef, tooltipNode, {
595
+ align: descriptor.align,
596
+ placement: descriptor.placement,
597
+ zIndex: 1100,
598
+ });
599
+ const setTooltipRef = useCallback((node) => {
600
+ setTooltipNode(node);
601
+ }, []);
602
+ return (_jsxs("span", { ref: ref, className: classNames("hjm-tooltip", className), "data-placement": descriptor.placement, "data-align": descriptor.align, "data-state": visible ? "open" : "closed", onPointerEnter: (event) => {
603
+ if (event.pointerType !== "touch")
604
+ clearTimer();
605
+ }, onPointerLeave: (event) => {
606
+ if (event.pointerType === "touch")
607
+ return;
608
+ suppressedRef.current = false;
609
+ schedule(false, 0, { reason: "pointer-leave" });
610
+ }, children: [renderedTrigger, visible ? (_jsx(AnchoredPortal, { anchorRef: triggerRef, ssrFallback: "inline", ...(portalContainer === undefined ? {} : { container: portalContainer }), children: _jsx("span", { ref: setTooltipRef, id: id, role: "tooltip", className: "hjm-tooltip__content", "data-placement": popupPosition.placement, "data-align": popupPosition.align, style: popupPosition.style, onPointerEnter: (event) => {
611
+ if (event.pointerType !== "touch")
612
+ clearTimer();
613
+ }, onPointerLeave: (event) => {
614
+ if (event.pointerType === "touch")
615
+ return;
616
+ suppressedRef.current = false;
617
+ schedule(false, 0, { reason: "pointer-leave" });
618
+ }, children: descriptor.content }) })) : null] }));
619
+ });
620
+ function menuTextValue(item) {
621
+ const value = item.textValue ?? (typeof item.label === "string" ? item.label : undefined);
622
+ if (value === undefined || value.trim().length === 0) {
623
+ throw new TypeError(`Menu item ${item.id} needs textValue for typeahead`);
624
+ }
625
+ return value.trim();
626
+ }
627
+ function validateMenuItems(items, sections, asyncState) {
628
+ if (items.length === 0 && (asyncState.status === "idle" || asyncState.status === "loadingMore")) {
629
+ throw new TypeError("Menu requires at least one item");
630
+ }
631
+ const ids = new Set();
632
+ for (const item of items) {
633
+ if (item.id.trim().length === 0)
634
+ throw new TypeError("Menu item id must not be empty");
635
+ if (ids.has(item.id))
636
+ throw new TypeError(`Duplicate Menu item id: ${item.id}`);
637
+ ids.add(item.id);
638
+ menuTextValue(item);
639
+ }
640
+ if (sections !== undefined) {
641
+ const sectionIds = new Set();
642
+ for (const section of sections) {
643
+ if (section.id.trim().length === 0)
644
+ throw new TypeError("Menu section id must not be empty");
645
+ if (sectionIds.has(section.id))
646
+ throw new TypeError(`Duplicate Menu section id: ${section.id}`);
647
+ sectionIds.add(section.id);
648
+ if (section.items.length === 0)
649
+ throw new TypeError(`Menu section ${section.id} must not be empty`);
650
+ if ((section.label?.trim().length ?? 0) === 0 &&
651
+ (section.accessibilityLabel?.trim().length ?? 0) === 0) {
652
+ throw new TypeError(`Menu section ${section.id} needs a label or accessibilityLabel`);
653
+ }
654
+ }
655
+ }
656
+ if ((asyncState.status === "idle" || asyncState.status === "loadingMore") &&
657
+ !items.some((item) => !item.disabled)) {
658
+ throw new TypeError("Menu requires at least one enabled item");
659
+ }
660
+ }
661
+ export const Menu = forwardRef(function Menu(props, ref) {
662
+ const { trigger, label, density = menuRecipe.defaults.density, align = "start", disabled = false, asyncState = { status: "idle" }, onAction, onActionAfterDismiss, portalContainer, open: openProp, defaultOpen, onOpenChange, className, } = props;
663
+ const sections = props.sections;
664
+ const items = sections === undefined ? props.items : sections.flatMap((section) => section.items);
665
+ if (label.trim().length === 0)
666
+ throw new TypeError("Menu label must not be empty");
667
+ validateMenuItems(items, sections, asyncState);
668
+ const selectionMode = props.selectionMode ?? "action";
669
+ const [open, changeOpen] = useOpenState({
670
+ ...(openProp === undefined ? {} : { open: openProp }),
671
+ ...(defaultOpen === undefined ? {} : { defaultOpen }),
672
+ ...(onOpenChange === undefined ? {} : { onOpenChange }),
673
+ });
674
+ const [focusIndex, setFocusIndex] = useState(() => items.findIndex((item) => !item.disabled));
675
+ const singleSelection = selectionMode === "single"
676
+ ? props
677
+ : undefined;
678
+ const multipleSelection = selectionMode === "multiple"
679
+ ? props
680
+ : undefined;
681
+ const [singleValue, setSingleValue] = useControllableState({
682
+ ...(singleSelection?.value !== undefined
683
+ ? { value: singleSelection.value }
684
+ : {}),
685
+ defaultValue: singleSelection?.defaultValue ?? null,
686
+ ...(singleSelection?.onValueChange !== undefined
687
+ ? {
688
+ onChange: (next) => {
689
+ if (next !== null)
690
+ singleSelection.onValueChange?.(next);
691
+ },
692
+ }
693
+ : {}),
694
+ });
695
+ const [multipleValue, setMultipleValue] = useControllableState({
696
+ ...(multipleSelection?.value !== undefined
697
+ ? { value: multipleSelection.value }
698
+ : {}),
699
+ defaultValue: multipleSelection?.defaultValue ?? new Set(),
700
+ ...(multipleSelection?.onValueChange !== undefined
701
+ ? { onChange: multipleSelection.onValueChange }
702
+ : {}),
703
+ });
704
+ const knownIds = new Set(items.map((item) => item.id));
705
+ if (selectionMode === "single" && singleValue !== null && !knownIds.has(singleValue)) {
706
+ throw new RangeError(`Unknown Menu single value: ${singleValue}`);
707
+ }
708
+ if (selectionMode === "multiple") {
709
+ for (const selectedId of multipleValue) {
710
+ if (!knownIds.has(selectedId))
711
+ throw new RangeError(`Unknown Menu multiple value: ${selectedId}`);
712
+ }
713
+ }
714
+ const triggerRef = useRef(null);
715
+ const itemRefs = useRef(new Map());
716
+ const wrapperRef = useRef(null);
717
+ const contentRef = useRef(null);
718
+ const [contentNode, setContentNode] = useState(null);
719
+ const restoreFocusRef = useRef(false);
720
+ const afterDismissIdRef = useRef(undefined);
721
+ const typeaheadRef = useRef({ value: "", time: 0 });
722
+ const id = `${useId().replaceAll(":", "")}-menu`;
723
+ const popupPosition = useAnchoredPopup(triggerRef, contentNode, { align, zIndex: 900 });
724
+ const setMenuContentRef = useCallback((node) => {
725
+ contentRef.current = node;
726
+ setContentNode(node);
727
+ if (typeof ref === "function")
728
+ ref(node);
729
+ else if (ref)
730
+ ref.current = node;
731
+ }, [ref]);
732
+ useEffect(() => {
733
+ if (!open) {
734
+ if (restoreFocusRef.current) {
735
+ restoreFocusRef.current = false;
736
+ queueMicrotask(() => triggerRef.current?.focus());
737
+ }
738
+ const completedId = afterDismissIdRef.current;
739
+ if (completedId !== undefined) {
740
+ afterDismissIdRef.current = undefined;
741
+ queueMicrotask(() => onActionAfterDismiss?.(completedId));
742
+ }
743
+ return;
744
+ }
745
+ const item = itemRefs.current.get(focusIndex);
746
+ if (item && !item.disabled)
747
+ item.focus();
748
+ else
749
+ contentRef.current?.focus();
750
+ }, [contentNode, focusIndex, onActionAfterDismiss, open]);
751
+ const itemIsDisabled = (item) => disabled || asyncState.status === "loading" || Boolean(item.disabled);
752
+ const firstEnabled = () => items.findIndex((item) => !itemIsDisabled(item));
753
+ const lastEnabled = () => {
754
+ for (let index = items.length - 1; index >= 0; index -= 1) {
755
+ if (items[index] && !itemIsDisabled(items[index]))
756
+ return index;
757
+ }
758
+ return firstEnabled();
759
+ };
760
+ const focusAt = (index) => {
761
+ setFocusIndex(index);
762
+ queueMicrotask(() => itemRefs.current.get(index)?.focus());
763
+ };
764
+ const moveFocus = (step) => {
765
+ for (let offset = 1; offset <= items.length; offset += 1) {
766
+ const next = (focusIndex + step * offset + items.length) % items.length;
767
+ if (items[next] && !itemIsDisabled(items[next])) {
768
+ focusAt(next);
769
+ return;
770
+ }
771
+ }
772
+ };
773
+ const close = (reason, restore) => {
774
+ if (restore)
775
+ restoreFocusRef.current = true;
776
+ changeOpen(false, { reason });
777
+ };
778
+ const openMenu = (focus = "first") => {
779
+ if (disabled)
780
+ return;
781
+ const index = focus === "first" ? firstEnabled() : lastEnabled();
782
+ setFocusIndex(index);
783
+ changeOpen(true, { reason: "trigger" });
784
+ };
785
+ const itemSelected = (id) => selectionMode === "single"
786
+ ? singleValue === id
787
+ : selectionMode === "multiple"
788
+ ? multipleValue.has(id)
789
+ : false;
790
+ const activateItem = (item, index) => {
791
+ if (itemIsDisabled(item))
792
+ return;
793
+ item.onSelect?.();
794
+ onAction?.(item.id);
795
+ if (selectionMode === "multiple") {
796
+ const next = new Set(multipleValue);
797
+ if (next.has(item.id))
798
+ next.delete(item.id);
799
+ else
800
+ next.add(item.id);
801
+ setMultipleValue(next);
802
+ focusAt(index);
803
+ return;
804
+ }
805
+ if (selectionMode === "single")
806
+ setSingleValue(item.id);
807
+ afterDismissIdRef.current = item.id;
808
+ close("selection", true);
809
+ };
810
+ const runTypeahead = (key) => {
811
+ const currentTime = Date.now();
812
+ const previous = currentTime - typeaheadRef.current.time < 500
813
+ ? typeaheadRef.current.value
814
+ : "";
815
+ const combined = `${previous}${key}`.toLocaleLowerCase();
816
+ const search = new Set(combined).size === 1 ? key.toLocaleLowerCase() : combined;
817
+ typeaheadRef.current = { value: combined, time: currentTime };
818
+ for (let offset = 1; offset <= items.length; offset += 1) {
819
+ const index = (focusIndex + offset + items.length) % items.length;
820
+ const item = items[index];
821
+ if (item &&
822
+ !itemIsDisabled(item) &&
823
+ menuTextValue(item).toLocaleLowerCase().startsWith(search)) {
824
+ focusAt(index);
825
+ return;
826
+ }
827
+ }
828
+ };
829
+ useEffect(() => {
830
+ if (!open)
831
+ return;
832
+ const handlePointerDown = (event) => {
833
+ if (event.target instanceof Node &&
834
+ !wrapperRef.current?.contains(event.target) &&
835
+ !contentRef.current?.contains(event.target)) {
836
+ close("outside", false);
837
+ }
838
+ };
839
+ document.addEventListener("pointerdown", handlePointerDown);
840
+ return () => document.removeEventListener("pointerdown", handlePointerDown);
841
+ });
842
+ const triggerProps = trigger.props;
843
+ const renderedTrigger = cloneElement(trigger, {
844
+ ref: composeRefs(triggerProps.ref, triggerRef),
845
+ "aria-controls": id,
846
+ "aria-expanded": open,
847
+ "aria-haspopup": "menu",
848
+ "aria-disabled": disabled || undefined,
849
+ disabled: triggerProps.disabled || disabled,
850
+ onClick: (event) => {
851
+ triggerProps.onClick?.(event);
852
+ if (event.defaultPrevented || triggerProps.disabled || disabled)
853
+ return;
854
+ if (open)
855
+ close("trigger", false);
856
+ else
857
+ openMenu();
858
+ },
859
+ onKeyDown: (event) => {
860
+ triggerProps.onKeyDown?.(event);
861
+ if (event.defaultPrevented)
862
+ return;
863
+ if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
864
+ event.preventDefault();
865
+ openMenu("first");
866
+ }
867
+ else if (event.key === "ArrowUp") {
868
+ event.preventDefault();
869
+ openMenu("last");
870
+ }
871
+ },
872
+ });
873
+ const handleMenuKeyDown = (event) => {
874
+ if (event.key === "ArrowDown") {
875
+ event.preventDefault();
876
+ moveFocus(1);
877
+ }
878
+ else if (event.key === "ArrowUp") {
879
+ event.preventDefault();
880
+ moveFocus(-1);
881
+ }
882
+ else if (event.key === "Home") {
883
+ event.preventDefault();
884
+ focusAt(firstEnabled());
885
+ }
886
+ else if (event.key === "End") {
887
+ event.preventDefault();
888
+ focusAt(lastEnabled());
889
+ }
890
+ else if (event.key === "Escape") {
891
+ event.preventDefault();
892
+ close("escape", true);
893
+ }
894
+ else if (event.key === "Tab") {
895
+ close("tab", false);
896
+ }
897
+ else if (event.key.length === 1 &&
898
+ event.key !== " " &&
899
+ !event.altKey &&
900
+ !event.ctrlKey &&
901
+ !event.metaKey) {
902
+ event.preventDefault();
903
+ runTypeahead(event.key);
904
+ }
905
+ };
906
+ const showItems = asyncState.status !== "empty" && asyncState.status !== "error";
907
+ return (_jsxs("span", { ref: wrapperRef, className: "hjm-menu", "data-state": open ? "open" : "closed", children: [renderedTrigger, open ? (_jsx(AnchoredPortal, { anchorRef: triggerRef, ssrFallback: "inline", ...(portalContainer === undefined ? {} : { container: portalContainer }), children: _jsxs("div", { ref: setMenuContentRef, id: id, role: "menu", "aria-label": label, className: classNames("hjm-menu__content", className), "data-density": density, "data-async-state": asyncState.status, "data-placement": popupPosition.placement, "data-align": popupPosition.align, "aria-busy": asyncState.status === "loading" || asyncState.status === "loadingMore" || undefined, style: popupPosition.style, tabIndex: -1, onKeyDown: handleMenuKeyDown, children: [asyncState.status !== "idle" ? (_jsx("div", { className: "hjm-menu__state-message", role: asyncState.status === "error" ? "alert" : "status", children: asyncState.message })) : null, showItems ? (sections === undefined ? items.map((item, index) => {
908
+ const selected = itemSelected(item.id);
909
+ const itemDisabled = itemIsDisabled(item);
910
+ const role = selectionMode === "single"
911
+ ? "menuitemradio"
912
+ : selectionMode === "multiple"
913
+ ? "menuitemcheckbox"
914
+ : "menuitem";
915
+ return renderMenuItem(item, index, role, selected, itemDisabled);
916
+ }) : sections.map((section, sectionIndex) => {
917
+ const sectionLabelId = `${id}-section-${sectionIndex}`;
918
+ const firstIndex = sections
919
+ .slice(0, sectionIndex)
920
+ .reduce((total, current) => total + current.items.length, 0);
921
+ return (_jsxs("div", { className: "hjm-menu__section-boundary", children: [sectionIndex > 0 ? _jsx("div", { role: "separator", className: "hjm-menu__separator" }) : null, _jsxs("div", { role: "group", "aria-labelledby": section.label ? sectionLabelId : undefined, "aria-label": section.label ? undefined : section.accessibilityLabel, className: "hjm-menu__section", children: [section.label ? (_jsx("div", { id: sectionLabelId, className: "hjm-menu__section-label", children: section.label })) : null, section.items.map((item, itemIndex) => {
922
+ const index = firstIndex + itemIndex;
923
+ const selected = itemSelected(item.id);
924
+ const itemDisabled = itemIsDisabled(item);
925
+ const role = selectionMode === "single"
926
+ ? "menuitemradio"
927
+ : selectionMode === "multiple"
928
+ ? "menuitemcheckbox"
929
+ : "menuitem";
930
+ return renderMenuItem(item, index, role, selected, itemDisabled);
931
+ })] })] }, section.id));
932
+ })) : null] }) })) : null] }));
933
+ function renderMenuItem(item, index, role, selected, itemDisabled) {
934
+ return (_jsxs("button", { ref: (node) => {
935
+ if (node) {
936
+ itemRefs.current.set(index, node);
937
+ if (open && !itemDisabled && index === focusIndex) {
938
+ queueMicrotask(() => {
939
+ if (node.isConnected && open)
940
+ node.focus();
941
+ });
942
+ }
943
+ }
944
+ else
945
+ itemRefs.current.delete(index);
946
+ }, type: "button", role: role, "aria-checked": selectionMode === "action" ? undefined : selected, className: "hjm-menu__item", "data-tone": item.tone ?? menuRecipe.defaults.itemTone, "data-state": itemDisabled ? "disabled" : selected ? "selected" : "idle", "data-focus": index === focusIndex || undefined, disabled: itemDisabled, tabIndex: !itemDisabled && index === focusIndex ? 0 : -1, onClick: () => activateItem(item, index), children: [item.tone === "danger" ? (_jsx("span", { className: "hjm-menu__danger-indicator", "aria-hidden": "true", children: "!" })) : null, item.leading ? _jsx("span", { className: "hjm-menu__leading", "aria-hidden": "true", children: item.leading }) : null, _jsxs("span", { className: "hjm-menu__copy", children: [_jsx("span", { children: item.label }), item.description ? _jsx("span", { className: "hjm-menu__description", children: item.description }) : null] }), item.trailing ? _jsx("span", { className: "hjm-menu__trailing", children: item.trailing }) : null] }, item.id));
947
+ }
948
+ });
949
+ //# sourceMappingURL=overlays.js.map