@hzblj/kara-components 0.11.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/lib/module/atoms/box/box.js +3 -0
  3. package/lib/module/atoms/box/box.js.map +1 -1
  4. package/lib/module/atoms/box/index.js +0 -1
  5. package/lib/module/atoms/box/index.js.map +1 -1
  6. package/lib/module/atoms/index.js +1 -0
  7. package/lib/module/atoms/index.js.map +1 -1
  8. package/lib/module/atoms/pressable/pressable.js +62 -15
  9. package/lib/module/atoms/pressable/pressable.js.map +1 -1
  10. package/lib/module/atoms/rn-text/rn-text.js +66 -26
  11. package/lib/module/atoms/rn-text/rn-text.js.map +1 -1
  12. package/lib/module/atoms/skeleton/skeleton.js +3 -3
  13. package/lib/module/atoms/skeleton/skeleton.js.map +1 -1
  14. package/lib/module/atoms/stagger/index.js +5 -0
  15. package/lib/module/atoms/stagger/index.js.map +1 -0
  16. package/lib/module/atoms/stagger/stagger.js +102 -0
  17. package/lib/module/atoms/stagger/stagger.js.map +1 -0
  18. package/lib/module/atoms/stagger/stagger.types.js +4 -0
  19. package/lib/module/atoms/stagger/stagger.types.js.map +1 -0
  20. package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js +3 -3
  21. package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js.map +1 -1
  22. package/lib/module/organisms/snackbar/snackbar.js +32 -20
  23. package/lib/module/organisms/snackbar/snackbar.js.map +1 -1
  24. package/lib/typescript/atoms/box/box.d.ts +3 -0
  25. package/lib/typescript/atoms/box/index.d.ts +0 -1
  26. package/lib/typescript/atoms/index.d.ts +1 -0
  27. package/lib/typescript/atoms/pressable/pressable.types.d.ts +4 -3
  28. package/lib/typescript/atoms/rn-text/rn-text.types.d.ts +7 -4
  29. package/lib/typescript/atoms/stagger/index.d.ts +2 -0
  30. package/lib/typescript/atoms/stagger/stagger.d.ts +2 -0
  31. package/lib/typescript/atoms/stagger/stagger.types.d.ts +32 -0
  32. package/lib/typescript/organisms/snackbar/snackbar.types.d.ts +6 -8
  33. package/package.json +4 -4
  34. package/src/atoms/box/box.tsx +3 -0
  35. package/src/atoms/box/index.ts +0 -1
  36. package/src/atoms/index.ts +1 -0
  37. package/src/atoms/pressable/pressable.tsx +162 -106
  38. package/src/atoms/pressable/pressable.types.ts +4 -2
  39. package/src/atoms/rn-text/rn-text.tsx +101 -32
  40. package/src/atoms/rn-text/rn-text.types.ts +7 -3
  41. package/src/atoms/skeleton/skeleton.tsx +3 -3
  42. package/src/atoms/stagger/index.ts +2 -0
  43. package/src/atoms/stagger/stagger.tsx +98 -0
  44. package/src/atoms/stagger/stagger.types.ts +35 -0
  45. package/src/molecules/scroll-edge-container/scroll-edge-container.android.tsx +3 -3
  46. package/src/organisms/snackbar/snackbar.tsx +20 -40
  47. package/src/organisms/snackbar/snackbar.types.ts +6 -8
  48. package/lib/module/atoms/box/a-box.js +0 -6
  49. package/lib/module/atoms/box/a-box.js.map +0 -1
  50. package/lib/module/motion.js +0 -19
  51. package/lib/module/motion.js.map +0 -1
  52. package/lib/typescript/atoms/box/a-box.d.ts +0 -49
  53. package/lib/typescript/motion.d.ts +0 -8
  54. package/src/atoms/box/a-box.ts +0 -5
  55. package/src/motion.ts +0 -13
@@ -1,14 +1,14 @@
1
1
  "use strict";
2
2
 
3
- import { duration as durations, easing, spacing, useReducedMotion } from '@hzblj/kara-theme';
3
+ import { timing } from '@hzblj/kara-sx';
4
+ import { spacing, useReducedMotion } from '@hzblj/kara-theme';
4
5
  import { useEffect, useRef } from 'react';
5
6
  import { AccessibilityInfo, Platform } from 'react-native';
6
- import { Easing, FadeIn, FadeInDown, FadeInUp, FadeOut, FadeOutDown, FadeOutUp, ReduceMotion, withTiming } from 'react-native-reanimated';
7
- import { ABox, Box } from "../../atoms/box/index.js";
7
+ import { withTiming } from 'react-native-reanimated';
8
+ import { Box } from "../../atoms/box/index.js";
8
9
  import { Pressable } from "../../atoms/pressable/index.js";
9
10
  import { RnText } from "../../atoms/rn-text/index.js";
10
11
  import { RnGlyph } from "../../glyph/index.js";
11
- import { timing } from "../../motion.js";
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  /** Material's `SnackbarDuration.Short` and `Long`. */
14
14
  const DURATIONS = {
@@ -19,10 +19,9 @@ const PLATFORM_ANIMATION = Platform.OS === 'android' ? 'scale' : 'slide';
19
19
 
20
20
  /** The close glyph at the label's height: an SF Symbol fills its box, Material's icon sits inside a padded one. */
21
21
  const CLOSE_SIZE = Platform.OS === 'ios' ? 's12' : 's16';
22
- const SMOOTH_OUT = Easing.bezier(...easing.smoothOut);
23
22
  const GROW = timing('quick');
24
23
 
25
- /** Material's snackbar enter: it fades in while it grows from 80 %, in 150 ms. */
24
+ /** Material's snackbar enter: it fades in while it grows from 80 %, in 150 ms. Further than kara's `scale` preset. */
26
25
  const scaleIn = () => {
27
26
  'worklet';
28
27
 
@@ -41,29 +40,42 @@ const scaleIn = () => {
41
40
  }
42
41
  };
43
42
  };
44
-
45
43
  /**
46
- * The presets in and out: arriving in `fast`, leaving in `quick`, both on `smoothOut`. `ReduceMotion.Never`, because
47
- * `useReducedMotion` has already picked the fade by then, and a fade is what should still run.
44
+ * kara's motion presets, which arrive in `fast`, leave in `quick` and fade with motion reduced by themselves. A slide
45
+ * travels further than a preset's default: in from the edge it sits on, and back out toward it.
48
46
  */
49
47
  const TRANSITIONS = {
50
48
  fade: {
51
- entering: FadeIn.duration(durations.fast).easing(SMOOTH_OUT).reduceMotion(ReduceMotion.Never),
52
- exiting: FadeOut.duration(durations.quick).easing(SMOOTH_OUT).reduceMotion(ReduceMotion.Never)
49
+ entering: 'fade',
50
+ exiting: 'fade'
53
51
  },
54
52
  // Material's exit is a 75 ms fade: `micro`.
55
53
  scale: {
56
54
  entering: scaleIn,
57
- exiting: FadeOut.duration(durations.micro).reduceMotion(ReduceMotion.Never)
55
+ exiting: {
56
+ duration: 'micro',
57
+ preset: 'fade'
58
+ }
58
59
  },
59
- // Slide: in from the edge it sits on, and back out toward it.
60
60
  slideBottom: {
61
- entering: FadeInDown.duration(durations.fast).easing(SMOOTH_OUT).reduceMotion(ReduceMotion.Never),
62
- exiting: FadeOutDown.duration(durations.quick).easing(SMOOTH_OUT).reduceMotion(ReduceMotion.Never)
61
+ entering: {
62
+ distance: 's24',
63
+ preset: 'fadeUp'
64
+ },
65
+ exiting: {
66
+ distance: 's24',
67
+ preset: 'fadeDown'
68
+ }
63
69
  },
64
70
  slideTop: {
65
- entering: FadeInUp.duration(durations.fast).easing(SMOOTH_OUT).reduceMotion(ReduceMotion.Never),
66
- exiting: FadeOutUp.duration(durations.quick).easing(SMOOTH_OUT).reduceMotion(ReduceMotion.Never)
71
+ entering: {
72
+ distance: 's24',
73
+ preset: 'fadeDown'
74
+ },
75
+ exiting: {
76
+ distance: 's24',
77
+ preset: 'fadeUp'
78
+ }
67
79
  }
68
80
  };
69
81
  const transitionsFor = (animation, position) => {
@@ -118,9 +130,9 @@ export const Snackbar = ({
118
130
  bottom: spacing[offset]
119
131
  };
120
132
  const hasTrailing = Boolean(actionLabel) || dismissible;
121
- // With motion reduced it only fades: nothing slides or grows.
122
- const preset = transitionsFor(reduced ? 'fade' : animation, position);
123
- return /*#__PURE__*/_jsxs(ABox, {
133
+ // The presets fade with motion reduced by themselves; Material's scale is a worklet of its own, so it is picked out.
134
+ const preset = transitionsFor(reduced && animation === 'scale' ? 'fade' : animation, position);
135
+ return /*#__PURE__*/_jsxs(Box, {
124
136
  align: actionOnNewLine ? 'stretch' : 'center',
125
137
  bg: "surface.high.contrast",
126
138
  borderRadius: "medium",
@@ -1 +1 @@
1
- {"version":3,"names":["duration","durations","easing","spacing","useReducedMotion","useEffect","useRef","AccessibilityInfo","Platform","Easing","FadeIn","FadeInDown","FadeInUp","FadeOut","FadeOutDown","FadeOutUp","ReduceMotion","withTiming","ABox","Box","Pressable","RnText","RnGlyph","timing","jsx","_jsx","jsxs","_jsxs","DURATIONS","long","short","PLATFORM_ANIMATION","OS","CLOSE_SIZE","SMOOTH_OUT","bezier","smoothOut","GROW","scaleIn","animations","opacity","transform","scale","initialValues","TRANSITIONS","fade","entering","fast","reduceMotion","Never","exiting","quick","micro","slideBottom","slideTop","transitionsFor","animation","position","millisecondsOf","undefined","Snackbar","actionLabel","actionOnNewLine","dismissible","message","offset","onAction","onDismiss","visible","reduced","dismiss","current","milliseconds","timer","setTimeout","clearTimeout","announceForAccessibility","edge","top","bottom","hasTrailing","Boolean","preset","align","bg","borderRadius","flexDirection","gap","left","s16","px","py","right","shadow","children","color","flex","variant","alignSelf","onPress","pressEffect","ripple","accessibilityLabel","name","size"],"sourceRoot":"../../../../src","sources":["organisms/snackbar/snackbar.tsx"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["timing","spacing","useReducedMotion","useEffect","useRef","AccessibilityInfo","Platform","withTiming","Box","Pressable","RnText","RnGlyph","jsx","_jsx","jsxs","_jsxs","DURATIONS","long","short","PLATFORM_ANIMATION","OS","CLOSE_SIZE","GROW","scaleIn","animations","opacity","transform","scale","initialValues","TRANSITIONS","fade","entering","exiting","duration","preset","slideBottom","distance","slideTop","transitionsFor","animation","position","millisecondsOf","undefined","Snackbar","actionLabel","actionOnNewLine","dismissible","message","offset","onAction","onDismiss","visible","reduced","dismiss","current","milliseconds","timer","setTimeout","clearTimeout","announceForAccessibility","edge","top","bottom","hasTrailing","Boolean","align","bg","borderRadius","flexDirection","gap","left","s16","px","py","right","shadow","children","color","flex","variant","alignSelf","onPress","pressEffect","ripple","accessibilityLabel","name","size"],"sourceRoot":"../../../../src","sources":["organisms/snackbar/snackbar.tsx"],"mappings":"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","ignoreList":[]}
@@ -2,6 +2,9 @@ import { View } from 'react-native';
2
2
  /**
3
3
  * A React Native `View` that takes the design system's `sx` props. The `sx()` HOC is the whole
4
4
  * implementation — Box is just its first caller, and any other style-taking component gets the same.
5
+ *
6
+ * That includes motion: `entering`, `exiting`, `layout`, `animate` or an animated `style` make it an `Animated.View`
7
+ * from its first frame, and without them it stays a plain `View`.
5
8
  */
6
9
  export declare const Box: import("react").MemoExoticComponent<{
7
10
  ({ style, ...props }: import("@hzblj/kara-sx").SxProps<import("react-native").ViewProps & import("react").RefAttributes<View>>): import("react").JSX.Element;
@@ -1,3 +1,2 @@
1
- export * from './a-box';
2
1
  export * from './box';
3
2
  export * from './box.types';
@@ -32,6 +32,7 @@ export * from './segmented-control';
32
32
  export * from './skeleton';
33
33
  export * from './slider';
34
34
  export * from './spacer';
35
+ export * from './stagger';
35
36
  export * from './surface';
36
37
  export * from './switch';
37
38
  export * from './text';
@@ -1,4 +1,4 @@
1
- import type { AccessibilityProps, 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
  import type { Haptic } from '../../haptics';
@@ -9,9 +9,10 @@ export type PressEffect = 'none' | 'opacity' | 'scale';
9
9
  * presses its own way by default: iOS eases the content down to 96%, Android draws Material's ripple over it,
10
10
  * clipped to the pressable's shape. `pressEffect` picks the same answer on both, and Android keeps its ripple on top
11
11
  * unless `ripple` is off. Give it the `borderRadius` of what it wraps (`full` around a `GlassView`). Screen readers read
12
- * it as a button unless `accessibilityRole` names another role. The React Native tier: it needs no host.
12
+ * it as a button unless `accessibilityRole` names another role. It takes the motion props a `Box` does: `animate` a
13
+ * selected state's `bg`, or let it arrive with `entering`. The React Native tier: it needs no host.
13
14
  */
14
- export type PressableProps = ResponsiveViewSx & AccessibilityProps & {
15
+ export type PressableProps = ResponsiveViewSx & Omit<MotionProps, 'animated'> & AccessibilityProps & {
15
16
  /** The pressable content. */
16
17
  children?: ReactNode;
17
18
  /** Ignores presses: no `onPress`, no haptic, no press effect or ripple. */
@@ -1,17 +1,20 @@
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
  * React Native's `Text` in a theme typography variant: the React Native tier, for text outside a native host. It takes
7
8
  * `Box`'s `sx` surface for its own box (margins, flex, padding, a background), all but `align`, which here aligns the
8
- * lines.
9
+ * lines, and the motion props a `Box` takes: `entering="fadeUp"` makes it an `Animated.Text` by itself.
9
10
  */
10
- export type RnTextProps = Omit<RNTextProps, 'style'> & Omit<ResponsiveViewSx, 'align'> & {
11
+ export type RnTextProps = Omit<RNTextProps, 'style'> & MotionProps & Omit<ResponsiveViewSx, 'align'> & {
11
12
  /** Horizontal alignment, as React Native's `textAlign`. */
12
13
  align?: 'center' | 'left' | 'right';
13
14
  /** The text's colour. `content.primary` by default. */
14
15
  color?: ColorToken;
16
+ /** Goes last, over the variant and the `sx`: an animated style from `useAnimatedStyle`, which it renders through Reanimated for. */
17
+ style?: StyleProp<AnimatedStyle<TextStyle>>;
15
18
  /** The typography variant. `body` by default. */
16
19
  variant?: TextVariant;
17
20
  };
@@ -0,0 +1,2 @@
1
+ export * from './stagger';
2
+ export * from './stagger.types';
@@ -0,0 +1,2 @@
1
+ import type { StaggerComponent } from './stagger.types';
2
+ export declare const Stagger: StaggerComponent;
@@ -0,0 +1,32 @@
1
+ import type { MotionPreset, MotionPresetConfig, ResponsiveViewSx } from '@hzblj/kara-sx';
2
+ import type { DurationSize, SpringName } from '@hzblj/kara-theme';
3
+ import type { ReactNode } from 'react';
4
+ /** A preset by name, or one adjusted: `{preset: 'fadeUp', duration: 'slow'}`. Its `delay` is where the first one starts. */
5
+ export type StaggerMotion = MotionPreset | MotionPresetConfig;
6
+ /**
7
+ * A `Box` whose children arrive one after another: each takes `entering` a step later than the one before. Children
8
+ * that arrive together are staggered among themselves, the first ones as much as a later batch (the next page of a
9
+ * list), while a single child added later arrives at once. A `Box`, `RnText` or `Pressable` child takes the motion
10
+ * itself; any other child is wrapped in a `Box` that does. With motion reduced every child fades, still one after
11
+ * another. The React Native tier: it needs no host.
12
+ */
13
+ export type StaggerProps = ResponsiveViewSx & {
14
+ /** What arrives. A child with an `entering` preset of its own keeps it and only takes the delay. */
15
+ children?: ReactNode;
16
+ /** How each child arrives. `fadeUp` by default. */
17
+ entering?: StaggerMotion;
18
+ /** How a child leaves when it is removed. They leave together, not one after another; none by default. */
19
+ exiting?: StaggerMotion;
20
+ /** Which end arrives first. `start` by default. */
21
+ from?: 'end' | 'start';
22
+ /** The step between one child and the next, a duration token. `stagger` (40 ms) by default, `micro` for a slower one. */
23
+ interval?: DurationSize;
24
+ /**
25
+ * Moves the other children into place when one arrives or leaves, and the stagger itself to its new size: `true` on
26
+ * the `smooth` spring, or a spring token. Off by default. Give what sits after it `layout` too, so it follows.
27
+ */
28
+ layout?: boolean | SpringName;
29
+ /** Test identifier, forwarded to the view. */
30
+ testID?: string;
31
+ };
32
+ export type StaggerComponent = (props: StaggerProps) => ReactNode;
@@ -1,7 +1,6 @@
1
+ import type { SxEntryExit } from '@hzblj/kara-sx';
1
2
  import type { SpaceSize } from '@hzblj/kara-theme';
2
- import type { ComponentProps, ReactNode } from 'react';
3
- import type Animated from 'react-native-reanimated';
4
- type LayoutAnimation = ComponentProps<typeof Animated.View>['entering'];
3
+ import type { ReactNode } from 'react';
5
4
  /** How it comes and goes. */
6
5
  export type SnackbarAnimation = 'fade' | 'scale' | 'slide';
7
6
  /** How long it stays: Material's short (4 s) and long (10 s), never, or a number of milliseconds. */
@@ -28,10 +27,10 @@ export type SnackbarProps = {
28
27
  dismissible?: boolean;
29
28
  /** How long it stays up before `onDismiss` is called. `indefinite` by default: it never hides itself. */
30
29
  duration?: SnackbarDuration;
31
- /** A Reanimated entering animation of your own, e.g. `FadeInDown.springify()`. It wins over `animation`. */
32
- entering?: LayoutAnimation;
33
- /** A Reanimated exiting animation of your own. It wins over `animation`. */
34
- exiting?: LayoutAnimation;
30
+ /** How it arrives instead: a motion preset (`'scale'`), or Reanimated's own, e.g. `FadeInDown.springify()`. It wins over `animation`. */
31
+ entering?: SxEntryExit;
32
+ /** How it leaves instead: a motion preset, or Reanimated's own. It wins over `animation`. */
33
+ exiting?: SxEntryExit;
35
34
  /** The message. */
36
35
  message: string;
37
36
  /** Between the snackbar and its edge, a space token. `s24` by default; raise it above a tab bar. */
@@ -46,4 +45,3 @@ export type SnackbarProps = {
46
45
  visible: boolean;
47
46
  };
48
47
  export type SnackbarComponent = (props: SnackbarProps) => ReactNode;
49
- export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hzblj/kara-components",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "The atoms, molecules, organisms and templates of @hzblj/kara-ui — real SwiftUI and Jetpack Compose views through @expo/ui.",
5
5
  "author": "Jan Blazej",
6
6
  "license": "MIT",
@@ -59,9 +59,9 @@
59
59
  ]
60
60
  },
61
61
  "dependencies": {
62
- "@hzblj/kara-native": "^0.11.0",
63
- "@hzblj/kara-sx": "^0.11.0",
64
- "@hzblj/kara-theme": "^0.11.0",
62
+ "@hzblj/kara-native": "^0.12.0",
63
+ "@hzblj/kara-sx": "^0.12.0",
64
+ "@hzblj/kara-theme": "^0.12.0",
65
65
  "sf-symbols-typescript": "^2.2.0"
66
66
  },
67
67
  "peerDependencies": {
@@ -4,5 +4,8 @@ import {View} from 'react-native'
4
4
  /**
5
5
  * A React Native `View` that takes the design system's `sx` props. The `sx()` HOC is the whole
6
6
  * implementation — Box is just its first caller, and any other style-taking component gets the same.
7
+ *
8
+ * That includes motion: `entering`, `exiting`, `layout`, `animate` or an animated `style` make it an `Animated.View`
9
+ * from its first frame, and without them it stays a plain `View`.
7
10
  */
8
11
  export const Box = sx(View)
@@ -1,3 +1,2 @@
1
- export * from './a-box'
2
1
  export * from './box'
3
2
  export * from './box.types'
@@ -32,6 +32,7 @@ export * from './segmented-control'
32
32
  export * from './skeleton'
33
33
  export * from './slider'
34
34
  export * from './spacer'
35
+ export * from './stagger'
35
36
  export * from './surface'
36
37
  export * from './switch'
37
38
  export * from './text'
@@ -1,17 +1,29 @@
1
- import {accessibilityProps, splitAccessibility, useSplitSx} from '@hzblj/kara-sx'
2
- import {opacity, useAppTheme, useReducedMotion, withOpacity} from '@hzblj/kara-theme'
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
- import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'
14
+ import Animated, {useAnimatedStyle, useSharedValue, withSpring, withTiming} from 'react-native-reanimated'
6
15
 
7
16
  import {haptics} from '../../haptics'
8
- import {timing} from '../../motion'
9
17
  import {splitForRipple} from '../../ripple-clip'
10
18
  import type {PressableComponent, PressableProps} from './pressable.types'
11
19
 
12
- /** The scale eases down and settles back a little slower, with no bounce: a press is not a toy. */
13
- const SCALE_IN = timing('quick')
14
- const SCALE_OUT = timing('fast')
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')
15
27
 
16
28
  /** A dim comes in at once and leaves a little slower, as UIKit's own highlight does. */
17
29
  const DIM_IN = timing('micro', 'out')
@@ -21,111 +33,155 @@ const PLATFORM_EFFECT = Platform.OS === 'ios' ? 'scale' : 'none'
21
33
 
22
34
  const AnimatedPressable = Animated.createAnimatedComponent(RNPressable)
23
35
 
24
- export const Pressable: PressableComponent = ({
25
- children,
26
- disabled = false,
27
- haptic = false,
28
- onPress,
29
- pressEffect = PLATFORM_EFFECT,
30
- pressedOpacity = 'o60',
31
- pressedScale = 0.96,
32
- ripple: withRipple = true,
33
- testID,
34
- ...props
35
- }: PressableProps) => {
36
- const [accessibility, ownProps] = splitAccessibility(props)
37
- const [resolved, rest] = useSplitSx(ownProps)
38
- const reduced = useReducedMotion()
39
- // With motion reduced the press still shows, and nothing moves: a scale dims instead.
40
- const effect = reduced && pressEffect === 'scale' ? 'opacity' : pressEffect
41
- // 0 at rest, 1 held: one value drives whichever effect is picked, so a change of effect needs no second animation.
42
- const pressed = useSharedValue(0)
43
- const feedback = haptic === true ? 'light' : haptic || undefined
44
- const a11y = accessibilityProps(accessibility, 'button')
45
- const dimmed = opacity[pressedOpacity]
46
-
47
- const theme = useAppTheme()
48
- const isAndroid = Platform.OS === 'android'
49
-
50
- const rStyle = useAnimatedStyle(() => {
51
- if (effect === 'scale') {
52
- return {transform: [{scale: 1 - (1 - pressedScale) * pressed.value}]}
53
- }
36
+ /** What `animate` covers by default, less the transform. */
37
+ const VISUAL_KEYS: readonly AnimatableKey[] = ['bg', 'borderColor', 'opacity']
54
38
 
55
- return effect === 'opacity' ? {opacity: 1 - (1 - dimmed) * pressed.value} : {}
56
- }, [dimmed, effect, pressedScale])
57
-
58
- // Android's press is Material's: a ripple over the content (the pressed state layer, `onSurface` at 10 %) clipped to
59
- // the shape by an outer view (`splitForRipple`). The effect, when there is one, moves that outer view, ripple and all.
60
- const ripple = useMemo(
61
- () =>
62
- isAndroid && withRipple
63
- ? {color: withOpacity(String(theme.color('content.primary')), 'o10'), foreground: true}
64
- : undefined,
65
- [isAndroid, theme, withRipple]
66
- )
67
- const androidStyle = useMemo(
68
- () => (isAndroid ? splitForRipple(resolved as ViewStyle) : undefined),
69
- [isAndroid, resolved]
70
- )
71
-
72
- // The finger is down: the Taptic Engine has until the release to wake, so the haptic lands with the press.
73
- const onPressIn = useCallback(() => {
74
- if (effect !== 'none') {
75
- pressed.value = withTiming(1, effect === 'scale' ? SCALE_IN : DIM_IN)
76
- }
39
+ const TRANSFORM_KEYS = new Set<AnimatableKey>(['offsetX', 'offsetY', 'rotate', 'scale'])
77
40
 
78
- if (feedback) {
79
- haptics.prepare(feedback)
80
- }
81
- }, [effect, feedback, pressed])
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
+ }
82
49
 
83
- const onPressOut = useCallback(() => {
84
- pressed.value = withTiming(0, effect === 'scale' ? SCALE_OUT : DIM_OUT)
85
- }, [effect, pressed])
50
+ if (animate === true) {
51
+ return {keys: VISUAL_KEYS}
52
+ }
86
53
 
87
- const onPressWithHaptic = useCallback(() => {
88
- if (feedback) {
89
- haptics.play(feedback)
90
- }
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
+ }
91
59
 
92
- onPress?.()
93
- }, [feedback, onPress])
60
+ return {...animate, keys: (animate.keys ?? VISUAL_KEYS).filter(key => !TRANSFORM_KEYS.has(key))}
61
+ }
94
62
 
95
- if (androidStyle) {
96
- return (
97
- <Animated.View style={[androidStyle.outer, rStyle]}>
98
- <RNPressable
99
- {...a11y}
100
- android_ripple={ripple}
101
- disabled={disabled}
102
- hitSlop={8}
103
- onPress={onPressWithHaptic}
104
- onPressIn={onPressIn}
105
- onPressOut={onPressOut}
106
- style={androidStyle.inner}
107
- testID={testID}
108
- {...rest}
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]}
109
149
  >
110
- {children}
111
- </RNPressable>
112
- </Animated.View>
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>
113
185
  )
114
186
  }
115
-
116
- return (
117
- <AnimatedPressable
118
- {...a11y}
119
- disabled={disabled}
120
- hitSlop={8}
121
- onPress={onPressWithHaptic}
122
- onPressIn={onPressIn}
123
- onPressOut={onPressOut}
124
- style={[resolved, rStyle]}
125
- testID={testID}
126
- {...rest}
127
- >
128
- {children}
129
- </AnimatedPressable>
130
- )
131
- }
187
+ )
@@ -1,4 +1,4 @@
1
- import type {AccessibilityProps, 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
 
@@ -12,9 +12,11 @@ export type PressEffect = 'none' | 'opacity' | 'scale'
12
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
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. The React Native tier: it needs no host.
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
18
  export type PressableProps = ResponsiveViewSx &
19
+ Omit<MotionProps, 'animated'> &
18
20
  AccessibilityProps & {
19
21
  /** The pressable content. */
20
22
  children?: ReactNode