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..3394b167c38fb --- /dev/null +++ b/change/@fluentui-react-calendar-preview-32caec0e-a7b4-4fb4-b487-d151db303d1e.json @@ -0,0 +1,6 @@ +{ + "type": "patch", + "comment": "fix: Align Calendar sizing, popup focus, and keyboard selection behavior.", + "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..4e066cb96530a 100644 --- a/packages/react-components/react-calendar-preview/library/README.md +++ b/packages/react-components/react-calendar-preview/library/README.md @@ -3,3 +3,18 @@ **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. + +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. 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/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/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..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(); } }; @@ -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/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/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/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/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/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, 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';