@retray-dev/ui-kit 6.0.0 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMPONENTS.md +8 -7
- package/dist/index.d.mts +47 -23
- package/dist/index.d.ts +47 -23
- package/dist/index.js +702 -634
- package/dist/index.mjs +695 -627
- package/package.json +1 -1
- package/src/components/Accordion/Accordion.tsx +10 -12
- package/src/components/Button/Button.tsx +20 -18
- package/src/components/Card/Card.tsx +21 -33
- package/src/components/CategoryStrip/CategoryStrip.tsx +45 -38
- package/src/components/Checkbox/Checkbox.tsx +31 -50
- package/src/components/Chip/Chip.tsx +34 -71
- package/src/components/DetailRow/DetailRow.tsx +13 -8
- package/src/components/IconButton/IconButton.tsx +20 -18
- package/src/components/Input/Input.tsx +39 -22
- package/src/components/ListItem/ListItem.tsx +22 -34
- package/src/components/MediaCard/MediaCard.tsx +24 -24
- package/src/components/MenuItem/MenuItem.tsx +52 -39
- package/src/components/MonthPicker/MonthPicker.tsx +12 -2
- package/src/components/Pressable/Pressable.tsx +27 -46
- package/src/components/Progress/Progress.tsx +21 -12
- package/src/components/RadioGroup/RadioGroup.tsx +52 -26
- package/src/components/Select/Select.tsx +17 -15
- package/src/components/Sheet/Sheet.tsx +4 -1
- package/src/components/Skeleton/Skeleton.tsx +24 -13
- package/src/components/Slider/Slider.tsx +11 -1
- package/src/components/Switch/Switch.tsx +44 -49
- package/src/components/Tabs/Tabs.tsx +39 -31
- package/src/components/Textarea/Textarea.tsx +29 -12
- package/src/components/Toggle/Toggle.tsx +39 -45
- package/src/utils/animations.ts +58 -0
- package/src/utils/useColorTransition.ts +40 -0
- package/src/utils/usePressScale.ts +73 -0
package/package.json
CHANGED
|
@@ -11,17 +11,13 @@ import Animated, {
|
|
|
11
11
|
useDerivedValue,
|
|
12
12
|
useAnimatedStyle,
|
|
13
13
|
withTiming,
|
|
14
|
-
Easing,
|
|
15
|
-
type EasingFunction,
|
|
16
14
|
} from 'react-native-reanimated'
|
|
17
|
-
|
|
18
|
-
const easingExpand: EasingFunction = Easing.bezier(0.23, 1, 0.32, 1) as unknown as EasingFunction
|
|
19
|
-
const easingCollapse: EasingFunction = Easing.in(Easing.ease)
|
|
20
15
|
import { Entypo } from '@expo/vector-icons'
|
|
21
16
|
import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
22
17
|
import { useTheme } from '../../theme'
|
|
23
18
|
import { s, vs, ms } from '../../utils/scaling'
|
|
24
19
|
import { renderIcon } from '../../utils/icons'
|
|
20
|
+
import { TIMINGS, EASINGS } from '../../utils/animations'
|
|
25
21
|
|
|
26
22
|
export interface AccordionItem {
|
|
27
23
|
value: string
|
|
@@ -71,20 +67,19 @@ function AccordionItemComponent({
|
|
|
71
67
|
isExpanded.value = isOpen
|
|
72
68
|
}, [isOpen])
|
|
73
69
|
|
|
74
|
-
// Derived animated height —
|
|
75
|
-
//
|
|
76
|
-
// withTiming wraps it so every change animates automatically.
|
|
70
|
+
// Derived animated height — height * Number(isExpanded) gives 0 when closed and
|
|
71
|
+
// the measured height when open. `withTiming` wraps it so every change animates.
|
|
77
72
|
const derivedHeight = useDerivedValue(() =>
|
|
78
73
|
withTiming(height.value * Number(isExpanded.value), {
|
|
79
|
-
duration:
|
|
80
|
-
easing: isExpanded.value ?
|
|
74
|
+
duration: isExpanded.value ? TIMINGS.expand.duration : TIMINGS.collapse.duration,
|
|
75
|
+
easing: isExpanded.value ? EASINGS.expand : EASINGS.collapse,
|
|
81
76
|
})
|
|
82
77
|
)
|
|
83
78
|
|
|
84
79
|
const derivedRotation = useDerivedValue(() =>
|
|
85
80
|
withTiming(isExpanded.value ? 1 : 0, {
|
|
86
|
-
duration:
|
|
87
|
-
easing: isExpanded.value ?
|
|
81
|
+
duration: isExpanded.value ? TIMINGS.expand.duration : TIMINGS.collapse.duration,
|
|
82
|
+
easing: isExpanded.value ? EASINGS.expand : EASINGS.collapse,
|
|
88
83
|
})
|
|
89
84
|
)
|
|
90
85
|
|
|
@@ -105,6 +100,9 @@ function AccordionItemComponent({
|
|
|
105
100
|
hapticSelection()
|
|
106
101
|
onToggle()
|
|
107
102
|
}}
|
|
103
|
+
accessibilityRole="button"
|
|
104
|
+
accessibilityState={{ expanded: isOpen }}
|
|
105
|
+
accessibilityLabel={item.trigger}
|
|
108
106
|
>
|
|
109
107
|
<View style={styles.triggerContent}>
|
|
110
108
|
{resolvedIcon ? <View style={styles.icon}>{resolvedIcon}</View> : null}
|
|
@@ -1,22 +1,21 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react'
|
|
2
2
|
import {
|
|
3
3
|
TouchableOpacity,
|
|
4
4
|
Text,
|
|
5
|
-
Animated,
|
|
6
5
|
ActivityIndicator,
|
|
7
6
|
StyleSheet,
|
|
8
7
|
TouchableOpacityProps,
|
|
9
8
|
ViewStyle,
|
|
10
9
|
TextStyle,
|
|
11
|
-
Platform,
|
|
12
10
|
} from 'react-native'
|
|
13
|
-
|
|
14
|
-
const nativeDriver = Platform.OS !== 'web'
|
|
11
|
+
import Animated from 'react-native-reanimated'
|
|
15
12
|
import { impactMedium } from '../../utils/haptics'
|
|
16
13
|
import { useTheme } from '../../theme'
|
|
17
14
|
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
18
15
|
import { renderIcon } from '../../utils/icons'
|
|
19
16
|
import { RADIUS, TYPOGRAPHY } from '../../tokens'
|
|
17
|
+
import { usePressScale } from '../../utils/usePressScale'
|
|
18
|
+
import { PRESS_SCALE } from '../../utils/animations'
|
|
20
19
|
|
|
21
20
|
// primary: filled primary — main CTA (pill-shaped, Airbnb-style)
|
|
22
21
|
// secondary: outlined primary border — alternative actions
|
|
@@ -65,20 +64,16 @@ export function Button({
|
|
|
65
64
|
disabled,
|
|
66
65
|
style,
|
|
67
66
|
onPress,
|
|
67
|
+
accessibilityLabel,
|
|
68
|
+
accessibilityHint,
|
|
68
69
|
...props
|
|
69
70
|
}: ButtonProps) {
|
|
70
71
|
const { colors } = useTheme()
|
|
71
72
|
const isDisabled = disabled || loading
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
Animated.spring(scale, { toValue: 0.95, useNativeDriver: nativeDriver, stiffness: 600, damping: 35, mass: 0.8 }).start()
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const handlePressOut = () => {
|
|
80
|
-
Animated.spring(scale, { toValue: 1, useNativeDriver: nativeDriver, stiffness: 280, damping: 22, mass: 0.8 }).start()
|
|
81
|
-
}
|
|
73
|
+
const { animatedStyle, onPressIn, onPressOut, hoverHandlers } = usePressScale({
|
|
74
|
+
pressScale: PRESS_SCALE.button,
|
|
75
|
+
disabled: isDisabled,
|
|
76
|
+
})
|
|
82
77
|
|
|
83
78
|
const handlePress: TouchableOpacityProps['onPress'] = (e) => {
|
|
84
79
|
impactMedium()
|
|
@@ -116,7 +111,10 @@ export function Button({
|
|
|
116
111
|
const { flex, ...restStyle } = flatStyle || {}
|
|
117
112
|
|
|
118
113
|
return (
|
|
119
|
-
<Animated.View
|
|
114
|
+
<Animated.View
|
|
115
|
+
style={[fullWidth && styles.fullWidth, flex !== undefined && { flex }, animatedStyle]}
|
|
116
|
+
{...hoverHandlers}
|
|
117
|
+
>
|
|
120
118
|
<TouchableOpacity
|
|
121
119
|
style={[
|
|
122
120
|
styles.base,
|
|
@@ -130,8 +128,12 @@ export function Button({
|
|
|
130
128
|
activeOpacity={1}
|
|
131
129
|
touchSoundDisabled={true}
|
|
132
130
|
onPress={handlePress}
|
|
133
|
-
onPressIn={
|
|
134
|
-
onPressOut={
|
|
131
|
+
onPressIn={onPressIn}
|
|
132
|
+
onPressOut={onPressOut}
|
|
133
|
+
accessibilityRole="button"
|
|
134
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
135
|
+
accessibilityHint={accessibilityHint}
|
|
136
|
+
accessibilityState={{ disabled: isDisabled, busy: loading }}
|
|
135
137
|
{...props}
|
|
136
138
|
>
|
|
137
139
|
{loading ? (
|
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import { View, Text, TouchableOpacity,
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Text, TouchableOpacity, StyleSheet, ViewStyle, TextStyle } from 'react-native'
|
|
3
|
+
import Animated from 'react-native-reanimated'
|
|
3
4
|
import { impactLight } from '../../utils/haptics'
|
|
4
5
|
import { useTheme } from '../../theme'
|
|
5
6
|
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
6
7
|
import { RADIUS } from '../../tokens'
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
import { usePressScale } from '../../utils/usePressScale'
|
|
9
|
+
import { SPRINGS, PRESS_SCALE } from '../../utils/animations'
|
|
9
10
|
|
|
10
11
|
export type CardVariant = 'elevated' | 'outlined' | 'filled'
|
|
11
12
|
|
|
12
13
|
export interface CardProps {
|
|
13
14
|
children: React.ReactNode
|
|
14
|
-
/** Visual style variant.
|
|
15
|
+
/** Visual style variant. `'elevated'` (default) has shadow, `'outlined'` has border only, `'filled'` uses accent background. */
|
|
15
16
|
variant?: CardVariant
|
|
16
17
|
/** Makes the card tappable. Adds press animation and haptic feedback. */
|
|
17
18
|
onPress?: () => void
|
|
18
19
|
style?: ViewStyle
|
|
20
|
+
/** Accessibility label for the card (when interactive). */
|
|
21
|
+
accessibilityLabel?: string
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export interface CardHeaderProps {
|
|
@@ -43,31 +46,14 @@ export interface CardFooterProps {
|
|
|
43
46
|
style?: ViewStyle
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
export function Card({ children, variant = 'elevated', onPress, style }: CardProps) {
|
|
49
|
+
export function Card({ children, variant = 'elevated', onPress, style, accessibilityLabel }: CardProps) {
|
|
47
50
|
const { colors } = useTheme()
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
useNativeDriver: nativeDriver,
|
|
55
|
-
stiffness: 400,
|
|
56
|
-
damping: 30,
|
|
57
|
-
mass: 1.0,
|
|
58
|
-
}).start()
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const handlePressOut = () => {
|
|
62
|
-
if (!onPress) return
|
|
63
|
-
Animated.spring(scale, {
|
|
64
|
-
toValue: 1,
|
|
65
|
-
useNativeDriver: nativeDriver,
|
|
66
|
-
stiffness: 250,
|
|
67
|
-
damping: 24,
|
|
68
|
-
mass: 1.0,
|
|
69
|
-
}).start()
|
|
70
|
-
}
|
|
51
|
+
const { animatedStyle, onPressIn, onPressOut, hoverHandlers } = usePressScale({
|
|
52
|
+
pressScale: PRESS_SCALE.card,
|
|
53
|
+
pressInSpring: SPRINGS.surfacePressIn,
|
|
54
|
+
pressOutSpring: SPRINGS.surfacePressOut,
|
|
55
|
+
disabled: !onPress,
|
|
56
|
+
})
|
|
71
57
|
|
|
72
58
|
const handlePress = () => {
|
|
73
59
|
if (!onPress) return
|
|
@@ -107,13 +93,15 @@ export function Card({ children, variant = 'elevated', onPress, style }: CardPro
|
|
|
107
93
|
|
|
108
94
|
if (onPress) {
|
|
109
95
|
return (
|
|
110
|
-
<Animated.View style={
|
|
96
|
+
<Animated.View style={animatedStyle} {...hoverHandlers}>
|
|
111
97
|
<TouchableOpacity
|
|
112
98
|
onPress={handlePress}
|
|
113
|
-
onPressIn={
|
|
114
|
-
onPressOut={
|
|
99
|
+
onPressIn={onPressIn}
|
|
100
|
+
onPressOut={onPressOut}
|
|
115
101
|
activeOpacity={1}
|
|
116
102
|
touchSoundDisabled={true}
|
|
103
|
+
accessibilityRole="button"
|
|
104
|
+
accessibilityLabel={accessibilityLabel}
|
|
117
105
|
>
|
|
118
106
|
{cardContent}
|
|
119
107
|
</TouchableOpacity>
|
|
@@ -150,7 +138,7 @@ export function CardFooter({ children, style }: CardFooterProps) {
|
|
|
150
138
|
|
|
151
139
|
const styles = StyleSheet.create({
|
|
152
140
|
card: {
|
|
153
|
-
borderRadius: RADIUS.md,
|
|
141
|
+
borderRadius: RADIUS.md,
|
|
154
142
|
borderWidth: 1,
|
|
155
143
|
},
|
|
156
144
|
header: {
|
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react'
|
|
2
2
|
import {
|
|
3
3
|
ScrollView,
|
|
4
4
|
TouchableOpacity,
|
|
5
|
-
Animated,
|
|
6
5
|
Text,
|
|
7
6
|
View,
|
|
8
7
|
StyleSheet,
|
|
9
8
|
ViewStyle,
|
|
10
|
-
Platform,
|
|
11
9
|
} from 'react-native'
|
|
10
|
+
import Animated, {
|
|
11
|
+
useAnimatedStyle,
|
|
12
|
+
interpolateColor,
|
|
13
|
+
} from 'react-native-reanimated'
|
|
12
14
|
import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
13
15
|
import { useTheme } from '../../theme'
|
|
14
16
|
import { s, vs, ms } from '../../utils/scaling'
|
|
15
17
|
import { renderIcon } from '../../utils/icons'
|
|
18
|
+
import { usePressScale } from '../../utils/usePressScale'
|
|
19
|
+
import { useColorTransition } from '../../utils/useColorTransition'
|
|
20
|
+
import { PRESS_SCALE } from '../../utils/animations'
|
|
16
21
|
import { RADIUS } from '../../tokens'
|
|
17
22
|
|
|
18
|
-
const nativeDriver = Platform.OS !== 'web'
|
|
19
|
-
|
|
20
23
|
export interface CategoryItem {
|
|
21
24
|
label: string
|
|
22
25
|
value: string
|
|
@@ -36,6 +39,7 @@ export interface CategoryStripProps {
|
|
|
36
39
|
style?: ViewStyle
|
|
37
40
|
/** Style applied to each pill item. */
|
|
38
41
|
itemStyle?: ViewStyle
|
|
42
|
+
accessibilityLabel?: string
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
function CategoryChip({
|
|
@@ -48,52 +52,52 @@ function CategoryChip({
|
|
|
48
52
|
onPress: () => void
|
|
49
53
|
}) {
|
|
50
54
|
const { colors } = useTheme()
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
55
|
+
const { animatedStyle: scaleStyle, onPressIn, onPressOut, hoverHandlers } = usePressScale({
|
|
56
|
+
pressScale: PRESS_SCALE.chip,
|
|
57
|
+
})
|
|
58
|
+
const progress = useColorTransition(selected)
|
|
56
59
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
const surfaceStyle = useAnimatedStyle(() => ({
|
|
61
|
+
backgroundColor: interpolateColor(progress.value, [0, 1], [colors.surface, colors.primary]),
|
|
62
|
+
borderColor: interpolateColor(progress.value, [0, 1], [colors.border, colors.primary]),
|
|
63
|
+
}))
|
|
60
64
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
const textColorStyle = useAnimatedStyle(() => ({
|
|
66
|
+
color: interpolateColor(progress.value, [0, 1], [colors.foregroundSubtle, colors.primaryForeground]),
|
|
67
|
+
}))
|
|
64
68
|
|
|
69
|
+
// Static color for icon — icon families take a static color prop, not animated.
|
|
70
|
+
const iconColor = selected ? colors.primaryForeground : colors.foregroundSubtle
|
|
65
71
|
const resolvedIcon =
|
|
66
72
|
typeof item.icon === 'string'
|
|
67
|
-
? renderIcon(item.icon, 16,
|
|
73
|
+
? renderIcon(item.icon, 16, iconColor)
|
|
68
74
|
: item.icon ?? null
|
|
69
75
|
|
|
70
76
|
return (
|
|
71
|
-
<Animated.View style={
|
|
77
|
+
<Animated.View style={scaleStyle} {...hoverHandlers}>
|
|
72
78
|
<TouchableOpacity
|
|
73
|
-
style={[
|
|
74
|
-
styles.chip,
|
|
75
|
-
{
|
|
76
|
-
backgroundColor: bgColor,
|
|
77
|
-
borderColor,
|
|
78
|
-
},
|
|
79
|
-
]}
|
|
80
79
|
onPress={onPress}
|
|
81
|
-
onPressIn={
|
|
82
|
-
onPressOut={
|
|
80
|
+
onPressIn={onPressIn}
|
|
81
|
+
onPressOut={onPressOut}
|
|
83
82
|
activeOpacity={1}
|
|
84
83
|
touchSoundDisabled={true}
|
|
84
|
+
accessibilityRole="button"
|
|
85
|
+
accessibilityLabel={item.label}
|
|
86
|
+
accessibilityState={{ selected }}
|
|
85
87
|
>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
{
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<
|
|
93
|
-
{
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
88
|
+
<Animated.View style={[styles.chip, surfaceStyle]}>
|
|
89
|
+
{resolvedIcon && <View style={styles.chipIcon}>{resolvedIcon}</View>}
|
|
90
|
+
<Animated.Text style={[styles.chipLabel, textColorStyle]} allowFontScaling={true}>
|
|
91
|
+
{item.label}
|
|
92
|
+
</Animated.Text>
|
|
93
|
+
{item.badge !== undefined && item.badge > 0 && (
|
|
94
|
+
<View style={[styles.chipBadge, { backgroundColor: colors.primary }]}>
|
|
95
|
+
<Text style={[styles.chipBadgeText, { color: colors.primaryForeground }]}>
|
|
96
|
+
{Math.min(item.badge, 99)}
|
|
97
|
+
</Text>
|
|
98
|
+
</View>
|
|
99
|
+
)}
|
|
100
|
+
</Animated.View>
|
|
97
101
|
</TouchableOpacity>
|
|
98
102
|
</Animated.View>
|
|
99
103
|
)
|
|
@@ -106,6 +110,7 @@ export function CategoryStrip({
|
|
|
106
110
|
multiSelect = false,
|
|
107
111
|
style,
|
|
108
112
|
itemStyle,
|
|
113
|
+
accessibilityLabel,
|
|
109
114
|
}: CategoryStripProps) {
|
|
110
115
|
const selected = Array.isArray(value) ? value : value ? [value] : []
|
|
111
116
|
|
|
@@ -128,6 +133,8 @@ export function CategoryStrip({
|
|
|
128
133
|
showsHorizontalScrollIndicator={false}
|
|
129
134
|
contentContainerStyle={[styles.container, style]}
|
|
130
135
|
style={styles.scroll}
|
|
136
|
+
accessibilityRole={multiSelect ? undefined : 'radiogroup'}
|
|
137
|
+
accessibilityLabel={accessibilityLabel}
|
|
131
138
|
>
|
|
132
139
|
{categories.map((cat) => (
|
|
133
140
|
<View key={cat.value} style={itemStyle}>
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import { TouchableOpacity,
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { TouchableOpacity, View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
interpolateColor,
|
|
6
|
+
withTiming,
|
|
7
|
+
} from 'react-native-reanimated'
|
|
3
8
|
import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
4
|
-
|
|
5
|
-
const nativeDriver = Platform.OS !== 'web'
|
|
6
9
|
import { useTheme } from '../../theme'
|
|
7
10
|
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
11
|
+
import { usePressScale } from '../../utils/usePressScale'
|
|
12
|
+
import { useColorTransition } from '../../utils/useColorTransition'
|
|
13
|
+
import { PRESS_SCALE, TIMINGS, EASINGS } from '../../utils/animations'
|
|
8
14
|
|
|
9
15
|
export interface CheckboxProps {
|
|
10
16
|
checked?: boolean
|
|
@@ -12,6 +18,7 @@ export interface CheckboxProps {
|
|
|
12
18
|
label?: string
|
|
13
19
|
disabled?: boolean
|
|
14
20
|
style?: ViewStyle
|
|
21
|
+
accessibilityLabel?: string
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
export function Checkbox({
|
|
@@ -20,45 +27,23 @@ export function Checkbox({
|
|
|
20
27
|
label,
|
|
21
28
|
disabled,
|
|
22
29
|
style,
|
|
30
|
+
accessibilityLabel,
|
|
23
31
|
}: CheckboxProps) {
|
|
24
32
|
const { colors } = useTheme()
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
Animated.parallel([
|
|
31
|
-
Animated.timing(bgOpacity, {
|
|
32
|
-
toValue: checked ? 1 : 0,
|
|
33
|
-
duration: 150,
|
|
34
|
-
useNativeDriver: false,
|
|
35
|
-
}),
|
|
36
|
-
Animated.timing(checkOpacity, {
|
|
37
|
-
toValue: checked ? 1 : 0,
|
|
38
|
-
duration: 120,
|
|
39
|
-
useNativeDriver: false,
|
|
40
|
-
}),
|
|
41
|
-
]).start()
|
|
42
|
-
}, [checked, bgOpacity, checkOpacity])
|
|
43
|
-
|
|
44
|
-
const borderColor = bgOpacity.interpolate({
|
|
45
|
-
inputRange: [0, 1],
|
|
46
|
-
outputRange: [colors.border, colors.primary],
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
const backgroundColor = bgOpacity.interpolate({
|
|
50
|
-
inputRange: [0, 1],
|
|
51
|
-
outputRange: ['transparent', colors.primary],
|
|
33
|
+
const { animatedStyle: scaleStyle, onPressIn, onPressOut } = usePressScale({
|
|
34
|
+
pressScale: PRESS_SCALE.button,
|
|
35
|
+
disabled,
|
|
52
36
|
})
|
|
37
|
+
const progress = useColorTransition(checked)
|
|
53
38
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
39
|
+
const boxStyle = useAnimatedStyle(() => ({
|
|
40
|
+
borderColor: interpolateColor(progress.value, [0, 1], [colors.border, colors.primary]),
|
|
41
|
+
backgroundColor: interpolateColor(progress.value, [0, 1], ['transparent', colors.primary]),
|
|
42
|
+
}))
|
|
58
43
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
}
|
|
44
|
+
const checkStyle = useAnimatedStyle(() => ({
|
|
45
|
+
opacity: withTiming(checked ? 1 : 0, { duration: TIMINGS.state.duration, easing: EASINGS.standard }),
|
|
46
|
+
}))
|
|
62
47
|
|
|
63
48
|
return (
|
|
64
49
|
<TouchableOpacity
|
|
@@ -67,24 +52,20 @@ export function Checkbox({
|
|
|
67
52
|
hapticSelection()
|
|
68
53
|
onCheckedChange?.(!checked)
|
|
69
54
|
}}
|
|
70
|
-
onPressIn={
|
|
71
|
-
onPressOut={
|
|
55
|
+
onPressIn={onPressIn}
|
|
56
|
+
onPressOut={onPressOut}
|
|
72
57
|
disabled={disabled}
|
|
73
58
|
activeOpacity={1}
|
|
74
59
|
touchSoundDisabled={true}
|
|
60
|
+
accessibilityRole="checkbox"
|
|
61
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
62
|
+
accessibilityState={{ checked, disabled: !!disabled }}
|
|
75
63
|
>
|
|
76
|
-
<Animated.View style={
|
|
64
|
+
<Animated.View style={scaleStyle}>
|
|
77
65
|
<Animated.View
|
|
78
|
-
style={[
|
|
79
|
-
styles.box,
|
|
80
|
-
{
|
|
81
|
-
borderColor,
|
|
82
|
-
backgroundColor,
|
|
83
|
-
opacity: disabled ? 0.45 : 1,
|
|
84
|
-
},
|
|
85
|
-
]}
|
|
66
|
+
style={[styles.box, { opacity: disabled ? 0.45 : 1 }, boxStyle]}
|
|
86
67
|
>
|
|
87
|
-
<Animated.View style={
|
|
68
|
+
<Animated.View style={checkStyle}>
|
|
88
69
|
<View style={[styles.checkmark, { borderColor: colors.primaryForeground }]} />
|
|
89
70
|
</Animated.View>
|
|
90
71
|
</Animated.View>
|
|
@@ -1,20 +1,16 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
StyleSheet,
|
|
8
|
-
ViewStyle,
|
|
9
|
-
Platform,
|
|
10
|
-
Easing,
|
|
11
|
-
} from 'react-native'
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { TouchableOpacity, View, StyleSheet, ViewStyle } from 'react-native'
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
interpolateColor,
|
|
6
|
+
} from 'react-native-reanimated'
|
|
12
7
|
import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
13
8
|
import { useTheme } from '../../theme'
|
|
14
9
|
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
15
10
|
import { renderIcon } from '../../utils/icons'
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
import { usePressScale } from '../../utils/usePressScale'
|
|
12
|
+
import { useColorTransition } from '../../utils/useColorTransition'
|
|
13
|
+
import { PRESS_SCALE } from '../../utils/animations'
|
|
18
14
|
|
|
19
15
|
export interface ChipProps {
|
|
20
16
|
label: string
|
|
@@ -25,6 +21,7 @@ export interface ChipProps {
|
|
|
25
21
|
/** Icon name from @expo/vector-icons resolved automatically. */
|
|
26
22
|
iconName?: string
|
|
27
23
|
style?: ViewStyle
|
|
24
|
+
accessibilityLabel?: string
|
|
28
25
|
}
|
|
29
26
|
|
|
30
27
|
export interface ChipOption {
|
|
@@ -46,74 +43,46 @@ export interface ChipGroupProps {
|
|
|
46
43
|
style?: ViewStyle
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
export function Chip({ label, selected = false, onPress, icon, iconName, style }: ChipProps) {
|
|
46
|
+
export function Chip({ label, selected = false, onPress, icon, iconName, style, accessibilityLabel }: ChipProps) {
|
|
50
47
|
const { colors } = useTheme()
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
Animated.timing(pressAnim, {
|
|
56
|
-
toValue: selected ? 1 : 0,
|
|
57
|
-
duration: 150,
|
|
58
|
-
easing: Easing.out(Easing.ease),
|
|
59
|
-
useNativeDriver: false,
|
|
60
|
-
}).start()
|
|
61
|
-
}, [selected, pressAnim])
|
|
62
|
-
|
|
63
|
-
const handlePressIn = () => {
|
|
64
|
-
Animated.spring(scale, {
|
|
65
|
-
toValue: 0.95,
|
|
66
|
-
useNativeDriver: nativeDriver,
|
|
67
|
-
speed: 40,
|
|
68
|
-
bounciness: 0,
|
|
69
|
-
}).start()
|
|
70
|
-
}
|
|
48
|
+
const { animatedStyle: scaleStyle, onPressIn, onPressOut, hoverHandlers } = usePressScale({
|
|
49
|
+
pressScale: PRESS_SCALE.chip,
|
|
50
|
+
})
|
|
51
|
+
const colorProgress = useColorTransition(selected)
|
|
71
52
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
53
|
+
const surfaceStyle = useAnimatedStyle(() => ({
|
|
54
|
+
backgroundColor: interpolateColor(colorProgress.value, [0, 1], [colors.surface, colors.primary]),
|
|
55
|
+
borderColor: interpolateColor(colorProgress.value, [0, 1], [colors.border, colors.primary]),
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
const textStyle = useAnimatedStyle(() => ({
|
|
59
|
+
color: interpolateColor(colorProgress.value, [0, 1], [colors.foreground, colors.primaryForeground]),
|
|
60
|
+
}))
|
|
80
61
|
|
|
81
62
|
const handlePress = () => {
|
|
82
63
|
hapticSelection()
|
|
83
64
|
onPress?.()
|
|
84
65
|
}
|
|
85
66
|
|
|
86
|
-
const backgroundColor = pressAnim.interpolate({
|
|
87
|
-
inputRange: [0, 1],
|
|
88
|
-
outputRange: [colors.surface, colors.primary],
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
const textColor = pressAnim.interpolate({
|
|
92
|
-
inputRange: [0, 1],
|
|
93
|
-
outputRange: [colors.foreground, colors.primaryForeground],
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
const borderColor = pressAnim.interpolate({
|
|
97
|
-
inputRange: [0, 1],
|
|
98
|
-
outputRange: [colors.border, colors.primary],
|
|
99
|
-
})
|
|
100
|
-
|
|
101
67
|
const resolvedIcon = iconName
|
|
102
68
|
? renderIcon(iconName, ms(13), selected ? colors.primaryForeground : colors.foreground)
|
|
103
69
|
: icon
|
|
104
70
|
|
|
105
71
|
return (
|
|
106
|
-
<Animated.View style={[styles.wrapper,
|
|
72
|
+
<Animated.View style={[styles.wrapper, scaleStyle, style]} {...hoverHandlers}>
|
|
107
73
|
<TouchableOpacity
|
|
108
74
|
onPress={handlePress}
|
|
109
|
-
onPressIn={
|
|
110
|
-
onPressOut={
|
|
75
|
+
onPressIn={onPressIn}
|
|
76
|
+
onPressOut={onPressOut}
|
|
111
77
|
activeOpacity={1}
|
|
112
78
|
touchSoundDisabled={true}
|
|
79
|
+
accessibilityRole="button"
|
|
80
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
81
|
+
accessibilityState={{ selected }}
|
|
113
82
|
>
|
|
114
|
-
<Animated.View style={[styles.chip,
|
|
83
|
+
<Animated.View style={[styles.chip, surfaceStyle]}>
|
|
115
84
|
{resolvedIcon ? <View style={styles.chipIcon}>{resolvedIcon}</View> : null}
|
|
116
|
-
<Animated.Text style={[styles.label,
|
|
85
|
+
<Animated.Text style={[styles.label, textStyle]} allowFontScaling={true}>
|
|
117
86
|
{label}
|
|
118
87
|
</Animated.Text>
|
|
119
88
|
</Animated.View>
|
|
@@ -129,18 +98,12 @@ export function ChipGroup({ options, value, onValueChange, multiSelect = false,
|
|
|
129
98
|
return
|
|
130
99
|
}
|
|
131
100
|
|
|
132
|
-
// Multiselect logic
|
|
133
101
|
const currentArray = Array.isArray(value) ? value : value ? [value] : []
|
|
134
102
|
const isSelected = currentArray.includes(optionValue)
|
|
135
103
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
newArray = currentArray.filter((v) => v !== optionValue)
|
|
140
|
-
} else {
|
|
141
|
-
// Add to selection
|
|
142
|
-
newArray = [...currentArray, optionValue]
|
|
143
|
-
}
|
|
104
|
+
const newArray: (string | number)[] = isSelected
|
|
105
|
+
? currentArray.filter((v) => v !== optionValue)
|
|
106
|
+
: [...currentArray, optionValue]
|
|
144
107
|
|
|
145
108
|
onValueChange?.(newArray)
|
|
146
109
|
}
|