Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions packages/react-aria/src/interactions/useFocusVisible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,17 +110,29 @@ function handleClickEvent(e: MouseEvent) {
}

function handleFocusEvent(e: FocusEvent) {
if (ignoreFocusEvent) {
return;
}

let target = getEventTarget(e);
let ownerWindow = getOwnerWindow(target);
let ownerDocument = getOwnerDocument(target);

// When the window regains focus, the browser restores focus to the element that was focused
// before, firing a focus event the user did not initiate. handleWindowBlur sets
// hasBlurredWindowRecently so restored focus doesn't switch to virtual modality below, but
// Safari fires the window/element focus pair twice when returning to a tab or app and the first
// element focus event clears the flag, so re-arm it whenever the window itself is focused.
// Like handleWindowBlur, this intentionally doesn't check isTrusted.
if (target === ownerWindow) {
hasBlurredWindowRecently = true;
return;
}

// Firefox fires two extra focus events when the user first clicks into an iframe:
// first on the window, then on the document. We ignore these events so they don't
// cause keyboard focus rings to appear.
let ownerWindow = getOwnerWindow(getEventTarget(e));
let ownerDocument = getOwnerDocument(getEventTarget(e));
if (
getEventTarget(e) === ownerWindow ||
getEventTarget(e) === ownerDocument ||
ignoreFocusEvent ||
!e.isTrusted
) {
if (target === ownerDocument || !e.isTrusted) {
return;
}

Expand Down
60 changes: 60 additions & 0 deletions packages/react-aria/test/interactions/useFocusVisible.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,45 @@ function toggleBrowserTabs(win = window) {
fireEvent(lastActiveElement, new Event('focus'));
}

// Focus the element the way the browser restores focus. useFocusVisible ignores untrusted focus
// events and jsdom only marks its own as trusted, so go through jsdom's focus() rather than
// fireEvent, using the unpatched copy saved by setupGlobalFocusEvents so it isn't recorded as a
// programmatic focus. jsdom won't dispatch focus again for the element that already has it, hence
// the blur first — useFocusVisible doesn't observe element blur, since its blur listener is on the
// window without capture and blur doesn't bubble.
function restoreFocus(element, win = window) {
const nativeFocus = hasSetupGlobalListeners.get(win).focus;
element.blur();
nativeFocus.call(element);
}

function toggleBrowserTabsSafari(win = window) {
// Safari fires the window/element focus pair twice when returning to a tab or app, and
// visibilitychange fires after all of the focus events.
// See https://github.com/adobe/react-spectrum/issues/7468
const lastActiveElement = win.document.activeElement;
// leave tab
fireEvent(lastActiveElement, new Event('blur'));
fireEvent(win, new Event('blur'));
Object.defineProperty(win.document, 'visibilityState', {
value: 'hidden',
writable: true
});
Object.defineProperty(win.document, 'hidden', {value: true, writable: true});
fireEvent(win.document, new Event('visibilitychange'));
// return to tab
fireEvent(win, new Event('focus', {target: win}));
restoreFocus(lastActiveElement, win);
fireEvent(win, new Event('focus', {target: win}));
restoreFocus(lastActiveElement, win);
Object.defineProperty(win.document, 'visibilityState', {
value: 'visible',
writable: true
});
Object.defineProperty(win.document, 'hidden', {value: false, writable: true});
fireEvent(win.document, new Event('visibilitychange'));
}

function toggleBrowserWindow(win = window) {
fireEvent(win, new Event('blur', {target: win}));
fireEvent(win, new Event('focus', {target: win}));
Expand Down Expand Up @@ -111,6 +150,27 @@ describe('useFocusVisible', function () {
expect(el.textContent).toBe('example');
});

it('returns positive isFocusVisible result after toggling browser tabs in Safari after keyboard navigation', async function () {
render(<Example />);
await user.tab();
let el = screen.getByText('example-focusVisible');

toggleBrowserTabsSafari();

expect(el.textContent).toBe('example-focusVisible');
});

it('returns negative isFocusVisible result after toggling browser tabs in Safari without prior keyboard navigation', async function () {
render(<Example />);
await user.tab();
let el = screen.getByText('example-focusVisible');

await user.click(el);
toggleBrowserTabsSafari();

expect(el.textContent).toBe('example');
});

it('returns positive isFocusVisible result after toggling browser window after keyboard navigation', async function () {
render(<Example />);
await user.tab();
Expand Down