Skip to content

fix(interactive-hooks): use keyDownEvents and keyUpEvents on react-native-macos 0.78+ - #4372

Draft
Saad Najmi (Saadnajmi) wants to merge 1 commit into
microsoft:mainfrom
Saadnajmi:macos-keydownevents
Draft

Saad Najmi (Saadnajmi) wants to merge 1 commit into
microsoft:mainfrom
Saadnajmi:macos-keydownevents

Conversation

@Saadnajmi

Copy link
Copy Markdown
Collaborator

Platforms Impacted

  • iOS
  • macOS
  • win32 (Office)
  • windows
  • android

This PR refactors the win32 code path. The win32 output does not change.

Description of changes

Why

FURN sends the deprecated react-native-macos props validKeysDown and validKeysUp on all versions of react-native-macos. New versions of react-native-macos do not support these props natively:

The result:

  • On react-native-macos 0.78 to 0.81.5, the native view ignores validKeysDown and validKeysUp. The native view does not get a list of the keys that FURN handles, so it does not block the native handling of these keys.
  • On react-native-macos 0.81.6 and later, FURN depends on the deprecated compatibility layer.

What changed

  • packages/utils/interactive-hooks/src/useKeyProps.ts:
    • One worker, getKeyPropsWorker(direction, userCallback, keys), makes the props for key down and for key up. getKeyDownPropsWorker and getKeyUpPropsWorker call this worker.
    • On react-native-macos 0.78 and later, the hooks send keyDownEvents or keyUpEvents. They do not send validKeysDown or validKeysUp.
    • The exports, signatures, and memoization of useKeyProps, useKeyDownProps, useKeyUpProps, preferKeyDownForKeyEvents, and useKeyCallback do not change. The emitted lib/useKeyProps.d.ts is the same as on main.
  • packages/utils/interactive-hooks/src/useKeyProps.types.ts: KeyPressProps.validKeysDown and KeyPressProps.validKeysUp are now @deprecated. The comments on keyDownEvents and keyUpEvents now say "win32 and react-native-macos 0.78+". The type of these fields stays any[], so consumers see no type change.
  • packages/utils/adapters/src/platformProps.ts: passthroughAllKeyEvents, validKeysDown, and validKeysUp in ViewAdditionsMacOS are now @deprecated. The filter masks do not change.
  • packages/utils/interactive-hooks/src/__tests__/useKeyProps.test.tsx: new unit tests for the props on each platform.
  • references/agents/focus/platform-evidence.md: C-FOCUS-004 now describes the new behavior and the native modifier matching (see below).
  • .changeset/: patch changesets for @fluentui-react-native/interactive-hooks and @fluentui-react-native/adapters.

Shared code path

Platform Props from useKeyDownProps and useKeyUpProps Change
iOS, Android undefined No change
react-native-windows onKeyDown or onKeyUp only. RNW filters on code, not on key (microsoft/react-native-windows#11049). No change
win32 onKeyDown and keyDownEvents, or onKeyUp and keyUpEvents. The events are keys.map((key) => ({ key })). No change
react-native-macos 0.78+ The same props as win32, from the same code path Changed. Before: validKeysDown or validKeysUp
react-native-macos 0.73, 0.74 onKeyDown and validKeysDown, or onKeyUp and validKeysUp No change

The worker reads Platform.constants.reactNativeVersion.minor. getRnVersion() in @fluentui-react-native/adapters (filters.base.ts) reads the same value, and the macOS filter masks use the same < 78 limit. The index of the adapters package does not export getRnVersion, so this PR does not add a public export.

The worker reads Platform.OS when it runs. It does not use Platform.select. The result on each platform is the same as before. This change lets the tests mock the platform, because the Platform.select in the win32 Jest preset always selects win32.

Legacy path for react-native-macos 0.73 and 0.74

The FURN peer range includes react-native-macos ^0.73.0 || ^0.74.0. These versions do not support keyDownEvents or keyUpEvents. On these versions, JS gets only the key events that validKeysDown or validKeysUp lists. Focusable views also get the native default for Space and Enter. Thus the legacy branch stays for these versions. A code comment tells maintainers to delete this branch when FURN drops support for react-native-macos versions before 0.78.

Modifier matching

  • A legacy string entry, for example 'Enter', matches Enter with all modifiers. The 0.81.6 compatibility layer does the same thing: it expands each string into all 16 combinations of Command, Control, Option, and Shift.
  • A { key: 'Enter' } entry has no modifier fields. In react-native-macos 0.81.9, each matcher uses a different rule for a modifier that the entry does not specify:
    • The JS matcher in Libraries/Components/View/View.js and the Paper native matcher in React/Views/RCTHandledKey.m set the modifier to false. These matchers match Enter only when the user presses no modifier.
    • The Fabric native matcher (KeyEvent.h, operator==(const KeyEvent&, const HandledKey&)) accepts all values for the modifier. This matcher matches Enter with all modifiers, the same as a legacy string entry.
  • On macOS, keyPressCallback in useKeyProps ignores events that have Option, Control, Command, or Shift. Thus the user callback runs for the same events as before.
  • On Paper, a modified key such as Shift+Enter now goes to the AppKit responder chain. Before this change, the legacy entry blocked it. This agrees with the macOS focus guidance in this repository: register only the keys that the control owns, and do not block key equivalents.

Remaining legacy references

This PR does not change these references. They stay because FURN supports react-native-macos 0.73 and 0.74:

  • packages/utils/interactive-hooks/src/useKeyProps.ts: the legacy branch for react-native-macos versions before 0.78.
  • packages/utils/interactive-hooks/src/useKeyProps.types.ts: the deprecated validKeysDown and validKeysUp fields.
  • packages/utils/adapters/src/filters.macos.ts: the 0.74 view mask lets passthroughAllKeyEvents, validKeysDown, and validKeysUp through, and the 0.81 view mask extends the 0.74 mask. The 0.81 mask keeps these props on purpose. Consumers on react-native-macos 0.81.6 and later can still send the legacy props, and the react-native-macos compatibility layer translates them. If the mask removed them, FURN would silently remove these consumer props.
  • packages/utils/adapters/src/platformProps.ts: ViewAdditionsMacOS keeps the three props. They are now @deprecated.
  • packages/utils/adapters/src/__drift__/macos.drift.ts: MacOSViewInjected lists the three props, because the filter mask lets them through and the react-native-macos 0.81 types do not declare them.
  • packages/components/Link/src/useLink.ts and packages/components/Text/src/Text.tsx: filteredProps sets validKeysDown and validKeysUp to undefined, together with keyDownEvents and keyUpEvents. These lines remove both forms, so they are correct on all versions of react-native-macos.

Verification

Automated checks (local macOS, Yarn 4.18.0, Node.js 26.10.0 unless the row says Node.js 22):

Command Result
yarn Passed
yarn format Passed. The formatter changed no other files.
yarn build Passed (root-prebuild and root-build)
yarn test in packages/utils/interactive-hooks Passed: 4 suites, 39 tests (22 new), 2 snapshots. No snapshot changed.
yarn lint in packages/utils/interactive-hooks and packages/utils/adapters Passed: 0 warnings, 0 errors
yarn lage test lint --scope @fluentui-react-native/interactive-hooks (the package, its dependencies, and all consumers) 57 of 58 tasks passed. 1 task failed: @fluentui-react-native/storybook-desktop-runtime test had a 5 s Jest timeout in useStorybookTheme.test.tsx. Another build made the machine load very high, and the suite took 224 s. With this change, yarn test in that package passed alone: 2 suites, 14 tests, 6 s. The failed test does not use key props.
yarn lage test lint --to @fluentui-react-native/adapters Passed: 4 tasks, including the drift checks (verify-drift)
yarn check-agent-instructions Passed
yarn change:check Passed
yarn clean-lage-cache, then yarn lage buildci (Node.js 26) 94 tasks passed and 1 task failed: @fluentui-react-native/desktop-driver test. Lage then stopped the other test tasks. The failure is UND_ERR_INVALID_ARG from undici on Node.js 26, and it occurs on every run. With Node.js 22 (the CI version), yarn test in desktop-driver passed alone: 13 suites, 58 tests passed, 3 skipped.
yarn lage buildci --continue --concurrency 6 (Node.js 22, after the run above) 169 of 171 targets passed (81 ran, 88 came from the cache) and 2 failed: desktop-driver test and storybook-desktop test. All of these failures were timeouts during high machine load from other builds. Neither package depends on interactive-hooks or adapters, and neither package has a diff from upstream/main. The failed storybook-desktop tests passed in a second run alone (4 of 4). desktop-driver passed alone (13 of 13 suites).

A mutation check changed the version limit from < 78 to < 100. As expected, 4 of the new tests then failed. The tests mock Platform.OS with jest.replaceProperty, the same as other FURN tests. They mock Platform.constants with jest.spyOn(Platform, 'constants', 'get').

JavaScript tests do not prove the native behavior. This PR did not build or run fluent-tester on macOS.

Manual test plan for fluent-tester on macOS (react-native-macos 0.81, Paper and Fabric):

  1. Start fluent-tester on macOS.
  2. Open the Button page. Press Tab until a Button has the focus.
  3. Press Space. Make sure that the Button activates one time.
  4. Press Enter. Make sure that the Button activates one time.
  5. Press Shift+Enter, and then press Command+Enter. Make sure that the Button does not activate.
  6. Open the Link page. Press Tab until a Link has the focus.
  7. Press Space, and then press Enter. Make sure that the Link activates one time for each key.
  8. Open the Menu page. Open a Menu.
  9. Press Down Arrow and Up Arrow. Make sure that the focus moves between the menu items.
  10. Move the focus to an item that has a submenu. Press Right Arrow. Make sure that the submenu opens. Press Left Arrow. Make sure that the submenu closes.
  11. Move the focus to a menu item. Press Enter. Make sure that the item activates one time and that the Menu closes.
  12. Optional: Do steps 2 to 11 again on react-native-macos 0.74 to examine the legacy path.
Before After
On react-native-macos 0.78 and later, FURN sends validKeysDown and validKeysUp. Versions 0.78 to 0.81.5 ignore these props. Version 0.81.6 and later translate them with a deprecated compatibility layer. On react-native-macos 0.78 and later, FURN sends keyDownEvents and keyUpEvents, the same as on win32.

Pull request checklist

This PR has considered (when applicable):

  • Automated Tests
  • Documentation and examples
  • Keyboard Accessibility (the native check needs the manual test plan above)
  • Voiceover
  • Internationalization and Right-to-left Layouts

Questions for reviewers

  1. The version check uses only the minor version, the same as getRnVersion(). A react-native-macos build from main reports version 1000.0.0, and a 1.x release would also have a low minor version. These builds would get the legacy path. Do we want to add a major === 0 condition here and in filters.base.ts?
  2. With { key } entries, Paper does not block modified keys such as Shift+Enter, but Fabric blocks them. Is this difference acceptable? The other option is to add explicit false modifier fields to each entry.
  3. Do we want a follow-up PR that removes the legacy branch, the deprecated types, and the mask entries when FURN drops react-native-macos 0.73 and 0.74?

…tive-macos 0.78+

react-native-macos 0.78 removed native support for validKeysDown and
validKeysUp. Share one worker between the key down and key up hooks, and
use the win32 keyDownEvents/keyUpEvents path on react-native-macos 0.78
and later. Keep the legacy validKeys props only for react-native-macos
0.73 and 0.74.

Mark the legacy macOS key props as deprecated in KeyPressProps and in the
adapters ViewAdditionsMacOS type, and update the C-FOCUS-004 evidence.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a127e053-064c-4e11-b063-fab2f57107ae
@github-actions

Copy link
Copy Markdown
Contributor

Bundle size report

Tree-shaken, minified production esbuild bundles with React and React Native runtimes externalized.

Scenario Modules-Mac (Δ) Modules-Win (Δ) Size-Mac (Δ) Size-Win (Δ)
components-button 62 (+0) 63 (+0) 49.94k (+0b) 49.87k (+0b)
components-catalog 159 (+0) 160 (+0) 155.26k (+0b) 155.19k (+0b)
design-theme-state 33 (+0) 34 (+0) 30.11k (+0b) 30.03k (+0b)
design-themed-root 37 (+0) 38 (+0) 32.07k (+0b) 32.00k (+0b)
design-color-lib 6 (+0) 6 (+0) 6.33k (+0b) 6.33k (+0b)
design-theme-state-legacy-source 34 (+0) 35 (+0) 31.77k (+0b) 31.69k (+0b)
design-legacy-theme-from-flex 57 (+0) 58 (+0) 97.72k (+0b) 97.59k (+0b)

The job is advisory: size changes are reported but do not fail the pull request. Bundle or analysis errors still fail.


View bundle-size workflow run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant