diff --git a/README.md b/README.md index 8769ef1..89133e3 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,8 @@ const styles = StyleSheet.create({ | `renderLine` | `ReactNode` | — | Divider line | | `beforeImageProps` | `PassThroughImageProps` | — | `before` (`expo-image`, minus `source` / `style` / `contentFit`) | | `afterImageProps` | `PassThroughImageProps` | — | `after` | +| `beforeLabel` | `string` | — | `Label on the before image` | +| `afterLabel` | `string` | — | `Label on the after image` | ## License diff --git a/example/App.tsx b/example/App.tsx index 1e9ed8d..3e4c2de 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -8,7 +8,9 @@ export default function App() { } @@ -16,7 +18,9 @@ export default function App() { } diff --git a/src/BeforeClip.tsx b/src/BeforeClip.tsx index f0696dc..0d8407e 100644 --- a/src/BeforeClip.tsx +++ b/src/BeforeClip.tsx @@ -1,7 +1,9 @@ import { Image, type ImageProps, type ImageSource } from "expo-image"; -import { StyleSheet } from "react-native"; +import { StyleSheet, View } from "react-native"; import Animated, { useAnimatedStyle, type DerivedValue } from "react-native-reanimated"; +import { Label } from "./Label"; + type PassThroughImageProps = Omit; interface BeforeClipProps { @@ -10,23 +12,31 @@ interface BeforeClipProps { imageProps?: PassThroughImageProps; measuredWidth: number; splitPx: DerivedValue; + beforeLabel?: string; } export function BeforeClip(props: BeforeClipProps) { - const { measuredWidth, splitPx, source, contentFit, imageProps } = props; + const { measuredWidth, splitPx, source, contentFit, imageProps, beforeLabel } = props; const clipStyle = useAnimatedStyle(() => ({ width: splitPx.get() })); return ( {measuredWidth > 0 && ( - + <> + + {beforeLabel && ( + + + )} + )} ); diff --git a/src/ImageComparisonSlider.test.tsx b/src/ImageComparisonSlider.test.tsx index f9fba07..d00145f 100644 --- a/src/ImageComparisonSlider.test.tsx +++ b/src/ImageComparisonSlider.test.tsx @@ -75,6 +75,8 @@ describe("ImageComparisonSlider", () => { renderLine: Custom line, beforeImageProps: { testID: "before-image" }, afterImageProps: { testID: "after-image" }, + beforeLabel: "Before", + afterLabel: "After", }); expect(getSlider()).toHaveProp("accessibilityState", { disabled: true }); @@ -83,6 +85,8 @@ describe("ImageComparisonSlider", () => { expect(screen.queryByTestId("before-image")).toBeNull(); expect(screen.queryByText("Custom knob")).toBeNull(); expect(screen.queryByText("Custom line")).toBeNull(); + expect(screen.queryByText("Before")).toBeNull(); + expect(screen.queryByText("After")).toBeNull(); }); it("renders custom knob and line content", async () => { @@ -132,4 +136,34 @@ describe("ImageComparisonSlider", () => { expect(getSlider()).toHaveProp("accessibilityValue", { min: 0, max: 100, now: 50 }); }); + + it("does not render labels by default", async () => { + await renderSlider(); + + expect(screen.queryByTestId("before-label")).toBeNull(); + expect(screen.queryByTestId("after-label")).toBeNull(); + }); + + it("renders before and after labels when provided", async () => { + await renderSlider({ beforeLabel: "Before", afterLabel: "After" }); + await layoutSlider(); + + expect(screen.getByText("Before")).toBeOnTheScreen(); + expect(screen.getByText("After")).toBeOnTheScreen(); + }); + + it("renders only the before label", async () => { + await renderSlider({ beforeLabel: "Before" }); + await layoutSlider(); + + expect(screen.getByText("Before")).toBeOnTheScreen(); + expect(screen.queryByTestId("after-label")).toBeNull(); + }); + + it("renders only the after label", async () => { + await renderSlider({ afterLabel: "After" }); + + expect(screen.getByText("After")).toBeOnTheScreen(); + expect(screen.queryByTestId("before-label")).toBeNull(); + }); }); diff --git a/src/ImageComparisonSlider.tsx b/src/ImageComparisonSlider.tsx index 0be5890..30d9ccf 100644 --- a/src/ImageComparisonSlider.tsx +++ b/src/ImageComparisonSlider.tsx @@ -6,6 +6,7 @@ import { useDerivedValue } from "react-native-reanimated"; import { BeforeClip } from "./BeforeClip"; import { Handle } from "./Handle"; +import { Label } from "./Label"; import { useComparisonSlider } from "./useComparisonSlider"; type PassThroughImageProps = Omit; @@ -44,6 +45,12 @@ export interface ImageComparisonSliderProps { /** after */ afterImageProps?: PassThroughImageProps; + + /** Label on the before image */ + beforeLabel?: string; + + /** Label on the after image */ + afterLabel?: string; } /** @@ -63,6 +70,8 @@ export function ImageComparisonSlider({ renderLine, beforeImageProps, afterImageProps, + beforeLabel, + afterLabel, }: ImageComparisonSliderProps) { const { measuredWidth, @@ -97,6 +106,7 @@ export function ImageComparisonSlider({ contentFit={contentFit} style={styles.fullImage} /> + {afterLabel && enabled && diff --git a/src/Label.tsx b/src/Label.tsx new file mode 100644 index 0000000..c4edb8d --- /dev/null +++ b/src/Label.tsx @@ -0,0 +1,32 @@ +import { StyleSheet, Text, View, type ViewStyle } from "react-native"; + +import { EDGE_MARGIN } from "./utils"; + +interface LabelProps { + text: string; + alignSelf?: ViewStyle["alignSelf"]; + testID?: string; +} + +export function Label({ text, alignSelf = "flex-end", testID }: LabelProps) { + return ( + + {text} + + ); +} + +const styles = StyleSheet.create({ + badge: { + margin: EDGE_MARGIN, + backgroundColor: "white", + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 8, + }, + text: { + color: "black", + fontSize: 12, + lineHeight: 16, + }, +});