Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions src/components/Slider/Slider.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View
testID={testID}
style={[styles.container, style]}
accessibilityRole="adjustable"
accessibilityValue={{
min,
max,
now: value,
}}
accessibilityState={disabled ? { disabled: true } : undefined}
>
<View
testID={`${testID}-track`}
style={[styles.track, { backgroundColor: inactiveColor }]}
onLayout={handleLayout}
onStartShouldSetResponder={() => !disabled}
onResponderGrant={handleResponder}
onResponderMove={handleResponder}
>
<View
testID={`${testID}-active`}
style={
{
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: `${pct * 100}%`,
backgroundColor: activeColor,
} as ViewStyle
}
/>
<View
testID={`${testID}-thumb`}
style={[
styles.thumb,
{
left: pct * width,
backgroundColor: thumbTintColor,
opacity: disabled ? 0.4 : 1,
} as ViewStyle,
]}
/>
</View>
</View>
);
}

export default Slider;
30 changes: 30 additions & 0 deletions src/components/Slider/types.ts
Original file line number Diff line number Diff line change
@@ -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<ViewStyle>;
/** @optional */
theme?: ThemeProp;
/** TestID for testing. */
testID?: string;
};
2 changes: 2 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down