feat : Switch
This commit is contained in:
@@ -20,6 +20,12 @@ export default function HomeTabScreen() {
|
|||||||
onPress={() => router.push('/index-old')}
|
onPress={() => router.push('/index-old')}
|
||||||
accessibilityLabel="Ouvrir la page d'exemple des champs"
|
accessibilityLabel="Ouvrir la page d'exemple des champs"
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
label="Voir la démo du switch"
|
||||||
|
variant="secondary"
|
||||||
|
onPress={() => router.push('/switch-demo')}
|
||||||
|
accessibilityLabel="Ouvrir la page de démonstration du switch"
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -43,5 +49,6 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
marginTop: spacing[16],
|
marginTop: spacing[16],
|
||||||
|
gap: spacing[8],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export default function RootLayout() {
|
|||||||
<Stack.Screen name="(auth)" />
|
<Stack.Screen name="(auth)" />
|
||||||
<Stack.Screen name="(tabs)" />
|
<Stack.Screen name="(tabs)" />
|
||||||
<Stack.Screen name="(org)" />
|
<Stack.Screen name="(org)" />
|
||||||
|
<Stack.Screen name="switch-demo" />
|
||||||
<Stack.Screen name="event/[id]" />
|
<Stack.Screen name="event/[id]" />
|
||||||
<Stack.Screen name="create/event" options={{ presentation: 'modal' }} />
|
<Stack.Screen name="create/event" options={{ presentation: 'modal' }} />
|
||||||
<Stack.Screen name="create/post" options={{ presentation: 'modal' }} />
|
<Stack.Screen name="create/post" options={{ presentation: 'modal' }} />
|
||||||
|
|||||||
118
src/app/switch-demo.tsx
Normal file
118
src/app/switch-demo.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { StyleSheet, Text, View } from 'react-native';
|
||||||
|
|
||||||
|
import { FormField, Switch } from '@/components/form';
|
||||||
|
import { Screen } from '@/components/ui/Screen';
|
||||||
|
import { colors } from '@/theme/colors';
|
||||||
|
import { spacing } from '@/theme/spacing';
|
||||||
|
import { typography } from '@/theme/typography';
|
||||||
|
|
||||||
|
export default function SwitchDemoScreen() {
|
||||||
|
const [pushEnabled, setPushEnabled] = useState(true);
|
||||||
|
const [weeklyDigestEnabled, setWeeklyDigestEnabled] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Screen contentContainerStyle={styles.content}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.kicker}>DESIGN SYSTEM · SWITCH</Text>
|
||||||
|
<Text style={styles.title}>Demos du composant Switch</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
label="Notifications push"
|
||||||
|
helper="Switch custom basé sur Expo/React Native"
|
||||||
|
>
|
||||||
|
<View style={styles.switchRow}>
|
||||||
|
<View style={styles.switchLabelWrap}>
|
||||||
|
<Text style={styles.switchLabel}>Activer les alertes de nouveaux événements</Text>
|
||||||
|
<Text style={styles.switchHint}>
|
||||||
|
{pushEnabled ? 'Activé' : 'Désactivé'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
accessibilityLabel="Activer les notifications push"
|
||||||
|
value={pushEnabled}
|
||||||
|
onValueChange={setPushEnabled}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
label="Digest hebdo"
|
||||||
|
helper="Exemple de taille large"
|
||||||
|
>
|
||||||
|
<View style={styles.switchRow}>
|
||||||
|
<View style={styles.switchLabelWrap}>
|
||||||
|
<Text style={styles.switchLabel}>Recevoir un recap chaque lundi</Text>
|
||||||
|
<Text style={styles.switchHint}>
|
||||||
|
{weeklyDigestEnabled ? 'Activé' : 'Désactivé'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
accessibilityLabel="Activer le digest hebdomadaire"
|
||||||
|
value={weeklyDigestEnabled}
|
||||||
|
onValueChange={setWeeklyDigestEnabled}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Etat desactive" helper="Le switch est non interactif">
|
||||||
|
<View style={styles.switchRow}>
|
||||||
|
<View style={styles.switchLabelWrap}>
|
||||||
|
<Text style={styles.switchLabel}>Mode maintenance</Text>
|
||||||
|
<Text style={styles.switchHint}>Indisponible</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
accessibilityLabel="Switch desactive"
|
||||||
|
value
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</FormField>
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
content: {
|
||||||
|
gap: spacing[16],
|
||||||
|
paddingTop: spacing[24],
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
gap: spacing[8],
|
||||||
|
marginBottom: spacing[8],
|
||||||
|
},
|
||||||
|
kicker: {
|
||||||
|
...typography.sectionLabel,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
...typography.body,
|
||||||
|
color: colors.textPrimary,
|
||||||
|
},
|
||||||
|
switchRow: {
|
||||||
|
minHeight: 56,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.inputBorder,
|
||||||
|
borderRadius: 14,
|
||||||
|
backgroundColor: colors.inputBackground,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: spacing[12],
|
||||||
|
},
|
||||||
|
switchLabelWrap: {
|
||||||
|
flex: 1,
|
||||||
|
gap: 2,
|
||||||
|
},
|
||||||
|
switchLabel: {
|
||||||
|
...typography.body,
|
||||||
|
color: colors.textPrimary,
|
||||||
|
},
|
||||||
|
switchHint: {
|
||||||
|
...typography.caption,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
});
|
||||||
151
src/components/form/Switch.tsx
Normal file
151
src/components/form/Switch.tsx
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
Animated,
|
||||||
|
Easing,
|
||||||
|
Pressable,
|
||||||
|
StyleSheet,
|
||||||
|
View,
|
||||||
|
type StyleProp,
|
||||||
|
type ViewStyle,
|
||||||
|
} from 'react-native';
|
||||||
|
|
||||||
|
import { colors } from '@/theme/colors';
|
||||||
|
|
||||||
|
export type SwitchSize = 'md' | 'lg';
|
||||||
|
|
||||||
|
export type SwitchProps = {
|
||||||
|
value: boolean;
|
||||||
|
onValueChange?: (value: boolean) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
testID?: string;
|
||||||
|
accessibilityLabel?: string;
|
||||||
|
size?: SwitchSize;
|
||||||
|
containerStyle?: StyleProp<ViewStyle>;
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIZE_BY_VARIANT: Record<
|
||||||
|
SwitchSize,
|
||||||
|
{ width: number; height: number; thumb: number; padding: number }
|
||||||
|
> = {
|
||||||
|
md: { width: 56, height: 34, thumb: 26, padding: 4 },
|
||||||
|
lg: { width: 64, height: 38, thumb: 30, padding: 4 },
|
||||||
|
};
|
||||||
|
|
||||||
|
function Switch({
|
||||||
|
size = 'md',
|
||||||
|
value,
|
||||||
|
disabled,
|
||||||
|
onValueChange,
|
||||||
|
testID,
|
||||||
|
accessibilityLabel,
|
||||||
|
containerStyle,
|
||||||
|
style,
|
||||||
|
}: SwitchProps) {
|
||||||
|
const metrics = SIZE_BY_VARIANT[size];
|
||||||
|
const travel = metrics.width - metrics.padding * 2 - metrics.thumb;
|
||||||
|
const progress = useRef(new Animated.Value(value ? 1 : 0)).current;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Animated.timing(progress, {
|
||||||
|
toValue: value ? 1 : 0,
|
||||||
|
duration: 180,
|
||||||
|
easing: Easing.out(Easing.cubic),
|
||||||
|
useNativeDriver: true,
|
||||||
|
}).start();
|
||||||
|
}, [progress, value]);
|
||||||
|
|
||||||
|
const thumbTranslateX = progress.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: [0, travel],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
testID={testID}
|
||||||
|
accessibilityRole="switch"
|
||||||
|
accessibilityState={{ checked: value, disabled }}
|
||||||
|
accessibilityLabel={accessibilityLabel}
|
||||||
|
disabled={disabled}
|
||||||
|
onPress={() => onValueChange?.(!value)}
|
||||||
|
style={[
|
||||||
|
styles.container,
|
||||||
|
{
|
||||||
|
width: metrics.width,
|
||||||
|
height: metrics.height,
|
||||||
|
borderRadius: metrics.height / 2,
|
||||||
|
opacity: disabled ? 0.5 : 1,
|
||||||
|
},
|
||||||
|
containerStyle,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{value ? (
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.secondary, colors.primary]}
|
||||||
|
start={{ x: 0, y: 0.5 }}
|
||||||
|
end={{ x: 1, y: 0.5 }}
|
||||||
|
style={[
|
||||||
|
styles.trackFill,
|
||||||
|
{ borderRadius: metrics.height / 2, borderColor: 'rgba(255,255,255,0.18)' },
|
||||||
|
]}
|
||||||
|
pointerEvents="none"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.trackFill,
|
||||||
|
styles.trackOff,
|
||||||
|
{ borderRadius: metrics.height / 2 },
|
||||||
|
]}
|
||||||
|
pointerEvents="none"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.thumb,
|
||||||
|
{
|
||||||
|
width: metrics.thumb,
|
||||||
|
height: metrics.thumb,
|
||||||
|
borderRadius: metrics.thumb / 2,
|
||||||
|
top: metrics.padding,
|
||||||
|
left: metrics.padding,
|
||||||
|
transform: [{ translateX: thumbTranslateX }],
|
||||||
|
backgroundColor: value ? '#FFFFFF' : colors.textStrong,
|
||||||
|
},
|
||||||
|
style,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
trackFill: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
trackOff: {
|
||||||
|
backgroundColor: colors.surfaceElevated,
|
||||||
|
borderColor: colors.inputBorder,
|
||||||
|
},
|
||||||
|
thumb: {
|
||||||
|
position: 'absolute',
|
||||||
|
shadowColor: '#000000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.25,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Switch;
|
||||||
@@ -2,11 +2,13 @@ export { default as FormField } from './FormField';
|
|||||||
export { default as Input } from './Input';
|
export { default as Input } from './Input';
|
||||||
export { default as PasswordInput } from './PasswordInput';
|
export { default as PasswordInput } from './PasswordInput';
|
||||||
export { default as SearchInput } from './SearchInput';
|
export { default as SearchInput } from './SearchInput';
|
||||||
|
export { default as Switch } from './Switch';
|
||||||
export { default as TextArea } from './TextArea';
|
export { default as TextArea } from './TextArea';
|
||||||
|
|
||||||
export type { FieldCounter, FormFieldProps } from './FormField';
|
export type { FieldCounter, FormFieldProps } from './FormField';
|
||||||
export type { InputProps, InputSize, InputState } from './Input';
|
export type { InputProps, InputSize, InputState } from './Input';
|
||||||
export type { PasswordInputProps, PasswordStrength } from './PasswordInput';
|
export type { PasswordInputProps, PasswordStrength } from './PasswordInput';
|
||||||
export type { SearchInputProps } from './SearchInput';
|
export type { SearchInputProps } from './SearchInput';
|
||||||
|
export type { SwitchProps, SwitchSize } from './Switch';
|
||||||
export type { TextAreaProps } from './TextArea';
|
export type { TextAreaProps } from './TextArea';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user