mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
Keep Android timeline still when tapping text (#4090)
* fix(android): keep timeline still when tapping text Selectable Android text takes touch focus. React Native then scrolls focused children using untransformed coordinates, which moves inverted timelines even though the user did not scroll.\n\nSkip that auto-scroll only for touch-focused, selectable, non-editable text in inverted vertical scroll views, preserving normal focus navigation and editable inputs. * test(e2e): isolate PR pane fixtures from bots
This commit is contained in:
@@ -3,8 +3,8 @@ import {
|
||||
openPrPane,
|
||||
expectPrPaneTitle,
|
||||
expectPrPaneState,
|
||||
expectPrPaneCheckSummary,
|
||||
expectPrPaneActivityCount,
|
||||
expectPrPaneChecks,
|
||||
expectPrPaneActivity,
|
||||
} from "../support/helpers/pr-pane";
|
||||
import { gotoWorkspace } from "../support/helpers/launcher";
|
||||
import {
|
||||
@@ -50,7 +50,6 @@ test.describe("PR pane", () => {
|
||||
],
|
||||
},
|
||||
{ title: "PR with reviews", state: "open", commentCount: 3 },
|
||||
{ title: "PR with no checks", state: "open" },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -102,25 +101,21 @@ test.describe("PR pane", () => {
|
||||
await expectPrPaneTitle(page, "Work in progress");
|
||||
});
|
||||
|
||||
test("renders check pills with correct success/failure/pending counts", async ({ page }) => {
|
||||
test("renders seeded checks in their success, failure, and pending groups", async ({ page }) => {
|
||||
await gotoWorkspace(page, workspaceByTitle.get("PR with mixed checks")!);
|
||||
await openPrPane(page);
|
||||
|
||||
await expectPrPaneCheckSummary(page, { success: 2, failure: 1, pending: 1 });
|
||||
await expectPrPaneChecks(page, {
|
||||
success: ["build-1", "build-2"],
|
||||
failure: ["deploy"],
|
||||
pending: ["security"],
|
||||
});
|
||||
});
|
||||
|
||||
test("renders activity rows with correct count", async ({ page }) => {
|
||||
test("renders every seeded activity comment", async ({ page }) => {
|
||||
await gotoWorkspace(page, workspaceByTitle.get("PR with reviews")!);
|
||||
await openPrPane(page);
|
||||
|
||||
await expectPrPaneActivityCount(page, 3);
|
||||
});
|
||||
|
||||
test("renders gracefully with zero checks", async ({ page }) => {
|
||||
await gotoWorkspace(page, workspaceByTitle.get("PR with no checks")!);
|
||||
await openPrPane(page);
|
||||
|
||||
await expectPrPaneCheckSummary(page, { success: 0, failure: 0, pending: 0 });
|
||||
await expectPrPaneTitle(page, "PR with no checks");
|
||||
await expectPrPaneActivity(page, ["Test comment 1", "Test comment 2", "Test comment 3"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import type { CheckPresentationCounts } from "@/git/check-presentation";
|
||||
import { getStateLabel } from "@/git/pull-request-panel/data";
|
||||
import { openPullRequestPanel } from "./workspace-tabs";
|
||||
|
||||
@@ -20,23 +19,31 @@ export async function expectPrPaneState(
|
||||
});
|
||||
}
|
||||
|
||||
async function assertCheckPill(page: Page, testId: string, count: number): Promise<void> {
|
||||
const locator = page.getByTestId(testId);
|
||||
await expect(locator).toHaveCount(count > 0 ? 1 : 0, { timeout: 15_000 });
|
||||
if (count > 0) {
|
||||
await expect(locator).toContainText(String(count));
|
||||
export async function expectPrPaneChecks(
|
||||
page: Page,
|
||||
checks: { success: string[]; failure: string[]; pending: string[] },
|
||||
): Promise<void> {
|
||||
const section = page.getByTestId("pr-pane-checks");
|
||||
const groups = [
|
||||
{ status: "failure", label: /failing checks?$/, names: checks.failure },
|
||||
{ status: "pending", label: /in progress checks?$/, names: checks.pending },
|
||||
{ status: "success", label: /successful checks?$/, names: checks.success },
|
||||
];
|
||||
|
||||
for (const group of groups) {
|
||||
const groupSection = section.getByTestId(`pr-pane-check-group-${group.status}`);
|
||||
await expect(groupSection.getByRole("button", { name: group.label })).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
for (const name of group.names) {
|
||||
await expect(groupSection.getByText(name, { exact: true })).toHaveCount(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function expectPrPaneCheckSummary(
|
||||
page: Page,
|
||||
counts: Pick<CheckPresentationCounts, "success" | "failure" | "pending">,
|
||||
): Promise<void> {
|
||||
await assertCheckPill(page, "pr-pane-check-success", counts.success);
|
||||
await assertCheckPill(page, "pr-pane-check-failure", counts.failure);
|
||||
await assertCheckPill(page, "pr-pane-check-pending", counts.pending);
|
||||
}
|
||||
|
||||
export async function expectPrPaneActivityCount(page: Page, count: number): Promise<void> {
|
||||
await expect(page.getByTestId("pr-pane-activity-row")).toHaveCount(count, { timeout: 15_000 });
|
||||
export async function expectPrPaneActivity(page: Page, bodies: string[]): Promise<void> {
|
||||
const activity = page.getByTestId("pr-pane-activity-row");
|
||||
for (const body of bodies) {
|
||||
await expect(activity.getByText(body, { exact: true })).toBeVisible({ timeout: 15_000 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ function CheckGroup({
|
||||
}) {
|
||||
const handlePress = useCallback(() => onToggle(group.status), [group.status, onToggle]);
|
||||
return (
|
||||
<View>
|
||||
<View testID={`pr-pane-check-group-${group.status}`}>
|
||||
<Pressable onPress={handlePress} style={styles.groupHeader} accessibilityRole="button">
|
||||
<Text style={styles.groupLabel}>{group.label}</Text>
|
||||
{collapsed ? (
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java
|
||||
index f708543..26df5e0 100644
|
||||
--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java
|
||||
+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java
|
||||
@@ -29,6 +29,7 @@ import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.widget.OverScroller;
|
||||
import android.widget.ScrollView;
|
||||
+import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ViewCompat;
|
||||
@@ -425,12 +426,23 @@ public class ReactScrollView extends ScrollView
|
||||
*/
|
||||
@Override
|
||||
public void requestChildFocus(View child, View focused) {
|
||||
- if (focused != null) {
|
||||
+ if (focused != null && shouldScrollToFocusedChild(focused)) {
|
||||
scrollToChild(focused);
|
||||
}
|
||||
super.requestChildFocus(child, focused);
|
||||
}
|
||||
|
||||
+ private boolean shouldScrollToFocusedChild(View focused) {
|
||||
+ if (getScaleY() >= 0 || !focused.isInTouchMode() || !(focused instanceof TextView)) {
|
||||
+ return true;
|
||||
+ }
|
||||
+
|
||||
+ // Selectable text takes focus when tapped. In inverted lists, scrolling to that focus uses
|
||||
+ // untransformed coordinates and unexpectedly moves the timeline.
|
||||
+ TextView textView = (TextView) focused;
|
||||
+ return !textView.isTextSelectable() || textView.onCheckIsTextEditor();
|
||||
+ }
|
||||
+
|
||||
private int getScrollDelta(View descendent) {
|
||||
descendent.getDrawingRect(mTempRect);
|
||||
offsetDescendantRectToMyCoords(descendent, mTempRect);
|
||||
@@ -12,6 +12,10 @@ const patchedPackages = [
|
||||
nodeModulesPath: "node_modules/react-native-markdown-display",
|
||||
patchPrefix: "react-native-markdown-display+",
|
||||
},
|
||||
{
|
||||
nodeModulesPath: "node_modules/react-native",
|
||||
patchPrefix: "react-native+",
|
||||
},
|
||||
{
|
||||
nodeModulesPath: "node_modules/react-native-draggable-flatlist",
|
||||
patchPrefix: "react-native-draggable-flatlist+",
|
||||
|
||||
Reference in New Issue
Block a user