Skip to content

Commit a5f4e54

Browse files
committed
fix(#2766): add focusSafely story
Update focusSafely method to account for focusing an element safely after a pointer event on another element.
1 parent 359fa51 commit a5f4e54

File tree

2 files changed

+54
-4
lines changed

2 files changed

+54
-4
lines changed

packages/@react-aria/focus/src/focusSafely.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,14 @@ const intersectionObserver = (() => {
5050
*/
5151
export function focusSafely(element: HTMLElement) {
5252
const modality = getInteractionModality();
53+
const lastFocusedElement = document.activeElement;
5354

5455
// If the user is interacting with a virtual cursor, e.g. screen reader, then
5556
// wait until after any animated transitions that are currently occurring on
5657
// the page before shifting focus. This avoids issues with VoiceOver on iOS
5758
// causing the page to scroll when moving focus if the element is transitioning
5859
// from off the screen.
5960
if (modality === 'virtual') {
60-
let lastFocusedElement = document.activeElement;
6161
runAfterTransition(() => {
6262
// If focus did not move and the element is still in the document, focus it.
6363
if (document.activeElement === lastFocusedElement && document.contains(element)) {
@@ -69,7 +69,19 @@ export function focusSafely(element: HTMLElement) {
6969
});
7070
} else {
7171
focusWithoutScrolling(element);
72-
if (intersectionObserver && modality !== 'pointer') {
72+
if (intersectionObserver &&
73+
(
74+
// Don't test for intersectionObserver to scroll the element into view
75+
// within the document body on pointer events, unless the element being focused
76+
// focusing safely is not the element that received interaction to focus it.
77+
modality !== 'pointer' ||
78+
!(
79+
lastFocusedElement.contains(element) ||
80+
element.contains(lastFocusedElement) ||
81+
element === lastFocusedElement
82+
)
83+
)
84+
) {
7385
intersectionObserver.observe(element);
7486
}
7587
}

packages/@react-aria/focus/stories/FocusScope.stories.tsx

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {FocusScope} from '../';
13+
import {focusSafely, FocusScope} from '../';
1414
import {Meta, Story} from '@storybook/react';
15-
import React, {ReactNode, useState} from 'react';
15+
import React, {ReactNode, useRef, useState} from 'react';
1616
import ReactDOM from 'react-dom';
1717

1818
const dialogsRoot = 'dialogsRoot';
@@ -116,3 +116,41 @@ KeyboardNavigationNoContain.args = {usePortal: false, contain: false};
116116

117117
export const KeyboardNavigationInsidePortalNoContain = Template().bind({});
118118
KeyboardNavigationInsidePortalNoContain.args = {usePortal: true, contain: false};
119+
120+
const FocusSafelyTemplate = (): Story => () => <FocusSafelyExample />;
121+
122+
function FocusSafelyExample() {
123+
const ref = useRef<HTMLButtonElement>();
124+
const w = 12.5;
125+
return (
126+
<div>
127+
<button
128+
type="button"
129+
onClick={() => focusSafely(ref.current)}>
130+
Focus Safely
131+
</button>
132+
<div
133+
style={{
134+
background: 'var(--spectrum-global-color-gray-50)',
135+
border: '1px solid lightgray',
136+
display: 'block',
137+
margin: '1rem 0',
138+
maxHeight: `${1.5 * w}rem`,
139+
minWidth: `${w}rem`,
140+
overflow: 'auto',
141+
padding: '0 1rem'
142+
}}>
143+
<p><code>button</code> below the fold ⬇︎</p>
144+
<button
145+
type="button"
146+
ref={ref}
147+
style={{
148+
margin: `${1.6 * w}rem 0`
149+
}}>Button to focus safely</button>
150+
<p><code>button</code> above the fold ⬆︎</p>
151+
</div>
152+
</div>
153+
);
154+
}
155+
156+
export const FocusSafely = FocusSafelyTemplate().bind({});

0 commit comments

Comments
 (0)