@passui/react 0.1.45 → 0.1.46

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 (140) hide show
  1. package/README.md +57 -46
  2. package/dist/types/src/components/accordion/accordion.d.ts +1 -1
  3. package/dist/types/src/components/breadcrumb/breadcrumb.d.ts +1 -1
  4. package/dist/types/src/components/inline-disclosure/inline-disclosure.d.ts +1 -1
  5. package/dist/types/src/components/popover/anchored-surface.d.ts +5 -1
  6. package/dist/types/src/components/popover/index.d.ts +41 -0
  7. package/dist/types/src/components/popover/popover-content.d.ts +32 -0
  8. package/dist/types/src/components/popover/popover-root.d.ts +22 -0
  9. package/dist/types/src/components/popover/popover-trigger.d.ts +23 -0
  10. package/dist/types/src/components/stepper/rolling-digit.d.ts +1 -1
  11. package/dist/types/src/components/table/table.d.ts +1 -1
  12. package/dist/types/src/components/view-controller/view-controller.d.ts +2 -2
  13. package/dist/types/src/index.d.ts +1 -1
  14. package/dist/types/src/machines/popover.machine.d.ts +30 -0
  15. package/package.json +307 -307
  16. package/src/appearance/index.ts +2 -2
  17. package/src/appearance/platform.tsx +76 -60
  18. package/src/appearance/theme.tsx +409 -377
  19. package/src/components/accordion/accordion.tsx +58 -49
  20. package/src/components/ambient/ambient.tsx +25 -25
  21. package/src/components/ambient/index.ts +1 -1
  22. package/src/components/aspect-ratio/aspect-ratio.tsx +21 -16
  23. package/src/components/breadcrumb/breadcrumb.tsx +63 -57
  24. package/src/components/button/button-styles.ts +39 -32
  25. package/src/components/button/button.tsx +91 -84
  26. package/src/components/button/icon-button.tsx +27 -25
  27. package/src/components/button/pending-spinner.tsx +8 -8
  28. package/src/components/button/use-pending-indicator.ts +11 -11
  29. package/src/components/chip/chip.tsx +35 -30
  30. package/src/components/code-block/code-block.tsx +40 -39
  31. package/src/components/combo-box/combo-box.tsx +5 -5
  32. package/src/components/command/command.tsx +124 -94
  33. package/src/components/compact-date-picker/calendar-month.ts +75 -53
  34. package/src/components/compact-date-picker/compact-date-picker.tsx +308 -289
  35. package/src/components/compact-time-picker/compact-time-picker.tsx +288 -281
  36. package/src/components/compact-time-picker/time-value.ts +32 -29
  37. package/src/components/content-unavailable-view/content-unavailable-view.tsx +64 -48
  38. package/src/components/content-view/content-view.tsx +168 -159
  39. package/src/components/content-view/index.ts +1 -1
  40. package/src/components/context-menu/conceal-menu-source.ts +25 -25
  41. package/src/components/context-menu/context-menu-host.tsx +439 -402
  42. package/src/components/context-menu/context-menu-store.ts +125 -123
  43. package/src/components/context-menu/menu-navigation.ts +25 -18
  44. package/src/components/context-menu/menu-position.ts +33 -29
  45. package/src/components/context-menu/menu-reveal.ts +32 -32
  46. package/src/components/context-menu/use-context-menu.ts +63 -56
  47. package/src/components/conversation/conversation.tsx +265 -263
  48. package/src/components/conversation/index.ts +1 -1
  49. package/src/components/date-picker/date-picker.tsx +95 -94
  50. package/src/components/date-picker/date-value.ts +20 -10
  51. package/src/components/dialog/dialog-host.tsx +105 -103
  52. package/src/components/dialog/dialog-store.ts +37 -36
  53. package/src/components/dialog/dialog.tsx +192 -179
  54. package/src/components/dialog/index.ts +1 -1
  55. package/src/components/disclosure-group/disclosure-group.tsx +93 -86
  56. package/src/components/divider/divider.tsx +24 -22
  57. package/src/components/inline-disclosure/inline-disclosure.tsx +40 -36
  58. package/src/components/keyboard-shortcut/keyboard-shortcut.tsx +23 -19
  59. package/src/components/labeled-content/labeled-content.tsx +68 -68
  60. package/src/components/locale-switcher/index.ts +1 -1
  61. package/src/components/locale-switcher/locale-switcher.tsx +279 -251
  62. package/src/components/menu/menu.tsx +75 -58
  63. package/src/components/morph/index.ts +2 -2
  64. package/src/components/morph/morph.tsx +227 -201
  65. package/src/components/morph/use-morph-disclosure.ts +19 -19
  66. package/src/components/one-time-code/one-time-code.tsx +216 -200
  67. package/src/components/picker/picker-trigger.ts +16 -13
  68. package/src/components/picker/picker.tsx +371 -345
  69. package/src/components/popover/anchored-surface.tsx +253 -229
  70. package/src/components/popover/index.ts +50 -0
  71. package/src/components/popover/popover-content.tsx +334 -0
  72. package/src/components/popover/popover-fit.ts +58 -53
  73. package/src/components/popover/popover-root.tsx +97 -0
  74. package/src/components/popover/popover-trigger.tsx +71 -0
  75. package/src/components/profile-image/profile-image.tsx +80 -64
  76. package/src/components/progress-view/progress-view.tsx +61 -59
  77. package/src/components/progress-view/use-liquid-progress.ts +38 -38
  78. package/src/components/radio-group/radio-group.tsx +71 -68
  79. package/src/components/search-field/expanding-search-field.tsx +191 -177
  80. package/src/components/search-field/search-field.tsx +59 -50
  81. package/src/components/search-list/search-list.tsx +246 -236
  82. package/src/components/section/section.tsx +58 -58
  83. package/src/components/secure-field/secure-field.tsx +3 -3
  84. package/src/components/segmented-control/segmented-control.tsx +136 -136
  85. package/src/components/sheet/modal-environment.ts +71 -64
  86. package/src/components/sheet/sheet-portal.tsx +58 -53
  87. package/src/components/sheet/sheet.tsx +146 -141
  88. package/src/components/sheet/use-sheet-focus.ts +97 -82
  89. package/src/components/sidebar/sidebar.tsx +78 -78
  90. package/src/components/skeleton/skeleton.tsx +30 -21
  91. package/src/components/slider/slider.tsx +113 -104
  92. package/src/components/slosh-slider/liquid-step.ts +32 -31
  93. package/src/components/slosh-slider/slosh-slider.tsx +265 -228
  94. package/src/components/stat-tile/index.ts +1 -1
  95. package/src/components/stat-tile/stat-tile.tsx +79 -68
  96. package/src/components/status-indicator/status-indicator.tsx +40 -37
  97. package/src/components/stepper/rolling-digit.tsx +82 -68
  98. package/src/components/stepper/stepper.tsx +122 -113
  99. package/src/components/swatch-picker/swatch-picker.tsx +120 -120
  100. package/src/components/tab-bar/tab-bar.tsx +149 -148
  101. package/src/components/tab-view/tab-view.tsx +140 -137
  102. package/src/components/table/table.tsx +117 -98
  103. package/src/components/text-editor/text-editor.tsx +34 -29
  104. package/src/components/text-field/text-field.tsx +182 -180
  105. package/src/components/toast/toast-store.ts +127 -115
  106. package/src/components/toast/toaster.tsx +361 -347
  107. package/src/components/toggle/toggle.tsx +297 -282
  108. package/src/components/tooltip/tooltip-host.tsx +157 -150
  109. package/src/components/view-controller/index.ts +1 -1
  110. package/src/components/view-controller/view-controller.tsx +64 -53
  111. package/src/components/wake-slider/wake-slider.tsx +220 -205
  112. package/src/hooks/scroll/minimize-decision.ts +11 -7
  113. package/src/hooks/use-anchored-disclosure.ts +19 -19
  114. package/src/hooks/use-anchored-placement.ts +29 -25
  115. package/src/hooks/use-dismiss.ts +33 -29
  116. package/src/hooks/use-floating-dismiss.ts +24 -22
  117. package/src/hooks/use-media-query.ts +67 -60
  118. package/src/hooks/use-minimize-on-scroll.ts +69 -60
  119. package/src/hooks/use-visual-viewport.ts +63 -63
  120. package/src/hooks/viewport/types.ts +16 -16
  121. package/src/hooks/viewport/viewport-metrics.ts +16 -10
  122. package/src/icons/icons.tsx +440 -328
  123. package/src/index.ts +91 -91
  124. package/src/lib/anchored-placement.ts +70 -65
  125. package/src/lib/class-names.ts +14 -13
  126. package/src/lib/clipboard.ts +8 -7
  127. package/src/lib/date.ts +61 -60
  128. package/src/lib/floating-layers.ts +39 -24
  129. package/src/lib/haptics.ts +10 -10
  130. package/src/lib/language.ts +9 -9
  131. package/src/lib/range-value.ts +27 -21
  132. package/src/lib/screen-edges.ts +33 -30
  133. package/src/lib/shapes.ts +38 -34
  134. package/src/machines/popover.machine.ts +51 -0
  135. package/src/motion/transitions.ts +46 -43
  136. package/src/styles/code-block.css +69 -43
  137. package/src/styles/motion-controls.css +393 -191
  138. package/src/tokens.ts +1 -1
  139. package/dist/types/src/components/popover/popover.d.ts +0 -21
  140. package/src/components/popover/popover.tsx +0 -334
@@ -1,42 +1,42 @@
1
- import { AnimatePresence, motion, useReducedMotion } from "motion/react"
2
- import { type ReactNode, useRef } from "react"
3
- import { useVisualViewport } from "../../hooks/use-visual-viewport"
4
- import { cn } from "../../lib/class-names"
5
- import { contentCrossfade, spring, zoomReveal } from "../../motion/transitions"
6
- import { SheetPortal } from "./sheet-portal"
7
- import { useSheetFocus } from "./use-sheet-focus"
1
+ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
2
+ import { type ReactNode, useRef } from "react";
3
+ import { useVisualViewport } from "../../hooks/use-visual-viewport";
4
+ import { cn } from "../../lib/class-names";
5
+ import { contentCrossfade, spring, zoomReveal } from "../../motion/transitions";
6
+ import { SheetPortal } from "./sheet-portal";
7
+ import { useSheetFocus } from "./use-sheet-focus";
8
8
 
9
- export type SheetPlacement = "adaptive" | "center"
10
- export type SheetSize = "sm" | "md" | "lg"
9
+ export type SheetPlacement = "adaptive" | "center";
10
+ export type SheetSize = "sm" | "md" | "lg";
11
11
 
12
12
  const outerPaddings: Record<SheetSize, string> = {
13
- sm: "p-4",
14
- md: "p-6",
15
- lg: "p-8",
16
- }
13
+ sm: "p-4",
14
+ md: "p-6",
15
+ lg: "p-8",
16
+ };
17
17
  const panelMaxWidths: Record<SheetSize, string> = {
18
- sm: "max-w-sm",
19
- md: "max-w-[560px]",
20
- lg: "max-w-2xl",
21
- }
18
+ sm: "max-w-sm",
19
+ md: "max-w-[560px]",
20
+ lg: "max-w-2xl",
21
+ };
22
22
 
23
23
  export interface SheetProps {
24
- /** Controls whether the sheet is presented. */
25
- open: boolean
26
- /** Called on Escape or an outside press. */
27
- onClose: () => void
28
- /** Allow tapping the backdrop to close. Escape still calls onClose. @default true */
29
- dismissOnOutsidePress?: boolean
30
- /** Read out by screen readers as the sheet's name. */
31
- label: string
32
- /** ID of a visible heading to use instead of label for the accessible name. */
33
- labelledBy?: string
34
- /** `adaptive` rises from the bottom on phones; `center` always floats. @default "adaptive" */
35
- placement?: SheetPlacement
36
- /** Outer padding and panel max-width. @default "md" */
37
- size?: SheetSize
38
- className?: string
39
- children: ReactNode
24
+ /** Controls whether the sheet is presented. */
25
+ open: boolean;
26
+ /** Called on Escape or an outside press. */
27
+ onClose: () => void;
28
+ /** Allow tapping the backdrop to close. Escape still calls onClose. @default true */
29
+ dismissOnOutsidePress?: boolean;
30
+ /** Read out by screen readers as the sheet's name. */
31
+ label: string;
32
+ /** ID of a visible heading to use instead of label for the accessible name. */
33
+ labelledBy?: string;
34
+ /** `adaptive` rises from the bottom on phones; `center` always floats. @default "adaptive" */
35
+ placement?: SheetPlacement;
36
+ /** Outer padding and panel max-width. @default "md" */
37
+ size?: SheetSize;
38
+ className?: string;
39
+ children: ReactNode;
40
40
  }
41
41
 
42
42
  /**
@@ -44,117 +44,122 @@ export interface SheetProps {
44
44
  * middle on larger screens. Escape or a tap outside closes it.
45
45
  */
46
46
  export function Sheet({ open, ...props }: SheetProps) {
47
- return (
48
- <AnimatePresence>
49
- {open && (
50
- <SheetPortal>
51
- <OpenSheet {...props} />
52
- </SheetPortal>
53
- )}
54
- </AnimatePresence>
55
- )
47
+ return (
48
+ <AnimatePresence>
49
+ {open && (
50
+ <SheetPortal>
51
+ <OpenSheet {...props} />
52
+ </SheetPortal>
53
+ )}
54
+ </AnimatePresence>
55
+ );
56
56
  }
57
57
 
58
58
  function OpenSheet({
59
- onClose,
60
- dismissOnOutsidePress = true,
61
- label,
62
- labelledBy,
63
- placement = "adaptive",
64
- size = "md",
65
- className,
66
- children,
59
+ onClose,
60
+ dismissOnOutsidePress = true,
61
+ label,
62
+ labelledBy,
63
+ placement = "adaptive",
64
+ size = "md",
65
+ className,
66
+ children,
67
67
  }: Omit<SheetProps, "open">) {
68
- const panel = useRef<HTMLDivElement>(null)
69
- const reducedMotion = useReducedMotion()
70
- const viewport = useVisualViewport()
71
- useSheetFocus(panel, onClose)
68
+ const panel = useRef<HTMLDivElement>(null);
69
+ const reducedMotion = useReducedMotion();
70
+ const viewport = useVisualViewport();
71
+ useSheetFocus(panel, onClose);
72
72
 
73
- return (
74
- <div
75
- className={cn(
76
- "fixed inset-0 z-[65] flex justify-center",
77
- placement === "center"
78
- ? cn("items-center", outerPaddings[size])
79
- : cn("items-end sm:items-center", `sm:${outerPaddings[size]}`)
80
- )}
81
- style={
82
- viewport
83
- ? {
84
- top: viewport.top,
85
- left: viewport.left,
86
- width: viewport.width,
87
- height: viewport.height,
88
- bottom: "auto",
89
- right: "auto",
90
- }
91
- : undefined
92
- }
93
- >
94
- <motion.div
95
- aria-hidden="true"
96
- className="absolute inset-0 bg-black/30 backdrop-blur-md"
97
- initial={{ opacity: 0 }}
98
- animate={{ opacity: 1 }}
99
- exit={{ opacity: 0 }}
100
- onClick={dismissOnOutsidePress ? onClose : undefined}
101
- />
102
- <motion.div
103
- ref={panel}
104
- role="dialog"
105
- aria-modal="true"
106
- aria-label={labelledBy ? undefined : label}
107
- aria-labelledby={labelledBy}
108
- tabIndex={-1}
109
- className={cn(
110
- "glass-menu relative flex w-full flex-col overflow-hidden",
111
- "max-h-[92%]",
112
- panelMaxWidths[size],
113
- placement === "center" ? "rounded-sheet" : "rounded-t-sheet sm:rounded-sheet",
114
- className
115
- )}
116
- style={{
117
- transformOrigin: placement === "center" ? "center center" : "bottom center",
118
- }}
119
- initial={
120
- reducedMotion
121
- ? { opacity: 0 }
122
- : placement === "center"
123
- ? zoomReveal.initial
124
- : { y: 40, opacity: 0, scale: 0.96 }
125
- }
126
- animate={
127
- reducedMotion
128
- ? { opacity: 1 }
129
- : placement === "center"
130
- ? zoomReveal.animate
131
- : { y: 0, opacity: 1, scale: 1 }
132
- }
133
- exit={
134
- reducedMotion
135
- ? { opacity: 0 }
136
- : placement === "center"
137
- ? zoomReveal.exit
138
- : { y: 30, opacity: 0, scale: 0.98 }
139
- }
140
- transition={
141
- reducedMotion
142
- ? { duration: 0 }
143
- : placement === "center"
144
- ? zoomReveal.transition
145
- : { ...spring.bouncy, opacity: { duration: 0.18 } }
146
- }
147
- >
148
- <motion.div
149
- className="flex min-h-0 flex-1 flex-col overflow-y-auto"
150
- initial={reducedMotion ? false : contentCrossfade.initial}
151
- animate={reducedMotion ? undefined : contentCrossfade.animate}
152
- exit={reducedMotion ? undefined : contentCrossfade.exit}
153
- transition={reducedMotion ? { duration: 0 } : contentCrossfade.transition}
154
- >
155
- {children}
156
- </motion.div>
157
- </motion.div>
158
- </div>
159
- )
73
+ return (
74
+ <div
75
+ className={cn(
76
+ "fixed inset-0 z-[65] flex justify-center",
77
+ placement === "center"
78
+ ? cn("items-center", outerPaddings[size])
79
+ : cn("items-end sm:items-center", `sm:${outerPaddings[size]}`),
80
+ )}
81
+ style={
82
+ viewport
83
+ ? {
84
+ top: viewport.top,
85
+ left: viewport.left,
86
+ width: viewport.width,
87
+ height: viewport.height,
88
+ bottom: "auto",
89
+ right: "auto",
90
+ }
91
+ : undefined
92
+ }
93
+ >
94
+ <motion.div
95
+ aria-hidden="true"
96
+ className="absolute inset-0 bg-black/30 backdrop-blur-md"
97
+ initial={{ opacity: 0 }}
98
+ animate={{ opacity: 1 }}
99
+ exit={{ opacity: 0 }}
100
+ onClick={dismissOnOutsidePress ? onClose : undefined}
101
+ />
102
+ <motion.div
103
+ ref={panel}
104
+ role="dialog"
105
+ aria-modal="true"
106
+ aria-label={labelledBy ? undefined : label}
107
+ aria-labelledby={labelledBy}
108
+ tabIndex={-1}
109
+ className={cn(
110
+ "glass-menu relative flex w-full flex-col overflow-hidden",
111
+ "max-h-[92%]",
112
+ panelMaxWidths[size],
113
+ placement === "center"
114
+ ? "rounded-sheet"
115
+ : "rounded-t-sheet sm:rounded-sheet",
116
+ className,
117
+ )}
118
+ style={{
119
+ transformOrigin:
120
+ placement === "center" ? "center center" : "bottom center",
121
+ }}
122
+ initial={
123
+ reducedMotion
124
+ ? { opacity: 0 }
125
+ : placement === "center"
126
+ ? zoomReveal.initial
127
+ : { y: 40, opacity: 0, scale: 0.96 }
128
+ }
129
+ animate={
130
+ reducedMotion
131
+ ? { opacity: 1 }
132
+ : placement === "center"
133
+ ? zoomReveal.animate
134
+ : { y: 0, opacity: 1, scale: 1 }
135
+ }
136
+ exit={
137
+ reducedMotion
138
+ ? { opacity: 0 }
139
+ : placement === "center"
140
+ ? zoomReveal.exit
141
+ : { y: 30, opacity: 0, scale: 0.98 }
142
+ }
143
+ transition={
144
+ reducedMotion
145
+ ? { duration: 0 }
146
+ : placement === "center"
147
+ ? zoomReveal.transition
148
+ : { ...spring.bouncy, opacity: { duration: 0.18 } }
149
+ }
150
+ >
151
+ <motion.div
152
+ className="flex min-h-0 flex-1 flex-col overflow-y-auto"
153
+ initial={reducedMotion ? false : contentCrossfade.initial}
154
+ animate={reducedMotion ? undefined : contentCrossfade.animate}
155
+ exit={reducedMotion ? undefined : contentCrossfade.exit}
156
+ transition={
157
+ reducedMotion ? { duration: 0 } : contentCrossfade.transition
158
+ }
159
+ >
160
+ {children}
161
+ </motion.div>
162
+ </motion.div>
163
+ </div>
164
+ );
160
165
  }
@@ -1,94 +1,109 @@
1
- import { type RefObject, useEffect, useLayoutEffect, useRef, useState } from "react"
2
- import { ownsFloatingLayer } from "../../lib/floating-layers"
3
- import { enterModalEnvironment } from "./modal-environment"
1
+ import {
2
+ type RefObject,
3
+ useEffect,
4
+ useLayoutEffect,
5
+ useRef,
6
+ useState,
7
+ } from "react";
8
+ import { ownsFloatingLayer } from "../../lib/floating-layers";
9
+ import { enterModalEnvironment } from "./modal-environment";
4
10
 
5
- const sheets: HTMLElement[] = []
11
+ const sheets: HTMLElement[] = [];
6
12
  const focusableSelector = [
7
- "a[href]",
8
- "button",
9
- "input:not([type=hidden])",
10
- "select",
11
- "textarea",
12
- "[tabindex]",
13
- "[contenteditable=true]",
14
- ].join(",")
13
+ "a[href]",
14
+ "button",
15
+ "input:not([type=hidden])",
16
+ "select",
17
+ "textarea",
18
+ "[tabindex]",
19
+ "[contenteditable=true]",
20
+ ].join(",");
15
21
 
16
22
  function focusableElements(panel: HTMLElement): HTMLElement[] {
17
- return Array.from(panel.querySelectorAll<HTMLElement>(focusableSelector)).filter(
18
- (element) =>
19
- element.tabIndex >= 0 &&
20
- !element.matches(":disabled") &&
21
- !element.closest("[inert]") &&
22
- element.getClientRects().length > 0 &&
23
- getComputedStyle(element).visibility !== "hidden"
24
- )
23
+ return Array.from(
24
+ panel.querySelectorAll<HTMLElement>(focusableSelector),
25
+ ).filter(
26
+ (element) =>
27
+ element.tabIndex >= 0 &&
28
+ !element.matches(":disabled") &&
29
+ !element.closest("[inert]") &&
30
+ element.getClientRects().length > 0 &&
31
+ getComputedStyle(element).visibility !== "hidden",
32
+ );
25
33
  }
26
34
 
27
35
  /** Keeps keyboard focus in the foremost sheet, including its floating pickers. */
28
- export function useSheetFocus(panel: RefObject<HTMLDivElement | null>, onClose: () => void) {
29
- // Capture before children mount, since a child may use autoFocus.
30
- const [opener] = useState(() =>
31
- typeof document === "undefined" ? null : document.activeElement
32
- )
33
- const close = useRef(onClose)
34
- useEffect(() => {
35
- close.current = onClose
36
- }, [onClose])
36
+ export function useSheetFocus(
37
+ panel: RefObject<HTMLDivElement | null>,
38
+ onClose: () => void,
39
+ ) {
40
+ // Capture before children mount, since a child may use autoFocus.
41
+ const [opener] = useState(() =>
42
+ typeof document === "undefined" ? null : document.activeElement,
43
+ );
44
+ const close = useRef(onClose);
45
+ useEffect(() => {
46
+ close.current = onClose;
47
+ }, [onClose]);
37
48
 
38
- useLayoutEffect(() => {
39
- const surface = panel.current
40
- if (!surface) return
41
- sheets.push(surface)
42
- const foremost = () => sheets.at(-1) === surface
43
- const focusFirst = () => (focusableElements(surface)[0] ?? surface).focus()
44
- const inFloatingLayer = (element: Element | null) => ownsFloatingLayer(surface, element)
45
- const belongsToSheet = (element: Element | null) =>
46
- surface.contains(element) || inFloatingLayer(element)
49
+ useLayoutEffect(() => {
50
+ const surface = panel.current;
51
+ if (!surface) return;
52
+ sheets.push(surface);
53
+ const foremost = () => sheets.at(-1) === surface;
54
+ const focusFirst = () => (focusableElements(surface)[0] ?? surface).focus();
55
+ const inFloatingLayer = (element: Element | null) =>
56
+ ownsFloatingLayer(surface, element);
57
+ const belongsToSheet = (element: Element | null) =>
58
+ surface.contains(element) || inFloatingLayer(element);
47
59
 
48
- if (!surface.contains(document.activeElement)) surface.focus({ preventScroll: true })
49
- const leaveModalEnvironment = enterModalEnvironment(surface)
60
+ if (!surface.contains(document.activeElement))
61
+ surface.focus({ preventScroll: true });
62
+ const leaveModalEnvironment = enterModalEnvironment(surface);
50
63
 
51
- const onFocus = (event: FocusEvent) => {
52
- if (foremost() && !belongsToSheet(event.target as Element | null)) focusFirst()
53
- }
54
- const onKey = (event: KeyboardEvent) => {
55
- if (!foremost() || event.defaultPrevented) return
56
- if (event.key === "Escape") {
57
- // Floating controls handle their own Escape before the sheet.
58
- if (inFloatingLayer(document.activeElement)) return
59
- event.preventDefault()
60
- event.stopPropagation()
61
- close.current()
62
- }
63
- if (event.key !== "Tab") return
64
- const controls = focusableElements(surface)
65
- const first = controls[0] ?? surface
66
- const last = controls.at(-1) ?? surface
67
- const active = document.activeElement
68
- if (
69
- event.shiftKey &&
70
- (active === surface || active === first || !surface.contains(active))
71
- ) {
72
- event.preventDefault()
73
- last.focus()
74
- } else if (
75
- !event.shiftKey &&
76
- (active === last || !surface.contains(active) || active === surface)
77
- ) {
78
- event.preventDefault()
79
- first.focus()
80
- }
81
- }
64
+ const onFocus = (event: FocusEvent) => {
65
+ if (foremost() && !belongsToSheet(event.target as Element | null))
66
+ focusFirst();
67
+ };
68
+ const onKey = (event: KeyboardEvent) => {
69
+ if (!foremost() || event.defaultPrevented) return;
70
+ if (event.key === "Escape") {
71
+ // Floating controls handle their own Escape before the sheet.
72
+ if (inFloatingLayer(document.activeElement)) return;
73
+ event.preventDefault();
74
+ event.stopPropagation();
75
+ close.current();
76
+ }
77
+ if (event.key !== "Tab") return;
78
+ const controls = focusableElements(surface);
79
+ const first = controls[0] ?? surface;
80
+ const last = controls.at(-1) ?? surface;
81
+ const active = document.activeElement;
82
+ if (
83
+ event.shiftKey &&
84
+ (active === surface || active === first || !surface.contains(active))
85
+ ) {
86
+ event.preventDefault();
87
+ last.focus();
88
+ } else if (
89
+ !event.shiftKey &&
90
+ (active === last || !surface.contains(active) || active === surface)
91
+ ) {
92
+ event.preventDefault();
93
+ first.focus();
94
+ }
95
+ };
82
96
 
83
- document.addEventListener("focusin", onFocus)
84
- window.addEventListener("keydown", onKey)
85
- return () => {
86
- const wasForemost = foremost()
87
- sheets.splice(sheets.indexOf(surface), 1)
88
- document.removeEventListener("focusin", onFocus)
89
- window.removeEventListener("keydown", onKey)
90
- leaveModalEnvironment()
91
- if (wasForemost && opener instanceof HTMLElement && opener.isConnected) opener.focus()
92
- }
93
- }, [panel, opener])
97
+ document.addEventListener("focusin", onFocus);
98
+ window.addEventListener("keydown", onKey);
99
+ return () => {
100
+ const wasForemost = foremost();
101
+ sheets.splice(sheets.indexOf(surface), 1);
102
+ document.removeEventListener("focusin", onFocus);
103
+ window.removeEventListener("keydown", onKey);
104
+ leaveModalEnvironment();
105
+ if (wasForemost && opener instanceof HTMLElement && opener.isConnected)
106
+ opener.focus();
107
+ };
108
+ }, [panel, opener]);
94
109
  }