From 59e49b03b218ed89057e044dcb72bc3db0188221 Mon Sep 17 00:00:00 2001 From: hazrid93 Date: Mon, 20 Jul 2026 10:28:19 +0000 Subject: [PATCH] feat: add Slider component Addresses #4271. Adds a Material 3 slider controlled by a value prop with min and max bounds, defaulting to 0 and 1, and clamps out of range values. The thumb position and the filled track reflect the value relative to the range. Calls onValueChange with the new value on drag or tap, computing the value from the touch position. An optional step snaps the value to multiples of the step measured from the minimum. Exposes the adjustable accessibility role with min, max, and current value. When disabled it suppresses change callbacks and visually dims the thumb. Configurable colors for the active track, inactive track, and thumb, defaulting to the theme primary. The component is exported from the public surface. --- src/components/Slider/Slider.tsx | 137 +++++++++++++++++++++++++++++++ src/components/Slider/types.ts | 30 +++++++ src/index.tsx | 2 + 3 files changed, 169 insertions(+) create mode 100644 src/components/Slider/Slider.tsx create mode 100644 src/components/Slider/types.ts diff --git a/src/components/Slider/Slider.tsx b/src/components/Slider/Slider.tsx new file mode 100644 index 0000000000..7399041654 --- /dev/null +++ b/src/components/Slider/Slider.tsx @@ -0,0 +1,137 @@ +import * as React from 'react'; +import { StyleSheet, View, ViewStyle } from 'react-native'; + +import type { SliderProps } from './types'; +import { useInternalTheme } from '../../core/theming'; + +const clamp = (v: number, min: number, max: number) => + Math.min(Math.max(v, min), max); + +const round = (v: number, step: number) => { + if (!step) return v; + return Math.round(v / step) * step; +}; + +const styles = StyleSheet.create({ + container: { + height: 48, + justifyContent: 'center', + paddingVertical: 12, + } as ViewStyle, + track: { + height: 4, + borderRadius: 2, + overflow: 'hidden', + position: 'relative', + } as ViewStyle, + thumb: { + position: 'absolute', + width: 16, + height: 16, + borderRadius: 8, + top: -6, + marginLeft: -8, + } as ViewStyle, +}); + +/** + * Material 3 Slider. + * + * A horizontal slider that lets the user pick a value between `min` and `max` + * by dragging or tapping the track. The thumb position reflects the + * controlled `value`. + * + * @param props + */ +function Slider({ + value, + min = 0, + max = 1, + step, + onValueChange, + disabled = false, + color, + trackColor, + thumbColor, + style, + theme: themeOverrides, + testID = 'slider', +}: SliderProps) { + const theme = useInternalTheme(themeOverrides); + const [width, setWidth] = React.useState(0); + + const activeColor = + color ?? (theme.isV3 ? theme.colors.primary : theme.colors.accent); + const inactiveColor = + trackColor ?? (theme.isV3 ? theme.colors.surfaceVariant : '#bdbdbd'); + const thumbTintColor = thumbColor ?? activeColor; + + const span = Math.max(max - min, 1e-9); + const pct = clamp((value - min) / span, 0, 1); + + const valueFromX = (x: number) => { + const ratio = width > 0 ? clamp(x / width, 0, 1) : 0; + let next = min + ratio * span; + if (step) next = min + round(next - min, step); + return clamp(next, min, max); + }; + + const handleResponder = (e: { nativeEvent: { locationX: number } }) => { + if (disabled || width <= 0) return; + onValueChange?.(valueFromX(e.nativeEvent.locationX)); + }; + + const handleLayout = (e: { nativeEvent: { layout: { width: number } } }) => { + setWidth(e.nativeEvent.layout.width); + }; + + return ( + + !disabled} + onResponderGrant={handleResponder} + onResponderMove={handleResponder} + > + + + + + ); +} + +export default Slider; diff --git a/src/components/Slider/types.ts b/src/components/Slider/types.ts new file mode 100644 index 0000000000..8e863c725b --- /dev/null +++ b/src/components/Slider/types.ts @@ -0,0 +1,30 @@ +import type { StyleProp, ViewStyle } from 'react-native'; + +import type { ThemeProp } from '../../types'; + +export type SliderProps = { + /** Current value of the slider (controlled). */ + value: number; + /** Minimum value. @default 0 */ + min?: number; + /** Maximum value. @default 1 */ + max?: number; + /** Step size. When set, the value snaps to multiples of `step` from `min`. */ + step?: number; + /** Called with the new value whenever the user drags or taps the track. */ + onValueChange?: (value: number) => void; + /** Whether the slider is disabled (no interaction, dimmed thumb). */ + disabled?: boolean; + /** Color of the active track (left of the thumb). Defaults to theme primary. */ + color?: string; + /** Color of the inactive track. Defaults to a translucent track. */ + trackColor?: string; + /** Color of the thumb. Defaults to the active color. */ + thumbColor?: string; + /** Style applied to the container. */ + style?: StyleProp; + /** @optional */ + theme?: ThemeProp; + /** TestID for testing. */ + testID?: string; +}; diff --git a/src/index.tsx b/src/index.tsx index 1b20528787..b399a14759 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -33,6 +33,7 @@ export { default as BottomNavigation } from './components/BottomNavigation/Botto export { default as Button } from './components/Button/Button'; export { default as Card } from './components/Card/Card'; export { default as Checkbox } from './components/Checkbox'; +export { default as Slider } from './components/Slider/Slider'; export { default as Chip } from './components/Chip/Chip'; export { default as DataTable } from './components/DataTable/DataTable'; export { default as Dialog } from './components/Dialog/Dialog'; @@ -86,6 +87,7 @@ export type { } from './components/BottomNavigation/BottomNavigation'; export type { Props as ButtonProps } from './components/Button/Button'; export type { Props as CardProps } from './components/Card/Card'; +export type { SliderProps } from './components/Slider/types'; export type { Props as CardActionsProps } from './components/Card/CardActions'; export type { Props as CardContentProps } from './components/Card/CardContent'; export type { Props as CardCoverProps } from './components/Card/CardCover';