Skip to content

Commit

Permalink
fix(useFocus): avoid closing floating element when focus moves inside…
Browse files Browse the repository at this point in the history
… shadow roots (#2773)

Co-authored-by: Sven Tschui <sven.tschui@ti8m.ch>
  • Loading branch information
sventschui and Sven Tschui committed Feb 3, 2024
1 parent f74524d commit a8583f3
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/four-windows-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@floating-ui/react": patch
---

Do not close the floating element when focus moves inside shadow roots
8 changes: 6 additions & 2 deletions packages/react/src/hooks/useFocus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,13 @@ export function useFocus<RT extends ReferenceType = ReferenceType>(
// When focusing the reference element (e.g. regular click), then
// clicking into the floating element, prevent it from hiding.
// Note: it must be focusable, e.g. `tabindex="-1"`.
// We can not rely on relatedTarget to point to the correct element
// as it will only point to the shadow host of the newly focused element
// and not the element that actually has received focus if it is located
// inside a shadow root.
if (
contains(refs.floating.current, relatedTarget) ||
contains(domReference, relatedTarget) ||
contains(refs.floating.current, activeEl) ||
contains(domReference, activeEl) ||
movedToFocusGuard
) {
return;
Expand Down
68 changes: 66 additions & 2 deletions packages/react/test/unit/useFocus.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {act, cleanup, fireEvent, render, screen} from '@testing-library/react';
import {act, cleanup, fireEvent, render, screen, within} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {cloneElement, useState} from 'react';
import {vi} from 'vitest';
Expand All @@ -16,6 +16,16 @@ import type {UseFocusProps} from '../../src/hooks/useFocus';

vi.useFakeTimers();

beforeAll(() => {
customElements.define('render-root', class RenderRoot extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' })
.appendChild(document.createElement('div'));
}
});
});

function App(props: UseFocusProps & {dismiss?: boolean; hover?: boolean}) {
const [open, setOpen] = useState(false);
const {refs, context} = useFloating({
Expand All @@ -30,7 +40,9 @@ function App(props: UseFocusProps & {dismiss?: boolean; hover?: boolean}) {

return (
<>
<button {...getReferenceProps({ref: refs.setReference})} />
<button {...getReferenceProps({ref: refs.setReference})} >
<span data-testid="inside-reference" tabIndex={0} />
</button>
{open && (
<div role="tooltip" {...getFloatingProps({ref: refs.setFloating})} />
)}
Expand Down Expand Up @@ -58,6 +70,58 @@ test('closes on blur', () => {
cleanup();
});

test('stays open when focus moves to tooltip rendered inside a shadow root', async () => {
const container = document.body.appendChild(document.createElement('render-root'));
const renderRoot = container.shadowRoot?.firstElementChild as HTMLElement;

render(<App />, { container: renderRoot });

const root = within(renderRoot)

// Open the tooltip by focusing the reference
const button = root.getByRole('button');
await fireEvent.focusIn(button);

// Move focus to the tooltip
const tooltip = root.getByRole('tooltip');
tooltip.focus();

// trigger the blur event caused by the focus move, note relatedTarget points to the shadow root here
fireEvent.focusOut(button, { relatedTarget: container })

act(() => {
vi.runAllTimers();
});
expect(root.getByRole('tooltip')).toBeInTheDocument();
cleanup();
});

test('stays open when focus moves to element inside reference that is rendered inside a shadow root', async () => {
const container = document.body.appendChild(document.createElement('render-root'));
const renderRoot = container.shadowRoot?.firstElementChild as HTMLElement;

render(<App />, { container: renderRoot });

const root = within(renderRoot)

// Open the tooltip by focusing the reference
const button = root.getByRole('button');
await fireEvent.focusIn(button);

// Move focus to an element inside the reference
const insideReference = root.getByTestId('inside-reference');
insideReference.focus();

// trigger the blur event caused by the focus move, note relatedTarget points to the shadow root here
fireEvent.focusOut(button, { relatedTarget: container })

act(() => {
vi.runAllTimers();
});
expect(root.getByRole('tooltip')).toBeInTheDocument();
cleanup();
});

test('does not open with a reference pointerDown dismissal', async () => {
render(<App dismiss />);
const button = screen.getByRole('button');
Expand Down

0 comments on commit a8583f3

Please sign in to comment.