Skip to content

Commit 5b154e5

Browse files
committed
Add test case for focus loop with shadow DOM
1 parent 6c32455 commit 5b154e5

File tree

1 file changed

+53
-0
lines changed

1 file changed

+53
-0
lines changed

packages/react/focus-scope/src/focus-scope.test.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,46 @@ describe('FocusScope', () => {
178178
});
179179
});
180180

181+
describe('given a FocusScope with shadow DOM elements', () => {
182+
let rendered: RenderResult;
183+
let tabbableFirst: HTMLButtonElement;
184+
let tabbableLast: HTMLInputElement;
185+
186+
beforeEach(async () => {
187+
rendered = render(
188+
<div>
189+
<FocusScope asChild loop trapped>
190+
<form>
191+
<button>Close</button>
192+
<ShadowHostField />
193+
</form>
194+
</FocusScope>
195+
</div>,
196+
);
197+
tabbableFirst = rendered.getByText('Close') as HTMLButtonElement;
198+
// Wait for the useEffect inside ShadowHostField to attach the shadow root
199+
await waitFor(() => {
200+
const host = rendered.container.querySelector('[data-shadow-host]');
201+
expect(host?.shadowRoot?.querySelector('input')).not.toBeNull();
202+
});
203+
tabbableLast = rendered.container
204+
.querySelector('[data-shadow-host]')!
205+
.shadowRoot!.querySelector('input') as HTMLInputElement;
206+
});
207+
208+
it('should focus the first element in scope on tab from the last shadow DOM element', () => {
209+
tabbableLast.focus();
210+
userEvent.tab();
211+
waitFor(() => expect(tabbableFirst).toHaveFocus());
212+
});
213+
214+
it('should focus the last shadow DOM element on shift+tab from the first element in scope', () => {
215+
tabbableFirst.focus();
216+
userEvent.tab({ shift: true });
217+
waitFor(() => expect(tabbableLast).toHaveFocus());
218+
});
219+
});
220+
181221
describe('given a FocusScope with internal focus handlers', () => {
182222
const handleLastFocusableElementBlur = vi.fn();
183223
let rendered: RenderResult;
@@ -214,3 +254,16 @@ function TestField({ label, ...props }: { label: string } & React.ComponentProps
214254
</label>
215255
);
216256
}
257+
258+
function ShadowHostField() {
259+
const ref = React.useRef<HTMLDivElement>(null);
260+
React.useEffect(() => {
261+
const el = ref.current;
262+
if (!el || el.shadowRoot) return;
263+
const shadow = el.attachShadow({ mode: 'open' });
264+
const input = document.createElement('input');
265+
input.type = 'text';
266+
shadow.appendChild(input);
267+
}, []);
268+
return <div ref={ref} data-shadow-host="" />;
269+
}

0 commit comments

Comments
 (0)