Skip to content

fix(react-calendar-preview): preserve focus and grid keyboard ownership - #36884

Merged
Dmytro Kirpa (dmytrokirpa) merged 18 commits into
masterfrom
dmytrokirpa-calendar-keyboard-fixes
Oct 15, 2026
Merged

Dmytro Kirpa (dmytrokirpa) merged 18 commits into
masterfrom
dmytrokirpa-calendar-keyboard-fixes

Conversation

@dmytrokirpa

@dmytrokirpa Dmytro Kirpa (dmytrokirpa) commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Previous Behavior

Calendar focus restoration relied on styled picker class names, and base month/year grids had no two-dimensional arrow navigation. Native grid or focusgroup owners could move focus before consumer handlers had a chance to cancel; modified arrows in the day grid were also intercepted.

New Behavior

Use reserved picker data markers for class-independent focus restoration, and provide row/column arrow navigation for base month/year grids with RTL, boundary, unavailable-cell, and allFocusable handling. Navigation skips candidates that cannot receive focus while retaining programmatic focus for tabIndex="-1" cells.

Owned arrows dispatch consumer handlers before native grid owners while preserving nested focusgroup/grid ownership, editable targets, modified arrows, cancellation, and normal bubbling for other keys. Day-slot callbacks follow the target's ancestor path in capture/bubble order with correct currentTarget, exactly-once dispatch, and element-scoped deduplication that does not suppress callbacks in nested Calendars. preventDefault() cancels navigation without suppressing consumer bubbling; a cell bubbling callback can stop propagation without cancelling Calendar movement.

Native-owner cancellation ordering relies on React 17+ capture delegation. No special React 16 native event bridge is included; the package peer range is unchanged.

These stable Calendar fixes are required by #36752: its headless Calendar wrappers compose the same base primitives and need the correct focus-restoration and keyboard behavior. This PR contains the shared Calendar implementation, regression tests, specification updates, and one patch change file; #36752 adds the headless wrappers and their public API.

Validation

  • NX_DAEMON=false yarn nx run react-calendar-preview:test --runInBand --verbose=false — passed (19 suites, 699 tests).
  • NX_DAEMON=false yarn nx run react-calendar-preview:e2e --browser chrome --spec src/components/Calendar/Calendar.cy.tsx — passed (50 browser tests).
  • NX_DAEMON=false yarn nx run-many --projects=react-calendar-preview --targets=lint,type-check --parallel=2 — passed.
  • git diff --check — passed.

Related Issue(s)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ed39bb7d-827b-40e9-bc3b-3bf27e334d85
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ed39bb7d-827b-40e9-bc3b-3bf27e334d85
Use reserved day/month root markers to preserve focus restoration for base and headless compositions. Add styled/classless and SSR regressions, document the markers, and include a patch change file.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ed39bb7d-827b-40e9-bc3b-3bf27e334d85
Handle month/year arrows by semantic row and column in the base layer. Dispatch owned day/month/year arrows through consumer-first capture handlers before native grid owners, without duplicate bubbling callbacks. Preserve editable targets, RTL, availability and non-arrow bubbling; keep styled Tabster roving behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ed39bb7d-827b-40e9-bc3b-3bf27e334d85
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ed39bb7d-827b-40e9-bc3b-3bf27e334d85
Revert Calendar behavior fixes for a separate PR and remove the unrelated CI changes, restoring the original two-file release tree.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ed39bb7d-827b-40e9-bc3b-3bf27e334d85
Restore the existing Calendar-only focus and keyboard fixes from the saved source snapshot, preserving the source, tests, and specification exactly. Consolidate the related release notes into one scoped patch change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 92cc9ccd-6e8f-4638-8c08-b2323aa950a1
@dmytrokirpa
Dmytro Kirpa (dmytrokirpa) changed the base branch from calendar-preview-release to master October 14, 2026 20:53
Restore private:true from master and remove the original initial-release change file from the Calendar source-only PR. Retain the consolidated patch note and the Calendar source, tests, and specification unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 92cc9ccd-6e8f-4638-8c08-b2323aa950a1
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
react-calendar-preview
Calendar
169.078 kB
48.106 kB
172.316 kB
49.137 kB
3.238 kB
1.031 kB

🤖 This report was generated against f11fae6ff9b0d6840adc2c052323f2d7ef9b6bb1

@github-actions

Copy link
Copy Markdown

Pull request demo site: URL

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e706f1a3-12e4-4e0a-bad1-c38b23bd9449

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Keyboard handling still bypasses day-button cancellation, misses native-listener ordering on React 16, and disables navigation inside non-editable editor regions.

3 open findings
What changed in this PR

Updates shared Calendar primitives to preserve class-independent focus restoration and keyboard ownership, supporting the headless Calendar wrappers in #36752.

Changes:

  • Adds picker markers for focus restoration.
  • Implements month/year grid arrow navigation and shared keyboard ownership handling.
  • Adds regression tests, specification updates, and a patch change file.
File Description
packages/​react-components/​react-calendar-preview/​library/​src/​utils/​calendarKeyboard.ts Shared keyboard ownership and navigation helpers.
packages/​react-components/​react-calendar-preview/​library/​src/​components/​CalendarYearGridCell/​useCalendarYearGridCell.ts Adds year-cell arrow handling.
packages/​react-components/​react-calendar-preview/​library/​src/​components/​CalendarYear/​useCalendarYear.tsx Shares grid dimensions and limits Tabster ownership.
packages/​react-components/​react-calendar-preview/​library/​src/​components/​CalendarMonthGridCell/​useCalendarMonthGridCell.ts Adds month-cell arrow handling.
packages/​react-components/​react-calendar-preview/​library/​src/​components/​CalendarMonth/​useCalendarMonth.tsx Adds picker marker and adjusts Tabster ownership.
packages/​react-components/​react-calendar-preview/​library/​src/​components/​CalendarDayGridCell/​useCalendarDayGridCell.ts Integrates arrow capture and ownership guards.
packages/​react-components/​react-calendar-preview/​library/​src/​components/​CalendarDay/​useCalendarDay.tsx Adds reserved day-picker marker.
packages/​react-components/​react-calendar-preview/​library/​src/​components/​Calendar/​useCalendar.tsx Restores focus using picker markers.
packages/​react-components/​react-calendar-preview/​library/​src/​components/​Calendar/​Calendar.gridKeyboard.test.tsx Tests navigation, cancellation, and event ordering.
packages/​react-components/​react-calendar-preview/​library/​src/​components/​Calendar/​Calendar.focus.test.tsx Tests focus restoration and ownership.
packages/​react-components/​react-calendar-preview/​library/​src/​components/​Calendar/​Calendar.cy.tsx Adds browser keyboard-navigation coverage.
packages/​react-components/​react-calendar-preview/​library/​src/​components/​Calendar/​Calendar-node.test.tsx Verifies server-rendered picker markers.
packages/​react-components/​react-calendar-preview/​library/​docs/​Spec.md Documents focus and keyboard contracts.
change/​@fluentui-react-calendar-preview-8454a189-0723-43f8-81db-d883c01d599a.json Records the patch release.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e706f1a3-12e4-4e0a-bad1-c38b23bd9449
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e706f1a3-12e4-4e0a-bad1-c38b23bd9449
PaulGMardling

This comment was marked as low quality.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focus and keyboard-ownership improvements. I verified the focused-button cancellation fix; the 667 Calendar unit tests, 41 browser tests, lint, and type-check pass. I am requesting changes for the five remaining issues described in the code-targeted comments in this single batched review.

Skip unfocusable grid candidates, preserve consumer propagation semantics, and remove the React 16 native event bridge.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e706f1a3-12e4-4e0a-bad1-c38b23bd9449

@PaulGMardling Paul Mardling (PaulGMardling) left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've taken another look at the latest changes.
The earlier examples are fixed.
This pass found two more issues that were already present, plus one regression introduced by the latest changes. I've left the three comments.

Handle shadow-root focus targets, native contenteditable inheritance, and key handlers on custom day content.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e706f1a3-12e4-4e0a-bad1-c38b23bd9449

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Custom-descendant cancellation and Shadow DOM focus detection remain incorrect.

1 open finding
3 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Run custom descendant handlers before grid navigation

packages/​react-components/​react-calendar-preview/​library/​src/​utils/​calendarKeyboard.ts:78

Calling the composed handler during cell capture moves focus before capture handlers on custom descendants run. For example, a focusable span supplied through dayLabel.children or renderYear cannot cancel ArrowRight with onKeyDownCapture={event => event.preventDefault()}. Forwarding the known day-slot roots does not cover these children; it also invokes slot bubbling callbacks before a child's stopPropagation() can suppress them. Adjust dispatch so custom descendant handlers run before navigation, while retaining native-grid cancellation ordering, and add nested-child cancellation and propagation tests.

🧠 Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Descendant cancellation and nested Calendar callback dispatch still have unresolved correctness issues.

1 open finding
1 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Early arrow dispatch ignores descendant preventDefault

packages/​react-components/​react-calendar-preview/​library/​src/​components/​CalendarYearGridCell/​useCalendarYearGridCell.ts:63

This predicate captures arrows from non-editable renderYear descendants before their handlers run. If renderYear returns a focusable span whose onKeyDownCapture or onKeyDown calls preventDefault(), ArrowRight moves focus before that cancellation occurs. Restrict early dispatch to cell-targeted events and leave custom descendants in normal bubbling, as the day-cell predicate does. Add descendant cancellation tests for both phases.

🧠 Review effort: Balanced

Keep nested Calendar keyboard events from suppressing enclosing day-slot callbacks, and cover bubbling with a nested Calendar regression.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e706f1a3-12e4-4e0a-bad1-c38b23bd9449

@PaulGMardling Paul Mardling (PaulGMardling) left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've had another look and have two more comments on keyboard handling.

@PaulGMardling Paul Mardling (PaulGMardling) left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working through the fixes. This looks good to move forward and unblock #36752

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e706f1a3-12e4-4e0a-bad1-c38b23bd9449
@dmytrokirpa
Dmytro Kirpa (dmytrokirpa) merged commit 45fcaec into master Oct 15, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants