@tamagui/focus-scope 2.7.7 → 3.0.0-beta.643.1

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 (69) hide show
  1. package/dist/cjs/FocusScope.cjs +346 -351
  2. package/dist/cjs/FocusScope.native.cjs +37 -0
  3. package/dist/cjs/FocusScope.native.js +21 -27
  4. package/dist/cjs/FocusScope.native.js.map +1 -1
  5. package/dist/cjs/FocusScopeController.cjs +48 -54
  6. package/dist/cjs/FocusScopeController.native.cjs +76 -0
  7. package/dist/cjs/FocusScopeController.native.js +50 -55
  8. package/dist/cjs/FocusScopeController.native.js.map +1 -1
  9. package/dist/cjs/index.cjs +10 -11
  10. package/dist/cjs/index.native.cjs +22 -0
  11. package/dist/cjs/index.native.js +10 -10
  12. package/dist/cjs/index.native.js.map +1 -1
  13. package/dist/cjs/types.cjs +10 -11
  14. package/dist/cjs/types.native.cjs +19 -0
  15. package/dist/cjs/types.native.js +10 -10
  16. package/dist/cjs/types.native.js.map +1 -1
  17. package/dist/esm/FocusScope.mjs +331 -328
  18. package/dist/esm/FocusScope.mjs.map +1 -1
  19. package/dist/esm/FocusScope.native.js +6 -3
  20. package/dist/esm/FocusScope.native.js.map +1 -1
  21. package/dist/esm/FocusScopeController.mjs +29 -27
  22. package/dist/esm/FocusScopeController.mjs.map +1 -1
  23. package/dist/esm/FocusScopeController.native.js +32 -30
  24. package/dist/esm/FocusScopeController.native.js.map +1 -1
  25. package/dist/esm/index.js +3 -3
  26. package/dist/esm/index.mjs +3 -3
  27. package/dist/esm/index.native.js +3 -3
  28. package/dist/esm/types.mjs +0 -2
  29. package/dist/esm/types.native.js +0 -2
  30. package/dist/jsx/FocusScope.mjs +331 -328
  31. package/dist/jsx/FocusScope.mjs.map +1 -1
  32. package/dist/jsx/FocusScope.native.cjs +37 -0
  33. package/dist/jsx/FocusScope.native.js +21 -27
  34. package/dist/jsx/FocusScope.native.js.map +1 -1
  35. package/dist/jsx/FocusScopeController.mjs +29 -27
  36. package/dist/jsx/FocusScopeController.mjs.map +1 -1
  37. package/dist/jsx/FocusScopeController.native.cjs +76 -0
  38. package/dist/jsx/FocusScopeController.native.js +50 -55
  39. package/dist/jsx/FocusScopeController.native.js.map +1 -1
  40. package/dist/jsx/index.js +3 -3
  41. package/dist/jsx/index.mjs +3 -3
  42. package/dist/jsx/index.native.cjs +22 -0
  43. package/dist/jsx/index.native.js +10 -10
  44. package/dist/jsx/index.native.js.map +1 -1
  45. package/dist/jsx/types.mjs +0 -2
  46. package/dist/jsx/types.native.cjs +19 -0
  47. package/dist/jsx/types.native.js +10 -10
  48. package/dist/jsx/types.native.js.map +1 -1
  49. package/package.json +9 -8
  50. package/src/FocusScope.native.tsx +2 -1
  51. package/src/FocusScope.tsx +62 -28
  52. package/src/FocusScopeController.tsx +3 -0
  53. package/src/types.tsx +16 -11
  54. package/types/FocusScope.d.ts +3 -4
  55. package/types/FocusScope.d.ts.map +1 -1
  56. package/types/FocusScope.native.d.ts +1 -2
  57. package/types/FocusScope.native.d.ts.map +1 -1
  58. package/types/FocusScopeController.d.ts +1 -1
  59. package/types/FocusScopeController.d.ts.map +1 -1
  60. package/types/types.d.ts +15 -9
  61. package/types/types.d.ts.map +1 -1
  62. package/dist/esm/index.js.map +0 -1
  63. package/dist/esm/index.mjs.map +0 -1
  64. package/dist/esm/index.native.js.map +0 -1
  65. package/dist/esm/types.mjs.map +0 -1
  66. package/dist/esm/types.native.js.map +0 -1
  67. package/dist/jsx/index.js.map +0 -1
  68. package/dist/jsx/index.mjs.map +0 -1
  69. package/dist/jsx/types.mjs.map +0 -1
@@ -1,358 +1,361 @@
1
- import { useComposedRefs } from "@tamagui/compose-refs";
1
+ import { createRefComponent, useComposedRefs } from "@tamagui/compose-refs";
2
2
  import { isWeb, useIsomorphicLayoutEffect } from "@tamagui/constants";
3
+ import { createChangeEventDetails } from "@tamagui/core";
3
4
  import { idle, useAsyncEffect } from "@tamagui/use-async";
4
5
  import { useEvent } from "@tamagui/use-event";
5
6
  import * as React from "react";
6
7
  import { useFocusScopeControllerContext } from "./FocusScopeController.mjs";
7
- import { Fragment, jsx } from "react/jsx-runtime";
8
- const AUTOFOCUS_ON_MOUNT = "focusScope.autoFocusOnMount";
9
- const AUTOFOCUS_ON_UNMOUNT = "focusScope.autoFocusOnUnmount";
10
- const EVENT_OPTIONS = {
11
- bubbles: false,
12
- cancelable: true
13
- };
14
- const FocusScope = React.forwardRef(function FocusScope2({
15
- __scopeFocusScope,
16
- ...props
17
- }, forwardedRef) {
18
- const context = useFocusScopeControllerContext("FocusScope", __scopeFocusScope, {
19
- warn: false,
20
- fallback: {}
21
- });
22
- const mergedProps = {
23
- ...props,
24
- enabled: context.enabled ?? props.enabled,
25
- loop: context.loop ?? props.loop,
26
- trapped: context.trapped ?? props.trapped,
27
- onMountAutoFocus: context.onMountAutoFocus ?? props.onMountAutoFocus,
28
- onUnmountAutoFocus: context.onUnmountAutoFocus ?? props.onUnmountAutoFocus,
29
- forceUnmount: context.forceUnmount ?? props.forceUnmount,
30
- focusOnIdle: context.focusOnIdle ?? props.focusOnIdle
31
- };
32
- const childProps = useFocusScope(mergedProps, forwardedRef);
33
- if (typeof mergedProps.children === "function") {
34
- return /* @__PURE__ */jsx(Fragment, {
35
- children: mergedProps.children(childProps)
36
- });
37
- }
38
- return React.cloneElement(React.Children.only(mergedProps.children), childProps);
8
+ import { jsx } from "react/jsx-runtime";
9
+
10
+ const FOCUS_SCOPE_STYLE = { display: "contents" };
11
+ const FocusScope = createRefComponent(function FocusScope2({ __scopeFocusScope, ...props }, forwardedRef) {
12
+ const context = useFocusScopeControllerContext("FocusScope", __scopeFocusScope, {
13
+ warn: false,
14
+ fallback: {}
15
+ });
16
+ const mergedProps = {
17
+ ...props,
18
+ enabled: context.enabled ?? props.enabled,
19
+ loop: context.loop ?? props.loop,
20
+ trapped: context.trapped ?? props.trapped,
21
+ noFocus: context.noFocus ?? props.noFocus,
22
+ onMountAutoFocus: context.onMountAutoFocus ?? props.onMountAutoFocus,
23
+ onUnmountAutoFocus: context.onUnmountAutoFocus ?? props.onUnmountAutoFocus,
24
+ forceUnmount: context.forceUnmount ?? props.forceUnmount,
25
+ focusOnIdle: context.focusOnIdle ?? props.focusOnIdle
26
+ };
27
+ const childProps = useFocusScope(mergedProps, forwardedRef);
28
+ const { ref, style, ...scopeProps } = childProps;
29
+ const children = mergedProps.children;
30
+ return /* @__PURE__ */ jsx("div", {
31
+ ...scopeProps,
32
+ ref,
33
+ style: getFocusScopeStyle(style),
34
+ children
35
+ });
39
36
  });
37
+ function getFocusScopeStyle(style) {
38
+ if (!style) return FOCUS_SCOPE_STYLE;
39
+ return {
40
+ ...style,
41
+ display: "contents"
42
+ };
43
+ }
40
44
  function setupFocusTrap(container, lastFocusedElementRef, focusScope) {
41
- const controller = new AbortController();
42
- let rafId = null;
43
- function scheduleRefocus() {
44
- if (rafId) return;
45
- const elementToFocus = lastFocusedElementRef.current;
46
- rafId = requestAnimationFrame(() => {
47
- rafId = null;
48
- if (focusScope.paused) return;
49
- if (focusScope.stopped) return;
50
- if (!container.isConnected) return;
51
- if (!container.contains(document.activeElement)) {
52
- focus(elementToFocus);
53
- }
54
- });
55
- }
56
- function handleFocusIn(event) {
57
- if (focusScope.paused) return;
58
- const target = event.target;
59
- if (container.contains(target)) {
60
- target?.addEventListener("blur", handleBlur, {
61
- signal: controller.signal
62
- });
63
- lastFocusedElementRef.current = target;
64
- } else if (target?.contains(container)) {} else {
65
- scheduleRefocus();
66
- }
67
- }
68
- function handleFocusOut(event) {
69
- controller.abort();
70
- if (focusScope.paused) return;
71
- const relatedTarget = event.relatedTarget;
72
- if (relatedTarget === null) return;
73
- if (relatedTarget.contains(container)) return;
74
- if (!container.contains(relatedTarget)) {
75
- scheduleRefocus();
76
- }
77
- }
78
- function handleBlur() {}
79
- document.addEventListener("focusin", handleFocusIn);
80
- document.addEventListener("focusout", handleFocusOut);
81
- return () => {
82
- if (rafId) {
83
- cancelAnimationFrame(rafId);
84
- rafId = null;
85
- }
86
- controller.abort();
87
- document.removeEventListener("focusin", handleFocusIn);
88
- document.removeEventListener("focusout", handleFocusOut);
89
- };
45
+ const controller = new AbortController();
46
+ let rafId = null;
47
+ function scheduleRefocus() {
48
+ if (rafId) return;
49
+ const elementToFocus = lastFocusedElementRef.current;
50
+ rafId = requestAnimationFrame(() => {
51
+ rafId = null;
52
+ if (focusScope.paused) return;
53
+ if (focusScope.stopped) return;
54
+ if (!container.isConnected) return;
55
+ if (!container.contains(document.activeElement)) {
56
+ focus(elementToFocus);
57
+ }
58
+ });
59
+ }
60
+ function handleFocusIn(event) {
61
+ if (focusScope.paused) return;
62
+ const target = event.target;
63
+ if (container.contains(target)) {
64
+ target?.addEventListener("blur", handleBlur, { signal: controller.signal });
65
+ lastFocusedElementRef.current = target;
66
+ } else if (target?.contains(container)) {} else {
67
+ scheduleRefocus();
68
+ }
69
+ }
70
+ function handleFocusOut(event) {
71
+ controller.abort();
72
+ if (focusScope.paused) return;
73
+ const relatedTarget = event.relatedTarget;
74
+ if (relatedTarget === null) return;
75
+ if (relatedTarget.contains(container)) return;
76
+ if (!container.contains(relatedTarget)) {
77
+ scheduleRefocus();
78
+ }
79
+ }
80
+ function handleBlur() {}
81
+ document.addEventListener("focusin", handleFocusIn);
82
+ document.addEventListener("focusout", handleFocusOut);
83
+ return () => {
84
+ if (rafId) {
85
+ cancelAnimationFrame(rafId);
86
+ rafId = null;
87
+ }
88
+ controller.abort();
89
+ document.removeEventListener("focusin", handleFocusIn);
90
+ document.removeEventListener("focusout", handleFocusOut);
91
+ };
90
92
  }
91
93
  function useFocusScope(props, forwardedRef) {
92
- const {
93
- loop = false,
94
- enabled = true,
95
- trapped = false,
96
- onMountAutoFocus: onMountAutoFocusProp,
97
- onUnmountAutoFocus: onUnmountAutoFocusProp,
98
- forceUnmount,
99
- focusOnIdle = true,
100
- children,
101
- ...scopeProps
102
- } = props;
103
- const [container, setContainer] = React.useState(null);
104
- const containerRef = React.useRef(null);
105
- const onMountAutoFocus = useEvent(onMountAutoFocusProp);
106
- const onUnmountAutoFocus = useEvent(onUnmountAutoFocusProp);
107
- const lastFocusedElementRef = React.useRef(null);
108
- const focusScopeRef = React.useRef({
109
- paused: false,
110
- stopped: false,
111
- // set to true when cleanup starts, signals trap to stop
112
- pause() {
113
- this.paused = true;
114
- },
115
- resume() {
116
- this.paused = false;
117
- },
118
- stop() {
119
- this.stopped = true;
120
- }
121
- });
122
- const focusScope = focusScopeRef.current;
123
- const trapCleanupRef = React.useRef(null);
124
- const setContainerRef = React.useCallback(node => {
125
- if (trapCleanupRef.current) {
126
- trapCleanupRef.current();
127
- trapCleanupRef.current = null;
128
- }
129
- containerRef.current = node;
130
- if (isWeb && node && enabled && trapped) {
131
- trapCleanupRef.current = setupFocusTrap(node, lastFocusedElementRef, focusScopeRef.current);
132
- }
133
- setContainer(node);
134
- }, [enabled, trapped]);
135
- const composedRefs = useComposedRefs(forwardedRef, setContainerRef);
136
- useIsomorphicLayoutEffect(() => {
137
- if (!trapped) {
138
- focusScope.stopped = true;
139
- if (trapCleanupRef.current) {
140
- trapCleanupRef.current();
141
- trapCleanupRef.current = null;
142
- }
143
- }
144
- return () => {
145
- if (trapCleanupRef.current) {
146
- trapCleanupRef.current();
147
- trapCleanupRef.current = null;
148
- }
149
- };
150
- }, [trapped, focusScope]);
151
- useAsyncEffect(async signal => {
152
- if (!enabled) return;
153
- if (!container) return;
154
- if (forceUnmount) return;
155
- if (trapped) {
156
- focusScope.stopped = false;
157
- }
158
- focusScopesStack.add(focusScope);
159
- const previouslyFocusedElement = document.activeElement;
160
- const hasFocusedCandidate = container.contains(previouslyFocusedElement) && previouslyFocusedElement !== null && !isHidden(previouslyFocusedElement, {
161
- upTo: container
162
- });
163
- if (!hasFocusedCandidate) {
164
- const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT, EVENT_OPTIONS);
165
- container.addEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus);
166
- container.dispatchEvent(mountEvent);
167
- if (!mountEvent.defaultPrevented) {
168
- if (focusOnIdle) {
169
- await idle(signal, typeof focusOnIdle == "object" ? focusOnIdle : {
170
- // we can't wait too long or else user can take an action and then we focus
171
- max: 200,
172
- min: typeof focusOnIdle == "number" ? focusOnIdle : 16
173
- });
174
- }
175
- const allCandidates = getTabbableCandidates(container);
176
- const linkedRemoved = removeLinks(allCandidates);
177
- const visibleCandidates = linkedRemoved.filter(candidate => !isHidden(candidate, {
178
- upTo: container
179
- }));
180
- focusFirst(visibleCandidates, {
181
- select: true
182
- });
183
- if (visibleCandidates.length > 0) {
184
- lastFocusedElementRef.current = visibleCandidates[0];
185
- } else {
186
- lastFocusedElementRef.current = container;
187
- }
188
- if (document.activeElement === previouslyFocusedElement && visibleCandidates.length === 0) {
189
- focus(container);
190
- }
191
- }
192
- }
193
- return () => {
194
- focusScope.stop();
195
- container.removeEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus);
196
- const unmountEvent = new CustomEvent(AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS);
197
- container.addEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus);
198
- container.dispatchEvent(unmountEvent);
199
- if (!unmountEvent.defaultPrevented) {
200
- const currentFocus = document.activeElement;
201
- const focusHasMovedOutside = currentFocus && currentFocus !== document.body && currentFocus !== container && !container.contains(currentFocus);
202
- if (!focusHasMovedOutside) {
203
- focus(previouslyFocusedElement ?? document.body, {
204
- select: true
205
- });
206
- }
207
- }
208
- container.removeEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus);
209
- focusScopesStack.remove(focusScope);
210
- };
211
- }, [enabled, container, forceUnmount, onMountAutoFocus, onUnmountAutoFocus, focusScope, focusOnIdle]);
212
- const handleKeyDown = React.useCallback(event => {
213
- if (!trapped) return;
214
- if (!loop) return;
215
- if (focusScope.paused) return;
216
- if (!enabled) return;
217
- if (!container) return;
218
- const isTabKey = event.key === "Tab" && !event.altKey && !event.ctrlKey && !event.metaKey;
219
- const focusedElement = document.activeElement;
220
- if (isTabKey && focusedElement) {
221
- const [first, last] = getTabbableEdges(container);
222
- const hasTabbableElementsInside = first && last;
223
- if (!hasTabbableElementsInside) {
224
- if (focusedElement === container) event.preventDefault();
225
- } else {
226
- if (!event.shiftKey && focusedElement === last) {
227
- event.preventDefault();
228
- if (loop) focus(first, {
229
- select: true
230
- });
231
- } else if (event.shiftKey && focusedElement === first) {
232
- event.preventDefault();
233
- if (loop) focus(last, {
234
- select: true
235
- });
236
- }
237
- }
238
- }
239
- }, [loop, trapped, focusScope.paused, enabled, container]);
240
- React.useEffect(() => {
241
- if (!container) return;
242
- if (!trapped) return;
243
- if (!loop) return;
244
- if (!enabled) return;
245
- const handleKeyDownCapture = event => {
246
- if (event.key === "Tab") {
247
- handleKeyDown(event);
248
- }
249
- };
250
- container.addEventListener("keydown", handleKeyDownCapture, true);
251
- return () => {
252
- container.removeEventListener("keydown", handleKeyDownCapture, true);
253
- };
254
- }, [container, trapped, loop, enabled, handleKeyDown]);
255
- const existingOnKeyDown = scopeProps.onKeyDown;
256
- const composedOnKeyDown = React.useCallback(event => {
257
- existingOnKeyDown?.(event);
258
- }, [existingOnKeyDown]);
259
- return {
260
- ...scopeProps,
261
- ref: composedRefs,
262
- onKeyDown: composedOnKeyDown
263
- };
94
+ const { loop = false, enabled = true, trapped: trappedProp = false, noFocus = false, onMountAutoFocus: onMountAutoFocusProp, onUnmountAutoFocus: onUnmountAutoFocusProp, forceUnmount, focusOnIdle = true, children: _children, asChild: _asChild, ...scopeProps } = props;
95
+ const trapped = noFocus ? false : trappedProp;
96
+ const [container, setContainer] = React.useState(null);
97
+ const containerRef = React.useRef(null);
98
+ const onMountAutoFocus = useEvent(onMountAutoFocusProp);
99
+ const onUnmountAutoFocus = useEvent(onUnmountAutoFocusProp);
100
+ const lastFocusedElementRef = React.useRef(null);
101
+ const focusScopeRef = React.useRef({
102
+ paused: false,
103
+ stopped: false,
104
+ pause() {
105
+ this.paused = true;
106
+ },
107
+ resume() {
108
+ this.paused = false;
109
+ },
110
+ stop() {
111
+ this.stopped = true;
112
+ }
113
+ });
114
+ const focusScope = focusScopeRef.current;
115
+ const trapCleanupRef = React.useRef(null);
116
+ const setContainerRef = React.useCallback((node) => {
117
+ if (trapCleanupRef.current) {
118
+ trapCleanupRef.current();
119
+ trapCleanupRef.current = null;
120
+ }
121
+ containerRef.current = node;
122
+ if (isWeb && node && enabled && trapped) {
123
+ trapCleanupRef.current = setupFocusTrap(node, lastFocusedElementRef, focusScopeRef.current);
124
+ }
125
+ setContainer(node);
126
+ }, [enabled, trapped]);
127
+ const composedRefs = useComposedRefs(forwardedRef, setContainerRef);
128
+ useIsomorphicLayoutEffect(() => {
129
+ if (!trapped) {
130
+ focusScope.stopped = true;
131
+ if (trapCleanupRef.current) {
132
+ trapCleanupRef.current();
133
+ trapCleanupRef.current = null;
134
+ }
135
+ }
136
+ return () => {
137
+ if (trapCleanupRef.current) {
138
+ trapCleanupRef.current();
139
+ trapCleanupRef.current = null;
140
+ }
141
+ };
142
+ }, [trapped, focusScope]);
143
+ useIsomorphicLayoutEffect(() => {
144
+ if (!isWeb) return;
145
+ if (!enabled || !noFocus || !container) return;
146
+ const blurActiveElement = () => {
147
+ const active = document.activeElement;
148
+ if (active && active !== document.body && typeof active.blur === "function") {
149
+ active.blur();
150
+ }
151
+ };
152
+ blurActiveElement();
153
+ document.addEventListener("focusin", blurActiveElement, true);
154
+ return () => {
155
+ document.removeEventListener("focusin", blurActiveElement, true);
156
+ };
157
+ }, [
158
+ enabled,
159
+ noFocus,
160
+ container
161
+ ]);
162
+ useAsyncEffect(async (signal) => {
163
+ if (!enabled) return;
164
+ if (!container) return;
165
+ if (forceUnmount) return;
166
+ if (noFocus) return;
167
+ if (trapped) {
168
+ focusScope.stopped = false;
169
+ }
170
+ focusScopesStack.add(focusScope);
171
+ const previouslyFocusedElement = document.activeElement;
172
+ const hasFocusedCandidate = container.contains(previouslyFocusedElement) && previouslyFocusedElement !== null && !isHidden(previouslyFocusedElement, { upTo: container });
173
+ if (!hasFocusedCandidate) {
174
+ const details = createChangeEventDetails("trigger-focus");
175
+ onMountAutoFocus?.(details);
176
+ if (!details.isCanceled) {
177
+ if (focusOnIdle) {
178
+ await idle(signal, typeof focusOnIdle == "object" ? focusOnIdle : {
179
+ max: 200,
180
+ min: typeof focusOnIdle == "number" ? focusOnIdle : 16
181
+ });
182
+ }
183
+ const allCandidates = getTabbableCandidates(container);
184
+ const linkedRemoved = removeLinks(allCandidates);
185
+ const visibleCandidates = linkedRemoved.filter((candidate) => !isHidden(candidate, { upTo: container }));
186
+ focusFirst(visibleCandidates, { select: true });
187
+ if (visibleCandidates.length > 0) {
188
+ lastFocusedElementRef.current = visibleCandidates[0];
189
+ } else {
190
+ lastFocusedElementRef.current = container;
191
+ }
192
+ if (document.activeElement === previouslyFocusedElement && visibleCandidates.length === 0) {
193
+ focus(container);
194
+ }
195
+ }
196
+ }
197
+ return () => {
198
+ focusScope.stop();
199
+ const details = createChangeEventDetails("focus-out");
200
+ onUnmountAutoFocus?.(details);
201
+ if (!details.isCanceled) {
202
+ const currentFocus = document.activeElement;
203
+ const focusHasMovedOutside = currentFocus && currentFocus !== document.body && currentFocus !== container && !container.contains(currentFocus);
204
+ if (!focusHasMovedOutside) {
205
+ focus(previouslyFocusedElement ?? document.body, { select: true });
206
+ }
207
+ }
208
+ focusScopesStack.remove(focusScope);
209
+ };
210
+ }, [
211
+ enabled,
212
+ container,
213
+ forceUnmount,
214
+ noFocus,
215
+ onMountAutoFocus,
216
+ onUnmountAutoFocus,
217
+ focusScope,
218
+ focusOnIdle
219
+ ]);
220
+ const handleKeyDown = React.useCallback((event) => {
221
+ if (!trapped) return;
222
+ if (!loop) return;
223
+ if (focusScope.paused) return;
224
+ if (!enabled) return;
225
+ if (!container) return;
226
+ const isTabKey = event.key === "Tab" && !event.altKey && !event.ctrlKey && !event.metaKey;
227
+ const focusedElement = document.activeElement;
228
+ if (isTabKey && focusedElement) {
229
+ const [first, last] = getTabbableEdges(container);
230
+ const hasTabbableElementsInside = first && last;
231
+ if (!hasTabbableElementsInside) {
232
+ if (focusedElement === container) event.preventDefault();
233
+ } else {
234
+ if (!event.shiftKey && focusedElement === last) {
235
+ event.preventDefault();
236
+ if (loop) focus(first, { select: true });
237
+ } else if (event.shiftKey && focusedElement === first) {
238
+ event.preventDefault();
239
+ if (loop) focus(last, { select: true });
240
+ }
241
+ }
242
+ }
243
+ }, [
244
+ loop,
245
+ trapped,
246
+ focusScope.paused,
247
+ enabled,
248
+ container
249
+ ]);
250
+ React.useEffect(() => {
251
+ if (!container) return;
252
+ if (!trapped) return;
253
+ if (!loop) return;
254
+ if (!enabled) return;
255
+ const handleKeyDownCapture = (event) => {
256
+ if (event.key === "Tab") {
257
+ handleKeyDown(event);
258
+ }
259
+ };
260
+ container.addEventListener("keydown", handleKeyDownCapture, true);
261
+ return () => {
262
+ container.removeEventListener("keydown", handleKeyDownCapture, true);
263
+ };
264
+ }, [
265
+ container,
266
+ trapped,
267
+ loop,
268
+ enabled,
269
+ handleKeyDown
270
+ ]);
271
+ const existingOnKeyDown = scopeProps.onKeyDown;
272
+ const composedOnKeyDown = React.useCallback((event) => {
273
+ existingOnKeyDown?.(event);
274
+ }, [existingOnKeyDown]);
275
+ return {
276
+ ...scopeProps,
277
+ ref: composedRefs,
278
+ onKeyDown: composedOnKeyDown
279
+ };
264
280
  }
265
- function focusFirst(candidates, {
266
- select = false
267
- } = {}) {
268
- const previouslyFocusedElement = document.activeElement;
269
- for (const candidate of candidates) {
270
- focus(candidate, {
271
- select
272
- });
273
- if (document.activeElement !== previouslyFocusedElement) return;
274
- }
281
+ function focusFirst(candidates, { select = false } = {}) {
282
+ const previouslyFocusedElement = document.activeElement;
283
+ for (const candidate of candidates) {
284
+ focus(candidate, { select });
285
+ if (document.activeElement !== previouslyFocusedElement) return;
286
+ }
275
287
  }
276
288
  function getTabbableEdges(container) {
277
- const candidates = getTabbableCandidates(container);
278
- const first = findVisible(candidates, container);
279
- const last = findVisible(candidates.reverse(), container);
280
- return [first, last];
289
+ const candidates = getTabbableCandidates(container);
290
+ const first = findVisible(candidates, container);
291
+ const last = findVisible(candidates.reverse(), container);
292
+ return [first, last];
281
293
  }
282
294
  function getTabbableCandidates(container) {
283
- const nodes = [];
284
- const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
285
- acceptNode: node => {
286
- const isHiddenInput = node.tagName === "INPUT" && node.type === "hidden";
287
- if (node.disabled || node.hidden || isHiddenInput) return NodeFilter.FILTER_SKIP;
288
- return node.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
289
- }
290
- });
291
- while (walker.nextNode()) nodes.push(walker.currentNode);
292
- return nodes;
295
+ const nodes = [];
296
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, { acceptNode: (node) => {
297
+ const isHiddenInput = node.tagName === "INPUT" && node.type === "hidden";
298
+ if (node.disabled || node.hidden || isHiddenInput) return NodeFilter.FILTER_SKIP;
299
+ return node.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
300
+ } });
301
+ while (walker.nextNode()) nodes.push(walker.currentNode);
302
+ return nodes;
293
303
  }
294
304
  function findVisible(elements, container) {
295
- for (const element of elements) {
296
- if (!isHidden(element, {
297
- upTo: container
298
- })) return element;
299
- }
305
+ for (const element of elements) {
306
+ if (!isHidden(element, { upTo: container })) return element;
307
+ }
300
308
  }
301
- function isHidden(node, {
302
- upTo
303
- }) {
304
- if (getComputedStyle(node).visibility === "hidden") return true;
305
- while (node) {
306
- if (upTo !== void 0 && node === upTo) return false;
307
- if (getComputedStyle(node).display === "none") return true;
308
- node = node.parentElement;
309
- }
310
- return false;
309
+ function isHidden(node, { upTo }) {
310
+ if (getComputedStyle(node).visibility === "hidden") return true;
311
+ while (node) {
312
+ if (upTo !== void 0 && node === upTo) return false;
313
+ if (getComputedStyle(node).display === "none") return true;
314
+ node = node.parentElement;
315
+ }
316
+ return false;
311
317
  }
312
318
  function isSelectableInput(element) {
313
- return element instanceof HTMLInputElement && "select" in element;
319
+ return element instanceof HTMLInputElement && "select" in element;
314
320
  }
315
- function focus(element, {
316
- select = false
317
- } = {}) {
318
- if (element?.focus) {
319
- const previouslyFocusedElement = document.activeElement;
320
- try {
321
- element.focus({
322
- preventScroll: true
323
- });
324
- if (element !== previouslyFocusedElement && isSelectableInput(element) && select) element.select();
325
- } catch (error) {}
326
- }
321
+ function focus(element, { select = false } = {}) {
322
+ if (element?.focus) {
323
+ const previouslyFocusedElement = document.activeElement;
324
+ try {
325
+ element.focus({ preventScroll: true });
326
+ if (element !== previouslyFocusedElement && isSelectableInput(element) && select) element.select();
327
+ } catch (error) {}
328
+ }
327
329
  }
328
330
  const focusScopesStack = createFocusScopesStack();
329
331
  function createFocusScopesStack() {
330
- let stack = [];
331
- return {
332
- add(focusScope) {
333
- const activeFocusScope = stack[0];
334
- if (focusScope !== activeFocusScope) {
335
- activeFocusScope?.pause();
336
- }
337
- stack = arrayRemove(stack, focusScope);
338
- stack.unshift(focusScope);
339
- },
340
- remove(focusScope) {
341
- stack = arrayRemove(stack, focusScope);
342
- stack[0]?.resume();
343
- }
344
- };
332
+ let stack = [];
333
+ return {
334
+ add(focusScope) {
335
+ const activeFocusScope = stack[0];
336
+ if (focusScope !== activeFocusScope) {
337
+ activeFocusScope?.pause();
338
+ }
339
+ stack = arrayRemove(stack, focusScope);
340
+ stack.unshift(focusScope);
341
+ },
342
+ remove(focusScope) {
343
+ stack = arrayRemove(stack, focusScope);
344
+ stack[0]?.resume();
345
+ }
346
+ };
345
347
  }
346
348
  function arrayRemove(array, item) {
347
- const updatedArray = [...array];
348
- const index = updatedArray.indexOf(item);
349
- if (index !== -1) {
350
- updatedArray.splice(index, 1);
351
- }
352
- return updatedArray;
349
+ const updatedArray = [...array];
350
+ const index = updatedArray.indexOf(item);
351
+ if (index !== -1) {
352
+ updatedArray.splice(index, 1);
353
+ }
354
+ return updatedArray;
353
355
  }
354
356
  function removeLinks(items) {
355
- return items.filter(item => item.tagName !== "A");
357
+ return items.filter((item) => item.tagName !== "A");
356
358
  }
359
+
357
360
  export { FocusScope, useFocusScope };
358
361
  //# sourceMappingURL=FocusScope.mjs.map