diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 17945ce64e40..e2252c731e70 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -1890,7 +1890,13 @@ - (void)focus - (void)blur { - [[self window] resignFirstResponder]; + // `NSWindow` does not implement `resignFirstResponder`; only NSResponder + // subclasses inherit it. Calling the selector on the window is a silent + // no-op, leaving programmatic `ref.blur()` from JS without effect. The + // correct AppKit equivalent is `makeFirstResponder:nil`, which clears + // the current first responder. Mirrors the working pattern at + // RCTTextInputComponentView.mm:995-997. + [[self window] makeFirstResponder:nil]; } - (BOOL)needsPanelToBecomeKey diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/VirtualView/RCTVirtualViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/VirtualView/RCTVirtualViewComponentView.mm index ba60397d95b7..adf21b150820 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/VirtualView/RCTVirtualViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/VirtualView/RCTVirtualViewComponentView.mm @@ -161,6 +161,38 @@ - (NSArray *)accessibilityChildren [self _unhideIfNeeded]; return [super accessibilityChildren]; } + +- (BOOL)acceptsFirstResponder +{ + // Mirror of the iOS `focusItemsInRect:` hook for keyboard / Tab + // navigation. AppKit queries `acceptsFirstResponder` while walking + // the responder chain to compute the next Tab target. If this view + // is hidden via the `hideOffscreenVirtualViewsOnIOS` optimization, + // we unhide here so a subsequent Tab landing actually finds a real + // view to focus. + // + // Caveat: a fully `hidden=YES` view is excluded from AppKit's + // responder chain at the parent level, so `acceptsFirstResponder` + // will not be queried until the view becomes visible. This override + // therefore only catches the "visible but flagged offscreen" case + // where `_unhideIfNeeded` is a fast no-op anyway. The complete fix + // for Tab-into-fully-hidden views requires switching the hiding + // strategy from `self.hidden = YES` to something that keeps the + // view in the responder chain (e.g. zero-alpha / out-of-bounds + // frame) — out of scope here; tracked as a follow-up. + [self _unhideIfNeeded]; + return [super acceptsFirstResponder]; +} + +- (id)accessibilityHitTest:(NSPoint)point +{ + // VoiceOver cursor / pointer-hover-with-VoiceOver path. Same + // rationale as `accessibilityChildren` above: a hit-test that + // would land on a flagged-offscreen virtual view should unhide + // it so the AX tree has a real element to return. + [self _unhideIfNeeded]; + return [super accessibilityHitTest:point]; +} #endif // macOS] - (void)prepareForRecycle diff --git a/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm b/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm index 4a517cdcf892..864cb579a9d7 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm +++ b/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm @@ -346,7 +346,19 @@ - (void)synchronouslyDispatchAccessbilityEventOnUIThread:(ReactTag)reactTag even RCTUIView *componentView = [_componentViewRegistry findComponentViewWithTag:reactTag]; // [macOS] #if !TARGET_OS_OSX // [macOS] UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, componentView); -#endif // [macOS] +#else // [macOS + // Move first-responder to the component view and post the + // AppKit-equivalent accessibility notification so VoiceOver picks + // up the focus change. Without this branch the Fabric path for + // `AccessibilityInfo.setAccessibilityFocus(reactTag)` silently + // no-ops on macOS — the old-arch path in + // RCTAccessibilityManager.mm:setAccessibilityFocus: does the + // equivalent work, which we mirror here. + if (componentView != nil) { + [[componentView window] makeFirstResponder:componentView]; + NSAccessibilityPostNotification(componentView, NSAccessibilityFocusedUIElementChangedNotification); + } +#endif // macOS] } }