import { type RenderResult, render, waitFor } from '@testing-library/vue' import { beforeEach, describe, expect, it, vi } from 'vitest' import { defineComponent } from 'vue' import { FocusScope } from '.' import userEvent from '@testing-library/user-event' const INNER_NAME_INPUT_LABEL = 'Name' const INNER_EMAIL_INPUT_LABEL = 'Email' const INNER_SUBMIT_LABEL = 'Submit' const TestField = ({ props: { label: String, }, template: ` `, }) describe('focusScope', () => { describe('given a default FocusScope', () => { let rendered: RenderResult let tabbableFirst: HTMLInputElement let tabbableSecond: HTMLInputElement let tabbableLast: HTMLButtonElement beforeEach(() => { rendered = render(defineComponent({ components: { TestField, FocusScope }, template: `
`, })) tabbableFirst = rendered.getByLabelText(INNER_NAME_INPUT_LABEL) as HTMLInputElement tabbableSecond = rendered.getByLabelText(INNER_EMAIL_INPUT_LABEL) as HTMLInputElement tabbableLast = rendered.getByText(INNER_SUBMIT_LABEL) as HTMLButtonElement }) it('should focus the next element in the scope on tab', async () => { tabbableFirst.focus() await userEvent.tab() expect(tabbableSecond).toBe(document.activeElement) }) it('should focus the last element in the scope on shift+tab from the first element in scope', async () => { tabbableFirst.focus() await userEvent.tab({ shift: true }) waitFor(() => expect(tabbableLast).toBe(document.activeElement)) }) it('should focus the first element in scope on tab from the last element in scope', async () => { tabbableLast.focus() await userEvent.tab() expect(tabbableFirst).toBe(document.activeElement) }) }) describe('given a FocusScope where the first focusable has a negative tabindex', () => { let rendered: RenderResult let tabbableSecond: HTMLInputElement let tabbableLast: HTMLButtonElement beforeEach(() => { rendered = render(defineComponent({ components: { TestField, FocusScope }, template: `
`, })) tabbableSecond = rendered.getByLabelText(INNER_EMAIL_INPUT_LABEL) as HTMLInputElement tabbableLast = rendered.getByText(INNER_SUBMIT_LABEL) as HTMLButtonElement }) it('should skip the element with a negative tabindex on tab', async () => { tabbableLast.focus() await userEvent.tab() expect(tabbableSecond).toBe(document.activeElement) }) it('should skip the element with a negative tabindex on shift+tab', async () => { tabbableSecond.focus() await userEvent.tab({ shift: true }) waitFor(() => expect(tabbableLast).toBe(document.activeElement)) }) }) describe('given a FocusScope with internal focus handlers', () => { const handleLastFocusableElementBlur = vi.fn() let rendered: RenderResult let tabbableFirst: HTMLInputElement beforeEach(() => { rendered = render(defineComponent({ components: { TestField, FocusScope }, props: { handleLastFocusableElementBlur }, template: `
`, })) tabbableFirst = rendered.getByLabelText(INNER_NAME_INPUT_LABEL) as HTMLInputElement }) it('should properly blur the last element in the scope before cycling back', async () => { // Tab back and then tab forward to cycle through the scope tabbableFirst.focus() await userEvent.tab({ shift: true }) await userEvent.tab() waitFor(() => expect(handleLastFocusableElementBlur).toHaveBeenCalledTimes(1)) }) }) })