diff --git a/.changeset/focus-management-foundation.md b/.changeset/focus-management-foundation.md new file mode 100644 index 00000000000..e9e9542c620 --- /dev/null +++ b/.changeset/focus-management-foundation.md @@ -0,0 +1,21 @@ +--- +"@fluentui-react-native/framework-base": minor +"@fluentui-react-native/design": minor +"@fluentui-react-native/components": minor +"@fluentui-react-native/storybook-desktop-runtime": patch +"@fluentui-react-native/storybook-desktop": minor +"@fluentui-react-native/desktop-driver": patch +"@fluentui-react-native/focus-zone": patch +--- + +Add ref-backed focus targets and cancellable focus requests, shared focusable pressable behavior, live focused-owner modality subscriptions, and shared-controller popup input boundaries. Migrate agentic components and commit TabList eligibility before requesting native focus. Preserve native activation, adapt unidentified Win32 key codes without duplicate presses, and expose Toggle/Select accessibility actions. Add owned targeted native focus qualification through smoke story/tag selectors. + +Keep the focusable pressable implementation native-only so React-only Framework Base consumers can load the generic entrypoint without importing the React Native runtime. + +Separate native keyboard press cleanup from activation pairing, preserving macOS keydown activation and clearing pressed feedback after interruption, blur, disable, or target replacement. Centralize endpoint-specific accessibility action contracts in Framework Base, and execute shared focus stories on macOS with narrowly scoped Windows-only pointer assertions. + +Carry owned modifier state on macOS native input events so rapid WebDriver chords such as Shift+Tab are delivered correctly. + +Clarify that FocusZone uses geometric directional navigation on macOS, while Windows and Win32 opt into geometry with use2DNavigation. + +Coalesce same-instance callback-ref handoffs without changing snapshots or mount generations, while rejecting detached-target activation immediately. Track repeated native key events separately from held physical keys so a single modifier release cannot leave later input modified. diff --git a/.changeset/layered-themed-root.md b/.changeset/layered-themed-root.md new file mode 100644 index 00000000000..84c77ed47e6 --- /dev/null +++ b/.changeset/layered-themed-root.md @@ -0,0 +1,5 @@ +--- +"@fluentui-react-native/design": minor +--- + +Add a View-compatible ThemedRoot with inherited theme and appearance options, default Flex tokens, and a stable scene context that tracks keyboard and pointer input without rerendering consumers. diff --git a/.changeset/native-focus-visuals.md b/.changeset/native-focus-visuals.md new file mode 100644 index 00000000000..df6aa2d86fc --- /dev/null +++ b/.changeset/native-focus-visuals.md @@ -0,0 +1,10 @@ +--- +"@fluentui-react-native/components": patch +"@fluentui-react-native/callout": patch +"@fluentui-react-native/focus-zone": patch +--- + +Use native focus visuals on all platforms by default while retaining the custom +FocusVisual implementation behind a shared evaluation switch. Require React +Native Windows 0.81.35 or newer for the native focus visual crash fix. +Refresh the Windows Callout and FocusZone NuGet locks to the same runtime version. diff --git a/.changeset/root-modality-focus-visuals.md b/.changeset/root-modality-focus-visuals.md new file mode 100644 index 00000000000..270f65d8e70 --- /dev/null +++ b/.changeset/root-modality-focus-visuals.md @@ -0,0 +1,5 @@ +--- +"@fluentui-react-native/components": minor +--- + +Add useFocusVisuals and shared focus-ring styling, and migrate focusable components to optional FocusRing state slots. Windows and macOS default to native rings; Win32 defaults to keyboard-modality-aware custom rings. The hook supports native-ring overrides and always-visible focused custom rings. Component scenes now require ThemedRoot, and unstable state no longer carries focusVisualProps or Checkbox's local focusVisible flag. diff --git a/.changeset/storybook-themed-scene.md b/.changeset/storybook-themed-scene.md new file mode 100644 index 00000000000..204a3c3b1fd --- /dev/null +++ b/.changeset/storybook-themed-scene.md @@ -0,0 +1,6 @@ +--- +"@fluentui-react-native/storybook-desktop-runtime": minor +"@fluentui-react-native/components": patch +--- + +Place the entire desktop Storybook app in one ThemedRoot, derive chrome colors from its theme state, and render component test scenes under a shared ThemedRoot wrapper. diff --git a/.changeset/wdio-focus-contracts.md b/.changeset/wdio-focus-contracts.md new file mode 100644 index 00000000000..3f6684d491d --- /dev/null +++ b/.changeset/wdio-focus-contracts.md @@ -0,0 +1,6 @@ +--- +"@fluentui-react-native/components": patch +"@fluentui-react-native/storybook-desktop": patch +--- + +Migrate all component catalog story tests to named WDIO callbacks and add Windows/Win32 focus contract coverage, including activation pairing, native target lifetime, nested modality, editable fields, TabList sequencing, and FocusZone navigation. Keep Node-only test helpers out of production emit, require story type coverage, preserve focused smoke filters, and allow worker HTTP handles to close naturally while retaining bounded cleanup. diff --git a/.github/actions/setup-desktop-driver/action.yml b/.github/actions/setup-desktop-driver/action.yml index 3ef48b768b8..bc15c0648a1 100644 --- a/.github/actions/setup-desktop-driver/action.yml +++ b/.github/actions/setup-desktop-driver/action.yml @@ -62,6 +62,10 @@ runs: run: | set -euo pipefail + # AppKit's all-controls Tab order is independent of input permissions or screen lock. + defaults write -g AppleKeyboardUIMode -int 3 + swift -e 'import AppKit; guard NSApplication.shared.isFullKeyboardAccessEnabled else { fputs("macOS Keyboard navigation is not enabled.\n", stderr); exit(1) }; print("macOS Keyboard navigation enabled.")' + cache_root="$RUNNER_TEMP/furn-desktop-driver-native" mkdir -p "$cache_root" echo "FURN_DESKTOP_DRIVER_CACHE_ROOT=$cache_root" >> "$GITHUB_ENV" diff --git a/.github/skills/agentic-component-authoring/SKILL.md b/.github/skills/agentic-component-authoring/SKILL.md index df27ed89b81..d83ef87e6ce 100644 --- a/.github/skills/agentic-component-authoring/SKILL.md +++ b/.github/skills/agentic-component-authoring/SKILL.md @@ -25,16 +25,17 @@ authoring rule in one always-loaded instruction file. ## Load focused references -| Work | Reference | -| ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -| Source interpretation, provenance, contract drafting, review, or divergences | [Contract authoring](../agentic-component-contract-authoring/SKILL.md) | -| Public props, slots, state types, native prop exposure, or exports | [Types and slots](references/types-and-slots.md) | -| Defaults, derived state, interaction hooks, accessibility, or slots | [State and accessibility](references/state-and-accessibility.md) | -| Tokens, theme caching, state precedence, or slot style application | [Styles and tokens](references/styles-and-tokens.md) | -| Pure slot rendering, component assembly, or display names | [Rendering and assembly](references/rendering.md) | -| Runtime tests, type tests, snapshots, Storybook stories, or validation | [Tests and stories](references/tests-and-stories.md) | -| Native React Native Windows Fabric components, codegen, registration, or UIA | [Windows Fabric native components](references/windows-fabric-native-components.md) | -| Cross-component duplication, shared extraction, or dependency hygiene | [Package optimization](../agentic-component-optimization/SKILL.md) | +| Work | Reference | +| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| Source interpretation, provenance, contract drafting, review, or divergences | [Contract authoring](../agentic-component-contract-authoring/SKILL.md) | +| Public props, slots, state types, native prop exposure, or exports | [Types and slots](references/types-and-slots.md) | +| Defaults, derived state, interaction hooks, accessibility, or slots | [State and accessibility](references/state-and-accessibility.md) | +| Focus, focus visuals, keyboard activation, or native focus refs | [Common focus authoring](references/focus.md), then [Windows/Win32](references/focus-windows.md) or [macOS](references/focus-macos.md) | +| Tokens, theme caching, state precedence, or slot style application | [Styles and tokens](references/styles-and-tokens.md) | +| Pure slot rendering, component assembly, or display names | [Rendering and assembly](references/rendering.md) | +| Runtime tests, type tests, snapshots, Storybook stories, or validation | [Tests and stories](references/tests-and-stories.md) | +| Native React Native Windows Fabric components, codegen, registration, or UIA | [Windows Fabric native components](references/windows-fabric-native-components.md) | +| Cross-component duplication, shared extraction, or dependency hygiene | [Package optimization](../agentic-component-optimization/SKILL.md) | A new higher-order component normally needs every reference. A focused fix should load only the affected reference and its immediate neighbors. Keep the component's colocated `SPEC.md` and companion files authoritative for its contract. diff --git a/.github/skills/agentic-component-authoring/references/focus-macos.md b/.github/skills/agentic-component-authoring/references/focus-macos.md new file mode 100644 index 00000000000..1e9cf6184c1 --- /dev/null +++ b/.github/skills/agentic-component-authoring/references/focus-macos.md @@ -0,0 +1,180 @@ +# macOS focus authoring + +Read [common focus authoring](focus.md) first. Match shared accessibility goals, +but do not transplant Windows click-focus, modifier, or key-up policies into +AppKit. + +**Reviewed implementation:** local `react-native-macos` 0.81.9, including Fabric +View, repository FocusZone, and Callout implementations, September 14, 2026. +Verify the renderer branch and resolved component before use. A `.macos.tsx` +file can be a stub: V1 Checkbox's macOS entrypoint warns and renders null, so it +is not a valid macOS focus reference. + +## First responder, key window, and actual target + +AppKit routes keyboard input through the key window and its first responder. +Keep window activation separate from control focus. A window can retain a first +responder while inactive; React `focused` alone does not describe whether its +focus visual should be presented as active. + +- Prefer a supported native host ref/command. In native adapters request focus + with the owning window's first-responder mechanism on the main thread. +- Do not treat `makeFirstResponder` returning true as conclusive proof that the + requested view owns focus. The current responder can refuse to resign, and + AppKit documents a case where the window becomes responder when the requested + object refuses. Confirm the actual target through focus events/observation. +- Resolve the actual editable control. An NSTextField may use the window's + shared field editor; the first responder can therefore differ from the visible + control/ref. Native focus-within mapping must recognize this relationship. +- Keep native view identity scoped to the mounted window/surface. Clear it on + detach/recycle and use common React 19 ref composition, not legacy componentRef + exposure or global numeric-tag lookup. + +Local references: +`packages\components\FocusZone\macos\RCTFocusZone.m:24-35,122-129`; +`packages\components\FocusZone\macos\RCTFocusZoneComponentView.mm:118-179`. +Installed RNmacOS `React\Fabric\Mounting\ComponentViews\View\RCTViewComponentView.mm:1719-1804` +implements focus/blur commands and responder event emission. + +## Pointer focus and click-through + +Do not force Windows-style focus-before-press on ordinary macOS controls. +V1 `useOnPressWithFocus` recognizes macOS as focus-capable but only requests +click focus on Windows/Win32. The installed Fabric View also overrides +`needsPanelToBecomeKey` with a comment preserving mouse-versus-keyboard focus. + +`acceptsFirstMouse` controls click-through in an inactive window; it is not an +alias for `focusable`, first-responder acceptance, or keyboard ring visibility. +Test first click to activate a window separately from an actual control action. +Menus can intentionally focus on hover, so document that owner-specific +exception rather than forcing all click/hover paths into one rule. + +Evidence: `packages\utils\interactive-hooks\src\useOnPressWithFocus.ts:14-29`; +installed RNmacOS `RCTViewComponentView.mm:1769-1779`; +`packages\components\Menu\src\MenuItem\useMenuItem.ts:68-120`. + +## Keyboard navigation, shortcuts, and editing + +- V1 key helpers prefer key-down on macOS, versus key-up on Windows/Win32. + The macOS branches use `validKeysDown`/`validKeysUp`; current RNmacOS native + handling also has `keyDownEvents`/`keyUpEvents`. Resolve and validate the + compatibility path instead of configuring both indiscriminately. +- Matching native handled-key entries suppress AppKit's `super` handling. + Register only keys owned by the control/scope so key equivalents, native + navigation, and text editing are not swallowed. +- Preserve Command shortcuts, Option text movement, Control/VoiceOver chords, + IME composition, and the distinction between Return and Space/default-button + activation. Do not copy Win32's modifier allowance or TabList Ctrl+Tab behavior. +- Confirm exactly-once keyboard/accessibility activation; an AX action is not + equivalent evidence to a physical key sequence. +- RNmacOS Pressability activates Return/Space on keydown; keyup supplies + `onPressOut`. Keep native press cleanup separate from Windows keyup-activation + pairing. Intervening keys, caller cancellation, blur, disable, and target + replacement must not strand pressed feedback or duplicate release callbacks. + Retain a pooled event only with `persist()` and release that reference when + the keyboard press ends. +- Use AppKit's key-view loop where applicable. Respect keyboard-navigation/Full + Keyboard Access settings and test enabled/disabled settings explicitly. + `NSApplication.isFullKeyboardAccessEnabled` is not KVO-observable according to + Apple; do not invent a KVO subscription or assume one cached startup value + remains current. +- The shared native focus stories enter through an editable control and Tab, + rather than forcing ordinary pointer clicks to focus. The all-controls lane + requires Keyboard navigation enabled; record that setting, obtain permission + before changing a developer's setting, and restore changes authorized as + temporary. Keep it enabled on development/CI Macs unless the owner chooses + otherwise; locking the desktop gates physical input, not this preference. + A Windows-only pointer + assertion may be a no-op on macOS, but the shared focus/activation assertions + must still execute. +- Do not equate VoiceOver navigation focus with the application's keyboard first + responder. Validate role/name/value and native action behavior in both modes. + +Evidence: `packages\utils\interactive-hooks\src\useKeyProps.ts:8-95,114-126`; +installed RNmacOS `RCTViewComponentView.mm:1807-1861`. + +## FocusZone and Fabric wrapper lifetime + +The local macOS FocusZone translates key events, recalculates the key-view loop, +and falls back to mounted descendant order when AppKit collapses a Fabric-backed +zone to one key-loop entry. The Fabric wrapper deliberately does not participate +as another first-responder stop; it forwards focus to its content view. + +Preserve that single-owner behavior. Do not add a second JS roving loop to the +same keys. Recompute eligibility when children mount/unmount and when props +change, and clear the default responder on recycle. Retain direction, wrap/stop, +RTL, nested zones, scrolling, and editable-child semantics. + +Evidence: `packages\components\FocusZone\macos\RCTFocusZone.m:584-675`; +`packages\components\FocusZone\macos\RCTFocusZoneComponentView.mm:118-195`. + +## Popup windows and lifecycle + +Callout's AppKit window can become key but not main. Its `setInitialFocus` and +`focusWindow` make the popup window key; that alone does not establish a particular +child as first responder. Its separate Fabric touch attachment also demonstrates +why React ancestry is not sufficient evidence for popup input capture. + +Define and test initial child focus after content attachment, Escape, nested +popup dismissal, outside click, window/application deactivation, and guarded +return to the parent control. Do not reactivate the application or steal the +outside-click target's focus as an unconditional cleanup action. + +The legacy ContextualMenu uses a macOS timer after a layout effect. Treat it as +evidence of a native readiness problem, not a reusable delay constant. Prefer a +mount/window-ready handshake with cancellation and focus confirmation. Keep a +bounded, documented workaround only if the supported renderer has no such path. + +Reuse lifecycle-safe local event-monitor/observer ownership: install once for +the owning popup/window, remove exactly once, and capture weak native owners. +Do not install OS-global keyboard hooks or require desktop-driver permissions +for ordinary production focus behavior. + +Evidence: +`packages\native\Callout\macos\CalloutWindow.swift:9-38`; +`packages\native\Callout\macos\CalloutView.swift:33-45,142-216`; +`packages\native\Callout\macos\RCTCalloutComponentView.mm:96-132`; +`packages\native\Callout\macos\GuardedEventMonitor.swift:1-29`; +`packages\components\ContextualMenu\src\ContextualMenu.tsx:39-55`. + +## Native versus custom ring drawing + +Keep native macOS rings enabled by default unless the reviewed control requires +a custom path. The installed Fabric View draws a rounded AppKit focus mask only +when `enableFocusRing` is true. A focusable view or an enum property by itself is +not proof that the ring mask/drawing is implemented correctly. + +When drawing a custom ring, disable the competing native path and preserve the +decorative/hit-testing contract. Revalidate mask bounds after layout/radius +changes, key-window transitions, clipping, appearance, Increase Contrast, and +backing-scale changes. Use AppKit points and conversions; do not copy Windows +Composition physical-pixel calculations into NSView layout. + +Evidence: installed RNmacOS `RCTViewComponentView.mm:1729-1753` and Apple's +focus-mask documentation below. Ring colors remain a native/theme concern; +do not resolve opaque system colors through a guessed web-color fallback. + +## Native service candidates and qualification + +An opt-in native projection can help with window activity, keyboard-navigation +preferences, renderer-scoped target observation, or popup readiness if public +React Native APIs do not supply them. A TurboModule is suitable for a non-view +capability/snapshot service; view focus belongs to refs or typed native commands. +Keep generated Apple glue in Objective-C++ as required by the pinned codegen; +use AppKit/Swift/C++ behind that boundary without introducing UIKit assumptions. + +Run physical keyboard/pointer scenarios, VoiceOver, active/inactive windows, +first-click behavior, keyboard-navigation settings, editable field editors, and +native/custom rings. Include Paper only where it remains supported and Fabric +as an independently qualified endpoint. This Windows-based documentation +investigation did not execute a macOS native run. + +## Public references + +- [AppKit makeFirstResponder]() +- [AppKit first responder and key-view loop](https://developer.apple.com/documentation/appkit/nswindow) +- [Click-through and acceptsFirstMouse]() +- [Full Keyboard Access query](https://developer.apple.com/documentation/appkit/nsapplication/isfullkeyboardaccessenabled) +- [AppKit field editor](https://developer.apple.com/library/archive/documentation/TextFonts/Conceptual/CocoaTextArchitecture/TextEditing/TextEditing.html) +- [Focus ring mask bounds](https://developer.apple.com/documentation/appkit/nsview/focusringmaskbounds) +- [Focus ring mask drawing]() diff --git a/.github/skills/agentic-component-authoring/references/focus-windows.md b/.github/skills/agentic-component-authoring/references/focus-windows.md new file mode 100644 index 00000000000..bcf96e33111 --- /dev/null +++ b/.github/skills/agentic-component-authoring/references/focus-windows.md @@ -0,0 +1,163 @@ +# Windows and Win32 focus authoring + +Read [common focus authoring](focus.md) first. This file applies the shipped V1 +Win32 behavioral model to both Windows endpoints, while keeping their native +implementations separate. + +## Identify the endpoint + +| Endpoint | Reviewed local implementation | Native owner | +| --------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------- | +| `win32` | `@office-iss/react-native-win32` 0.81.8, Paper; prebuilt REX host in Storybook | Office Win32 host/view implementation | +| `windows` | `react-native-windows` 0.81.35, Fabric | Windows App SDK Composition component views and React Native island | + +These are observed dependency versions on September 14, 2026, not minimum-version +guarantees for every feature. Recheck the lockfile, platform resolution, and host +before implementation. Office Win32 Paper is **not** RNW's old UWP XAML renderer, +even though both are described as old-architecture/Paper in some contexts. + +## Carry V1 behavior across, not its plumbing + +For both endpoints preserve, unless the component's reviewed contract differs: + +- focus-before-activation for Windows pointer interaction where the V1 control + requests it; +- one action per Enter/Space/accessibility invocation, including menu-close and + keydown/blur/keyup sequences; +- selection/focusability commit before imperative focus and Narrator announcement; +- explicit per-control disabled discoverability; +- menu hover-focus suppression, navigation ownership, and guarded focus return. + +Use existing V1 Button/Checkbox/RadioGroup/Switch E2E sequences as scenarios for +both Windows test lanes. The V1 implementation need not run inside Fabric: map +its observed behavior to a shared scenario contract, run V1 on Win32 as the +reference, then verify the new implementation independently on both renderers. +Do not require matching native element classes or identical ring pixels. + +Sources: +`packages\utils\interactive-hooks\src\useOnPressWithFocus.ts:14-29`; +`packages\components\Button\src\useButton.ts:15-17,40-66,99-113`; +`packages\components\TabList\src\Tab\useTab.win32.ts:64-110`; +`packages\components\Menu\src\MenuItem\useMenuItem.ts:68-120`. + +## Focus requests and native identity + +On Win32 use the host-supported ref/command path. V1 `componentRef` and +`useViewCommandFocus` are compatibility evidence, not the desired new public API. +Apply the common React 19 ref rules and resolve the actual mounted target. + +On Fabric prefer the supported host focus method or a generated native command. +Native adapters operate on the correct mounted `ComponentView` in its owning +surface/island. They must not reuse Paper UIManager/view-manager identifiers or +assume a React ref can be serialized into a TurboModule argument. + +The existing Fabric FocusZone uses `TryFocus(FocusState::Keyboard)` and checks +the root's focused component after the request +(`packages\components\FocusZone\windows\FRNFocusZone\FocusZoneComponentView.cpp:175-224`). +Reuse this request-versus-observation pattern. Do not report success merely +because JavaScript called an optional `.focus()` method. + +RNW's reviewed root stores a native focus state and resolves programmatic focus +using its own policy. A request for the already-focused component returns early. +This matters for modality-only changes: native focus reasons and the design +root's modality are related, but are not automatically the same source. +Do not synthesize a blur/refocus cycle simply to redraw a ring. + +Installed evidence: +`Microsoft.ReactNative\Fabric\Composition\RootComponentView.cpp:79-118`. +This native implementation detail is not a promised public JavaScript API. + +## Keyboard delivery and exactly-once activation + +- Inspect the actual installed Pressable and native view handlers. Do not add + key-up activation on top of an existing native press action. +- Use `useFocusablePressable` rather than per-control key activation. Native + qualification found Office Win32 events with `code="Unidentified"` and a + valid `key`. The helper supplies one paired key-up fallback only when Win32's + code-based Pressability cannot activate that key; recognized codes stay native. + Pairing is cancelled on blur, disable, and target-generation changes. +- Preserve required native accessibility actions. Checkbox and Switch supply `Toggle`; + Tab supplies `Select`. The Win32 UIA toggle/selection patterns were unavailable + without those action declarations even though JavaScript state was correct. +- V1 `useKeyProps` prefers key-up on Windows/Win32. It supplies Win32 + handled-key descriptors, but its Windows branch deliberately omits descriptors + because of the historical `key` versus `code` mismatch. Translate and test the + pinned Fabric representation instead of copying either branch blindly. +- Treat native `Handled`/capture registration and JavaScript + `preventDefault`/`stopPropagation` as separate mechanisms. Establish which + cancellation reaches native navigation at each phase. +- Pair keyboard invocation within the appropriate scene/gesture lifetime. + Do not reproduce Button's module-global arming flag as a process-global + cross-scene dependency. +- Forward caller handlers and preserve text editing, IME, modifiers, key repeat, + and accessibility actions. Global root capture observes input; it must not + consume every key. + +Sources: `packages\utils\interactive-hooks\src\useKeyProps.ts:8-95,114-126`; +`packages\components\FocusZone\windows\FRNFocusZone\FocusZoneComponentView.cpp:71-133`. + +## Native and custom focus visuals + +Current new-component defaults remain native on Fabric and custom on Win32. +V1 uses native Win32 rings and appearance-specific focused borders, so this is +a choice to qualify, not evidence that Win32 native rings are unavailable. + +- Request native rings only on the actual focus owner. Menu hover focus and + disabled-focusable cases may require a different visibility policy from a + generic keyboard-focus rule. +- On the custom path retain the optional slot and mounted configured ring Views. + Keep the root/slot ref stable through focus and theme changes. +- Qualify Button primary versus high contrast separately; do not apply its + two-tone Win32 inner-border recipe universally. +- Use platform/Fluent colors for native drawing. Preserve opaque system-color + values; do not turn them into guessed CSS literals. +- For Fabric native visuals, use the renderer's focus/border hosting and + lifecycle. React layout units and Composition physical pixels differ; apply + the validated point-scale conversion. Test clipping/hosting and recycling. + +RNW 0.81.35 has explicit focus hosting and native theme handling in installed +`Microsoft.ReactNative\Fabric\Composition\CompositionViewComponentView.cpp` +and `Theme.cpp`. See [Windows Fabric native components](windows-fabric-native-components.md) +for codegen, UI-thread, registration, native visual, and UIA requirements. + +## Collections, popups, and platform services + +Use one native/JS navigation owner. Wait for committed target eligibility and +then confirm the resulting native focus; handle disabled/removed destinations. +Preserve Narrator's selection-before-focus order and test rapid navigation. + +Distinguish a Fabric island/popup and an Office Win32 popup. Returning to a +window is not the same as restoring its intended control. Carry a live target +and dismissal reason, and never steal focus after an outside click or host +deactivation. + +Consider `native-lib` only for a demonstrated missing capability such as +surface-scoped native focus observation, window activation, or safely shared +native target resolution. Keep view commands in their owning native components. +Fabric TurboModule support does not establish registration support in a +prebuilt Office Win32 host; that endpoint needs its own host integration or an +explicit capability-unavailable result. + +## Qualification + +Run separate Win32 and Fabric native lanes for the same behavior scenarios: +pointer focus, Enter/Space, stale key-up, repeat/modifiers, accessibility invoke, +selection-before-focus, disabled/removed targets, menu hover, popup return, and +same-target modality changes. Include Narrator, high contrast, display scaling, +active/inactive windows, and nested surfaces. + +Gate on actual focused element and action/announcement count, not just selected +React state or a mocked ref call. A skipped input test is not qualification. +Use owned app/Metro lifecycle commands and preserve unrelated sessions. + +The catalog now uses named WDIO callbacks. The complete Windows and Win32 smoke +lanes each cover 18 cases with native assertions; component-focused cases use +the `desktop-focus` tag. Check aggregate result counts for zero skips before +claiming qualification. A disconnected input desktop can leave semantic cases +passing while all physical-input cases skip. + +For delegated modifier chords, assert parent delivery and unchanged collection +selection, not immovable native focus: the host is allowed to handle the chord. +For native editors, distinguish noneditable/nonfocusable from UIA `IsEnabled`; +Win32's observed enabled-state announcement gap is documented in Input's +interaction companion and does not replace native focus assertions. diff --git a/.github/skills/agentic-component-authoring/references/focus.md b/.github/skills/agentic-component-authoring/references/focus.md new file mode 100644 index 00000000000..da21bd550d1 --- /dev/null +++ b/.github/skills/agentic-component-authoring/references/focus.md @@ -0,0 +1,186 @@ +# Common desktop focus authoring + +Read this file for every focusable component, collection, popup, focus hook, or +native focus adapter. Then load only the applicable platform detail: + +- [Windows and Win32](focus-windows.md) +- [macOS](focus-macos.md) + +These files define authoring and review obligations. Execution status in +the [desktop focus plan](../../../../packages/agentic/components/WIN32-FOCUS-PLAN.md) +distinguishes implemented APIs from native-only follow-ups. Keep each reviewed contract and +source-specific divergences authoritative. + +## Behavioral authority and platform evidence + +Use shipped V1 Win32 behavior as the compatibility model for **both** Office +Win32 Paper and React Native Windows Fabric. Preserve user-observable outcomes: +which element focuses, action count/timing, selection and announcement order, +disabled discoverability, key ownership, and dismissal return. Implement those +outcomes through each renderer's supported APIs, not copied Paper internals. + +On macOS preserve the same accessibility goals but follow AppKit's first +responder, key-window, keyboard-navigation, click, and shortcut conventions. +Document intentional differences rather than imposing Windows input behavior. + +Before changing behavior, record the public export, platform-resolved file, +renderer/version, host, and relevant V1 sequence. Distinguish declarations, +implementation, authored tests, and native results actually executed. A passing +snapshot or `Platform.OS` mock does not prove native focus delivery. + +## Keep these concepts separate + +| Concept | Owner and invariant | +| ---------------------- | ------------------------------------------------------------------------------------- | +| Input modality | Scene controller; stable `RootSettings`, shared by nested theme boundaries. | +| Native self-focus | Actual mounted focus target, not an ancestor receiving a bubbled event. | +| Focus within | Explicit scope/ancestor state, never substituted for self-focus. | +| Focus request | Cancellable intent to move focus; not confirmation that it moved. | +| Focus-visible policy | Component/scope policy using modality and explicit focus intent. | +| Native window activity | Endpoint adapter; a retained focused object need not belong to the active/key window. | +| Selection | Component or collection state according to its controlled/uncontrolled contract. | +| Ring geometry/colors | Styling phase and renderer, not navigation or activation logic. | + +Programmatic focus must not rewrite physical modality to keyboard. A keyboard +menu invocation, hover focus, accessibility action, and dismissal restoration +may need different visual intent. Keep that intent scoped to the request/owner. + +## Modern refs and slot composition + +- Agentic components target React 19.1.4+: receive `ref` as a prop and preserve the + declared native root using `PropsWithRefOf`. Do not add legacy + `componentRef` props or `forwardRef` wrappers to new components. +- Use `React.ComponentRef` for ordinary host-instance typing when + supported. Codegen schemas are a separate grammar: preserve their validated + ref spelling, including existing `React.ElementRef`, until the pinned + generator accepts a change. A newer TypeScript alias does not establish + codegen compatibility. +- The public root ref, internal focus-target ref, popup-anchor ref, and scope ref + can refer to different native objects. Card's structural root must not silently + become its overlay; Input's public View must not silently become its TextInput. + Use the inner slot's ref or a deliberately reviewed imperative API. +- Reuse Framework Base's slot ref composition. It merges base-slot/render refs + and preserves refs returned by slot transforms; do not create another + component-local merge-ref helper or write into the consumer's ref yourself. +- Forward refs through the custom JSX runtime and compatible `as` overrides. + Do not assume the callable slot function is a mounted native target. +- Support object refs, callback refs, React 19 callback cleanup, null detach, + replacement, and development setup/cleanup cycles. Registration cleanup must + be idempotent and cannot remove a newer target registered under the same key. +- Resolve targets at commit/attachment time, not while rendering. A registration + should identify the current host, owning surface/window, and mount generation. + Never keep a naked renderer tag as a permanent target identity. +- A ref becoming non-null is not proof that native focusability/layout has been + committed. Schedule focus from the owning effect/lifecycle after the needed + state is applied, then confirm through the native focus event or an explicit + native result. Cancel on detach, disable, superseding request, or scope close. +- Do not call `focus()` during render or expose a success-shaped fallback for + unsupported targets. Distinguish requested, confirmed, cancelled, not-mounted, + not-focusable, and unsupported outcomes in the proposed adapter contract. + +Existing mechanisms: `packages\framework-base\src\component-patterns\slot.ts:14-76,118-142`, +`render.ts:38-75`, and `useSlot.test.tsx:453-490`. The framework still supports +older React consumers; do not remove its compatibility behavior merely because +agentic components use React 19. + +## State, activation, and rendering + +1. Resolve disabled/interactive state and the actual focus target in + `use_unstable`. + Use `useFocusablePressable` for pressable targets and `useFocusTarget` for + editable or other native targets. Carry `focusTarget`/`focusTargetRef` in + private state; compose the internal ref on the actual focus slot. +2. Compose interaction handlers with exactly one activation owner. Native + Pressable activation, key handlers, and accessibility actions must not each + invoke the same action independently. + Separate activation cancellation from cleanup of an already-started native + press. macOS activates on keydown; Windows/Win32 activate on keyup. Always + finish pressed feedback on release, self-blur, disable, or target replacement + without generating a new action or forwarding `onPressOut` twice. +3. Call `useFocusVisuals` after resolving focus. Store its private optional + `FocusRing` in state and apply `enableFocusRing` only to the focus target. +4. In the styling phase call `applyFocusRingStyles` with theme state and resolved + radius. Its shared defaults are `strokeFocusInner`/`strokeFocusOuter` and + `strokeWidth.thin`/`strokeWidth.thick`. Keep native colors opaque and theme + caches immutable; pass disabled/noninteractive targets as unfocused. +5. Render the optional slot inside the target. Native/custom rings must not + compete; mounted custom ring geometry must not change merely on focus/blur. + +Current `alwaysVisible` means visible **while focused** for any modality and +selects the custom path. It does not focus the control or make an unfocused +control visible. Native window activity remains a separate renderer obligation. +Do not add this option to every public component prop surface without a contract. + +`useRootSettings` remains a stable non-subscribing query. `useRootInputModality` +provides opt-in notifications; `useFocusVisuals` subscribes only while focused +on the custom path. `RootInputBoundary` attaches additional native popup content +to the same controller without changing themes. Do not add scene-wide React +updates or per-control pointer trackers. + +Focus targets expose `requestFocus(intent)`, an observable snapshot, and a +mount generation. A request reports `requested` until a focus event confirms it; +detach, disable, replacement, or explicit cancellation invalidate pending work. +Same-commit ref handoffs to the identical native instance preserve focus, +snapshot identity, and mount generation; inline forwarding callbacks must not +create a subscription/render loop or cancel a valid keyboard press. Registration +epochs reject stale cleanup independently. Genuine detach notifications are +coalesced to the end of the commit, but `current` clears and pending requests +cancel immediately. Never treat the last observed focus snapshot as proof that +a target is still mounted. Higher-level native window activity and cross-window +restore remain separately qualified. + +For nested controls, preserve `target`/`currentTarget` or equivalent native +identity and separate self-focus from focus-within. Functional Input borders, +selection visuals, and hover styling are not generic focus rings. +Do not apply this self-focus filter to Pressability's responder press: a valid +pointer press can originate on a noninteractive label/icon descendant. + +## Collections and popup scopes + +- Use one owner for arrows/Home/End/Tab within a scope. Do not run a JS roving + algorithm and a native FocusZone over the same keys. +- Apply selection/focusability changes before requesting a target. Confirm + focus before treating it as the active native element; retain controlled + selection semantics. +- Define initial focus and dismissal behavior for keyboard, pointer, + accessibility, Escape, outside click, and application deactivation. +- Restore only to a live eligible target in the intended active window. Do not + steal focus back from an outside-click destination. +- React ancestry is not native-window ancestry. A popup can inherit themes and + settings while requiring a separate event attachment to the same controller. +- Keep navigation and restoration in the owner; leaf MenuItem/Tab components + must not invent a global focus manager. + +## Abstraction and native boundaries + +Extend existing helpers before adding layers. General interaction/target +contracts belong in Framework Base; modality and appearance remain in design; +component-specific eligibility, layout, and selection remain in components. + +A future `native-lib` may provide opt-in native capabilities and shared native +projections. It must depend downward on React Native/platform APIs, never on +design or components. Keep view-bound operations on native refs/generated +commands. Use TurboModules for non-view services that actually need them. +Do not add a process-global `focus(tag)` registry, arbitrary OS handle access, +or production dependencies on the desktop automation driver. + +## Required evidence before shipping + +- Object/callback refs, cleanup, `as` replacements, ref replacement, and + unmount/remount retain exactly one live focus target. +- Native focus, active/selected state, event order, and action counts agree. +- Disabled and disabled-focusable policies are intentional per control. +- Same-target modality changes update only the relevant visual consumer. +- Nested owners, popup boundaries, and independent scenes do not leak state. +- Native keyboard/pointer and accessibility runs are non-skipped on each + supported endpoint; include Narrator/VoiceOver and active/inactive windows. +- Theme/high contrast, scaling, clipping, rounded targets, and native/custom + ring exclusivity have renderer-specific evidence. + +## Public references + +- [React 19 refs and cleanup](https://react.dev/blog/2024/12/05/react-19) +- [React imperative handles](https://react.dev/reference/react/useImperativeHandle) +- [React Native 0.81 native commands](https://reactnative.dev/docs/0.81/the-new-architecture/fabric-component-native-commands) +- [Types and slots](types-and-slots.md) +- [State and accessibility](state-and-accessibility.md) diff --git a/.github/skills/agentic-component-authoring/references/state-and-accessibility.md b/.github/skills/agentic-component-authoring/references/state-and-accessibility.md index 2587845514c..69b977c37a0 100644 --- a/.github/skills/agentic-component-authoring/references/state-and-accessibility.md +++ b/.github/skills/agentic-component-authoring/references/state-and-accessibility.md @@ -110,6 +110,27 @@ The component must own its role and state semantics while preserving unrelated c Button warns in development when an icon-only instance lacks an `accessibilityLabel`. Put warnings in an effect so render remains free of observable side effects, and make the dependency list match every value used by the warning. +For a component-owned toggle or select action, use `resolveAccessibilityAction` +from Framework Base with the semantic action, `Platform.OS`, and the caller's +action list. Assign its `accessibilityActions` and compare incoming +`event.nativeEvent.actionName` with its `name`; do not duplicate platform strings. +Windows Fabric uses lowercase names, while Win32 uses `Toggle`/`Select`. +macOS retains those title-case names as custom actions, not an invented +`activate`/AXPress fallback. + +Keep state ownership and disabled guards in the component. Run accepted +component behavior first, then forward the original caller accessibility event +exactly once, including custom and disabled events. Do not synthesize `onPress` +or create a second activation path. The resolver preserves caller labels and +custom names while deduplicating declarations. + +Future universal non-styling accessibility helpers belong in +[`framework-base/src/accessibility`](../../../../packages/framework-base/src/accessibility/AGENTS.md). +Read its source-evidence, native/generic boundary, handler-composition, and +validation requirements before extending it. Verify actual UIA/AX action +invocation and callback counts; a state read after a physical click does not +prove accessibility action dispatch. + ## Use framework interaction and slot hooks Interactive roots should use the framework state hook, such as `usePressableState`, so hovered, pressed, and focused @@ -145,6 +166,10 @@ so another component can extend the state stage without importing package intern ## Platform behavior +For focus ownership, activation, and native target lifetime, read +[common focus authoring](focus.md), followed by the +[Windows/Win32](focus-windows.md) or [macOS](focus-macos.md) detail. + Keep platform-specific native imports out of shared files when React Native forks expose incompatible types. Put fork imports in platform files or redeclare a small platform-neutral contract. Surface unsupported platform behavior rather than silently pretending it succeeded. diff --git a/.github/skills/agentic-component-authoring/references/styles-and-tokens.md b/.github/skills/agentic-component-authoring/references/styles-and-tokens.md index 8198cf58078..26c3882f6ef 100644 --- a/.github/skills/agentic-component-authoring/references/styles-and-tokens.md +++ b/.github/skills/agentic-component-authoring/references/styles-and-tokens.md @@ -119,23 +119,20 @@ Preserve consumer slot behavior unless the component owns it. Button no longer f toggle container use `flexShrink` so constrained labels can wrap. A consumer can still request truncation through the content slot. -## Keep focus visuals mounted +## Share focus behavior and styling -Agentic focusable components render `FocusVisual` inside the interactive slot. Configure its outer -ring and optional inner ring from the component's semantic focus tokens and resolved radius, but keep both configured -Views mounted at rest. `FocusVisual` changes only opacity when focus changes and owns accessibility and hit testing. +Follow [common focus authoring](focus.md) and its Windows/Win32 or macOS detail. +State hooks use `useFocusVisuals` to create a private optional `FocusRing` and determine the native +`enableFocusRing` setting. The custom slot is absent on the native path; when present, its configured Views +remain mounted across visibility changes. -Do not apply React Native `outline*` props conditionally and do not enable the RNW native focus ring. RNW 0.81 Fabric -creates both through a late `BorderPrimitive`; on a background-filled target its owning-root bookkeeping can insert at -index 1 in an empty visual and fail-fast. A style helper alone is insufficient because the invariant is native View -lifetime. +Keep `ThemeState` in the styling phase. Call `applyFocusRingStyles(state.FocusRing, state, resolvedRadius)` +to bind shared focus colors and widths while retaining component-specific geometry. The helper uses an +immutable theme stylesheet; props, focus state, and radius remain outside that cache. -Keep the ring policy local to the higher-order component: - -- choose single versus dual rings from the component specification -- resolve colors, widths, radius, and positioning from its tokens and variants -- place the visual inside the actual focus target -- keep functional component borders separate from focus feedback +Native rings default to Windows/macOS; Win32 uses the root-modality-aware custom ring. The Windows native +path requires RNW 0.81.35 or newer. Do not introduce `outline*` focus styles, local modality trackers, or +functional border changes to implement a focus ring. ## Selected text without layout shift diff --git a/.github/skills/agentic-component-authoring/references/tests-and-stories.md b/.github/skills/agentic-component-authoring/references/tests-and-stories.md index 5b1fff7e908..006f26b8c78 100644 --- a/.github/skills/agentic-component-authoring/references/tests-and-stories.md +++ b/.github/skills/agentic-component-authoring/references/tests-and-stories.md @@ -7,7 +7,12 @@ are [`button.test.tsx`](../../../../packages/agentic/components/src/components/b ## Runtime tests -Use `@testing-library/react-native`. Do not import `react-test-renderer` directly. +Use `render` from the package-private `src/common/renderWithTheme` helper for +component scenes. It places a `ThemedRoot` outside the scene and any supplied +wrapper, preserving the same root settings through rerenders. Import events, +queries, and types from `@testing-library/react-native`; do not import +`react-test-renderer` directly. Tests can dispatch root modality events through +the helper's `test-scene-root` test ID. Prefer public semantics: @@ -134,8 +139,8 @@ Win32 is distinct from its WebDriver `platformName` of `windows`. Branch inside callbacks for genuine platform differences and use explicit skip reasons for unsupported `browser.capabilities['furn:features']`. -Button is the proof of concept and no longer uses custom `desktopDriver` -plans. Keep names static and callbacks self-contained. Node helpers may be +All catalog tests use WDIO; custom `desktopDriver` plans remain only in runner +compatibility fixtures. Keep names static and callbacks self-contained. Node helpers may be dynamically imported inside each callback, but callbacks cannot capture story-module bindings. The shared Babel config strips all test functions before native dependency collection. Named cases get independent previews, @@ -147,14 +152,75 @@ The single-function `wdio` form remains supported. Run with smoke mode runs these functions alongside remaining legacy plans, grouped by story. See the [executable test contract](../../../../packages/agentic/storybook-desktop/README.md#executable-tests-inside-stories). -Legacy Checkbox and Input tests still use static JSON -`parameters.desktopDriver` plans typed with `DesktopStoryTests` from -`@fluentui-react-native/desktop-driver/authoring`. Keep those plans static and -use declarative `platforms` and `requires` until they are migrated; do not add -functions or dynamic values to the old format. - -The components package's `test:stories` project checks the experimental -Button stories without emitting them into the component library. +The components package's `test:stories` project checks every test-bearing story +without emitting it into the component library. Add new test-bearing files to +`tsconfig.stories.json`; the representative runner contract checks this coverage. +Shared Node-only helpers use `*.wdio.ts` and are dynamically imported by source +extension inside callbacks. They are checked by the story project and excluded +from production emit. + +Native-rendered story helpers use `*.story-helpers.tsx` and are also excluded +from library emit while checked by the story project. Use `StoryStatus` for +counter/modality probes: its named accessible View is observable on macOS +Fabric, where a plain paragraph's `testID` is not exposed as an AX element. + +Focus cases use native `focused`, `checked`, and `selected` properties together +with activation and event-order probes. Cover stale key-up, repeats, disabled +stops, same-target modality, nested themes, focus requests, and selection-before- +focus. Use `try/finally` to release held WebDriver actions. Never replace native +state checks with rendered text alone or a default value for an unsupported +property. Desktop support does not imply identical pointer focus or activation +timing. + +### Desktop focus test scope + +Keep shared requirements executable on Windows Fabric, Office Win32, and macOS. +`requireDesktopFocus` checks native capabilities, not an operating-system +allowlist. A missing capability skips the case with a reason; a platform +difference usually changes one assertion rather than skipping the entire case. + +| Behavior | Scope and expected result | +| -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Exactly-once activation, release cleanup, disabled eligibility, focus requests, self-focus, and scene modality | All three desktop endpoints. Assert native focus/state and action counts; include overlapping keys and blur/disable during a press. | +| Focus automatically follows an ordinary pointer press | Windows/Win32 only. Use `expectWindowsPointerFocus`; it deliberately does nothing on macOS, without skipping the case's other assertions. Do not use it for a shared keyboard/programmatic focus requirement. | +| Return/Space activation phase | macOS activates on keydown; Windows/Win32 activate on the paired keyup. Assert the held-key count per platform, then the shared final count. A later blur cannot undo a macOS action that already occurred. | +| Native Tab/Shift+Tab entry, disabled skipping, and TabList navigation | Shared, with macOS Keyboard navigation enabled for the all-controls Tab lane. Use `focusByTab` from the story's editable entry; do not simulate click-focus on the control under test. | +| Modified navigation | Shared for chords delivered to the app. Shift+Arrow is portable; macOS Control+Arrow can be a system shortcut and must not be required to reach a TabList handler. Preserve Command/Option text editing separately. | +| FocusZone navigation | Shared native ownership, platform-specific destinations: Windows/Win32 default to linear traversal unless `use2DNavigation` is enabled; AppKit is geometric. Its macOS scene explicitly requests focus on click, unlike ordinary Pressables. | + +Prefer a narrowly named platform assertion helper when several stories need the +same exception. Do not introduce a generic conditional-expectation layer merely +to hide branching, or make an entire test pass without executing any assertion. +Keep injected `platform` checks inside the extracted callback or its dynamically +imported `*.wdio.ts` helpers; never infer the target from the Node host OS. + +On macOS, record the Keyboard navigation setting as part of the run. With that +setting off, AppKit can legitimately skip buttons and return to an editable +field. Interactive development and CI Macs should have this preference enabled; +the shared CI setup enables and verifies it before app startup. Keep input +authority and locked/noninteractive desktop handling separate from this +preference. Do not silently change machine preferences inside a test or +reinterpret the setting as a missing driver feature. Follow the machine owner's +preference, restore changes authorized as temporary, and test the disabled- +setting policy separately when claiming coverage of both modes. + +Modality text and focus counters do not establish ring visibility. Use separate +resolved-style coverage without incidental scene rerenders, plus native visual +evidence for renderer appearance. Likewise, a checked/selected state read after +a physical click does not exercise UIA/AX action dispatch. + +Report passed, failed, and skipped counts together. A zero-exit smoke traversal +with platform-skipped focus cases is not cross-platform focus qualification. + +The pinned RNmacOS 0.81.9 Fabric View does not project +`accessibilityState.disabled`/`selected` to AXEnabled/AXSelected. Those native +announcement assertions are isolated in explicitly skipped cases; shared +focus, activation, and selection-callback cases still run. Do not substitute +rendered state for an AX result or describe these skips as intended macOS +semantics. Track the native projection gap and revisit the cases when the +renderer is upgraded. An already-disabled AppKit responder may also remain +first responder until focus is moved; rejected-request tests start from a +separate native focus owner rather than assuming disabling itself moved focus. Button uses focused appearance, size, shape, icon, selection, disabled, and constrained-content stories. Icon uses a source and size overview plus focused font, image, SVG, size, color, and accessibility stories. @@ -182,6 +248,11 @@ yarn workspace @fluentui-react-native/agentic-components-storybook storybook bun Run the smallest affected package test while iterating. Run the full package sequence before completion. Run the root `yarn build` when public types, manifests, or project references change. +Run repository/package tests before the owned native Storybook lifecycle, not +concurrently with it: some CLI fixtures still write into the app's generated +state directory. A lease/nonce mismatch is an infrastructure failure, not a +focus-test result; never bypass ownership or authenticated readiness to proceed. + A successful bundle proves story discovery and compilation only. For visual changes, inspect the running target-platform story across hover, pressed, disabled, optional-slot, and constrained-content scenarios. diff --git a/.github/skills/agentic-component-authoring/references/types-and-slots.md b/.github/skills/agentic-component-authoring/references/types-and-slots.md index 5d4f9aaa7ba..9b80c2b30be 100644 --- a/.github/skills/agentic-component-authoring/references/types-and-slots.md +++ b/.github/skills/agentic-component-authoring/references/types-and-slots.md @@ -55,6 +55,11 @@ When a component needs an internal root ref as well, keep both refs and pass one when rendering the slot so Framework Base composes them. Never overwrite the consumer ref. If a primitive can render unrelated native instance types, omit a top-level ref unless it can define one sound, stable imperative contract. +For focus targets, attachment generations, callback cleanup, and view-command +boundaries, follow [common focus authoring](focus.md#modern-refs-and-slot-composition). +Modern ref props do not imply that a structural root is the keyboard focus target +or that a native focus command has completed. + ## Make resolved state complete Build state from: diff --git a/.github/skills/agentic-component-authoring/references/windows-fabric-native-components.md b/.github/skills/agentic-component-authoring/references/windows-fabric-native-components.md index bc676e88ee7..7e9ae6f4bef 100644 --- a/.github/skills/agentic-component-authoring/references/windows-fabric-native-components.md +++ b/.github/skills/agentic-component-authoring/references/windows-fabric-native-components.md @@ -5,6 +5,12 @@ view. It targets the repository's React Native Windows 0.81 line. Verify APIs against the installed `react-native-windows` package before adopting guidance from a newer RNW branch. +For behavior, native target lifetime, and modern React refs, read +[common focus authoring](focus.md) and [Windows/Win32 focus authoring](focus-windows.md). +Shipped V1 Win32 behavior is also the model for Fabric, but Office Win32 Paper +is not the UWP XAML Paper implementation described in the compatibility table +below. Do not transfer its native registration or view-manager assumptions. + ## Decide whether native code is required Prefer a JavaScript component, slots, and React Native primitives when they can diff --git a/.github/skills/agentic-component-contract-authoring/references/sources/flex-x3.md b/.github/skills/agentic-component-contract-authoring/references/sources/flex-x3.md index 1a4180acd8e..5d44e024b87 100644 --- a/.github/skills/agentic-component-contract-authoring/references/sources/flex-x3.md +++ b/.github/skills/agentic-component-contract-authoring/references/sources/flex-x3.md @@ -33,13 +33,13 @@ cross-platform canonical reference unless the work targets iOS. Translate web concepts deliberately: -| Flex evidence | React Native contract | -| ------------------------------------ | ---------------------------------------------------------------------------------- | -| ARIA attributes or native elements | `accessibilityRole`, `accessibilityState`, labels, and UIA/AX expectations | -| CSS pseudo-classes | explicit interaction state and precedence | -| browser focus selectors and outlines | persistent `FocusVisual`; never conditional `outline*` or RNW native focus visuals | -| CSS pixel or layout rules | React Native units, minimum targets, and platform behavior | -| unsupported browser capability | explicit divergence, dependency, or blocker | +| Flex evidence | React Native contract | +| ------------------------------------ | ---------------------------------------------------------------------------------------- | +| ARIA attributes or native elements | `accessibilityRole`, `accessibilityState`, labels, and UIA/AX expectations | +| CSS pseudo-classes | explicit interaction state and precedence | +| browser focus selectors and outlines | shared root-modality policy and optional `FocusRing`, persistent only on the custom path | +| CSS pixel or layout rules | React Native units, minimum targets, and platform behavior | +| unsupported browser capability | explicit divergence, dependency, or blocker | Do not copy Flex prose or token tables into the public package. A mutable Marketplace or X3 HEAD change is candidate drift and does not invalidate a diff --git a/apps/bundle-size/scenarios.json b/apps/bundle-size/scenarios.json index 280a192da91..d86c4dda253 100644 --- a/apps/bundle-size/scenarios.json +++ b/apps/bundle-size/scenarios.json @@ -20,6 +20,12 @@ "exports": ["FlexThemeReference", "ThemeProvider", "useThemeState"], "forbiddenInputPatterns": ["/theming/compat/", "/tokens/legacy/", "/color-lib/"] }, + { + "name": "design-themed-root", + "module": "@fluentui-react-native/design", + "exports": ["ThemedRoot", "useRootSettings", "useThemeState"], + "forbiddenInputPatterns": ["/theming/compat/", "/tokens/legacy/", "/color-lib/"] + }, { "name": "design-color-lib", "module": "@fluentui-react-native/design/color-lib", diff --git a/apps/storybook/AGENTS.md b/apps/storybook/AGENTS.md index 5fcb1dd14ef..278e70f2810 100644 --- a/apps/storybook/AGENTS.md +++ b/apps/storybook/AGENTS.md @@ -46,8 +46,8 @@ Read [`agent-map.yaml`](agent-map.yaml) first for the compact architecture, look - macOS, Windows, and Win32 use their source-built native helpers. Keep the deterministic fake provider limited to package contract tests. - Author new tests as component story `wdio` functions or named function - collections, not app-local test scripts. Button demonstrates migration from - the legacy `parameters.desktopDriver` plans, which remain supported. + collections, not app-local test scripts. All catalog tests use WDIO; legacy + `parameters.desktopDriver` support is retained for runner compatibility. The app owns identity, package discovery, platform exclusions, and manifests. - Run executable tests with `yarn storybook test --` and optionally `--test `. Callbacks receive the target endpoint as `platform`; diff --git a/apps/storybook/README.md b/apps/storybook/README.md index 6daefd57ca5..b24ae090605 100644 --- a/apps/storybook/README.md +++ b/apps/storybook/README.md @@ -15,10 +15,12 @@ runs Storybook in **liteMode**, which mocks out the heavy default on-device UI chain, which does not bundle cleanly with this repo's Metro + Babel + pnpm-linker toolchain (Reanimated's Babel plugin crashes when Metro bundles Reanimated from source). -The shared app shell includes a persistent theme header above the Storybook UI. It can leave stories -unwrapped (`No theme`, the default) or apply the default light, dark, or high-contrast FURN Theme. -The selected Theme wraps the preview decorator, so it applies to every rendered story and remains -selected while navigating between stories. +The shared app shell places one `ThemedRoot` above the theme header and the entire Storybook UI. +Choose the default Flex tokens (`Default Flex`, initially selected) or the default light, dark, +or high-contrast FURN theme. The header, sidebar, addon controls, preview, and stories all inherit +that scene's theme and input-modality settings. Chrome colors are derived from `useThemeState`; +switching themes preserves Storybook and the selected story rather than remounting them. +Story decorators inherit the app root instead of creating preview-only theme boundaries. The macOS, Windows Fabric, and Win32 Paper native endpoints live in this workspace and share the same entry point and generated story catalog. Story discovery and native identity stay @@ -328,5 +330,13 @@ Follow the package-level story authoring instructions in `../../packages/agentic `*.stories.tsx` file next to its component; package discovery belongs in `storybook.config.mts`, adapted by `src/main.ts`. See `../../packages/agentic/components/src/components/button/button.stories.tsx` for the canonical higher-order component example. Prefer named `wdio` functions with stable `testID` selectors and injected -`platform` context, as demonstrated by Button. Checkbox and Input retain the -legacy static `parameters.desktopDriver` format during migration. +`platform` context, as demonstrated by Button. All catalog tests, including +Checkbox, Input, Switch, TabList, and native FocusZone, use this pattern. +Focus-specific cases are tagged `desktop-focus`; the complete native +`stories-and-tests` smoke lane also runs FocusZone and the default control cases. + +To run just the focused component cases through the owned Windows/Win32 +lifecycle, set `STORYBOOK_SMOKE_TAG=desktop-focus` before invoking +`storybook smoke --windows --mode stories-and-tests` or the corresponding +`--win32` command. The callback runner still requires authenticated native +readiness and never substitutes JavaScript state for native focus. diff --git a/apps/storybook/agent-map.yaml b/apps/storybook/agent-map.yaml index 27d9228d5f1..df305c7a802 100644 --- a/apps/storybook/agent-map.yaml +++ b/apps/storybook/agent-map.yaml @@ -6,7 +6,8 @@ sources: config_adapter: src/main.ts app_entry: index.js app_shell: src/StorybookApp.tsx - theme_host: src/StorybookTheme.tsx + theme_host: ../../packages/agentic/storybook-desktop-runtime/src/StorybookTheme.tsx + theme_palette: ../../packages/agentic/storybook-desktop-runtime/src/useStorybookTheme.ts shared_cli: ../../packages/agentic/storybook-desktop shared_runtime: ../../packages/agentic/storybook-desktop-runtime generated_requires: diff --git a/docs/pages/Theming/Basics.md b/docs/pages/Theming/Basics.md index 80e8d47bfca..00bd645f280 100644 --- a/docs/pages/Theming/Basics.md +++ b/docs/pages/Theming/Basics.md @@ -24,6 +24,69 @@ We have default themes for each platform. See [this page](./DefaultThemes.md) fo ## Applying a FURN theme +### Scene roots with Flex themes + +`ThemedRoot` from `@fluentui-react-native/design` (also available from +`@fluentui-react-native/design/theming`) is a `View` with the theme and appearance +options of `ThemeProvider`. It forwards View props, children, and the native View +ref without adding layout or background styles. + +```tsx +import { ThemedRoot } from '@fluentui-react-native/design'; + +export const App = () => ( + + + + + + +); +``` + +An omitted `theme` inherits the nearest theme boundary, including an existing +`ThemeProvider`. Without a boundary it uses a default `FlexThemeReference`. +That default supplies the existing baseline Flex tokens; use an appearance-aware +theme recipe when colors should vary with the resolved appearance. +Nested roots can override individual `appearance` fields, `appearanceSource`, +or `fallbackAppearance` while retaining the inherited source and other options. +An explicit `theme` starts a new theme/appearance configuration, using that +source's defaults plus the root's options. + +The outermost `ThemedRoot` also provides `RootContext`. `useRootSettings()` returns +the same object throughout a mounted scene, including under nested roots that +replace the theme. Its read-only `inputModality` is initially `'pointer'`, becomes +`'keyboard'` on non-modifier key-down/up, and returns to `'pointer'` on pointer-down or touch-start. +Read the property in an event handler when making a focus decision: + +```tsx +import { useRootSettings } from '@fluentui-react-native/design'; + +const root = useRootSettings(); +const onFocus = () => { + const showKeyboardFocus = root.inputModality === 'keyboard'; + // Apply the focus behavior appropriate to this component. +}; +``` + +Reading `useRootSettings()` does not subscribe to changes; do not destructure +the property during render if it needs to remain current in an event handler. +Use `useRootInputModality(subscribe)` for an opt-in reactive read. Focus visuals +subscribe only while focused on the custom-ring path, so input changes do not +rerender the scene or every component. Theme and appearance remain reactive. +The hooks throw outside a +`ThemedRoot`; independent scenes maintain independent modality state. + +Only the outermost root installs tracking handlers. Keyboard and pointer capture +handlers observe descendant input before bubbling handlers, and responder +capture provides a touch fallback without claiming the responder by default. +Caller handlers still run after tracking, and caller responder return values are +preserved. Keyboard tracking requires a platform that emits View key events +(macOS, Windows, Win32, or web); this component does not add native hardware-key +support to iOS or Android. Wrap native popup/window content in `RootInputBoundary` +when its events need another attachment. This shares the existing scene +controller and theme rather than creating an independent root. + ### Populating the Theme The theme is made available by the `ThemeProvider`. It takes in a `ThemeReference` as a value, which is the object created by the functions referenced in the pages above. diff --git a/package.json b/package.json index 260f993a455..bcfc5da3c88 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "launch-editor@npm:^2.9.1": "2.14.1", "lodash": "^4.18.1", "path-to-regexp@npm:8.3.0": "^8.4.2", + "react-native-windows": "0.81.35", "shell-quote": "^1.9.0", "socks@npm:^2.8.3": "2.8.9", "unicorn-magic@npm:^0.3.0": "^0.4.0", diff --git a/packages/agentic/components/AGENTS.md b/packages/agentic/components/AGENTS.md index 545ba889d8d..ff96a662f3f 100644 --- a/packages/agentic/components/AGENTS.md +++ b/packages/agentic/components/AGENTS.md @@ -9,11 +9,19 @@ invariants; detailed authoring recipes live in the - Higher-order components live in `src/components`; read `src/components/AGENTS.md`. - Primitive components live in `src/primitives`; read `src/primitives/AGENTS.md`. - Story files are library source and follow the tests and stories reference. -- Prefer executable tests under top-level `wdio`, as one callback or a map - of named callbacks. Button is the migration proof of concept; legacy - `parameters.desktopDriver` plans remain supported for other components. +- Focus work starts with the [common focus instructions](../../../.github/skills/agentic-component-authoring/references/focus.md), + then the [Windows/Win32](../../../.github/skills/agentic-component-authoring/references/focus-windows.md) or + [macOS](../../../.github/skills/agentic-component-authoring/references/focus-macos.md) detail. V1 Win32 is the + behavioral baseline for both Windows endpoints; renderer-specific plumbing remains separate. +- Author executable tests under top-level `wdio`, as one callback or a map + of named callbacks. All catalog tests use this pattern; legacy + `parameters.desktopDriver` plans remain only in runner compatibility fixtures. Use stable `testID` selectors and the injected target `platform` for real platform differences, with explicit skips for unsupported capabilities. +- Shared focus tests run on macOS, Windows, and Win32. Gate only genuine + platform-specific assertions, such as Windows pointer-focus, rather than the + entire case. Follow the tests-and-stories reference for native activation + timing, macOS Keyboard navigation prerequisites, and pass/skip evidence. - Executable tests are typed with `WdioStory` from `storybook-desktop/testing` using `import type`. Keep callbacks self-contained; dynamically import Node helpers inside @@ -50,8 +58,10 @@ invariants; detailed authoring recipes live in the - Export higher-order components and public types explicitly from `src/index.ts`; export primitives and their public types explicitly from `src/primitives/index.ts`. Never use wildcard exports. - Colocate runtime tests, type tests, and Storybook stories with the implementation. -- Keep legacy desktop story plans inline and statically extractable; do not hide them - behind variables, spreads, functions, or computed values. +- Include every test-bearing story in `tsconfig.stories.json`. The representative + runner contract rejects legacy catalog plans and missing story type coverage. +- Node-only shared test helpers use `*.wdio.ts`, are excluded from production + emit, and are imported dynamically inside the stripped callbacks. - Use package scripts for format, lint, build, tests, and snapshots. - Do not copy web-only APIs, CSS behavior, or DOM assumptions into React Native. - Do not copy or mechanically transform private source bodies into this public diff --git a/packages/agentic/components/WIN32-FOCUS-PLAN.md b/packages/agentic/components/WIN32-FOCUS-PLAN.md new file mode 100644 index 00000000000..ce765d75f63 --- /dev/null +++ b/packages/agentic/components/WIN32-FOCUS-PLAN.md @@ -0,0 +1,673 @@ +# Desktop focus and focus-visual improvement plan + +**Status:** Shared component rework implemented; full native qualification remains incomplete. +**Reviewed baseline:** `69ce270dde98588f8386e60fa4c720cad3af0142`, September 14, 2026. +**Scope:** New agentic components on Office Win32 Paper, React Native Windows +Fabric, and React Native macOS. **V1 Win32 is the behavioral reference for both +Windows endpoints**, not only a compatibility check for Win32. macOS shares the +focus/accessibility goals but has explicit AppKit-specific adaptation. + +The historical filename is retained so existing plan references remain valid. +This refinement adds common/platform authoring instructions, modern-ref +requirements, macOS source research, and a gated `native-lib` assessment. + +## Execution status (September 15-16, 2026 UTC) + +Implemented: + +- Framework Base `useFocusTarget` and `useFocusablePressable`: stable ref-backed + targets, attachment generations, cancellable request status, native focus + confirmation, self-focus filtering, callback-ref handoffs, and Windows pointer + focus before activation. +- Design `useRootInputModality` and `RootInputBoundary`: stable root settings, + deduplicated focused-owner subscriptions, modifier-only filtering, and popup + event attachments sharing the scene controller. +- All eleven focus-ring consumers plus Input use the target foundation. + TabList commits eligibility/selection before requesting focus and distinguishes + its tab stop from confirmed native focus. Card keeps its structural ref and + respects a caller's nonfocusable overlay setting. +- Switch no longer toggles independently on key-up. Native Win32 qualification + exposed `code="Unidentified"`; the shared helper now uses one generation/blur- + guarded, key-based fallback only where native code-based activation cannot run. + Checkbox/Switch and Tab now use Framework Base's shared + `resolveAccessibilityAction` for declaration and event-name agreement. +- The WDIO support from origin/main `8d31a6b27` is integrated in merge + `64c4328ac`. All catalog tests now use named WDIO callbacks: 22 cases across + 12 stories. Shared Node-only helpers and every test-bearing story are checked + without production emit. Compatibility fixtures retain the legacy parser. + Owned smoke selectors apply to WDIO without bypassing leases or readiness. + +Earlier native qualification: all 18 original WDIO cases passed with real input +and zero skips on both Windows Fabric and Win32, before the follow-up repairs +below. The full traversals rendered 155 and 149 stories, +respectively. The upstream authenticated-initial-render readiness fix replaces +the earlier unsuccessful local experiments; the previous Button selection gate +is no longer blocking this suite. + +The cases cover pointer focus, exactly-once Enter/Space activation, repeated +keydown, stale key-up after Tab/blur, disabled tab stops, same-target modality +without refocus, nested roots, programmatic request confirmation and disabled/ +detached targets, native toggle state, caret editing, selection-before-focus, +Home/End/wrap, manual selection, modifier propagation, and linear/geometric +FocusZone navigation. Modifier chords reach the parent without changing +TabList selection; a native host may legitimately move focus in response. + +Win32 TextInput still reports UIA `IsEnabled=true` when configured noneditable, +nonfocusable, and accessibility-disabled. The focus case verifies native Tab +exclusion rather than treating that enabled-state announcement gap as proof of +focusability. It remains separate from the verified focus contract. + +Affected workspace suites pass, as do the root build and package lint/format. +The earlier Windows uncached repository test graph stopped in two unchanged +codemod fixture comparisons. The September 15 macOS rerun passes all 80 tasks +after the Framework Base import-boundary repair below; those fixture failures +were not reproduced or modified on macOS. + +Still gated: a Windows/Win32 rerun of the repaired 22-case catalog, actual UIA/AX +action invocation, VoiceOver, complete three-lane P0/V1 parity, +the native enabled/selected-state announcement gaps, full popup/window restoration and activity +policy, and P4 visual/high-contrast/scale qualification. A complete new menu owner +or RadioGroup is not introduced. N0/native-lib remains deferred: observed +component activation/ref gaps were resolved without a new native module; native +window observation still needs its own scoped proof and host integration. + +### macOS pre-PR validation (September 15, 2026) + +**Repaired:** The initial `foundation-settings` failure came from an eager +React Native import through Framework Base's generic entrypoint. The focus hook +now uses the established `.native.ts` split and type-only shared declarations. +The later press-release regression is also fixed: native keyboard press +bookkeeping is separate from activation pairing, preserving macOS keydown +activation while finishing feedback on release, interruption, self-blur, +disable, or target replacement. Genuine detach/replacement notifies subscribers +even when the focus boolean is unchanged; same-instance callback-ref handoffs +preserve snapshot identity and mount generation without rerender loops. +Framework and actual RNmacOS Pressability integration tests cover cleanup and +exactly-once callbacks. + +**Repaired:** `resolveAccessibilityAction` in +`packages/framework-base/src/accessibility` resolves names and declarations +together and removes duplicates while preserving caller labels and custom +actions. Checkbox/Switch/Tab use `toggle`/`select` on Windows Fabric and retain +`Toggle`/`Select` on Win32 and the existing macOS custom-action path. No generic +`activate` fallback or synthetic press was added. Source-checked transport +evidence and future helper rules live in that directory's `AGENTS.md`. + +**Observed:** Framework Base passes 22 suites/175 tests; Components passes +57 suites/440 tests. Package format, lint, builds, the unified TypeScript build, +and all 80 tasks in `yarn lage test --no-cache` pass. The native macOS driver +build/handshake/self-test contract passes, including deterministic modifier +flag tests. + +**Observed:** The final owned macOS smoke builds and launches the app, renders +all **165 stories**, executes **22 WDIO cases: 20 passed, 2 skipped**, and stops +its app/services. All fourteen focus behavior cases execute without a platform +skip: the original twelve plus overlapping-key and repeated-modifier release +regressions. Six +baseline cases also pass. The two new, isolated native accessibility-state +cases explicitly skip for the RNmacOS 0.81.9 Fabric gaps below. + +The resumed final review also reproduced and repaired two follow-up edge cases: +unconditional snapshot publication could loop on same-instance inline ref +callbacks, and duplicated held-key entries could retain Shift after one keyup. +Regression coverage verifies stable handoffs while focused/pressed, immediate +rejection of detached-target activation, genuine-detach cleanup, physical-key +deduplication, and a native forward Tab after repeated Shift down and release. +Run the native lifecycle after repository tests: a concurrent attempt failed +with a lease nonce mismatch before any case ran, while the serial rerun passed. + +The port preserves native differences rather than simply deleting skips: + +| Concern | Implemented test policy | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Pointer focus | `expectWindowsPointerFocus` asserts the Windows/Win32 click-focus contract only; the rest of each case runs on macOS. Activate the app through an editable entry before counting clicks. | +| Keyboard entry and activation | Real Tab from a story's editable entry establishes focus. macOS activates Return/Space on keydown; Windows/Win32 activate on paired keyup. Final counts and cleanup remain shared. | +| Modifier chords | macOS native input now derives CGEvent flags from the owned key ledger, fixing rapid Shift+Tab and modified-arrow delivery. The app-level test does not require macOS system-owned Control+Arrow to reach TabList. | +| Native observation | `StoryStatus` exposes counters through named accessible Views because RNmacOS Fabric paragraphs are not standalone AX elements. Native focus and checked-state assertions remain native. | +| FocusZone | AppKit uses geometric directional navigation even when `use2DNavigation` is false; Windows/Win32 default to linear traversal and opt into geometry. Both navigation and Tab-exit cases pass on macOS. | + +Keyboard navigation was initially disabled on this development Mac; enabling it +made the native all-controls Tab lane available. This is an AppKit preference, +not keyboard injection authority or a screen-unlock operation. Per the owner's +direction it is now left enabled, and shared macOS CI setup enables and verifies +it. The hosted CI job's separate `physicalClick` mask remains in place because +of its recorded input-interception limitation. + +**Remaining evidence/renderer limits:** + +- RNmacOS 0.81.9 Fabric excludes the `accessibilityState.disabled`/`selected` + native update block, so AXEnabled/AXSelected do not reflect these props. + The two skipped cases expose this gap; focus exclusion, state-owner + callbacks, and activation guards are tested separately, not presented as AX + state evidence. +- An AppKit view can remain first responder after becoming nonfocusable. + Rejected-request tests move focus to another native owner first rather than + assuming that disabling itself transferred focus. +- Actual Windows UIA Toggle/SelectionItem.Select and macOS AX custom-action + invocation still need native qualification; physical clicks and state reads + are not substitutes. VoiceOver, native/custom ring visual comparison, and + broader window/restore scenarios remain unexecuted. +- Windows and Win32 need the repaired catalog rerun; their earlier 18-case + results do not cover these follow-up changes. + +Keep the wider three-lane V1 parity, exhaustive popup restoration, P4 +scale/clipping/contrast matrix, and N0/native-lib work explicitly deferred. +Those follow-ups are not a reason to add a new menu owner, RadioGroup, or native +module to this PR, but they must not be described as completed qualification. + +The investigation used three independent read-only passes over V1 leaf controls, +navigation/popups, and shared interaction/native-JavaScript adapters, followed by +a comparison with the new components. Repository paths and line ranges below +refer to the reviewed baseline. **Observed** describes source behavior, not a +claim that native interaction was executed. **Inferred** identifies a likely gap +that still needs a native reproduction. + +## Platform contract and authoring deliverables + +| Target | Behavioral authority | Implementation evidence | +| ------------------ | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | +| Office Win32 Paper | Shipped V1 Win32 behavior and E2E sequences | Installed `@office-iss/react-native-win32` 0.81.8 and the supported Office/REX host | +| Windows Fabric | The same V1 Win32 focus, activation, navigation, and announcement outcomes | Installed `react-native-windows` 0.81.35, Composition component views, native focus state, and UIA | +| macOS | Common accessibility goals adapted to AppKit click, keyboard, and window conventions | Installed `react-native-macos` 0.81.9, local FocusZone/Callout implementations, and AppKit/VoiceOver | + +Versions are observations of this checkout, not claims about the latest release. +Preserve outcomes across Windows implementations without copying Paper view +managers, key descriptors, ref bridges, native classes, or pixels into Fabric. + +The initial detailed instruction structure is now authored: + +| File | Responsibility | +| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | +| `.github\skills\agentic-component-authoring\references\focus.md` | Shared vocabulary, ownership, React 19 refs/cleanup, slot integration, abstractions, and qualification | +| `.github\skills\agentic-component-authoring\references\focus-windows.md` | V1-backed behavior, separate Win32/Fabric transports, keyboard handling, native focus reasons, Narrator, and visuals | +| `.github\skills\agentic-component-authoring\references\focus-macos.md` | First responder/key window, click-through, keyboard settings, field editors, popup readiness, AppKit rings, and VoiceOver | + +Skill/package/ref guidance routes to these files. The common file owns shared +rules and breakouts own platform detail; the existing +`AGENTS.md#focus-visual-policy` anchor remains a router for component specs. +Each implementation stage must refine these instructions from verified evidence. +Proposed abstractions are not APIs that already exist. + +## Recommendation + +Fix focus ownership, native focus requests, and keyboard activation before +changing ring appearance. Keep the current separation: + +- The scene root owns input modality. +- Interaction helpers own the actual focus target and event delivery. +- Collection/popup owners coordinate navigation, selection, and focus return. +- `useFocusVisuals` chooses visual policy. +- `applyFocusRingStyles` supplies shared theme defaults; component styles supply + geometry and preserve semantic differences. + +Use V1 Win32's observable focus/activation sequences as the starting contract +for Fabric too. Reuse current slot/ref composition and native FocusZone/Callout +ownership instead of building a parallel universal focus manager. + +V1 does **not** support the assumption that Win32 native focus rings are generally +unavailable. It exposes and uses `enableFocusRing`, including per-item suppression +for menu hover. Preserve the new hook's current Win32 custom-ring default until +an explicit native/custom comparison supports a reviewed policy change. + +## Evidence and differences + +The V1 comparison drives both Windows implementation lanes. macOS differences +and renderer-specific evidence are detailed after this index. + +| Area | Observed V1 behavior | New-component comparison and consequence | +| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Public compatibility baseline | `ButtonV1` and `CheckboxV1` are explicit public aliases; Win32 Tab has its own implementation. [V1, V2, V6] | Follow public exports and platform resolution, not similarly named legacy or mobile implementations. Input's generic V1 file is a stub, so it is not a Win32 behavior reference. [V9] | +| Pointer focus | Checkbox/Switch use `useOnPressWithFocus`; that helper requests imperative focus on Windows/Win32 before forwarding the press. [V2, V3, H1] | New controls wrap React Native `Pressable` and forward `onPress`, without this explicit focus-request layer. This is a delivery/parity question, not proof that native Pressable never focuses. [N1] | +| Activation timing | Shared key helpers use Win32 key-up activation, handled-key descriptors, and propagation control. Button also pairs keyboard invocation with earlier keydown and resets on blur to prevent a menu-close/reopen sequence. [V1, H2] | New Button relies on native Pressable; Switch additionally toggles in `onKeyUp`. Qualify exactly-once activation before either adding or removing handlers. [N1, N2] | +| Focus after selection | Win32 V1 Tab changes selection/invocation state, then focuses through an effect; its comment explains the Narrator ordering requirement. [V6] | New TabList changes active/selected values and immediately calls optional `.focus()`. The destination may still have old native focusability, and state can advance without confirmed focus. These are inferred races, not reproduced failures. [N3] | +| Menu pointer versus keyboard feedback | V1 MenuItem focuses/blurs on hover and changes `enableFocusRing` according to hover/focus origin. Disabled Win32 menu items can remain focusable; tabs use a different disabled policy. [V4, V6] | A universal `focused && keyboard` rule is not a complete menu policy. New MenuItem is a leaf, not a replacement for the V1 menu owner. Do not copy disabled-focusable behavior into every component. [N4] | +| Programmatic visibility | The inspected Button two-tone branch tests focused state, and Radio/Switch use focused token states; these callsites do not consult a scene-root modality value. Menu separately suppresses hover-origin native rings. [V1, V4, V10] | The new default follows the last root modality even for programmatic focus. Treat this as an explicit new policy to qualify, not proven V1 parity; owner intent may need to override it. [N5] | +| Scope ownership | MenuList explicitly refocuses a submenu trigger on close. MenuPopover owns initial focus and contains navigation keys. FocusZone exposes native navigation/default-tabbable configuration. [V5, V7] | Root modality is not a focus manager. Keep navigation and dismissal return at the owning scope; a popup's `target` prop alone does not prove all native restore paths. | +| Same-target modality changes | V1 control helpers update local interaction state and menus explicitly manage pointer-origin feedback. [H3, V4] | `ThemedRoot` mutates a stable object; `useFocusVisuals` samples it only during render. A modality-only change can leave the already-focused custom ring stale until another render. Its test currently supplies that render manually. [N5, N6] | +| Imperative target identity | V1 controls carry component refs through the shared focus adapter; a native Win32 focus surface exists, but wrapper/ref compatibility must be checked per entrypoint. [H1, H4] | Card's ref targets its structural View while its Pressable overlay owns focus. Input similarly separates structural root and TextInput. Preserve public ref contracts while providing an explicit internal focus target. [N7] | +| Ring geometry and contrast | V1 Button chooses a Win32 primary/non-high-contrast two-tone path and defaults the native ring off for that path. Checkbox defaults to the native ring; Radio and Switch also have focused token-driven borders. There is no single V1 ring recipe to copy everywhere. [V1, V2, V10] | The new helper applies the same inner/outer token widths to all custom rings, and `FocusVisual` places both rings through absolute-fill Views. Retain this structure initially, then measure insets, clipping, contrast, and actual Win32 rendering against V1. [N8] | +| Validation | V1 has behavioral E2E specifications for Button Enter/Space, Checkbox click/Space, RadioGroup arrows/disabled skipping/Tab, and Switch click/Enter/Space, as well as snapshots and tester scenes. These are defined coverage, not tests executed during this investigation. [V8, V11] | The September 12 Win32 run's three authored input plans all skipped unsupported physical-click/keyboard capabilities. Rendering 143 stories was not focus qualification. New tests replace `Platform.OS` and dispatch synthetic events. [N6, N9] | + +### Evidence index + +Paths are repository-relative except the explicitly identified installed-package +paths. The installed Win32 JavaScript inspected was +`@office-iss/react-native-win32@0.81.8`; re-resolve it before implementation. + +- **V1:** `packages\components\Button\src\index.ts:27-28`; + `packages\components\Button\src\useButton.ts:15-17,37-74,99-113`; + `packages\components\Button\src\Button.tsx:113-140`; + `packages\components\Button\src\ButtonTokens.win32.ts:27-40,56-66,103-111,148-158`. +- **V2:** `packages\components\Checkbox\src\index.ts:22-23`; + `packages\components\Checkbox\src\useCheckbox.ts:54-62,95-98`. +- **V3:** `packages\components\Switch\src\useSwitch.ts:100-104,124-137`. +- **V4:** `packages\components\Menu\src\MenuItem\useMenuItem.ts:29-45,68-89,113-120,142-149`. +- **V5:** `packages\components\Menu\src\MenuList\useMenuList.ts:94-121`; + `packages\components\Menu\src\MenuPopover\useMenuPopover.ts:42-84,100-118`. +- **V6:** `packages\components\TabList\src\Tab\useTab.win32.ts:64-110,142-152`; + `packages\components\TabList\src\TabList\useTabList.ts:142-176,183-200`. +- **V7:** `packages\components\FocusZone\src\FocusZone.tsx:15-45`; + `packages\components\FocusTrapZone\src\FocusTrapZone.ts:15-31`. + These wrappers do not by themselves prove native trap/restoration semantics. +- **V8:** `apps\tester-core\src\TestComponents\FocusZone\FocusZoneE2ETest.tsx:1-120`; + `apps\tester-core\src\TestComponents\TabList\TabListE2ETest.tsx:1-60`; + `apps\tester-core\src\TestComponents\Menu\E2EMenuTest.tsx:1-110`. +- **V9:** `packages\components\Input\src\Input.tsx:22-39`. +- **V10:** `packages\components\RadioGroup\src\Radio\RadioTokens.win32.ts:80,97-102`; + `packages\components\Switch\src\Switch.styling.ts:14-21,32-50`; + `packages\components\Switch\src\SwitchTokens.win32.ts:16-17,50-95`. +- **V11:** `apps\E2E\src\ButtonV1\specs\ButtonV1.spec.win.ts:77-94`; + `apps\E2E\src\CheckboxV1\specs\CheckboxV1.spec.win.ts:81-120`; + `apps\E2E\src\RadioGroupV1\specs\RadioGroupV1.spec.win.ts:123-198`; + `apps\E2E\src\Switch\specs\Switch.spec.win.ts:60-148`. +- **H1:** `packages\utils\interactive-hooks\src\useOnPressWithFocus.ts:14-29`; + `packages\utils\interactive-hooks\src\useViewCommandFocus.ts:8-29`. + Reuse the focus-adapter contract, not an assumed equivalence between every ref + and a focusable native view. +- **H2:** `packages\utils\interactive-hooks\src\useKeyProps.ts:16-39,44-95,114-126`. +- **H3:** `packages\utils\interactive-hooks\src\useAsPressable.ts:47-73,139-149`; + `packages\utils\interactive-hooks\src\usePressableState.ts:44-106`. +- **H4:** Installed Win32 package: + `Libraries\Components\View\View.win32.js:114-188,240-243`; + `Libraries\Components\Pressable\Pressable.win32.js:220-390`; + `src-win\Libraries\Components\Touchable\TouchableWin32.tsx:250-272,583-626`. + JS exposes capture/handled-key processing, focusability, and imperative focus + surfaces. The audited JS/types do not establish a native focus-visible callback + or the C++ ring's rendering/event-order guarantees. +- **N1:** `packages\agentic\components\src\components\button\useButton.ts:48-66`; + `packages\framework-base\src\hooks\usePressableState.ts:37-78`. +- **N2:** `packages\agentic\components\src\components\switch\useSwitch.ts:123-148`. +- **N3:** `packages\agentic\components\src\components\tablist\useTabList.ts:124-164`; + `packages\agentic\components\src\components\tab\useTab.ts:38-46,58-92`. +- **N4:** `packages\agentic\components\src\components\menu-item\useMenuItem.ts:38-81`. +- **N5:** `packages\agentic\design\src\theming\ThemedRoot.tsx:28-83`; + `packages\agentic\design\src\theming\rootContext.ts:3-24`; + `packages\agentic\components\src\common\useFocusVisuals.ts:34-42`. +- **N6:** `packages\agentic\components\src\common\useFocusVisuals.test.tsx:53-72`; + `packages\agentic\components\src\common\focusVisualPolicy.test.tsx:41-84`. +- **N7:** `packages\agentic\components\src\components\card\useCard.ts:59-110`; + `packages\agentic\components\src\components\card\useCardStyles.ts:17-24`; + `packages\agentic\components\src\components\input\useInput.ts:142-179`. +- **N8:** `packages\agentic\components\src\common\applyFocusRingStyles.ts:10-45`; + `packages\agentic\components\src\primitives\focus-visual\focus-visual.tsx:44-84`. +- **N9:** Local, ignored historical evidence: + `apps\storybook\artifacts\win32\desktop-driver\run.json:3-43`, + finished September 12, 2026 at 07:31:17 UTC. Recreate equivalent evidence on an + input-capable host; do not treat this artifact as a portable test dependency. + +### Added Fabric, macOS, and modern-ref evidence + +- **W1:** `packages\components\FocusZone\windows\FRNFocusZone\FocusZoneComponentView.cpp:33-65,71-133,175-224` + subscribes to native focus/key events, requests `TryFocus(FocusState::Keyboard)`, + and checks actual focused identity. This is a Fabric implementation model for + V1 outcomes, not a reason to copy its transport to Win32. +- **W2:** Installed RNW 0.81.35 + `Microsoft.ReactNative\Fabric\Composition\RootComponentView.cpp:79-118` + stores native focus state, resolves programmatic focus through native policy, + and returns early for an already-focused target. + `CompositionViewComponentView.cpp:189-201,419-426,728-791` owns focus visual + hosting. These are projection candidates, not established public JS APIs. +- **M1:** `packages\utils\interactive-hooks\src\useOnPressWithFocus.ts:14-29` + forces click focus only on Windows/Win32. + `packages\utils\interactive-hooks\src\useKeyProps.ts:44-95,114-126` prefers + key-down on macOS and key-up on Windows/Win32, with different key descriptors. +- **M2:** Installed RNmacOS 0.81.9 + `React\Fabric\Mounting\ComponentViews\View\RCTViewComponentView.mm:1719-1804` + implements AppKit focus/blur, ring-mask drawing, responder events, and pointer + focus behavior. Lines 1807-1861 emit key events and use native handled-key + entries to decide whether AppKit receives `super` handling. +- **M3:** `packages\components\FocusZone\macos\RCTFocusZone.m:24-35,122-129,584-675` + resolves editable focus targets and key-view navigation, including a hierarchy + fallback for Fabric-backed zones. + `packages\components\FocusZone\macos\RCTFocusZoneComponentView.mm:118-195` + forwards focus to its content rather than becoming a duplicate tab stop and + clears default-responder state on recycle. +- **M4:** `packages\native\Callout\macos\CalloutWindow.swift:9-38`; + `packages\native\Callout\macos\CalloutView.swift:33-45,142-216`; + `packages\native\Callout\macos\RCTCalloutComponentView.mm:96-132`. + Popup key-window activation, initial child focus, and a separate Fabric touch + attachment are distinct concerns. + `packages\native\Callout\macos\GuardedEventMonitor.swift:1-29` owns monitor cleanup. +- **M5:** `packages\components\ContextualMenu\src\ContextualMenu.tsx:39-55` + contains a macOS layout-effect/timer workaround: a readiness problem to solve, + not a general timer recipe. + `packages\components\Checkbox\src\Checkbox.macos.tsx:22-30` is a stub, not a + working V1 macOS focus reference. +- **R1:** `packages\framework-base\src\component-patterns\slot.ts:14-76,118-142` + already composes refs, callback cleanup, and transform refs; + `packages\framework-base\src\component-patterns\render.ts:38-75` preserves the + custom runtime path. + `packages\framework-base\src\component-patterns\useSlot.test.tsx:453-490` + covers stable native refs and compatible replacements. +- **R2:** `packages\agentic\components\src\refs.types.test.tsx:30-84` distinguishes + structural and pressable root refs. + `packages\native\Callout\src\CalloutNativeComponent.ts:63-70` demonstrates + typed native commands with host refs rather than global tag arguments. + +### macOS adaptation decisions + +| Concern | Required distinction | +| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Pointer activation | Do not impose Windows focus-before-press on ordinary AppKit controls; test inactive-window first click and `acceptsFirstMouse` separately. | +| Keyboard phase and shortcuts | Qualify key-down activation, key equivalents, Command/Option/Control chords, IME, and Return/Space; do not transplant Win32 modifier rules. | +| Focus identity | Track key window and actual first responder, including field-editor ownership; distinguish VoiceOver navigation focus. | +| Keyboard settings | Respect key-view-loop and keyboard-navigation/Full Keyboard Access settings; do not assume a static startup snapshot or unsupported KVO subscription. | +| Popup lifetime | Window activation does not prove child focus. Wait for content/window readiness, cancel stale requests, and avoid unconditional reactivation on dismissal. | +| Ring drawing | Preserve AppKit mask/bounds, window activity, points/backing scale, and Increase Contrast; Composition geometry is not interchangeable. | + +Apple documents that `makeFirstResponder` can fail, and that a true result can +mean the window became responder when the requested object refused. Confirm +the actual responder, not just the boolean. The macOS authoring breakout links +the official AppKit sources and the local implementation evidence. + +## Abstractions to build or extend + +Names in this table other than existing helpers are **proposed contracts**. +Final public names require review and compile-time coverage. + +| Abstraction | Placement and payoff | Constraints | +| ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| Ref-backed focus target/registration | Framework Base; one mounted-target contract for TabList, Card overlays, editable slots, and popup return | Reuse slot ref composition; track attachment generation/scope and cleanup without redirecting public refs. | +| Cancellable focus request/observation | Framework Base contract with injectable platform adapters | Distinguish request/confirmation/cancellation/unavailability; apply eligibility first. No success-shaped optional-ref no-op. | +| Root input controller with focused-owner subscription | Design; stable settings and targeted refresh with multiple native event attachments per scene | No context-value churn, process-global gesture flag, or duplicate per-control modality tracker. | +| Focus scope and return intent | Component-private coordination, reusing native FocusZone/Callout | One navigation owner; request intent does not rewrite global physical modality. | +| Existing focus visual policy and styling | Extend `useFocusVisuals` and `applyFocusRingStyles` | Keep behavior and ThemeState separate; preserve component geometry and native renderer choices. | +| Native capability/environment projection | Optional `native-lib` service if N0 proves a gap | Narrow capabilities, snapshots, and events for app-owned windows/surfaces; no React refs across module arguments. | +| Shared native target/window lifecycle helpers | Optional projection layer reused by FocusZone/Callout | Extract demonstrated duplication; preserve generated registration, UI-thread ownership, and recycle cleanup. | + +### Modern-ref acceptance contract + +Agentic code uses React 19 `ref` props and `PropsWithRefOf`, not new V1 +`componentRef` APIs or `forwardRef` wrappers. Preserve object refs, callback +cleanup, slot `as` replacements, transform refs, development setup/cleanup +cycles, and replacement/unmount cancellation. Ordinary TypeScript may use +`React.ComponentRef`; codegen-specific syntax must match the pinned generator. + +Use existing `prepareSlotProps` ref composition rather than a new merge-ref +utility. Public root, actual focus target, scope, and anchor refs remain distinct. +Never keep a numeric tag as durable identity or submit focus before native +attachment/eligibility is ready. Native confirmation must identify the intended +target, not just a queued command or activated window. Preserve Framework Base's +older-React compatibility while applying the React 19 agentic contract. + +## Is a native-lib package useful? + +**Recommendation: yes as an opt-in, capability-driven foundation; not as an +up-front rewrite or prerequisite for JavaScript-only fixes.** Native focus/window +observation and cross-window readiness are plausible gaps. Local macOS +FocusZone/Callout already duplicate native target lookup and lifecycle work. +Prove the first shared consumer pair before creating the package. + +Proposed location/name: `packages/native/native-lib`, +`@fluentui-react-native/native-lib`, subject to boundary review. + +### Separate three native API responsibilities + +| Kind | Appropriate surface | Avoid | +| ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| Non-view capability, window activity, keyboard-navigation snapshot/observation | Codegen TurboModule and platform-neutral facade where existing RN APIs are insufficient | Global keyboard hooks, production AX/UIA automation, guessed defaults, synchronous cross-thread waits | +| Operation on a mounted control | Existing host `.focus()` or typed native command in the owning renderer/component adapter | TurboModule `focus(reactRef)` or arbitrary process-global `focus(tag)` | +| Shared native implementation | C++/WinRT on Windows; AppKit with Objective-C++ codegen glue/native helpers on macOS | Leaking native types into shared TS, editing generated headers, or mixing UIKit/UWP assumptions with desktop Fabric | + +TurboModules and Fabric views may share a package, but module registration does +not create a view command or its lifetime. If a native service must identify a +target, use a scoped registration token with a mount generation acquired through +owned renderer attachment; reject stale or cross-runtime tokens. + +### Dependency and support contract + +Preserve `components -> framework-base/design`. Introduce native-lib through +optional platform adapters or injected capabilities only after review. Do not +make every Framework Base import eagerly load native code. Native-lib must not +import Framework Base, design, components, or desktop-driver. Existing source +boundary rules, manifests, and project references need an explicit amendment if +the new dependency is approved. + +Keep FocusZone/Callout schemas and visual ownership in their packages. They may +reuse a projection library; do not duplicate their registrations in a monolithic +native-lib view manager. + +Windows Fabric/macOS module support does not establish registration in the +prebuilt Office Win32 host. Win32 needs a supported host extension/projection, +or explicit capability unavailability while existing ref paths keep working. +Do not use top-level `getEnforcing` in an optional facade. + +### N0. Bounded native feasibility spike + +Start only after P0 identifies a concrete missing signal. JS-only P1/P2 work can +proceed independently. + +1. Select one service gap and two real consumers, such as FocusZone and Callout. + Check existing renderer/public APIs before adding a bridge. +2. Define the minimal schema, capabilities, freshness/event ordering, explicit + errors, cleanup, target lifetime, and UI/main-thread scheduling. +3. Build one Windows Fabric and one AppKit projection. Add a module only for the + non-view service; mounted-view commands remain ref-based. +4. Prove attach/recycle/unmount, runtime reload, inactive windows, multiple + surfaces/popups, and module absence in native runs. +5. Measure import/startup/bundle cost. Verify package/autolinking, C++/WinRT + registration, Apple pod/Objective-C++ codegen, and Release packaging. Do not + set `includesGeneratedCode` on a cross-platform package unless generated code + is actually shipped for every supported platform. +6. Establish Office Win32 host integration separately. Promote the package only + if it removes duplication or closes the observed gap; otherwise keep the + adapter local and record why. + +Deliver an ADR before public exports or manifests change. A native package +cannot replace correct selection sequencing, React ref cleanup, or visual policy. + +## Implementation sequence + +### P0. Establish three native desktop qualification lanes + +**Owner:** Storybook/desktop-driver test infrastructure and component stories. +**Prerequisite for behavioral rollout:** real physical-click and keyboard support. + +1. Use paired V1/new Win32 scenes to establish behavior for Button, Checkbox, + Switch, Radio/Tab navigation, and popup opening/dismissal. Run the same + behavioral scenarios against new Windows Fabric components in a separate + host; do not require loading V1 Paper components into Fabric. Add macOS + variants for AppKit-specific click, key phase, keyboard settings, field + editor, first-responder, and key-window behavior. Use stable test IDs, + recorded theme/scale, and existing V1 E2E sequences [V11]. Exclude mobile/stub + entrypoints from reference claims. +2. Record native focused element, event target/currentTarget, event phase, + keydown/up, press-in/out/press, focus/blur, modality, and activation count. + Capture focus state before and after native property updates. +3. Include focus arriving from outside the scene, imperative `.focus()`, popup + creation, click on the already-focused target, and keyboard use after that + click without a focus transfer. +4. Compare native/custom rings independently on all three endpoints, retaining + current defaults during investigation. Record Fabric native focus reasons + and macOS key-window/first-responder state where exposed. Establish whether + support is sufficient, not just whether a prop survives JavaScript. +5. Turn selected traces into deterministic regressions. Keep diagnostic logging + test-only and remove it from normal runtime paths. + +**Exit:** recorded, non-skipped native results for focus delivery and exactly-once +activation. If the host lacks input capability, report the lane as blocked, not +passed. Confirm popup restoration in native tests rather than inferring it from +`target`, FocusTrapZone names, or snapshot output. + +### P1. Repair the focus-target and activation contract + +**Owner:** `framework-base` interaction/focus adapters; component state hooks. +**Depends on:** P0 event/ref findings. + +- Provide one platform-aware focus-request path for real native targets, preserving + caller refs and the V1 focus-before-press behavior where the new host needs it. + Implement V1 observable outcomes on Fabric too, with explicit AppKit differences + on macOS. Keep fork imports in `.win32.ts`, `.windows.ts`, and `.macos.ts` + adapters. Do not import all legacy interaction hooks into the new package. +- Track actual self-focus separately from descendant focus. Qualify bubbling + focus/blur on nested pressables; do not mark every ancestor as the focused + control. Disabled, removed, and re-enabled controls must not retain phantom + focus. +- Centralize keyboard phase, handled-key descriptors, propagation rules, repeat, + modifier, and accessibility-activation behavior where shared. Do not install a + second activation path on top of native Pressable without proving it is needed. +- Use Button's keydown/keyup pairing as evidence for the menu-close/reopen case, + but do not copy its module-global guard blindly. Prefer a scoped gesture token + that can survive the relevant popup lifetime without coupling unrelated scenes. +- Resolve Switch's `onPress` plus `onKeyUp` path from native traces. Required result: + one toggle and one caller notification per intentional activation. +- Keep disabled and disabled-focusable policy explicit per control. V1 menu items + and tabs differ intentionally; any public `disabledFocusable` addition needs + its own reviewed accessibility/API contract. +- Preserve Card's structural ref and Input's TextInput slot ref contracts; add + internal focus-target plumbing rather than silently redirecting public refs. +- Reuse Framework Base's ref composition and cleanup and meet the modern-ref + acceptance contract above. Do not recreate V1 componentRef or freeze tags at + render time. Native-lib use is gated by N0, not assumed. + +**Exit:** Button/Checkbox/Switch canaries pass pointer, Enter, Space, repeat, +modified-key, accessibility activation, blur-between-keydown/up, and unmount cases. +No duplicated action, lost caller event, or parent/child double focus. + +### P2. Make focus-visible state current without rerendering the scene + +**Owner:** design root controller plus component `useFocusVisuals`. +**Depends on:** P0 event ordering and P1 reliable self-focus. + +- Preserve stable `RootSettings` identity and the single root tracker. Add an + opt-in modality-change notification path for the active custom-ring owner. + Subscribe only while it is focused; do not change the context value or broadcast + React state updates throughout the scene. +- Reconcile the current snapshot on focus/subscription so a focus-before-key + delivery sequence cannot permanently miss the initiating keyboard modality. + Remove the manual `rerender` requirement from the same-target regression test. +- Verify which pointer/responder events actually arrive on each endpoint. Retain only the + necessary root capture surfaces and compose user handlers without swallowing + navigation or claiming the responder. +- Separate current physical modality from explicit programmatic focus intent. + A menu opened by pointer may need different feedback from a keyboard-invoked + popup or restored keyboard focus; do not globally change modality to keyboard + merely because `.focus()` was called. +- Model native-window/event boundaries separately from React theme boundaries. + A popup can inherit theme/settings yet require its own event-registration + boundary if events do not reach the outer root. Register it against the owning + scene controller; do not create an unrelated theme root just to repair capture. +- Retain `alwaysVisible` as a focused-only custom-path override. Expose owner-level + intent through internal composition first; do not add it to every component's + public props by default. +- Reconcile renderer focus state and window activity separately from physical + modality. Use AppKit readiness and scoped Fabric signals; add native projections + only if existing supported APIs cannot meet the contract. + +**Exit:** an already-focused ring updates on keyboard/pointer changes without +manual rerender, refocus, or unrelated state changes. Theme switches preserve the +controller. Nested themes share it; independent scenes remain isolated; popup +subscriptions clean up. Unfocused components do not rerender for modality changes. + +### P3. Correct composite navigation and focus-return ownership + +**Owner:** TabList/Tab and existing popup owners; native focus adapters. +**Depends on:** P1; P2 for correct visual feedback. + +- Separate a requested destination from confirmed native focus. For TabList, apply + the target's focusability/selection state before requesting focus, cancel stale + requests, and reconcile through the resulting focus event. Avoid optional-chain + no-ops that leave active state pretending focus moved. +- Evaluate reusing the existing FocusZone navigation contract versus retaining a + JS roving implementation. Pick one owner per scope; do not let both consume the + same arrows. Preserve automatic/manual selection modes. +- Port V1's selection-before-focus accessibility ordering, but do not add its + Ctrl+Tab behavior to the new public TabList contract without review. +- Recover when the active item is removed/disabled and when all items are disabled. + Preserve unrelated caller handlers and reserve text-editing keys for Input. +- Define initial focus, submenu-close return, Escape/Tab handling, hover focus, + pointer capture, and return-to-trigger by dismissal reason. Outside-click + dismissal must not steal focus back from the control the user just clicked. +- If a trigger is removed or becomes unfocusable, use an explicitly owned fallback + or leave focus at the user's destination; never issue a stale ref request. + +**Boundary:** the current new MenuItem is only a leaf. Implement its reliable +focus/activation now; a complete new menu owner, FocusTrapZone redesign, or new +RadioGroup is not implied by this plan. Apply popup cases to existing owners and +carry their requirements into future container contracts. + +**Exit:** native focus, active item, selected item, and visible ring agree after +arrow/Home/End movement, rapid changes, removal, popup opening, and dismissal. +Verify Narrator ordering on both Windows endpoints and VoiceOver behavior on +macOS, not only React state values. + +### P4. Qualify visual parity and roll out deliberately + +**Owner:** component styles, shared focus styles, and conformance stories. +**Depends on:** P1-P3 as relevant to each component. + +- Keep `ThemeState` separate from `useFocusVisuals`. Cache theme-only values in + `applyFocusRingStyles`; keep resolved radius, target bounds, inset/outset, and + interaction state outside shared theme caches. +- Compare V1 and new rings on filled/transparent backgrounds, rounded/square/ + circular targets, high contrast, scaling, clipped ancestors, nested controls, + and selected/disabled states. Test opaque native system colors in the native + renderer rather than converting them into guessed literal colors. +- Only add shared geometry options when measured cases require them. Do not copy + Button's special inner-border treatment to every control or equate native + Windows Fabric/AppKit geometry with Win32 rendering. +- Revisit the Win32 native-ring default only with the P0/P4 evidence and an explicit + contract decision. Native and custom visible rings must never compete. + Include Button primary versus high-contrast behavior and menu hover suppression + in this decision; renderer choice and requested ring visibility are separate. +- Roll out from canaries to all eleven focus-visual consumers, update specifications, + preserve the standalone decorative FocusVisual contract, and record changesets + for intentional public behavior/type changes. + +**Exit:** reviewed behavior and visual evidence on Win32, Windows Fabric, and +macOS. Fabric is a first-class V1-backed target, not only a regression lane. +A successful story traversal is an additional smoke +check, not a substitute for native input and accessibility qualification. + +## Required acceptance matrix + +| Scenario | Required result | +| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| Tab/Shift+Tab enters/leaves scene | Real focus target is correct; keyboard ring is visible; one tab stop per declared owner. | +| Pointer activation, including already-focused target | Correct native focus and exactly one action; ring follows the control's pointer policy immediately. | +| Keyboard after pointer without focus transfer | Active custom ring updates without explicit rerender; modifier-only/IME input follows the reviewed classification. | +| Enter/Space, repeat, modifiers, keydown then blur/unmount | No duplicate toggle, stale key-up activation, or menu reopening. | +| Disabled/re-enabled/removed target | Correct per-control discoverability; no activation or phantom self-focus. | +| Nested controls | Only the actual owner displays focus; child activation does not invoke ancestors unintentionally. | +| TabList movement/removal | Actual native focus tracks committed focusability; automatic/manual selection and Narrator order remain correct. | +| Popup keyboard/pointer open and each dismissal reason | Correct initial focus, key containment, visual intent, and guarded focus return. | +| Theme/high-contrast changes while focused | Stable scene/slot identity and current colors; geometry does not clip or duplicate native visuals. | +| Independent scenes and native popup boundaries | No shared invocation guard leakage, stale controller listener, or missed popup modality event. | +| Object/callback refs, cleanup, replacements, remounts | One live target per attachment generation; public root, inner target, and anchor contracts remain intact. | +| macOS inactive-window first click and keyboard settings | AppKit-appropriate focus/activation and key-view navigation, without forced Windows click-focus behavior. | +| macOS field editors and VoiceOver | Editable native targets and accessibility focus/actions are not confused with wrapper self-focus. | +| Fabric native focus reasons and island transitions | V1 observable behavior with correct native ownership, intent, and confirmed target acquisition. | +| Optional native service absent or target stale | Existing supported paths work; unavailable/stale outcomes are explicit and JS import does not require an absent module. | + +## Validation and limits + +Use owning workspace scripts for unit/type checks and relevant component stories +for native plans. Keep tests under `ThemedRoot`; include real focus event payloads +and ref behavior in integration coverage. Assert handler forwarding and exact +action counts, not only mocked `.focus()` calls. + +The final qualification lane must advertise physical-click and keyboard +capabilities and produce non-skipped results. Use separate app-owned Win32, +Windows Fabric, and macOS lifecycles +and stop owned Metro/app processes between runs. Protect unrelated sessions and +do not alter disabled-input policy to make a report appear green. + +The original investigation/refinement researched macOS native code from a +Windows checkout without running native tests. Subsequent executed results are +recorded in the execution status above. Native event ordering, complete +Callout/FocusTrapZone restoration, renderer ring behavior, and the native-lib +spike remain explicit evidence gates. Do not port mobile stubs, macOS timing +workarounds, legacy global state, or all V1 APIs wholesale. + +Public architecture references: + +- [React 19 ref props and callback cleanup](https://react.dev/blog/2024/12/05/react-19) +- [React Native 0.81 Fabric native commands](https://reactnative.dev/docs/0.81/the-new-architecture/fabric-component-native-commands) +- [React Native 0.81 TurboModules](https://reactnative.dev/docs/0.81/turbo-native-modules-introduction) +- [RNW New Architecture](https://microsoft.github.io/react-native-windows/docs/new-architecture) +- [RNW native components](https://microsoft.github.io/react-native-windows/docs/native-platform-components) +- [AppKit first responder and key window](https://developer.apple.com/documentation/appkit/nswindow) +- [AppKit makeFirstResponder result semantics]() + +Web documentation supplies architectural guidance; the pinned installed sources +remain authoritative for this checkout's detailed APIs. diff --git a/packages/agentic/components/src/common/StoryStatus.story-helpers.tsx b/packages/agentic/components/src/common/StoryStatus.story-helpers.tsx new file mode 100644 index 00000000000..4081f9a786b --- /dev/null +++ b/packages/agentic/components/src/common/StoryStatus.story-helpers.tsx @@ -0,0 +1,11 @@ +/** @jsxImportSource @fluentui-react-native/framework-base */ +import { Text, View } from 'react-native'; + +/** Fabric paragraphs are not standalone AX elements on macOS; expose the status on a named View. */ +export function StoryStatus({ children, testID }: { children: string; testID: string }) { + return ( + + {children} + + ); +} diff --git a/packages/agentic/components/src/common/applyFocusRingStyles.ts b/packages/agentic/components/src/common/applyFocusRingStyles.ts new file mode 100644 index 00000000000..c35b474375c --- /dev/null +++ b/packages/agentic/components/src/common/applyFocusRingStyles.ts @@ -0,0 +1,41 @@ +import { StyleSheet } from 'react-native'; +import type { ViewStyle } from 'react-native'; + +import { themedStyleSheetFactory } from '@fluentui-react-native/design'; +import type { ThemeState } from '@fluentui-react-native/design'; +import { attachSlotProps } from '@fluentui-react-native/framework-base'; + +import type { FocusVisualsState } from './useFocusVisuals'; + +const getStyles = themedStyleSheetFactory('FocusRing', ({ tokens }) => + StyleSheet.create({ + outer: { + borderColor: tokens.color.strokeFocusOuter, + borderStyle: 'solid', + borderWidth: tokens.strokeWidth.thick, + }, + inner: { + borderColor: tokens.color.strokeFocusInner, + borderStyle: 'solid', + borderWidth: tokens.strokeWidth.thin, + }, + }), +); + +/** + * Applies shared theme defaults without replacing the hook's visibility decision. + */ +export function applyFocusRingStyles( + FocusRing: FocusVisualsState['FocusRing'], + theme: ThemeState, + borderRadius?: ViewStyle['borderRadius'], +): void { + if (FocusRing) { + const styles = getStyles(theme); + const radius = { borderRadius }; + attachSlotProps(FocusRing, { + style: [styles.outer, radius], + inner: { style: [styles.inner, radius] }, + }); + } +} diff --git a/packages/agentic/components/src/common/desktopFocus.wdio.ts b/packages/agentic/components/src/common/desktopFocus.wdio.ts new file mode 100644 index 00000000000..450cab32541 --- /dev/null +++ b/packages/agentic/components/src/common/desktopFocus.wdio.ts @@ -0,0 +1,43 @@ +import type { WdioStoryContext } from '@fluentui-react-native/storybook-desktop/testing'; + +export function requireDesktopFocus({ browser, skip }: Pick): boolean { + const features = browser.capabilities['furn:features']; + if (!features) { + throw new Error('Desktop Driver did not provide feature capabilities.'); + } + if (!features.physicalClick || !features.keyboard || !features.focus) { + skip('Native focus qualification requires physical pointer input, keyboard input, and focus observation.'); + return false; + } + return true; +} + +/** Click-to-focus is Windows-specific; ordinary AppKit controls need not take focus on click. */ +export async function expectWindowsPointerFocus( + { browser, platform }: Pick, + testId: string, +): Promise { + if (platform === 'windows' || platform === 'win32') { + await expectNativeState(browser, testId, 'focused', true); + } +} + +export async function focusByTab(browser: WdioStoryContext['browser'], entryId: string, targetId: string): Promise { + await (await browser.$(`~${entryId}`)).click(); + await expectNativeState(browser, entryId, 'focused', true); + await browser.keys('\uE004'); + await expectNativeState(browser, targetId, 'focused', true); +} + +export async function expectNativeState( + browser: WdioStoryContext['browser'], + testId: string, + property: 'focused' | 'selected' | 'checked', + value: boolean, +): Promise { + await browser.waitUntil(async () => (await (await browser.$(`~${testId}`)).getProperty(property)) === value, { + timeout: 5000, + interval: 50, + timeoutMsg: `Expected native ${property}=${value} on "${testId}".`, + }); +} diff --git a/packages/agentic/components/src/common/focusVisualPolicy.test.tsx b/packages/agentic/components/src/common/focusVisualPolicy.test.tsx new file mode 100644 index 00000000000..51b9761abc7 --- /dev/null +++ b/packages/agentic/components/src/common/focusVisualPolicy.test.tsx @@ -0,0 +1,120 @@ +/** @jsxImportSource @fluentui-react-native/framework-base */ +import { cloneElement } from 'react'; +import type { ReactElement } from 'react'; +import { Platform, StyleSheet } from 'react-native'; +import type { AccessibilityRole } from 'react-native'; + +import { fireEvent } from '@testing-library/react-native'; +import { defaultFlexTokens } from '@fluentui-react-native/design/testing'; +import { render } from './renderWithTheme'; + +import { Accordion } from '../components/accordion/accordion'; +import { Button } from '../components/button/button'; +import { Card } from '../components/card/card'; +import { Checkbox } from '../components/checkbox/checkbox'; +import { ListItem } from '../components/list-item/list-item'; +import { ListboxItem } from '../components/listbox-item/listbox-item'; +import { MenuItem } from '../components/menu-item/menu-item'; +import { Radio } from '../components/radio/radio'; +import { Switch } from '../components/switch/switch'; +import { Tab } from '../components/tab/tab'; +import { Tag } from '../components/tag/tag'; + +const cases: { name: string; element: ReactElement<{ disabled?: boolean }>; role: AccessibilityRole }[] = [ + { name: 'Accordion', element: , role: 'button' }, + { name: 'Button', element: