Skip to content
Closed
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 @@ -1429,6 +1429,75 @@ export default ({
</View>
);
},
},
{
title: 'Tab Index',
name: 'tab-index',
render(): React.Node {
return (
<View
testID="view-test-tab-index"
style={{
padding: 10,
gap: 10,
}}>
<RNTesterText style={{fontSize: 16, fontWeight: 'bold'}}>
Tab Index Example
</RNTesterText>
<RNTesterText style={{fontSize: 12}}>
These views should be focusable with tabIndex:
</RNTesterText>
<View
style={{
backgroundColor: 'lightblue',
padding: 10,
borderWidth: 1,
borderColor: 'blue',
}}
tabIndex={1}
accessible
accessibilityLabel="First tab index view">
<RNTesterText>tabIndex: 1</RNTesterText>
</View>
<View
style={{
backgroundColor: 'lightgreen',
padding: 10,
borderWidth: 1,
borderColor: 'green',
}}
tabIndex={3}
accessible
accessibilityLabel="Third tab index view">
<RNTesterText>tabIndex: 3</RNTesterText>
</View>
<View
style={{
backgroundColor: 'lightyellow',
padding: 10,
borderWidth: 1,
borderColor: 'orange',
}}
tabIndex={2}
accessible
accessibilityLabel="Second tab index view">
<RNTesterText>tabIndex: 2</RNTesterText>
</View>
<View
style={{
backgroundColor: 'lightcoral',
padding: 10,
borderWidth: 1,
borderColor: 'red',
}}
tabIndex={-1}
accessible
accessibilityLabel="Negative tab index view">
<RNTesterText>tabIndex: -1 (not in tab order)</RNTesterText>
</View>
</View>
);
},
}, // ]TODO(macOS ISS#2323203)
// Windows]
],
Expand Down
7 changes: 7 additions & 0 deletions packages/e2e-test-app-fabric/test/ViewComponentTest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,11 @@ describe('View Tests', () => {
const dump = await dumpVisualTree('nativeid');
expect(dump).toMatchSnapshot();
});
test('Views can have tabIndex', async () => {
await searchBox('tab');
const componentsTab = await app.findElementByTestID('view-test-tab-index');
await componentsTab.waitForDisplayed({timeout: 5000});
const dump = await dumpVisualTree('view-test-tab-index');
expect(dump).toMatchSnapshot();
});
});
5 changes: 5 additions & 0 deletions vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "ComponentView.h"

#include <Fabric/AbiState.h>
#include <limits>
#include "DynamicReader.h"

#include "ComponentView.g.cpp"
Expand Down Expand Up @@ -612,6 +613,10 @@ bool ComponentView::focusable() const noexcept {
return false;
}

int ComponentView::tabIndex() const noexcept {
return std::numeric_limits<int>::max();
}

facebook::react::SharedViewEventEmitter ComponentView::eventEmitterAtPoint(facebook::react::Point pt) noexcept {
return nullptr;
}
Expand Down
1 change: 1 addition & 0 deletions vnext/Microsoft.ReactNative/Fabric/ComponentView.h
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ struct ComponentView
bool TryFocus() noexcept;

virtual bool focusable() const noexcept;
virtual int tabIndex() const noexcept;
virtual facebook::react::SharedViewEventEmitter eventEmitterAtPoint(facebook::react::Point pt) noexcept;
virtual facebook::react::SharedViewEventEmitter eventEmitter() noexcept;
virtual facebook::react::Tag Tag() const noexcept;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,34 @@ void ViewComponentView::updateProps(
Visual().Comment(winrt::to_hstring(newViewProps.testId));
}

// Update tabIndex using Windows.UI.Composition APIs for focus management
if (oldViewProps.tabIndex != newViewProps.tabIndex) {
auto visual =
winrt::Microsoft::ReactNative::Composition::Experimental::CompositionContextHelper::InnerVisual(Visual());

if (newViewProps.tabIndex != std::numeric_limits<int>::max()) {
// Use Windows.UI.Composition Visual Properties API to store tabIndex for focus navigation
visual.Properties().InsertScalar(L"ReactNative.TabIndex", static_cast<float>(newViewProps.tabIndex));

// Enable Windows.UI.Composition focus participation by setting IsHitTestVisible
// This integrates with the native Windows focus system for proper tab order handling
visual.IsHitTestVisible(true);

// Set focus capability in Visual properties for Windows.UI.Composition focus system
visual.Properties().InsertBoolean(L"ReactNative.Focusable", true);
} else {
// Remove Windows.UI.Composition focus properties when tabIndex is not set
if (visual.Properties().TryGetScalar(L"ReactNative.TabIndex")) {
visual.Properties().Remove(L"ReactNative.TabIndex");
}
if (visual.Properties().TryGetBoolean(L"ReactNative.Focusable")) {
visual.Properties().Remove(L"ReactNative.Focusable");
}
// Reset hit test visibility when not focusable through tabIndex
visual.IsHitTestVisible(false);
}
}

// update BaseComponentView props
updateAccessibilityProps(oldViewProps, newViewProps);
updateTransformProps(oldViewProps, newViewProps, Visual());
Expand Down Expand Up @@ -1323,9 +1351,46 @@ winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ViewComponentV
}

bool ViewComponentView::focusable() const noexcept {
// Windows.UI.Composition focus logic - check Visual Properties for focus capability
if (m_visual) {
auto visual =
winrt::Microsoft::ReactNative::Composition::Experimental::CompositionContextHelper::InnerVisual(Visual());

// Check if explicitly marked as focusable in Windows.UI.Composition Visual Properties
if (auto focusableValue = visual.Properties().TryGetBoolean(L"ReactNative.Focusable")) {
return focusableValue.Value();
}

// Check Windows.UI.Composition Visual Properties for tabIndex-based focus capability
if (auto tabIndexValue = visual.Properties().TryGetScalar(L"ReactNative.TabIndex")) {
auto tabIndex = static_cast<int>(tabIndexValue.Value());
// Elements with tabIndex >= -1 are focusable (negative means focusable but not in tab order)
return tabIndex >= -1;
}
}

// Fallback to React Native props-based focus logic if not set in Windows.UI.Composition
if (m_props->tabIndex != std::numeric_limits<int>::max()) {
return m_props->tabIndex >= -1;
}

// Default React Native focusable behavior from props
return m_props->focusable;
}

int ViewComponentView::tabIndex() const noexcept {
// Windows.UI.Composition focus logic - read tabIndex from Visual Properties
if (m_visual) {
auto visual =
winrt::Microsoft::ReactNative::Composition::Experimental::CompositionContextHelper::InnerVisual(Visual());
if (auto tabIndexValue = visual.Properties().TryGetScalar(L"ReactNative.TabIndex")) {
return static_cast<int>(tabIndexValue.Value());
}
}
// Fallback to React Native props if not set in Windows.UI.Composition Visual Properties
return m_props->tabIndex;
}

std::string ViewComponentView::DefaultControlType() const noexcept {
return "group";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ struct ViewComponentView : public ViewComponentViewT<
facebook::react::LayoutMetrics const &oldLayoutMetrics) noexcept override;
void prepareForRecycle() noexcept override;
bool focusable() const noexcept override;
int tabIndex() const noexcept;
void OnKeyDown(const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept override;
void OnKeyUp(const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept override;
std::string DefaultControlType() const noexcept override;
Expand Down
20 changes: 18 additions & 2 deletions vnext/Microsoft.ReactNative/Fabric/Composition/FocusManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,24 +112,34 @@ void GettingFocusEventArgs::TrySetNewFocusedComponent(

namespace winrt::Microsoft::ReactNative::Composition::implementation {

// Windows.UI.Composition-aware focus navigation helper
// This function implements the core focus navigation logic using Windows.UI.Composition APIs
// and React Native component tree traversal for tab order management
winrt::Microsoft::ReactNative::implementation::ComponentView *NavigateFocusHelper(
winrt::Microsoft::ReactNative::implementation::ComponentView &view,
winrt::Microsoft::ReactNative::FocusNavigationReason reason) {

// Windows.UI.Composition focus logic: Check if this view is focusable first
if (reason == winrt::Microsoft::ReactNative::FocusNavigationReason::First) {
if (view.focusable()) {
return &view;
}
}

winrt::Microsoft::ReactNative::implementation::ComponentView *toFocus = nullptr;

// Focus navigation through component tree with Windows.UI.Composition integration
Mso::Functor<bool(::winrt::Microsoft::ReactNative::implementation::ComponentView & v)> fn =
[reason, &toFocus](::winrt::Microsoft::ReactNative::implementation::ComponentView &v) noexcept
-> bool { return (toFocus = NavigateFocusHelper(v, reason)); };
[reason, &toFocus](::winrt::Microsoft::ReactNative::implementation::ComponentView &v) noexcept -> bool {
return (toFocus = NavigateFocusHelper(v, reason));
};

// Traverse children using Windows.UI.Composition-aware focus logic
if (view.runOnChildren(reason == winrt::Microsoft::ReactNative::FocusNavigationReason::First, fn)) {
return toFocus;
}

// Windows.UI.Composition focus logic: Check if this view is focusable for last navigation
if (reason == winrt::Microsoft::ReactNative::FocusNavigationReason::Last) {
if (view.focusable()) {
return &view;
Expand All @@ -139,9 +149,12 @@ winrt::Microsoft::ReactNative::implementation::ComponentView *NavigateFocusHelpe
return nullptr;
}

// Windows.UI.Composition focus API: Find the first focusable element in the component tree
// Uses Windows.UI.Composition Visual Properties to determine focus capability
winrt::Microsoft::ReactNative::ComponentView FocusManager::FindFirstFocusableElement(
const winrt::Microsoft::ReactNative::ComponentView &searchScope) noexcept {
auto selfSearchScope = winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(searchScope);
// Use Windows.UI.Composition-aware navigation helper for focus search
auto view = NavigateFocusHelper(*selfSearchScope, winrt::Microsoft::ReactNative::FocusNavigationReason::First);
if (view) {
winrt::Microsoft::ReactNative::ComponentView component{nullptr};
Expand All @@ -152,9 +165,12 @@ winrt::Microsoft::ReactNative::ComponentView FocusManager::FindFirstFocusableEle
return nullptr;
}

// Windows.UI.Composition focus API: Find the last focusable element in the component tree
// Uses Windows.UI.Composition Visual Properties to determine focus capability
winrt::Microsoft::ReactNative::ComponentView FocusManager::FindLastFocusableElement(
const winrt::Microsoft::ReactNative::ComponentView &searchScope) noexcept {
auto selfSearchScope = winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(searchScope);
// Use Windows.UI.Composition-aware navigation helper for focus search
auto view = NavigateFocusHelper(*selfSearchScope, winrt::Microsoft::ReactNative::FocusNavigationReason::Last);
if (view) {
winrt::Microsoft::ReactNative::ComponentView component{nullptr};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <react/renderer/components/view/conversions.h>
#include <react/renderer/core/graphicsConversions.h>
#include <react/renderer/core/propsConversions.h>
#include <limits>

namespace facebook::react {

Expand Down Expand Up @@ -71,7 +72,11 @@ HostPlatformViewProps::HostPlatformViewProps(
rawProps,
"accessibilityLiveRegion",
sourceProps.accessibilityLiveRegion,
"none")) {}
"none")),
tabIndex(
ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
? sourceProps.tabIndex
: convertRawProp(context, rawProps, "tabIndex", sourceProps.tabIndex, std::numeric_limits<int>::max())) {}

#define WINDOWS_VIEW_EVENT_CASE(eventType) \
case CONSTEXPR_RAW_PROPS_KEY_HASH("on" #eventType): { \
Expand Down Expand Up @@ -117,6 +122,7 @@ void HostPlatformViewProps::setProp(
RAW_SET_PROP_SWITCH_CASE_BASIC(keyDownEvents);
RAW_SET_PROP_SWITCH_CASE_BASIC(keyUpEvents);
RAW_SET_PROP_SWITCH_CASE_BASIC(tooltip);
RAW_SET_PROP_SWITCH_CASE_BASIC(tabIndex);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include <react/renderer/components/view/BaseViewProps.h>
#include <react/renderer/core/PropsParserContext.h>
#include <limits>
#include "CompositionAccessibilityProps.h"
#include "KeyEvent.h"
#include "WindowsViewEvents.h"
Expand Down Expand Up @@ -38,5 +39,6 @@ class HostPlatformViewProps : public BaseViewProps {
std::optional<std::string> tooltip{};
std::vector<HandledKeyEvent> keyDownEvents{};
std::vector<HandledKeyEvent> keyUpEvents{};
int tabIndex{std::numeric_limits<int>::max()};
};
} // namespace facebook::react
Loading