From b4e21ea7ce363cd079ff1c61c6a02a04c9290a51 Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Sun, 18 Oct 2026 09:48:54 +0200 Subject: [PATCH 1/3] fix(react-calendar-preview): preserve popup focus and compat geometry Forward native FocusOptions through picker handles and restore compat picker and action dimensions. Cover focus forwarding and exact five/six-week LTR/RTL layout geometry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...-32caec0e-a7b4-4fb4-b487-d151db303d1e.json | 6 ++ ...-987737e5-9b81-4989-bd70-c79b4d70225b.json | 6 ++ .../react-calendar-preview/library/README.md | 10 ++ .../library/etc/react-calendar-preview.api.md | 6 +- .../src/components/Calendar/Calendar.cy.tsx | 91 +++++++++++++++++++ .../Calendar/useCalendarStyles.styles.ts | 26 +++++- .../CalendarDay/CalendarDay.test.tsx | 11 +++ .../CalendarDay/CalendarDay.types.ts | 2 +- .../components/CalendarDay/useCalendarDay.tsx | 4 +- .../useCalendarDayStyles.styles.ts | 9 +- .../CalendarMonth/CalendarMonth.test.tsx | 14 +++ .../CalendarMonth/CalendarMonth.types.ts | 2 +- .../CalendarMonth/useCalendarMonth.tsx | 6 +- .../CalendarYear/CalendarYear.test.tsx | 11 +++ .../CalendarYear/CalendarYear.types.ts | 2 +- .../CalendarYear/useCalendarYear.tsx | 4 +- .../hooks/useCalendarPickerStyles.styles.ts | 3 +- .../library/src/utils/calendarDimensions.ts | 2 + 18 files changed, 192 insertions(+), 23 deletions(-) create mode 100644 change/@fluentui-react-calendar-preview-32caec0e-a7b4-4fb4-b487-d151db303d1e.json create mode 100644 change/@fluentui-react-calendar-preview-987737e5-9b81-4989-bd70-c79b4d70225b.json create mode 100644 packages/react-components/react-calendar-preview/library/src/utils/calendarDimensions.ts diff --git a/change/@fluentui-react-calendar-preview-32caec0e-a7b4-4fb4-b487-d151db303d1e.json b/change/@fluentui-react-calendar-preview-32caec0e-a7b4-4fb4-b487-d151db303d1e.json new file mode 100644 index 0000000000000..e96ccaad4076b --- /dev/null +++ b/change/@fluentui-react-calendar-preview-32caec0e-a7b4-4fb4-b487-d151db303d1e.json @@ -0,0 +1,6 @@ +{ + "type": "patch", + "comment": "Match compat Calendar picker footprints and Go to today action geometry, including week-number and overlay layouts.", + "packageName": "@fluentui/react-calendar-preview", + "email": "dmytrokirpa@microsoft.com" +} diff --git a/change/@fluentui-react-calendar-preview-987737e5-9b81-4989-bd70-c79b4d70225b.json b/change/@fluentui-react-calendar-preview-987737e5-9b81-4989-bd70-c79b4d70225b.json new file mode 100644 index 0000000000000..ce3ac08746c31 --- /dev/null +++ b/change/@fluentui-react-calendar-preview-987737e5-9b81-4989-bd70-c79b4d70225b.json @@ -0,0 +1,6 @@ +{ + "type": "patch", + "comment": "Allow Calendar picker focus handles to forward native FocusOptions so popup focus can prevent page scrolling.", + "packageName": "@fluentui/react-calendar-preview", + "email": "dmytrokirpa@microsoft.com" +} diff --git a/packages/react-components/react-calendar-preview/library/README.md b/packages/react-components/react-calendar-preview/library/README.md index f3353ebc6041b..e6bcb9e06babd 100644 --- a/packages/react-components/react-calendar-preview/library/README.md +++ b/packages/react-components/react-calendar-preview/library/README.md @@ -3,3 +3,13 @@ **React Calendar components for [Fluent UI React](https://react.fluentui.dev/)** These are not production-ready components and **should never be used in product**. This space is useful for testing new components whose APIs might change before final release. + +The `CalendarDay`, `CalendarMonth`, and `CalendarYear` ref handles accept native `FocusOptions`. +Use `ref.current?.focus({ preventScroll: true })` when moving focus into a positioned popup so +focus entry does not scroll the surrounding page. Calling `focus()` without options retains the +default browser scrolling behavior. + +Calendar uses the compat picker footprint for single-picker, side-by-side, and week-number +layouts. The divider does not increase the overall width, and the "Go to today" action retains +the compat height and spacing. These defaults are applied by Calendar itself, including when +hosted in DatePicker; consumer slot styles still take precedence. diff --git a/packages/react-components/react-calendar-preview/library/etc/react-calendar-preview.api.md b/packages/react-components/react-calendar-preview/library/etc/react-calendar-preview.api.md index bd8c2dfe51b59..0a0ec4852a0b9 100644 --- a/packages/react-components/react-calendar-preview/library/etc/react-calendar-preview.api.md +++ b/packages/react-components/react-calendar-preview/library/etc/react-calendar-preview.api.md @@ -240,7 +240,7 @@ export type CalendarDayGridRowState = ComponentState & // @public export type CalendarDayHandle = { - focus(): void; + focus(options?: FocusOptions): void; }; // @public @@ -417,7 +417,7 @@ export type CalendarMonthGridRowState = ComponentState // @public export type CalendarYearHandle = { - focus(): void; + focus(options?: FocusOptions): void; }; // @public diff --git a/packages/react-components/react-calendar-preview/library/src/components/Calendar/Calendar.cy.tsx b/packages/react-components/react-calendar-preview/library/src/components/Calendar/Calendar.cy.tsx index 9e743d3ca4310..97ce63271f462 100644 --- a/packages/react-components/react-calendar-preview/library/src/components/Calendar/Calendar.cy.tsx +++ b/packages/react-components/react-calendar-preview/library/src/components/Calendar/Calendar.cy.tsx @@ -11,6 +11,7 @@ import { calendarMonthClassNames } from '../CalendarMonth/useCalendarMonthStyles import { calendarMonthGridCellClassNames } from '../CalendarMonthGridCell/useCalendarMonthGridCellStyles.styles'; import { calendarYearGridCellClassNames } from '../CalendarYearGridCell/useCalendarYearGridCellStyles.styles'; import type { JSXElement } from '@fluentui/react-utilities'; +import type { CalendarProps } from './Calendar.types'; const mountFluent = (element: JSXElement, dir: 'ltr' | 'rtl' = 'ltr') => { mount( @@ -36,6 +37,96 @@ const nextMonth = `.${calendarDayClassNames.nextMonthButton}`; const goToToday = `.${calendarClassNames.goToTodayButton}`; describe('Calendar', () => { + describe('layout geometry', () => { + const layouts: { name: string; props: CalendarProps; width: number; monthOnly?: boolean }[] = [ + { name: 'side by side', props: { layout: 'sideBySide' }, width: 440 }, + { name: 'side by side with week numbers', props: { layout: 'sideBySide', showWeekNumbers: true }, width: 470 }, + { name: 'day only', props: { monthPicker: null }, width: 220 }, + { name: 'day only with week numbers', props: { monthPicker: null, showWeekNumbers: true }, width: 250 }, + { name: 'overlay', props: { layout: 'overlay' }, width: 220 }, + { name: 'month only', props: { dayPicker: null, showWeekNumbers: true }, width: 220, monthOnly: true }, + ]; + + for (const dir of ['ltr', 'rtl'] as const) { + for (const sixWeeks of [false, true]) { + for (const { name, props, width, monthOnly } of layouts) { + it(`preserves ${name} dimensions with ${sixWeeks ? 'six' : 'five'} weeks in ${dir}`, () => { + const date = sixWeeks ? new Date(2020, 7, 18) : today; + const dayHeight = sixWeeks ? 248 : 220; + const sideBySide = props.layout === 'sideBySide'; + const height = monthOnly ? 241 : sideBySide ? Math.max(dayHeight, 241) : dayHeight + 33; + mountFluent(, dir); + + cy.get(`.${calendarClassNames.root}`).should(elements => { + const root = elements[0]; + const bounds = root.getBoundingClientRect(); + expect(bounds.width).to.equal(width); + expect(bounds.height).to.equal(height); + const action = root.querySelector(goToToday)!; + const actionBounds = action.getBoundingClientRect(); + const actionStyles = getComputedStyle(action); + expect(actionBounds.height).to.equal(30); + expect(actionStyles.marginTop).to.equal('3px'); + expect(actionStyles.lineHeight).to.equal('30px'); + + if (sideBySide) { + const dayPicker = root.querySelector(`.${calendarDayClassNames.root}`)!; + const monthPicker = root.querySelector(`.${calendarMonthClassNames.root}`)!; + const dayBounds = dayPicker.getBoundingClientRect(); + const monthBounds = monthPicker.getBoundingClientRect(); + expect(dayBounds.width).to.equal(props.showWeekNumbers ? 250 : 220); + expect(monthBounds.width).to.equal(220); + expect( + dir === 'ltr' ? monthBounds.left - dayBounds.right : dayBounds.left - monthBounds.right, + ).to.equal(1); + } + }); + }); + } + } + } + + it('does not reserve action space when the action is hidden', () => { + mountFluent(); + cy.get(`.${calendarClassNames.root}`).should(elements => { + const bounds = elements[0].getBoundingClientRect(); + expect(bounds.width).to.equal(440); + expect(bounds.height).to.equal(220); + }); + cy.get(goToToday).should('not.exist'); + }); + + it('retains the single-picker footprint when overlay navigation switches views', () => { + mountFluent(); + cy.get(heading).click(); + cy.get(`.${calendarMonthClassNames.root}`).should('be.visible'); + cy.get(`.${calendarClassNames.root}`).should(elements => { + const bounds = elements[0].getBoundingClientRect(); + expect(bounds.width).to.equal(220); + expect(bounds.height).to.equal(241); + }); + cy.get(`.${calendarMonthClassNames.heading}`).click(); + cy.get(`.${calendarClassNames.root}`).should(elements => { + const bounds = elements[0].getBoundingClientRect(); + expect(bounds.width).to.equal(220); + expect(bounds.height).to.equal(241); + }); + }); + + it('preserves consumer root and action sizing overrides', () => { + mountFluent( + , + ); + cy.get(`.${calendarClassNames.root}`).should('have.css', 'width', '500px'); + cy.get(goToToday).should('have.css', 'height', '40px').and('have.css', 'margin-top', '0px'); + }); + }); + describe('day grid keyboard navigation', () => { it('keeps enclosing day-label callbacks when a nested Calendar handles an arrow', () => { const onOuterKeyDown = cy.stub().as('onOuterKeyDown'); diff --git a/packages/react-components/react-calendar-preview/library/src/components/Calendar/useCalendarStyles.styles.ts b/packages/react-components/react-calendar-preview/library/src/components/Calendar/useCalendarStyles.styles.ts index 1e3fb0ed44399..ad9ce86154a2f 100644 --- a/packages/react-components/react-calendar-preview/library/src/components/Calendar/useCalendarStyles.styles.ts +++ b/packages/react-components/react-calendar-preview/library/src/components/Calendar/useCalendarStyles.styles.ts @@ -2,6 +2,7 @@ import { tokens } from '@fluentui/react-theme'; import { makeStyles, mergeClasses } from '@griffel/react'; +import { calendarGridWidth, calendarGridWidthWithWeekNumbers } from '../../utils/calendarDimensions'; import type { SlotClassNames } from '@fluentui/react-utilities'; import type { CalendarSlots, CalendarState } from './Calendar.types'; @@ -21,7 +22,8 @@ export const calendarClassNames: SlotClassNames = { const useRootStyles = makeStyles({ base: { display: 'flex', - width: 'fit-content', + // The divider belongs inside the picker footprint, not in addition to it. + width: `calc(${calendarGridWidth} + 2 * ${tokens.spacingHorizontalM})`, }, normalize: { boxShadow: 'none', @@ -32,6 +34,15 @@ const useRootStyles = makeStyles({ monthPickerNotVisible: { flexDirection: 'column', }, + dayAndMonthPickersVisible: { + width: `calc(2 * ${calendarGridWidth} + 4 * ${tokens.spacingHorizontalM})`, + }, + dayPickerWithWeekNumbers: { + width: `calc(${calendarGridWidthWithWeekNumbers} + 2 * ${tokens.spacingHorizontalM})`, + }, + dayAndMonthPickersWithWeekNumbers: { + width: `calc(${calendarGridWidthWithWeekNumbers} + ${calendarGridWidth} + 4 * ${tokens.spacingHorizontalM})`, + }, }); const useDividerStyles = makeStyles({ @@ -59,12 +70,14 @@ const useGoTodayButtonStyles = makeStyles({ fontFamily: 'inherit', fontSize: tokens.fontSizeBase200, fontWeight: tokens.fontWeightRegular, + // Preserve compat's 30px action and 3px top gap using the surrounding spacing/stroke tokens. + height: `calc(${tokens.spacingVerticalXXXL} - 2 * ${tokens.strokeWidthThin})`, minWidth: 'auto', - lineHeight: tokens.lineHeightBase200, + lineHeight: `calc(${tokens.spacingVerticalXXXL} - 2 * ${tokens.strokeWidthThin})`, marginInlineEnd: tokens.spacingHorizontalL, - marginBlockStart: tokens.spacingVerticalXXS, + marginBlockStart: `calc(${tokens.spacingVerticalXXS} + ${tokens.strokeWidthThin})`, overflow: 'visible', - padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalXS}`, + padding: `0 ${tokens.spacingHorizontalXS}`, '&:hover': { backgroundColor: tokens.colorTransparentBackground, color: tokens.colorBrandForeground1, @@ -107,7 +120,7 @@ export const useCalendarStyles_unstable = (state: CalendarState): CalendarState const goTodayButtonStyles = useGoTodayButtonStyles(); const liveRegionStyles = useLiveRegionStyles(); - const { isMonthPickerVisible } = state; + const { isDayPickerVisible, isMonthPickerVisible, showWeekNumbers } = state; /* eslint-disable react-hooks/immutability */ state.root.className = mergeClasses( @@ -115,6 +128,9 @@ export const useCalendarStyles_unstable = (state: CalendarState): CalendarState rootStyles.base, rootStyles.normalize, !isMonthPickerVisible && rootStyles.monthPickerNotVisible, + isDayPickerVisible && isMonthPickerVisible && rootStyles.dayAndMonthPickersVisible, + isDayPickerVisible && showWeekNumbers && rootStyles.dayPickerWithWeekNumbers, + isDayPickerVisible && isMonthPickerVisible && showWeekNumbers && rootStyles.dayAndMonthPickersWithWeekNumbers, state.root.className, ); diff --git a/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/CalendarDay.test.tsx b/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/CalendarDay.test.tsx index 2da407c533f6e..4758a3d043b56 100644 --- a/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/CalendarDay.test.tsx +++ b/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/CalendarDay.test.tsx @@ -225,6 +225,17 @@ describe('CalendarDay', () => { expect(document.activeElement).toBe(navigatedCell); }); + it('forwards native focus options to the navigated day', () => { + const ref = React.createRef(); + const { container } = render(); + const cell = container.querySelector('button[aria-label="September 18, 2020"]')!.closest('td')!; + const focus = jest.spyOn(cell, 'focus'); + ref.current!.focus({ preventScroll: true }); + expect(focus).toHaveBeenCalledWith({ preventScroll: true }); + expect(document.activeElement).toBe(cell); + focus.mockRestore(); + }); + it('preserves selection and focus with customized day content, props, and refs', () => { const ref = React.createRef(); const cellRef = React.createRef(); diff --git a/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/CalendarDay.types.ts b/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/CalendarDay.types.ts index 1a4c183feac64..a560308d84d37 100644 --- a/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/CalendarDay.types.ts +++ b/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/CalendarDay.types.ts @@ -22,7 +22,7 @@ export type CalendarDayHandle = { /** * Moves focus to the day the grid is currently navigated to. */ - focus(): void; + focus(options?: FocusOptions): void; }; /** diff --git a/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/useCalendarDay.tsx b/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/useCalendarDay.tsx index 80a3b78e55675..022f931600610 100644 --- a/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/useCalendarDay.tsx +++ b/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/useCalendarDay.tsx @@ -109,8 +109,8 @@ export const useCalendarDayBase_unstable = ( React.useImperativeHandle( ref, () => ({ - focus() { - navigatedDayRef.current?.focus?.(); + focus(options?: FocusOptions) { + navigatedDayRef.current?.focus?.(options); }, }), [], diff --git a/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/useCalendarDayStyles.styles.ts b/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/useCalendarDayStyles.styles.ts index 8e7b1e6fc8421..24cea25b2bb86 100644 --- a/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/useCalendarDayStyles.styles.ts +++ b/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/useCalendarDayStyles.styles.ts @@ -2,6 +2,7 @@ import { tokens } from '@fluentui/react-theme'; import { makeStyles, mergeClasses } from '@griffel/react'; +import { calendarGridWidth, calendarGridWidthWithWeekNumbers } from '../../utils/calendarDimensions'; import { useCalendarContext_unstable } from '../../contexts/calendarContext'; import type { SlotClassNames } from '@fluentui/react-utilities'; import type { CalendarDaySlots, CalendarDayState } from './CalendarDay.types'; @@ -27,7 +28,7 @@ const useRootStyles = makeStyles({ base: { boxSizing: 'content-box', padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalM}`, - width: '196px', + width: calendarGridWidth, }, normalize: { boxShadow: 'none', @@ -36,7 +37,7 @@ const useRootStyles = makeStyles({ padding: '0', }, showWeekNumbers: { - width: '226px', + width: calendarGridWidthWithWeekNumbers, }, }); @@ -60,10 +61,10 @@ const useGridStyles = makeStyles({ position: 'relative', tableLayout: 'fixed', textAlign: 'center', - width: '196px', + width: calendarGridWidth, }, showWeekNumbers: { - width: '226px', + width: calendarGridWidthWithWeekNumbers, }, }); diff --git a/packages/react-components/react-calendar-preview/library/src/components/CalendarMonth/CalendarMonth.test.tsx b/packages/react-components/react-calendar-preview/library/src/components/CalendarMonth/CalendarMonth.test.tsx index 96a73f22535d7..c6c7f19ee6fea 100644 --- a/packages/react-components/react-calendar-preview/library/src/components/CalendarMonth/CalendarMonth.test.tsx +++ b/packages/react-components/react-calendar-preview/library/src/components/CalendarMonth/CalendarMonth.test.tsx @@ -27,6 +27,20 @@ describe('CalendarMonth', () => { expect(() => render()).not.toThrow(); }); + it.each([false, true])('forwards focus options with year picker open=%s', yearPickerOpen => { + const ref = React.createRef(); + const { getByRole } = render(); + if (yearPickerOpen) { + fireEvent.click(getByRole('button', { name: '2025, change year' })); + } + const cell = getByRole('gridcell', { name: yearPickerOpen ? '2025' : 'January' }); + const focus = jest.spyOn(cell, 'focus'); + ref.current!.focus({ preventScroll: true }); + expect(focus).toHaveBeenCalledWith({ preventScroll: true }); + expect(cell).toHaveFocus(); + focus.mockRestore(); + }); + it('preserves an explicitly empty selection when opening the year picker', () => { const { getByRole, getAllByRole } = render(); diff --git a/packages/react-components/react-calendar-preview/library/src/components/CalendarMonth/CalendarMonth.types.ts b/packages/react-components/react-calendar-preview/library/src/components/CalendarMonth/CalendarMonth.types.ts index 3a83a1bace628..c1e709254ba6b 100644 --- a/packages/react-components/react-calendar-preview/library/src/components/CalendarMonth/CalendarMonth.types.ts +++ b/packages/react-components/react-calendar-preview/library/src/components/CalendarMonth/CalendarMonth.types.ts @@ -20,7 +20,7 @@ export type CalendarMonthHandle = { * Moves focus to the navigated month (or the first available month if it is disabled), * or to the year picker while it is open. */ - focus(): void; + focus(options?: FocusOptions): void; }; /** diff --git a/packages/react-components/react-calendar-preview/library/src/components/CalendarMonth/useCalendarMonth.tsx b/packages/react-components/react-calendar-preview/library/src/components/CalendarMonth/useCalendarMonth.tsx index ca3398ee6cb99..cd7cfda29be90 100644 --- a/packages/react-components/react-calendar-preview/library/src/components/CalendarMonth/useCalendarMonth.tsx +++ b/packages/react-components/react-calendar-preview/library/src/components/CalendarMonth/useCalendarMonth.tsx @@ -88,11 +88,11 @@ export const useCalendarMonthBase_unstable = ( const [isYearPickerVisible, setIsYearPickerVisible] = React.useState(false); - const focus = React.useCallback(() => { + const focus = React.useCallback((options?: FocusOptions) => { if (yearPickerRef.current) { - yearPickerRef.current.focus(); + yearPickerRef.current.focus(options); } else { - navigatedMonthRef.current?.focus(); + navigatedMonthRef.current?.focus(options); } }, []); diff --git a/packages/react-components/react-calendar-preview/library/src/components/CalendarYear/CalendarYear.test.tsx b/packages/react-components/react-calendar-preview/library/src/components/CalendarYear/CalendarYear.test.tsx index cbeb69fb1bd29..cc9b6d70f3f39 100644 --- a/packages/react-components/react-calendar-preview/library/src/components/CalendarYear/CalendarYear.test.tsx +++ b/packages/react-components/react-calendar-preview/library/src/components/CalendarYear/CalendarYear.test.tsx @@ -94,6 +94,17 @@ describe('CalendarYear', () => { expect(getByRole('gridcell', { name: '2027' })).toHaveFocus(); }); + it('forwards native focus options to the navigated year', () => { + const ref = React.createRef(); + const { getByRole } = render(); + const year = getByRole('gridcell', { name: '2027' }); + const focus = jest.spyOn(year, 'focus'); + ref.current!.focus({ preventScroll: true }); + expect(focus).toHaveBeenCalledWith({ preventScroll: true }); + expect(year).toHaveFocus(); + focus.mockRestore(); + }); + it('focuses an available year when the navigated year is disabled', () => { const ref = React.createRef(); const { getByRole } = render(, { diff --git a/packages/react-components/react-calendar-preview/library/src/components/CalendarYear/CalendarYear.types.ts b/packages/react-components/react-calendar-preview/library/src/components/CalendarYear/CalendarYear.types.ts index b75d9136065cc..2f0fe47586954 100644 --- a/packages/react-components/react-calendar-preview/library/src/components/CalendarYear/CalendarYear.types.ts +++ b/packages/react-components/react-calendar-preview/library/src/components/CalendarYear/CalendarYear.types.ts @@ -18,7 +18,7 @@ export type CalendarYearHandle = { * Focuses the navigated year, or the selected/current year when navigation is unspecified, * within the focusable visible range. */ - focus(): void; + focus(options?: FocusOptions): void; }; /** diff --git a/packages/react-components/react-calendar-preview/library/src/components/CalendarYear/useCalendarYear.tsx b/packages/react-components/react-calendar-preview/library/src/components/CalendarYear/useCalendarYear.tsx index 7c1371f1db40d..269935a8903e4 100644 --- a/packages/react-components/react-calendar-preview/library/src/components/CalendarYear/useCalendarYear.tsx +++ b/packages/react-components/react-calendar-preview/library/src/components/CalendarYear/useCalendarYear.tsx @@ -102,10 +102,10 @@ export const useCalendarYearBase_unstable = ( React.useImperativeHandle( ref, () => ({ - focus() { + focus(options?: FocusOptions) { [navigatedYearRef.current, selectedYearRef.current, currentYearRef.current] .find(element => element && !element.disabled) - ?.focus(); + ?.focus(options); }, }), [], diff --git a/packages/react-components/react-calendar-preview/library/src/hooks/useCalendarPickerStyles.styles.ts b/packages/react-components/react-calendar-preview/library/src/hooks/useCalendarPickerStyles.styles.ts index dbf12df746a43..8a7be82384f5b 100644 --- a/packages/react-components/react-calendar-preview/library/src/hooks/useCalendarPickerStyles.styles.ts +++ b/packages/react-components/react-calendar-preview/library/src/hooks/useCalendarPickerStyles.styles.ts @@ -2,6 +2,7 @@ import { tokens } from '@fluentui/react-theme'; import { makeStyles } from '@griffel/react'; +import { calendarGridWidth } from '../utils/calendarDimensions'; /** * Chrome shared by the month and year pickers, which render the same header, navigation and grid @@ -12,7 +13,7 @@ export const useCalendarPickerStyles = makeStyles({ boxSizing: 'content-box', overflow: 'hidden', padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalM}`, - width: '196px', + width: calendarGridWidth, }, normalize: { boxShadow: 'none', diff --git a/packages/react-components/react-calendar-preview/library/src/utils/calendarDimensions.ts b/packages/react-components/react-calendar-preview/library/src/utils/calendarDimensions.ts new file mode 100644 index 0000000000000..2307f23f58364 --- /dev/null +++ b/packages/react-components/react-calendar-preview/library/src/utils/calendarDimensions.ts @@ -0,0 +1,2 @@ +export const calendarGridWidth = '196px'; +export const calendarGridWidthWithWeekNumbers = '226px'; From ba6a0e9db790d3b791bc7b48c4d06007d803b0c3 Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Sun, 18 Oct 2026 10:31:56 +0200 Subject: [PATCH 2/3] fix(react-calendar-preview): notify keyboard callbacks before native cancellation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ew-55b038b4-d20a-4a69-9030-e18fee6e601d.json | 6 ++++++ .../react-calendar-preview/library/README.md | 4 ++++ .../src/components/Calendar/Calendar.test.tsx | 17 +++++++++++++++++ .../src/components/Calendar/useCalendar.tsx | 2 +- .../components/CalendarDay/useCalendarDay.tsx | 2 +- .../useCalendarDayGridCell.ts | 2 +- .../useCalendarMonthGridCell.ts | 2 +- .../useCalendarYearGridCell.ts | 2 +- 8 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 change/@fluentui-react-calendar-preview-55b038b4-d20a-4a69-9030-e18fee6e601d.json diff --git a/change/@fluentui-react-calendar-preview-55b038b4-d20a-4a69-9030-e18fee6e601d.json b/change/@fluentui-react-calendar-preview-55b038b4-d20a-4a69-9030-e18fee6e601d.json new file mode 100644 index 0000000000000..ddc31108a7a04 --- /dev/null +++ b/change/@fluentui-react-calendar-preview-55b038b4-d20a-4a69-9030-e18fee6e601d.json @@ -0,0 +1,6 @@ +{ + "type": "patch", + "comment": "Notify Calendar keyboard selection and action callbacks before preventing native activation so DatePicker can honor consumer cancellation.", + "packageName": "@fluentui/react-calendar-preview", + "email": "dmytrokirpa@microsoft.com" +} diff --git a/packages/react-components/react-calendar-preview/library/README.md b/packages/react-components/react-calendar-preview/library/README.md index e6bcb9e06babd..1c15face132c4 100644 --- a/packages/react-components/react-calendar-preview/library/README.md +++ b/packages/react-components/react-calendar-preview/library/README.md @@ -13,3 +13,7 @@ Calendar uses the compat picker footprint for single-picker, side-by-side, and w layouts. The divider does not increase the overall width, and the "Go to today" action retains the compat height and spacing. These defaults are applied by Calendar itself, including when hosted in DatePicker; consumer slot styles still take precedence. + +Keyboard activation invokes selection/action callbacks before preventing the browser's default +Enter or Space behavior. Consumers can therefore distinguish canceled selection from a handled +native key action, as they can with pointer activation. diff --git a/packages/react-components/react-calendar-preview/library/src/components/Calendar/Calendar.test.tsx b/packages/react-components/react-calendar-preview/library/src/components/Calendar/Calendar.test.tsx index 05efef4ce1ce4..6a13643559cfe 100644 --- a/packages/react-components/react-calendar-preview/library/src/components/Calendar/Calendar.test.tsx +++ b/packages/react-components/react-calendar-preview/library/src/components/Calendar/Calendar.test.tsx @@ -10,6 +10,23 @@ import type { CalendarMonthHandle } from '../CalendarMonth/CalendarMonth.types'; expect.extend(toHaveNoViolations); describe('Calendar', () => { + it.each(['Enter', ' '])('notifies day selection before preventing native %s activation', key => { + const onSelectDate = jest.fn(event => expect(event.defaultPrevented).toBe(false)); + const { getByRole } = render(); + const cell = getByRole('button', { name: 'January 16, 2030' }).closest('td')!; + expect(fireEvent.keyDown(cell, { key })).toBe(false); + expect(onSelectDate).toHaveBeenCalledTimes(1); + }); + + it('notifies month-only selection before preventing native Enter activation', () => { + const onSelectDate = jest.fn(event => expect(event.defaultPrevented).toBe(false)); + const { getByRole } = render( + , + ); + expect(fireEvent.keyDown(getByRole('gridcell', { name: 'February' }), { key: 'Enter' })).toBe(false); + expect(onSelectDate).toHaveBeenCalledTimes(1); + }); + isConformant({ Component: Calendar, displayName: 'Calendar', diff --git a/packages/react-components/react-calendar-preview/library/src/components/Calendar/useCalendar.tsx b/packages/react-components/react-calendar-preview/library/src/components/Calendar/useCalendar.tsx index 415af505c942b..08d37221fa537 100644 --- a/packages/react-components/react-calendar-preview/library/src/components/Calendar/useCalendar.tsx +++ b/packages/react-components/react-calendar-preview/library/src/components/Calendar/useCalendar.tsx @@ -510,8 +510,8 @@ export const useCalendarBase_unstable = ( onClick: onGotoToday, onKeyDown: (ev: React.KeyboardEvent) => { if (ev.key === Enter) { - ev.preventDefault(); onGotoToday(ev); + ev.preventDefault(); } }, type: 'button', diff --git a/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/useCalendarDay.tsx b/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/useCalendarDay.tsx index 022f931600610..35e488a8f0229 100644 --- a/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/useCalendarDay.tsx +++ b/packages/react-components/react-calendar-preview/library/src/components/CalendarDay/useCalendarDay.tsx @@ -39,8 +39,8 @@ const onButtonKeyDown = ): ((ev: React.KeyboardEvent) => void) => ev => { if (ev.key === Enter) { - ev.preventDefault(); callback?.(ev); + ev.preventDefault(); } }; diff --git a/packages/react-components/react-calendar-preview/library/src/components/CalendarDayGridCell/useCalendarDayGridCell.ts b/packages/react-components/react-calendar-preview/library/src/components/CalendarDayGridCell/useCalendarDayGridCell.ts index 62565c2804934..7b08a641543ab 100644 --- a/packages/react-components/react-calendar-preview/library/src/components/CalendarDayGridCell/useCalendarDayGridCell.ts +++ b/packages/react-components/react-calendar-preview/library/src/components/CalendarDayGridCell/useCalendarDayGridCell.ts @@ -296,12 +296,12 @@ export const useCalendarDayGridCell_unstable = ( } if ((ev.key === Enter || ev.key === Space) && day.isInBounds) { - ev.preventDefault(); /* * `day.onSelected` is the grid's own handler, so keyboard activation resolves the same date range and * navigation as a click does. */ day.onSelected(ev); + ev.preventDefault(); } else { navigateMonthEdge(ev, originalDate); } diff --git a/packages/react-components/react-calendar-preview/library/src/components/CalendarMonthGridCell/useCalendarMonthGridCell.ts b/packages/react-components/react-calendar-preview/library/src/components/CalendarMonthGridCell/useCalendarMonthGridCell.ts index d273f7d540baa..57ef359fab153 100644 --- a/packages/react-components/react-calendar-preview/library/src/components/CalendarMonthGridCell/useCalendarMonthGridCell.ts +++ b/packages/react-components/react-calendar-preview/library/src/components/CalendarMonthGridCell/useCalendarMonthGridCell.ts @@ -38,8 +38,8 @@ export const useCalendarMonthGridCell_unstable = ( } handleCalendarGridKeyDown(event, dir, allFocusable); if (event.key === Enter && onSelect) { - event.preventDefault(); onSelect(event); + event.preventDefault(); } }, nativeProps.onKeyDownCapture, diff --git a/packages/react-components/react-calendar-preview/library/src/components/CalendarYearGridCell/useCalendarYearGridCell.ts b/packages/react-components/react-calendar-preview/library/src/components/CalendarYearGridCell/useCalendarYearGridCell.ts index 28b5485c10fb3..df9eb5ad0a1c3 100644 --- a/packages/react-components/react-calendar-preview/library/src/components/CalendarYearGridCell/useCalendarYearGridCell.ts +++ b/packages/react-components/react-calendar-preview/library/src/components/CalendarYearGridCell/useCalendarYearGridCell.ts @@ -55,8 +55,8 @@ export const useCalendarYearGridCell_unstable = ( } handleCalendarGridKeyDown(event, dir, allFocusable); if (event.key === Enter && onSelect) { - event.preventDefault(); onSelect(event); + event.preventDefault(); } }, nativeProps.onKeyDownCapture, From 142da164f033fb7450d1c50f15327f8973bec925 Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Mon, 19 Oct 2026 11:53:42 +0200 Subject: [PATCH 3/3] fix: Consolidate Calendar Preview change notes --- ...endar-preview-32caec0e-a7b4-4fb4-b487-d151db303d1e.json | 2 +- ...endar-preview-55b038b4-d20a-4a69-9030-e18fee6e601d.json | 6 ------ ...endar-preview-987737e5-9b81-4989-bd70-c79b4d70225b.json | 6 ------ .../react-calendar-preview/library/README.md | 7 ++++--- 4 files changed, 5 insertions(+), 16 deletions(-) delete mode 100644 change/@fluentui-react-calendar-preview-55b038b4-d20a-4a69-9030-e18fee6e601d.json delete mode 100644 change/@fluentui-react-calendar-preview-987737e5-9b81-4989-bd70-c79b4d70225b.json diff --git a/change/@fluentui-react-calendar-preview-32caec0e-a7b4-4fb4-b487-d151db303d1e.json b/change/@fluentui-react-calendar-preview-32caec0e-a7b4-4fb4-b487-d151db303d1e.json index e96ccaad4076b..3394b167c38fb 100644 --- a/change/@fluentui-react-calendar-preview-32caec0e-a7b4-4fb4-b487-d151db303d1e.json +++ b/change/@fluentui-react-calendar-preview-32caec0e-a7b4-4fb4-b487-d151db303d1e.json @@ -1,6 +1,6 @@ { "type": "patch", - "comment": "Match compat Calendar picker footprints and Go to today action geometry, including week-number and overlay layouts.", + "comment": "fix: Align Calendar sizing, popup focus, and keyboard selection behavior.", "packageName": "@fluentui/react-calendar-preview", "email": "dmytrokirpa@microsoft.com" } diff --git a/change/@fluentui-react-calendar-preview-55b038b4-d20a-4a69-9030-e18fee6e601d.json b/change/@fluentui-react-calendar-preview-55b038b4-d20a-4a69-9030-e18fee6e601d.json deleted file mode 100644 index ddc31108a7a04..0000000000000 --- a/change/@fluentui-react-calendar-preview-55b038b4-d20a-4a69-9030-e18fee6e601d.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "type": "patch", - "comment": "Notify Calendar keyboard selection and action callbacks before preventing native activation so DatePicker can honor consumer cancellation.", - "packageName": "@fluentui/react-calendar-preview", - "email": "dmytrokirpa@microsoft.com" -} diff --git a/change/@fluentui-react-calendar-preview-987737e5-9b81-4989-bd70-c79b4d70225b.json b/change/@fluentui-react-calendar-preview-987737e5-9b81-4989-bd70-c79b4d70225b.json deleted file mode 100644 index ce3ac08746c31..0000000000000 --- a/change/@fluentui-react-calendar-preview-987737e5-9b81-4989-bd70-c79b4d70225b.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "type": "patch", - "comment": "Allow Calendar picker focus handles to forward native FocusOptions so popup focus can prevent page scrolling.", - "packageName": "@fluentui/react-calendar-preview", - "email": "dmytrokirpa@microsoft.com" -} diff --git a/packages/react-components/react-calendar-preview/library/README.md b/packages/react-components/react-calendar-preview/library/README.md index 1c15face132c4..4e066cb96530a 100644 --- a/packages/react-components/react-calendar-preview/library/README.md +++ b/packages/react-components/react-calendar-preview/library/README.md @@ -14,6 +14,7 @@ layouts. The divider does not increase the overall width, and the "Go to today" the compat height and spacing. These defaults are applied by Calendar itself, including when hosted in DatePicker; consumer slot styles still take precedence. -Keyboard activation invokes selection/action callbacks before preventing the browser's default -Enter or Space behavior. Consumers can therefore distinguish canceled selection from a handled -native key action, as they can with pointer activation. +Keyboard date selection invokes date-selection callbacks before preventing the browser's default +Enter or Space behavior. Consumers can therefore distinguish canceled date selection from a handled +native key action, as they can with pointer activation. This ordering guarantee does not apply to +navigation or action callbacks.