From df7c45ba0a5ccf586d3e2e9b76468175cfbe3609 Mon Sep 17 00:00:00 2001 From: Harini Malothu Date: Fri, 20 Feb 2026 15:05:06 +0530 Subject: [PATCH 01/11] Workflow for stale branches --- .github/workflows/cleanup-stale-branches.yml | 67 +++++++++++++++++++ docs/branch-lifecycle-policy.md | 69 ++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 .github/workflows/cleanup-stale-branches.yml create mode 100644 docs/branch-lifecycle-policy.md diff --git a/.github/workflows/cleanup-stale-branches.yml b/.github/workflows/cleanup-stale-branches.yml new file mode 100644 index 00000000000..8f65ce09014 --- /dev/null +++ b/.github/workflows/cleanup-stale-branches.yml @@ -0,0 +1,67 @@ +name: Cleanup Stale Branches + +on: + schedule: + - cron: '0 8 * * 1' # Every Monday at 08:00 UTC + workflow_dispatch: + inputs: + dry_run: + description: 'List stale branches without deleting' + type: boolean + default: true + +permissions: + contents: write + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Delete stale branches + # To preserve your branch from cleanup, do ONE of: + # 1. Rename it under archive/: git branch -m my-branch archive/my-branch + # 2. Keep an open PR (including drafts) pointing to it + # 3. Add the branch pattern to PROTECTED below + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + DRY_RUN="${{ github.event_name == 'schedule' && 'true' || inputs.dry_run }}" + COPILOT_DAYS=90 + DEFAULT_DAYS=180 + COPILOT_CUTOFF=$(date -d "$COPILOT_DAYS days ago" +%s) + DEFAULT_CUTOFF=$(date -d "$DEFAULT_DAYS days ago" +%s) + PROTECTED='^(main|master|develop|release/|hotfix/|archive/)' + + echo "Stale threshold: copilot/* = $COPILOT_DAYS days, others = $DEFAULT_DAYS days | Dry run: $DRY_RUN" + + git branch -r --format='%(refname:short) %(committerdate:unix)' | while read -r REF DATE; do + BRANCH="${REF#origin/}" + [ "$BRANCH" = "HEAD" ] && continue + echo "$BRANCH" | grep -qE "$PROTECTED" && continue + + # copilot branches: 90 days, all others: 180 days + if echo "$BRANCH" | grep -q '^copilot'; then + [ "$DATE" -ge "$COPILOT_CUTOFF" ] 2>/dev/null && continue + else + [ "$DATE" -ge "$DEFAULT_CUTOFF" ] 2>/dev/null && continue + fi + + # Skip branches with open PRs (including drafts) + PR_COUNT=$(gh pr list --head "$BRANCH" --state open --json number --jq 'length' 2>/dev/null || echo "0") + if [ "$PR_COUNT" -gt 0 ]; then + echo "[skipped] $BRANCH (has open PR)" + continue + fi + + LAST=$(date -d "@$DATE" +%Y-%m-%d) + if [ "$DRY_RUN" = "true" ]; then + echo "[stale] $BRANCH (last commit: $LAST)" + else + echo "Deleting $BRANCH (last commit: $LAST)" + git push origin --delete "$BRANCH" || echo " Failed to delete $BRANCH" + fi + done diff --git a/docs/branch-lifecycle-policy.md b/docs/branch-lifecycle-policy.md new file mode 100644 index 00000000000..efa4e123258 --- /dev/null +++ b/docs/branch-lifecycle-policy.md @@ -0,0 +1,69 @@ +# Branch Lifecycle Policy + +This repository enforces a branch lifecycle policy to keep the branch list clean and manageable. Stale branches are identified automatically and removed after a period of inactivity. + +## How It Works + +A scheduled GitHub Action ([cleanup-stale-branches.yml](../.github/workflows/cleanup-stale-branches.yml)) runs **every Monday at 08:00 UTC** and scans all remote branches for inactivity. + +| Parameter | Default | Description | +|-----------|---------|-------------| +| Stale threshold | **90 days** | Branches with no commits in this period are considered stale | +| Dry run (scheduled) | **true** | Scheduled runs only report; manual dispatch can delete | + +## Protected Branches + +The following branch patterns are **always excluded** from cleanup: + +- `main` / `master` +- `develop` +- `release/*` +- `hotfix/*` +- `archive/*` + +## How to Keep a Branch + +If your branch must be retained beyond the staleness threshold (e.g., for archival or long-running work), use **any** of these methods: + +### 1. Use a protected prefix +Move or rename your branch under `archive/`: +``` +git branch -m my-old-branch archive/my-old-branch +git push origin archive/my-old-branch :my-old-branch +``` + +### 2. Tag the last commit message +Include one of these markers in any commit message on the branch: +- `[archive]` +- `[keep]` +- `[retain]` +- `[no-cleanup]` + +Example: +``` +git commit --allow-empty -m "[keep] Retaining branch for future reference" +git push +``` + +### 3. Notify maintainers +Open an issue or message the maintainers with the branch name and reason for retention. They can add it to the protected patterns in the workflow file. + +## Manual Cleanup + +Maintainers can trigger the workflow manually from the **Actions** tab: + +1. Go to **Actions → Cleanup Stale Branches** +2. Click **Run workflow** +3. Choose `dry_run = false` to actually delete stale branches +4. Optionally adjust the staleness threshold (in days) + +## FAQ + +**Q: What happens if my branch is deleted by mistake?** +A: Git branch deletions on GitHub are reversible for a short period. You can also restore from a local clone if you still have the branch locally. See [GitHub docs on restoring deleted branches](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-branches-in-your-repository/restoring-a-deleted-branch). + +**Q: Will this delete branches with open pull requests?** +A: The workflow deletes by inactivity date only. If you have an open PR from a stale branch, the branch may still be deleted. Keep the branch active or mark it with `[keep]` to prevent this. + +**Q: Can I change the staleness threshold?** +A: Yes — edit `DEFAULT_STALE_DAYS` in the workflow file, or override it per-run via the manual dispatch input. From 6de27e650999d2f223017f2ba51ea269ad7a3f61 Mon Sep 17 00:00:00 2001 From: Harini Malothu Date: Fri, 20 Feb 2026 15:11:46 +0530 Subject: [PATCH 02/11] Workflow for stale branches --- .github/workflows/cleanup-stale-branches.yml | 2 +- docs/branch-lifecycle-policy.md | 56 ++++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/cleanup-stale-branches.yml b/.github/workflows/cleanup-stale-branches.yml index 8f65ce09014..db3287c879c 100644 --- a/.github/workflows/cleanup-stale-branches.yml +++ b/.github/workflows/cleanup-stale-branches.yml @@ -34,7 +34,7 @@ jobs: DEFAULT_DAYS=180 COPILOT_CUTOFF=$(date -d "$COPILOT_DAYS days ago" +%s) DEFAULT_CUTOFF=$(date -d "$DEFAULT_DAYS days ago" +%s) - PROTECTED='^(main|master|develop|release/|hotfix/|archive/)' + PROTECTED='^(main|master|develop|release/|hotfix/|archive/|[0-9]+\.[0-9]+-stable|preview-)' echo "Stale threshold: copilot/* = $COPILOT_DAYS days, others = $DEFAULT_DAYS days | Dry run: $DRY_RUN" diff --git a/docs/branch-lifecycle-policy.md b/docs/branch-lifecycle-policy.md index efa4e123258..cf5d53b9702 100644 --- a/docs/branch-lifecycle-policy.md +++ b/docs/branch-lifecycle-policy.md @@ -6,47 +6,48 @@ This repository enforces a branch lifecycle policy to keep the branch list clean A scheduled GitHub Action ([cleanup-stale-branches.yml](../.github/workflows/cleanup-stale-branches.yml)) runs **every Monday at 08:00 UTC** and scans all remote branches for inactivity. -| Parameter | Default | Description | -|-----------|---------|-------------| -| Stale threshold | **90 days** | Branches with no commits in this period are considered stale | -| Dry run (scheduled) | **true** | Scheduled runs only report; manual dispatch can delete | +### Staleness Thresholds + +| Branch type | Stale after | Examples | +|-------------|-------------|----------| +| `copilot/*` | **90 days** | `copilot/fix-xyz`, `copilot/workspace` | +| All other branches | **180 days** | `user/feature`, `abhi/experiment` | + +Scheduled runs are always **dry run** (report only). Deletions require a manual trigger. ## Protected Branches -The following branch patterns are **always excluded** from cleanup: +The following branch patterns are **always excluded** from cleanup, regardless of age: - `main` / `master` - `develop` - `release/*` - `hotfix/*` - `archive/*` +- `*-stable` (e.g., `0.72-stable`, `0.80-stable`) +- `preview-*` (e.g., `preview-0.80-test`) + +## Additional Safeguards + +- **Open PRs** — Branches with any open pull request (including drafts) are automatically skipped. ## How to Keep a Branch -If your branch must be retained beyond the staleness threshold (e.g., for archival or long-running work), use **any** of these methods: +If your branch must be retained beyond the staleness threshold, use **any** of these methods: ### 1. Use a protected prefix -Move or rename your branch under `archive/`: +Rename your branch under `archive/`: ``` git branch -m my-old-branch archive/my-old-branch -git push origin archive/my-old-branch :my-old-branch +git push origin archive/my-old-branch +git push origin --delete my-old-branch ``` -### 2. Tag the last commit message -Include one of these markers in any commit message on the branch: -- `[archive]` -- `[keep]` -- `[retain]` -- `[no-cleanup]` - -Example: -``` -git commit --allow-empty -m "[keep] Retaining branch for future reference" -git push -``` +### 2. Keep an open PR +Create or keep a pull request (even a draft) from your branch. The workflow skips any branch with an open PR. -### 3. Notify maintainers -Open an issue or message the maintainers with the branch name and reason for retention. They can add it to the protected patterns in the workflow file. +### 3. Add the pattern to the workflow +Add your branch prefix to the `PROTECTED` regex in the workflow file and submit a PR. ## Manual Cleanup @@ -54,16 +55,15 @@ Maintainers can trigger the workflow manually from the **Actions** tab: 1. Go to **Actions → Cleanup Stale Branches** 2. Click **Run workflow** -3. Choose `dry_run = false` to actually delete stale branches -4. Optionally adjust the staleness threshold (in days) +3. Set `dry_run = false` to actually delete stale branches ## FAQ **Q: What happens if my branch is deleted by mistake?** -A: Git branch deletions on GitHub are reversible for a short period. You can also restore from a local clone if you still have the branch locally. See [GitHub docs on restoring deleted branches](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-branches-in-your-repository/restoring-a-deleted-branch). +A: Git branch deletions on GitHub are reversible for a short period. You can also restore from a local clone. See [GitHub docs on restoring deleted branches](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-branches-in-your-repository/restoring-a-deleted-branch). **Q: Will this delete branches with open pull requests?** -A: The workflow deletes by inactivity date only. If you have an open PR from a stale branch, the branch may still be deleted. Keep the branch active or mark it with `[keep]` to prevent this. +A: No. The workflow checks for open PRs (including drafts) and skips those branches. -**Q: Can I change the staleness threshold?** -A: Yes — edit `DEFAULT_STALE_DAYS` in the workflow file, or override it per-run via the manual dispatch input. +**Q: What about stable and preview branches?** +A: All `*-stable` and `preview-*` branches are protected by default and will never be deleted. From 4f309afb9d9817dda4df934cb67f57db9b47a675 Mon Sep 17 00:00:00 2001 From: Harini Malothu Date: Fri, 20 Mar 2026 16:28:53 +0530 Subject: [PATCH 03/11] Implement button property --- .../Composition/CompositionEventHandler.cpp | 54 ++++++++++++++++++- .../components/view/HostPlatformViewProps.cpp | 26 +++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp index 0ff00758bb7..01eaf359155 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp @@ -1210,6 +1210,30 @@ void CompositionEventHandler::onPointerPressed( ActiveTouch activeTouch{0}; activeTouch.touchType = UITouchType::Mouse; + // Map PointerUpdateKind to W3C button value + // https://developer.mozilla.org/docs/Web/API/MouseEvent/button + auto updateKind = pointerPoint.Properties().PointerUpdateKind(); + switch (updateKind) { + case Composition::Input::PointerUpdateKind::LeftButtonPressed: + activeTouch.button = 0; + break; + case Composition::Input::PointerUpdateKind::MiddleButtonPressed: + activeTouch.button = 1; + break; + case Composition::Input::PointerUpdateKind::RightButtonPressed: + activeTouch.button = 2; + break; + case Composition::Input::PointerUpdateKind::XButton1Pressed: + activeTouch.button = 3; + break; + case Composition::Input::PointerUpdateKind::XButton2Pressed: + activeTouch.button = 4; + break; + default: + activeTouch.button = -1; + break; + } + while (targetComponentView) { if (auto eventEmitter = winrt::get_self(targetComponentView) @@ -1394,8 +1418,34 @@ facebook::react::PointerEvent CompositionEventHandler::CreatePointerEventFromAct event.detail = 0; - // event.button = activeTouch.button; - // event.buttons = ButtonMaskToButtons(activeTouch.buttonMask); + event.button = activeTouch.button; + + // Build W3C buttons bitmask from the active button + // https://developer.mozilla.org/docs/Web/API/MouseEvent/buttons + if (IsEndishEventType(eventType)) { + event.buttons = 0; + } else { + switch (activeTouch.button) { + case 0: + event.buttons = 1; + break; // primary + case 1: + event.buttons = 4; + break; // auxiliary (middle) + case 2: + event.buttons = 2; + break; // secondary (right) + case 3: + event.buttons = 8; + break; // X1 + case 4: + event.buttons = 16; + break; // X2 + default: + event.buttons = 0; + break; + } + } // UpdatePointerEventModifierFlags(event, activeTouch.modifierFlags); diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.cpp b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.cpp index ff5b778be44..7cfb23b4853 100644 --- a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.cpp @@ -85,6 +85,22 @@ HostPlatformViewProps::HostPlatformViewProps( return; \ } +// Upstream BaseViewProps::setProp is missing VIEW_EVENT_CASE entries for +// several W3C pointer events that exist in ViewEvents::Offset. We add them +// here so that JS handlers like onPointerDown/onPointerUp/onClick register +// in the native event bitset and can be detected by IsViewListeningToEvent. +#define UPSTREAM_VIEW_EVENT_CASE(eventType) \ + case CONSTEXPR_RAW_PROPS_KEY_HASH("on" #eventType): { \ + const auto offset = ViewEvents::Offset::eventType; \ + ViewEvents defaultViewEvents{}; \ + bool res = defaultViewEvents[offset]; \ + if (value.hasValue()) { \ + fromRawValue(context, value, res); \ + } \ + events[offset] = res; \ + return; \ + } + void HostPlatformViewProps::setProp( const PropsParserContext &context, RawPropsPropNameHash hash, @@ -98,6 +114,16 @@ void HostPlatformViewProps::setProp( static auto defaults = HostPlatformViewProps{}; switch (hash) { + // Missing upstream W3C pointer event cases + UPSTREAM_VIEW_EVENT_CASE(Click); + UPSTREAM_VIEW_EVENT_CASE(ClickCapture); + UPSTREAM_VIEW_EVENT_CASE(PointerDown); + UPSTREAM_VIEW_EVENT_CASE(PointerDownCapture); + UPSTREAM_VIEW_EVENT_CASE(PointerUp); + UPSTREAM_VIEW_EVENT_CASE(PointerUpCapture); + UPSTREAM_VIEW_EVENT_CASE(GotPointerCapture); + UPSTREAM_VIEW_EVENT_CASE(LostPointerCapture); + // Windows-specific events WINDOWS_VIEW_EVENT_CASE(Focus); WINDOWS_VIEW_EVENT_CASE(Blur); WINDOWS_VIEW_EVENT_CASE(KeyUp); From 21b8722f44f16268e0243d9442de6636fa27fb3b Mon Sep 17 00:00:00 2001 From: Harini Malothu Date: Fri, 20 Mar 2026 16:36:08 +0530 Subject: [PATCH 04/11] Change files --- ...ative-windows-c4c264df-7619-4559-b43f-9761d5d68d63.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-c4c264df-7619-4559-b43f-9761d5d68d63.json diff --git a/change/react-native-windows-c4c264df-7619-4559-b43f-9761d5d68d63.json b/change/react-native-windows-c4c264df-7619-4559-b43f-9761d5d68d63.json new file mode 100644 index 00000000000..a8131090345 --- /dev/null +++ b/change/react-native-windows-c4c264df-7619-4559-b43f-9761d5d68d63.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "Implement button property", + "packageName": "react-native-windows", + "email": "hmalothu@microsoft.com", + "dependentChangeType": "none" +} From 49dc4acbaddf0e8e5fa01caca52dc46681c94687 Mon Sep 17 00:00:00 2001 From: Harini Malothu Date: Mon, 23 Mar 2026 13:45:28 +0530 Subject: [PATCH 05/11] Forked BaseViewProps,Added examples --- packages/playground/Samples/simple.tsx | 57 +- .../components/view/HostPlatformViewProps.cpp | 26 - .../components/view/BaseViewProps.cpp | 664 ++++++++++++++++++ vnext/overrides.json | 7 + 4 files changed, 721 insertions(+), 33 deletions(-) create mode 100644 vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/BaseViewProps.cpp diff --git a/packages/playground/Samples/simple.tsx b/packages/playground/Samples/simple.tsx index 3434f1e43c4..006139777c7 100644 --- a/packages/playground/Samples/simple.tsx +++ b/packages/playground/Samples/simple.tsx @@ -3,16 +3,59 @@ * Licensed under the MIT License. * @format */ -import React from 'react'; -import {AppRegistry, View} from 'react-native'; +import React, {useState} from 'react'; +import {AppRegistry, View, Text} from 'react-native'; const Bootstrap = () => { + const [lastEvent, setLastEvent] = useState('Click the box'); + return ( - - + + { + const {button, buttons} = e.nativeEvent; + const label = + button === 0 + ? 'Left' + : button === 1 + ? 'Middle' + : button === 2 + ? 'Right' + : `Unknown(${button})`; + setLastEvent( + `${label} button pressed (button=${button}, buttons=${buttons})`, + ); + }} + onPointerUp={e => { + const {button, buttons} = e.nativeEvent; + const label = + button === 0 + ? 'Left' + : button === 1 + ? 'Middle' + : button === 2 + ? 'Right' + : `Unknown(${button})`; + setLastEvent( + `${label} button released (button=${button}, buttons=${buttons})`, + ); + }} + // @ts-ignore + onClick={() => setLastEvent('onClick fired')}> + Click Me + + {lastEvent} ); }; diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.cpp b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.cpp index 7cfb23b4853..ff5b778be44 100644 --- a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/components/view/HostPlatformViewProps.cpp @@ -85,22 +85,6 @@ HostPlatformViewProps::HostPlatformViewProps( return; \ } -// Upstream BaseViewProps::setProp is missing VIEW_EVENT_CASE entries for -// several W3C pointer events that exist in ViewEvents::Offset. We add them -// here so that JS handlers like onPointerDown/onPointerUp/onClick register -// in the native event bitset and can be detected by IsViewListeningToEvent. -#define UPSTREAM_VIEW_EVENT_CASE(eventType) \ - case CONSTEXPR_RAW_PROPS_KEY_HASH("on" #eventType): { \ - const auto offset = ViewEvents::Offset::eventType; \ - ViewEvents defaultViewEvents{}; \ - bool res = defaultViewEvents[offset]; \ - if (value.hasValue()) { \ - fromRawValue(context, value, res); \ - } \ - events[offset] = res; \ - return; \ - } - void HostPlatformViewProps::setProp( const PropsParserContext &context, RawPropsPropNameHash hash, @@ -114,16 +98,6 @@ void HostPlatformViewProps::setProp( static auto defaults = HostPlatformViewProps{}; switch (hash) { - // Missing upstream W3C pointer event cases - UPSTREAM_VIEW_EVENT_CASE(Click); - UPSTREAM_VIEW_EVENT_CASE(ClickCapture); - UPSTREAM_VIEW_EVENT_CASE(PointerDown); - UPSTREAM_VIEW_EVENT_CASE(PointerDownCapture); - UPSTREAM_VIEW_EVENT_CASE(PointerUp); - UPSTREAM_VIEW_EVENT_CASE(PointerUpCapture); - UPSTREAM_VIEW_EVENT_CASE(GotPointerCapture); - UPSTREAM_VIEW_EVENT_CASE(LostPointerCapture); - // Windows-specific events WINDOWS_VIEW_EVENT_CASE(Focus); WINDOWS_VIEW_EVENT_CASE(Blur); WINDOWS_VIEW_EVENT_CASE(KeyUp); diff --git a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/BaseViewProps.cpp b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/BaseViewProps.cpp new file mode 100644 index 00000000000..00f9698cb7a --- /dev/null +++ b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/BaseViewProps.cpp @@ -0,0 +1,664 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "BaseViewProps.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +namespace { + +std::array getTranslateForTransformOrigin( + float viewWidth, + float viewHeight, + TransformOrigin transformOrigin) { + float viewCenterX = viewWidth / 2; + float viewCenterY = viewHeight / 2; + + std::array origin = {viewCenterX, viewCenterY, transformOrigin.z}; + + for (size_t i = 0; i < transformOrigin.xy.size(); ++i) { + auto& currentOrigin = transformOrigin.xy[i]; + if (currentOrigin.unit == UnitType::Point) { + origin[i] = currentOrigin.value; + } else if (currentOrigin.unit == UnitType::Percent) { + origin[i] = + ((i == 0) ? viewWidth : viewHeight) * currentOrigin.value / 100.0f; + } + } + + float newTranslateX = -viewCenterX + origin[0]; + float newTranslateY = -viewCenterY + origin[1]; + float newTranslateZ = origin[2]; + + return std::array{newTranslateX, newTranslateY, newTranslateZ}; +} + +} // namespace + +BaseViewProps::BaseViewProps( + const PropsParserContext& context, + const BaseViewProps& sourceProps, + const RawProps& rawProps, + const std::function& filterObjectKeys) + : YogaStylableProps(context, sourceProps, rawProps, filterObjectKeys), + AccessibilityProps(context, sourceProps, rawProps), + opacity( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.opacity + : convertRawProp( + context, + rawProps, + "opacity", + sourceProps.opacity, + (Float)1.0)), + backgroundColor( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.backgroundColor + : convertRawProp( + context, + rawProps, + "backgroundColor", + sourceProps.backgroundColor, + {})), + borderRadii( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.borderRadii + : convertRawProp( + context, + rawProps, + "border", + "Radius", + sourceProps.borderRadii, + {})), + borderColors( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.borderColors + : convertRawProp( + context, + rawProps, + "border", + "Color", + sourceProps.borderColors, + {})), + borderCurves( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.borderCurves + : convertRawProp( + context, + rawProps, + "border", + "Curve", + sourceProps.borderCurves, + {})), + borderStyles( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.borderStyles + : convertRawProp( + context, + rawProps, + "border", + "Style", + sourceProps.borderStyles, + {})), + outlineColor( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.outlineColor + : convertRawProp( + context, + rawProps, + "outlineColor", + sourceProps.outlineColor, + {})), + outlineOffset( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.outlineOffset + : convertRawProp( + context, + rawProps, + "outlineOffset", + sourceProps.outlineOffset, + {})), + outlineStyle( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.outlineStyle + : convertRawProp( + context, + rawProps, + "outlineStyle", + sourceProps.outlineStyle, + {})), + outlineWidth( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.outlineWidth + : convertRawProp( + context, + rawProps, + "outlineWidth", + sourceProps.outlineWidth, + {})), + shadowColor( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.shadowColor + : convertRawProp( + context, + rawProps, + "shadowColor", + sourceProps.shadowColor, + {})), + shadowOffset( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.shadowOffset + : convertRawProp( + context, + rawProps, + "shadowOffset", + sourceProps.shadowOffset, + {})), + shadowOpacity( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.shadowOpacity + : convertRawProp( + context, + rawProps, + "shadowOpacity", + sourceProps.shadowOpacity, + {})), + shadowRadius( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.shadowRadius + : convertRawProp( + context, + rawProps, + "shadowRadius", + sourceProps.shadowRadius, + {})), + cursor( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.cursor + : convertRawProp( + context, + rawProps, + "cursor", + sourceProps.cursor, + {})), + boxShadow( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.boxShadow + : convertRawProp( + context, + rawProps, + "boxShadow", + sourceProps.boxShadow, + {})), + filter( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.filter + : convertRawProp( + context, + rawProps, + "filter", + sourceProps.filter, + {})), + backgroundImage( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.backgroundImage + : convertRawProp( + context, + rawProps, + "experimental_backgroundImage", + sourceProps.backgroundImage, + {})), + backgroundSize( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.backgroundSize + : convertRawProp( + context, + rawProps, + "experimental_backgroundSize", + sourceProps.backgroundSize, + {})), + backgroundPosition( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.backgroundPosition + : convertRawProp( + context, + rawProps, + "experimental_backgroundPosition", + sourceProps.backgroundPosition, + {})), + backgroundRepeat( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.backgroundRepeat + : convertRawProp( + context, + rawProps, + "experimental_backgroundRepeat", + sourceProps.backgroundRepeat, + {})), + mixBlendMode( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.mixBlendMode + : convertRawProp( + context, + rawProps, + "mixBlendMode", + sourceProps.mixBlendMode, + {})), + isolation( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.isolation + : convertRawProp( + context, + rawProps, + "isolation", + sourceProps.isolation, + {})), + transform( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.transform + : convertRawProp( + context, + rawProps, + "transform", + sourceProps.transform, + {})), + transformOrigin( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.transformOrigin + : convertRawProp( + context, + rawProps, + "transformOrigin", + sourceProps.transformOrigin, + {})), + backfaceVisibility( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.backfaceVisibility + : convertRawProp( + context, + rawProps, + "backfaceVisibility", + sourceProps.backfaceVisibility, + {})), + shouldRasterize( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.shouldRasterize + : convertRawProp( + context, + rawProps, + "shouldRasterizeIOS", + sourceProps.shouldRasterize, + {})), + zIndex( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.zIndex + : convertRawProp( + context, + rawProps, + "zIndex", + sourceProps.zIndex, + {})), + pointerEvents( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.pointerEvents + : convertRawProp( + context, + rawProps, + "pointerEvents", + sourceProps.pointerEvents, + {})), + hitSlop( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.hitSlop + : convertRawProp( + context, + rawProps, + "hitSlop", + sourceProps.hitSlop, + {})), + onLayout( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.onLayout + : convertRawProp( + context, + rawProps, + "onLayout", + sourceProps.onLayout, + {})), + events( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.events + : convertRawProp(context, rawProps, sourceProps.events, {})), + collapsable( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.collapsable + : convertRawProp( + context, + rawProps, + "collapsable", + sourceProps.collapsable, + true)), + collapsableChildren( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.collapsableChildren + : convertRawProp( + context, + rawProps, + "collapsableChildren", + sourceProps.collapsableChildren, + true)), + removeClippedSubviews( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.removeClippedSubviews + : convertRawProp( + context, + rawProps, + "removeClippedSubviews", + sourceProps.removeClippedSubviews, + false)) {} + +#define VIEW_EVENT_CASE(eventType) \ + case CONSTEXPR_RAW_PROPS_KEY_HASH("on" #eventType): { \ + const auto offset = ViewEvents::Offset::eventType; \ + ViewEvents defaultViewEvents{}; \ + bool res = defaultViewEvents[offset]; \ + if (value.hasValue()) { \ + fromRawValue(context, value, res); \ + } \ + events[offset] = res; \ + return; \ + } + +void BaseViewProps::setProp( + const PropsParserContext& context, + RawPropsPropNameHash hash, + const char* propName, + const RawValue& value) { + // All Props structs setProp methods must always, unconditionally, + // call all super::setProp methods, since multiple structs may + // reuse the same values. + YogaStylableProps::setProp(context, hash, propName, value); + AccessibilityProps::setProp(context, hash, propName, value); + + static auto defaults = BaseViewProps{}; + + switch (hash) { + RAW_SET_PROP_SWITCH_CASE_BASIC(opacity); + RAW_SET_PROP_SWITCH_CASE_BASIC(backgroundColor); + RAW_SET_PROP_SWITCH_CASE(backgroundImage, "experimental_backgroundImage"); + RAW_SET_PROP_SWITCH_CASE(backgroundSize, "experimental_backgroundSize"); + RAW_SET_PROP_SWITCH_CASE( + backgroundPosition, "experimental_backgroundPosition"); + RAW_SET_PROP_SWITCH_CASE(backgroundRepeat, "experimental_backgroundRepeat"); + RAW_SET_PROP_SWITCH_CASE_BASIC(shadowColor); + RAW_SET_PROP_SWITCH_CASE_BASIC(shadowOffset); + RAW_SET_PROP_SWITCH_CASE_BASIC(shadowOpacity); + RAW_SET_PROP_SWITCH_CASE_BASIC(shadowRadius); + RAW_SET_PROP_SWITCH_CASE_BASIC(transform); + RAW_SET_PROP_SWITCH_CASE_BASIC(backfaceVisibility); + RAW_SET_PROP_SWITCH_CASE_BASIC(shouldRasterize); + RAW_SET_PROP_SWITCH_CASE_BASIC(zIndex); + RAW_SET_PROP_SWITCH_CASE_BASIC(pointerEvents); + RAW_SET_PROP_SWITCH_CASE_BASIC(isolation); + RAW_SET_PROP_SWITCH_CASE_BASIC(hitSlop); + RAW_SET_PROP_SWITCH_CASE_BASIC(onLayout); + RAW_SET_PROP_SWITCH_CASE_BASIC(collapsable); + RAW_SET_PROP_SWITCH_CASE_BASIC(collapsableChildren); + RAW_SET_PROP_SWITCH_CASE_BASIC(removeClippedSubviews); + RAW_SET_PROP_SWITCH_CASE_BASIC(cursor); + RAW_SET_PROP_SWITCH_CASE_BASIC(outlineColor); + RAW_SET_PROP_SWITCH_CASE_BASIC(outlineOffset); + RAW_SET_PROP_SWITCH_CASE_BASIC(outlineStyle); + RAW_SET_PROP_SWITCH_CASE_BASIC(outlineWidth); + RAW_SET_PROP_SWITCH_CASE_BASIC(filter); + RAW_SET_PROP_SWITCH_CASE_BASIC(boxShadow); + RAW_SET_PROP_SWITCH_CASE_BASIC(mixBlendMode); + // events field + VIEW_EVENT_CASE(PointerEnter); + VIEW_EVENT_CASE(PointerEnterCapture); + VIEW_EVENT_CASE(PointerMove); + VIEW_EVENT_CASE(PointerMoveCapture); + VIEW_EVENT_CASE(PointerLeave); + VIEW_EVENT_CASE(PointerLeaveCapture); + VIEW_EVENT_CASE(PointerOver); + VIEW_EVENT_CASE(PointerOverCapture); + VIEW_EVENT_CASE(PointerOut); + VIEW_EVENT_CASE(PointerOutCapture); + // [Windows + VIEW_EVENT_CASE(Click); + VIEW_EVENT_CASE(ClickCapture); + VIEW_EVENT_CASE(PointerDown); + VIEW_EVENT_CASE(PointerDownCapture); + VIEW_EVENT_CASE(PointerUp); + VIEW_EVENT_CASE(PointerUpCapture); + VIEW_EVENT_CASE(GotPointerCapture); + VIEW_EVENT_CASE(LostPointerCapture); + // Windows] + VIEW_EVENT_CASE(MoveShouldSetResponder); + VIEW_EVENT_CASE(MoveShouldSetResponderCapture); + VIEW_EVENT_CASE(StartShouldSetResponder); + VIEW_EVENT_CASE(StartShouldSetResponderCapture); + VIEW_EVENT_CASE(ResponderGrant); + VIEW_EVENT_CASE(ResponderReject); + VIEW_EVENT_CASE(ResponderStart); + VIEW_EVENT_CASE(ResponderEnd); + VIEW_EVENT_CASE(ResponderRelease); + VIEW_EVENT_CASE(ResponderMove); + VIEW_EVENT_CASE(ResponderTerminate); + VIEW_EVENT_CASE(ResponderTerminationRequest); + VIEW_EVENT_CASE(ShouldBlockNativeResponder); + VIEW_EVENT_CASE(TouchStart); + VIEW_EVENT_CASE(TouchMove); + VIEW_EVENT_CASE(TouchEnd); + VIEW_EVENT_CASE(TouchCancel); + // BorderRadii + SET_CASCADED_RECTANGLE_CORNERS(borderRadii, "border", "Radius", value); + SET_CASCADED_RECTANGLE_EDGES(borderColors, "border", "Color", value); + SET_CASCADED_RECTANGLE_EDGES(borderStyles, "border", "Style", value); + } +} + +#pragma mark - Convenience Methods + +static BorderRadii ensureNoOverlap(const BorderRadii& radii, const Size& size) { + // "Corner curves must not overlap: When the sum of any two adjacent border + // radii exceeds the size of the border box, UAs must proportionally reduce + // the used values of all border radii until none of them overlap." + // Source: https://www.w3.org/TR/css-backgrounds-3/#corner-overlap + + float leftEdgeRadii = radii.topLeft.vertical + radii.bottomLeft.vertical; + float topEdgeRadii = radii.topLeft.horizontal + radii.topRight.horizontal; + float rightEdgeRadii = radii.topRight.vertical + radii.bottomRight.vertical; + float bottomEdgeRadii = + radii.bottomLeft.horizontal + radii.bottomRight.horizontal; + + float leftEdgeRadiiScale = + (leftEdgeRadii > 0) ? std::min(size.height / leftEdgeRadii, (Float)1) : 0; + float topEdgeRadiiScale = + (topEdgeRadii > 0) ? std::min(size.width / topEdgeRadii, (Float)1) : 0; + float rightEdgeRadiiScale = (rightEdgeRadii > 0) + ? std::min(size.height / rightEdgeRadii, (Float)1) + : 0; + float bottomEdgeRadiiScale = (bottomEdgeRadii > 0) + ? std::min(size.width / bottomEdgeRadii, (Float)1) + : 0; + + return BorderRadii{ + .topLeft = + {static_cast( + radii.topLeft.vertical * + std::min(topEdgeRadiiScale, leftEdgeRadiiScale)), + static_cast( + radii.topLeft.horizontal * + std::min(topEdgeRadiiScale, leftEdgeRadiiScale))}, + .topRight = + {static_cast( + radii.topRight.vertical * + std::min(topEdgeRadiiScale, rightEdgeRadiiScale)), + static_cast( + radii.topRight.horizontal * + std::min(topEdgeRadiiScale, rightEdgeRadiiScale))}, + .bottomLeft = + {static_cast( + radii.bottomLeft.vertical * + std::min(bottomEdgeRadiiScale, leftEdgeRadiiScale)), + static_cast( + radii.bottomLeft.horizontal * + std::min(bottomEdgeRadiiScale, leftEdgeRadiiScale))}, + .bottomRight = + {static_cast( + radii.bottomRight.vertical * + std::min(bottomEdgeRadiiScale, rightEdgeRadiiScale)), + static_cast( + radii.bottomRight.horizontal * + std::min(bottomEdgeRadiiScale, rightEdgeRadiiScale))}, + }; +} + +static BorderRadii radiiPercentToPoint( + const RectangleCorners& radii, + const Size& size) { + return BorderRadii{ + .topLeft = + {radii.topLeft.resolve(size.height), + radii.topLeft.resolve(size.width)}, + .topRight = + {radii.topRight.resolve(size.height), + radii.topRight.resolve(size.width)}, + .bottomLeft = + {radii.bottomLeft.resolve(size.height), + radii.bottomLeft.resolve(size.width)}, + .bottomRight = + {radii.bottomRight.resolve(size.height), + radii.bottomRight.resolve(size.width)}, + }; +} + +CascadedBorderWidths BaseViewProps::getBorderWidths() const { + return CascadedBorderWidths{ + .left = optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Left)), + .top = optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Top)), + .right = optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Right)), + .bottom = + optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Bottom)), + .start = optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Start)), + .end = optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::End)), + .horizontal = + optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Horizontal)), + .vertical = + optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::Vertical)), + .all = optionalFloatFromYogaValue(yogaStyle.border(yoga::Edge::All)), + }; +} + +BorderMetrics BaseViewProps::resolveBorderMetrics( + const LayoutMetrics& layoutMetrics) const { + auto isRTL = + bool{layoutMetrics.layoutDirection == LayoutDirection::RightToLeft}; + + auto borderWidths = getBorderWidths(); + + BorderRadii radii = radiiPercentToPoint( + borderRadii.resolve(isRTL, ValueUnit{0.0f, UnitType::Point}), + layoutMetrics.frame.size); + + return { + .borderColors = borderColors.resolve(isRTL, {}), + .borderWidths = borderWidths.resolve(isRTL, 0), + .borderRadii = ensureNoOverlap(radii, layoutMetrics.frame.size), + .borderCurves = borderCurves.resolve(isRTL, BorderCurve::Circular), + .borderStyles = borderStyles.resolve(isRTL, BorderStyle::Solid), + }; +} + +Transform BaseViewProps::resolveTransform( + const LayoutMetrics& layoutMetrics) const { + const auto& frameSize = layoutMetrics.frame.size; + return resolveTransform(frameSize, transform, transformOrigin); +} + +Transform BaseViewProps::resolveTransform( + const Size& frameSize, + const Transform& transform, + const TransformOrigin& transformOrigin) { + auto transformMatrix = Transform{}; + + // transform is matrix + if (transform.operations.size() == 1 && + transform.operations[0].type == TransformOperationType::Arbitrary) { + transformMatrix = transform; + } else { + for (const auto& operation : transform.operations) { + transformMatrix = transformMatrix * + Transform::FromTransformOperation(operation, frameSize, transform); + } + } + + if (transformOrigin.isSet()) { + std::array translateOffsets = getTranslateForTransformOrigin( + frameSize.width, frameSize.height, transformOrigin); + transformMatrix = + Transform::Translate( + translateOffsets[0], translateOffsets[1], translateOffsets[2]) * + transformMatrix * + Transform::Translate( + -translateOffsets[0], -translateOffsets[1], -translateOffsets[2]); + } + + return transformMatrix; +} + +bool BaseViewProps::getClipsContentToBounds() const { + return yogaStyle.overflow() != yoga::Overflow::Visible; +} + +#pragma mark - DebugStringConvertible + +#if RN_DEBUG_STRING_CONVERTIBLE +SharedDebugStringConvertibleList BaseViewProps::getDebugProps() const { + const auto& defaultBaseViewProps = BaseViewProps(); + + return AccessibilityProps::getDebugProps() + + YogaStylableProps::getDebugProps() + + SharedDebugStringConvertibleList{ + debugStringConvertibleItem( + "opacity", opacity, defaultBaseViewProps.opacity), + debugStringConvertibleItem( + "backgroundColor", + backgroundColor, + defaultBaseViewProps.backgroundColor), + debugStringConvertibleItem( + "zIndex", zIndex, defaultBaseViewProps.zIndex.value_or(0)), + debugStringConvertibleItem( + "pointerEvents", + pointerEvents, + defaultBaseViewProps.pointerEvents), + debugStringConvertibleItem( + "transform", transform, defaultBaseViewProps.transform), + debugStringConvertibleItem( + "backgroundImage", + backgroundImage, + defaultBaseViewProps.backgroundImage), + }; +} +#endif + +} // namespace facebook::react diff --git a/vnext/overrides.json b/vnext/overrides.json index 3a54c646bd2..9642b5cb3d1 100644 --- a/vnext/overrides.json +++ b/vnext/overrides.json @@ -259,6 +259,13 @@ "baseFile": "packages/react-native/ReactCommon/react/renderer/components/view/accessibilityPropsConversions.h", "baseHash": "72cc422293c0e70f454225476cdabd3458bbce5e" }, + { + "type": "patch", + "file": "ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/BaseViewProps.cpp", + "baseFile": "packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp", + "baseHash": "24baef3c27668197629949d63f1170f2a43e2c15", + "issue": 15827 + }, { "type": "patch", "file": "ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/core/EventDispatcher.cpp", From f51e3bbe606db0468ae4005d81c61c36aa4b2d71 Mon Sep 17 00:00:00 2001 From: Harini Malothu Date: Mon, 23 Mar 2026 14:40:27 +0530 Subject: [PATCH 06/11] Updated example changes --- packages/playground/Samples/click.tsx | 85 ++++++++++++++++++++------ packages/playground/Samples/simple.tsx | 57 +++-------------- 2 files changed, 73 insertions(+), 69 deletions(-) diff --git a/packages/playground/Samples/click.tsx b/packages/playground/Samples/click.tsx index bc1ad0feeff..b46232c8733 100644 --- a/packages/playground/Samples/click.tsx +++ b/packages/playground/Samples/click.tsx @@ -10,6 +10,7 @@ import {AppRegistry, Text, TouchableHighlight, View} from 'react-native'; export default class Bootstrap extends React.Component { state = { ticker: 0, + lastEvent: 'Click the box to test pointer events', }; onSmallIncrement = () => { @@ -27,32 +28,78 @@ export default class Bootstrap extends React.Component { console.log(' onLargeIncrement !'); }; + buttonLabel = (button: number) => { + switch (button) { + case 0: + return 'Left'; + case 1: + return 'Middle'; + case 2: + return 'Right'; + default: + return `Unknown(${button})`; + } + }; + render() { return ( - - + {/* Pointer event test */} + { + const {button, buttons} = e.nativeEvent; + this.setState({ + lastEvent: `${this.buttonLabel(button)} pressed (button=${button}, buttons=${buttons})`, + }); + }} + onPointerUp={(e: any) => { + const {button, buttons} = e.nativeEvent; + this.setState({ + lastEvent: `${this.buttonLabel(button)} released (button=${button}, buttons=${buttons})`, + }); + }} + {...{onClick: this.onLargeIncrement}}> + Click Me + + + {this.state.lastEvent} + + + {/* Original click test */} + - - - {this.state.ticker.toString()} - - + style={{backgroundColor: 'orange', margin: 15}} + onPress={this.onMediumIncrement} + {...{ + // Use weird format as work around for the fact that these props are not part of the @types/react-native yet + focusable: true, + }}> + + + + {this.state.ticker.toString()} + + + - + ); } diff --git a/packages/playground/Samples/simple.tsx b/packages/playground/Samples/simple.tsx index 006139777c7..3434f1e43c4 100644 --- a/packages/playground/Samples/simple.tsx +++ b/packages/playground/Samples/simple.tsx @@ -3,59 +3,16 @@ * Licensed under the MIT License. * @format */ -import React, {useState} from 'react'; -import {AppRegistry, View, Text} from 'react-native'; +import React from 'react'; +import {AppRegistry, View} from 'react-native'; const Bootstrap = () => { - const [lastEvent, setLastEvent] = useState('Click the box'); - return ( - - { - const {button, buttons} = e.nativeEvent; - const label = - button === 0 - ? 'Left' - : button === 1 - ? 'Middle' - : button === 2 - ? 'Right' - : `Unknown(${button})`; - setLastEvent( - `${label} button pressed (button=${button}, buttons=${buttons})`, - ); - }} - onPointerUp={e => { - const {button, buttons} = e.nativeEvent; - const label = - button === 0 - ? 'Left' - : button === 1 - ? 'Middle' - : button === 2 - ? 'Right' - : `Unknown(${button})`; - setLastEvent( - `${label} button released (button=${button}, buttons=${buttons})`, - ); - }} - // @ts-ignore - onClick={() => setLastEvent('onClick fired')}> - Click Me - - {lastEvent} + + ); }; From 267060ab5946d158a4b808ba8ac092ca98a1ecd4 Mon Sep 17 00:00:00 2001 From: Harini Malothu Date: Mon, 23 Mar 2026 14:52:24 +0530 Subject: [PATCH 07/11] fix lint --- packages/playground/Samples/click.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/playground/Samples/click.tsx b/packages/playground/Samples/click.tsx index b46232c8733..f9c5cddaecd 100644 --- a/packages/playground/Samples/click.tsx +++ b/packages/playground/Samples/click.tsx @@ -58,13 +58,17 @@ export default class Bootstrap extends React.Component { onPointerDown={(e: any) => { const {button, buttons} = e.nativeEvent; this.setState({ - lastEvent: `${this.buttonLabel(button)} pressed (button=${button}, buttons=${buttons})`, + lastEvent: `${this.buttonLabel( + button, + )} pressed (button=${button}, buttons=${buttons})`, }); }} onPointerUp={(e: any) => { const {button, buttons} = e.nativeEvent; this.setState({ - lastEvent: `${this.buttonLabel(button)} released (button=${button}, buttons=${buttons})`, + lastEvent: `${this.buttonLabel( + button, + )} released (button=${button}, buttons=${buttons})`, }); }} {...{onClick: this.onLargeIncrement}}> From 88f3ae2701f79f2814a4687815c51e882de84030 Mon Sep 17 00:00:00 2001 From: Harini Malothu Date: Tue, 24 Mar 2026 15:12:51 +0530 Subject: [PATCH 08/11] added e2e test cases --- .../Pointer/PointerButtonExample.windows.js | 115 ++++++++++++++++++ .../src/js/utils/RNTesterList.windows.js | 5 + .../test/PointerButtonComponentTest.test.ts | 104 ++++++++++++++++ 3 files changed, 224 insertions(+) create mode 100644 packages/@react-native-windows/tester/src/js/examples-win/Pointer/PointerButtonExample.windows.js create mode 100644 packages/e2e-test-app-fabric/test/PointerButtonComponentTest.test.ts diff --git a/packages/@react-native-windows/tester/src/js/examples-win/Pointer/PointerButtonExample.windows.js b/packages/@react-native-windows/tester/src/js/examples-win/Pointer/PointerButtonExample.windows.js new file mode 100644 index 00000000000..2871caac930 --- /dev/null +++ b/packages/@react-native-windows/tester/src/js/examples-win/Pointer/PointerButtonExample.windows.js @@ -0,0 +1,115 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; + +const React = require('react'); +const {StyleSheet, Text, View} = require('react-native'); + +function buttonLabel(button: number): string { + switch (button) { + case 0: + return 'Left'; + case 1: + return 'Middle'; + case 2: + return 'Right'; + case 3: + return 'X1'; + case 4: + return 'X2'; + default: + return `Unknown(${button})`; + } +} + +function PointerDownButtonExample(): React.Node { + const [text, setText] = React.useState( + 'Click the box to test pointer events', + ); + return ( + + {text} + { + const {button, buttons} = e.nativeEvent; + setText( + `PointerDown: ${buttonLabel(button)} (button=${button}, buttons=${buttons})`, + ); + }} + /> + + ); +} + +function PointerUpButtonExample(): React.Node { + const [text, setText] = React.useState( + 'Click the box to test pointer up events', + ); + return ( + + {text} + { + const {button, buttons} = e.nativeEvent; + setText( + `PointerUp: ${buttonLabel(button)} (button=${button}, buttons=${buttons})`, + ); + }} + /> + + ); +} + +exports.displayName = 'PointerButtonExample'; +exports.framework = 'React'; +exports.category = 'Basic'; +exports.title = 'Pointer Button'; +exports.documentationURL = + 'https://developer.mozilla.org/docs/Web/API/PointerEvent/button'; +exports.description = + 'Tests that PointerEvent.button and PointerEvent.buttons are correctly populated.'; + +exports.examples = [ + { + title: 'onPointerDown button property', + description: + 'Click the box to verify the button property on onPointerDown events.', + render: function (): React.Node { + return ; + }, + }, + { + title: 'onPointerUp button property', + description: + 'Click the box to verify the button property on onPointerUp events.', + render: function (): React.Node { + return ; + }, + }, +] as Array; + +const styles = StyleSheet.create({ + targetBox: { + backgroundColor: 'magenta', + width: 200, + height: 200, + margin: 10, + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + }, +}); diff --git a/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js b/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js index c9c15047b6c..82649db6480 100644 --- a/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js +++ b/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js @@ -384,6 +384,11 @@ const APIs: Array = ([ category: 'Basic', module: require('../examples/PointerEvents/PointerEventsExample'), }, + { + key: 'PointerButtonExample', + category: 'Basic', + module: require('../examples-win/Pointer/PointerButtonExample'), + }, { key: 'RTLExample', category: 'Basic', diff --git a/packages/e2e-test-app-fabric/test/PointerButtonComponentTest.test.ts b/packages/e2e-test-app-fabric/test/PointerButtonComponentTest.test.ts new file mode 100644 index 00000000000..198ea935d5b --- /dev/null +++ b/packages/e2e-test-app-fabric/test/PointerButtonComponentTest.test.ts @@ -0,0 +1,104 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @format + */ + +import {dumpVisualTree} from '@react-native-windows/automation-commands'; +import {goToApiExample} from './RNTesterNavigation'; +import {verifyNoErrorLogs} from './Helpers'; +import {app} from '@react-native-windows/automation'; + +beforeAll(async () => { + // If window is partially offscreen, tests will fail to click on certain elements + await app.setWindowPosition(0, 0); + await app.setWindowSize(1000, 1250); + await goToApiExample('Pointer Button'); +}); + +afterEach(async () => { + await verifyNoErrorLogs(); +}); + +const searchBox = async (input: string) => { + const searchBox = await app.findElementByTestID('example_search'); + await app.waitUntil( + async () => { + await searchBox.setValue(input); + if (input === '') { + return (await searchBox.getText()) === 'Search...'; + } else { + return (await searchBox.getText()) === input; + } + }, + { + interval: 1500, + timeout: 5000, + timeoutMsg: `Unable to enter correct search text into test searchbox.`, + }, + ); +}; + +describe('Pointer Button Tests', () => { + test('onPointerDown reports correct button property on left click', async () => { + await searchBox('onPointerDown'); + const component = await app.findElementByTestID('pointer-button-target'); + await component.waitForDisplayed({timeout: 5000}); + const dump = await dumpVisualTree('pointer-button-target'); + expect(dump).toMatchSnapshot(); + + // Left click triggers onPointerDown with button=0 + await component.click(); + const stateText = await app.findElementByTestID('pointer-button-state'); + + await app.waitUntil( + async () => { + const currentText = await stateText.getText(); + return currentText.includes('button=0'); + }, + { + timeout: 5000, + timeoutMsg: + 'State text not updated after onPointerDown with button property.', + }, + ); + + const text = await stateText.getText(); + expect(text).toContain('PointerDown'); + expect(text).toContain('button=0'); + expect(text).toContain('buttons=1'); + }); + test('onPointerUp reports correct button property on left click', async () => { + await searchBox('onPointerUp'); + const component = await app.findElementByTestID( + 'pointer-up-button-target', + ); + await component.waitForDisplayed({timeout: 5000}); + const dump = await dumpVisualTree('pointer-up-button-target'); + expect(dump).toMatchSnapshot(); + + // Left click release triggers onPointerUp with button=0 + await component.click(); + const stateText = await app.findElementByTestID( + 'pointer-up-button-state', + ); + + await app.waitUntil( + async () => { + const currentText = await stateText.getText(); + return currentText.includes('button=0'); + }, + { + timeout: 5000, + timeoutMsg: + 'State text not updated after onPointerUp with button property.', + }, + ); + + const text = await stateText.getText(); + expect(text).toContain('PointerUp'); + expect(text).toContain('button=0'); + expect(text).toContain('buttons=0'); + }); +}); From 9fba849619f0dc923dd161adf20ae4c3a3c6bb22 Mon Sep 17 00:00:00 2001 From: Harini Malothu Date: Tue, 24 Mar 2026 17:39:08 +0530 Subject: [PATCH 09/11] update snapshots --- .../__snapshots__/HomeUIADump.test.ts.snap | 186 +++++++++--------- .../PointerButtonComponentTest.test.ts.snap | 53 +++++ .../PressableComponentTest.test.ts.snap | 4 +- .../TextComponentTest.test.ts.snap | 54 ++--- .../TouchableComponentTest.test.ts.snap | 8 +- .../__snapshots__/snapshotPages.test.js.snap | 153 +++++++++++++- 6 files changed, 327 insertions(+), 131 deletions(-) create mode 100644 packages/e2e-test-app-fabric/test/__snapshots__/PointerButtonComponentTest.test.ts.snap diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap index 83cd2b99f8a..16457c7c5cf 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap @@ -1469,87 +1469,6 @@ exports[`Home UIA Tree Dump Custom Native Accessibility Example 1`] = ` } `; -exports[`Home UIA Tree Dump Cxx TurboModule 1`] = ` -{ - "Automation Tree": { - "AutomationId": "Cxx TurboModule", - "ControlType": 50026, - "IsKeyboardFocusable": true, - "LocalizedControlType": "group", - "Name": "Cxx TurboModule Usage of Cxx TurboModule", - "__Children": [ - { - "AutomationId": "", - "ControlType": 50020, - "LocalizedControlType": "text", - "Name": "Cxx TurboModule", - "TextRangePattern.GetText": "Cxx TurboModule", - }, - { - "AutomationId": "", - "ControlType": 50020, - "LocalizedControlType": "text", - "Name": "Usage of Cxx TurboModule", - "TextRangePattern.GetText": "Usage of Cxx TurboModule", - }, - ], - }, - "Component Tree": { - "Type": "Microsoft.ReactNative.Composition.ViewComponentView", - "_Props": { - "AccessibilityLabel": "Cxx TurboModule Usage of Cxx TurboModule", - "TestId": "Cxx TurboModule", - }, - "__Children": [ - { - "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", - "_Props": {}, - }, - { - "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", - "_Props": {}, - }, - ], - }, - "Visual Tree": { - "Brush": { - "Brush Type": "ColorBrush", - "Color": "rgba(255, 255, 255, 255)", - }, - "Comment": "Cxx TurboModule", - "Offset": "0, 0, 0", - "Size": "966, 78", - "Visual Type": "SpriteVisual", - "__Children": [ - { - "Offset": "16, 16, 0", - "Size": "141, 25", - "Visual Type": "SpriteVisual", - "__Children": [ - { - "Offset": "0, 0, 0", - "Size": "141, 25", - "Visual Type": "SpriteVisual", - }, - ], - }, - { - "Offset": "16, 45, 0", - "Size": "934, 17", - "Visual Type": "SpriteVisual", - "__Children": [ - { - "Offset": "0, 0, 0", - "Size": "934, 17", - "Visual Type": "SpriteVisual", - }, - ], - }, - ], - }, -} -`; - exports[`Home UIA Tree Dump DevSettings 1`] = ` { "Automation Tree": { @@ -2264,12 +2183,12 @@ exports[`Home UIA Tree Dump Filter 1`] = ` }, { "Offset": "16, 45, 0", - "Size": "934, 16", + "Size": "934, 17", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "934, 16", + "Size": "934, 17", "Visual Type": "SpriteVisual", }, ], @@ -2669,12 +2588,12 @@ exports[`Home UIA Tree Dump Keyboard 1`] = ` }, { "Offset": "16, 45, 0", - "Size": "934, 17", + "Size": "934, 16", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "934, 17", + "Size": "934, 16", "Visual Type": "SpriteVisual", }, ], @@ -2750,12 +2669,12 @@ exports[`Home UIA Tree Dump Keyboard 2`] = ` }, { "Offset": "16, 45, 0", - "Size": "934, 17", + "Size": "934, 16", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "934, 17", + "Size": "934, 16", "Visual Type": "SpriteVisual", }, ], @@ -3057,7 +2976,7 @@ exports[`Home UIA Tree Dump Layout Events 1`] = ` }, "Comment": "Layout Events", "Offset": "0, 0, 0", - "Size": "966, 77", + "Size": "966, 78", "Visual Type": "SpriteVisual", "__Children": [ { @@ -3138,7 +3057,7 @@ exports[`Home UIA Tree Dump Legacy Native Module 1`] = ` }, "Comment": "Legacy Native Module", "Offset": "0, 0, 0", - "Size": "966, 78", + "Size": "966, 77", "Visual Type": "SpriteVisual", "__Children": [ { @@ -4127,12 +4046,12 @@ exports[`Home UIA Tree Dump Native Animated Example 1`] = ` }, { "Offset": "16, 45, 0", - "Size": "934, 16", + "Size": "934, 17", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "934, 16", + "Size": "934, 17", "Visual Type": "SpriteVisual", }, ], @@ -4289,12 +4208,12 @@ exports[`Home UIA Tree Dump PanResponder Sample 1`] = ` }, { "Offset": "16, 45, 0", - "Size": "934, 17", + "Size": "934, 16", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "934, 17", + "Size": "934, 16", "Visual Type": "SpriteVisual", }, ], @@ -4547,6 +4466,87 @@ exports[`Home UIA Tree Dump PlatformColor 1`] = ` } `; +exports[`Home UIA Tree Dump Pointer Button 1`] = ` +{ + "Automation Tree": { + "AutomationId": "Pointer Button", + "ControlType": 50026, + "IsKeyboardFocusable": true, + "LocalizedControlType": "group", + "Name": "Pointer Button Tests that PointerEvent.button and PointerEvent.buttons are correctly populated.", + "__Children": [ + { + "AutomationId": "", + "ControlType": 50020, + "LocalizedControlType": "text", + "Name": "Pointer Button", + "TextRangePattern.GetText": "Pointer Button", + }, + { + "AutomationId": "", + "ControlType": 50020, + "LocalizedControlType": "text", + "Name": "Tests that PointerEvent.button and PointerEvent.buttons are correctly populated.", + "TextRangePattern.GetText": "Tests that PointerEvent.button and PointerEvent.buttons are correctly populated.", + }, + ], + }, + "Component Tree": { + "Type": "Microsoft.ReactNative.Composition.ViewComponentView", + "_Props": { + "AccessibilityLabel": "Pointer Button Tests that PointerEvent.button and PointerEvent.buttons are correctly populated.", + "TestId": "Pointer Button", + }, + "__Children": [ + { + "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", + "_Props": {}, + }, + { + "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", + "_Props": {}, + }, + ], + }, + "Visual Tree": { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(255, 255, 255, 255)", + }, + "Comment": "Pointer Button", + "Offset": "0, 0, 0", + "Size": "966, 78", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "16, 16, 0", + "Size": "115, 25", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "115, 25", + "Visual Type": "SpriteVisual", + }, + ], + }, + { + "Offset": "16, 45, 0", + "Size": "934, 17", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "934, 17", + "Visual Type": "SpriteVisual", + }, + ], + }, + ], + }, +} +`; + exports[`Home UIA Tree Dump Pointer Events 1`] = ` { "Automation Tree": { diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/PointerButtonComponentTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/PointerButtonComponentTest.test.ts.snap new file mode 100644 index 00000000000..9ea6e8adb43 --- /dev/null +++ b/packages/e2e-test-app-fabric/test/__snapshots__/PointerButtonComponentTest.test.ts.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Pointer Button Tests onPointerDown reports correct button property on left click 1`] = ` +{ + "Automation Tree": { + "AutomationId": "pointer-button-target", + "ControlType": 50026, + "LocalizedControlType": "group", + }, + "Component Tree": { + "Type": "Microsoft.ReactNative.Composition.ViewComponentView", + "_Props": { + "TestId": "pointer-button-target", + }, + }, + "Visual Tree": { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(255, 0, 255, 255)", + }, + "Comment": "pointer-button-target", + "Offset": "0, 0, 0", + "Size": "200, 200", + "Visual Type": "SpriteVisual", + }, +} +`; + +exports[`Pointer Button Tests onPointerUp reports correct button property on left click 1`] = ` +{ + "Automation Tree": { + "AutomationId": "pointer-up-button-target", + "ControlType": 50026, + "LocalizedControlType": "group", + }, + "Component Tree": { + "Type": "Microsoft.ReactNative.Composition.ViewComponentView", + "_Props": { + "TestId": "pointer-up-button-target", + }, + }, + "Visual Tree": { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(255, 0, 255, 255)", + }, + "Comment": "pointer-up-button-target", + "Offset": "0, 0, 0", + "Size": "200, 200", + "Visual Type": "SpriteVisual", + }, +} +`; diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap index 5e8c7116993..4d9b837e37b 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap @@ -1961,8 +1961,8 @@ exports[`Pressable Tests Text can have pressable behavior 1`] = ` { "Automation Tree": { "AutomationId": "tappable_text", - "ControlType": 50020, - "LocalizedControlType": "text", + "ControlType": 50005, + "LocalizedControlType": "link", "Name": "Text has built-in onPress handling", "TextRangePattern.GetText": "Text has built-in onPress handling", }, diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/TextComponentTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/TextComponentTest.test.ts.snap index be1ba4318a3..9a870819a63 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/TextComponentTest.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/TextComponentTest.test.ts.snap @@ -234,7 +234,7 @@ exports[`Text Tests Text can be restricted to one line 1`] = ` "Visual Tree": { "Comment": "text-one-line", "Offset": "0, 0, 0", - "Size": "300, 19", + "Size": "300, 20", "Visual Type": "SpriteVisual", }, } @@ -258,7 +258,7 @@ exports[`Text Tests Text can be selectable 1`] = ` "Visual Tree": { "Comment": "text-selectable", "Offset": "0, 0, 0", - "Size": "916, 19", + "Size": "916, 20", "Visual Type": "SpriteVisual", }, } @@ -396,12 +396,12 @@ exports[`Text Tests Text can have advanced borders 1`] = ` "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 39", + "Size": "916, 40", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 39", + "Size": "916, 40", "Visual Type": "SpriteVisual", "__Children": [ { @@ -437,7 +437,7 @@ exports[`Text Tests Text can have advanced borders 1`] = ` "Color": "rgba(255, 0, 0, 255)", }, "Offset": "-10, 20, 0", - "Size": "10, 13", + "Size": "10, 14", "Visual Type": "SpriteVisual", }, { @@ -473,7 +473,7 @@ exports[`Text Tests Text can have advanced borders 1`] = ` "Color": "rgba(255, 0, 0, 255)", }, "Offset": "0, 22, 0", - "Size": "20, 9", + "Size": "20, 10", "Visual Type": "SpriteVisual", }, ], @@ -482,12 +482,12 @@ exports[`Text Tests Text can have advanced borders 1`] = ` }, { "Offset": "0, 38, 0", - "Size": "916, 40", + "Size": "916, 39", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 40", + "Size": "916, 39", "Visual Type": "SpriteVisual", "__Children": [ { @@ -523,7 +523,7 @@ exports[`Text Tests Text can have advanced borders 1`] = ` "Color": "rgba(0, 0, 255, 255)", }, "Offset": "-10, 20, 0", - "Size": "10, 14", + "Size": "10, 13", "Visual Type": "SpriteVisual", }, { @@ -559,7 +559,7 @@ exports[`Text Tests Text can have advanced borders 1`] = ` "Color": "rgba(0, 0, 255, 255)", }, "Offset": "0, 22, 0", - "Size": "20, 10", + "Size": "20, 9", "Visual Type": "SpriteVisual", }, ], @@ -661,8 +661,8 @@ exports[`Text Tests Text can have an outer color 1`] = ` { "Automation Tree": { "AutomationId": "text-outer-color", - "ControlType": 50020, - "LocalizedControlType": "text", + "ControlType": 50005, + "LocalizedControlType": "link", "Name": "(Normal text,(R)red(G)green(B)blue(C)cyan(M)magenta(Y)yellow(K)black(and bold(and tiny bold italic blue(and tiny normal blue))))", "TextRangePattern.GetText": "(Normal text,(R)red(G)green(B)blue(C)cyan(M)magenta(Y)yellow(K)black(and bold(and tiny bold italic blue(and tiny normal blue))))", }, @@ -675,7 +675,7 @@ exports[`Text Tests Text can have an outer color 1`] = ` "Visual Tree": { "Comment": "text-outer-color", "Offset": "0, 0, 0", - "Size": "916, 20", + "Size": "916, 19", "Visual Type": "SpriteVisual", }, } @@ -740,7 +740,7 @@ exports[`Text Tests Text can have borders 1`] = ` "Visual Tree": { "Comment": "text-border", "Offset": "0, 0, 0", - "Size": "916, 384", + "Size": "916, 385", "Visual Type": "SpriteVisual", "__Children": [ { @@ -853,12 +853,12 @@ exports[`Text Tests Text can have borders 1`] = ` }, { "Offset": "0, 365, 0", - "Size": "916, 20", + "Size": "916, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 20", + "Size": "916, 19", "Visual Type": "SpriteVisual", }, ], @@ -886,7 +886,7 @@ exports[`Text Tests Text can have decoration lines: Solid Line Through 1`] = ` "Visual Tree": { "Comment": "text-decoration-solid-linethru", "Offset": "0, 0, 0", - "Size": "916, 20", + "Size": "916, 19", "Visual Type": "SpriteVisual", }, } @@ -910,7 +910,7 @@ exports[`Text Tests Text can have decoration lines: Underline 1`] = ` "Visual Tree": { "Comment": "text-decoration-underline", "Offset": "0, 0, 0", - "Size": "916, 20", + "Size": "916, 19", "Visual Type": "SpriteVisual", }, } @@ -953,7 +953,7 @@ exports[`Text Tests Text can have inline views/images 1`] = ` "Visual Tree": { "Comment": "text-view", "Offset": "0, 0, 0", - "Size": "916, 27", + "Size": "916, 26", "Visual Type": "SpriteVisual", "__Children": [ { @@ -1044,17 +1044,17 @@ exports[`Text Tests Text can have nested views 1`] = ` "Visual Tree": { "Comment": "text-nested-view", "Offset": "0, 0, 0", - "Size": "916, 41", + "Size": "916, 40", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 19", + "Size": "916, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 19", + "Size": "916, 20", "Visual Type": "SpriteVisual", }, ], @@ -1230,17 +1230,17 @@ exports[`Text Tests Texts can clip inline View/Images 1`] = ` "Visual Tree": { "Comment": "text-view-images-clipped", "Offset": "0, 0, 0", - "Size": "916, 222", + "Size": "916, 223", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 20", + "Size": "916, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 20", + "Size": "916, 19", "Visual Type": "SpriteVisual", }, ], @@ -1300,12 +1300,12 @@ exports[`Text Tests Texts can clip inline View/Images 1`] = ` }, { "Offset": "0, 103, 0", - "Size": "916, 19", + "Size": "916, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 19", + "Size": "916, 20", "Visual Type": "SpriteVisual", }, ], diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/TouchableComponentTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/TouchableComponentTest.test.ts.snap index e5670892632..dacfdc72efa 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/TouchableComponentTest.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/TouchableComponentTest.test.ts.snap @@ -4,8 +4,8 @@ exports[`Touchable Tests Text components can be tappable 1`] = ` { "Automation Tree": { "AutomationId": "tappable_text", - "ControlType": 50020, - "LocalizedControlType": "text", + "ControlType": 50005, + "LocalizedControlType": "link", "Name": "Text has built-in onPress handling", "TextRangePattern.GetText": "Text has built-in onPress handling", }, @@ -120,10 +120,10 @@ exports[`Touchable Tests Touchables can be defined in a set using accessibilityP }, { "AutomationId": "", - "ControlType": 50020, + "ControlType": 50005, "IsKeyboardFocusable": true, "LiveSetting": "Assertive", - "LocalizedControlType": "text", + "LocalizedControlType": "link", "Name": "TouchableWithoutFeedback (Control 3 in Set of 3)", "TextRangePattern.GetText": "TouchableWithoutFeedback (Control 3 in Set of 3)", }, diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap b/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap index d9fdfc834f3..731a03eea1e 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap @@ -36574,6 +36574,60 @@ exports[`snapshotAllPages PlatformColor 5`] = ` `; +exports[`snapshotAllPages Pointer Button 1`] = ` + + + Click the box to test pointer events + + + +`; + +exports[`snapshotAllPages Pointer Button 2`] = ` + + + Click the box to test pointer up events + + + +`; + exports[`snapshotAllPages Pointer Events 1`] = ` Move fast and be normal , + + Move fast and be italic, but just be longer so that you don't fit on a single line and make sure text is not truncated. + , ] `; @@ -75303,6 +75371,81 @@ exports[`snapshotAllPages Text 46`] = ` `; exports[`snapshotAllPages Text 47`] = ` + + + Link Text + + + + Nested Link + + + + Before + + + Nested Link + + After + + + + Nested Link 1 + + - + + Nested Link 2 + + + +`; + +exports[`snapshotAllPages Text 48`] = ` `; -exports[`snapshotAllPages Text 48`] = ` +exports[`snapshotAllPages Text 49`] = ` `; -exports[`snapshotAllPages Text 49`] = ` +exports[`snapshotAllPages Text 50`] = ` `; -exports[`snapshotAllPages Text 50`] = ` +exports[`snapshotAllPages Text 51`] = ` @@ -75560,7 +75703,7 @@ exports[`snapshotAllPages Text 50`] = ` `; -exports[`snapshotAllPages Text 51`] = ` +exports[`snapshotAllPages Text 52`] = ` `; -exports[`snapshotAllPages Text 52`] = ` +exports[`snapshotAllPages Text 53`] = ` Date: Tue, 24 Mar 2026 20:44:04 +0530 Subject: [PATCH 10/11] update snapshot --- .../test/__snapshots__/HomeUIADump.test.ts.snap | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap index 10136a267ab..16457c7c5cf 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/HomeUIADump.test.ts.snap @@ -4839,7 +4839,7 @@ exports[`Home UIA Tree Dump RTLExample 1`] = ` }, "Comment": "RTLExample", "Offset": "0, 0, 0", - "Size": "966, 78", + "Size": "966, 77", "Visual Type": "SpriteVisual", "__Children": [ { @@ -5354,7 +5354,7 @@ exports[`Home UIA Tree Dump Share 1`] = ` }, "Comment": "Share", "Offset": "0, 0, 0", - "Size": "966, 77", + "Size": "966, 78", "Visual Type": "SpriteVisual", "__Children": [ { @@ -6331,12 +6331,12 @@ exports[`Home UIA Tree Dump URL 1`] = ` "__Children": [ { "Offset": "16, 16, 0", - "Size": "32, 25", + "Size": "32, 24", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "32, 25", + "Size": "32, 24", "Visual Type": "SpriteVisual", }, ], @@ -6493,12 +6493,12 @@ exports[`Home UIA Tree Dump WebSocket 1`] = ` "__Children": [ { "Offset": "16, 16, 0", - "Size": "89, 24", + "Size": "89, 25", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "89, 24", + "Size": "89, 25", "Visual Type": "SpriteVisual", }, ], From aeb3afde71dc2b5d426416297601b61afbece46a Mon Sep 17 00:00:00 2001 From: Harini Malothu Date: Tue, 24 Mar 2026 23:16:09 +0530 Subject: [PATCH 11/11] updated test cases --- .../test/PointerButtonComponentTest.test.ts | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/packages/e2e-test-app-fabric/test/PointerButtonComponentTest.test.ts b/packages/e2e-test-app-fabric/test/PointerButtonComponentTest.test.ts index 198ea935d5b..3160a935a8e 100644 --- a/packages/e2e-test-app-fabric/test/PointerButtonComponentTest.test.ts +++ b/packages/e2e-test-app-fabric/test/PointerButtonComponentTest.test.ts @@ -69,6 +69,58 @@ describe('Pointer Button Tests', () => { expect(text).toContain('button=0'); expect(text).toContain('buttons=1'); }); + test('onPointerDown reports correct button property on middle click', async () => { + await searchBox('onPointerDown'); + const component = await app.findElementByTestID('pointer-button-target'); + await component.waitForDisplayed({timeout: 5000}); + + // Middle click triggers onPointerDown with button=1 + await component.click({button: 'middle'}); + const stateText = await app.findElementByTestID('pointer-button-state'); + + await app.waitUntil( + async () => { + const currentText = await stateText.getText(); + return currentText.includes('button=1'); + }, + { + timeout: 5000, + timeoutMsg: + 'State text not updated after onPointerDown with middle button property.', + }, + ); + + const text = await stateText.getText(); + expect(text).toContain('PointerDown'); + expect(text).toContain('button=1'); + expect(text).toContain('buttons=4'); + }); + test('onPointerDown reports correct button property on right click', async () => { + await searchBox('onPointerDown'); + const component = await app.findElementByTestID('pointer-button-target'); + await component.waitForDisplayed({timeout: 5000}); + + // Right click triggers onPointerDown with button=2 + await component.click({button: 'right'}); + const stateText = await app.findElementByTestID('pointer-button-state'); + + await app.waitUntil( + async () => { + const currentText = await stateText.getText(); + return currentText.includes('button=2'); + }, + { + timeout: 5000, + timeoutMsg: + 'State text not updated after onPointerDown with right button property.', + }, + ); + + const text = await stateText.getText(); + expect(text).toContain('PointerDown'); + expect(text).toContain('button=2'); + expect(text).toContain('buttons=2'); + }); test('onPointerUp reports correct button property on left click', async () => { await searchBox('onPointerUp'); const component = await app.findElementByTestID( @@ -101,4 +153,64 @@ describe('Pointer Button Tests', () => { expect(text).toContain('button=0'); expect(text).toContain('buttons=0'); }); + test('onPointerUp reports correct button property on middle click', async () => { + await searchBox('onPointerUp'); + const component = await app.findElementByTestID( + 'pointer-up-button-target', + ); + await component.waitForDisplayed({timeout: 5000}); + + // Middle click release triggers onPointerUp with button=1 + await component.click({button: 'middle'}); + const stateText = await app.findElementByTestID( + 'pointer-up-button-state', + ); + + await app.waitUntil( + async () => { + const currentText = await stateText.getText(); + return currentText.includes('button=1'); + }, + { + timeout: 5000, + timeoutMsg: + 'State text not updated after onPointerUp with middle button property.', + }, + ); + + const text = await stateText.getText(); + expect(text).toContain('PointerUp'); + expect(text).toContain('button=1'); + expect(text).toContain('buttons=0'); + }); + test('onPointerUp reports correct button property on right click', async () => { + await searchBox('onPointerUp'); + const component = await app.findElementByTestID( + 'pointer-up-button-target', + ); + await component.waitForDisplayed({timeout: 5000}); + + // Right click release triggers onPointerUp with button=2 + await component.click({button: 'right'}); + const stateText = await app.findElementByTestID( + 'pointer-up-button-state', + ); + + await app.waitUntil( + async () => { + const currentText = await stateText.getText(); + return currentText.includes('button=2'); + }, + { + timeout: 5000, + timeoutMsg: + 'State text not updated after onPointerUp with right button property.', + }, + ); + + const text = await stateText.getText(); + expect(text).toContain('PointerUp'); + expect(text).toContain('button=2'); + expect(text).toContain('buttons=0'); + }); });