1import { Slider } from "@expo/ui/swift-ui";
2import {
3 accessibilityLabel,
4 accessibilityValue,
5 disabled,
6 tint,
7} from "@expo/ui/swift-ui/modifiers";
8import * as Haptics from "expo-haptics";
9import { useRef } from "react";
10import { View } from "react-native";
11
12import { AppText } from "./AppText";
13import { SymbolView } from "./AppSymbol";
14import { NativeThemeHost } from "./NativeThemeHost";
15import type { SliderRowProps } from "./SliderRow.types";
16import { useControlTheme } from "./useControlTheme";
17
18export type { SliderRowProps } from "./SliderRow.types";
19
20export function SliderRow(props: SliderRowProps) {
21 const { colors } = useControlTheme();
22 // The value of the drag in progress; null until the drag moves.
23 const draft = useRef<number | null>(null);
24 const editing = useRef(false);
25 const commit = (value: number) => {
26 if (props.disabled) return;
27 const next = Math.min(
28 props.max,
29 Math.max(props.min, props.min + Math.round((value - props.min) / props.step) * props.step),
30 );
31 if (next === props.value) return;
32 void Haptics.selectionAsync().catch(() => undefined);
33 props.onChange(next);
34 };
35 return (
36 <View className={props.disabled ? "gap-1 p-4 opacity-[0.45]" : "gap-1 p-4"}>
37 <View className="flex-row items-center gap-4">
38 <SymbolView name={props.icon} size={22} tintColorClassName="accent-icon" />
39 <AppText className="flex-1 text-lg text-foreground">{props.label}</AppText>
40 <AppText className="text-base font-t3-medium text-foreground-muted">
41 {props.valueLabel}
42 </AppText>
43 </View>
44 <View className="flex-row items-center gap-3">
45 {props.minimumIcon ? (
46 <SymbolView name={props.minimumIcon} size={15} tintColorClassName="accent-icon-muted" />
47 ) : null}
48 <NativeThemeHost style={{ flex: 1, height: 44 }}>
49 <Slider
50 min={props.min}
51 max={props.max}
52 step={props.step}
53 value={props.value}
54 modifiers={[
55 tint(colors["--color-primary"]),
56 disabled(Boolean(props.disabled)),
57 accessibilityLabel(props.label),
58 accessibilityValue(props.valueLabel),
59 ]}
60 onValueChange={(value) => {
61 // Accessibility adjustments do not begin a touch editing session.
62 if (editing.current) draft.current = value;
63 else commit(value);
64 }}
65 onEditingChanged={(isEditing) => {
66 editing.current = isEditing;
67 const value = draft.current;
68 draft.current = null;
69 if (!isEditing && value !== null) commit(value);
70 }}
71 />
72 </NativeThemeHost>
73 {props.maximumIcon ? (
74 <SymbolView name={props.maximumIcon} size={22} tintColorClassName="accent-icon-muted" />
75 ) : null}
76 </View>
77 </View>
78 );
79}