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
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Only show focus visuals when using keyboard to move focus",
"packageName": "react-native-windows",
"email": "30809111+acoates-ms@users.noreply.github.com",
"dependentChangeType": "patch"
}
10 changes: 9 additions & 1 deletion vnext/Microsoft.ReactNative/ComponentView.idl
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ namespace Microsoft.ReactNative
Last,
};

enum FocusState
{
Unfocused = 0,
Pointer,
Keyboard,
Programmatic,
};

[webhosthidden]
[experimental]
interface IComponentState
Expand Down Expand Up @@ -99,7 +107,7 @@ namespace Microsoft.ReactNative
LayoutMetrics LayoutMetrics { get; };
IInspectable UserData;

Boolean TryFocus();
Boolean TryFocus(FocusState focusState);

DOC_STRING("Used to handle key down events when this component is focused, or if a child component did not handle the key down")
event Windows.Foundation.EventHandler<Microsoft.ReactNative.Composition.Input.KeyRoutedEventArgs> KeyDown;
Expand Down
6 changes: 4 additions & 2 deletions vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ void ComponentView::parent(const winrt::Microsoft::ReactNative::ComponentView &p
oldRootView->TrySetFocusedComponent(
oldParent,
winrt::Microsoft::ReactNative::FocusNavigationDirection::None,
winrt::Microsoft::ReactNative::FocusState::Programmatic,
true /*forceNoSelectionIfCannotMove*/);
}
}
Expand Down Expand Up @@ -431,9 +432,10 @@ void ComponentView::GotFocus(winrt::event_token const &token) noexcept {
m_gotFocusEvent.remove(token);
}

bool ComponentView::TryFocus() noexcept {
bool ComponentView::TryFocus(winrt::Microsoft::ReactNative::FocusState focusState) noexcept {
if (auto root = rootComponentView()) {
return root->TrySetFocusedComponent(*get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None);
return root->TrySetFocusedComponent(
*get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None, focusState);
}

return false;
Expand Down
2 changes: 1 addition & 1 deletion vnext/Microsoft.ReactNative/Fabric/ComponentView.h
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ struct ComponentView

LayoutMetrics LayoutMetrics() const noexcept;

bool TryFocus() noexcept;
bool TryFocus(winrt::Microsoft::ReactNative::FocusState focusState) noexcept;

virtual bool focusable() const noexcept;
virtual facebook::react::SharedViewEventEmitter eventEmitterAtPoint(facebook::react::Point pt) noexcept;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,8 @@ int64_t CompositionEventHandler::SendMessage(HWND hwnd, uint32_t msg, uint64_t w

void CompositionEventHandler::onKeyDown(
const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept {
RootComponentView().UseKeyboardForProgrammaticFocus(true);

if (auto focusedComponent = RootComponentView().GetFocusedComponent()) {
winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(focusedComponent)->OnKeyDown(args);

Expand All @@ -631,7 +633,7 @@ void CompositionEventHandler::onKeyDown(
}

if (!fCtrl && args.Key() == winrt::Windows::System::VirtualKey::Tab) {
if (RootComponentView().TryMoveFocus(!fShift)) {
if (RootComponentView().TryMoveFocus(!fShift, winrt::Microsoft::ReactNative::FocusState::Keyboard)) {
args.Handled(true);
}

Expand All @@ -641,6 +643,8 @@ void CompositionEventHandler::onKeyDown(

void CompositionEventHandler::onKeyUp(
const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept {
RootComponentView().UseKeyboardForProgrammaticFocus(true);

if (auto focusedComponent = RootComponentView().GetFocusedComponent()) {
winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(focusedComponent)->OnKeyUp(args);

Expand Down Expand Up @@ -1173,6 +1177,8 @@ void CompositionEventHandler::onPointerPressed(
winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept {
namespace Composition = winrt::Microsoft::ReactNative::Composition;

RootComponentView().UseKeyboardForProgrammaticFocus(false);

// Clears any active text selection when left pointer is pressed
if (pointerPoint.Properties().PointerUpdateKind() != Composition::Input::PointerUpdateKind::RightButtonPressed) {
RootComponentView().ClearCurrentTextSelection();
Expand Down Expand Up @@ -1269,6 +1275,8 @@ void CompositionEventHandler::onPointerReleased(
winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept {
int pointerId = pointerPoint.PointerId();

RootComponentView().UseKeyboardForProgrammaticFocus(false);

auto activeTouch = std::find_if(m_activeTouches.begin(), m_activeTouches.end(), [pointerId](const auto &pair) {
return pair.second.touch.identifier == pointerId;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,8 @@ void ComponentView::onGotFocus(
const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept {
if (args.OriginalSource() == Tag()) {
m_eventEmitter->onFocus();
if (viewProps()->enableFocusRing) {
if (viewProps()->enableFocusRing &&
rootComponentView()->focusState() == winrt::Microsoft::ReactNative::FocusState::Keyboard) {
facebook::react::Rect focusRect = m_layoutMetrics.frame;
focusRect.origin.x -= (FOCUS_VISUAL_WIDTH * 2);
focusRect.origin.y -= (FOCUS_VISUAL_WIDTH * 2);
Expand Down Expand Up @@ -426,15 +427,20 @@ void ComponentView::HandleCommand(const winrt::Microsoft::ReactNative::HandleCom
auto commandName = args.CommandName();
if (commandName == L"focus") {
if (auto root = rootComponentView()) {
root->TrySetFocusedComponent(*get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None);
root->TrySetFocusedComponent(
*get_strong(),
winrt::Microsoft::ReactNative::FocusNavigationDirection::None,
winrt::Microsoft::ReactNative::FocusState::Programmatic);
}
return;
}
if (commandName == L"blur") {
if (auto root = rootComponentView()) {
root->TrySetFocusedComponent(
nullptr, winrt::Microsoft::ReactNative::FocusNavigationDirection::None); // Todo store this component as
// previously focused element
nullptr,
winrt::Microsoft::ReactNative::FocusNavigationDirection::None,
winrt::Microsoft::ReactNative::FocusState::Programmatic); // Todo store this component as
// previously focused element
}
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ void ContentIslandComponentView::ConnectInternal() noexcept {
m_navigationHost.DepartFocusRequested([wkThis = get_weak()](const auto &, const auto &args) {
if (auto strongThis = wkThis.get()) {
const bool next = (args.Request().Reason() != winrt::Microsoft::UI::Input::FocusNavigationReason::Last);
strongThis->rootComponentView()->TryMoveFocus(next);
strongThis->rootComponentView()->TryMoveFocus(next, winrt::Microsoft::ReactNative::FocusState::Programmatic);
args.Result(winrt::Microsoft::UI::Input::FocusNavigationResult::Moved);
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,10 @@ void ParagraphComponentView::OnPointerPressed(

// Focuses so we receive onLostFocus when clicking elsewhere
if (auto root = rootComponentView()) {
root->TrySetFocusedComponent(*get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None);
root->TrySetFocusedComponent(
*get_strong(),
winrt::Microsoft::ReactNative::FocusNavigationDirection::None,
winrt::Microsoft::ReactNative::FocusState::Pointer);
}

args.Handled(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ winrt::Microsoft::ReactNative::ComponentView RootComponentView::GetFocusedCompon

void RootComponentView::SetFocusedComponent(
const winrt::Microsoft::ReactNative::ComponentView &value,
winrt::Microsoft::ReactNative::FocusNavigationDirection direction) noexcept {
winrt::Microsoft::ReactNative::FocusNavigationDirection direction,
winrt::Microsoft::ReactNative::FocusState focusState) noexcept {
if (m_focusedComponent == value)
return;

Expand All @@ -97,11 +98,26 @@ void RootComponentView::SetFocusedComponent(
winrt::get_self<winrt::Microsoft::ReactNative::implementation::ReactNativeIsland>(rootView)->TrySetFocus();
}
m_focusedComponent = value;
if (focusState == winrt::Microsoft::ReactNative::FocusState::Programmatic) {
focusState =
(!m_useKeyboardForProgrammaticFocus || m_focusState == winrt::Microsoft::ReactNative::FocusState::Pointer)
? winrt::Microsoft::ReactNative::FocusState::Pointer
: winrt::Microsoft::ReactNative::FocusState::Keyboard;
}
m_focusState = focusState;
auto args = winrt::make<winrt::Microsoft::ReactNative::implementation::GotFocusEventArgs>(value, direction);
winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(value)->onGotFocus(args);
}
}

winrt::Microsoft::ReactNative::FocusState RootComponentView::focusState() const noexcept {
return m_focusState;
}

void RootComponentView::UseKeyboardForProgrammaticFocus(bool value) noexcept {
m_useKeyboardForProgrammaticFocus = value;
}

bool RootComponentView::NavigateFocus(const winrt::Microsoft::ReactNative::FocusNavigationRequest &request) noexcept {
if (request.Reason() == winrt::Microsoft::ReactNative::FocusNavigationReason::Restore) {
if (m_focusedComponent)
Expand All @@ -116,14 +132,16 @@ bool RootComponentView::NavigateFocus(const winrt::Microsoft::ReactNative::Focus
view,
request.Reason() == winrt::Microsoft::ReactNative::FocusNavigationReason::First
? winrt::Microsoft::ReactNative::FocusNavigationDirection::First
: winrt::Microsoft::ReactNative::FocusNavigationDirection::Last);
: winrt::Microsoft::ReactNative::FocusNavigationDirection::Last,
winrt::Microsoft::ReactNative::FocusState::Programmatic);
}
return view != nullptr;
}

bool RootComponentView::TrySetFocusedComponent(
const winrt::Microsoft::ReactNative::ComponentView &view,
winrt::Microsoft::ReactNative::FocusNavigationDirection direction,
winrt::Microsoft::ReactNative::FocusState focusState,
bool forceNoSelectionIfCannotMove /*= false*/) noexcept {
auto target = view;
auto selfView = winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(target);
Expand Down Expand Up @@ -157,23 +175,24 @@ bool RootComponentView::TrySetFocusedComponent(

winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(losingFocusArgs.NewFocusedComponent())
->rootComponentView()
->SetFocusedComponent(gettingFocusArgs.NewFocusedComponent(), direction);
->SetFocusedComponent(gettingFocusArgs.NewFocusedComponent(), direction, focusState);
} else {
SetFocusedComponent(nullptr, direction);
SetFocusedComponent(nullptr, direction, focusState);
}

return true;
}

bool RootComponentView::TryMoveFocus(bool next) noexcept {
bool RootComponentView::TryMoveFocus(bool next, winrt::Microsoft::ReactNative::FocusState focusState) noexcept {
if (!m_focusedComponent) {
return NavigateFocus(winrt::Microsoft::ReactNative::FocusNavigationRequest(
next ? winrt::Microsoft::ReactNative::FocusNavigationReason::First
: winrt::Microsoft::ReactNative::FocusNavigationReason::Last));
}

Mso::Functor<bool(const winrt::Microsoft::ReactNative::ComponentView &)> fn =
[currentlyFocused = m_focusedComponent, next](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
[currentlyFocused = m_focusedComponent, next, focusState](
const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
if (view == currentlyFocused)
return false;
auto selfView = winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(view);
Expand All @@ -185,7 +204,8 @@ bool RootComponentView::TryMoveFocus(bool next) noexcept {
->TrySetFocusedComponent(
view,
next ? winrt::Microsoft::ReactNative::FocusNavigationDirection::Next
: winrt::Microsoft::ReactNative::FocusNavigationDirection::Previous);
: winrt::Microsoft::ReactNative::FocusNavigationDirection::Previous,
focusState);
};

if (winrt::Microsoft::ReactNative::implementation::walkTree(m_focusedComponent, next, fn)) {
Expand Down Expand Up @@ -249,7 +269,10 @@ void RootComponentView::start(const winrt::Microsoft::ReactNative::ReactNativeIs
}

void RootComponentView::stop() noexcept {
SetFocusedComponent(nullptr, winrt::Microsoft::ReactNative::FocusNavigationDirection::None);
SetFocusedComponent(
nullptr,
winrt::Microsoft::ReactNative::FocusNavigationDirection::None,
winrt::Microsoft::ReactNative::FocusState::Programmatic);
if (m_visualAddedToIsland) {
if (auto rootView = m_wkRootView.get()) {
winrt::get_self<winrt::Microsoft::ReactNative::implementation::ReactNativeIsland>(rootView)->RemoveRenderedVisual(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,17 @@ struct RootComponentView : RootComponentViewT<RootComponentView, ViewComponentVi
winrt::Microsoft::ReactNative::ComponentView GetFocusedComponent() noexcept;
void SetFocusedComponent(
const winrt::Microsoft::ReactNative::ComponentView &value,
winrt::Microsoft::ReactNative::FocusNavigationDirection direction) noexcept;
winrt::Microsoft::ReactNative::FocusNavigationDirection direction,
winrt::Microsoft::ReactNative::FocusState focusState) noexcept;
bool TrySetFocusedComponent(
const winrt::Microsoft::ReactNative::ComponentView &view,
winrt::Microsoft::ReactNative::FocusNavigationDirection direction,
winrt::Microsoft::ReactNative::FocusState focusState,
bool forceNoSelectionIfCannotMove = false) noexcept;

bool NavigateFocus(const winrt::Microsoft::ReactNative::FocusNavigationRequest &request) noexcept;

bool TryMoveFocus(bool next) noexcept;
bool TryMoveFocus(bool next, winrt::Microsoft::ReactNative::FocusState focusState) noexcept;

RootComponentView *rootComponentView() const noexcept override;

Expand Down Expand Up @@ -90,14 +92,20 @@ struct RootComponentView : RootComponentViewT<RootComponentView, ViewComponentVi
void ClearCurrentTextSelection() noexcept;
void SetViewWithTextSelection(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept;

winrt::Microsoft::ReactNative::FocusState focusState() const noexcept;

void UseKeyboardForProgrammaticFocus(bool value) noexcept;

private:
// should this be a ReactTaggedView? - It shouldn't actually matter since if the view is going away it should always
// be clearing its focus But being a reactTaggedView might make it easier to identify cases where that isn't
// happening.
winrt::Microsoft::ReactNative::ComponentView m_focusedComponent{nullptr};
winrt::Microsoft::ReactNative::FocusState m_focusState{winrt::Microsoft::ReactNative::FocusState::Unfocused};
winrt::weak_ref<winrt::Microsoft::ReactNative::ReactNativeIsland> m_wkRootView{nullptr};
winrt::weak_ref<winrt::Microsoft::ReactNative::Composition::PortalComponentView> m_wkPortal{nullptr};
bool m_visualAddedToIsland{false};
bool m_useKeyboardForProgrammaticFocus{true};

::Microsoft::ReactNative::ReactTaggedView m_viewWithTextSelection{
winrt::Microsoft::ReactNative::ComponentView{nullptr}};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,10 @@ void SwitchComponentView::OnPointerPressed(
m_supressAnimationForNextFrame = true;

if (auto root = rootComponentView()) {
root->TrySetFocusedComponent(*get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None);
root->TrySetFocusedComponent(
*get_strong(),
winrt::Microsoft::ReactNative::FocusNavigationDirection::None,
winrt::Microsoft::ReactNative::FocusState::Pointer);
}

updateVisuals();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,9 @@ struct CompTextHost : public winrt::implements<CompTextHost, ITextHost> {
winrt::check_hresult(
m_outer->QueryInterface(winrt::guid_of<winrt::Microsoft::ReactNative::ComponentView>(), winrt::put_abi(view)));
m_outer->rootComponentView()->TrySetFocusedComponent(
view, winrt::Microsoft::ReactNative::FocusNavigationDirection::None);
view,
winrt::Microsoft::ReactNative::FocusNavigationDirection::None,
winrt::Microsoft::ReactNative::FocusState::Programmatic);
// assert(false);
// TODO focus
}
Expand Down Expand Up @@ -1469,7 +1471,10 @@ void WindowsTextInputComponentView::onMounted() noexcept {
// Handle autoFocus property - focus the component when mounted if autoFocus is true
if (windowsTextInputProps().autoFocus) {
if (auto root = rootComponentView()) {
root->TrySetFocusedComponent(*get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None);
root->TrySetFocusedComponent(
*get_strong(),
winrt::Microsoft::ReactNative::FocusNavigationDirection::None,
winrt::Microsoft::ReactNative::FocusState::Programmatic);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,10 @@ HRESULT UiaSetFocusHelper(::Microsoft::ReactNative::ReactTaggedView &view) noexc
if (rootCV == nullptr)
return UIA_E_ELEMENTNOTAVAILABLE;

return rootCV->TrySetFocusedComponent(strongView, winrt::Microsoft::ReactNative::FocusNavigationDirection::None)
return rootCV->TrySetFocusedComponent(
strongView,
winrt::Microsoft::ReactNative::FocusNavigationDirection::None,
winrt::Microsoft::ReactNative::FocusState::Programmatic)
? S_OK
: E_FAIL;
}
Expand Down
Loading