@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.
@@ -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
- function handleFocusOut(event: FocusEvent) {
62
- if (focusScope.paused || !container) return
63
- if (!container.contains(event.relatedTarget as HTMLElement | null)) {
64
- focus(lastFocusedElementRef.current, { select: true })
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
- document.addEventListener('focusin', handleFocusIn)
69
- document.addEventListener('focusout', handleFocusOut)
70
- return () => {
71
- document.removeEventListener('focusin', handleFocusIn)
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
- }, [trapped, forceUnmount, container, focusScope.paused])
75
-
76
- React.useEffect(() => {
77
- if (!container) return
78
- if (forceUnmount) return
79
- focusScopesStack.add(focusScope)
80
- const previouslyFocusedElement = document.activeElement as HTMLElement | null
81
- const hasFocusedCandidate = container.contains(previouslyFocusedElement)
82
-
83
- if (!hasFocusedCandidate) {
84
- const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT, EVENT_OPTIONS)
85
- container.addEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus)
86
- container.dispatchEvent(mountEvent)
87
- if (!mountEvent.defaultPrevented) {
88
- focusFirst(removeLinks(getTabbableCandidates(container)), { select: true })
89
- if (document.activeElement === previouslyFocusedElement) {
90
- focus(container)
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
- return () => {
96
- container.removeEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus)
110
+ return () => {
111
+ container.removeEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus)
97
112
 
98
- const unmountEvent = new CustomEvent(AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS)
99
- container.addEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus)
100
- container.dispatchEvent(unmountEvent)
101
- if (!unmountEvent.defaultPrevented) {
102
- focus(previouslyFocusedElement ?? document.body, { select: true })
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
- }, [container, forceUnmount, onMountAutoFocus, onUnmountAutoFocus, focusScope])
110
-
111
- // Takes care of looping focus (when tabbing whilst at the edges)
112
- const handleKeyDown = React.useCallback(
113
- (event: React.KeyboardEvent) => {
114
- if (!(loop || trapped)) return
115
- if (focusScope.paused) return
116
-
117
- const isTabKey =
118
- event.key === 'Tab' && !event.altKey && !event.ctrlKey && !event.metaKey
119
- const focusedElement = document.activeElement as HTMLElement | null
120
-
121
- if (isTabKey && focusedElement) {
122
- const container = event.currentTarget as HTMLElement
123
- const [first, last] = getTabbableEdges(container)
124
- const hasTabbableElementsInside = first && last
125
-
126
- // we can only wrap focus if we have tabbable edges
127
- if (!hasTabbableElementsInside) {
128
- if (focusedElement === container) event.preventDefault()
129
- } else {
130
- if (!event.shiftKey && focusedElement === last) {
131
- event.preventDefault()
132
- if (loop) focus(first, { select: true })
133
- } else if (event.shiftKey && focusedElement === first) {
134
- event.preventDefault()
135
- if (loop) focus(last, { select: true })
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
- [loop, trapped, focusScope.paused],
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
- FocusScope.displayName = FOCUS_SCOPE_NAME
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
  }
@@ -32,5 +32,11 @@ export interface FocusScopeProps {
32
32
  */
33
33
  forceUnmount?: boolean
34
34
 
35
- children?: React.ReactNode
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
  }
@@ -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;AAgBnD,QAAA,MAAM,UAAU,wFAmIf,CAAA;AA8ID,OAAO,EAAE,UAAU,EAAE,CAAA;AAErB,YAAY,EAAE,eAAe,EAAE,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"}