@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.
package/dist/index.js ADDED
@@ -0,0 +1,1875 @@
1
+ import { createContext, useRef, useEffect, useCallback, useId, useState, useContext } from 'react';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import { announce } from '@a13y/core/runtime/announce';
4
+
5
+ var useAccessibleButton = (props) => {
6
+ const { label, onPress, isDisabled = false, role = "button", elementType = "button" } = props;
7
+ const buttonRef = useRef(null);
8
+ const isPressedRef = useRef(false);
9
+ useEffect(() => {
10
+ {
11
+ import('@a13y/devtools/runtime/invariants').then(
12
+ ({ assertHasAccessibleName, assertKeyboardAccessible }) => {
13
+ if (buttonRef.current) {
14
+ assertHasAccessibleName(buttonRef.current, "useAccessibleButton");
15
+ assertKeyboardAccessible(buttonRef.current, "useAccessibleButton");
16
+ }
17
+ }
18
+ );
19
+ }
20
+ }, []);
21
+ const handlePress = useCallback(
22
+ (event) => {
23
+ if (isDisabled) {
24
+ return;
25
+ }
26
+ onPress(event);
27
+ },
28
+ [onPress, isDisabled]
29
+ );
30
+ const handlePointerDown = useCallback(
31
+ (event) => {
32
+ if (isDisabled) {
33
+ event.preventDefault();
34
+ return;
35
+ }
36
+ isPressedRef.current = true;
37
+ handlePress({ type: "mouse" });
38
+ },
39
+ [handlePress, isDisabled]
40
+ );
41
+ const handleKeyDown = useCallback(
42
+ (event) => {
43
+ if (isDisabled) {
44
+ return;
45
+ }
46
+ if (event.key === "Enter" || event.key === " ") {
47
+ event.preventDefault();
48
+ handlePress({ type: "keyboard", key: event.key });
49
+ }
50
+ },
51
+ [handlePress, isDisabled]
52
+ );
53
+ const buttonProps = {
54
+ role,
55
+ tabIndex: isDisabled ? -1 : 0,
56
+ "aria-label": label,
57
+ "aria-disabled": isDisabled ? true : void 0,
58
+ disabled: elementType === "button" ? isDisabled : void 0,
59
+ onPointerDown: handlePointerDown,
60
+ onKeyDown: handleKeyDown
61
+ };
62
+ return {
63
+ buttonProps,
64
+ isPressed: isPressedRef.current
65
+ };
66
+ };
67
+ var AccessibleButton = (props) => {
68
+ const {
69
+ children,
70
+ label,
71
+ onPress,
72
+ disabled = false,
73
+ variant = "primary",
74
+ className = "",
75
+ type = "button"
76
+ } = props;
77
+ const { buttonProps } = useAccessibleButton({
78
+ label,
79
+ onPress,
80
+ isDisabled: disabled
81
+ });
82
+ const baseStyles = "inline-flex items-center justify-center font-medium transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 disabled:opacity-50 disabled:cursor-not-allowed";
83
+ const variantStyles = {
84
+ primary: "bg-blue-600 text-white hover:bg-blue-700 focus-visible:outline-blue-600",
85
+ secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300 focus-visible:outline-gray-500",
86
+ danger: "bg-red-600 text-white hover:bg-red-700 focus-visible:outline-red-600",
87
+ ghost: "bg-transparent text-gray-700 hover:bg-gray-100 focus-visible:outline-gray-500"
88
+ };
89
+ const combinedClassName = `${baseStyles} ${variantStyles[variant]} ${className}`.trim();
90
+ return /* @__PURE__ */ jsx(
91
+ "button",
92
+ {
93
+ ...buttonProps,
94
+ type,
95
+ className: combinedClassName,
96
+ style: {
97
+ padding: "0.5rem 1rem",
98
+ borderRadius: "0.375rem",
99
+ border: "none",
100
+ cursor: disabled ? "not-allowed" : "pointer"
101
+ },
102
+ children
103
+ }
104
+ );
105
+ };
106
+ var useFocusTrap = (props) => {
107
+ const { isActive, onEscape, restoreFocus = true, autoFocus = true } = props;
108
+ const trapRef = useRef(null);
109
+ const focusTrapRef = useRef(null);
110
+ const previousFocusRef = useRef(null);
111
+ useEffect(() => {
112
+ if (!isActive || !trapRef.current) {
113
+ return;
114
+ }
115
+ if (restoreFocus) {
116
+ previousFocusRef.current = document.activeElement;
117
+ }
118
+ import('@a13y/core/runtime/focus').then(({ createFocusTrap }) => {
119
+ if (!trapRef.current) {
120
+ return;
121
+ }
122
+ const options = {
123
+ returnFocus: false,
124
+ onEscape
125
+ };
126
+ if (autoFocus) {
127
+ options.initialFocus = void 0;
128
+ }
129
+ const trap = createFocusTrap(trapRef.current, options);
130
+ trap.activate();
131
+ focusTrapRef.current = trap;
132
+ {
133
+ import('@a13y/devtools/runtime/validators').then(({ focusValidator }) => {
134
+ if (trapRef.current) {
135
+ focusValidator.validateFocusTrap(trapRef.current, true);
136
+ }
137
+ });
138
+ }
139
+ });
140
+ return () => {
141
+ if (focusTrapRef.current) {
142
+ focusTrapRef.current.deactivate();
143
+ focusTrapRef.current = null;
144
+ }
145
+ if (restoreFocus && previousFocusRef.current) {
146
+ previousFocusRef.current.focus();
147
+ {
148
+ import('@a13y/devtools/runtime/validators').then(({ focusValidator }) => {
149
+ if (previousFocusRef.current) {
150
+ focusValidator.expectFocusRestoration(
151
+ previousFocusRef.current,
152
+ "focus trap deactivation"
153
+ );
154
+ }
155
+ });
156
+ }
157
+ }
158
+ };
159
+ }, [isActive, onEscape, restoreFocus, autoFocus]);
160
+ return {
161
+ trapRef
162
+ };
163
+ };
164
+
165
+ // src/hooks/use-accessible-dialog.ts
166
+ var useAccessibleDialog = (props) => {
167
+ const {
168
+ isOpen,
169
+ onClose,
170
+ title,
171
+ description,
172
+ role = "dialog",
173
+ isModal = true,
174
+ closeOnBackdropClick = true
175
+ } = props;
176
+ {
177
+ if (!title || title.trim().length === 0) {
178
+ throw new Error(
179
+ '@a13y/react [useAccessibleDialog]: "title" prop is required for accessibility'
180
+ );
181
+ }
182
+ }
183
+ const dialogRef = useRef(null);
184
+ const titleId = useId();
185
+ const descriptionId = useId();
186
+ const { trapRef } = useFocusTrap({
187
+ isActive: isOpen,
188
+ onEscape: onClose,
189
+ restoreFocus: true,
190
+ autoFocus: true
191
+ });
192
+ useEffect(() => {
193
+ if (dialogRef.current && trapRef.current !== dialogRef.current) {
194
+ trapRef.current = dialogRef.current;
195
+ }
196
+ }, [trapRef]);
197
+ useEffect(() => {
198
+ if (!isOpen || !isModal) {
199
+ return;
200
+ }
201
+ const originalOverflow = document.body.style.overflow;
202
+ document.body.style.overflow = "hidden";
203
+ return () => {
204
+ document.body.style.overflow = originalOverflow;
205
+ };
206
+ }, [isOpen, isModal]);
207
+ useEffect(() => {
208
+ if (isOpen) {
209
+ import('@a13y/devtools/runtime/invariants').then(
210
+ ({ assertHasAccessibleName, assertValidAriaAttributes }) => {
211
+ if (dialogRef.current) {
212
+ assertHasAccessibleName(dialogRef.current, "useAccessibleDialog");
213
+ assertValidAriaAttributes(dialogRef.current);
214
+ }
215
+ }
216
+ );
217
+ }
218
+ }, [isOpen]);
219
+ const dialogProps = {
220
+ ref: dialogRef,
221
+ role,
222
+ "aria-labelledby": titleId,
223
+ "aria-describedby": description ? descriptionId : void 0,
224
+ "aria-modal": isModal,
225
+ tabIndex: -1
226
+ };
227
+ const titleProps = {
228
+ id: titleId
229
+ };
230
+ const descriptionProps = description ? { id: descriptionId } : null;
231
+ const backdropProps = closeOnBackdropClick && isModal ? {
232
+ onClick: onClose,
233
+ "aria-hidden": true
234
+ } : null;
235
+ return {
236
+ dialogProps,
237
+ titleProps,
238
+ descriptionProps,
239
+ backdropProps,
240
+ close: onClose
241
+ };
242
+ };
243
+ var AccessibleDialog = (props) => {
244
+ const {
245
+ isOpen,
246
+ onClose,
247
+ title,
248
+ children,
249
+ description,
250
+ role = "dialog",
251
+ showCloseButton = true,
252
+ className = "",
253
+ backdropClassName = ""
254
+ } = props;
255
+ const { dialogProps, titleProps, descriptionProps, backdropProps, close } = useAccessibleDialog({
256
+ isOpen,
257
+ onClose,
258
+ title,
259
+ description,
260
+ role,
261
+ isModal: true,
262
+ closeOnBackdropClick: true
263
+ });
264
+ if (!isOpen) {
265
+ return null;
266
+ }
267
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
268
+ backdropProps && /* @__PURE__ */ jsx(
269
+ "div",
270
+ {
271
+ ...backdropProps,
272
+ className: backdropClassName,
273
+ style: {
274
+ position: "fixed",
275
+ inset: 0,
276
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
277
+ display: "flex",
278
+ alignItems: "center",
279
+ justifyContent: "center",
280
+ zIndex: 50
281
+ }
282
+ }
283
+ ),
284
+ /* @__PURE__ */ jsxs(
285
+ "div",
286
+ {
287
+ ref: dialogProps.ref,
288
+ role: dialogProps.role,
289
+ "aria-labelledby": dialogProps["aria-labelledby"],
290
+ "aria-describedby": dialogProps["aria-describedby"],
291
+ "aria-modal": dialogProps["aria-modal"],
292
+ tabIndex: dialogProps.tabIndex,
293
+ className,
294
+ style: {
295
+ position: "fixed",
296
+ top: "50%",
297
+ left: "50%",
298
+ transform: "translate(-50%, -50%)",
299
+ backgroundColor: "white",
300
+ borderRadius: "0.5rem",
301
+ boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.1)",
302
+ padding: "1.5rem",
303
+ maxWidth: "32rem",
304
+ width: "90vw",
305
+ maxHeight: "90vh",
306
+ overflow: "auto",
307
+ zIndex: 51
308
+ },
309
+ children: [
310
+ showCloseButton && /* @__PURE__ */ jsx(
311
+ "button",
312
+ {
313
+ type: "button",
314
+ onClick: close,
315
+ "aria-label": "Close dialog",
316
+ style: {
317
+ position: "absolute",
318
+ top: "1rem",
319
+ right: "1rem",
320
+ padding: "0.5rem",
321
+ border: "none",
322
+ background: "transparent",
323
+ cursor: "pointer",
324
+ fontSize: "1.25rem",
325
+ lineHeight: 1,
326
+ color: "#6b7280"
327
+ },
328
+ children: "\u2715"
329
+ }
330
+ ),
331
+ /* @__PURE__ */ jsx(
332
+ "h2",
333
+ {
334
+ ...titleProps,
335
+ style: {
336
+ fontSize: "1.25rem",
337
+ fontWeight: 600,
338
+ marginBottom: description ? "0.5rem" : "1rem",
339
+ paddingRight: showCloseButton ? "2rem" : 0
340
+ },
341
+ children: title
342
+ }
343
+ ),
344
+ descriptionProps && description && /* @__PURE__ */ jsx(
345
+ "p",
346
+ {
347
+ ...descriptionProps,
348
+ style: {
349
+ fontSize: "0.875rem",
350
+ color: "#6b7280",
351
+ marginBottom: "1rem"
352
+ },
353
+ children: description
354
+ }
355
+ ),
356
+ /* @__PURE__ */ jsx("div", { children })
357
+ ]
358
+ }
359
+ )
360
+ ] });
361
+ };
362
+ var useKeyboardNavigation = (props) => {
363
+ const {
364
+ orientation,
365
+ loop = false,
366
+ onNavigate,
367
+ defaultIndex = 0,
368
+ currentIndex: controlledIndex
369
+ } = props;
370
+ const isControlled = controlledIndex !== void 0;
371
+ const [uncontrolledIndex, setUncontrolledIndex] = useState(defaultIndex);
372
+ const currentIndex = isControlled ? controlledIndex : uncontrolledIndex;
373
+ const itemsRef = useRef(/* @__PURE__ */ new Map());
374
+ const containerRef = useRef(null);
375
+ const setCurrentIndex = useCallback(
376
+ (index) => {
377
+ if (!isControlled) {
378
+ setUncontrolledIndex(index);
379
+ }
380
+ onNavigate?.(index);
381
+ const element = itemsRef.current.get(index);
382
+ if (element) {
383
+ element.focus();
384
+ }
385
+ },
386
+ [isControlled, onNavigate]
387
+ );
388
+ const navigate = useCallback(
389
+ (direction) => {
390
+ const itemCount = itemsRef.current.size;
391
+ if (itemCount === 0) {
392
+ return;
393
+ }
394
+ let nextIndex = currentIndex;
395
+ switch (direction) {
396
+ case "forward":
397
+ nextIndex = currentIndex + 1;
398
+ if (nextIndex >= itemCount) {
399
+ nextIndex = loop ? 0 : itemCount - 1;
400
+ }
401
+ break;
402
+ case "backward":
403
+ nextIndex = currentIndex - 1;
404
+ if (nextIndex < 0) {
405
+ nextIndex = loop ? itemCount - 1 : 0;
406
+ }
407
+ break;
408
+ case "first":
409
+ nextIndex = 0;
410
+ break;
411
+ case "last":
412
+ nextIndex = itemCount - 1;
413
+ break;
414
+ }
415
+ if (nextIndex !== currentIndex) {
416
+ setCurrentIndex(nextIndex);
417
+ }
418
+ },
419
+ [currentIndex, loop, setCurrentIndex]
420
+ );
421
+ const handleKeyDown = useCallback(
422
+ (event) => {
423
+ const { key } = event;
424
+ let direction = null;
425
+ if (key === "ArrowRight") {
426
+ if (orientation === "horizontal" || orientation === "both") {
427
+ direction = "forward";
428
+ }
429
+ } else if (key === "ArrowLeft") {
430
+ if (orientation === "horizontal" || orientation === "both") {
431
+ direction = "backward";
432
+ }
433
+ } else if (key === "ArrowDown") {
434
+ if (orientation === "vertical" || orientation === "both") {
435
+ direction = "forward";
436
+ }
437
+ } else if (key === "ArrowUp") {
438
+ if (orientation === "vertical" || orientation === "both") {
439
+ direction = "backward";
440
+ }
441
+ } else if (key === "Home") {
442
+ direction = "first";
443
+ } else if (key === "End") {
444
+ direction = "last";
445
+ }
446
+ if (direction) {
447
+ event.preventDefault();
448
+ navigate(direction);
449
+ }
450
+ },
451
+ [orientation, navigate]
452
+ );
453
+ const getItemProps = useCallback(
454
+ (index) => {
455
+ return {
456
+ ref: (element) => {
457
+ if (element) {
458
+ itemsRef.current.set(index, element);
459
+ } else {
460
+ itemsRef.current.delete(index);
461
+ }
462
+ },
463
+ tabIndex: index === currentIndex ? 0 : -1,
464
+ onKeyDown: handleKeyDown,
465
+ "data-index": index
466
+ };
467
+ },
468
+ [currentIndex, handleKeyDown]
469
+ );
470
+ useEffect(() => {
471
+ {
472
+ import('@a13y/devtools/runtime/validators').then(({ keyboardValidator }) => {
473
+ if (containerRef.current) {
474
+ keyboardValidator.validateContainer(containerRef.current);
475
+ }
476
+ const container = Array.from(itemsRef.current.values())[0]?.parentElement;
477
+ if (container) {
478
+ keyboardValidator.validateRovingTabindex(container);
479
+ }
480
+ });
481
+ }
482
+ }, []);
483
+ const containerProps = {
484
+ role: "toolbar",
485
+ "aria-orientation": orientation
486
+ };
487
+ return {
488
+ currentIndex,
489
+ setCurrentIndex,
490
+ getItemProps,
491
+ containerProps
492
+ };
493
+ };
494
+ var AccessibleMenu = (props) => {
495
+ const { label, trigger, items, className = "", menuClassName = "" } = props;
496
+ const [isOpen, setIsOpen] = useState(false);
497
+ const { buttonProps: triggerProps } = useAccessibleButton({
498
+ label,
499
+ onPress: () => setIsOpen(!isOpen)
500
+ });
501
+ const { trapRef } = useFocusTrap({
502
+ isActive: isOpen,
503
+ onEscape: () => setIsOpen(false),
504
+ restoreFocus: true
505
+ });
506
+ const { getItemProps } = useKeyboardNavigation({
507
+ orientation: "vertical",
508
+ loop: true
509
+ });
510
+ const handleItemPress = (item) => {
511
+ if (item.disabled) {
512
+ return;
513
+ }
514
+ item.onPress();
515
+ setIsOpen(false);
516
+ };
517
+ return /* @__PURE__ */ jsxs("div", { style: { position: "relative", display: "inline-block" }, children: [
518
+ /* @__PURE__ */ jsx(
519
+ "button",
520
+ {
521
+ ...triggerProps,
522
+ className,
523
+ "aria-expanded": isOpen,
524
+ "aria-haspopup": "true",
525
+ style: {
526
+ padding: "0.5rem 1rem",
527
+ border: "1px solid #d1d5db",
528
+ borderRadius: "0.375rem",
529
+ backgroundColor: "white",
530
+ cursor: "pointer",
531
+ fontSize: "0.875rem",
532
+ fontWeight: 500
533
+ },
534
+ children: trigger
535
+ }
536
+ ),
537
+ isOpen && /* @__PURE__ */ jsx(
538
+ "div",
539
+ {
540
+ ref: trapRef,
541
+ role: "menu",
542
+ "aria-orientation": "vertical",
543
+ className: menuClassName,
544
+ style: {
545
+ position: "absolute",
546
+ top: "calc(100% + 0.25rem)",
547
+ left: 0,
548
+ minWidth: "12rem",
549
+ backgroundColor: "white",
550
+ border: "1px solid #e5e7eb",
551
+ borderRadius: "0.375rem",
552
+ boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1)",
553
+ padding: "0.25rem",
554
+ zIndex: 50
555
+ },
556
+ children: items.map((item, index) => {
557
+ const itemProps = getItemProps(index);
558
+ return /* @__PURE__ */ jsxs(
559
+ "button",
560
+ {
561
+ ...itemProps,
562
+ role: "menuitem",
563
+ disabled: item.disabled,
564
+ onClick: () => handleItemPress(item),
565
+ style: {
566
+ display: "flex",
567
+ alignItems: "center",
568
+ gap: "0.75rem",
569
+ width: "100%",
570
+ padding: "0.5rem 0.75rem",
571
+ border: "none",
572
+ background: "transparent",
573
+ textAlign: "left",
574
+ fontSize: "0.875rem",
575
+ cursor: item.disabled ? "not-allowed" : "pointer",
576
+ borderRadius: "0.25rem",
577
+ color: item.disabled ? "#9ca3af" : "#111827",
578
+ opacity: item.disabled ? 0.5 : 1
579
+ },
580
+ onMouseEnter: (e) => {
581
+ if (!item.disabled) {
582
+ e.currentTarget.style.backgroundColor = "#f3f4f6";
583
+ }
584
+ },
585
+ onMouseLeave: (e) => {
586
+ e.currentTarget.style.backgroundColor = "transparent";
587
+ },
588
+ children: [
589
+ item.icon && /* @__PURE__ */ jsx("span", { children: item.icon }),
590
+ /* @__PURE__ */ jsx("span", { children: item.label })
591
+ ]
592
+ },
593
+ item.id
594
+ );
595
+ })
596
+ }
597
+ )
598
+ ] });
599
+ };
600
+ var AccessibleModal = (props) => {
601
+ const {
602
+ isOpen,
603
+ onClose,
604
+ title,
605
+ children,
606
+ footer,
607
+ size = "md",
608
+ closeOnBackdropClick = false,
609
+ className = ""
610
+ } = props;
611
+ const { dialogProps, titleProps, backdropProps, close } = useAccessibleDialog({
612
+ isOpen,
613
+ onClose,
614
+ title,
615
+ role: "dialog",
616
+ isModal: true,
617
+ closeOnBackdropClick
618
+ });
619
+ if (!isOpen) {
620
+ return null;
621
+ }
622
+ const sizeStyles = {
623
+ sm: { maxWidth: "24rem" },
624
+ md: { maxWidth: "32rem" },
625
+ lg: { maxWidth: "48rem" },
626
+ xl: { maxWidth: "64rem" },
627
+ full: { maxWidth: "95vw", maxHeight: "95vh" }
628
+ };
629
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
630
+ backdropProps && /* @__PURE__ */ jsx(
631
+ "div",
632
+ {
633
+ ...backdropProps,
634
+ style: {
635
+ position: "fixed",
636
+ inset: 0,
637
+ backgroundColor: "rgba(0, 0, 0, 0.6)",
638
+ display: "flex",
639
+ alignItems: "center",
640
+ justifyContent: "center",
641
+ zIndex: 50
642
+ }
643
+ }
644
+ ),
645
+ /* @__PURE__ */ jsxs(
646
+ "div",
647
+ {
648
+ ref: dialogProps.ref,
649
+ role: dialogProps.role,
650
+ "aria-labelledby": dialogProps["aria-labelledby"],
651
+ "aria-describedby": dialogProps["aria-describedby"],
652
+ "aria-modal": dialogProps["aria-modal"],
653
+ tabIndex: dialogProps.tabIndex,
654
+ className,
655
+ style: {
656
+ position: "fixed",
657
+ top: "50%",
658
+ left: "50%",
659
+ transform: "translate(-50%, -50%)",
660
+ backgroundColor: "white",
661
+ borderRadius: "0.5rem",
662
+ boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
663
+ width: "90vw",
664
+ maxHeight: "90vh",
665
+ display: "flex",
666
+ flexDirection: "column",
667
+ zIndex: 51,
668
+ ...sizeStyles[size]
669
+ },
670
+ children: [
671
+ /* @__PURE__ */ jsxs(
672
+ "div",
673
+ {
674
+ style: {
675
+ padding: "1.5rem",
676
+ borderBottom: "1px solid #e5e7eb",
677
+ display: "flex",
678
+ alignItems: "center",
679
+ justifyContent: "space-between"
680
+ },
681
+ children: [
682
+ /* @__PURE__ */ jsx(
683
+ "h2",
684
+ {
685
+ ...titleProps,
686
+ style: {
687
+ fontSize: "1.25rem",
688
+ fontWeight: 600,
689
+ margin: 0
690
+ },
691
+ children: title
692
+ }
693
+ ),
694
+ /* @__PURE__ */ jsx(
695
+ "button",
696
+ {
697
+ type: "button",
698
+ onClick: close,
699
+ "aria-label": "Close modal",
700
+ style: {
701
+ padding: "0.5rem",
702
+ border: "none",
703
+ background: "transparent",
704
+ cursor: "pointer",
705
+ fontSize: "1.25rem",
706
+ lineHeight: 1,
707
+ color: "#6b7280",
708
+ borderRadius: "0.25rem"
709
+ },
710
+ children: "\u2715"
711
+ }
712
+ )
713
+ ]
714
+ }
715
+ ),
716
+ /* @__PURE__ */ jsx(
717
+ "div",
718
+ {
719
+ style: {
720
+ flex: 1,
721
+ overflowY: "auto",
722
+ padding: "1.5rem"
723
+ },
724
+ children
725
+ }
726
+ ),
727
+ footer && /* @__PURE__ */ jsx(
728
+ "div",
729
+ {
730
+ style: {
731
+ padding: "1.5rem",
732
+ borderTop: "1px solid #e5e7eb",
733
+ display: "flex",
734
+ gap: "0.75rem",
735
+ justifyContent: "flex-end"
736
+ },
737
+ children: footer
738
+ }
739
+ )
740
+ ]
741
+ }
742
+ )
743
+ ] });
744
+ };
745
+ var AccessibleTabs = (props) => {
746
+ const {
747
+ tabs,
748
+ defaultTab = 0,
749
+ selectedTab: controlledTab,
750
+ onTabChange,
751
+ className = "",
752
+ panelClassName = ""
753
+ } = props;
754
+ const isControlled = controlledTab !== void 0;
755
+ const [uncontrolledTab, setUncontrolledTab] = useState(defaultTab);
756
+ const selectedIndex = isControlled ? controlledTab : uncontrolledTab;
757
+ const { getItemProps, setCurrentIndex } = useKeyboardNavigation({
758
+ orientation: "horizontal",
759
+ loop: false,
760
+ currentIndex: selectedIndex,
761
+ onNavigate: (index) => {
762
+ if (tabs[index]?.disabled) {
763
+ return;
764
+ }
765
+ if (!isControlled) {
766
+ setUncontrolledTab(index);
767
+ }
768
+ onTabChange?.(index);
769
+ }
770
+ });
771
+ const handleTabClick = (index) => {
772
+ if (tabs[index]?.disabled) {
773
+ return;
774
+ }
775
+ setCurrentIndex(index);
776
+ if (!isControlled) {
777
+ setUncontrolledTab(index);
778
+ }
779
+ onTabChange?.(index);
780
+ };
781
+ const selectedTab = tabs[selectedIndex];
782
+ return /* @__PURE__ */ jsxs("div", { className, children: [
783
+ /* @__PURE__ */ jsx(
784
+ "div",
785
+ {
786
+ role: "tablist",
787
+ "aria-orientation": "horizontal",
788
+ style: {
789
+ display: "flex",
790
+ borderBottom: "2px solid #e5e7eb",
791
+ gap: "0.25rem"
792
+ },
793
+ children: tabs.map((tab, index) => {
794
+ const itemProps = getItemProps(index);
795
+ const isSelected = index === selectedIndex;
796
+ return /* @__PURE__ */ jsxs(
797
+ "button",
798
+ {
799
+ ...itemProps,
800
+ id: `tab-${tab.id}`,
801
+ role: "tab",
802
+ "aria-selected": isSelected,
803
+ "aria-controls": `panel-${tab.id}`,
804
+ disabled: tab.disabled,
805
+ onClick: () => handleTabClick(index),
806
+ style: {
807
+ display: "flex",
808
+ alignItems: "center",
809
+ gap: "0.5rem",
810
+ padding: "0.75rem 1rem",
811
+ border: "none",
812
+ background: "transparent",
813
+ cursor: tab.disabled ? "not-allowed" : "pointer",
814
+ fontSize: "0.875rem",
815
+ fontWeight: isSelected ? 600 : 400,
816
+ color: tab.disabled ? "#9ca3af" : isSelected ? "#2563eb" : "#6b7280",
817
+ borderBottom: isSelected ? "2px solid #2563eb" : "none",
818
+ marginBottom: "-2px",
819
+ opacity: tab.disabled ? 0.5 : 1,
820
+ transition: "color 0.2s"
821
+ },
822
+ children: [
823
+ tab.icon && /* @__PURE__ */ jsx("span", { children: tab.icon }),
824
+ /* @__PURE__ */ jsx("span", { children: tab.label })
825
+ ]
826
+ },
827
+ tab.id
828
+ );
829
+ })
830
+ }
831
+ ),
832
+ selectedTab && /* @__PURE__ */ jsx(
833
+ "div",
834
+ {
835
+ id: `panel-${selectedTab.id}`,
836
+ role: "tabpanel",
837
+ "aria-labelledby": `tab-${selectedTab.id}`,
838
+ className: panelClassName,
839
+ style: {
840
+ padding: "1.5rem"
841
+ },
842
+ children: selectedTab.content
843
+ }
844
+ )
845
+ ] });
846
+ };
847
+ var useAccessibleForm = (config) => {
848
+ const {
849
+ fields,
850
+ validate: formValidator,
851
+ onSubmit,
852
+ autoFocusError = true,
853
+ announceErrors = true,
854
+ validateOnBlur = true,
855
+ validateOnChange = true
856
+ } = config;
857
+ const initialValues = Object.keys(fields).reduce((acc, key) => {
858
+ acc[key] = fields[key].initialValue;
859
+ return acc;
860
+ }, {});
861
+ const [values, setValues] = useState(initialValues);
862
+ const [errors, setErrors] = useState({});
863
+ const [touched, setTouched] = useState({});
864
+ const [isSubmitting, setIsSubmitting] = useState(false);
865
+ const [hasSubmitted, setHasSubmitted] = useState(false);
866
+ const fieldRefs = useRef(/* @__PURE__ */ new Map());
867
+ const validateField = useCallback(
868
+ (name) => {
869
+ const fieldConfig = fields[name];
870
+ const value = values[name];
871
+ if (fieldConfig.required) {
872
+ const isEmpty = value === "" || value === null || value === void 0 || Array.isArray(value) && value.length === 0;
873
+ if (isEmpty) {
874
+ const errorMessage = fieldConfig.requiredMessage || `${String(name)} is required`;
875
+ setErrors((prev) => ({ ...prev, [name]: errorMessage }));
876
+ return false;
877
+ }
878
+ }
879
+ if (fieldConfig.validate) {
880
+ const result = fieldConfig.validate(value);
881
+ if (result !== true) {
882
+ setErrors((prev) => ({ ...prev, [name]: result }));
883
+ return false;
884
+ }
885
+ }
886
+ setErrors((prev) => {
887
+ const newErrors = { ...prev };
888
+ delete newErrors[name];
889
+ return newErrors;
890
+ });
891
+ return true;
892
+ },
893
+ [fields, values]
894
+ );
895
+ const validateForm = useCallback(() => {
896
+ let isValid2 = true;
897
+ const newErrors = {};
898
+ for (const name of Object.keys(fields)) {
899
+ const fieldConfig = fields[name];
900
+ const value = values[name];
901
+ if (fieldConfig.required) {
902
+ const isEmpty = value === "" || value === null || value === void 0 || Array.isArray(value) && value.length === 0;
903
+ if (isEmpty) {
904
+ const errorMessage = fieldConfig.requiredMessage || `${String(name)} is required`;
905
+ newErrors[name] = errorMessage;
906
+ isValid2 = false;
907
+ continue;
908
+ }
909
+ }
910
+ if (fieldConfig.validate) {
911
+ const result = fieldConfig.validate(value);
912
+ if (result !== true) {
913
+ newErrors[name] = result;
914
+ isValid2 = false;
915
+ }
916
+ }
917
+ }
918
+ if (formValidator) {
919
+ const formErrors = formValidator(values);
920
+ if (formErrors) {
921
+ Object.assign(newErrors, formErrors);
922
+ isValid2 = false;
923
+ }
924
+ }
925
+ setErrors(newErrors);
926
+ return isValid2;
927
+ }, [fields, values, formValidator]);
928
+ const setFieldValue = useCallback((name, value) => {
929
+ setValues((prev) => ({ ...prev, [name]: value }));
930
+ }, []);
931
+ const setFieldError = useCallback((name, error) => {
932
+ setErrors((prev) => ({ ...prev, [name]: error }));
933
+ }, []);
934
+ const getFieldProps = useCallback(
935
+ (name, options) => {
936
+ const fieldConfig = fields[name];
937
+ const hasError = !!errors[name];
938
+ const errorId = `${String(name)}-error`;
939
+ return {
940
+ name: String(name),
941
+ value: values[name],
942
+ onChange: (value) => {
943
+ setFieldValue(name, value);
944
+ if (validateOnChange && touched[name]) {
945
+ validateField(name);
946
+ }
947
+ },
948
+ onBlur: () => {
949
+ setTouched((prev) => ({ ...prev, [name]: true }));
950
+ if (validateOnBlur) {
951
+ validateField(name);
952
+ }
953
+ },
954
+ "aria-invalid": hasError,
955
+ "aria-describedby": hasError ? options?.["aria-describedby"] ? `${errorId} ${options["aria-describedby"]}` : errorId : options?.["aria-describedby"],
956
+ "aria-required": fieldConfig.required
957
+ };
958
+ },
959
+ [
960
+ fields,
961
+ values,
962
+ errors,
963
+ touched,
964
+ setFieldValue,
965
+ validateField,
966
+ validateOnBlur,
967
+ validateOnChange
968
+ ]
969
+ );
970
+ const handleSubmit = useCallback(
971
+ async (e) => {
972
+ e?.preventDefault();
973
+ setHasSubmitted(true);
974
+ const isValid2 = validateForm();
975
+ if (!isValid2) {
976
+ const errorCount = Object.keys(errors).length;
977
+ if (announceErrors) {
978
+ const errorMessage = errorCount === 1 ? "Form has 1 error. Please correct it and try again." : `Form has ${errorCount} errors. Please correct them and try again.`;
979
+ announce(errorMessage, { politeness: "assertive" });
980
+ }
981
+ if (autoFocusError) {
982
+ const firstErrorField = Object.keys(errors)[0];
983
+ const fieldElement = fieldRefs.current.get(firstErrorField);
984
+ if (fieldElement) {
985
+ fieldElement.focus();
986
+ }
987
+ }
988
+ return;
989
+ }
990
+ setIsSubmitting(true);
991
+ try {
992
+ await onSubmit(values);
993
+ announce("Form submitted successfully", { politeness: "polite" });
994
+ } catch (error) {
995
+ if (announceErrors) {
996
+ announce("Form submission failed. Please try again.", {
997
+ politeness: "assertive"
998
+ });
999
+ }
1000
+ throw error;
1001
+ } finally {
1002
+ setIsSubmitting(false);
1003
+ }
1004
+ },
1005
+ [validateForm, errors, announceErrors, autoFocusError, onSubmit, values]
1006
+ );
1007
+ const reset = useCallback(() => {
1008
+ setValues(initialValues);
1009
+ setErrors({});
1010
+ setTouched({});
1011
+ setHasSubmitted(false);
1012
+ }, [initialValues]);
1013
+ const clearErrors = useCallback(() => {
1014
+ setErrors({});
1015
+ }, []);
1016
+ const isValid = Object.keys(errors).length === 0;
1017
+ return {
1018
+ state: {
1019
+ values,
1020
+ errors,
1021
+ touched,
1022
+ isSubmitting,
1023
+ isValid,
1024
+ hasSubmitted
1025
+ },
1026
+ getFieldProps,
1027
+ setFieldValue,
1028
+ setFieldError,
1029
+ setErrors,
1030
+ validateField,
1031
+ validateForm,
1032
+ handleSubmit,
1033
+ reset,
1034
+ clearErrors,
1035
+ fieldRefs: fieldRefs.current
1036
+ };
1037
+ };
1038
+ var useFormField = (props) => {
1039
+ const {
1040
+ label,
1041
+ initialValue = "",
1042
+ validate: validator,
1043
+ required = false,
1044
+ requiredMessage,
1045
+ helpText,
1046
+ validateOnBlur = true,
1047
+ validateOnChange = true,
1048
+ announceErrors = true,
1049
+ onChange,
1050
+ onBlur
1051
+ } = props;
1052
+ {
1053
+ if (!label || label.trim().length === 0) {
1054
+ throw new Error('@a13y/react [useFormField]: "label" prop is required for accessibility');
1055
+ }
1056
+ }
1057
+ const [value, setValue] = useState(initialValue);
1058
+ const [error, setError] = useState(null);
1059
+ const [isTouched, setIsTouched] = useState(false);
1060
+ const fieldRef = useRef(null);
1061
+ const id = useId();
1062
+ const labelId = `${id}-label`;
1063
+ const errorId = `${id}-error`;
1064
+ const helpTextId = `${id}-help`;
1065
+ const validate = useCallback(() => {
1066
+ if (required) {
1067
+ const isEmpty = value === "" || value === null || value === void 0 || Array.isArray(value) && value.length === 0;
1068
+ if (isEmpty) {
1069
+ const errorMessage = requiredMessage || `${label} is required`;
1070
+ setError(errorMessage);
1071
+ return false;
1072
+ }
1073
+ }
1074
+ if (validator) {
1075
+ const result = validator(value);
1076
+ if (result !== true) {
1077
+ setError(result);
1078
+ return false;
1079
+ }
1080
+ }
1081
+ setError(null);
1082
+ return true;
1083
+ }, [value, required, validator, label, requiredMessage]);
1084
+ useEffect(() => {
1085
+ if (error && isTouched && announceErrors) {
1086
+ announce(error, { politeness: "assertive", delay: 100 });
1087
+ }
1088
+ }, [error, isTouched, announceErrors]);
1089
+ const handleChange = useCallback(
1090
+ (newValue) => {
1091
+ setValue(newValue);
1092
+ onChange?.(newValue);
1093
+ if (validateOnChange && isTouched) {
1094
+ setTimeout(() => validate(), 0);
1095
+ }
1096
+ },
1097
+ [onChange, validateOnChange, isTouched, validate]
1098
+ );
1099
+ const handleBlur = useCallback(() => {
1100
+ setIsTouched(true);
1101
+ onBlur?.();
1102
+ if (validateOnBlur) {
1103
+ validate();
1104
+ }
1105
+ }, [onBlur, validateOnBlur, validate]);
1106
+ const clearError = useCallback(() => {
1107
+ setError(null);
1108
+ }, []);
1109
+ const reset = useCallback(() => {
1110
+ setValue(initialValue);
1111
+ setError(null);
1112
+ setIsTouched(false);
1113
+ }, [initialValue]);
1114
+ const isValid = error === null;
1115
+ const describedBy = [helpText ? helpTextId : null, error ? errorId : null].filter(Boolean).join(" ");
1116
+ return {
1117
+ id,
1118
+ labelId,
1119
+ errorId,
1120
+ helpTextId,
1121
+ value,
1122
+ error,
1123
+ isTouched,
1124
+ isValid,
1125
+ setValue: handleChange,
1126
+ setError,
1127
+ validate,
1128
+ clearError,
1129
+ reset,
1130
+ labelProps: {
1131
+ id: labelId,
1132
+ htmlFor: id
1133
+ },
1134
+ inputProps: {
1135
+ id,
1136
+ name: id,
1137
+ value,
1138
+ onChange: handleChange,
1139
+ onBlur: handleBlur,
1140
+ "aria-labelledby": labelId,
1141
+ "aria-describedby": describedBy || void 0,
1142
+ "aria-invalid": !isValid,
1143
+ "aria-required": required ? true : void 0,
1144
+ ref: fieldRef
1145
+ },
1146
+ errorProps: {
1147
+ id: errorId,
1148
+ role: "alert",
1149
+ "aria-live": "polite"
1150
+ },
1151
+ helpTextProps: {
1152
+ id: helpTextId
1153
+ },
1154
+ fieldRef
1155
+ };
1156
+ };
1157
+ var DialogStackContext = createContext(null);
1158
+ var useDialogStack = () => {
1159
+ const context = useContext(DialogStackContext);
1160
+ if (!context) {
1161
+ throw new Error("useDialogStack must be used within DialogStackProvider");
1162
+ }
1163
+ return context;
1164
+ };
1165
+ var DialogStackProvider = (props) => {
1166
+ const { children, baseZIndex = 1e3, zIndexIncrement = 10 } = props;
1167
+ const [stack, setStack] = useState([]);
1168
+ const push = (dialog) => {
1169
+ const zIndex = baseZIndex + stack.length * zIndexIncrement;
1170
+ setStack((prev) => [...prev, { ...dialog, zIndex }]);
1171
+ };
1172
+ const pop = () => {
1173
+ setStack((prev) => {
1174
+ const newStack = [...prev];
1175
+ const dialog = newStack.pop();
1176
+ dialog?.onClose();
1177
+ return newStack;
1178
+ });
1179
+ };
1180
+ const close = (id) => {
1181
+ setStack((prev) => {
1182
+ const index = prev.findIndex((d) => d.id === id);
1183
+ if (index === -1) return prev;
1184
+ const closedDialogs = prev.slice(index);
1185
+ for (const d of closedDialogs) {
1186
+ d.onClose();
1187
+ }
1188
+ return prev.slice(0, index);
1189
+ });
1190
+ };
1191
+ const closeAll = () => {
1192
+ for (const d of stack) {
1193
+ d.onClose();
1194
+ }
1195
+ setStack([]);
1196
+ };
1197
+ useEffect(() => {
1198
+ if (stack.length > 0) {
1199
+ const originalOverflow = document.body.style.overflow;
1200
+ document.body.style.overflow = "hidden";
1201
+ return () => {
1202
+ document.body.style.overflow = originalOverflow;
1203
+ };
1204
+ }
1205
+ }, [stack.length]);
1206
+ const contextValue = {
1207
+ push,
1208
+ pop,
1209
+ close,
1210
+ closeAll,
1211
+ depth: stack.length
1212
+ };
1213
+ return /* @__PURE__ */ jsxs(DialogStackContext.Provider, { value: contextValue, children: [
1214
+ children,
1215
+ stack.map((dialog) => /* @__PURE__ */ jsx("div", { style: { zIndex: dialog.zIndex }, children: /* @__PURE__ */ jsx(
1216
+ AccessibleDialog,
1217
+ {
1218
+ isOpen: true,
1219
+ onClose: () => close(dialog.id),
1220
+ title: dialog.title,
1221
+ description: dialog.description,
1222
+ backdropClassName: "dialog-stack-backdrop",
1223
+ children: dialog.content
1224
+ }
1225
+ ) }, dialog.id))
1226
+ ] });
1227
+ };
1228
+ var InfiniteList = (props) => {
1229
+ const {
1230
+ items,
1231
+ loadMore,
1232
+ hasMore,
1233
+ isLoading,
1234
+ renderItem,
1235
+ getItemKey,
1236
+ loadingIndicator,
1237
+ emptyState,
1238
+ "aria-label": ariaLabel,
1239
+ threshold = 200,
1240
+ className = ""
1241
+ } = props;
1242
+ const [previousCount, setPreviousCount] = useState(items.length);
1243
+ const sentinelRef = useRef(null);
1244
+ const listRef = useRef(null);
1245
+ useEffect(() => {
1246
+ if (items.length > previousCount && !isLoading) {
1247
+ const newItemsCount = items.length - previousCount;
1248
+ announce(
1249
+ `${newItemsCount} new item${newItemsCount === 1 ? "" : "s"} loaded. Total: ${items.length}`,
1250
+ { politeness: "polite", delay: 500 }
1251
+ );
1252
+ setPreviousCount(items.length);
1253
+ }
1254
+ }, [items.length, previousCount, isLoading]);
1255
+ useEffect(() => {
1256
+ if (isLoading) {
1257
+ announce("Loading more items", { politeness: "polite" });
1258
+ }
1259
+ }, [isLoading]);
1260
+ useEffect(() => {
1261
+ if (!hasMore || isLoading || !sentinelRef.current) {
1262
+ return;
1263
+ }
1264
+ const observer = new IntersectionObserver(
1265
+ (entries) => {
1266
+ const sentinel = entries[0];
1267
+ if (sentinel?.isIntersecting) {
1268
+ loadMore();
1269
+ }
1270
+ },
1271
+ {
1272
+ root: null,
1273
+ rootMargin: `${threshold}px`,
1274
+ threshold: 0
1275
+ }
1276
+ );
1277
+ observer.observe(sentinelRef.current);
1278
+ return () => observer.disconnect();
1279
+ }, [hasMore, isLoading, loadMore, threshold]);
1280
+ if (items.length === 0 && !isLoading) {
1281
+ return /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", children: emptyState || /* @__PURE__ */ jsx("p", { children: "No items to display" }) });
1282
+ }
1283
+ return /* @__PURE__ */ jsxs("div", { ref: listRef, className, children: [
1284
+ /* @__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))) }),
1285
+ isLoading && /* @__PURE__ */ jsx(
1286
+ "div",
1287
+ {
1288
+ role: "status",
1289
+ "aria-live": "polite",
1290
+ "aria-label": "Loading more items",
1291
+ style: {
1292
+ padding: "1rem",
1293
+ textAlign: "center"
1294
+ },
1295
+ children: loadingIndicator || /* @__PURE__ */ jsx("span", { children: "Loading..." })
1296
+ }
1297
+ ),
1298
+ hasMore && !isLoading && /* @__PURE__ */ jsx("div", { ref: sentinelRef, "aria-hidden": "true", style: { height: "1px", visibility: "hidden" } }),
1299
+ !hasMore && items.length > 0 && /* @__PURE__ */ jsxs(
1300
+ "div",
1301
+ {
1302
+ role: "status",
1303
+ "aria-live": "polite",
1304
+ style: {
1305
+ padding: "1rem",
1306
+ textAlign: "center",
1307
+ color: "#6b7280",
1308
+ fontSize: "0.875rem"
1309
+ },
1310
+ children: [
1311
+ "End of list. Total: ",
1312
+ items.length,
1313
+ " items."
1314
+ ]
1315
+ }
1316
+ )
1317
+ ] });
1318
+ };
1319
+ var NestedMenu = (props) => {
1320
+ const { label, trigger, items, className = "" } = props;
1321
+ const [isOpen, setIsOpen] = useState(false);
1322
+ const [openSubmenuId, setOpenSubmenuId] = useState(null);
1323
+ const { buttonProps } = useAccessibleButton({
1324
+ label,
1325
+ onPress: () => setIsOpen(!isOpen)
1326
+ });
1327
+ useEffect(() => {
1328
+ const handleEscape = (e) => {
1329
+ if (e.key === "Escape" && isOpen) {
1330
+ setIsOpen(false);
1331
+ setOpenSubmenuId(null);
1332
+ }
1333
+ };
1334
+ document.addEventListener("keydown", handleEscape);
1335
+ return () => document.removeEventListener("keydown", handleEscape);
1336
+ }, [isOpen]);
1337
+ return /* @__PURE__ */ jsxs("div", { style: { position: "relative", display: "inline-block" }, children: [
1338
+ /* @__PURE__ */ jsx(
1339
+ "button",
1340
+ {
1341
+ ...buttonProps,
1342
+ "aria-expanded": isOpen,
1343
+ "aria-haspopup": "true",
1344
+ className,
1345
+ style: {
1346
+ padding: "0.5rem 1rem",
1347
+ border: "1px solid #d1d5db",
1348
+ borderRadius: "0.375rem",
1349
+ backgroundColor: "white",
1350
+ cursor: "pointer"
1351
+ },
1352
+ children: trigger
1353
+ }
1354
+ ),
1355
+ isOpen && /* @__PURE__ */ jsx(
1356
+ MenuLevel,
1357
+ {
1358
+ items,
1359
+ onClose: () => setIsOpen(false),
1360
+ openSubmenuId,
1361
+ onSubmenuChange: setOpenSubmenuId,
1362
+ depth: 0
1363
+ }
1364
+ )
1365
+ ] });
1366
+ };
1367
+ var MenuLevel = (props) => {
1368
+ const { items, onClose, openSubmenuId, onSubmenuChange, depth } = props;
1369
+ const menuRef = useRef(null);
1370
+ const { getItemProps, setCurrentIndex } = useKeyboardNavigation({
1371
+ orientation: "vertical",
1372
+ loop: true
1373
+ });
1374
+ const handleItemKeyDown = (e, item) => {
1375
+ if (e.key === "ArrowRight" && item.submenu) {
1376
+ e.preventDefault();
1377
+ e.stopPropagation();
1378
+ onSubmenuChange(item.id);
1379
+ }
1380
+ if (e.key === "ArrowLeft" && depth > 0) {
1381
+ e.preventDefault();
1382
+ e.stopPropagation();
1383
+ onSubmenuChange(null);
1384
+ }
1385
+ if (e.key === "Enter" || e.key === " ") {
1386
+ e.preventDefault();
1387
+ e.stopPropagation();
1388
+ if (item.submenu) {
1389
+ onSubmenuChange(item.id);
1390
+ } else if (item.onPress && !item.disabled) {
1391
+ item.onPress();
1392
+ onClose();
1393
+ }
1394
+ }
1395
+ };
1396
+ const handleItemClick = (item) => {
1397
+ if (item.disabled) return;
1398
+ if (item.submenu) {
1399
+ onSubmenuChange(openSubmenuId === item.id ? null : item.id);
1400
+ } else if (item.onPress) {
1401
+ item.onPress();
1402
+ onClose();
1403
+ }
1404
+ };
1405
+ return /* @__PURE__ */ jsx(
1406
+ "div",
1407
+ {
1408
+ ref: menuRef,
1409
+ role: "menu",
1410
+ "aria-orientation": "vertical",
1411
+ style: {
1412
+ position: depth === 0 ? "absolute" : "absolute",
1413
+ top: depth === 0 ? "calc(100% + 0.25rem)" : 0,
1414
+ left: depth === 0 ? 0 : "100%",
1415
+ minWidth: "12rem",
1416
+ backgroundColor: "white",
1417
+ border: "1px solid #e5e7eb",
1418
+ borderRadius: "0.375rem",
1419
+ boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1)",
1420
+ padding: "0.25rem",
1421
+ zIndex: 50 + depth
1422
+ },
1423
+ children: items.map((item, index) => {
1424
+ const itemProps = getItemProps(index);
1425
+ const hasSubmenu = !!item.submenu;
1426
+ const isSubmenuOpen = openSubmenuId === item.id;
1427
+ return /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
1428
+ /* @__PURE__ */ jsxs(
1429
+ "button",
1430
+ {
1431
+ ...itemProps,
1432
+ role: "menuitem",
1433
+ "aria-haspopup": hasSubmenu ? "true" : void 0,
1434
+ "aria-expanded": hasSubmenu ? isSubmenuOpen : void 0,
1435
+ disabled: item.disabled,
1436
+ onClick: () => handleItemClick(item),
1437
+ onKeyDown: (e) => handleItemKeyDown(e, item),
1438
+ style: {
1439
+ display: "flex",
1440
+ alignItems: "center",
1441
+ justifyContent: "space-between",
1442
+ gap: "0.75rem",
1443
+ width: "100%",
1444
+ padding: "0.5rem 0.75rem",
1445
+ border: "none",
1446
+ background: "transparent",
1447
+ textAlign: "left",
1448
+ fontSize: "0.875rem",
1449
+ cursor: item.disabled ? "not-allowed" : "pointer",
1450
+ borderRadius: "0.25rem",
1451
+ color: item.disabled ? "#9ca3af" : "#111827",
1452
+ opacity: item.disabled ? 0.5 : 1
1453
+ },
1454
+ onMouseEnter: (e) => {
1455
+ if (!item.disabled) {
1456
+ e.currentTarget.style.backgroundColor = "#f3f4f6";
1457
+ setCurrentIndex(index);
1458
+ if (hasSubmenu) {
1459
+ onSubmenuChange(item.id);
1460
+ }
1461
+ }
1462
+ },
1463
+ onMouseLeave: (e) => {
1464
+ e.currentTarget.style.backgroundColor = "transparent";
1465
+ },
1466
+ children: [
1467
+ /* @__PURE__ */ jsxs("span", { style: { display: "flex", alignItems: "center", gap: "0.5rem" }, children: [
1468
+ item.icon && /* @__PURE__ */ jsx("span", { children: item.icon }),
1469
+ /* @__PURE__ */ jsx("span", { children: item.label })
1470
+ ] }),
1471
+ hasSubmenu && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u25B6" })
1472
+ ]
1473
+ }
1474
+ ),
1475
+ hasSubmenu && isSubmenuOpen && item.submenu && /* @__PURE__ */ jsx(
1476
+ MenuLevel,
1477
+ {
1478
+ items: item.submenu,
1479
+ onClose,
1480
+ openSubmenuId: null,
1481
+ onSubmenuChange: () => {
1482
+ },
1483
+ depth: depth + 1
1484
+ }
1485
+ )
1486
+ ] }, item.id);
1487
+ })
1488
+ }
1489
+ );
1490
+ };
1491
+ var VirtualizedList = (props) => {
1492
+ const {
1493
+ items,
1494
+ itemHeight,
1495
+ height,
1496
+ renderItem,
1497
+ getItemKey,
1498
+ "aria-label": ariaLabel,
1499
+ overscan = 3,
1500
+ className = "",
1501
+ emptyState
1502
+ } = props;
1503
+ const [scrollTop, setScrollTop] = useState(0);
1504
+ const [visibleRange, setVisibleRange] = useState({ start: 0, end: 0 });
1505
+ const containerRef = useRef(null);
1506
+ const previousRangeRef = useRef({ start: 0, end: 0 });
1507
+ const totalHeight = items.length * itemHeight;
1508
+ const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
1509
+ const endIndex = Math.min(
1510
+ items.length - 1,
1511
+ Math.ceil((scrollTop + height) / itemHeight) + overscan
1512
+ );
1513
+ const visibleItems = items.slice(startIndex, endIndex + 1);
1514
+ useEffect(() => {
1515
+ const newRange = { start: startIndex, end: endIndex };
1516
+ const rangeChanged = Math.abs(newRange.start - previousRangeRef.current.start) > 10 || Math.abs(newRange.end - previousRangeRef.current.end) > 10;
1517
+ if (rangeChanged && items.length > 0) {
1518
+ setVisibleRange(newRange);
1519
+ previousRangeRef.current = newRange;
1520
+ const message = `Showing items ${newRange.start + 1} to ${newRange.end + 1} of ${items.length}`;
1521
+ announce(message, { politeness: "polite", delay: 300 });
1522
+ }
1523
+ }, [startIndex, endIndex, items.length]);
1524
+ const handleScroll = (e) => {
1525
+ setScrollTop(e.currentTarget.scrollTop);
1526
+ };
1527
+ const handleKeyDown = (e) => {
1528
+ if (!containerRef.current) return;
1529
+ const scrollAmount = itemHeight * 5;
1530
+ switch (e.key) {
1531
+ case "PageDown":
1532
+ e.preventDefault();
1533
+ containerRef.current.scrollBy({ top: scrollAmount, behavior: "smooth" });
1534
+ break;
1535
+ case "PageUp":
1536
+ e.preventDefault();
1537
+ containerRef.current.scrollBy({ top: -scrollAmount, behavior: "smooth" });
1538
+ break;
1539
+ case "Home":
1540
+ e.preventDefault();
1541
+ containerRef.current.scrollTo({ top: 0, behavior: "smooth" });
1542
+ break;
1543
+ case "End":
1544
+ e.preventDefault();
1545
+ containerRef.current.scrollTo({ top: totalHeight, behavior: "smooth" });
1546
+ break;
1547
+ }
1548
+ };
1549
+ if (items.length === 0) {
1550
+ return /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", children: emptyState || /* @__PURE__ */ jsx("p", { children: "No items to display" }) });
1551
+ }
1552
+ return /* @__PURE__ */ jsxs(
1553
+ "div",
1554
+ {
1555
+ ref: containerRef,
1556
+ role: "list",
1557
+ "aria-label": ariaLabel,
1558
+ tabIndex: 0,
1559
+ className,
1560
+ onScroll: handleScroll,
1561
+ onKeyDown: handleKeyDown,
1562
+ style: {
1563
+ height: `${height}px`,
1564
+ overflow: "auto",
1565
+ position: "relative",
1566
+ outline: "none"
1567
+ },
1568
+ children: [
1569
+ /* @__PURE__ */ jsx("div", { style: { height: `${totalHeight}px`, position: "relative" }, children: visibleItems.map((item, virtualIndex) => {
1570
+ const actualIndex = startIndex + virtualIndex;
1571
+ const itemStyle = {
1572
+ position: "absolute",
1573
+ top: `${actualIndex * itemHeight}px`,
1574
+ left: 0,
1575
+ right: 0,
1576
+ height: `${itemHeight}px`
1577
+ };
1578
+ return /* @__PURE__ */ jsx(
1579
+ "div",
1580
+ {
1581
+ role: "listitem",
1582
+ "aria-setsize": items.length,
1583
+ "aria-posinset": actualIndex + 1,
1584
+ style: itemStyle,
1585
+ children: renderItem(item, actualIndex)
1586
+ },
1587
+ getItemKey(item, actualIndex)
1588
+ );
1589
+ }) }),
1590
+ /* @__PURE__ */ jsx(
1591
+ "div",
1592
+ {
1593
+ "aria-live": "polite",
1594
+ "aria-atomic": "true",
1595
+ style: {
1596
+ position: "absolute",
1597
+ left: "-10000px",
1598
+ width: "1px",
1599
+ height: "1px",
1600
+ overflow: "hidden"
1601
+ },
1602
+ children: `Showing items ${visibleRange.start + 1} to ${visibleRange.end + 1} of ${items.length}`
1603
+ }
1604
+ )
1605
+ ]
1606
+ }
1607
+ );
1608
+ };
1609
+ var WizardContext = createContext(null);
1610
+ var useWizard = () => {
1611
+ const context = useContext(WizardContext);
1612
+ if (!context) {
1613
+ throw new Error("useWizard must be used within Wizard component");
1614
+ }
1615
+ return context;
1616
+ };
1617
+ var Wizard = (props) => {
1618
+ const { steps, onComplete, onCancel, initialStep = 0, className = "" } = props;
1619
+ const [currentStep, setCurrentStep] = useState(initialStep);
1620
+ const [validationError, setValidationError] = useState(null);
1621
+ const [visitedSteps, setVisitedSteps] = useState(/* @__PURE__ */ new Set([initialStep]));
1622
+ const totalSteps = steps.length;
1623
+ const step = steps[currentStep] ?? steps[0];
1624
+ if (currentStep < 0 || currentStep >= totalSteps) {
1625
+ setCurrentStep(0);
1626
+ }
1627
+ const isLastStep = currentStep === totalSteps - 1;
1628
+ const canGoPrevious = currentStep > 0;
1629
+ const canGoNext = currentStep < totalSteps - 1;
1630
+ const validateCurrentStep = () => {
1631
+ if (!step.validate) return true;
1632
+ const result = step.validate();
1633
+ if (result === true) {
1634
+ setValidationError(null);
1635
+ return true;
1636
+ }
1637
+ setValidationError(result);
1638
+ announce(`Validation error: ${result}`, { politeness: "assertive" });
1639
+ return false;
1640
+ };
1641
+ const goToStep = (index) => {
1642
+ if (index < 0 || index >= totalSteps) return;
1643
+ if (index > currentStep && !validateCurrentStep()) {
1644
+ return;
1645
+ }
1646
+ setCurrentStep(index);
1647
+ setVisitedSteps((prev) => /* @__PURE__ */ new Set([...prev, index]));
1648
+ setValidationError(null);
1649
+ const newStep = steps[index];
1650
+ if (newStep) {
1651
+ announce(
1652
+ `Step ${index + 1} of ${totalSteps}: ${newStep.label}${newStep.optional ? " (optional)" : ""}`,
1653
+ { politeness: "polite" }
1654
+ );
1655
+ }
1656
+ };
1657
+ const next = () => {
1658
+ if (!canGoNext) return;
1659
+ if (validateCurrentStep()) {
1660
+ goToStep(currentStep + 1);
1661
+ }
1662
+ };
1663
+ const previous = () => {
1664
+ if (!canGoPrevious) return;
1665
+ goToStep(currentStep - 1);
1666
+ };
1667
+ const handleComplete = () => {
1668
+ if (validateCurrentStep()) {
1669
+ announce("Wizard completed", { politeness: "polite" });
1670
+ onComplete();
1671
+ }
1672
+ };
1673
+ const contextValue = {
1674
+ currentStep,
1675
+ totalSteps,
1676
+ step,
1677
+ next,
1678
+ previous,
1679
+ goToStep,
1680
+ canGoNext,
1681
+ canGoPrevious,
1682
+ isLastStep,
1683
+ validationError
1684
+ };
1685
+ return /* @__PURE__ */ jsx(WizardContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs("div", { className, style: { maxWidth: "48rem", margin: "0 auto" }, children: [
1686
+ /* @__PURE__ */ jsx("nav", { "aria-label": "Wizard progress", children: /* @__PURE__ */ jsx(
1687
+ "ol",
1688
+ {
1689
+ style: {
1690
+ display: "flex",
1691
+ justifyContent: "space-between",
1692
+ marginBottom: "2rem",
1693
+ padding: 0,
1694
+ listStyle: "none"
1695
+ },
1696
+ children: steps.map((s, index) => {
1697
+ const isActive = index === currentStep;
1698
+ const isCompleted = visitedSteps.has(index) && index < currentStep;
1699
+ const isClickable = visitedSteps.has(index) || index === currentStep;
1700
+ return /* @__PURE__ */ jsxs(
1701
+ "li",
1702
+ {
1703
+ style: {
1704
+ flex: 1,
1705
+ display: "flex",
1706
+ flexDirection: "column",
1707
+ alignItems: "center"
1708
+ },
1709
+ children: [
1710
+ /* @__PURE__ */ jsx(
1711
+ "button",
1712
+ {
1713
+ type: "button",
1714
+ onClick: () => isClickable && goToStep(index),
1715
+ "aria-current": isActive ? "step" : void 0,
1716
+ disabled: !isClickable,
1717
+ style: {
1718
+ width: "2.5rem",
1719
+ height: "2.5rem",
1720
+ borderRadius: "50%",
1721
+ border: `2px solid ${isActive || isCompleted ? "#2563eb" : "#d1d5db"}`,
1722
+ backgroundColor: isCompleted ? "#2563eb" : isActive ? "white" : "#f3f4f6",
1723
+ color: isCompleted ? "white" : isActive ? "#2563eb" : "#9ca3af",
1724
+ fontWeight: 600,
1725
+ cursor: isClickable ? "pointer" : "not-allowed",
1726
+ marginBottom: "0.5rem"
1727
+ },
1728
+ children: isCompleted ? "\u2713" : index + 1
1729
+ }
1730
+ ),
1731
+ /* @__PURE__ */ jsxs(
1732
+ "span",
1733
+ {
1734
+ style: {
1735
+ fontSize: "0.875rem",
1736
+ color: isActive ? "#2563eb" : "#6b7280",
1737
+ fontWeight: isActive ? 600 : 400,
1738
+ textAlign: "center"
1739
+ },
1740
+ children: [
1741
+ s.label,
1742
+ s.optional && /* @__PURE__ */ jsx("span", { style: { display: "block", fontSize: "0.75rem", color: "#9ca3af" }, children: "(Optional)" })
1743
+ ]
1744
+ }
1745
+ )
1746
+ ]
1747
+ },
1748
+ s.id
1749
+ );
1750
+ })
1751
+ }
1752
+ ) }),
1753
+ validationError && /* @__PURE__ */ jsx(
1754
+ "div",
1755
+ {
1756
+ role: "alert",
1757
+ "aria-live": "assertive",
1758
+ style: {
1759
+ padding: "0.75rem",
1760
+ marginBottom: "1rem",
1761
+ backgroundColor: "#fef2f2",
1762
+ border: "1px solid #fecaca",
1763
+ borderRadius: "0.375rem",
1764
+ color: "#991b1b"
1765
+ },
1766
+ children: validationError
1767
+ }
1768
+ ),
1769
+ /* @__PURE__ */ jsxs(
1770
+ "div",
1771
+ {
1772
+ role: "region",
1773
+ "aria-labelledby": `step-${step.id}-label`,
1774
+ style: { marginBottom: "2rem" },
1775
+ children: [
1776
+ /* @__PURE__ */ jsx(
1777
+ "h2",
1778
+ {
1779
+ id: `step-${step.id}-label`,
1780
+ style: { fontSize: "1.5rem", fontWeight: 600, marginBottom: "1rem" },
1781
+ children: step.label
1782
+ }
1783
+ ),
1784
+ step.content
1785
+ ]
1786
+ }
1787
+ ),
1788
+ /* @__PURE__ */ jsxs(
1789
+ "div",
1790
+ {
1791
+ style: {
1792
+ display: "flex",
1793
+ justifyContent: "space-between",
1794
+ gap: "1rem"
1795
+ },
1796
+ children: [
1797
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "0.5rem" }, children: [
1798
+ onCancel && /* @__PURE__ */ jsx(
1799
+ "button",
1800
+ {
1801
+ type: "button",
1802
+ onClick: onCancel,
1803
+ style: {
1804
+ padding: "0.5rem 1rem",
1805
+ border: "1px solid #d1d5db",
1806
+ borderRadius: "0.375rem",
1807
+ backgroundColor: "white",
1808
+ cursor: "pointer"
1809
+ },
1810
+ children: "Cancel"
1811
+ }
1812
+ ),
1813
+ canGoPrevious && /* @__PURE__ */ jsx(
1814
+ "button",
1815
+ {
1816
+ type: "button",
1817
+ onClick: previous,
1818
+ style: {
1819
+ padding: "0.5rem 1rem",
1820
+ border: "1px solid #d1d5db",
1821
+ borderRadius: "0.375rem",
1822
+ backgroundColor: "white",
1823
+ cursor: "pointer"
1824
+ },
1825
+ children: "\u2190 Previous"
1826
+ }
1827
+ )
1828
+ ] }),
1829
+ /* @__PURE__ */ jsx("div", { children: isLastStep ? /* @__PURE__ */ jsx(
1830
+ "button",
1831
+ {
1832
+ type: "button",
1833
+ onClick: handleComplete,
1834
+ style: {
1835
+ padding: "0.5rem 1.5rem",
1836
+ border: "none",
1837
+ borderRadius: "0.375rem",
1838
+ backgroundColor: "#2563eb",
1839
+ color: "white",
1840
+ fontWeight: 600,
1841
+ cursor: "pointer"
1842
+ },
1843
+ children: "Complete"
1844
+ }
1845
+ ) : /* @__PURE__ */ jsx(
1846
+ "button",
1847
+ {
1848
+ type: "button",
1849
+ onClick: next,
1850
+ disabled: !canGoNext,
1851
+ style: {
1852
+ padding: "0.5rem 1.5rem",
1853
+ border: "none",
1854
+ borderRadius: "0.375rem",
1855
+ backgroundColor: "#2563eb",
1856
+ color: "white",
1857
+ fontWeight: 600,
1858
+ cursor: canGoNext ? "pointer" : "not-allowed",
1859
+ opacity: canGoNext ? 1 : 0.5
1860
+ },
1861
+ children: "Next \u2192"
1862
+ }
1863
+ ) })
1864
+ ]
1865
+ }
1866
+ )
1867
+ ] }) });
1868
+ };
1869
+
1870
+ // src/index.ts
1871
+ var VERSION = "0.0.0";
1872
+
1873
+ export { AccessibleButton, AccessibleDialog, AccessibleMenu, AccessibleModal, AccessibleTabs, DialogStackProvider, InfiniteList, NestedMenu, VERSION, VirtualizedList, Wizard, useAccessibleButton, useAccessibleDialog, useAccessibleForm, useDialogStack, useFocusTrap, useFormField, useKeyboardNavigation, useWizard };
1874
+ //# sourceMappingURL=index.js.map
1875
+ //# sourceMappingURL=index.js.map