fix(app): trace the row's corners with its press highlight
The highlight is an overlay inside the pressable rather than a background on it, because the node Reanimated animates cannot also carry a themed Unistyles style. An overlay inherits nothing, so a rounded sidebar row flashed square. Take the corners from the pressable's own style. The row already declares its shape once, and a highlight that restated it would be a second copy free to drift; highlightStyle is left meaning only what the highlight is painted with. Corners apply before it, so a caller can still depart from the row's shape.
This commit is contained in:
@@ -2,6 +2,7 @@ import { forwardRef, useMemo } from "react";
|
|||||||
import { Pressable, StyleSheet, View } from "react-native";
|
import { Pressable, StyleSheet, View } from "react-native";
|
||||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||||
import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
|
import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
|
||||||
|
import { resolveHighlightCorners } from "./press-highlight.shape";
|
||||||
import type { PressHighlightProps } from "./press-highlight.types";
|
import type { PressHighlightProps } from "./press-highlight.types";
|
||||||
|
|
||||||
export const PressHighlight = forwardRef<View, PressHighlightProps>(function PressHighlight(
|
export const PressHighlight = forwardRef<View, PressHighlightProps>(function PressHighlight(
|
||||||
@@ -9,6 +10,7 @@ export const PressHighlight = forwardRef<View, PressHighlightProps>(function Pre
|
|||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const highlighted = useSharedValue(0);
|
const highlighted = useSharedValue(0);
|
||||||
|
const corners = useMemo(() => resolveHighlightCorners(props.style), [props.style]);
|
||||||
const animatedHighlightStyle = useAnimatedStyle(() => ({ opacity: highlighted.value }));
|
const animatedHighlightStyle = useAnimatedStyle(() => ({ opacity: highlighted.value }));
|
||||||
// This gesture owns visual acknowledgement only. Pressable remains the sole owner of
|
// This gesture owns visual acknowledgement only. Pressable remains the sole owner of
|
||||||
// activation, cancellation, accessibility, and the caller's drag/long-press callbacks.
|
// activation, cancellation, accessibility, and the caller's drag/long-press callbacks.
|
||||||
@@ -29,8 +31,9 @@ export const PressHighlight = forwardRef<View, PressHighlightProps>(function Pre
|
|||||||
|
|
||||||
const highlight = highlightStyle ? (
|
const highlight = highlightStyle ? (
|
||||||
<Animated.View pointerEvents="none" style={[styles.highlight, animatedHighlightStyle]}>
|
<Animated.View pointerEvents="none" style={[styles.highlight, animatedHighlightStyle]}>
|
||||||
{/* Keep the themed Unistyles node separate from the node Reanimated patches. */}
|
{/* Keep the themed Unistyles node separate from the node Reanimated patches. The
|
||||||
<View style={[styles.highlightFill, highlightStyle]} />
|
pressable's corners come first so `highlightStyle` can still override them. */}
|
||||||
|
<View style={[styles.highlightFill, corners, highlightStyle]} />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
) : null;
|
) : null;
|
||||||
const renderedChildren =
|
const renderedChildren =
|
||||||
|
|||||||
@@ -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<ViewStyle> = [{ 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({});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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<ViewStyle> | ((state: never) => StyleProp<ViewStyle>) | 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;
|
||||||
|
}
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
import type { PressableProps, StyleProp, ViewStyle } from "react-native";
|
import type { PressableProps, StyleProp, ViewStyle } from "react-native";
|
||||||
|
|
||||||
export interface PressHighlightProps extends PressableProps {
|
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<ViewStyle>;
|
highlightStyle?: StyleProp<ViewStyle>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user