import { describe, it, expect, vi, beforeEach } from 'vitest' import { useDatePickerFocusTrap } from '../useDatePickerFocusTrap' import { ref } from 'vue' import type { Ref, ComponentPublicInstance } from 'vue' describe('useDatePickerFocusTrap', () => { let isDatePickerVisible: Ref let datePickerRef: Ref let onClose: () => void let restoreFocus: () => void beforeEach(() => { isDatePickerVisible = ref(true) datePickerRef = ref(null) onClose = vi.fn() restoreFocus = vi.fn() }) it('should do nothing if date picker is closed', () => { isDatePickerVisible.value = false const { handleMenuKeydown } = useDatePickerFocusTrap({ isDatePickerVisible, datePickerRef, }) const event = new KeyboardEvent('keydown', { key: 'Tab' }) const preventDefault = vi.spyOn(event, 'preventDefault') handleMenuKeydown(event) expect(preventDefault).not.toHaveBeenCalled() }) it('should delegate Escape handling to onClose when provided', () => { const { handleMenuKeydown } = useDatePickerFocusTrap({ isDatePickerVisible, datePickerRef, onClose, restoreFocus, }) const event = new KeyboardEvent('keydown', { key: 'Escape' }) const preventDefault = vi.spyOn(event, 'preventDefault') const stopPropagation = vi.spyOn(event, 'stopPropagation') handleMenuKeydown(event) expect(isDatePickerVisible.value).toBe(true) expect(onClose).toHaveBeenCalled() expect(restoreFocus).not.toHaveBeenCalled() expect(preventDefault).toHaveBeenCalled() expect(stopPropagation).toHaveBeenCalled() }) it('should let non-Tab keys pass without interference', () => { const { handleMenuKeydown } = useDatePickerFocusTrap({ isDatePickerVisible, datePickerRef, }) const event = new KeyboardEvent('keydown', { key: 'ArrowRight' }) const preventDefault = vi.spyOn(event, 'preventDefault') handleMenuKeydown(event) expect(preventDefault).not.toHaveBeenCalled() }) it('should focus Today button when Tab is pressed inside a date grid', () => { const rootEl = document.createElement('div') // Create Today button const todayButton = document.createElement('button') todayButton.className = 'date-picker__today-button' todayButton.focus = vi.fn() rootEl.appendChild(todayButton) // Create grid const grid = document.createElement('div') grid.className = 'v-date-picker-month' // Create active cell inside grid const cell = document.createElement('div') const cellBtn = document.createElement('button') cell.appendChild(cellBtn) grid.appendChild(cell) rootEl.appendChild(grid) datePickerRef.value = { $el: rootEl } as unknown as ComponentPublicInstance const { handleMenuKeydown } = useDatePickerFocusTrap({ isDatePickerVisible, datePickerRef, }) const event = new KeyboardEvent('keydown', { key: 'Tab' }) Object.defineProperty(event, 'target', { value: cellBtn }) const preventDefault = vi.spyOn(event, 'preventDefault') handleMenuKeydown(event) expect(preventDefault).toHaveBeenCalled() expect(todayButton.focus).toHaveBeenCalled() }) it('should cycle focus normally if not in a grid', () => { const rootEl = document.createElement('div') const btn1 = document.createElement('button') btn1.focus = vi.fn() const btn2 = document.createElement('button') btn2.focus = vi.fn() rootEl.appendChild(btn1) rootEl.appendChild(btn2) datePickerRef.value = { $el: rootEl } as unknown as ComponentPublicInstance const { handleMenuKeydown } = useDatePickerFocusTrap({ isDatePickerVisible, datePickerRef, }) // Simuler le focus actuel sur btn1 vi.spyOn(document, 'activeElement', 'get').mockReturnValue(btn1) const event = new KeyboardEvent('keydown', { key: 'Tab' }) Object.defineProperty(event, 'target', { value: btn1 }) handleMenuKeydown(event) expect(btn2.focus).toHaveBeenCalled() }) it('should cycle focus backward with Shift+Tab', () => { const rootEl = document.createElement('div') const btn1 = document.createElement('button') btn1.focus = vi.fn() const btn2 = document.createElement('button') btn2.focus = vi.fn() rootEl.appendChild(btn1) rootEl.appendChild(btn2) datePickerRef.value = { $el: rootEl } as unknown as ComponentPublicInstance const { handleMenuKeydown } = useDatePickerFocusTrap({ isDatePickerVisible, datePickerRef, }) vi.spyOn(document, 'activeElement', 'get').mockReturnValue(btn2) const event = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true }) Object.defineProperty(event, 'target', { value: btn2 }) handleMenuKeydown(event) expect(btn1.focus).toHaveBeenCalled() }) it('should cycle from last to first with Tab', () => { const rootEl = document.createElement('div') const btn1 = document.createElement('button') btn1.focus = vi.fn() const btn2 = document.createElement('button') btn2.focus = vi.fn() rootEl.appendChild(btn1) rootEl.appendChild(btn2) datePickerRef.value = { $el: rootEl } as unknown as ComponentPublicInstance const { handleMenuKeydown } = useDatePickerFocusTrap({ isDatePickerVisible, datePickerRef, }) vi.spyOn(document, 'activeElement', 'get').mockReturnValue(btn2) const event = new KeyboardEvent('keydown', { key: 'Tab' }) Object.defineProperty(event, 'target', { value: btn2 }) handleMenuKeydown(event) expect(btn1.focus).toHaveBeenCalled() }) it('should cycle from first to last with Shift+Tab', () => { const rootEl = document.createElement('div') const btn1 = document.createElement('button') btn1.focus = vi.fn() const btn2 = document.createElement('button') btn2.focus = vi.fn() rootEl.appendChild(btn1) rootEl.appendChild(btn2) datePickerRef.value = { $el: rootEl } as unknown as ComponentPublicInstance const { handleMenuKeydown } = useDatePickerFocusTrap({ isDatePickerVisible, datePickerRef, }) vi.spyOn(document, 'activeElement', 'get').mockReturnValue(btn1) const event = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true }) Object.defineProperty(event, 'target', { value: btn1 }) handleMenuKeydown(event) expect(btn2.focus).toHaveBeenCalled() }) it('should not move from day to day with Tab inside the grid', () => { const rootEl = document.createElement('div') const todayButton = document.createElement('button') todayButton.className = 'date-picker__today-button' todayButton.focus = vi.fn() rootEl.appendChild(todayButton) const grid = document.createElement('div') grid.className = 'v-date-picker-month' const day1 = document.createElement('button') day1.focus = vi.fn() const day2 = document.createElement('button') day2.focus = vi.fn() grid.appendChild(day1) grid.appendChild(day2) rootEl.appendChild(grid) datePickerRef.value = { $el: rootEl } as unknown as ComponentPublicInstance const { handleMenuKeydown } = useDatePickerFocusTrap({ isDatePickerVisible, datePickerRef, }) const event = new KeyboardEvent('keydown', { key: 'Tab' }) Object.defineProperty(event, 'target', { value: day2 }) handleMenuKeydown(event) expect(todayButton.focus).toHaveBeenCalled() expect(day1.focus).not.toHaveBeenCalled() }) it('should move Shift+Tab from grid to the last focusable before the grid', () => { const rootEl = document.createElement('div') const prevButton = document.createElement('button') prevButton.className = 'prev-month' prevButton.focus = vi.fn() rootEl.appendChild(prevButton) const nextButton = document.createElement('button') nextButton.className = 'next-month' nextButton.focus = vi.fn() rootEl.appendChild(nextButton) const grid = document.createElement('div') grid.className = 'v-date-picker-month' const day1 = document.createElement('button') day1.focus = vi.fn() grid.appendChild(day1) rootEl.appendChild(grid) const todayButton = document.createElement('button') todayButton.className = 'date-picker__today-button' todayButton.focus = vi.fn() rootEl.appendChild(todayButton) datePickerRef.value = { $el: rootEl } as unknown as ComponentPublicInstance const { handleMenuKeydown } = useDatePickerFocusTrap({ isDatePickerVisible, datePickerRef, }) vi.spyOn(document, 'activeElement', 'get').mockReturnValue(day1) const event = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true }) Object.defineProperty(event, 'target', { value: day1 }) handleMenuKeydown(event) expect(nextButton.focus).toHaveBeenCalled() expect(prevButton.focus).not.toHaveBeenCalled() }) it('should focus selected day when Shift+Tab from Today button', () => { const rootEl = document.createElement('div') const todayButton = document.createElement('button') todayButton.className = 'date-picker__today-button' todayButton.focus = vi.fn() rootEl.appendChild(todayButton) const grid = document.createElement('div') grid.className = 'v-date-picker-month' const selectedDay = document.createElement('button') selectedDay.setAttribute('data-v-date', '2024-06-15') selectedDay.focus = vi.fn() grid.appendChild(selectedDay) rootEl.appendChild(grid) datePickerRef.value = { $el: rootEl } as unknown as ComponentPublicInstance const { handleMenuKeydown } = useDatePickerFocusTrap({ isDatePickerVisible, datePickerRef, getInitialFocusDate: () => new Date(2024, 5, 15), }) vi.spyOn(document, 'activeElement', 'get').mockReturnValue(todayButton) const event = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true }) Object.defineProperty(event, 'target', { value: todayButton }) handleMenuKeydown(event) expect(selectedDay.focus).toHaveBeenCalled() }) it('should focus selected month when Shift+Tab from Today button in month view', () => { const rootEl = document.createElement('div') const todayButton = document.createElement('button') todayButton.className = 'date-picker__today-button' todayButton.focus = vi.fn() rootEl.appendChild(todayButton) const monthsGrid = document.createElement('div') monthsGrid.className = 'v-date-picker-months' for (let i = 0; i < 12; i++) { const monthButton = document.createElement('button') monthButton.className = 'v-btn' monthButton.textContent = `${i}` monthButton.focus = vi.fn() if (i === 8) monthButton.classList.add('v-btn--active') monthsGrid.appendChild(monthButton) } rootEl.appendChild(monthsGrid) datePickerRef.value = { $el: rootEl } as unknown as ComponentPublicInstance const { handleMenuKeydown } = useDatePickerFocusTrap({ isDatePickerVisible, datePickerRef, getInitialFocusDate: () => new Date(2024, 8, 15), }) vi.spyOn(document, 'activeElement', 'get').mockReturnValue(todayButton) const event = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true }) Object.defineProperty(event, 'target', { value: todayButton }) handleMenuKeydown(event) expect((monthsGrid.children[8] as HTMLButtonElement).focus).toHaveBeenCalled() }) it('should focus the active month proxy when present', () => { const rootEl = document.createElement('div') const todayButton = document.createElement('button') todayButton.className = 'date-picker__today-button' rootEl.appendChild(todayButton) const monthsGrid = document.createElement('div') monthsGrid.className = 'v-date-picker-months' const monthsContent = document.createElement('div') monthsContent.className = 'v-date-picker-months__content' for (let i = 0; i < 12; i++) { const proxy = document.createElement('div') proxy.dataset.syDatePickerOption = 'month' proxy.setAttribute('role', 'button') proxy.setAttribute('aria-pressed', i === 8 ? 'true' : 'false') proxy.focus = vi.fn() const monthButton = document.createElement('button') monthButton.className = i === 8 ? 'v-btn v-btn--active' : 'v-btn' monthButton.textContent = `${i}` proxy.appendChild(monthButton) monthsContent.appendChild(proxy) } monthsGrid.appendChild(monthsContent) rootEl.appendChild(monthsGrid) datePickerRef.value = { $el: rootEl } as unknown as ComponentPublicInstance const { handleMenuKeydown } = useDatePickerFocusTrap({ isDatePickerVisible, datePickerRef, getInitialFocusDate: () => new Date(2024, 8, 15), }) vi.spyOn(document, 'activeElement', 'get').mockReturnValue(todayButton) const event = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true }) Object.defineProperty(event, 'target', { value: todayButton }) handleMenuKeydown(event) expect((monthsContent.children[8] as HTMLDivElement).focus).toHaveBeenCalled() }) it('should focus selected year when Shift+Tab from Today button in year view', () => { const rootEl = document.createElement('div') const todayButton = document.createElement('button') todayButton.className = 'date-picker__today-button' todayButton.focus = vi.fn() rootEl.appendChild(todayButton) const yearsGrid = document.createElement('div') yearsGrid.className = 'v-date-picker-years' const year2004 = document.createElement('button') year2004.className = 'v-btn' year2004.textContent = '2004' year2004.focus = vi.fn() yearsGrid.appendChild(year2004) const year2005 = document.createElement('button') year2005.className = 'v-btn v-btn--active' year2005.textContent = '2005' year2005.focus = vi.fn() yearsGrid.appendChild(year2005) rootEl.appendChild(yearsGrid) datePickerRef.value = { $el: rootEl } as unknown as ComponentPublicInstance const { handleMenuKeydown } = useDatePickerFocusTrap({ isDatePickerVisible, datePickerRef, getInitialFocusDate: () => new Date(2005, 8, 15), }) vi.spyOn(document, 'activeElement', 'get').mockReturnValue(todayButton) const event = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true }) Object.defineProperty(event, 'target', { value: todayButton }) handleMenuKeydown(event) expect(year2005.focus).toHaveBeenCalled() }) it('should focus the active year proxy when present', () => { const rootEl = document.createElement('div') const todayButton = document.createElement('button') todayButton.className = 'date-picker__today-button' rootEl.appendChild(todayButton) const yearsGrid = document.createElement('div') yearsGrid.className = 'v-date-picker-years' const yearsContent = document.createElement('div') yearsContent.className = 'v-date-picker-years__content' ;['2004', '2005', '2006'].forEach((year) => { const proxy = document.createElement('div') proxy.dataset.syDatePickerOption = 'year' proxy.setAttribute('role', 'button') proxy.setAttribute('aria-label', year) proxy.setAttribute('aria-pressed', year === '2005' ? 'true' : 'false') proxy.focus = vi.fn() const yearButton = document.createElement('button') yearButton.className = year === '2005' ? 'v-btn v-btn--active' : 'v-btn' yearButton.textContent = year proxy.appendChild(yearButton) yearsContent.appendChild(proxy) }) yearsGrid.appendChild(yearsContent) rootEl.appendChild(yearsGrid) datePickerRef.value = { $el: rootEl } as unknown as ComponentPublicInstance const { handleMenuKeydown } = useDatePickerFocusTrap({ isDatePickerVisible, datePickerRef, getInitialFocusDate: () => new Date(2005, 8, 15), }) vi.spyOn(document, 'activeElement', 'get').mockReturnValue(todayButton) const event = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true }) Object.defineProperty(event, 'target', { value: todayButton }) handleMenuKeydown(event) expect((yearsContent.children[1] as HTMLDivElement).focus).toHaveBeenCalled() }) it('should focus selected day when Tab wraps from last focusable to grid', () => { const rootEl = document.createElement('div') const grid = document.createElement('div') grid.className = 'v-date-picker-month' const selectedDayCell = document.createElement('div') selectedDayCell.setAttribute('data-v-date', '2024-06-20') selectedDayCell.setAttribute('role', 'gridcell') selectedDayCell.focus = vi.fn() const selectedDay = document.createElement('button') selectedDay.setAttribute('tabindex', '-1') selectedDayCell.appendChild(selectedDay) grid.appendChild(selectedDayCell) rootEl.appendChild(grid) const todayButton = document.createElement('button') todayButton.className = 'date-picker__today-button' todayButton.focus = vi.fn() rootEl.appendChild(todayButton) const monthsButton = document.createElement('button') monthsButton.focus = vi.fn() rootEl.appendChild(monthsButton) datePickerRef.value = { $el: rootEl } as unknown as ComponentPublicInstance const { handleMenuKeydown } = useDatePickerFocusTrap({ isDatePickerVisible, datePickerRef, getInitialFocusDate: () => new Date(2024, 5, 20), }) vi.spyOn(document, 'activeElement', 'get').mockReturnValue(monthsButton) const event = new KeyboardEvent('keydown', { key: 'Tab' }) Object.defineProperty(event, 'target', { value: monthsButton }) handleMenuKeydown(event) expect(selectedDayCell.focus).toHaveBeenCalled() expect(todayButton.focus).not.toHaveBeenCalled() }) it('should handle active element not in focusables via compareDocumentPosition', () => { const rootEl = document.createElement('div') const btn1 = document.createElement('button') btn1.focus = vi.fn() rootEl.appendChild(btn1) const nonFocusableGridDay = document.createElement('div') nonFocusableGridDay.setAttribute('tabindex', '-1') rootEl.appendChild(nonFocusableGridDay) const btn2 = document.createElement('button') btn2.focus = vi.fn() rootEl.appendChild(btn2) datePickerRef.value = { $el: rootEl } as unknown as ComponentPublicInstance const { handleMenuKeydown } = useDatePickerFocusTrap({ isDatePickerVisible, datePickerRef, }) vi.spyOn(document, 'activeElement', 'get').mockReturnValue(nonFocusableGridDay) const event = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true }) Object.defineProperty(event, 'target', { value: nonFocusableGridDay }) handleMenuKeydown(event) expect(btn1.focus).toHaveBeenCalled() }) })