@hzblj/kara-components 0.10.0 → 0.12.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/CHANGELOG.md +55 -0
- package/lib/module/atoms/activity-indicator/activity-indicator.android.js +6 -1
- package/lib/module/atoms/activity-indicator/activity-indicator.android.js.map +1 -1
- package/lib/module/atoms/activity-indicator/activity-indicator.ios.js +2 -0
- package/lib/module/atoms/activity-indicator/activity-indicator.ios.js.map +1 -1
- package/lib/module/atoms/avatar/avatar.js +5 -0
- package/lib/module/atoms/avatar/avatar.js.map +1 -1
- package/lib/module/atoms/box/box.js +3 -0
- package/lib/module/atoms/box/box.js.map +1 -1
- package/lib/module/atoms/box/index.js +0 -1
- package/lib/module/atoms/box/index.js.map +1 -1
- package/lib/module/atoms/button/button-tokens.js +2 -2
- package/lib/module/atoms/button/button-tokens.js.map +1 -1
- package/lib/module/atoms/button/button.android.js +6 -2
- package/lib/module/atoms/button/button.android.js.map +1 -1
- package/lib/module/atoms/button/button.ios.js +7 -2
- package/lib/module/atoms/button/button.ios.js.map +1 -1
- package/lib/module/atoms/card/card.android.js +5 -3
- package/lib/module/atoms/card/card.android.js.map +1 -1
- package/lib/module/atoms/card/card.ios.js +7 -3
- package/lib/module/atoms/card/card.ios.js.map +1 -1
- package/lib/module/atoms/checkbox/checkbox.android.js +1 -1
- package/lib/module/atoms/checkbox/checkbox.android.js.map +1 -1
- package/lib/module/atoms/fab/fab.android.js +9 -1
- package/lib/module/atoms/fab/fab.android.js.map +1 -1
- package/lib/module/atoms/fab/fab.ios.js +9 -3
- package/lib/module/atoms/fab/fab.ios.js.map +1 -1
- package/lib/module/atoms/glass-view/glass-view.android.js +1 -1
- package/lib/module/atoms/glass-view/glass-view.android.js.map +1 -1
- package/lib/module/atoms/h-stack/h-stack.ios.js +5 -3
- package/lib/module/atoms/h-stack/h-stack.ios.js.map +1 -1
- package/lib/module/atoms/icon-button/icon-button.android.js +9 -0
- package/lib/module/atoms/icon-button/icon-button.android.js.map +1 -1
- package/lib/module/atoms/icon-button/icon-button.ios.js +9 -2
- package/lib/module/atoms/icon-button/icon-button.ios.js.map +1 -1
- package/lib/module/atoms/image/image.js +4 -4
- package/lib/module/atoms/image/image.js.map +1 -1
- package/lib/module/atoms/index.js +1 -0
- package/lib/module/atoms/index.js.map +1 -1
- package/lib/module/atoms/pressable/pressable.js +75 -27
- package/lib/module/atoms/pressable/pressable.js.map +1 -1
- package/lib/module/atoms/progress/progress.android.js +7 -0
- package/lib/module/atoms/progress/progress.android.js.map +1 -1
- package/lib/module/atoms/progress/progress.ios.js +6 -1
- package/lib/module/atoms/progress/progress.ios.js.map +1 -1
- package/lib/module/atoms/rn-text/rn-text.js +66 -26
- package/lib/module/atoms/rn-text/rn-text.js.map +1 -1
- package/lib/module/atoms/segmented-control/segmented-control.android.js +1 -1
- package/lib/module/atoms/segmented-control/segmented-control.android.js.map +1 -1
- package/lib/module/atoms/segmented-control/segmented-control.ios.js +1 -1
- package/lib/module/atoms/segmented-control/segmented-control.ios.js.map +1 -1
- package/lib/module/atoms/skeleton/skeleton.js +22 -8
- package/lib/module/atoms/skeleton/skeleton.js.map +1 -1
- package/lib/module/atoms/stagger/index.js +5 -0
- package/lib/module/atoms/stagger/index.js.map +1 -0
- package/lib/module/atoms/stagger/stagger.js +102 -0
- package/lib/module/atoms/stagger/stagger.js.map +1 -0
- package/lib/module/atoms/stagger/stagger.types.js +4 -0
- package/lib/module/atoms/stagger/stagger.types.js.map +1 -0
- package/lib/module/atoms/surface/surface.android.js +5 -3
- package/lib/module/atoms/surface/surface.android.js.map +1 -1
- package/lib/module/atoms/surface/surface.ios.js +7 -3
- package/lib/module/atoms/surface/surface.ios.js.map +1 -1
- package/lib/module/atoms/switch/switch.android.js +1 -1
- package/lib/module/atoms/switch/switch.android.js.map +1 -1
- package/lib/module/atoms/text/text.android.js +5 -3
- package/lib/module/atoms/text/text.android.js.map +1 -1
- package/lib/module/atoms/text/text.ios.js +6 -4
- package/lib/module/atoms/text/text.ios.js.map +1 -1
- package/lib/module/atoms/v-stack/v-stack.ios.js +5 -3
- package/lib/module/atoms/v-stack/v-stack.ios.js.map +1 -1
- package/lib/module/atoms/z-stack/z-stack.ios.js +5 -3
- package/lib/module/atoms/z-stack/z-stack.ios.js.map +1 -1
- package/lib/module/glyph/glyph.android.js +2 -0
- package/lib/module/glyph/glyph.android.js.map +1 -1
- package/lib/module/glyph/glyph.ios.js +5 -2
- package/lib/module/glyph/glyph.ios.js.map +1 -1
- package/lib/module/molecules/collapsible/collapsible.android.js +18 -8
- package/lib/module/molecules/collapsible/collapsible.android.js.map +1 -1
- package/lib/module/molecules/labeled-content/labeled-content.js +4 -1
- package/lib/module/molecules/labeled-content/labeled-content.js.map +1 -1
- package/lib/module/molecules/link/link.js +4 -1
- package/lib/module/molecules/link/link.js.map +1 -1
- package/lib/module/molecules/list-item/list-item.js +4 -1
- package/lib/module/molecules/list-item/list-item.js.map +1 -1
- package/lib/module/molecules/navigation-bar/navigation-bar.android.js +1 -1
- package/lib/module/molecules/navigation-bar/navigation-bar.android.js.map +1 -1
- package/lib/module/molecules/navigation-bar/navigation-bar.ios.js +1 -1
- package/lib/module/molecules/navigation-bar/navigation-bar.ios.js.map +1 -1
- package/lib/module/molecules/rn-list-item/rn-list-item.js +10 -2
- package/lib/module/molecules/rn-list-item/rn-list-item.js.map +1 -1
- package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js +5 -6
- package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js.map +1 -1
- package/lib/module/molecules/stepper/stepper.ios.js +6 -2
- package/lib/module/molecules/stepper/stepper.ios.js.map +1 -1
- package/lib/module/organisms/fab-menu/fab-menu.android.js +8 -5
- package/lib/module/organisms/fab-menu/fab-menu.android.js.map +1 -1
- package/lib/module/organisms/fab-menu/fab-menu.ios.js +5 -2
- package/lib/module/organisms/fab-menu/fab-menu.ios.js.map +1 -1
- package/lib/module/organisms/snackbar/snackbar.js +155 -18
- package/lib/module/organisms/snackbar/snackbar.js.map +1 -1
- package/lib/module/organisms/swipe-actions/swipe-actions.android.js +1 -1
- package/lib/module/organisms/swipe-actions/swipe-actions.android.js.map +1 -1
- package/lib/module/use-stack-modifiers.js +10 -8
- package/lib/module/use-stack-modifiers.js.map +1 -1
- package/lib/typescript/atoms/activity-indicator/activity-indicator.types.d.ts +2 -0
- package/lib/typescript/atoms/avatar/avatar.types.d.ts +5 -0
- package/lib/typescript/atoms/box/box.d.ts +3 -0
- package/lib/typescript/atoms/box/index.d.ts +0 -1
- package/lib/typescript/atoms/button/button-tokens.d.ts +1 -1
- package/lib/typescript/atoms/button/button.types.d.ts +2 -0
- package/lib/typescript/atoms/card/card.types.d.ts +2 -2
- package/lib/typescript/atoms/fab/fab.types.d.ts +8 -1
- package/lib/typescript/atoms/h-stack/h-stack.types.d.ts +6 -3
- package/lib/typescript/atoms/icon-button/icon-button.types.d.ts +7 -0
- package/lib/typescript/atoms/image/image.d.ts +2 -2
- package/lib/typescript/atoms/index.d.ts +1 -0
- package/lib/typescript/atoms/pressable/pressable.types.d.ts +10 -9
- package/lib/typescript/atoms/progress/progress.types.d.ts +2 -0
- package/lib/typescript/atoms/rn-text/rn-text.types.d.ts +7 -4
- package/lib/typescript/atoms/skeleton/skeleton.types.d.ts +3 -2
- package/lib/typescript/atoms/stagger/index.d.ts +2 -0
- package/lib/typescript/atoms/stagger/stagger.d.ts +2 -0
- package/lib/typescript/atoms/stagger/stagger.types.d.ts +32 -0
- package/lib/typescript/atoms/surface/surface.types.d.ts +2 -2
- package/lib/typescript/atoms/text/text.types.d.ts +2 -2
- package/lib/typescript/atoms/v-stack/v-stack.types.d.ts +3 -3
- package/lib/typescript/atoms/z-stack/z-stack.types.d.ts +3 -3
- package/lib/typescript/glyph/glyph.types.d.ts +2 -0
- package/lib/typescript/molecules/labeled-content/labeled-content.types.d.ts +4 -2
- package/lib/typescript/molecules/link/link.types.d.ts +2 -1
- package/lib/typescript/molecules/list-item/list-item.types.d.ts +5 -3
- package/lib/typescript/molecules/stepper/stepper.ios.d.ts +1 -1
- package/lib/typescript/organisms/fab-menu/fab-menu.types.d.ts +5 -0
- package/lib/typescript/organisms/snackbar/snackbar.types.d.ts +35 -4
- package/lib/typescript/use-stack-modifiers.d.ts +6 -5
- package/package.json +4 -4
- package/src/atoms/activity-indicator/activity-indicator.android.tsx +7 -1
- package/src/atoms/activity-indicator/activity-indicator.ios.tsx +9 -1
- package/src/atoms/activity-indicator/activity-indicator.types.ts +2 -0
- package/src/atoms/avatar/avatar.tsx +5 -1
- package/src/atoms/avatar/avatar.types.ts +5 -0
- package/src/atoms/box/box.tsx +3 -0
- package/src/atoms/box/index.ts +0 -1
- package/src/atoms/button/button-tokens.ts +2 -2
- package/src/atoms/button/button.android.tsx +5 -3
- package/src/atoms/button/button.ios.tsx +10 -2
- package/src/atoms/button/button.types.ts +2 -0
- package/src/atoms/card/card.android.tsx +6 -4
- package/src/atoms/card/card.ios.tsx +7 -3
- package/src/atoms/card/card.types.ts +8 -7
- package/src/atoms/checkbox/checkbox.android.tsx +1 -1
- package/src/atoms/fab/fab.android.tsx +5 -3
- package/src/atoms/fab/fab.ios.tsx +8 -4
- package/src/atoms/fab/fab.types.ts +8 -1
- package/src/atoms/glass-view/glass-view.android.tsx +1 -1
- package/src/atoms/h-stack/h-stack.ios.tsx +6 -4
- package/src/atoms/h-stack/h-stack.types.ts +17 -13
- package/src/atoms/icon-button/icon-button.android.tsx +16 -3
- package/src/atoms/icon-button/icon-button.ios.tsx +13 -4
- package/src/atoms/icon-button/icon-button.types.ts +7 -0
- package/src/atoms/image/image.tsx +3 -3
- package/src/atoms/index.ts +1 -0
- package/src/atoms/pressable/pressable.tsx +164 -100
- package/src/atoms/pressable/pressable.types.ts +32 -29
- package/src/atoms/progress/progress.android.tsx +5 -3
- package/src/atoms/progress/progress.ios.tsx +5 -2
- package/src/atoms/progress/progress.types.ts +2 -0
- package/src/atoms/rn-text/rn-text.tsx +101 -32
- package/src/atoms/rn-text/rn-text.types.ts +7 -3
- package/src/atoms/segmented-control/segmented-control.android.tsx +1 -1
- package/src/atoms/segmented-control/segmented-control.ios.tsx +1 -1
- package/src/atoms/skeleton/skeleton.tsx +20 -6
- package/src/atoms/skeleton/skeleton.types.ts +3 -2
- package/src/atoms/stagger/index.ts +2 -0
- package/src/atoms/stagger/stagger.tsx +98 -0
- package/src/atoms/stagger/stagger.types.ts +35 -0
- package/src/atoms/surface/surface.android.tsx +6 -4
- package/src/atoms/surface/surface.ios.tsx +7 -3
- package/src/atoms/surface/surface.types.ts +10 -9
- package/src/atoms/switch/switch.android.tsx +1 -1
- package/src/atoms/text/text.android.tsx +6 -4
- package/src/atoms/text/text.ios.tsx +6 -4
- package/src/atoms/text/text.types.ts +23 -22
- package/src/atoms/v-stack/v-stack.ios.tsx +6 -4
- package/src/atoms/v-stack/v-stack.types.ts +17 -16
- package/src/atoms/z-stack/z-stack.ios.tsx +6 -4
- package/src/atoms/z-stack/z-stack.types.ts +15 -14
- package/src/glyph/glyph.android.tsx +14 -2
- package/src/glyph/glyph.ios.tsx +13 -3
- package/src/glyph/glyph.types.ts +2 -0
- package/src/molecules/collapsible/collapsible.android.tsx +27 -7
- package/src/molecules/labeled-content/labeled-content.tsx +12 -1
- package/src/molecules/labeled-content/labeled-content.types.ts +4 -2
- package/src/molecules/link/link.tsx +2 -1
- package/src/molecules/link/link.types.ts +2 -1
- package/src/molecules/list-item/list-item.tsx +20 -2
- package/src/molecules/list-item/list-item.types.ts +5 -3
- package/src/molecules/navigation-bar/navigation-bar.android.tsx +1 -1
- package/src/molecules/navigation-bar/navigation-bar.ios.tsx +1 -1
- package/src/molecules/rn-list-item/rn-list-item.tsx +18 -3
- package/src/molecules/scroll-edge-container/scroll-edge-container.android.tsx +5 -4
- package/src/molecules/stepper/stepper.ios.tsx +8 -2
- package/src/organisms/fab-menu/fab-menu.android.tsx +9 -5
- package/src/organisms/fab-menu/fab-menu.ios.tsx +6 -2
- package/src/organisms/fab-menu/fab-menu.types.ts +5 -0
- package/src/organisms/snackbar/snackbar.tsx +125 -21
- package/src/organisms/snackbar/snackbar.types.ts +38 -4
- package/src/organisms/swipe-actions/swipe-actions.android.tsx +1 -1
- package/src/use-stack-modifiers.ts +20 -7
- package/lib/module/atoms/box/a-box.js +0 -6
- package/lib/module/atoms/box/a-box.js.map +0 -1
- package/lib/typescript/atoms/box/a-box.d.ts +0 -49
- package/src/atoms/box/a-box.ts +0 -5
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
type AnimatableKey,
|
|
3
|
+
accessibilityProps,
|
|
4
|
+
markMotionComponent,
|
|
5
|
+
type SxAnimate,
|
|
6
|
+
splitAccessibility,
|
|
7
|
+
spring,
|
|
8
|
+
timing,
|
|
9
|
+
useSplitSxMotion,
|
|
10
|
+
} from '@hzblj/kara-sx'
|
|
11
|
+
import {opacity, type SpringName, springs, useAppTheme, useReducedMotion, withOpacity} from '@hzblj/kara-theme'
|
|
3
12
|
import {useCallback, useMemo} from 'react'
|
|
4
13
|
import {Platform, Pressable as RNPressable, type ViewStyle} from 'react-native'
|
|
5
14
|
import Animated, {useAnimatedStyle, useSharedValue, withSpring, withTiming} from 'react-native-reanimated'
|
|
@@ -8,116 +17,171 @@ import {haptics} from '../../haptics'
|
|
|
8
17
|
import {splitForRipple} from '../../ripple-clip'
|
|
9
18
|
import type {PressableComponent, PressableProps} from './pressable.types'
|
|
10
19
|
|
|
11
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The scale follows the finger on springs, as UIKit's own presses do: down on `interactive`, back on `smooth`, neither
|
|
22
|
+
* with a bounce (a press is not a toy). A spring keeps the speed it has, so a tap let go halfway down eases back instead
|
|
23
|
+
* of turning on the spot, which a timed curve restarted from rest cannot do.
|
|
24
|
+
*/
|
|
25
|
+
const SCALE_IN = spring('interactive')
|
|
26
|
+
const SCALE_OUT = spring('smooth')
|
|
12
27
|
|
|
13
|
-
/** A dim comes in
|
|
14
|
-
const DIM_IN =
|
|
15
|
-
const DIM_OUT =
|
|
28
|
+
/** A dim comes in at once and leaves a little slower, as UIKit's own highlight does. */
|
|
29
|
+
const DIM_IN = timing('micro', 'out')
|
|
30
|
+
const DIM_OUT = timing('quick', 'out')
|
|
16
31
|
|
|
17
32
|
const PLATFORM_EFFECT = Platform.OS === 'ios' ? 'scale' : 'none'
|
|
18
33
|
|
|
19
34
|
const AnimatedPressable = Animated.createAnimatedComponent(RNPressable)
|
|
20
35
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
disabled = false,
|
|
24
|
-
haptic = false,
|
|
25
|
-
onPress,
|
|
26
|
-
pressEffect = PLATFORM_EFFECT,
|
|
27
|
-
pressedOpacity = 'o60',
|
|
28
|
-
pressedScale = 0.96,
|
|
29
|
-
ripple: withRipple = true,
|
|
30
|
-
testID,
|
|
31
|
-
...props
|
|
32
|
-
}: PressableProps) => {
|
|
33
|
-
const [resolved, rest] = useSplitSx(props)
|
|
34
|
-
// 0 at rest, 1 held: one value drives whichever effect is picked, so a change of effect needs no second animation.
|
|
35
|
-
const pressed = useSharedValue(0)
|
|
36
|
-
const feedback = haptic === true ? 'light' : haptic || undefined
|
|
37
|
-
const dimmed = opacity[pressedOpacity]
|
|
38
|
-
|
|
39
|
-
const theme = useAppTheme()
|
|
40
|
-
const isAndroid = Platform.OS === 'android'
|
|
41
|
-
|
|
42
|
-
const rStyle = useAnimatedStyle(() => {
|
|
43
|
-
if (pressEffect === 'scale') {
|
|
44
|
-
return {transform: [{scale: 1 - (1 - pressedScale) * pressed.value}]}
|
|
45
|
-
}
|
|
36
|
+
/** What `animate` covers by default, less the transform. */
|
|
37
|
+
const VISUAL_KEYS: readonly AnimatableKey[] = ['bg', 'borderColor', 'opacity']
|
|
46
38
|
|
|
47
|
-
|
|
48
|
-
}, [dimmed, pressEffect, pressedScale])
|
|
49
|
-
|
|
50
|
-
// Android's press is Material's: a ripple over the content (the pressed state layer, `onSurface` at 10 %) clipped to
|
|
51
|
-
// the shape by an outer view (`splitForRipple`). The effect, when there is one, moves that outer view, ripple and all.
|
|
52
|
-
const ripple = useMemo(
|
|
53
|
-
() =>
|
|
54
|
-
isAndroid && withRipple
|
|
55
|
-
? {color: withOpacity(String(theme.color('content.primary')), 'o10'), foreground: true}
|
|
56
|
-
: undefined,
|
|
57
|
-
[isAndroid, theme, withRipple]
|
|
58
|
-
)
|
|
59
|
-
const androidStyle = useMemo(
|
|
60
|
-
() => (isAndroid ? splitForRipple(resolved as ViewStyle) : undefined),
|
|
61
|
-
[isAndroid, resolved]
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
// The finger is down: the Taptic Engine has until the release to wake, so the haptic lands with the press.
|
|
65
|
-
const onPressIn = useCallback(() => {
|
|
66
|
-
if (pressEffect === 'scale') {
|
|
67
|
-
pressed.value = withSpring(1, SPRING)
|
|
68
|
-
} else if (pressEffect === 'opacity') {
|
|
69
|
-
pressed.value = withTiming(1, DIM_IN)
|
|
70
|
-
}
|
|
39
|
+
const TRANSFORM_KEYS = new Set<AnimatableKey>(['offsetX', 'offsetY', 'rotate', 'scale'])
|
|
71
40
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The press scale owns the transform, so `animate` leaves it alone: a transition on the same transform would take it
|
|
43
|
+
* back from the press at every re-render. A duration or spring token keeps its timing.
|
|
44
|
+
*/
|
|
45
|
+
const withoutTransform = (animate: SxAnimate | undefined): SxAnimate | undefined => {
|
|
46
|
+
if (animate === undefined || animate === false) {
|
|
47
|
+
return animate
|
|
48
|
+
}
|
|
76
49
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
50
|
+
if (animate === true) {
|
|
51
|
+
return {keys: VISUAL_KEYS}
|
|
52
|
+
}
|
|
80
53
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
54
|
+
if (typeof animate === 'string') {
|
|
55
|
+
return animate in springs
|
|
56
|
+
? {keys: VISUAL_KEYS, spring: animate as SpringName}
|
|
57
|
+
: {duration: animate as Exclude<typeof animate, SpringName>, keys: VISUAL_KEYS}
|
|
58
|
+
}
|
|
85
59
|
|
|
86
|
-
|
|
87
|
-
|
|
60
|
+
return {...animate, keys: (animate.keys ?? VISUAL_KEYS).filter(key => !TRANSFORM_KEYS.has(key))}
|
|
61
|
+
}
|
|
88
62
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
63
|
+
export const Pressable: PressableComponent = markMotionComponent(
|
|
64
|
+
({
|
|
65
|
+
children,
|
|
66
|
+
disabled = false,
|
|
67
|
+
haptic = false,
|
|
68
|
+
onPress,
|
|
69
|
+
pressEffect = PLATFORM_EFFECT,
|
|
70
|
+
pressedOpacity = 'o60',
|
|
71
|
+
pressedScale = 0.96,
|
|
72
|
+
ripple: withRipple = true,
|
|
73
|
+
testID,
|
|
74
|
+
...props
|
|
75
|
+
}: PressableProps) => {
|
|
76
|
+
const [accessibility, ownProps] = splitAccessibility(props)
|
|
77
|
+
const reduced = useReducedMotion()
|
|
78
|
+
// With motion reduced the press still shows, and nothing moves: a scale dims instead.
|
|
79
|
+
const effect = reduced && pressEffect === 'scale' ? 'opacity' : pressEffect
|
|
80
|
+
// Always a Reanimated view (the press effect is one), so the motion props need no latch here.
|
|
81
|
+
const [resolved, motion, rest] = useSplitSxMotion(
|
|
82
|
+
effect === 'scale' && ownProps.animate !== undefined
|
|
83
|
+
? {...ownProps, animate: withoutTransform(ownProps.animate as SxAnimate)}
|
|
84
|
+
: ownProps
|
|
85
|
+
)
|
|
86
|
+
// 0 at rest, 1 held: one value drives whichever effect is picked, so a change of effect needs no second animation.
|
|
87
|
+
const pressed = useSharedValue(0)
|
|
88
|
+
const feedback = haptic === true ? 'light' : haptic || undefined
|
|
89
|
+
const a11y = accessibilityProps(accessibility, 'button')
|
|
90
|
+
const dimmed = opacity[pressedOpacity]
|
|
91
|
+
|
|
92
|
+
const theme = useAppTheme()
|
|
93
|
+
const isAndroid = Platform.OS === 'android'
|
|
94
|
+
|
|
95
|
+
const rStyle = useAnimatedStyle(() => {
|
|
96
|
+
if (effect === 'scale') {
|
|
97
|
+
return {transform: [{scale: 1 - (1 - pressedScale) * pressed.value}]}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return effect === 'opacity' ? {opacity: 1 - (1 - dimmed) * pressed.value} : {}
|
|
101
|
+
}, [dimmed, effect, pressedScale])
|
|
102
|
+
|
|
103
|
+
// Android's press is Material's: a ripple over the content (the pressed state layer, `onSurface` at 10 %) clipped to
|
|
104
|
+
// the shape by an outer view (`splitForRipple`). The effect, when there is one, moves that outer view, ripple and all.
|
|
105
|
+
const ripple = useMemo(
|
|
106
|
+
() =>
|
|
107
|
+
isAndroid && withRipple
|
|
108
|
+
? {color: withOpacity(String(theme.color('content.primary')), 'o10'), foreground: true}
|
|
109
|
+
: undefined,
|
|
110
|
+
[isAndroid, theme, withRipple]
|
|
111
|
+
)
|
|
112
|
+
const androidStyle = useMemo(
|
|
113
|
+
() => (isAndroid ? splitForRipple(resolved as ViewStyle) : undefined),
|
|
114
|
+
[isAndroid, resolved]
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
// The finger is down: the Taptic Engine has until the release to wake, so the haptic lands with the press.
|
|
118
|
+
const onPressIn = useCallback(() => {
|
|
119
|
+
if (effect === 'scale') {
|
|
120
|
+
pressed.value = withSpring(1, SCALE_IN)
|
|
121
|
+
} else if (effect === 'opacity') {
|
|
122
|
+
pressed.value = withTiming(1, DIM_IN)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (feedback) {
|
|
126
|
+
haptics.prepare(feedback)
|
|
127
|
+
}
|
|
128
|
+
}, [effect, feedback, pressed])
|
|
129
|
+
|
|
130
|
+
const onPressOut = useCallback(() => {
|
|
131
|
+
pressed.value = effect === 'scale' ? withSpring(0, SCALE_OUT) : withTiming(0, DIM_OUT)
|
|
132
|
+
}, [effect, pressed])
|
|
133
|
+
|
|
134
|
+
const onPressWithHaptic = useCallback(() => {
|
|
135
|
+
if (feedback) {
|
|
136
|
+
haptics.play(feedback)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
onPress?.()
|
|
140
|
+
}, [feedback, onPress])
|
|
141
|
+
|
|
142
|
+
if (androidStyle) {
|
|
143
|
+
return (
|
|
144
|
+
<Animated.View
|
|
145
|
+
entering={motion.entering}
|
|
146
|
+
exiting={motion.exiting}
|
|
147
|
+
layout={motion.layout}
|
|
148
|
+
style={[androidStyle.outer, motion.style, rStyle]}
|
|
102
149
|
>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
150
|
+
<RNPressable
|
|
151
|
+
{...a11y}
|
|
152
|
+
android_ripple={ripple}
|
|
153
|
+
disabled={disabled}
|
|
154
|
+
hitSlop={8}
|
|
155
|
+
onPress={onPressWithHaptic}
|
|
156
|
+
onPressIn={onPressIn}
|
|
157
|
+
onPressOut={onPressOut}
|
|
158
|
+
style={androidStyle.inner}
|
|
159
|
+
testID={testID}
|
|
160
|
+
{...rest}
|
|
161
|
+
>
|
|
162
|
+
{children}
|
|
163
|
+
</RNPressable>
|
|
164
|
+
</Animated.View>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<AnimatedPressable
|
|
170
|
+
{...a11y}
|
|
171
|
+
disabled={disabled}
|
|
172
|
+
entering={motion.entering}
|
|
173
|
+
exiting={motion.exiting}
|
|
174
|
+
hitSlop={8}
|
|
175
|
+
layout={motion.layout}
|
|
176
|
+
onPress={onPressWithHaptic}
|
|
177
|
+
onPressIn={onPressIn}
|
|
178
|
+
onPressOut={onPressOut}
|
|
179
|
+
style={[resolved, motion.style, rStyle]}
|
|
180
|
+
testID={testID}
|
|
181
|
+
{...rest}
|
|
182
|
+
>
|
|
183
|
+
{children}
|
|
184
|
+
</AnimatedPressable>
|
|
106
185
|
)
|
|
107
186
|
}
|
|
108
|
-
|
|
109
|
-
return (
|
|
110
|
-
<AnimatedPressable
|
|
111
|
-
disabled={disabled}
|
|
112
|
-
hitSlop={8}
|
|
113
|
-
onPress={onPressWithHaptic}
|
|
114
|
-
onPressIn={onPressIn}
|
|
115
|
-
onPressOut={onPressOut}
|
|
116
|
-
style={[resolved, rStyle]}
|
|
117
|
-
testID={testID}
|
|
118
|
-
{...rest}
|
|
119
|
-
>
|
|
120
|
-
{children}
|
|
121
|
-
</AnimatedPressable>
|
|
122
|
-
)
|
|
123
|
-
}
|
|
187
|
+
)
|
|
@@ -1,42 +1,45 @@
|
|
|
1
|
-
import type {ResponsiveViewSx} from '@hzblj/kara-sx'
|
|
1
|
+
import type {AccessibilityProps, MotionProps, ResponsiveViewSx} from '@hzblj/kara-sx'
|
|
2
2
|
import type {OpacitySize} from '@hzblj/kara-theme'
|
|
3
3
|
import type {ReactNode} from 'react'
|
|
4
4
|
|
|
5
5
|
import type {Haptic} from '../../haptics'
|
|
6
6
|
|
|
7
|
-
/** How the content answers a press: it
|
|
7
|
+
/** How the content answers a press: it eases down, it dims, or it stays still. */
|
|
8
8
|
export type PressEffect = 'none' | 'opacity' | 'scale'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* React Native's `Pressable`, styled through `sx` like a `Box`, taking touches 8 points past its edges. Each platform
|
|
12
|
-
* presses its own way by default: iOS
|
|
12
|
+
* presses its own way by default: iOS eases the content down to 96%, Android draws Material's ripple over it,
|
|
13
13
|
* clipped to the pressable's shape. `pressEffect` picks the same answer on both, and Android keeps its ripple on top
|
|
14
|
-
* unless `ripple` is off. Give it the `borderRadius` of what it wraps (`full` around a `GlassView`).
|
|
15
|
-
*
|
|
14
|
+
* unless `ripple` is off. Give it the `borderRadius` of what it wraps (`full` around a `GlassView`). Screen readers read
|
|
15
|
+
* it as a button unless `accessibilityRole` names another role. It takes the motion props a `Box` does: `animate` a
|
|
16
|
+
* selected state's `bg`, or let it arrive with `entering`. The React Native tier: it needs no host.
|
|
16
17
|
*/
|
|
17
|
-
export type PressableProps = ResponsiveViewSx &
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
18
|
+
export type PressableProps = ResponsiveViewSx &
|
|
19
|
+
Omit<MotionProps, 'animated'> &
|
|
20
|
+
AccessibilityProps & {
|
|
21
|
+
/** The pressable content. */
|
|
22
|
+
children?: ReactNode
|
|
23
|
+
/** Ignores presses: no `onPress`, no haptic, no press effect or ripple. */
|
|
24
|
+
disabled?: boolean
|
|
25
|
+
/** Played on press. `true` is a `light` impact; a name picks the haptic, e.g. `selection` for a choice. */
|
|
26
|
+
haptic?: boolean | Haptic
|
|
27
|
+
/** Called when the press is released, after the `haptic` plays. */
|
|
28
|
+
onPress?: () => void
|
|
29
|
+
/**
|
|
30
|
+
* How the content answers while it is held. `scale` eases it down to `pressedScale`, `opacity` dims it to
|
|
31
|
+
* `pressedOpacity`, `none` leaves it still. `scale` on iOS and `none` on Android by default, where the ripple is the
|
|
32
|
+
* press. With motion reduced, `scale` dims instead: the press still shows, and nothing moves.
|
|
33
|
+
*/
|
|
34
|
+
pressEffect?: PressEffect
|
|
35
|
+
/** What `opacity` dims to, an opacity token. `o60` by default. */
|
|
36
|
+
pressedOpacity?: OpacitySize
|
|
37
|
+
/** What `scale` eases down to. `0.96` by default: lower reads as a squash, not a press. */
|
|
38
|
+
pressedScale?: number
|
|
39
|
+
/** Android's ripple: Material's pressed state layer over the content, clipped to the shape. On by default. iOS has none. */
|
|
40
|
+
ripple?: boolean
|
|
41
|
+
/** Test identifier, forwarded to the view. */
|
|
42
|
+
testID?: string
|
|
43
|
+
}
|
|
41
44
|
|
|
42
45
|
export type PressableComponent = (props: PressableProps) => ReactNode
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import {CircularProgressIndicator, LinearProgressIndicator} from '@expo/ui/jetpack-compose'
|
|
2
|
+
import {useAccessibilityModifiers} from '@hzblj/kara-sx'
|
|
2
3
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
3
4
|
|
|
4
5
|
import {useHostGuard} from '../../host-context'
|
|
5
6
|
import type {ProgressComponent, ProgressProps} from './progress.types'
|
|
6
7
|
|
|
7
|
-
export const Progress: ProgressComponent = ({circular = false, value}: ProgressProps) => {
|
|
8
|
+
export const Progress: ProgressComponent = ({accessibilityLabel, circular = false, value}: ProgressProps) => {
|
|
8
9
|
useHostGuard('Progress')
|
|
9
10
|
const theme = useAppTheme()
|
|
10
11
|
const color = theme.color('fill.accent.primary')
|
|
12
|
+
const modifiers = useAccessibilityModifiers({accessibilityLabel})
|
|
11
13
|
|
|
12
14
|
if (circular) {
|
|
13
|
-
return <CircularProgressIndicator color={color} progress={value} />
|
|
15
|
+
return <CircularProgressIndicator color={color} modifiers={modifiers} progress={value} />
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
return <LinearProgressIndicator color={color} progress={value} />
|
|
18
|
+
return <LinearProgressIndicator color={color} modifiers={modifiers} progress={value} />
|
|
17
19
|
}
|
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import {Gauge, ProgressView} from '@expo/ui/swift-ui'
|
|
2
2
|
import {gaugeStyle, progressViewStyle, tint} from '@expo/ui/swift-ui/modifiers'
|
|
3
|
+
import {useAccessibilityModifiers} from '@hzblj/kara-sx'
|
|
3
4
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
4
5
|
import {useMemo} from 'react'
|
|
5
6
|
|
|
6
7
|
import {useHostGuard} from '../../host-context'
|
|
7
8
|
import type {ProgressComponent, ProgressProps} from './progress.types'
|
|
8
9
|
|
|
9
|
-
export const Progress: ProgressComponent = ({circular = false, value}: ProgressProps) => {
|
|
10
|
+
export const Progress: ProgressComponent = ({accessibilityLabel, circular = false, value}: ProgressProps) => {
|
|
10
11
|
useHostGuard('Progress')
|
|
11
12
|
const theme = useAppTheme()
|
|
13
|
+
const accessibilityModifiers = useAccessibilityModifiers({accessibilityLabel})
|
|
12
14
|
|
|
13
15
|
const ring = circular && value !== undefined
|
|
14
16
|
const modifiers = useMemo(
|
|
15
17
|
() => [
|
|
16
18
|
ring ? gaugeStyle('circularCapacity') : progressViewStyle(circular ? 'circular' : 'linear'),
|
|
17
19
|
tint(theme.color('fill.accent.primary')),
|
|
20
|
+
...accessibilityModifiers,
|
|
18
21
|
],
|
|
19
|
-
[circular, ring, theme]
|
|
22
|
+
[accessibilityModifiers, circular, ring, theme]
|
|
20
23
|
)
|
|
21
24
|
|
|
22
25
|
// SwiftUI's circular `ProgressView` ignores its value on iOS and spins; a ring that fills is a capacity `Gauge`.
|
|
@@ -5,6 +5,8 @@ import type {ReactNode} from 'react'
|
|
|
5
5
|
* value, which `ProgressView` cannot draw on iOS) / Compose's `LinearProgressIndicator` and `CircularProgressIndicator`.
|
|
6
6
|
*/
|
|
7
7
|
export type ProgressProps = {
|
|
8
|
+
/** What is in progress, for VoiceOver and TalkBack: "Uploading". They read how far along it is themselves. */
|
|
9
|
+
accessibilityLabel?: string
|
|
8
10
|
/** A circular indicator instead of a bar. Off by default. */
|
|
9
11
|
circular?: boolean
|
|
10
12
|
/** Between `0` and `1`. Leave it unset for an indeterminate indicator. */
|
|
@@ -1,36 +1,105 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
isAnimatedStyle,
|
|
3
|
+
markMotionComponent,
|
|
4
|
+
type SxMotion,
|
|
5
|
+
themeKey,
|
|
6
|
+
useAnimatedLatch,
|
|
7
|
+
useSplitSxMotion,
|
|
8
|
+
} from '@hzblj/kara-sx'
|
|
9
|
+
import {
|
|
10
|
+
type ColorToken,
|
|
11
|
+
MAX_FONT_SCALE,
|
|
12
|
+
type TextVariant,
|
|
13
|
+
type Theme,
|
|
14
|
+
useAppTheme,
|
|
15
|
+
useFontScale,
|
|
16
|
+
} from '@hzblj/kara-theme'
|
|
17
|
+
import {Platform, Text as RNText, type StyleProp, type TextStyle} from 'react-native'
|
|
18
|
+
import Animated from 'react-native-reanimated'
|
|
4
19
|
|
|
5
20
|
import type {RnTextComponent, RnTextProps} from './rn-text.types'
|
|
6
21
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
22
|
+
/**
|
|
23
|
+
* A hundredth of a point off every line on iOS, which no one can see and which keeps the last line on screen. React
|
|
24
|
+
* Native measures a paragraph and rounds the height up to the pixel grid, then draws it into the frame Yoga laid out,
|
|
25
|
+
* and that frame can come back a hair short of the exact height (`53.9998` for three 18 pt lines, depending on where
|
|
26
|
+
* the text sits on the page). With `lineHeight` fixed, the lines need exactly 54 and the text container drops the last
|
|
27
|
+
* one, cutting the line before it off mid-word. Lines a hair shorter leave the rounding room to absorb the error.
|
|
28
|
+
*/
|
|
29
|
+
const LINE_HEIGHT_SLACK = Platform.OS === 'ios' ? 0.01 : 0
|
|
30
|
+
|
|
31
|
+
/** Variants, colours and alignments are few, so the text half of the style is shared like the `sx` half. */
|
|
32
|
+
const MAX_TEXT_STYLES = 512
|
|
33
|
+
|
|
34
|
+
const textStyles = new Map<string, TextStyle>()
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The variant at the user's text size, in its colour. React Native scales `fontSize` for the user's setting but leaves
|
|
38
|
+
* `lineHeight` alone, so a grown glyph ends up in an ungrown line and clips. Scaling it here by the same factor keeps
|
|
39
|
+
* the ratio the variant was designed at, and by the same ceiling `maxFontSizeMultiplier` caps the glyph with
|
|
40
|
+
* (`useFontScale` is capped already), so the two cannot disagree.
|
|
41
|
+
*
|
|
42
|
+
* The native tiers need none of this: SwiftUI's `lineSpacing` is extra space *on top of* the font's own line
|
|
43
|
+
* height, which scales with it, and Compose reads `lineHeight` in `sp`.
|
|
44
|
+
*/
|
|
45
|
+
const textStyle = (
|
|
46
|
+
theme: Theme,
|
|
47
|
+
variant: TextVariant,
|
|
48
|
+
color: ColorToken,
|
|
49
|
+
align: RnTextProps['align'],
|
|
50
|
+
fontScale: number
|
|
51
|
+
): TextStyle => {
|
|
52
|
+
const key = `${themeKey(theme)}|${variant}|${color}|${align}|${fontScale}`
|
|
53
|
+
const existing = textStyles.get(key)
|
|
54
|
+
|
|
55
|
+
if (existing) {
|
|
56
|
+
return existing
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (textStyles.size >= MAX_TEXT_STYLES) {
|
|
60
|
+
textStyles.clear()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const typography = theme.typography[variant]
|
|
64
|
+
const style: TextStyle = {
|
|
65
|
+
...typography,
|
|
66
|
+
color: theme.color(color),
|
|
67
|
+
lineHeight: typography.lineHeight * fontScale - LINE_HEIGHT_SLACK,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (align) {
|
|
71
|
+
style.textAlign = align
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
textStyles.set(key, style)
|
|
75
|
+
|
|
76
|
+
return style
|
|
36
77
|
}
|
|
78
|
+
|
|
79
|
+
export const RnText: RnTextComponent = markMotionComponent(
|
|
80
|
+
({align, color = 'content.primary', style, variant = 'body', ...props}: RnTextProps) => {
|
|
81
|
+
const theme = useAppTheme()
|
|
82
|
+
const fontScale = useFontScale()
|
|
83
|
+
// A view style, which a text accepts whole — only React Native's types disagree, on `userSelect`'s width.
|
|
84
|
+
const [sx, motion, rest] = useSplitSxMotion(props) as [TextStyle, SxMotion, Record<string, unknown>]
|
|
85
|
+
const animated = useAnimatedLatch(motion.animated || isAnimatedStyle(style), 'RnText')
|
|
86
|
+
const text = textStyle(theme, variant, color, align, fontScale)
|
|
87
|
+
|
|
88
|
+
if (!animated) {
|
|
89
|
+
return (
|
|
90
|
+
<RNText maxFontSizeMultiplier={MAX_FONT_SCALE} style={[text, sx, style as StyleProp<TextStyle>]} {...rest} />
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Animated.Text
|
|
96
|
+
entering={motion.entering}
|
|
97
|
+
exiting={motion.exiting}
|
|
98
|
+
layout={motion.layout}
|
|
99
|
+
maxFontSizeMultiplier={MAX_FONT_SCALE}
|
|
100
|
+
style={[text, sx, motion.style, style]}
|
|
101
|
+
{...rest}
|
|
102
|
+
/>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
)
|
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
import type {ResponsiveViewSx} from '@hzblj/kara-sx'
|
|
1
|
+
import type {MotionProps, ResponsiveViewSx} from '@hzblj/kara-sx'
|
|
2
2
|
import type {ColorToken, TextVariant} from '@hzblj/kara-theme'
|
|
3
3
|
import type {ReactNode} from 'react'
|
|
4
|
-
import type {TextProps as RNTextProps} from 'react-native'
|
|
4
|
+
import type {TextProps as RNTextProps, StyleProp, TextStyle} from 'react-native'
|
|
5
|
+
import type {AnimatedStyle} from 'react-native-reanimated'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* React Native's `Text` in a theme typography variant: the React Native tier, for text outside a native host. It takes
|
|
8
9
|
* `Box`'s `sx` surface for its own box (margins, flex, padding, a background), all but `align`, which here aligns the
|
|
9
|
-
* lines.
|
|
10
|
+
* lines, and the motion props a `Box` takes: `entering="fadeUp"` makes it an `Animated.Text` by itself.
|
|
10
11
|
*/
|
|
11
12
|
export type RnTextProps = Omit<RNTextProps, 'style'> &
|
|
13
|
+
MotionProps &
|
|
12
14
|
Omit<ResponsiveViewSx, 'align'> & {
|
|
13
15
|
/** Horizontal alignment, as React Native's `textAlign`. */
|
|
14
16
|
align?: 'center' | 'left' | 'right'
|
|
15
17
|
/** The text's colour. `content.primary` by default. */
|
|
16
18
|
color?: ColorToken
|
|
19
|
+
/** Goes last, over the variant and the `sx`: an animated style from `useAnimatedStyle`, which it renders through Reanimated for. */
|
|
20
|
+
style?: StyleProp<AnimatedStyle<TextStyle>>
|
|
17
21
|
/** The typography variant. `body` by default. */
|
|
18
22
|
variant?: TextVariant
|
|
19
23
|
}
|
|
@@ -29,7 +29,7 @@ export const SegmentedControl: SegmentedControlComponent = ({
|
|
|
29
29
|
() => ({
|
|
30
30
|
activeBorderColor: theme.color('fill.accent.primary'),
|
|
31
31
|
activeContainerColor: theme.color('fill.accent.primary'),
|
|
32
|
-
activeContentColor: theme.color('content.on.
|
|
32
|
+
activeContentColor: theme.color('content.on.accent'),
|
|
33
33
|
disabledActiveBorderColor: theme.color('fill.accent.disabled'),
|
|
34
34
|
disabledActiveContainerColor: theme.color('fill.accent.disabled'),
|
|
35
35
|
disabledActiveContentColor: theme.color('btn.content.disabled'),
|
|
@@ -33,7 +33,7 @@ export const SegmentedControl: SegmentedControlComponent = ({
|
|
|
33
33
|
return (
|
|
34
34
|
<NativeSegmentedControl
|
|
35
35
|
activePillColor={theme.color('fill.accent.primary')}
|
|
36
|
-
activeTextColor={theme.color('content.on.
|
|
36
|
+
activeTextColor={theme.color('content.on.accent')}
|
|
37
37
|
disabled={disabled}
|
|
38
38
|
fontFamily={face.fontFamily}
|
|
39
39
|
fontSize={face.fontSize}
|
|
@@ -1,21 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {timing} from '@hzblj/kara-sx'
|
|
2
|
+
import {duration, opacity, useReducedMotion} from '@hzblj/kara-theme'
|
|
2
3
|
import {useEffect} from 'react'
|
|
3
|
-
import {useAnimatedStyle, useSharedValue, withRepeat, withTiming} from 'react-native-reanimated'
|
|
4
|
+
import {cancelAnimation, useAnimatedStyle, useSharedValue, withRepeat, withTiming} from 'react-native-reanimated'
|
|
4
5
|
|
|
5
|
-
import {
|
|
6
|
+
import {Box} from '../box'
|
|
6
7
|
import type {SkeletonComponent, SkeletonProps} from './skeleton.types'
|
|
7
8
|
|
|
9
|
+
/** A breath, not a transition, so it sits off the scale: `slow` twice each way, at constant speed. */
|
|
10
|
+
const PULSE = {...timing('slow', 'linear'), duration: duration.slow * 2}
|
|
11
|
+
|
|
8
12
|
export const Skeleton: SkeletonComponent = ({borderRadius = 'small', h, testID, w}: SkeletonProps) => {
|
|
13
|
+
const reduced = useReducedMotion()
|
|
9
14
|
const pulse = useSharedValue<number>(opacity.o40)
|
|
10
15
|
|
|
16
|
+
// With motion reduced it holds still, halfway between the two ends of the pulse.
|
|
11
17
|
useEffect(() => {
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
if (reduced) {
|
|
19
|
+
cancelAnimation(pulse)
|
|
20
|
+
pulse.value = opacity.o70
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pulse.value = opacity.o40
|
|
25
|
+
pulse.value = withRepeat(withTiming(opacity.o100, PULSE), -1, true)
|
|
26
|
+
}, [pulse, reduced])
|
|
14
27
|
|
|
15
28
|
const rStyle = useAnimatedStyle(() => ({opacity: pulse.value}), [])
|
|
16
29
|
|
|
17
30
|
return (
|
|
18
|
-
<
|
|
31
|
+
<Box
|
|
32
|
+
aria-hidden
|
|
19
33
|
bg="fill.quaternary"
|
|
20
34
|
borderRadius={borderRadius}
|
|
21
35
|
h={h}
|