@react-aria/focus 3.17.0 → 3.18.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.
@@ -1,6 +1,6 @@
1
1
  import {getFocusableTreeWalker as $9bf71ea28793e738$export$2d6ec8fc375ceafa} from "./FocusScope.module.js";
2
- import {useState as $hGAaG$useState} from "react";
3
2
  import {useLayoutEffect as $hGAaG$useLayoutEffect} from "@react-aria/utils";
3
+ import {useState as $hGAaG$useState} from "react";
4
4
 
5
5
  /*
6
6
  * Copyright 2022 Adobe. All rights reserved.
@@ -36,8 +36,8 @@ function $83013635b024ae3d$export$eac1895992b9f3d6(ref, options) {
36
36
  childList: true,
37
37
  attributes: true,
38
38
  attributeFilter: [
39
- "tabIndex",
40
- "disabled"
39
+ 'tabIndex',
40
+ 'disabled'
41
41
  ]
42
42
  });
43
43
  return ()=>{
@@ -1 +1 @@
1
- {"mappings":";;;;AAAA;;;;;;;;;;CAUC;;;AAmBM,SAAS,0CAAoB,GAAuB,EAAE,OAAqC;IAChG,IAAI,aAAa,oBAAA,8BAAA,QAAS,UAAU;IACpC,IAAI,CAAC,kBAAkB,oBAAoB,GAAG,CAAA,GAAA,eAAO,EAAE;IAEvD,CAAA,GAAA,sBAAc,EAAE;QACd,IAAI,CAAA,gBAAA,0BAAA,IAAK,OAAO,KAAI,CAAC,YAAY;YAC/B,IAAI,SAAS;gBACX,IAAI,IAAI,OAAO,EAAE;oBACf,IAAI,SAAS,CAAA,GAAA,yCAAqB,EAAE,IAAI,OAAO,EAAE;wBAAC,UAAU;oBAAI;oBAChE,oBAAoB,CAAC,CAAC,OAAO,QAAQ;gBACvC;YACF;YAEA;YAEA,qFAAqF;YACrF,IAAI,WAAW,IAAI,iBAAiB;YACpC,SAAS,OAAO,CAAC,IAAI,OAAO,EAAE;gBAC5B,SAAS;gBACT,WAAW;gBACX,YAAY;gBACZ,iBAAiB;oBAAC;oBAAY;iBAAW;YAC3C;YAEA,OAAO;gBACL,qFAAqF;gBACrF,wFAAwF;gBACxF,4FAA4F;gBAC5F,yFAAyF;gBACzF,SAAS,UAAU;YACrB;QACF;IACF;IAEA,OAAO,aAAa,QAAQ;AAC9B","sources":["packages/@react-aria/focus/src/useHasTabbableChild.ts"],"sourcesContent":["/*\n * Copyright 2022 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {getFocusableTreeWalker} from './FocusScope';\nimport {RefObject, useState} from 'react';\nimport {useLayoutEffect} from '@react-aria/utils';\n\ninterface AriaHasTabbableChildOptions {\n isDisabled?: boolean\n}\n\n// This was created for a special empty case of a component that can have child or\n// be empty, like Collection/Virtualizer/Table/ListView/etc. When these components\n// are empty they can have a message with a tabbable element, which is like them\n// being not empty, when it comes to focus and tab order.\n\n/**\n * Returns whether an element has a tabbable child, and updates as children change.\n * @private\n */\nexport function useHasTabbableChild(ref: RefObject<Element>, options?: AriaHasTabbableChildOptions): boolean {\n let isDisabled = options?.isDisabled;\n let [hasTabbableChild, setHasTabbableChild] = useState(false);\n\n useLayoutEffect(() => {\n if (ref?.current && !isDisabled) {\n let update = () => {\n if (ref.current) {\n let walker = getFocusableTreeWalker(ref.current, {tabbable: true});\n setHasTabbableChild(!!walker.nextNode());\n }\n };\n\n update();\n\n // Update when new elements are inserted, or the tabIndex/disabled attribute updates.\n let observer = new MutationObserver(update);\n observer.observe(ref.current, {\n subtree: true,\n childList: true,\n attributes: true,\n attributeFilter: ['tabIndex', 'disabled']\n });\n\n return () => {\n // Disconnect mutation observer when a React update occurs on the top-level component\n // so we update synchronously after re-rendering. Otherwise React will emit act warnings\n // in tests since mutation observers fire asynchronously. The mutation observer is necessary\n // so we also update if a child component re-renders and adds/removes something tabbable.\n observer.disconnect();\n };\n }\n });\n\n return isDisabled ? false : hasTabbableChild;\n}\n"],"names":[],"version":3,"file":"useHasTabbableChild.module.js.map"}
1
+ {"mappings":";;;;AAAA;;;;;;;;;;CAUC;;;AAoBM,SAAS,0CAAoB,GAA8B,EAAE,OAAqC;IACvG,IAAI,aAAa,oBAAA,8BAAA,QAAS,UAAU;IACpC,IAAI,CAAC,kBAAkB,oBAAoB,GAAG,CAAA,GAAA,eAAO,EAAE;IAEvD,CAAA,GAAA,sBAAc,EAAE;QACd,IAAI,CAAA,gBAAA,0BAAA,IAAK,OAAO,KAAI,CAAC,YAAY;YAC/B,IAAI,SAAS;gBACX,IAAI,IAAI,OAAO,EAAE;oBACf,IAAI,SAAS,CAAA,GAAA,yCAAqB,EAAE,IAAI,OAAO,EAAE;wBAAC,UAAU;oBAAI;oBAChE,oBAAoB,CAAC,CAAC,OAAO,QAAQ;gBACvC;YACF;YAEA;YAEA,qFAAqF;YACrF,IAAI,WAAW,IAAI,iBAAiB;YACpC,SAAS,OAAO,CAAC,IAAI,OAAO,EAAE;gBAC5B,SAAS;gBACT,WAAW;gBACX,YAAY;gBACZ,iBAAiB;oBAAC;oBAAY;iBAAW;YAC3C;YAEA,OAAO;gBACL,qFAAqF;gBACrF,wFAAwF;gBACxF,4FAA4F;gBAC5F,yFAAyF;gBACzF,SAAS,UAAU;YACrB;QACF;IACF;IAEA,OAAO,aAAa,QAAQ;AAC9B","sources":["packages/@react-aria/focus/src/useHasTabbableChild.ts"],"sourcesContent":["/*\n * Copyright 2022 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {getFocusableTreeWalker} from './FocusScope';\nimport {RefObject} from '@react-types/shared';\nimport {useLayoutEffect} from '@react-aria/utils';\nimport {useState} from 'react';\n\ninterface AriaHasTabbableChildOptions {\n isDisabled?: boolean\n}\n\n// This was created for a special empty case of a component that can have child or\n// be empty, like Collection/Virtualizer/Table/ListView/etc. When these components\n// are empty they can have a message with a tabbable element, which is like them\n// being not empty, when it comes to focus and tab order.\n\n/**\n * Returns whether an element has a tabbable child, and updates as children change.\n * @private\n */\nexport function useHasTabbableChild(ref: RefObject<Element | null>, options?: AriaHasTabbableChildOptions): boolean {\n let isDisabled = options?.isDisabled;\n let [hasTabbableChild, setHasTabbableChild] = useState(false);\n\n useLayoutEffect(() => {\n if (ref?.current && !isDisabled) {\n let update = () => {\n if (ref.current) {\n let walker = getFocusableTreeWalker(ref.current, {tabbable: true});\n setHasTabbableChild(!!walker.nextNode());\n }\n };\n\n update();\n\n // Update when new elements are inserted, or the tabIndex/disabled attribute updates.\n let observer = new MutationObserver(update);\n observer.observe(ref.current, {\n subtree: true,\n childList: true,\n attributes: true,\n attributeFilter: ['tabIndex', 'disabled']\n });\n\n return () => {\n // Disconnect mutation observer when a React update occurs on the top-level component\n // so we update synchronously after re-rendering. Otherwise React will emit act warnings\n // in tests since mutation observers fire asynchronously. The mutation observer is necessary\n // so we also update if a child component re-renders and adds/removes something tabbable.\n observer.disconnect();\n };\n }\n });\n\n return isDisabled ? false : hasTabbableChild;\n}\n"],"names":[],"version":3,"file":"useHasTabbableChild.module.js.map"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-aria/focus",
3
- "version": "3.17.0",
3
+ "version": "3.18.0",
4
4
  "description": "Spectrum UI components in React",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/main.js",
@@ -22,17 +22,17 @@
22
22
  "url": "https://github.com/adobe/react-spectrum"
23
23
  },
24
24
  "dependencies": {
25
- "@react-aria/interactions": "^3.21.2",
26
- "@react-aria/utils": "^3.24.0",
27
- "@react-types/shared": "^3.23.0",
25
+ "@react-aria/interactions": "^3.22.0",
26
+ "@react-aria/utils": "^3.25.0",
27
+ "@react-types/shared": "^3.24.0",
28
28
  "@swc/helpers": "^0.5.0",
29
29
  "clsx": "^2.0.0"
30
30
  },
31
31
  "peerDependencies": {
32
- "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
32
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
33
33
  },
34
34
  "publishConfig": {
35
35
  "access": "public"
36
36
  },
37
- "gitHead": "f645f29edc1322153fd60af4640cbcab1d992dbd"
37
+ "gitHead": "86d80e3216bc32e75108831cf3a5a720bc849206"
38
38
  }
package/src/FocusRing.tsx CHANGED
@@ -45,7 +45,7 @@ export function FocusRing(props: FocusRingProps) {
45
45
  let {isFocused, isFocusVisible, focusProps} = useFocusRing(props);
46
46
  let child = React.Children.only(children);
47
47
 
48
- return React.cloneElement(child, mergeProps(child.props, {
48
+ return React.cloneElement(child, mergeProps(child.props as any, {
49
49
  ...focusProps,
50
50
  className: clsx({
51
51
  [focusClass || '']: isFocused,
@@ -10,11 +10,11 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {FocusableElement} from '@react-types/shared';
13
+ import {FocusableElement, RefObject} from '@react-types/shared';
14
14
  import {focusSafely} from './focusSafely';
15
15
  import {getOwnerDocument, useLayoutEffect} from '@react-aria/utils';
16
16
  import {isElementVisible} from './isElementVisible';
17
- import React, {ReactNode, RefObject, useContext, useEffect, useMemo, useRef} from 'react';
17
+ import React, {ReactNode, useContext, useEffect, useMemo, useRef} from 'react';
18
18
 
19
19
  export interface FocusScopeProps {
20
20
  /** The contents of the focus scope. */
@@ -58,13 +58,14 @@ export interface FocusManager {
58
58
  focusLast(opts?: FocusManagerOptions): FocusableElement | null
59
59
  }
60
60
 
61
- type ScopeRef = RefObject<Element[]> | null;
61
+ type ScopeRef = RefObject<Element[] | null> | null;
62
62
  interface IFocusContext {
63
63
  focusManager: FocusManager,
64
64
  parentNode: TreeNode | null
65
65
  }
66
66
 
67
67
  const FocusContext = React.createContext<IFocusContext | null>(null);
68
+ const RESTORE_FOCUS_EVENT = 'react-aria-focus-scope-restore';
68
69
 
69
70
  let activeScope: ScopeRef = null;
70
71
 
@@ -117,12 +118,21 @@ export function FocusScope(props: FocusScopeProps) {
117
118
  // Find all rendered nodes between the sentinels and add them to the scope.
118
119
  let node = startRef.current?.nextSibling!;
119
120
  let nodes: Element[] = [];
121
+ let stopPropagation = e => e.stopPropagation();
120
122
  while (node && node !== endRef.current) {
121
123
  nodes.push(node as Element);
124
+ // Stop custom restore focus event from propagating to parent focus scopes.
125
+ node.addEventListener(RESTORE_FOCUS_EVENT, stopPropagation);
122
126
  node = node.nextSibling as Element;
123
127
  }
124
128
 
125
129
  scopeRef.current = nodes;
130
+
131
+ return () => {
132
+ for (let node of nodes) {
133
+ node.removeEventListener(RESTORE_FOCUS_EVENT, stopPropagation);
134
+ }
135
+ };
126
136
  }, [children]);
127
137
 
128
138
  useActiveScopeTracker(scopeRef, restoreFocus, contain);
@@ -192,7 +202,7 @@ export function useFocusManager(): FocusManager | undefined {
192
202
  return useContext(FocusContext)?.focusManager;
193
203
  }
194
204
 
195
- function createFocusManagerForScope(scopeRef: React.RefObject<Element[]>): FocusManager {
205
+ function createFocusManagerForScope(scopeRef: React.RefObject<Element[] | null>): FocusManager {
196
206
  return {
197
207
  focusNext(opts: FocusManagerOptions = {}) {
198
208
  let scope = scopeRef.current!;
@@ -299,10 +309,10 @@ function shouldContainFocus(scopeRef: ScopeRef) {
299
309
  return true;
300
310
  }
301
311
 
302
- function useFocusContainment(scopeRef: RefObject<Element[]>, contain?: boolean) {
303
- let focusedNode = useRef<FocusableElement>();
312
+ function useFocusContainment(scopeRef: RefObject<Element[] | null>, contain?: boolean) {
313
+ let focusedNode = useRef<FocusableElement>(undefined);
304
314
 
305
- let raf = useRef<ReturnType<typeof requestAnimationFrame>>();
315
+ let raf = useRef<ReturnType<typeof requestAnimationFrame>>(undefined);
306
316
  useLayoutEffect(() => {
307
317
  let scope = scopeRef.current;
308
318
  if (!contain) {
@@ -470,7 +480,7 @@ function focusElement(element: FocusableElement | null, scroll = false) {
470
480
  }
471
481
  }
472
482
 
473
- function focusFirstInScope(scope: Element[], tabbable:boolean = true) {
483
+ function getFirstInScope(scope: Element[], tabbable = true) {
474
484
  let sentinel = scope[0].previousElementSibling!;
475
485
  let scopeRoot = getScopeRoot(scope);
476
486
  let walker = getFocusableTreeWalker(scopeRoot, {tabbable}, scope);
@@ -485,10 +495,14 @@ function focusFirstInScope(scope: Element[], tabbable:boolean = true) {
485
495
  nextNode = walker.nextNode();
486
496
  }
487
497
 
488
- focusElement(nextNode as FocusableElement);
498
+ return nextNode as FocusableElement;
489
499
  }
490
500
 
491
- function useAutoFocus(scopeRef: RefObject<Element[]>, autoFocus?: boolean) {
501
+ function focusFirstInScope(scope: Element[], tabbable:boolean = true) {
502
+ focusElement(getFirstInScope(scope, tabbable));
503
+ }
504
+
505
+ function useAutoFocus(scopeRef: RefObject<Element[] | null>, autoFocus?: boolean) {
492
506
  const autoFocusRef = React.useRef(autoFocus);
493
507
  useEffect(() => {
494
508
  if (autoFocusRef.current) {
@@ -502,7 +516,7 @@ function useAutoFocus(scopeRef: RefObject<Element[]>, autoFocus?: boolean) {
502
516
  }, [scopeRef]);
503
517
  }
504
518
 
505
- function useActiveScopeTracker(scopeRef: RefObject<Element[]>, restore?: boolean, contain?: boolean) {
519
+ function useActiveScopeTracker(scopeRef: RefObject<Element[] | null>, restore?: boolean, contain?: boolean) {
506
520
  // tracks the active scope, in case restore and contain are both false.
507
521
  // if either are true, this is tracked in useRestoreFocus or useFocusContainment.
508
522
  useLayoutEffect(() => {
@@ -544,7 +558,7 @@ function shouldRestoreFocus(scopeRef: ScopeRef) {
544
558
  return scope?.scopeRef === scopeRef;
545
559
  }
546
560
 
547
- function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus?: boolean, contain?: boolean) {
561
+ function useRestoreFocus(scopeRef: RefObject<Element[] | null>, restoreFocus?: boolean, contain?: boolean) {
548
562
  // create a ref during render instead of useLayoutEffect so the active element is saved before a child with autoFocus=true mounts.
549
563
  // eslint-disable-next-line no-restricted-globals
550
564
  const nodeToRestoreRef = useRef(typeof document !== 'undefined' ? getOwnerDocument(scopeRef.current ? scopeRef.current[0] : undefined).activeElement as FocusableElement : null);
@@ -679,8 +693,7 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus?: boolean,
679
693
  && nodeToRestore
680
694
  && (
681
695
  // eslint-disable-next-line react-hooks/exhaustive-deps
682
- isElementInScope(ownerDocument.activeElement, scopeRef.current)
683
- || (ownerDocument.activeElement === ownerDocument.body && shouldRestoreFocus(scopeRef))
696
+ (isElementInScope(ownerDocument.activeElement, scopeRef.current) || (ownerDocument.activeElement === ownerDocument.body && shouldRestoreFocus(scopeRef)))
684
697
  )
685
698
  ) {
686
699
  // freeze the focusScopeTree so it persists after the raf, otherwise during unmount nodes are removed from it
@@ -692,7 +705,7 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus?: boolean,
692
705
  let treeNode = clonedTree.getTreeNode(scopeRef);
693
706
  while (treeNode) {
694
707
  if (treeNode.nodeToRestore && treeNode.nodeToRestore.isConnected) {
695
- focusElement(treeNode.nodeToRestore);
708
+ restoreFocusToElement(treeNode.nodeToRestore);
696
709
  return;
697
710
  }
698
711
  treeNode = treeNode.parent;
@@ -703,7 +716,8 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus?: boolean,
703
716
  treeNode = clonedTree.getTreeNode(scopeRef);
704
717
  while (treeNode) {
705
718
  if (treeNode.scopeRef && treeNode.scopeRef.current && focusScopeTree.getTreeNode(treeNode.scopeRef)) {
706
- focusFirstInScope(treeNode.scopeRef.current, true);
719
+ let node = getFirstInScope(treeNode.scopeRef.current, true);
720
+ restoreFocusToElement(node);
707
721
  return;
708
722
  }
709
723
  treeNode = treeNode.parent;
@@ -715,6 +729,15 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus?: boolean,
715
729
  }, [scopeRef, restoreFocus]);
716
730
  }
717
731
 
732
+ function restoreFocusToElement(node: FocusableElement) {
733
+ // Dispatch a custom event that parent elements can intercept to customize focus restoration.
734
+ // For example, virtualized collection components reuse DOM elements, so the original element
735
+ // might still exist in the DOM but representing a different item.
736
+ if (node.dispatchEvent(new CustomEvent(RESTORE_FOCUS_EVENT, {bubbles: true, cancelable: true}))) {
737
+ focusElement(node);
738
+ }
739
+ }
740
+
718
741
  /**
719
742
  * Create a [TreeWalker]{@link https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker}
720
743
  * that matches all focusable/tabbable elements.
@@ -754,7 +777,7 @@ export function getFocusableTreeWalker(root: Element, opts?: FocusManagerOptions
754
777
  /**
755
778
  * Creates a FocusManager object that can be used to move focus within an element.
756
779
  */
757
- export function createFocusManager(ref: RefObject<Element>, defaultOptions: FocusManagerOptions = {}): FocusManager {
780
+ export function createFocusManager(ref: RefObject<Element | null>, defaultOptions: FocusManagerOptions = {}): FocusManager {
758
781
  return {
759
782
  focusNext(opts: FocusManagerOptions = {}) {
760
783
  let root = ref.current;
@@ -10,10 +10,10 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {DOMAttributes, FocusableDOMProps, FocusableElement, FocusableProps} from '@react-types/shared';
13
+ import {DOMAttributes, FocusableDOMProps, FocusableElement, FocusableProps, RefObject} from '@react-types/shared';
14
14
  import {focusSafely} from './';
15
15
  import {mergeProps, useObjectRef, useSyncRef} from '@react-aria/utils';
16
- import React, {ForwardedRef, MutableRefObject, ReactNode, RefObject, useContext, useEffect, useRef} from 'react';
16
+ import React, {ForwardedRef, MutableRefObject, ReactNode, useContext, useEffect, useRef} from 'react';
17
17
  import {useFocus, useKeyboard} from '@react-aria/interactions';
18
18
 
19
19
  export interface FocusableOptions extends FocusableProps, FocusableDOMProps {
@@ -32,7 +32,7 @@ interface FocusableContextValue extends FocusableProviderProps {
32
32
 
33
33
  let FocusableContext = React.createContext<FocusableContextValue | null>(null);
34
34
 
35
- function useFocusableContext(ref: RefObject<FocusableElement>): FocusableContextValue {
35
+ function useFocusableContext(ref: RefObject<FocusableElement | null>): FocusableContextValue {
36
36
  let context = useContext(FocusableContext) || {};
37
37
  useSyncRef(context, ref);
38
38
 
@@ -70,7 +70,7 @@ export interface FocusableAria {
70
70
  /**
71
71
  * Used to make an element focusable and capable of auto focus.
72
72
  */
73
- export function useFocusable(props: FocusableOptions, domRef: RefObject<FocusableElement>): FocusableAria {
73
+ export function useFocusable(props: FocusableOptions, domRef: RefObject<FocusableElement | null>): FocusableAria {
74
74
  let {focusProps} = useFocus(props);
75
75
  let {keyboardProps} = useKeyboard(props);
76
76
  let interactions = mergeProps(focusProps, keyboardProps);
@@ -11,8 +11,9 @@
11
11
  */
12
12
 
13
13
  import {getFocusableTreeWalker} from './FocusScope';
14
- import {RefObject, useState} from 'react';
14
+ import {RefObject} from '@react-types/shared';
15
15
  import {useLayoutEffect} from '@react-aria/utils';
16
+ import {useState} from 'react';
16
17
 
17
18
  interface AriaHasTabbableChildOptions {
18
19
  isDisabled?: boolean
@@ -27,7 +28,7 @@ interface AriaHasTabbableChildOptions {
27
28
  * Returns whether an element has a tabbable child, and updates as children change.
28
29
  * @private
29
30
  */
30
- export function useHasTabbableChild(ref: RefObject<Element>, options?: AriaHasTabbableChildOptions): boolean {
31
+ export function useHasTabbableChild(ref: RefObject<Element | null>, options?: AriaHasTabbableChildOptions): boolean {
31
32
  let isDisabled = options?.isDisabled;
32
33
  let [hasTabbableChild, setHasTabbableChild] = useState(false);
33
34