@a13y/react 0.1.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.
@@ -0,0 +1,1170 @@
1
+ import { createContext, useContext, useState, useEffect, useRef, useCallback, useId } from 'react';
2
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
+ import { announce } from '@a13y/core/runtime/announce';
4
+
5
+ var useFocusTrap = (props) => {
6
+ const { isActive, onEscape, restoreFocus = true, autoFocus = true } = props;
7
+ const trapRef = useRef(null);
8
+ const focusTrapRef = useRef(null);
9
+ const previousFocusRef = useRef(null);
10
+ useEffect(() => {
11
+ if (!isActive || !trapRef.current) {
12
+ return;
13
+ }
14
+ if (restoreFocus) {
15
+ previousFocusRef.current = document.activeElement;
16
+ }
17
+ import('@a13y/core/runtime/focus').then(({ createFocusTrap }) => {
18
+ if (!trapRef.current) {
19
+ return;
20
+ }
21
+ const options = {
22
+ returnFocus: false,
23
+ onEscape
24
+ };
25
+ if (autoFocus) {
26
+ options.initialFocus = void 0;
27
+ }
28
+ const trap = createFocusTrap(trapRef.current, options);
29
+ trap.activate();
30
+ focusTrapRef.current = trap;
31
+ {
32
+ import('@a13y/devtools/runtime/validators').then(({ focusValidator }) => {
33
+ if (trapRef.current) {
34
+ focusValidator.validateFocusTrap(trapRef.current, true);
35
+ }
36
+ });
37
+ }
38
+ });
39
+ return () => {
40
+ if (focusTrapRef.current) {
41
+ focusTrapRef.current.deactivate();
42
+ focusTrapRef.current = null;
43
+ }
44
+ if (restoreFocus && previousFocusRef.current) {
45
+ previousFocusRef.current.focus();
46
+ {
47
+ import('@a13y/devtools/runtime/validators').then(({ focusValidator }) => {
48
+ if (previousFocusRef.current) {
49
+ focusValidator.expectFocusRestoration(
50
+ previousFocusRef.current,
51
+ "focus trap deactivation"
52
+ );
53
+ }
54
+ });
55
+ }
56
+ }
57
+ };
58
+ }, [isActive, onEscape, restoreFocus, autoFocus]);
59
+ return {
60
+ trapRef
61
+ };
62
+ };
63
+
64
+ // src/hooks/use-accessible-dialog.ts
65
+ var useAccessibleDialog = (props) => {
66
+ const {
67
+ isOpen,
68
+ onClose,
69
+ title,
70
+ description,
71
+ role = "dialog",
72
+ isModal = true,
73
+ closeOnBackdropClick = true
74
+ } = props;
75
+ {
76
+ if (!title || title.trim().length === 0) {
77
+ throw new Error(
78
+ '@a13y/react [useAccessibleDialog]: "title" prop is required for accessibility'
79
+ );
80
+ }
81
+ }
82
+ const dialogRef = useRef(null);
83
+ const titleId = useId();
84
+ const descriptionId = useId();
85
+ const { trapRef } = useFocusTrap({
86
+ isActive: isOpen,
87
+ onEscape: onClose,
88
+ restoreFocus: true,
89
+ autoFocus: true
90
+ });
91
+ useEffect(() => {
92
+ if (dialogRef.current && trapRef.current !== dialogRef.current) {
93
+ trapRef.current = dialogRef.current;
94
+ }
95
+ }, [trapRef]);
96
+ useEffect(() => {
97
+ if (!isOpen || !isModal) {
98
+ return;
99
+ }
100
+ const originalOverflow = document.body.style.overflow;
101
+ document.body.style.overflow = "hidden";
102
+ return () => {
103
+ document.body.style.overflow = originalOverflow;
104
+ };
105
+ }, [isOpen, isModal]);
106
+ useEffect(() => {
107
+ if (isOpen) {
108
+ import('@a13y/devtools/runtime/invariants').then(
109
+ ({ assertHasAccessibleName, assertValidAriaAttributes }) => {
110
+ if (dialogRef.current) {
111
+ assertHasAccessibleName(dialogRef.current, "useAccessibleDialog");
112
+ assertValidAriaAttributes(dialogRef.current);
113
+ }
114
+ }
115
+ );
116
+ }
117
+ }, [isOpen]);
118
+ const dialogProps = {
119
+ ref: dialogRef,
120
+ role,
121
+ "aria-labelledby": titleId,
122
+ "aria-describedby": description ? descriptionId : void 0,
123
+ "aria-modal": isModal,
124
+ tabIndex: -1
125
+ };
126
+ const titleProps = {
127
+ id: titleId
128
+ };
129
+ const descriptionProps = description ? { id: descriptionId } : null;
130
+ const backdropProps = closeOnBackdropClick && isModal ? {
131
+ onClick: onClose,
132
+ "aria-hidden": true
133
+ } : null;
134
+ return {
135
+ dialogProps,
136
+ titleProps,
137
+ descriptionProps,
138
+ backdropProps,
139
+ close: onClose
140
+ };
141
+ };
142
+ var AccessibleDialog = (props) => {
143
+ const {
144
+ isOpen,
145
+ onClose,
146
+ title,
147
+ children,
148
+ description,
149
+ role = "dialog",
150
+ showCloseButton = true,
151
+ className = "",
152
+ backdropClassName = ""
153
+ } = props;
154
+ const { dialogProps, titleProps, descriptionProps, backdropProps, close } = useAccessibleDialog({
155
+ isOpen,
156
+ onClose,
157
+ title,
158
+ description,
159
+ role,
160
+ isModal: true,
161
+ closeOnBackdropClick: true
162
+ });
163
+ if (!isOpen) {
164
+ return null;
165
+ }
166
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
167
+ backdropProps && /* @__PURE__ */ jsx(
168
+ "div",
169
+ {
170
+ ...backdropProps,
171
+ className: backdropClassName,
172
+ style: {
173
+ position: "fixed",
174
+ inset: 0,
175
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
176
+ display: "flex",
177
+ alignItems: "center",
178
+ justifyContent: "center",
179
+ zIndex: 50
180
+ }
181
+ }
182
+ ),
183
+ /* @__PURE__ */ jsxs(
184
+ "div",
185
+ {
186
+ ref: dialogProps.ref,
187
+ role: dialogProps.role,
188
+ "aria-labelledby": dialogProps["aria-labelledby"],
189
+ "aria-describedby": dialogProps["aria-describedby"],
190
+ "aria-modal": dialogProps["aria-modal"],
191
+ tabIndex: dialogProps.tabIndex,
192
+ className,
193
+ style: {
194
+ position: "fixed",
195
+ top: "50%",
196
+ left: "50%",
197
+ transform: "translate(-50%, -50%)",
198
+ backgroundColor: "white",
199
+ borderRadius: "0.5rem",
200
+ boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.1)",
201
+ padding: "1.5rem",
202
+ maxWidth: "32rem",
203
+ width: "90vw",
204
+ maxHeight: "90vh",
205
+ overflow: "auto",
206
+ zIndex: 51
207
+ },
208
+ children: [
209
+ showCloseButton && /* @__PURE__ */ jsx(
210
+ "button",
211
+ {
212
+ type: "button",
213
+ onClick: close,
214
+ "aria-label": "Close dialog",
215
+ style: {
216
+ position: "absolute",
217
+ top: "1rem",
218
+ right: "1rem",
219
+ padding: "0.5rem",
220
+ border: "none",
221
+ background: "transparent",
222
+ cursor: "pointer",
223
+ fontSize: "1.25rem",
224
+ lineHeight: 1,
225
+ color: "#6b7280"
226
+ },
227
+ children: "\u2715"
228
+ }
229
+ ),
230
+ /* @__PURE__ */ jsx(
231
+ "h2",
232
+ {
233
+ ...titleProps,
234
+ style: {
235
+ fontSize: "1.25rem",
236
+ fontWeight: 600,
237
+ marginBottom: description ? "0.5rem" : "1rem",
238
+ paddingRight: showCloseButton ? "2rem" : 0
239
+ },
240
+ children: title
241
+ }
242
+ ),
243
+ descriptionProps && description && /* @__PURE__ */ jsx(
244
+ "p",
245
+ {
246
+ ...descriptionProps,
247
+ style: {
248
+ fontSize: "0.875rem",
249
+ color: "#6b7280",
250
+ marginBottom: "1rem"
251
+ },
252
+ children: description
253
+ }
254
+ ),
255
+ /* @__PURE__ */ jsx("div", { children })
256
+ ]
257
+ }
258
+ )
259
+ ] });
260
+ };
261
+ var DialogStackContext = createContext(null);
262
+ var useDialogStack = () => {
263
+ const context = useContext(DialogStackContext);
264
+ if (!context) {
265
+ throw new Error("useDialogStack must be used within DialogStackProvider");
266
+ }
267
+ return context;
268
+ };
269
+ var DialogStackProvider = (props) => {
270
+ const { children, baseZIndex = 1e3, zIndexIncrement = 10 } = props;
271
+ const [stack, setStack] = useState([]);
272
+ const push = (dialog) => {
273
+ const zIndex = baseZIndex + stack.length * zIndexIncrement;
274
+ setStack((prev) => [...prev, { ...dialog, zIndex }]);
275
+ };
276
+ const pop = () => {
277
+ setStack((prev) => {
278
+ const newStack = [...prev];
279
+ const dialog = newStack.pop();
280
+ dialog?.onClose();
281
+ return newStack;
282
+ });
283
+ };
284
+ const close = (id) => {
285
+ setStack((prev) => {
286
+ const index = prev.findIndex((d) => d.id === id);
287
+ if (index === -1) return prev;
288
+ const closedDialogs = prev.slice(index);
289
+ for (const d of closedDialogs) {
290
+ d.onClose();
291
+ }
292
+ return prev.slice(0, index);
293
+ });
294
+ };
295
+ const closeAll = () => {
296
+ for (const d of stack) {
297
+ d.onClose();
298
+ }
299
+ setStack([]);
300
+ };
301
+ useEffect(() => {
302
+ if (stack.length > 0) {
303
+ const originalOverflow = document.body.style.overflow;
304
+ document.body.style.overflow = "hidden";
305
+ return () => {
306
+ document.body.style.overflow = originalOverflow;
307
+ };
308
+ }
309
+ }, [stack.length]);
310
+ const contextValue = {
311
+ push,
312
+ pop,
313
+ close,
314
+ closeAll,
315
+ depth: stack.length
316
+ };
317
+ return /* @__PURE__ */ jsxs(DialogStackContext.Provider, { value: contextValue, children: [
318
+ children,
319
+ stack.map((dialog) => /* @__PURE__ */ jsx("div", { style: { zIndex: dialog.zIndex }, children: /* @__PURE__ */ jsx(
320
+ AccessibleDialog,
321
+ {
322
+ isOpen: true,
323
+ onClose: () => close(dialog.id),
324
+ title: dialog.title,
325
+ description: dialog.description,
326
+ backdropClassName: "dialog-stack-backdrop",
327
+ children: dialog.content
328
+ }
329
+ ) }, dialog.id))
330
+ ] });
331
+ };
332
+ var InfiniteList = (props) => {
333
+ const {
334
+ items,
335
+ loadMore,
336
+ hasMore,
337
+ isLoading,
338
+ renderItem,
339
+ getItemKey,
340
+ loadingIndicator,
341
+ emptyState,
342
+ "aria-label": ariaLabel,
343
+ threshold = 200,
344
+ className = ""
345
+ } = props;
346
+ const [previousCount, setPreviousCount] = useState(items.length);
347
+ const sentinelRef = useRef(null);
348
+ const listRef = useRef(null);
349
+ useEffect(() => {
350
+ if (items.length > previousCount && !isLoading) {
351
+ const newItemsCount = items.length - previousCount;
352
+ announce(
353
+ `${newItemsCount} new item${newItemsCount === 1 ? "" : "s"} loaded. Total: ${items.length}`,
354
+ { politeness: "polite", delay: 500 }
355
+ );
356
+ setPreviousCount(items.length);
357
+ }
358
+ }, [items.length, previousCount, isLoading]);
359
+ useEffect(() => {
360
+ if (isLoading) {
361
+ announce("Loading more items", { politeness: "polite" });
362
+ }
363
+ }, [isLoading]);
364
+ useEffect(() => {
365
+ if (!hasMore || isLoading || !sentinelRef.current) {
366
+ return;
367
+ }
368
+ const observer = new IntersectionObserver(
369
+ (entries) => {
370
+ const sentinel = entries[0];
371
+ if (sentinel?.isIntersecting) {
372
+ loadMore();
373
+ }
374
+ },
375
+ {
376
+ root: null,
377
+ rootMargin: `${threshold}px`,
378
+ threshold: 0
379
+ }
380
+ );
381
+ observer.observe(sentinelRef.current);
382
+ return () => observer.disconnect();
383
+ }, [hasMore, isLoading, loadMore, threshold]);
384
+ if (items.length === 0 && !isLoading) {
385
+ return /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", children: emptyState || /* @__PURE__ */ jsx("p", { children: "No items to display" }) });
386
+ }
387
+ return /* @__PURE__ */ jsxs("div", { ref: listRef, className, children: [
388
+ /* @__PURE__ */ jsx("div", { role: "list", "aria-label": ariaLabel, "aria-busy": isLoading, children: items.map((item, index) => /* @__PURE__ */ jsx("div", { role: "listitem", children: renderItem(item, index) }, getItemKey(item, index))) }),
389
+ isLoading && /* @__PURE__ */ jsx(
390
+ "div",
391
+ {
392
+ role: "status",
393
+ "aria-live": "polite",
394
+ "aria-label": "Loading more items",
395
+ style: {
396
+ padding: "1rem",
397
+ textAlign: "center"
398
+ },
399
+ children: loadingIndicator || /* @__PURE__ */ jsx("span", { children: "Loading..." })
400
+ }
401
+ ),
402
+ hasMore && !isLoading && /* @__PURE__ */ jsx("div", { ref: sentinelRef, "aria-hidden": "true", style: { height: "1px", visibility: "hidden" } }),
403
+ !hasMore && items.length > 0 && /* @__PURE__ */ jsxs(
404
+ "div",
405
+ {
406
+ role: "status",
407
+ "aria-live": "polite",
408
+ style: {
409
+ padding: "1rem",
410
+ textAlign: "center",
411
+ color: "#6b7280",
412
+ fontSize: "0.875rem"
413
+ },
414
+ children: [
415
+ "End of list. Total: ",
416
+ items.length,
417
+ " items."
418
+ ]
419
+ }
420
+ )
421
+ ] });
422
+ };
423
+ var useAccessibleButton = (props) => {
424
+ const { label, onPress, isDisabled = false, role = "button", elementType = "button" } = props;
425
+ const buttonRef = useRef(null);
426
+ const isPressedRef = useRef(false);
427
+ useEffect(() => {
428
+ {
429
+ import('@a13y/devtools/runtime/invariants').then(
430
+ ({ assertHasAccessibleName, assertKeyboardAccessible }) => {
431
+ if (buttonRef.current) {
432
+ assertHasAccessibleName(buttonRef.current, "useAccessibleButton");
433
+ assertKeyboardAccessible(buttonRef.current, "useAccessibleButton");
434
+ }
435
+ }
436
+ );
437
+ }
438
+ }, []);
439
+ const handlePress = useCallback(
440
+ (event) => {
441
+ if (isDisabled) {
442
+ return;
443
+ }
444
+ onPress(event);
445
+ },
446
+ [onPress, isDisabled]
447
+ );
448
+ const handlePointerDown = useCallback(
449
+ (event) => {
450
+ if (isDisabled) {
451
+ event.preventDefault();
452
+ return;
453
+ }
454
+ isPressedRef.current = true;
455
+ handlePress({ type: "mouse" });
456
+ },
457
+ [handlePress, isDisabled]
458
+ );
459
+ const handleKeyDown = useCallback(
460
+ (event) => {
461
+ if (isDisabled) {
462
+ return;
463
+ }
464
+ if (event.key === "Enter" || event.key === " ") {
465
+ event.preventDefault();
466
+ handlePress({ type: "keyboard", key: event.key });
467
+ }
468
+ },
469
+ [handlePress, isDisabled]
470
+ );
471
+ const buttonProps = {
472
+ role,
473
+ tabIndex: isDisabled ? -1 : 0,
474
+ "aria-label": label,
475
+ "aria-disabled": isDisabled ? true : void 0,
476
+ disabled: elementType === "button" ? isDisabled : void 0,
477
+ onPointerDown: handlePointerDown,
478
+ onKeyDown: handleKeyDown
479
+ };
480
+ return {
481
+ buttonProps,
482
+ isPressed: isPressedRef.current
483
+ };
484
+ };
485
+ var useKeyboardNavigation = (props) => {
486
+ const {
487
+ orientation,
488
+ loop = false,
489
+ onNavigate,
490
+ defaultIndex = 0,
491
+ currentIndex: controlledIndex
492
+ } = props;
493
+ const isControlled = controlledIndex !== void 0;
494
+ const [uncontrolledIndex, setUncontrolledIndex] = useState(defaultIndex);
495
+ const currentIndex = isControlled ? controlledIndex : uncontrolledIndex;
496
+ const itemsRef = useRef(/* @__PURE__ */ new Map());
497
+ const containerRef = useRef(null);
498
+ const setCurrentIndex = useCallback(
499
+ (index) => {
500
+ if (!isControlled) {
501
+ setUncontrolledIndex(index);
502
+ }
503
+ onNavigate?.(index);
504
+ const element = itemsRef.current.get(index);
505
+ if (element) {
506
+ element.focus();
507
+ }
508
+ },
509
+ [isControlled, onNavigate]
510
+ );
511
+ const navigate = useCallback(
512
+ (direction) => {
513
+ const itemCount = itemsRef.current.size;
514
+ if (itemCount === 0) {
515
+ return;
516
+ }
517
+ let nextIndex = currentIndex;
518
+ switch (direction) {
519
+ case "forward":
520
+ nextIndex = currentIndex + 1;
521
+ if (nextIndex >= itemCount) {
522
+ nextIndex = loop ? 0 : itemCount - 1;
523
+ }
524
+ break;
525
+ case "backward":
526
+ nextIndex = currentIndex - 1;
527
+ if (nextIndex < 0) {
528
+ nextIndex = loop ? itemCount - 1 : 0;
529
+ }
530
+ break;
531
+ case "first":
532
+ nextIndex = 0;
533
+ break;
534
+ case "last":
535
+ nextIndex = itemCount - 1;
536
+ break;
537
+ }
538
+ if (nextIndex !== currentIndex) {
539
+ setCurrentIndex(nextIndex);
540
+ }
541
+ },
542
+ [currentIndex, loop, setCurrentIndex]
543
+ );
544
+ const handleKeyDown = useCallback(
545
+ (event) => {
546
+ const { key } = event;
547
+ let direction = null;
548
+ if (key === "ArrowRight") {
549
+ if (orientation === "horizontal" || orientation === "both") {
550
+ direction = "forward";
551
+ }
552
+ } else if (key === "ArrowLeft") {
553
+ if (orientation === "horizontal" || orientation === "both") {
554
+ direction = "backward";
555
+ }
556
+ } else if (key === "ArrowDown") {
557
+ if (orientation === "vertical" || orientation === "both") {
558
+ direction = "forward";
559
+ }
560
+ } else if (key === "ArrowUp") {
561
+ if (orientation === "vertical" || orientation === "both") {
562
+ direction = "backward";
563
+ }
564
+ } else if (key === "Home") {
565
+ direction = "first";
566
+ } else if (key === "End") {
567
+ direction = "last";
568
+ }
569
+ if (direction) {
570
+ event.preventDefault();
571
+ navigate(direction);
572
+ }
573
+ },
574
+ [orientation, navigate]
575
+ );
576
+ const getItemProps = useCallback(
577
+ (index) => {
578
+ return {
579
+ ref: (element) => {
580
+ if (element) {
581
+ itemsRef.current.set(index, element);
582
+ } else {
583
+ itemsRef.current.delete(index);
584
+ }
585
+ },
586
+ tabIndex: index === currentIndex ? 0 : -1,
587
+ onKeyDown: handleKeyDown,
588
+ "data-index": index
589
+ };
590
+ },
591
+ [currentIndex, handleKeyDown]
592
+ );
593
+ useEffect(() => {
594
+ {
595
+ import('@a13y/devtools/runtime/validators').then(({ keyboardValidator }) => {
596
+ if (containerRef.current) {
597
+ keyboardValidator.validateContainer(containerRef.current);
598
+ }
599
+ const container = Array.from(itemsRef.current.values())[0]?.parentElement;
600
+ if (container) {
601
+ keyboardValidator.validateRovingTabindex(container);
602
+ }
603
+ });
604
+ }
605
+ }, []);
606
+ const containerProps = {
607
+ role: "toolbar",
608
+ "aria-orientation": orientation
609
+ };
610
+ return {
611
+ currentIndex,
612
+ setCurrentIndex,
613
+ getItemProps,
614
+ containerProps
615
+ };
616
+ };
617
+ var NestedMenu = (props) => {
618
+ const { label, trigger, items, className = "" } = props;
619
+ const [isOpen, setIsOpen] = useState(false);
620
+ const [openSubmenuId, setOpenSubmenuId] = useState(null);
621
+ const { buttonProps } = useAccessibleButton({
622
+ label,
623
+ onPress: () => setIsOpen(!isOpen)
624
+ });
625
+ useEffect(() => {
626
+ const handleEscape = (e) => {
627
+ if (e.key === "Escape" && isOpen) {
628
+ setIsOpen(false);
629
+ setOpenSubmenuId(null);
630
+ }
631
+ };
632
+ document.addEventListener("keydown", handleEscape);
633
+ return () => document.removeEventListener("keydown", handleEscape);
634
+ }, [isOpen]);
635
+ return /* @__PURE__ */ jsxs("div", { style: { position: "relative", display: "inline-block" }, children: [
636
+ /* @__PURE__ */ jsx(
637
+ "button",
638
+ {
639
+ ...buttonProps,
640
+ "aria-expanded": isOpen,
641
+ "aria-haspopup": "true",
642
+ className,
643
+ style: {
644
+ padding: "0.5rem 1rem",
645
+ border: "1px solid #d1d5db",
646
+ borderRadius: "0.375rem",
647
+ backgroundColor: "white",
648
+ cursor: "pointer"
649
+ },
650
+ children: trigger
651
+ }
652
+ ),
653
+ isOpen && /* @__PURE__ */ jsx(
654
+ MenuLevel,
655
+ {
656
+ items,
657
+ onClose: () => setIsOpen(false),
658
+ openSubmenuId,
659
+ onSubmenuChange: setOpenSubmenuId,
660
+ depth: 0
661
+ }
662
+ )
663
+ ] });
664
+ };
665
+ var MenuLevel = (props) => {
666
+ const { items, onClose, openSubmenuId, onSubmenuChange, depth } = props;
667
+ const menuRef = useRef(null);
668
+ const { getItemProps, setCurrentIndex } = useKeyboardNavigation({
669
+ orientation: "vertical",
670
+ loop: true
671
+ });
672
+ const handleItemKeyDown = (e, item) => {
673
+ if (e.key === "ArrowRight" && item.submenu) {
674
+ e.preventDefault();
675
+ e.stopPropagation();
676
+ onSubmenuChange(item.id);
677
+ }
678
+ if (e.key === "ArrowLeft" && depth > 0) {
679
+ e.preventDefault();
680
+ e.stopPropagation();
681
+ onSubmenuChange(null);
682
+ }
683
+ if (e.key === "Enter" || e.key === " ") {
684
+ e.preventDefault();
685
+ e.stopPropagation();
686
+ if (item.submenu) {
687
+ onSubmenuChange(item.id);
688
+ } else if (item.onPress && !item.disabled) {
689
+ item.onPress();
690
+ onClose();
691
+ }
692
+ }
693
+ };
694
+ const handleItemClick = (item) => {
695
+ if (item.disabled) return;
696
+ if (item.submenu) {
697
+ onSubmenuChange(openSubmenuId === item.id ? null : item.id);
698
+ } else if (item.onPress) {
699
+ item.onPress();
700
+ onClose();
701
+ }
702
+ };
703
+ return /* @__PURE__ */ jsx(
704
+ "div",
705
+ {
706
+ ref: menuRef,
707
+ role: "menu",
708
+ "aria-orientation": "vertical",
709
+ style: {
710
+ position: depth === 0 ? "absolute" : "absolute",
711
+ top: depth === 0 ? "calc(100% + 0.25rem)" : 0,
712
+ left: depth === 0 ? 0 : "100%",
713
+ minWidth: "12rem",
714
+ backgroundColor: "white",
715
+ border: "1px solid #e5e7eb",
716
+ borderRadius: "0.375rem",
717
+ boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1)",
718
+ padding: "0.25rem",
719
+ zIndex: 50 + depth
720
+ },
721
+ children: items.map((item, index) => {
722
+ const itemProps = getItemProps(index);
723
+ const hasSubmenu = !!item.submenu;
724
+ const isSubmenuOpen = openSubmenuId === item.id;
725
+ return /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
726
+ /* @__PURE__ */ jsxs(
727
+ "button",
728
+ {
729
+ ...itemProps,
730
+ role: "menuitem",
731
+ "aria-haspopup": hasSubmenu ? "true" : void 0,
732
+ "aria-expanded": hasSubmenu ? isSubmenuOpen : void 0,
733
+ disabled: item.disabled,
734
+ onClick: () => handleItemClick(item),
735
+ onKeyDown: (e) => handleItemKeyDown(e, item),
736
+ style: {
737
+ display: "flex",
738
+ alignItems: "center",
739
+ justifyContent: "space-between",
740
+ gap: "0.75rem",
741
+ width: "100%",
742
+ padding: "0.5rem 0.75rem",
743
+ border: "none",
744
+ background: "transparent",
745
+ textAlign: "left",
746
+ fontSize: "0.875rem",
747
+ cursor: item.disabled ? "not-allowed" : "pointer",
748
+ borderRadius: "0.25rem",
749
+ color: item.disabled ? "#9ca3af" : "#111827",
750
+ opacity: item.disabled ? 0.5 : 1
751
+ },
752
+ onMouseEnter: (e) => {
753
+ if (!item.disabled) {
754
+ e.currentTarget.style.backgroundColor = "#f3f4f6";
755
+ setCurrentIndex(index);
756
+ if (hasSubmenu) {
757
+ onSubmenuChange(item.id);
758
+ }
759
+ }
760
+ },
761
+ onMouseLeave: (e) => {
762
+ e.currentTarget.style.backgroundColor = "transparent";
763
+ },
764
+ children: [
765
+ /* @__PURE__ */ jsxs("span", { style: { display: "flex", alignItems: "center", gap: "0.5rem" }, children: [
766
+ item.icon && /* @__PURE__ */ jsx("span", { children: item.icon }),
767
+ /* @__PURE__ */ jsx("span", { children: item.label })
768
+ ] }),
769
+ hasSubmenu && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u25B6" })
770
+ ]
771
+ }
772
+ ),
773
+ hasSubmenu && isSubmenuOpen && item.submenu && /* @__PURE__ */ jsx(
774
+ MenuLevel,
775
+ {
776
+ items: item.submenu,
777
+ onClose,
778
+ openSubmenuId: null,
779
+ onSubmenuChange: () => {
780
+ },
781
+ depth: depth + 1
782
+ }
783
+ )
784
+ ] }, item.id);
785
+ })
786
+ }
787
+ );
788
+ };
789
+ var VirtualizedList = (props) => {
790
+ const {
791
+ items,
792
+ itemHeight,
793
+ height,
794
+ renderItem,
795
+ getItemKey,
796
+ "aria-label": ariaLabel,
797
+ overscan = 3,
798
+ className = "",
799
+ emptyState
800
+ } = props;
801
+ const [scrollTop, setScrollTop] = useState(0);
802
+ const [visibleRange, setVisibleRange] = useState({ start: 0, end: 0 });
803
+ const containerRef = useRef(null);
804
+ const previousRangeRef = useRef({ start: 0, end: 0 });
805
+ const totalHeight = items.length * itemHeight;
806
+ const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
807
+ const endIndex = Math.min(
808
+ items.length - 1,
809
+ Math.ceil((scrollTop + height) / itemHeight) + overscan
810
+ );
811
+ const visibleItems = items.slice(startIndex, endIndex + 1);
812
+ useEffect(() => {
813
+ const newRange = { start: startIndex, end: endIndex };
814
+ const rangeChanged = Math.abs(newRange.start - previousRangeRef.current.start) > 10 || Math.abs(newRange.end - previousRangeRef.current.end) > 10;
815
+ if (rangeChanged && items.length > 0) {
816
+ setVisibleRange(newRange);
817
+ previousRangeRef.current = newRange;
818
+ const message = `Showing items ${newRange.start + 1} to ${newRange.end + 1} of ${items.length}`;
819
+ announce(message, { politeness: "polite", delay: 300 });
820
+ }
821
+ }, [startIndex, endIndex, items.length]);
822
+ const handleScroll = (e) => {
823
+ setScrollTop(e.currentTarget.scrollTop);
824
+ };
825
+ const handleKeyDown = (e) => {
826
+ if (!containerRef.current) return;
827
+ const scrollAmount = itemHeight * 5;
828
+ switch (e.key) {
829
+ case "PageDown":
830
+ e.preventDefault();
831
+ containerRef.current.scrollBy({ top: scrollAmount, behavior: "smooth" });
832
+ break;
833
+ case "PageUp":
834
+ e.preventDefault();
835
+ containerRef.current.scrollBy({ top: -scrollAmount, behavior: "smooth" });
836
+ break;
837
+ case "Home":
838
+ e.preventDefault();
839
+ containerRef.current.scrollTo({ top: 0, behavior: "smooth" });
840
+ break;
841
+ case "End":
842
+ e.preventDefault();
843
+ containerRef.current.scrollTo({ top: totalHeight, behavior: "smooth" });
844
+ break;
845
+ }
846
+ };
847
+ if (items.length === 0) {
848
+ return /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", children: emptyState || /* @__PURE__ */ jsx("p", { children: "No items to display" }) });
849
+ }
850
+ return /* @__PURE__ */ jsxs(
851
+ "div",
852
+ {
853
+ ref: containerRef,
854
+ role: "list",
855
+ "aria-label": ariaLabel,
856
+ tabIndex: 0,
857
+ className,
858
+ onScroll: handleScroll,
859
+ onKeyDown: handleKeyDown,
860
+ style: {
861
+ height: `${height}px`,
862
+ overflow: "auto",
863
+ position: "relative",
864
+ outline: "none"
865
+ },
866
+ children: [
867
+ /* @__PURE__ */ jsx("div", { style: { height: `${totalHeight}px`, position: "relative" }, children: visibleItems.map((item, virtualIndex) => {
868
+ const actualIndex = startIndex + virtualIndex;
869
+ const itemStyle = {
870
+ position: "absolute",
871
+ top: `${actualIndex * itemHeight}px`,
872
+ left: 0,
873
+ right: 0,
874
+ height: `${itemHeight}px`
875
+ };
876
+ return /* @__PURE__ */ jsx(
877
+ "div",
878
+ {
879
+ role: "listitem",
880
+ "aria-setsize": items.length,
881
+ "aria-posinset": actualIndex + 1,
882
+ style: itemStyle,
883
+ children: renderItem(item, actualIndex)
884
+ },
885
+ getItemKey(item, actualIndex)
886
+ );
887
+ }) }),
888
+ /* @__PURE__ */ jsx(
889
+ "div",
890
+ {
891
+ "aria-live": "polite",
892
+ "aria-atomic": "true",
893
+ style: {
894
+ position: "absolute",
895
+ left: "-10000px",
896
+ width: "1px",
897
+ height: "1px",
898
+ overflow: "hidden"
899
+ },
900
+ children: `Showing items ${visibleRange.start + 1} to ${visibleRange.end + 1} of ${items.length}`
901
+ }
902
+ )
903
+ ]
904
+ }
905
+ );
906
+ };
907
+ var WizardContext = createContext(null);
908
+ var useWizard = () => {
909
+ const context = useContext(WizardContext);
910
+ if (!context) {
911
+ throw new Error("useWizard must be used within Wizard component");
912
+ }
913
+ return context;
914
+ };
915
+ var Wizard = (props) => {
916
+ const { steps, onComplete, onCancel, initialStep = 0, className = "" } = props;
917
+ const [currentStep, setCurrentStep] = useState(initialStep);
918
+ const [validationError, setValidationError] = useState(null);
919
+ const [visitedSteps, setVisitedSteps] = useState(/* @__PURE__ */ new Set([initialStep]));
920
+ const totalSteps = steps.length;
921
+ const step = steps[currentStep] ?? steps[0];
922
+ if (currentStep < 0 || currentStep >= totalSteps) {
923
+ setCurrentStep(0);
924
+ }
925
+ const isLastStep = currentStep === totalSteps - 1;
926
+ const canGoPrevious = currentStep > 0;
927
+ const canGoNext = currentStep < totalSteps - 1;
928
+ const validateCurrentStep = () => {
929
+ if (!step.validate) return true;
930
+ const result = step.validate();
931
+ if (result === true) {
932
+ setValidationError(null);
933
+ return true;
934
+ }
935
+ setValidationError(result);
936
+ announce(`Validation error: ${result}`, { politeness: "assertive" });
937
+ return false;
938
+ };
939
+ const goToStep = (index) => {
940
+ if (index < 0 || index >= totalSteps) return;
941
+ if (index > currentStep && !validateCurrentStep()) {
942
+ return;
943
+ }
944
+ setCurrentStep(index);
945
+ setVisitedSteps((prev) => /* @__PURE__ */ new Set([...prev, index]));
946
+ setValidationError(null);
947
+ const newStep = steps[index];
948
+ if (newStep) {
949
+ announce(
950
+ `Step ${index + 1} of ${totalSteps}: ${newStep.label}${newStep.optional ? " (optional)" : ""}`,
951
+ { politeness: "polite" }
952
+ );
953
+ }
954
+ };
955
+ const next = () => {
956
+ if (!canGoNext) return;
957
+ if (validateCurrentStep()) {
958
+ goToStep(currentStep + 1);
959
+ }
960
+ };
961
+ const previous = () => {
962
+ if (!canGoPrevious) return;
963
+ goToStep(currentStep - 1);
964
+ };
965
+ const handleComplete = () => {
966
+ if (validateCurrentStep()) {
967
+ announce("Wizard completed", { politeness: "polite" });
968
+ onComplete();
969
+ }
970
+ };
971
+ const contextValue = {
972
+ currentStep,
973
+ totalSteps,
974
+ step,
975
+ next,
976
+ previous,
977
+ goToStep,
978
+ canGoNext,
979
+ canGoPrevious,
980
+ isLastStep,
981
+ validationError
982
+ };
983
+ return /* @__PURE__ */ jsx(WizardContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs("div", { className, style: { maxWidth: "48rem", margin: "0 auto" }, children: [
984
+ /* @__PURE__ */ jsx("nav", { "aria-label": "Wizard progress", children: /* @__PURE__ */ jsx(
985
+ "ol",
986
+ {
987
+ style: {
988
+ display: "flex",
989
+ justifyContent: "space-between",
990
+ marginBottom: "2rem",
991
+ padding: 0,
992
+ listStyle: "none"
993
+ },
994
+ children: steps.map((s, index) => {
995
+ const isActive = index === currentStep;
996
+ const isCompleted = visitedSteps.has(index) && index < currentStep;
997
+ const isClickable = visitedSteps.has(index) || index === currentStep;
998
+ return /* @__PURE__ */ jsxs(
999
+ "li",
1000
+ {
1001
+ style: {
1002
+ flex: 1,
1003
+ display: "flex",
1004
+ flexDirection: "column",
1005
+ alignItems: "center"
1006
+ },
1007
+ children: [
1008
+ /* @__PURE__ */ jsx(
1009
+ "button",
1010
+ {
1011
+ type: "button",
1012
+ onClick: () => isClickable && goToStep(index),
1013
+ "aria-current": isActive ? "step" : void 0,
1014
+ disabled: !isClickable,
1015
+ style: {
1016
+ width: "2.5rem",
1017
+ height: "2.5rem",
1018
+ borderRadius: "50%",
1019
+ border: `2px solid ${isActive || isCompleted ? "#2563eb" : "#d1d5db"}`,
1020
+ backgroundColor: isCompleted ? "#2563eb" : isActive ? "white" : "#f3f4f6",
1021
+ color: isCompleted ? "white" : isActive ? "#2563eb" : "#9ca3af",
1022
+ fontWeight: 600,
1023
+ cursor: isClickable ? "pointer" : "not-allowed",
1024
+ marginBottom: "0.5rem"
1025
+ },
1026
+ children: isCompleted ? "\u2713" : index + 1
1027
+ }
1028
+ ),
1029
+ /* @__PURE__ */ jsxs(
1030
+ "span",
1031
+ {
1032
+ style: {
1033
+ fontSize: "0.875rem",
1034
+ color: isActive ? "#2563eb" : "#6b7280",
1035
+ fontWeight: isActive ? 600 : 400,
1036
+ textAlign: "center"
1037
+ },
1038
+ children: [
1039
+ s.label,
1040
+ s.optional && /* @__PURE__ */ jsx("span", { style: { display: "block", fontSize: "0.75rem", color: "#9ca3af" }, children: "(Optional)" })
1041
+ ]
1042
+ }
1043
+ )
1044
+ ]
1045
+ },
1046
+ s.id
1047
+ );
1048
+ })
1049
+ }
1050
+ ) }),
1051
+ validationError && /* @__PURE__ */ jsx(
1052
+ "div",
1053
+ {
1054
+ role: "alert",
1055
+ "aria-live": "assertive",
1056
+ style: {
1057
+ padding: "0.75rem",
1058
+ marginBottom: "1rem",
1059
+ backgroundColor: "#fef2f2",
1060
+ border: "1px solid #fecaca",
1061
+ borderRadius: "0.375rem",
1062
+ color: "#991b1b"
1063
+ },
1064
+ children: validationError
1065
+ }
1066
+ ),
1067
+ /* @__PURE__ */ jsxs(
1068
+ "div",
1069
+ {
1070
+ role: "region",
1071
+ "aria-labelledby": `step-${step.id}-label`,
1072
+ style: { marginBottom: "2rem" },
1073
+ children: [
1074
+ /* @__PURE__ */ jsx(
1075
+ "h2",
1076
+ {
1077
+ id: `step-${step.id}-label`,
1078
+ style: { fontSize: "1.5rem", fontWeight: 600, marginBottom: "1rem" },
1079
+ children: step.label
1080
+ }
1081
+ ),
1082
+ step.content
1083
+ ]
1084
+ }
1085
+ ),
1086
+ /* @__PURE__ */ jsxs(
1087
+ "div",
1088
+ {
1089
+ style: {
1090
+ display: "flex",
1091
+ justifyContent: "space-between",
1092
+ gap: "1rem"
1093
+ },
1094
+ children: [
1095
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "0.5rem" }, children: [
1096
+ onCancel && /* @__PURE__ */ jsx(
1097
+ "button",
1098
+ {
1099
+ type: "button",
1100
+ onClick: onCancel,
1101
+ style: {
1102
+ padding: "0.5rem 1rem",
1103
+ border: "1px solid #d1d5db",
1104
+ borderRadius: "0.375rem",
1105
+ backgroundColor: "white",
1106
+ cursor: "pointer"
1107
+ },
1108
+ children: "Cancel"
1109
+ }
1110
+ ),
1111
+ canGoPrevious && /* @__PURE__ */ jsx(
1112
+ "button",
1113
+ {
1114
+ type: "button",
1115
+ onClick: previous,
1116
+ style: {
1117
+ padding: "0.5rem 1rem",
1118
+ border: "1px solid #d1d5db",
1119
+ borderRadius: "0.375rem",
1120
+ backgroundColor: "white",
1121
+ cursor: "pointer"
1122
+ },
1123
+ children: "\u2190 Previous"
1124
+ }
1125
+ )
1126
+ ] }),
1127
+ /* @__PURE__ */ jsx("div", { children: isLastStep ? /* @__PURE__ */ jsx(
1128
+ "button",
1129
+ {
1130
+ type: "button",
1131
+ onClick: handleComplete,
1132
+ style: {
1133
+ padding: "0.5rem 1.5rem",
1134
+ border: "none",
1135
+ borderRadius: "0.375rem",
1136
+ backgroundColor: "#2563eb",
1137
+ color: "white",
1138
+ fontWeight: 600,
1139
+ cursor: "pointer"
1140
+ },
1141
+ children: "Complete"
1142
+ }
1143
+ ) : /* @__PURE__ */ jsx(
1144
+ "button",
1145
+ {
1146
+ type: "button",
1147
+ onClick: next,
1148
+ disabled: !canGoNext,
1149
+ style: {
1150
+ padding: "0.5rem 1.5rem",
1151
+ border: "none",
1152
+ borderRadius: "0.375rem",
1153
+ backgroundColor: "#2563eb",
1154
+ color: "white",
1155
+ fontWeight: 600,
1156
+ cursor: canGoNext ? "pointer" : "not-allowed",
1157
+ opacity: canGoNext ? 1 : 0.5
1158
+ },
1159
+ children: "Next \u2192"
1160
+ }
1161
+ ) })
1162
+ ]
1163
+ }
1164
+ )
1165
+ ] }) });
1166
+ };
1167
+
1168
+ export { DialogStackProvider, InfiniteList, NestedMenu, VirtualizedList, Wizard, useDialogStack, useWizard };
1169
+ //# sourceMappingURL=index.js.map
1170
+ //# sourceMappingURL=index.js.map