diff --git a/packages/app/src/components/ui/press-highlight.native.tsx b/packages/app/src/components/ui/press-highlight.native.tsx index b809534fc..858e68843 100644 --- a/packages/app/src/components/ui/press-highlight.native.tsx +++ b/packages/app/src/components/ui/press-highlight.native.tsx @@ -2,6 +2,7 @@ import { forwardRef, useMemo } from "react"; import { Pressable, StyleSheet, View } from "react-native"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated"; +import { resolveHighlightCorners } from "./press-highlight.shape"; import type { PressHighlightProps } from "./press-highlight.types"; export const PressHighlight = forwardRef(function PressHighlight( @@ -9,6 +10,7 @@ export const PressHighlight = forwardRef(function Pre ref, ) { const highlighted = useSharedValue(0); + const corners = useMemo(() => resolveHighlightCorners(props.style), [props.style]); const animatedHighlightStyle = useAnimatedStyle(() => ({ opacity: highlighted.value })); // This gesture owns visual acknowledgement only. Pressable remains the sole owner of // activation, cancellation, accessibility, and the caller's drag/long-press callbacks. @@ -29,8 +31,9 @@ export const PressHighlight = forwardRef(function Pre const highlight = highlightStyle ? ( - {/* Keep the themed Unistyles node separate from the node Reanimated patches. */} - + {/* Keep the themed Unistyles node separate from the node Reanimated patches. The + pressable's corners come first so `highlightStyle` can still override them. */} + ) : null; const renderedChildren = diff --git a/packages/app/src/components/ui/press-highlight.shape.test.ts b/packages/app/src/components/ui/press-highlight.shape.test.ts new file mode 100644 index 000000000..10a4bcd2b --- /dev/null +++ b/packages/app/src/components/ui/press-highlight.shape.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; +import type { StyleProp, ViewStyle } from "react-native"; +import { resolveHighlightCorners } from "./press-highlight.shape"; + +describe("resolveHighlightCorners", () => { + it("takes the radius from the pressable's own style, the way a sidebar row declares it", () => { + const row = { minHeight: 36, borderRadius: 8, backgroundColor: "#111" }; + expect(resolveHighlightCorners(row)).toEqual({ borderRadius: 8 }); + }); + + it("reads through the style array a row builds per interaction state", () => { + const row: StyleProp = [{ borderRadius: 8 }, false, { backgroundColor: "#222" }]; + expect(resolveHighlightCorners(row)).toEqual({ borderRadius: 8 }); + }); + + it("keeps the last radius when a later entry in the array reshapes the row", () => { + expect(resolveHighlightCorners([{ borderRadius: 8 }, { borderRadius: 999 }])).toEqual({ + borderRadius: 999, + }); + }); + + it("carries every corner a row sets individually", () => { + const row = { borderTopLeftRadius: 8, borderBottomRightRadius: 2, paddingLeft: 12 }; + expect(resolveHighlightCorners(row)).toEqual({ + borderTopLeftRadius: 8, + borderBottomRightRadius: 2, + }); + }); + + it("returns no corners for a square row, rather than a zero it never asked for", () => { + expect(resolveHighlightCorners({ backgroundColor: "#111" })).toEqual({}); + expect(resolveHighlightCorners(undefined)).toEqual({}); + }); + + it("leaves a style resolved per press state alone", () => { + expect(resolveHighlightCorners(() => ({ borderRadius: 8 }))).toEqual({}); + }); +}); diff --git a/packages/app/src/components/ui/press-highlight.shape.ts b/packages/app/src/components/ui/press-highlight.shape.ts new file mode 100644 index 000000000..b3bf3b463 --- /dev/null +++ b/packages/app/src/components/ui/press-highlight.shape.ts @@ -0,0 +1,48 @@ +import { StyleSheet, type StyleProp, type ViewStyle } from "react-native"; + +/** + * A press highlight is drawn as an overlay inside the pressable rather than as a background on + * it, because the node Reanimated animates cannot also carry the caller's themed style. An + * overlay inherits nothing, so a rounded row gets a square flash unless the corners are copied + * across. + * + * They are copied from the row's own style, not asked for again: the row already declares its + * shape once, and a highlight that had to restate it would be a second copy free to drift. That + * leaves `highlightStyle` to mean only what the highlight is painted with. + */ +const CORNER_PROPERTIES = [ + "borderRadius", + "borderTopLeftRadius", + "borderTopRightRadius", + "borderBottomLeftRadius", + "borderBottomRightRadius", + "borderStartStartRadius", + "borderStartEndRadius", + "borderEndStartRadius", + "borderEndEndRadius", +] as const; + +/** + * The corner radii a pressable's own style gives its highlight. A style resolved per press state + * is left alone — its shape is not knowable before the press it describes, and every corner it + * could name is one the caller can still set through `highlightStyle`. + */ +export function resolveHighlightCorners( + style: StyleProp | ((state: never) => StyleProp) | undefined, +): ViewStyle { + if (typeof style === "function") { + return {}; + } + const flattened = StyleSheet.flatten(style); + if (!flattened) { + return {}; + } + const corners: ViewStyle = {}; + for (const property of CORNER_PROPERTIES) { + const value = flattened[property]; + if (value !== undefined) { + corners[property] = value; + } + } + return corners; +} diff --git a/packages/app/src/components/ui/press-highlight.types.ts b/packages/app/src/components/ui/press-highlight.types.ts index 2f0e3896b..a84ed1eda 100644 --- a/packages/app/src/components/ui/press-highlight.types.ts +++ b/packages/app/src/components/ui/press-highlight.types.ts @@ -1,6 +1,13 @@ import type { PressableProps, StyleProp, ViewStyle } from "react-native"; export interface PressHighlightProps extends PressableProps { - /** Native finger-down surface. Desktop hover remains owned by the caller. */ + /** + * What the native finger-down surface is painted with — a background colour, an opacity. + * Desktop hover remains owned by the caller. + * + * Shape is not part of it: the highlight takes its corners from the pressable's own `style`, so + * a rounded row gets a rounded flash without restating the radius. Set a corner here only to + * depart from the row's shape on purpose. + */ highlightStyle?: StyleProp; }