From c8d64314e5b9039fc6cb985f0dd666e1c6187b37 Mon Sep 17 00:00:00 2001 From: Aryan Bagade <73382554+AryanBagade@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:56:24 -0800 Subject: [PATCH] fix(calendar): prevent setFocusedDate from stealing focus (#9463) * fix(calendar): prevent setFocusedDate from stealing focus (#9427) Signed-off-by: Aryan Bagade * test(calendar): add tests for setFocusedDate focus behavior Signed-off-by: Aryan Bagade * fix: remove unused variable Signed-off-by: Aryan Bagade * move tests and fix --------- Signed-off-by: Aryan Bagade Co-authored-by: Robert Snow --- .../calendar/src/useCalendarCell.ts | 10 ++++ .../calendar/src/useCalendarState.ts | 7 ++- .../test/Calendar.test.js | 46 +++++++++++++++++-- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/packages/@react-aria/calendar/src/useCalendarCell.ts b/packages/@react-aria/calendar/src/useCalendarCell.ts index 9db8185c699..6b059764bec 100644 --- a/packages/@react-aria/calendar/src/useCalendarCell.ts +++ b/packages/@react-aria/calendar/src/useCalendarCell.ts @@ -174,6 +174,7 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta onPressStart(e) { if (state.isReadOnly) { state.setFocusedDate(date); + state.setFocused(true); return; } @@ -186,12 +187,14 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta if (isSameDay(date, state.highlightedRange.start)) { state.setAnchorDate(state.highlightedRange.end); state.setFocusedDate(date); + state.setFocused(true); state.setDragging(true); isRangeBoundaryPressed.current = true; return; } else if (isSameDay(date, state.highlightedRange.end)) { state.setAnchorDate(state.highlightedRange.start); state.setFocusedDate(date); + state.setFocused(true); state.setDragging(true); isRangeBoundaryPressed.current = true; return; @@ -204,6 +207,7 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta state.selectDate(date); state.setFocusedDate(date); + state.setFocused(true); isAnchorPressed.current = true; }; @@ -227,6 +231,7 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta if (!('anchorDate' in state) && !state.isReadOnly) { state.selectDate(date); state.setFocusedDate(date); + state.setFocused(true); } }, onPressUp(e) { @@ -240,6 +245,7 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta if ('anchorDate' in state && touchDragTimerRef.current) { state.selectDate(date); state.setFocusedDate(date); + state.setFocused(true); } if ('anchorDate' in state) { @@ -252,6 +258,7 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta // When releasing a drag or pressing the end date of a range, select it. state.selectDate(date); state.setFocusedDate(date); + state.setFocused(true); } else if (e.pointerType === 'keyboard' && !state.anchorDate) { // For range selection, auto-advance the focused date by one if using keyboard. // This gives an indication that you're selecting a range rather than a single date. @@ -264,11 +271,13 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta } if (!state.isInvalid(nextDay)) { state.setFocusedDate(nextDay); + state.setFocused(true); } } else if (e.pointerType === 'virtual') { // For screen readers, just select the date on click. state.selectDate(date); state.setFocusedDate(date); + state.setFocused(true); } } } @@ -316,6 +325,7 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta onFocus() { if (!isDisabled) { state.setFocusedDate(date); + state.setFocused(true); } }, tabIndex, diff --git a/packages/@react-stately/calendar/src/useCalendarState.ts b/packages/@react-stately/calendar/src/useCalendarState.ts index 4d289535a4e..5286f894682 100644 --- a/packages/@react-stately/calendar/src/useCalendarState.ts +++ b/packages/@react-stately/calendar/src/useCalendarState.ts @@ -50,8 +50,8 @@ export interface CalendarStateOptions extends C * @default {months: 1} */ visibleDuration?: DateDuration, - /** - * Determines the alignment of the visible months on initial render based on the current selection or current date if there is no selection. + /** + * Determines the alignment of the visible months on initial render based on the current selection or current date if there is no selection. * @default 'center' */ selectionAlignment?: 'start' | 'center' | 'end' @@ -206,7 +206,6 @@ export function useCalendarState(props: Calenda isValueInvalid, setFocusedDate(date) { focusCell(date); - setFocused(true); }, focusNextDay() { focusCell(focusedDate.add({days: 1})); @@ -336,7 +335,7 @@ export function useCalendarState(props: Calenda let dates: (CalendarDate | null)[] = []; date = startOfWeek(date, locale, firstDayOfWeek); - + // startOfWeek will clamp dates within the calendar system's valid range, which may // start in the middle of a week. In this case, add null placeholders. let dayOfWeek = getDayOfWeek(date, locale, firstDayOfWeek); diff --git a/packages/react-aria-components/test/Calendar.test.js b/packages/react-aria-components/test/Calendar.test.js index 30c7228292c..d5b67877557 100644 --- a/packages/react-aria-components/test/Calendar.test.js +++ b/packages/react-aria-components/test/Calendar.test.js @@ -11,7 +11,7 @@ */ import {act, fireEvent, pointerMap, render, within} from '@react-spectrum/test-utils-internal'; -import {Button, Calendar, CalendarCell, CalendarContext, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, CalendarStateContext, Heading} from 'react-aria-components'; +import {Button, ButtonContext, Calendar, CalendarCell, CalendarContext, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, CalendarStateContext, Heading} from 'react-aria-components'; import {CalendarDate, getLocalTimeZone, startOfMonth, startOfWeek, today} from '@internationalized/date'; import React, {useContext} from 'react'; import userEvent from '@testing-library/user-event'; @@ -101,7 +101,7 @@ describe('Calendar', () => { }); it('should support aria props on the Calendar', () => { - let {getByRole} = renderCalendar({ + let {getByRole} = renderCalendar({ 'aria-label': 'label', 'aria-labelledby': 'labelledby', 'aria-describedby': 'describedby', @@ -112,7 +112,7 @@ describe('Calendar', () => { expect(group).toHaveAttribute('aria-label', expect.stringContaining('label')); expect(group).toHaveAttribute('aria-labelledby', expect.stringContaining('labelledby')); expect(group).toHaveAttribute('aria-describedby', 'describedby'); - expect(group).toHaveAttribute('aria-details', 'details'); + expect(group).toHaveAttribute('aria-details', 'details'); }); it('should support custom CalendarGridHeader', () => { @@ -417,4 +417,44 @@ describe('Calendar', () => { await user.keyboard('[ArrowLeft][Enter]'); expect(calendar.getByLabelText(/selected/)).toBe(day16); }); + + it('should not become focused just by setting the focused date', async () => { + let DatePicker = () => { + let state = useContext(CalendarStateContext); + return ; + }; + + let MyCalendar = () => { + return ( + +
+ + + + + + +
+ + + {(day) => ( + + {day} + + )} + + + {(date) => isSelected ? 'selected' : ''} />} + + +
+ ); + }; + let {getByRole} = render( + + ); + let setFocusedDateButton = getByRole('button', {name: 'Set focused date'}); + await user.click(setFocusedDateButton); + expect(setFocusedDateButton).toHaveFocus(); + }); });