@tamagui/focus-scope 1.43.1 → 1.43.3
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/cjs/FocusScope.js +123 -115
- package/dist/cjs/FocusScope.js.map +2 -2
- package/dist/esm/FocusScope.js +121 -114
- package/dist/esm/FocusScope.js.map +2 -2
- package/dist/jsx/FocusScope.js +120 -114
- package/dist/jsx/FocusScope.js.map +2 -2
- package/dist/jsx/FocusScope.mjs +120 -114
- package/dist/jsx/FocusScope.mjs.map +2 -2
- package/package.json +4 -4
- package/src/FocusScope.tsx +136 -126
- package/src/FocusScopeProps.tsx +7 -1
- package/types/FocusScope.d.ts +11 -0
- package/types/FocusScope.d.ts.map +1 -1
- package/types/FocusScopeProps.d.ts +5 -1
- package/types/FocusScopeProps.d.ts.map +1 -1
package/src/FocusScope.tsx
CHANGED
|
@@ -14,144 +14,155 @@ type FocusableTarget = HTMLElement | { focus(): void }
|
|
|
14
14
|
* FocusScope
|
|
15
15
|
* -----------------------------------------------------------------------------------------------*/
|
|
16
16
|
|
|
17
|
-
const FOCUS_SCOPE_NAME = 'FocusScope'
|
|
18
|
-
|
|
19
17
|
type FocusScopeElement = HTMLDivElement
|
|
20
18
|
|
|
21
19
|
const FocusScope = React.forwardRef<FocusScopeElement, FocusScopeProps>(
|
|
22
|
-
(props, forwardedRef)
|
|
23
|
-
const
|
|
24
|
-
loop = false,
|
|
25
|
-
trapped = false,
|
|
26
|
-
onMountAutoFocus: onMountAutoFocusProp,
|
|
27
|
-
onUnmountAutoFocus: onUnmountAutoFocusProp,
|
|
28
|
-
children,
|
|
29
|
-
forceUnmount,
|
|
30
|
-
...scopeProps
|
|
31
|
-
} = props
|
|
32
|
-
const [container, setContainer] = React.useState<HTMLElement | null>(null)
|
|
33
|
-
const onMountAutoFocus = useEvent(onMountAutoFocusProp)
|
|
34
|
-
const onUnmountAutoFocus = useEvent(onUnmountAutoFocusProp)
|
|
35
|
-
const lastFocusedElementRef = React.useRef<HTMLElement | null>(null)
|
|
36
|
-
const composedRefs = useComposedRefs(forwardedRef, (node) => setContainer(node))
|
|
37
|
-
|
|
38
|
-
const focusScope = React.useRef({
|
|
39
|
-
paused: false,
|
|
40
|
-
pause() {
|
|
41
|
-
this.paused = true
|
|
42
|
-
},
|
|
43
|
-
resume() {
|
|
44
|
-
this.paused = false
|
|
45
|
-
},
|
|
46
|
-
}).current
|
|
47
|
-
|
|
48
|
-
// Takes care of trapping focus if focus is moved outside programmatically for example
|
|
49
|
-
React.useEffect(() => {
|
|
50
|
-
if (!trapped) return
|
|
51
|
-
function handleFocusIn(event: FocusEvent) {
|
|
52
|
-
if (focusScope.paused || !container) return
|
|
53
|
-
const target = event.target as HTMLElement | null
|
|
54
|
-
if (container.contains(target)) {
|
|
55
|
-
lastFocusedElementRef.current = target
|
|
56
|
-
} else {
|
|
57
|
-
focus(lastFocusedElementRef.current, { select: true })
|
|
58
|
-
}
|
|
59
|
-
}
|
|
20
|
+
function FocusScope(props, forwardedRef) {
|
|
21
|
+
const childProps = useFocusScope(props, forwardedRef)
|
|
60
22
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
23
|
+
if (typeof props.children === 'function') {
|
|
24
|
+
return <>{props.children(childProps)}</>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return React.cloneElement(React.Children.only(props.children) as any, childProps)
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
/* -------------------------------------------------------------------------------------------------
|
|
32
|
+
* useFocusScope
|
|
33
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
34
|
+
|
|
35
|
+
export function useFocusScope(
|
|
36
|
+
props: FocusScopeProps,
|
|
37
|
+
forwardedRef: React.ForwardedRef<FocusScopeElement>
|
|
38
|
+
) {
|
|
39
|
+
const {
|
|
40
|
+
loop = false,
|
|
41
|
+
trapped = false,
|
|
42
|
+
onMountAutoFocus: onMountAutoFocusProp,
|
|
43
|
+
onUnmountAutoFocus: onUnmountAutoFocusProp,
|
|
44
|
+
forceUnmount,
|
|
45
|
+
...scopeProps
|
|
46
|
+
} = props
|
|
47
|
+
const [container, setContainer] = React.useState<HTMLElement | null>(null)
|
|
48
|
+
const onMountAutoFocus = useEvent(onMountAutoFocusProp)
|
|
49
|
+
const onUnmountAutoFocus = useEvent(onUnmountAutoFocusProp)
|
|
50
|
+
const lastFocusedElementRef = React.useRef<HTMLElement | null>(null)
|
|
51
|
+
const composedRefs = useComposedRefs(forwardedRef, (node) => setContainer(node))
|
|
52
|
+
|
|
53
|
+
const focusScope = React.useRef({
|
|
54
|
+
paused: false,
|
|
55
|
+
pause() {
|
|
56
|
+
this.paused = true
|
|
57
|
+
},
|
|
58
|
+
resume() {
|
|
59
|
+
this.paused = false
|
|
60
|
+
},
|
|
61
|
+
}).current
|
|
62
|
+
|
|
63
|
+
// Takes care of trapping focus if focus is moved outside programmatically for example
|
|
64
|
+
React.useEffect(() => {
|
|
65
|
+
if (!trapped) return
|
|
66
|
+
function handleFocusIn(event: FocusEvent) {
|
|
67
|
+
if (focusScope.paused || !container) return
|
|
68
|
+
const target = event.target as HTMLElement | null
|
|
69
|
+
if (container.contains(target)) {
|
|
70
|
+
lastFocusedElementRef.current = target
|
|
71
|
+
} else {
|
|
72
|
+
focus(lastFocusedElementRef.current, { select: true })
|
|
66
73
|
}
|
|
74
|
+
}
|
|
67
75
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
document.removeEventListener('focusout', handleFocusOut)
|
|
76
|
+
function handleFocusOut(event: FocusEvent) {
|
|
77
|
+
if (focusScope.paused || !container) return
|
|
78
|
+
if (!container.contains(event.relatedTarget as HTMLElement | null)) {
|
|
79
|
+
focus(lastFocusedElementRef.current, { select: true })
|
|
73
80
|
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
document.addEventListener('focusin', handleFocusIn)
|
|
84
|
+
document.addEventListener('focusout', handleFocusOut)
|
|
85
|
+
return () => {
|
|
86
|
+
document.removeEventListener('focusin', handleFocusIn)
|
|
87
|
+
document.removeEventListener('focusout', handleFocusOut)
|
|
88
|
+
}
|
|
89
|
+
}, [trapped, forceUnmount, container, focusScope.paused])
|
|
90
|
+
|
|
91
|
+
React.useEffect(() => {
|
|
92
|
+
if (!container) return
|
|
93
|
+
if (forceUnmount) return
|
|
94
|
+
focusScopesStack.add(focusScope)
|
|
95
|
+
const previouslyFocusedElement = document.activeElement as HTMLElement | null
|
|
96
|
+
const hasFocusedCandidate = container.contains(previouslyFocusedElement)
|
|
97
|
+
|
|
98
|
+
if (!hasFocusedCandidate) {
|
|
99
|
+
const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT, EVENT_OPTIONS)
|
|
100
|
+
container.addEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus)
|
|
101
|
+
container.dispatchEvent(mountEvent)
|
|
102
|
+
if (!mountEvent.defaultPrevented) {
|
|
103
|
+
focusFirst(removeLinks(getTabbableCandidates(container)), { select: true })
|
|
104
|
+
if (document.activeElement === previouslyFocusedElement) {
|
|
105
|
+
focus(container)
|
|
92
106
|
}
|
|
93
107
|
}
|
|
108
|
+
}
|
|
94
109
|
|
|
95
|
-
|
|
96
|
-
|
|
110
|
+
return () => {
|
|
111
|
+
container.removeEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus)
|
|
97
112
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
// we need to remove the listener after we `dispatchEvent`
|
|
105
|
-
container.removeEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus)
|
|
106
|
-
|
|
107
|
-
focusScopesStack.remove(focusScope)
|
|
113
|
+
const unmountEvent = new CustomEvent(AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS)
|
|
114
|
+
container.addEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus)
|
|
115
|
+
container.dispatchEvent(unmountEvent)
|
|
116
|
+
if (!unmountEvent.defaultPrevented) {
|
|
117
|
+
focus(previouslyFocusedElement ?? document.body, { select: true })
|
|
108
118
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
119
|
+
// we need to remove the listener after we `dispatchEvent`
|
|
120
|
+
container.removeEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus)
|
|
121
|
+
|
|
122
|
+
focusScopesStack.remove(focusScope)
|
|
123
|
+
}
|
|
124
|
+
}, [container, forceUnmount, onMountAutoFocus, onUnmountAutoFocus, focusScope])
|
|
125
|
+
|
|
126
|
+
// Takes care of looping focus (when tabbing whilst at the edges)
|
|
127
|
+
const handleKeyDown = React.useCallback(
|
|
128
|
+
(event: React.KeyboardEvent) => {
|
|
129
|
+
if (!trapped) return
|
|
130
|
+
if (!loop) return
|
|
131
|
+
if (focusScope.paused) return
|
|
132
|
+
|
|
133
|
+
const isTabKey =
|
|
134
|
+
event.key === 'Tab' && !event.altKey && !event.ctrlKey && !event.metaKey
|
|
135
|
+
const focusedElement = document.activeElement as HTMLElement | null
|
|
136
|
+
|
|
137
|
+
if (isTabKey && focusedElement) {
|
|
138
|
+
const container = event.currentTarget as HTMLElement
|
|
139
|
+
const [first, last] = getTabbableEdges(container)
|
|
140
|
+
const hasTabbableElementsInside = first && last
|
|
141
|
+
|
|
142
|
+
// we can only wrap focus if we have tabbable edges
|
|
143
|
+
if (!hasTabbableElementsInside) {
|
|
144
|
+
if (focusedElement === container) event.preventDefault()
|
|
145
|
+
} else {
|
|
146
|
+
if (!event.shiftKey && focusedElement === last) {
|
|
147
|
+
event.preventDefault()
|
|
148
|
+
if (loop) focus(first, { select: true })
|
|
149
|
+
} else if (event.shiftKey && focusedElement === first) {
|
|
150
|
+
event.preventDefault()
|
|
151
|
+
if (loop) focus(last, { select: true })
|
|
137
152
|
}
|
|
138
153
|
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const child = React.Children.only(children)
|
|
144
|
-
|
|
145
|
-
return React.cloneElement(child as any, {
|
|
146
|
-
tabIndex: -1,
|
|
147
|
-
...scopeProps,
|
|
148
|
-
ref: composedRefs,
|
|
149
|
-
onKeyDown: handleKeyDown,
|
|
150
|
-
})
|
|
151
|
-
},
|
|
152
|
-
)
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
[loop, trapped, focusScope.paused]
|
|
157
|
+
)
|
|
153
158
|
|
|
154
|
-
|
|
159
|
+
return {
|
|
160
|
+
tabIndex: -1,
|
|
161
|
+
...scopeProps,
|
|
162
|
+
ref: composedRefs,
|
|
163
|
+
onKeyDown: handleKeyDown,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
155
166
|
|
|
156
167
|
/* -------------------------------------------------------------------------------------------------
|
|
157
168
|
* Utils
|
|
@@ -194,8 +205,7 @@ function getTabbableCandidates(container: HTMLElement) {
|
|
|
194
205
|
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
|
|
195
206
|
acceptNode: (node: any) => {
|
|
196
207
|
const isHiddenInput = node.tagName === 'INPUT' && node.type === 'hidden'
|
|
197
|
-
if (node.disabled || node.hidden || isHiddenInput)
|
|
198
|
-
return NodeFilter.FILTER_SKIP
|
|
208
|
+
if (node.disabled || node.hidden || isHiddenInput) return NodeFilter.FILTER_SKIP
|
|
199
209
|
// `.tabIndex` is not the same as the `tabindex` attribute. It works on the
|
|
200
210
|
// runtime's understanding of tabbability, so this automatically accounts
|
|
201
211
|
// for any kind of element that could be tabbed to.
|
|
@@ -231,7 +241,7 @@ function isHidden(node: HTMLElement, { upTo }: { upTo?: HTMLElement }) {
|
|
|
231
241
|
}
|
|
232
242
|
|
|
233
243
|
function isSelectableInput(
|
|
234
|
-
element: any
|
|
244
|
+
element: any
|
|
235
245
|
): element is FocusableTarget & { select: () => void } {
|
|
236
246
|
return element instanceof HTMLInputElement && 'select' in element
|
|
237
247
|
}
|
package/src/FocusScopeProps.tsx
CHANGED
|
@@ -32,5 +32,11 @@ export interface FocusScopeProps {
|
|
|
32
32
|
*/
|
|
33
33
|
forceUnmount?: boolean
|
|
34
34
|
|
|
35
|
-
children?:
|
|
35
|
+
children?:
|
|
36
|
+
| React.ReactNode
|
|
37
|
+
| ((props: {
|
|
38
|
+
onKeyDown: (event: React.KeyboardEvent) => void
|
|
39
|
+
tabIndex: number
|
|
40
|
+
ref: React.ForwardedRef<any>
|
|
41
|
+
}) => React.ReactNode)
|
|
36
42
|
}
|
package/types/FocusScope.d.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { FocusScopeProps } from './FocusScopeProps';
|
|
3
|
+
type FocusScopeElement = HTMLDivElement;
|
|
3
4
|
declare const FocusScope: React.ForwardRefExoticComponent<FocusScopeProps & React.RefAttributes<HTMLDivElement>>;
|
|
5
|
+
export declare function useFocusScope(props: FocusScopeProps, forwardedRef: React.ForwardedRef<FocusScopeElement>): {
|
|
6
|
+
ref: (node: HTMLDivElement) => void;
|
|
7
|
+
onKeyDown: (event: React.KeyboardEvent) => void;
|
|
8
|
+
children?: React.ReactNode | ((props: {
|
|
9
|
+
onKeyDown: (event: React.KeyboardEvent<Element>) => void;
|
|
10
|
+
tabIndex: number;
|
|
11
|
+
ref: React.ForwardedRef<any>;
|
|
12
|
+
}) => React.ReactNode);
|
|
13
|
+
tabIndex: number;
|
|
14
|
+
};
|
|
4
15
|
export { FocusScope };
|
|
5
16
|
export type { FocusScopeProps };
|
|
6
17
|
//# sourceMappingURL=FocusScope.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FocusScope.d.ts","sourceRoot":"","sources":["../src/FocusScope.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"FocusScope.d.ts","sourceRoot":"","sources":["../src/FocusScope.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAYnD,KAAK,iBAAiB,GAAG,cAAc,CAAA;AAEvC,QAAA,MAAM,UAAU,wFAUf,CAAA;AAMD,wBAAgB,aAAa,CAC3B,KAAK,EAAE,eAAe,EACtB,YAAY,EAAE,KAAK,CAAC,YAAY,CAAC,iBAAiB,CAAC;;uBA2FzC,mBAAmB;;;;;;;EAqC9B;AA2ID,OAAO,EAAE,UAAU,EAAE,CAAA;AAErB,YAAY,EAAE,eAAe,EAAE,CAAA"}
|
|
@@ -26,6 +26,10 @@ export interface FocusScopeProps {
|
|
|
26
26
|
* If unmount is animated, you want to force re-focus at start of animation not after
|
|
27
27
|
*/
|
|
28
28
|
forceUnmount?: boolean;
|
|
29
|
-
children?: React.ReactNode
|
|
29
|
+
children?: React.ReactNode | ((props: {
|
|
30
|
+
onKeyDown: (event: React.KeyboardEvent) => void;
|
|
31
|
+
tabIndex: number;
|
|
32
|
+
ref: React.ForwardedRef<any>;
|
|
33
|
+
}) => React.ReactNode);
|
|
30
34
|
}
|
|
31
35
|
//# sourceMappingURL=FocusScopeProps.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FocusScopeProps.d.ts","sourceRoot":"","sources":["../src/FocusScopeProps.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;IAEd;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAEzC;;;OAGG;IACH,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAE3C;;OAEG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IAEtB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B"}
|
|
1
|
+
{"version":3,"file":"FocusScopeProps.d.ts","sourceRoot":"","sources":["../src/FocusScopeProps.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;IAEd;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAEzC;;;OAGG;IACH,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAE3C;;OAEG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IAEtB,QAAQ,CAAC,EACL,KAAK,CAAC,SAAS,GACf,CAAC,CAAC,KAAK,EAAE;QACP,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,aAAa,KAAK,IAAI,CAAA;QAC/C,QAAQ,EAAE,MAAM,CAAA;QAChB,GAAG,EAAE,KAAK,CAAC,YAAY,CAAC,GAAG,CAAC,CAAA;KAC7B,KAAK,KAAK,CAAC,SAAS,CAAC,CAAA;CAC3B"}
|