Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,19 @@ - (void)synchronouslyDispatchAccessbilityEventOnUIThread:(ReactTag)reactTag even
RCTUIView<RCTComponentViewProtocol> *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]
}
}

Expand Down