@dloizides/ui-motion 1.0.1 → 1.0.2

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 CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.2
4
+
5
+ ### Fixed
6
+
7
+ - **`Collapse` — a closed region is now HIDDEN, not merely clipped.** `overflow: hidden` +
8
+ `height: 0` clips, but the clipped child keeps its own non-empty box: it stayed in the tab
9
+ order, was still announced by screen readers, and was still reported VISIBLE by Playwright —
10
+ while the owning header said `aria-expanded="false"`. Found by an aml-v2 E2E test that could
11
+ see a collapsed settings panel. A settled-closed region now sets `visibility: hidden` (web),
12
+ `aria-hidden`, `accessibilityElementsHidden`, `importantForAccessibility="no-hide-descendants"`
13
+ and `pointerEvents="none"`.
14
+
15
+ `visibility: hidden` rather than `display: none` deliberately: `display: none` would zero the
16
+ `onLayout` measurement the component animates back to, so reopening would animate 0 -> 0.
17
+
18
+ "Settled closed" is a state distinct from `!open` — it is set only once the collapse has
19
+ actually finished, so the content collapses instead of vanishing, and an interrupted collapse
20
+ (a reopen mid-animation, `finished: false`) never hides. It is also set immediately whenever
21
+ there is nothing to animate: before the first `onLayout` and under reduced motion.
22
+
23
+ - **`collapseSettlesClosed(open, finished)`** exported from `motionUtils` — the settle rule as a
24
+ pure function, because the animation callback it guards is unreachable from a jsdom test
25
+ (`onLayout` needs a `ResizeObserver`).
26
+
27
+
3
28
  ## 1.0.1
4
29
 
5
30
  Initial release — the shared motion layer of the dloizides.com RN-web UI kit.
package/dist/index.d.mts CHANGED
@@ -138,8 +138,18 @@ declare const FadeIn: ({ children, duration, delay, translateY, style, testID, }
138
138
  * web and native. Height is a layout prop, so the native driver can never be used
139
139
  * here (`useNativeDriver: false`).
140
140
  *
141
- * The children stay MOUNTED at all times (only clipped by `overflow: hidden`), so
142
- * their state is preserved across open/close and screen readers still reach them.
141
+ * The children stay MOUNTED at all times, so their state survives open/close and a
142
+ * consumer's `aria-controls` always resolves to a real node. MOUNTED IS NOT EXPOSED,
143
+ * though: once the collapse has settled shut the content is hidden from the
144
+ * accessibility tree, the tab order and hit-testing.
145
+ *
146
+ * THAT DISTINCTION IS A BUG FIX, and this comment used to assert the bug ("screen
147
+ * readers still reach them"). `overflow: hidden` + `height: 0` CLIPS; it does not hide.
148
+ * The clipped child keeps its own non-empty box, so it stayed focusable by Tab,
149
+ * announced by screen readers, and reported VISIBLE by Playwright — while the owning
150
+ * header said `aria-expanded="false"`. Found via an aml-v2 E2E test that could see a
151
+ * collapsed settings panel.
152
+ *
143
153
  * Reduced-motion snaps to the target height with no animation.
144
154
  */
145
155
 
package/dist/index.d.ts CHANGED
@@ -138,8 +138,18 @@ declare const FadeIn: ({ children, duration, delay, translateY, style, testID, }
138
138
  * web and native. Height is a layout prop, so the native driver can never be used
139
139
  * here (`useNativeDriver: false`).
140
140
  *
141
- * The children stay MOUNTED at all times (only clipped by `overflow: hidden`), so
142
- * their state is preserved across open/close and screen readers still reach them.
141
+ * The children stay MOUNTED at all times, so their state survives open/close and a
142
+ * consumer's `aria-controls` always resolves to a real node. MOUNTED IS NOT EXPOSED,
143
+ * though: once the collapse has settled shut the content is hidden from the
144
+ * accessibility tree, the tab order and hit-testing.
145
+ *
146
+ * THAT DISTINCTION IS A BUG FIX, and this comment used to assert the bug ("screen
147
+ * readers still reach them"). `overflow: hidden` + `height: 0` CLIPS; it does not hide.
148
+ * The clipped child keeps its own non-empty box, so it stayed focusable by Tab,
149
+ * announced by screen readers, and reported VISIBLE by Playwright — while the owning
150
+ * header said `aria-expanded="false"`. Found via an aml-v2 E2E test that could see a
151
+ * collapsed settings panel.
152
+ *
143
153
  * Reduced-motion snaps to the target height with no animation.
144
154
  */
145
155
 
package/dist/index.js CHANGED
@@ -24,6 +24,9 @@ function collapseTargetHeight(open, measured) {
24
24
  if (measured === null) return void 0;
25
25
  return open ? measured : 0;
26
26
  }
27
+ function collapseSettlesClosed(open, finished) {
28
+ return finished && !open;
29
+ }
27
30
  function resolveScale(pressed, reduced, pressedScale) {
28
31
  if (reduced) return 1;
29
32
  return pressed ? pressedScale : 1;
@@ -132,11 +135,13 @@ var FadeIn = ({
132
135
  }, [progress, translateY]);
133
136
  return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Animated.View, { style: [animatedStyle, style], testID, children });
134
137
  };
138
+ var IS_WEB = reactNative.Platform.OS === "web";
135
139
  var styles = reactNative.StyleSheet.create({
136
140
  outer: {
137
141
  overflow: "hidden"
138
142
  }
139
143
  });
144
+ var HIDDEN_WHEN_SETTLED_CLOSED = { visibility: "hidden" };
140
145
  var Collapse = ({
141
146
  open,
142
147
  children,
@@ -146,6 +151,7 @@ var Collapse = ({
146
151
  }) => {
147
152
  const reduced = rnWebHooks.useReducedMotion();
148
153
  const [measured, setMeasured] = react.useState(null);
154
+ const [settledClosed, setSettledClosed] = react.useState(!open);
149
155
  const heightAnim = react.useRef(new reactNative.Animated.Value(0)).current;
150
156
  const initialized = react.useRef(false);
151
157
  const onContentLayout = react.useCallback((event) => {
@@ -153,16 +159,17 @@ var Collapse = ({
153
159
  setMeasured((previous) => previous === nextHeight ? previous : nextHeight);
154
160
  }, []);
155
161
  react.useEffect(() => {
162
+ if (open) setSettledClosed(false);
156
163
  const target = collapseTargetHeight(open, measured);
157
- if (target === void 0) return void 0;
158
- if (!initialized.current) {
159
- heightAnim.setValue(target);
160
- initialized.current = true;
164
+ if (target === void 0) {
165
+ if (!open) setSettledClosed(true);
161
166
  return void 0;
162
167
  }
163
168
  const effectiveDuration = resolveDuration(duration, reduced);
164
- if (effectiveDuration === 0) {
169
+ if (!initialized.current || effectiveDuration === 0) {
165
170
  heightAnim.setValue(target);
171
+ initialized.current = true;
172
+ if (!open) setSettledClosed(true);
166
173
  return void 0;
167
174
  }
168
175
  const anim = reactNative.Animated.timing(heightAnim, {
@@ -172,11 +179,29 @@ var Collapse = ({
172
179
  // Height is a layout prop — the native driver cannot animate it.
173
180
  useNativeDriver: false
174
181
  });
175
- anim.start();
182
+ anim.start(({ finished }) => {
183
+ if (collapseSettlesClosed(open, finished)) setSettledClosed(true);
184
+ });
176
185
  return () => anim.stop();
177
186
  }, [open, measured, duration, reduced, heightAnim]);
178
187
  const outerHeight = measured === null ? void 0 : heightAnim;
179
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Animated.View, { style: [styles.outer, { height: outerHeight }, style], testID, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { onLayout: onContentLayout, children }) });
188
+ return /* @__PURE__ */ jsxRuntime.jsx(
189
+ reactNative.Animated.View,
190
+ {
191
+ accessibilityElementsHidden: settledClosed,
192
+ "aria-hidden": settledClosed ? true : void 0,
193
+ importantForAccessibility: settledClosed ? "no-hide-descendants" : "auto",
194
+ pointerEvents: settledClosed ? "none" : "auto",
195
+ style: [
196
+ styles.outer,
197
+ { height: outerHeight },
198
+ style,
199
+ settledClosed && IS_WEB ? HIDDEN_WHEN_SETTLED_CLOSED : null
200
+ ],
201
+ testID,
202
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { onLayout: onContentLayout, children })
203
+ }
204
+ );
180
205
  };
181
206
  var PressableScale = ({
182
207
  pressedScale = DEFAULT_PRESSED_SCALE,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/motionTokens.ts","../src/motionUtils.ts","../src/constants.ts","../src/hooks/useEnterExit.ts","../src/FadeIn/FadeIn.tsx","../src/Collapse/Collapse.tsx","../src/PressableScale/PressableScale.tsx","../src/Skeleton/Skeleton.tsx"],"names":["DEFAULT_MOTION","Easing","Platform","useReducedMotion","useRef","Animated","useState","useEffect","anim","useMemo","jsx","StyleSheet","useCallback","View","Pressable","styles"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * motionTokens — the RN-ready motion scale, DERIVED from `@dloizides/design-tokens`.\n *\n * design-tokens ships framework-agnostic VALUES (durations in ms + easings as a\n * `css` string plus `[x1,y1,x2,y2]` bezier control points). This module turns the\n * bezier tuples into RN `Easing.bezier(...)` functions ONCE, so every primitive in\n * this package animates on the same curves the web/CSS side uses — one source, two\n * runtimes. Nothing here is app-specific; motion is uniform across portals.\n */\nimport { Easing, type EasingFunction } from 'react-native';\n\nimport {\n DEFAULT_MOTION,\n type Motion,\n type MotionDurations,\n type MotionEasings,\n} from '@dloizides/design-tokens';\n\n/** Re-export the raw token group so consumers can read durations/css strings directly. */\nexport { DEFAULT_MOTION };\nexport type { Motion, MotionDurations, MotionEasings };\n\n/** Durations in ms (numbers). `instant` (0) is the reduced-motion collapse target. */\nexport const motionDurations: MotionDurations = DEFAULT_MOTION.durations;\n\n/** The easing token group in its raw (`css` + `bezier`) form. */\nexport const motionEasingTokens: MotionEasings = DEFAULT_MOTION.easings;\n\n/** The named RN easing curves, one per {@link MotionEasings} key. */\nexport type MotionEasingName = keyof MotionEasings;\n\n/**\n * RN `EasingFunction`s built from the design-token bezier tuples. Built once at\n * module load; the same three functions are reused by every primitive.\n */\nexport const motionEasings: Record<MotionEasingName, EasingFunction> = {\n standard: Easing.bezier(...motionEasingTokens.standard.bezier),\n decelerate: Easing.bezier(...motionEasingTokens.decelerate.bezier),\n accelerate: Easing.bezier(...motionEasingTokens.accelerate.bezier),\n};\n","/**\n * motionUtils — the pure decision logic behind the primitives, extracted so it can\n * be unit-tested WITHOUT driving RN `Animated` (which needs a running animation\n * frame loop). Each primitive imports these and only wires the result into an\n * `Animated.Value`; the interesting branching lives here where a plain assertion\n * can pin it.\n */\nimport { Platform } from 'react-native';\n\nimport { motionDurations } from './motionTokens';\n\n/**\n * The animation duration a primitive should actually use. Collapses to\n * `instant` (0) under reduced-motion — the single source of the \"every animation\n * collapses to instant\" rule the whole package promises. `0` means \"jump to the\n * end state on the next frame\", i.e. no visible motion.\n */\nexport function resolveDuration(duration: number, reduced: boolean): number {\n return reduced ? motionDurations.instant : duration;\n}\n\n/**\n * Whether RN `Animated` should use the native driver. Layout props (height) can\n * NEVER be native-driven, so callers animating height must pass `false` regardless\n * — this helper is for opacity/transform animations, which are native-drivable on\n * native and must fall back to the JS driver on web.\n */\nexport function shouldUseNativeDriver(): boolean {\n return Platform.OS !== 'web';\n}\n\n/**\n * The target height for a {@link Collapse}: the measured content height when open,\n * `0` when closed. `undefined` while the content has not been measured yet — the\n * caller leaves height `auto` for that first layout pass so the content can report\n * its natural size.\n */\nexport function collapseTargetHeight(open: boolean, measured: number | null): number | undefined {\n if (measured === null) return undefined;\n return open ? measured : 0;\n}\n\n/**\n * The scale a {@link PressableScale} should animate to. `1` (no scale) whenever\n * reduced-motion is on OR the control is not pressed; the pressed scale otherwise.\n */\nexport function resolveScale(pressed: boolean, reduced: boolean, pressedScale: number): number {\n if (reduced) return 1;\n return pressed ? pressedScale : 1;\n}\n\n/**\n * The `[from, to]` translateX range for a {@link Skeleton} shimmer sweep across a\n * block of the given pixel width — the highlight travels from just off the left\n * edge to just off the right edge.\n */\nexport function shimmerRange(width: number): readonly [number, number] {\n return [-width, width];\n}\n","/**\n * Default testID strings for the motion primitives. Consumers can override each\n * via the component's `testID` prop; these are the fallbacks so Playwright/RTL\n * selectors have a stable default.\n */\nexport const MOTION_TEST_IDS = {\n fadeIn: 'fade-in',\n collapse: 'collapse',\n pressableScale: 'pressable-scale',\n skeleton: 'skeleton',\n} as const;\n\n/** Default press-in scale for {@link PressableScale}. */\nexport const DEFAULT_PRESSED_SCALE = 0.96;\n\n/** Default enter translate-Y (px) for {@link FadeIn}. */\nexport const DEFAULT_FADE_TRANSLATE_Y = 8;\n\n/** Default shimmer sweep duration (ms) for {@link Skeleton}. */\nexport const DEFAULT_SKELETON_DURATION_MS = 1200;\n","/**\n * useEnterExit — the reusable enter/exit animation promoted from the ToastHost\n * pattern in `@dloizides/ui-feedback` (fade + optional translate/scale via\n * `Animated`, native driver off on web).\n *\n * The problem it solves: an exiting element must stay MOUNTED until its fade-out\n * finishes, otherwise it vanishes instantly. Callers can't easily track that, so\n * the hook owns a `mounted` flag alongside the animated `style` — keep the node in\n * the tree while `mounted` is true, and it will unmount itself once the exit\n * completes.\n *\n * Reduced-motion (via `@dloizides/rn-web-hooks`) collapses the duration to 0, so\n * both enter and exit are instant and `mounted` flips synchronously.\n */\nimport { useEffect, useMemo, useRef, useState } from 'react';\n\nimport { Animated } from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { motionDurations, motionEasings } from '../motionTokens';\nimport { resolveDuration, shouldUseNativeDriver } from '../motionUtils';\n\nexport interface UseEnterExitOptions {\n /** Whether the element should be visible (entered). */\n visible: boolean;\n /** Enter/exit duration in ms. Defaults to the `base` motion duration (200). */\n duration?: number;\n /**\n * Initial translate-Y offset (px) the element enters from / exits to. `0`\n * (default) disables the translate, leaving a pure fade.\n */\n translateY?: number;\n /**\n * Initial scale the element enters from / exits to. `1` (default) disables the\n * scale, leaving a pure fade.\n */\n fromScale?: number;\n}\n\nexport interface UseEnterExitResult {\n /** Animated style to spread onto an `Animated.View`. */\n style: {\n opacity: Animated.Value;\n transform: Animated.WithAnimatedArray<{ translateY: Animated.AnimatedInterpolation<number> } | { scale: Animated.AnimatedInterpolation<number> }>;\n };\n /** True while the element must remain in the tree (entered OR exiting). */\n mounted: boolean;\n}\n\nexport function useEnterExit(options: UseEnterExitOptions): UseEnterExitResult {\n const { visible, duration = motionDurations.base, translateY = 0, fromScale = 1 } = options;\n\n const reduced = useReducedMotion();\n const progress = useRef(new Animated.Value(visible ? 1 : 0)).current;\n const [mounted, setMounted] = useState(visible);\n\n useEffect(() => {\n const effectiveDuration = resolveDuration(duration, reduced);\n\n if (visible) {\n // Enter: mount immediately, then animate in.\n setMounted(true);\n if (effectiveDuration === 0) {\n progress.setValue(1);\n return undefined;\n }\n const anim = Animated.timing(progress, {\n toValue: 1,\n duration: effectiveDuration,\n easing: motionEasings.decelerate,\n useNativeDriver: shouldUseNativeDriver(),\n });\n anim.start();\n return () => anim.stop();\n }\n\n // Exit: animate out, then unmount once finished.\n if (effectiveDuration === 0) {\n progress.setValue(0);\n setMounted(false);\n return undefined;\n }\n const anim = Animated.timing(progress, {\n toValue: 0,\n duration: effectiveDuration,\n easing: motionEasings.accelerate,\n useNativeDriver: shouldUseNativeDriver(),\n });\n anim.start(({ finished }) => {\n if (finished) setMounted(false);\n });\n return () => anim.stop();\n }, [visible, duration, reduced, progress]);\n\n const style = useMemo(() => {\n const transform: Array<\n | { translateY: Animated.AnimatedInterpolation<number> }\n | { scale: Animated.AnimatedInterpolation<number> }\n > = [];\n if (translateY !== 0) {\n transform.push({\n translateY: progress.interpolate({ inputRange: [0, 1], outputRange: [translateY, 0] }),\n });\n }\n if (fromScale !== 1) {\n transform.push({\n scale: progress.interpolate({ inputRange: [0, 1], outputRange: [fromScale, 1] }),\n });\n }\n return { opacity: progress, transform };\n }, [progress, translateY, fromScale]);\n\n return { style, mounted };\n}\n","/**\n * FadeIn — fades its children in (with a small upward translate) once, on mount.\n * The lightweight, mount-only sibling of {@link useEnterExit} (which handles the\n * full enter/exit lifecycle). Under reduced-motion it renders its children at rest\n * immediately — no fade, no translate.\n */\nimport React, { useEffect, useMemo, useRef } from 'react';\n\nimport { Animated, type StyleProp, type ViewStyle } from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { motionDurations, motionEasings } from '../motionTokens';\nimport { shouldUseNativeDriver } from '../motionUtils';\nimport { DEFAULT_FADE_TRANSLATE_Y, MOTION_TEST_IDS } from '../constants';\n\nexport interface FadeInProps {\n children: React.ReactNode;\n /** Fade duration in ms. Defaults to the `base` motion duration (200). */\n duration?: number;\n /** Delay before the fade starts, in ms. Default 0. */\n delay?: number;\n /** Enter translate-Y offset (px). `0` disables the translate. Default 8. */\n translateY?: number;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nexport const FadeIn = ({\n children,\n duration = motionDurations.base,\n delay = 0,\n translateY = DEFAULT_FADE_TRANSLATE_Y,\n style,\n testID = MOTION_TEST_IDS.fadeIn,\n}: FadeInProps): React.ReactElement => {\n const reduced = useReducedMotion();\n const progress = useRef(new Animated.Value(reduced ? 1 : 0)).current;\n\n useEffect(() => {\n if (reduced) {\n progress.setValue(1);\n return undefined;\n }\n const anim = Animated.timing(progress, {\n toValue: 1,\n duration,\n delay,\n easing: motionEasings.decelerate,\n useNativeDriver: shouldUseNativeDriver(),\n });\n anim.start();\n return () => anim.stop();\n }, [reduced, duration, delay, progress]);\n\n const animatedStyle = useMemo((): Animated.WithAnimatedObject<ViewStyle> => {\n if (translateY === 0) {\n return { opacity: progress };\n }\n return {\n opacity: progress,\n transform: [{ translateY: progress.interpolate({ inputRange: [0, 1], outputRange: [translateY, 0] }) }],\n };\n }, [progress, translateY]);\n\n return (\n <Animated.View style={[animatedStyle, style]} testID={testID}>\n {children}\n </Animated.View>\n );\n};\n\nexport default FadeIn;\n","/**\n * Collapse — an animated height expand/collapse that WORKS ON WEB.\n *\n * WHY NOT LayoutAnimation: RN's `LayoutAnimation` (what ui-layout's Accordion uses)\n * is a NATIVE-only no-op under react-native-web, so an app that relied on it got no\n * animation on the web build. Collapse instead measures the content's natural height\n * via `onLayout` and animates a real `Animated.Value` height — the same code path on\n * web and native. Height is a layout prop, so the native driver can never be used\n * here (`useNativeDriver: false`).\n *\n * The children stay MOUNTED at all times (only clipped by `overflow: hidden`), so\n * their state is preserved across open/close and screen readers still reach them.\n * Reduced-motion snaps to the target height with no animation.\n */\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\n\nimport {\n Animated,\n type LayoutChangeEvent,\n StyleSheet,\n type StyleProp,\n View,\n type ViewStyle,\n} from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { motionDurations, motionEasings } from '../motionTokens';\nimport { collapseTargetHeight, resolveDuration } from '../motionUtils';\nimport { MOTION_TEST_IDS } from '../constants';\n\nconst styles = StyleSheet.create({\n outer: {\n overflow: 'hidden',\n },\n});\n\nexport interface CollapseProps {\n /** Whether the content is expanded. */\n open: boolean;\n children: React.ReactNode;\n /** Expand/collapse duration in ms. Defaults to the `base` motion duration (200). */\n duration?: number;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nexport const Collapse = ({\n open,\n children,\n duration = motionDurations.base,\n style,\n testID = MOTION_TEST_IDS.collapse,\n}: CollapseProps): React.ReactElement => {\n const reduced = useReducedMotion();\n const [measured, setMeasured] = useState<number | null>(null);\n const heightAnim = useRef(new Animated.Value(0)).current;\n // Skip the FIRST animation: on mount we snap to the initial state, then animate\n // only subsequent open/close changes.\n const initialized = useRef(false);\n\n const onContentLayout = useCallback((event: LayoutChangeEvent): void => {\n const nextHeight = event.nativeEvent.layout.height;\n setMeasured((previous) => (previous === nextHeight ? previous : nextHeight));\n }, []);\n\n useEffect(() => {\n const target = collapseTargetHeight(open, measured);\n if (target === undefined) return undefined;\n\n if (!initialized.current) {\n heightAnim.setValue(target);\n initialized.current = true;\n return undefined;\n }\n\n const effectiveDuration = resolveDuration(duration, reduced);\n if (effectiveDuration === 0) {\n heightAnim.setValue(target);\n return undefined;\n }\n\n const anim = Animated.timing(heightAnim, {\n toValue: target,\n duration: effectiveDuration,\n easing: motionEasings.standard,\n // Height is a layout prop — the native driver cannot animate it.\n useNativeDriver: false,\n });\n anim.start();\n return () => anim.stop();\n }, [open, measured, duration, reduced, heightAnim]);\n\n // Until measured, leave height `auto` (undefined) so the content reports its\n // natural size; after that, drive the animated height.\n const outerHeight = measured === null ? undefined : heightAnim;\n\n return (\n <Animated.View style={[styles.outer, { height: outerHeight }, style]} testID={testID}>\n <View onLayout={onContentLayout}>{children}</View>\n </Animated.View>\n );\n};\n\nexport default Collapse;\n","/**\n * PressableScale — a `Pressable` that scales down on press-in and springs back on\n * press-out, giving tactile feedback without the flat opacity dip of a bare\n * `activeOpacity`. Built to replace `activeOpacity` usage in `@dloizides/ui-buttons`.\n *\n * It forwards EVERY `Pressable` prop (onPress, accessibility*, disabled, hitSlop,\n * …) so it is a drop-in wrapper; the scale is applied to an inner `Animated.View`\n * so the outer `Pressable` keeps its full press/a11y behaviour. Under reduced-motion\n * the scale is suppressed entirely (the element stays at scale 1).\n */\nimport React, { useCallback, useRef } from 'react';\n\nimport {\n Animated,\n type GestureResponderEvent,\n Pressable,\n type PressableProps,\n type PressableStateCallbackType,\n type StyleProp,\n type ViewStyle,\n} from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { motionDurations, motionEasings } from '../motionTokens';\nimport { shouldUseNativeDriver } from '../motionUtils';\nimport { DEFAULT_PRESSED_SCALE, MOTION_TEST_IDS } from '../constants';\n\nexport interface PressableScaleProps extends PressableProps {\n /** Scale to shrink to on press-in. Default 0.96. */\n pressedScale?: number;\n /** Style applied to the inner animated wrapper that carries the scale transform. */\n innerStyle?: StyleProp<ViewStyle>;\n}\n\nexport const PressableScale = ({\n pressedScale = DEFAULT_PRESSED_SCALE,\n innerStyle,\n onPressIn,\n onPressOut,\n children,\n testID = MOTION_TEST_IDS.pressableScale,\n ...rest\n}: PressableScaleProps): React.ReactElement => {\n const reduced = useReducedMotion();\n const scale = useRef(new Animated.Value(1)).current;\n\n const animateTo = useCallback(\n (toValue: number): void => {\n if (reduced) {\n scale.setValue(1);\n return;\n }\n Animated.timing(scale, {\n toValue,\n duration: motionDurations.fast,\n easing: motionEasings.standard,\n useNativeDriver: shouldUseNativeDriver(),\n }).start();\n },\n [reduced, scale],\n );\n\n const handlePressIn = useCallback(\n (event: GestureResponderEvent): void => {\n animateTo(pressedScale);\n onPressIn?.(event);\n },\n [animateTo, pressedScale, onPressIn],\n );\n\n const handlePressOut = useCallback(\n (event: GestureResponderEvent): void => {\n animateTo(1);\n onPressOut?.(event);\n },\n [animateTo, onPressOut],\n );\n\n const renderInner = (state: PressableStateCallbackType): React.ReactNode => (\n <Animated.View style={[{ transform: [{ scale }] }, innerStyle]}>\n {typeof children === 'function' ? children(state) : children}\n </Animated.View>\n );\n\n return (\n <Pressable {...rest} testID={testID} onPressIn={handlePressIn} onPressOut={handlePressOut}>\n {renderInner}\n </Pressable>\n );\n};\n\nexport default PressableScale;\n","/**\n * Skeleton — a rounded placeholder block with a highlight band that sweeps across\n * it, upgrading the opacity-pulse skeleton in `@dloizides/ui-feedback` to a moving\n * shimmer (a stronger \"content is loading\" signal).\n *\n * The block measures its own pixel width via `onLayout`, then loops a highlight\n * `Animated.View` from just off the left edge to just off the right edge (transform\n * translateX — native-drivable on native, JS driver on web). Under reduced-motion\n * the sweep is suppressed and a static block is shown, so it never animates against\n * the user's OS preference.\n */\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\n\nimport {\n Animated,\n Easing,\n type LayoutChangeEvent,\n StyleSheet,\n type StyleProp,\n View,\n type ViewStyle,\n} from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { shouldUseNativeDriver, shimmerRange } from '../motionUtils';\nimport { DEFAULT_SKELETON_DURATION_MS, MOTION_TEST_IDS } from '../constants';\n\nconst DEFAULT_HEIGHT = 16;\nconst DEFAULT_BORDER_RADIUS = 4;\nconst DEFAULT_BG = 'rgba(0, 0, 0, 0.08)';\nconst DEFAULT_HIGHLIGHT = 'rgba(255, 255, 255, 0.35)';\nconst HIGHLIGHT_WIDTH_RATIO = 0.4;\n\nconst styles = StyleSheet.create({\n block: {\n overflow: 'hidden',\n },\n highlight: {\n position: 'absolute',\n top: 0,\n bottom: 0,\n },\n});\n\nexport interface SkeletonProps {\n /** Block width. Number (px) or percentage string. Default '100%'. */\n width?: number | string;\n /** Block height in px. Default 16. */\n height?: number;\n /** Corner radius in px. Default 4. */\n borderRadius?: number;\n /** Shimmer sweep duration in ms. Default 1200. */\n duration?: number;\n /** Base block colour. */\n backgroundColor?: string;\n /** Sweeping highlight colour. */\n highlightColor?: string;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nexport const Skeleton = ({\n width = '100%',\n height = DEFAULT_HEIGHT,\n borderRadius = DEFAULT_BORDER_RADIUS,\n duration = DEFAULT_SKELETON_DURATION_MS,\n backgroundColor = DEFAULT_BG,\n highlightColor = DEFAULT_HIGHLIGHT,\n style,\n testID = MOTION_TEST_IDS.skeleton,\n}: SkeletonProps): React.ReactElement => {\n const reduced = useReducedMotion();\n const shimmer = useRef(new Animated.Value(0)).current;\n const [measuredWidth, setMeasuredWidth] = useState(0);\n\n const onLayout = useCallback((event: LayoutChangeEvent): void => {\n const nextWidth = event.nativeEvent.layout.width;\n setMeasuredWidth((previous) => (previous === nextWidth ? previous : nextWidth));\n }, []);\n\n useEffect(() => {\n if (reduced || measuredWidth === 0) return undefined;\n shimmer.setValue(0);\n const anim = Animated.loop(\n Animated.timing(shimmer, {\n toValue: 1,\n duration,\n easing: Easing.linear,\n useNativeDriver: shouldUseNativeDriver(),\n }),\n );\n anim.start();\n return () => anim.stop();\n }, [reduced, measuredWidth, duration, shimmer]);\n\n const [from, to] = shimmerRange(measuredWidth);\n const translateX = shimmer.interpolate({ inputRange: [0, 1], outputRange: [from, to] });\n const highlightWidth = measuredWidth * HIGHLIGHT_WIDTH_RATIO;\n\n return (\n <View\n accessibilityState={{ busy: true }}\n onLayout={onLayout}\n style={[styles.block, { width: width as ViewStyle['width'], height, borderRadius, backgroundColor }, style]}\n testID={testID}\n >\n {reduced || measuredWidth === 0 ? null : (\n <Animated.View\n style={[styles.highlight, { width: highlightWidth, backgroundColor: highlightColor, transform: [{ translateX }] }]}\n />\n )}\n </View>\n );\n};\n\nexport default Skeleton;\n"]}
1
+ {"version":3,"sources":["../src/motionTokens.ts","../src/motionUtils.ts","../src/constants.ts","../src/hooks/useEnterExit.ts","../src/FadeIn/FadeIn.tsx","../src/Collapse/Collapse.tsx","../src/PressableScale/PressableScale.tsx","../src/Skeleton/Skeleton.tsx"],"names":["DEFAULT_MOTION","Easing","Platform","useReducedMotion","useRef","Animated","useState","useEffect","anim","useMemo","jsx","StyleSheet","useCallback","View","Pressable","styles"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * motionTokens — the RN-ready motion scale, DERIVED from `@dloizides/design-tokens`.\n *\n * design-tokens ships framework-agnostic VALUES (durations in ms + easings as a\n * `css` string plus `[x1,y1,x2,y2]` bezier control points). This module turns the\n * bezier tuples into RN `Easing.bezier(...)` functions ONCE, so every primitive in\n * this package animates on the same curves the web/CSS side uses — one source, two\n * runtimes. Nothing here is app-specific; motion is uniform across portals.\n */\nimport { Easing, type EasingFunction } from 'react-native';\n\nimport {\n DEFAULT_MOTION,\n type Motion,\n type MotionDurations,\n type MotionEasings,\n} from '@dloizides/design-tokens';\n\n/** Re-export the raw token group so consumers can read durations/css strings directly. */\nexport { DEFAULT_MOTION };\nexport type { Motion, MotionDurations, MotionEasings };\n\n/** Durations in ms (numbers). `instant` (0) is the reduced-motion collapse target. */\nexport const motionDurations: MotionDurations = DEFAULT_MOTION.durations;\n\n/** The easing token group in its raw (`css` + `bezier`) form. */\nexport const motionEasingTokens: MotionEasings = DEFAULT_MOTION.easings;\n\n/** The named RN easing curves, one per {@link MotionEasings} key. */\nexport type MotionEasingName = keyof MotionEasings;\n\n/**\n * RN `EasingFunction`s built from the design-token bezier tuples. Built once at\n * module load; the same three functions are reused by every primitive.\n */\nexport const motionEasings: Record<MotionEasingName, EasingFunction> = {\n standard: Easing.bezier(...motionEasingTokens.standard.bezier),\n decelerate: Easing.bezier(...motionEasingTokens.decelerate.bezier),\n accelerate: Easing.bezier(...motionEasingTokens.accelerate.bezier),\n};\n","/**\n * motionUtils — the pure decision logic behind the primitives, extracted so it can\n * be unit-tested WITHOUT driving RN `Animated` (which needs a running animation\n * frame loop). Each primitive imports these and only wires the result into an\n * `Animated.Value`; the interesting branching lives here where a plain assertion\n * can pin it.\n */\nimport { Platform } from 'react-native';\n\nimport { motionDurations } from './motionTokens';\n\n/**\n * The animation duration a primitive should actually use. Collapses to\n * `instant` (0) under reduced-motion — the single source of the \"every animation\n * collapses to instant\" rule the whole package promises. `0` means \"jump to the\n * end state on the next frame\", i.e. no visible motion.\n */\nexport function resolveDuration(duration: number, reduced: boolean): number {\n return reduced ? motionDurations.instant : duration;\n}\n\n/**\n * Whether RN `Animated` should use the native driver. Layout props (height) can\n * NEVER be native-driven, so callers animating height must pass `false` regardless\n * — this helper is for opacity/transform animations, which are native-drivable on\n * native and must fall back to the JS driver on web.\n */\nexport function shouldUseNativeDriver(): boolean {\n return Platform.OS !== 'web';\n}\n\n/**\n * The target height for a {@link Collapse}: the measured content height when open,\n * `0` when closed. `undefined` while the content has not been measured yet — the\n * caller leaves height `auto` for that first layout pass so the content can report\n * its natural size.\n */\nexport function collapseTargetHeight(open: boolean, measured: number | null): number | undefined {\n if (measured === null) return undefined;\n return open ? measured : 0;\n}\n\n/**\n * Whether a finished collapse animation leaves the region SETTLED CLOSED — the state in which the\n * content is taken out of the accessibility tree, the tab order and hit-testing.\n *\n * Both halves matter. `!open` alone is true from the animation's FIRST frame, so hiding on it would\n * make the content vanish instead of collapse. `finished` alone is true for a completed EXPAND. And\n * `finished` is false when a reopen interrupted the collapse, where hiding would hide an element\n * that is already on its way back open.\n *\n * This lives here, and not inline in the animation callback, because that callback is unreachable\n * from a jsdom test: `onLayout` needs a `ResizeObserver`, so `Collapse` never measures and never\n * animates there. A test asserting the rule through the DOM would be asserting nothing.\n */\nexport function collapseSettlesClosed(open: boolean, finished: boolean): boolean {\n return finished && !open;\n}\n\n/**\n * The scale a {@link PressableScale} should animate to. `1` (no scale) whenever\n * reduced-motion is on OR the control is not pressed; the pressed scale otherwise.\n */\nexport function resolveScale(pressed: boolean, reduced: boolean, pressedScale: number): number {\n if (reduced) return 1;\n return pressed ? pressedScale : 1;\n}\n\n/**\n * The `[from, to]` translateX range for a {@link Skeleton} shimmer sweep across a\n * block of the given pixel width — the highlight travels from just off the left\n * edge to just off the right edge.\n */\nexport function shimmerRange(width: number): readonly [number, number] {\n return [-width, width];\n}\n","/**\n * Default testID strings for the motion primitives. Consumers can override each\n * via the component's `testID` prop; these are the fallbacks so Playwright/RTL\n * selectors have a stable default.\n */\nexport const MOTION_TEST_IDS = {\n fadeIn: 'fade-in',\n collapse: 'collapse',\n pressableScale: 'pressable-scale',\n skeleton: 'skeleton',\n} as const;\n\n/** Default press-in scale for {@link PressableScale}. */\nexport const DEFAULT_PRESSED_SCALE = 0.96;\n\n/** Default enter translate-Y (px) for {@link FadeIn}. */\nexport const DEFAULT_FADE_TRANSLATE_Y = 8;\n\n/** Default shimmer sweep duration (ms) for {@link Skeleton}. */\nexport const DEFAULT_SKELETON_DURATION_MS = 1200;\n","/**\n * useEnterExit — the reusable enter/exit animation promoted from the ToastHost\n * pattern in `@dloizides/ui-feedback` (fade + optional translate/scale via\n * `Animated`, native driver off on web).\n *\n * The problem it solves: an exiting element must stay MOUNTED until its fade-out\n * finishes, otherwise it vanishes instantly. Callers can't easily track that, so\n * the hook owns a `mounted` flag alongside the animated `style` — keep the node in\n * the tree while `mounted` is true, and it will unmount itself once the exit\n * completes.\n *\n * Reduced-motion (via `@dloizides/rn-web-hooks`) collapses the duration to 0, so\n * both enter and exit are instant and `mounted` flips synchronously.\n */\nimport { useEffect, useMemo, useRef, useState } from 'react';\n\nimport { Animated } from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { motionDurations, motionEasings } from '../motionTokens';\nimport { resolveDuration, shouldUseNativeDriver } from '../motionUtils';\n\nexport interface UseEnterExitOptions {\n /** Whether the element should be visible (entered). */\n visible: boolean;\n /** Enter/exit duration in ms. Defaults to the `base` motion duration (200). */\n duration?: number;\n /**\n * Initial translate-Y offset (px) the element enters from / exits to. `0`\n * (default) disables the translate, leaving a pure fade.\n */\n translateY?: number;\n /**\n * Initial scale the element enters from / exits to. `1` (default) disables the\n * scale, leaving a pure fade.\n */\n fromScale?: number;\n}\n\nexport interface UseEnterExitResult {\n /** Animated style to spread onto an `Animated.View`. */\n style: {\n opacity: Animated.Value;\n transform: Animated.WithAnimatedArray<{ translateY: Animated.AnimatedInterpolation<number> } | { scale: Animated.AnimatedInterpolation<number> }>;\n };\n /** True while the element must remain in the tree (entered OR exiting). */\n mounted: boolean;\n}\n\nexport function useEnterExit(options: UseEnterExitOptions): UseEnterExitResult {\n const { visible, duration = motionDurations.base, translateY = 0, fromScale = 1 } = options;\n\n const reduced = useReducedMotion();\n const progress = useRef(new Animated.Value(visible ? 1 : 0)).current;\n const [mounted, setMounted] = useState(visible);\n\n useEffect(() => {\n const effectiveDuration = resolveDuration(duration, reduced);\n\n if (visible) {\n // Enter: mount immediately, then animate in.\n setMounted(true);\n if (effectiveDuration === 0) {\n progress.setValue(1);\n return undefined;\n }\n const anim = Animated.timing(progress, {\n toValue: 1,\n duration: effectiveDuration,\n easing: motionEasings.decelerate,\n useNativeDriver: shouldUseNativeDriver(),\n });\n anim.start();\n return () => anim.stop();\n }\n\n // Exit: animate out, then unmount once finished.\n if (effectiveDuration === 0) {\n progress.setValue(0);\n setMounted(false);\n return undefined;\n }\n const anim = Animated.timing(progress, {\n toValue: 0,\n duration: effectiveDuration,\n easing: motionEasings.accelerate,\n useNativeDriver: shouldUseNativeDriver(),\n });\n anim.start(({ finished }) => {\n if (finished) setMounted(false);\n });\n return () => anim.stop();\n }, [visible, duration, reduced, progress]);\n\n const style = useMemo(() => {\n const transform: Array<\n | { translateY: Animated.AnimatedInterpolation<number> }\n | { scale: Animated.AnimatedInterpolation<number> }\n > = [];\n if (translateY !== 0) {\n transform.push({\n translateY: progress.interpolate({ inputRange: [0, 1], outputRange: [translateY, 0] }),\n });\n }\n if (fromScale !== 1) {\n transform.push({\n scale: progress.interpolate({ inputRange: [0, 1], outputRange: [fromScale, 1] }),\n });\n }\n return { opacity: progress, transform };\n }, [progress, translateY, fromScale]);\n\n return { style, mounted };\n}\n","/**\n * FadeIn — fades its children in (with a small upward translate) once, on mount.\n * The lightweight, mount-only sibling of {@link useEnterExit} (which handles the\n * full enter/exit lifecycle). Under reduced-motion it renders its children at rest\n * immediately — no fade, no translate.\n */\nimport React, { useEffect, useMemo, useRef } from 'react';\n\nimport { Animated, type StyleProp, type ViewStyle } from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { motionDurations, motionEasings } from '../motionTokens';\nimport { shouldUseNativeDriver } from '../motionUtils';\nimport { DEFAULT_FADE_TRANSLATE_Y, MOTION_TEST_IDS } from '../constants';\n\nexport interface FadeInProps {\n children: React.ReactNode;\n /** Fade duration in ms. Defaults to the `base` motion duration (200). */\n duration?: number;\n /** Delay before the fade starts, in ms. Default 0. */\n delay?: number;\n /** Enter translate-Y offset (px). `0` disables the translate. Default 8. */\n translateY?: number;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nexport const FadeIn = ({\n children,\n duration = motionDurations.base,\n delay = 0,\n translateY = DEFAULT_FADE_TRANSLATE_Y,\n style,\n testID = MOTION_TEST_IDS.fadeIn,\n}: FadeInProps): React.ReactElement => {\n const reduced = useReducedMotion();\n const progress = useRef(new Animated.Value(reduced ? 1 : 0)).current;\n\n useEffect(() => {\n if (reduced) {\n progress.setValue(1);\n return undefined;\n }\n const anim = Animated.timing(progress, {\n toValue: 1,\n duration,\n delay,\n easing: motionEasings.decelerate,\n useNativeDriver: shouldUseNativeDriver(),\n });\n anim.start();\n return () => anim.stop();\n }, [reduced, duration, delay, progress]);\n\n const animatedStyle = useMemo((): Animated.WithAnimatedObject<ViewStyle> => {\n if (translateY === 0) {\n return { opacity: progress };\n }\n return {\n opacity: progress,\n transform: [{ translateY: progress.interpolate({ inputRange: [0, 1], outputRange: [translateY, 0] }) }],\n };\n }, [progress, translateY]);\n\n return (\n <Animated.View style={[animatedStyle, style]} testID={testID}>\n {children}\n </Animated.View>\n );\n};\n\nexport default FadeIn;\n","/**\n * Collapse — an animated height expand/collapse that WORKS ON WEB.\n *\n * WHY NOT LayoutAnimation: RN's `LayoutAnimation` (what ui-layout's Accordion uses)\n * is a NATIVE-only no-op under react-native-web, so an app that relied on it got no\n * animation on the web build. Collapse instead measures the content's natural height\n * via `onLayout` and animates a real `Animated.Value` height — the same code path on\n * web and native. Height is a layout prop, so the native driver can never be used\n * here (`useNativeDriver: false`).\n *\n * The children stay MOUNTED at all times, so their state survives open/close and a\n * consumer's `aria-controls` always resolves to a real node. MOUNTED IS NOT EXPOSED,\n * though: once the collapse has settled shut the content is hidden from the\n * accessibility tree, the tab order and hit-testing.\n *\n * THAT DISTINCTION IS A BUG FIX, and this comment used to assert the bug (\"screen\n * readers still reach them\"). `overflow: hidden` + `height: 0` CLIPS; it does not hide.\n * The clipped child keeps its own non-empty box, so it stayed focusable by Tab,\n * announced by screen readers, and reported VISIBLE by Playwright — while the owning\n * header said `aria-expanded=\"false\"`. Found via an aml-v2 E2E test that could see a\n * collapsed settings panel.\n *\n * Reduced-motion snaps to the target height with no animation.\n */\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\n\nimport {\n Animated,\n type LayoutChangeEvent,\n Platform,\n StyleSheet,\n type StyleProp,\n View,\n type ViewStyle,\n} from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { motionDurations, motionEasings } from '../motionTokens';\nimport { collapseSettlesClosed, collapseTargetHeight, resolveDuration } from '../motionUtils';\nimport { MOTION_TEST_IDS } from '../constants';\n\nconst IS_WEB = Platform.OS === 'web';\n\nconst styles = StyleSheet.create({\n outer: {\n overflow: 'hidden',\n },\n});\n\n/**\n * WEB ONLY, and the whole point of the fix: `visibility: hidden` is what actually makes a collapsed\n * region CLOSED — it takes the content out of the accessibility tree AND the tab order, and both\n * browsers and Playwright then report it hidden. `overflow: hidden` + `height: 0` does none of that.\n *\n * Deliberately NOT `display: none`, which would achieve the same hiding and then zero the `onLayout`\n * measurement this component animates back to — so reopening would animate 0 → 0. `visibility`\n * keeps the layout box, and therefore the measurement, intact.\n */\n// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- `visibility` is a react-native-web style; RN's own ViewStyle does not declare it.\nconst HIDDEN_WHEN_SETTLED_CLOSED = { visibility: 'hidden' } as unknown as ViewStyle;\n\nexport interface CollapseProps {\n /** Whether the content is expanded. */\n open: boolean;\n children: React.ReactNode;\n /** Expand/collapse duration in ms. Defaults to the `base` motion duration (200). */\n duration?: number;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nexport const Collapse = ({\n open,\n children,\n duration = motionDurations.base,\n style,\n testID = MOTION_TEST_IDS.collapse,\n}: CollapseProps): React.ReactElement => {\n const reduced = useReducedMotion();\n const [measured, setMeasured] = useState<number | null>(null);\n // \"Closed AND finished closing\". Distinct from `!open`, which is true from the first frame of the\n // collapse animation — hiding then would make the content vanish instead of collapse.\n const [settledClosed, setSettledClosed] = useState(!open);\n const heightAnim = useRef(new Animated.Value(0)).current;\n // Skip the FIRST animation: on mount we snap to the initial state, then animate\n // only subsequent open/close changes.\n const initialized = useRef(false);\n\n const onContentLayout = useCallback((event: LayoutChangeEvent): void => {\n const nextHeight = event.nativeEvent.layout.height;\n setMeasured((previous) => (previous === nextHeight ? previous : nextHeight));\n }, []);\n\n useEffect(() => {\n // Opening is immediate — the content has to be reachable for the whole expand, not just after it.\n if (open) setSettledClosed(false);\n\n const target = collapseTargetHeight(open, measured);\n if (target === undefined) {\n // Nothing has been measured yet, so there is no height to animate and the close is\n // INSTANT — settled the moment it is asked for. Returning early without settling (as this\n // did first) left a closed region exposed for the whole pre-layout window, which is exactly\n // the defect this state exists to fix: hiding is a property of `open`, not of measurement.\n if (!open) setSettledClosed(true);\n return undefined;\n }\n\n // Two more instant paths: the FIRST effect snaps to the initial state rather than animating\n // it, and reduced motion resolves every duration to 0.\n const effectiveDuration = resolveDuration(duration, reduced);\n if (!initialized.current || effectiveDuration === 0) {\n heightAnim.setValue(target);\n initialized.current = true;\n if (!open) setSettledClosed(true);\n return undefined;\n }\n\n const anim = Animated.timing(heightAnim, {\n toValue: target,\n duration: effectiveDuration,\n easing: motionEasings.standard,\n // Height is a layout prop — the native driver cannot animate it.\n useNativeDriver: false,\n });\n anim.start(({ finished }) => {\n if (collapseSettlesClosed(open, finished)) setSettledClosed(true);\n });\n return () => anim.stop();\n }, [open, measured, duration, reduced, heightAnim]);\n\n // Until measured, leave height `auto` (undefined) so the content reports its\n // natural size; after that, drive the animated height.\n const outerHeight = measured === null ? undefined : heightAnim;\n\n return (\n <Animated.View\n accessibilityElementsHidden={settledClosed}\n aria-hidden={settledClosed ? true : undefined}\n importantForAccessibility={settledClosed ? 'no-hide-descendants' : 'auto'}\n pointerEvents={settledClosed ? 'none' : 'auto'}\n // The hide comes AFTER the caller's `style`: whether a closed region is exposed is a\n // correctness property of Collapse, not something a consumer should override by accident.\n style={[\n styles.outer,\n { height: outerHeight },\n style,\n settledClosed && IS_WEB ? HIDDEN_WHEN_SETTLED_CLOSED : null,\n ]}\n testID={testID}\n >\n <View onLayout={onContentLayout}>{children}</View>\n </Animated.View>\n );\n};\n\nexport default Collapse;\n","/**\n * PressableScale — a `Pressable` that scales down on press-in and springs back on\n * press-out, giving tactile feedback without the flat opacity dip of a bare\n * `activeOpacity`. Built to replace `activeOpacity` usage in `@dloizides/ui-buttons`.\n *\n * It forwards EVERY `Pressable` prop (onPress, accessibility*, disabled, hitSlop,\n * …) so it is a drop-in wrapper; the scale is applied to an inner `Animated.View`\n * so the outer `Pressable` keeps its full press/a11y behaviour. Under reduced-motion\n * the scale is suppressed entirely (the element stays at scale 1).\n */\nimport React, { useCallback, useRef } from 'react';\n\nimport {\n Animated,\n type GestureResponderEvent,\n Pressable,\n type PressableProps,\n type PressableStateCallbackType,\n type StyleProp,\n type ViewStyle,\n} from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { motionDurations, motionEasings } from '../motionTokens';\nimport { shouldUseNativeDriver } from '../motionUtils';\nimport { DEFAULT_PRESSED_SCALE, MOTION_TEST_IDS } from '../constants';\n\nexport interface PressableScaleProps extends PressableProps {\n /** Scale to shrink to on press-in. Default 0.96. */\n pressedScale?: number;\n /** Style applied to the inner animated wrapper that carries the scale transform. */\n innerStyle?: StyleProp<ViewStyle>;\n}\n\nexport const PressableScale = ({\n pressedScale = DEFAULT_PRESSED_SCALE,\n innerStyle,\n onPressIn,\n onPressOut,\n children,\n testID = MOTION_TEST_IDS.pressableScale,\n ...rest\n}: PressableScaleProps): React.ReactElement => {\n const reduced = useReducedMotion();\n const scale = useRef(new Animated.Value(1)).current;\n\n const animateTo = useCallback(\n (toValue: number): void => {\n if (reduced) {\n scale.setValue(1);\n return;\n }\n Animated.timing(scale, {\n toValue,\n duration: motionDurations.fast,\n easing: motionEasings.standard,\n useNativeDriver: shouldUseNativeDriver(),\n }).start();\n },\n [reduced, scale],\n );\n\n const handlePressIn = useCallback(\n (event: GestureResponderEvent): void => {\n animateTo(pressedScale);\n onPressIn?.(event);\n },\n [animateTo, pressedScale, onPressIn],\n );\n\n const handlePressOut = useCallback(\n (event: GestureResponderEvent): void => {\n animateTo(1);\n onPressOut?.(event);\n },\n [animateTo, onPressOut],\n );\n\n const renderInner = (state: PressableStateCallbackType): React.ReactNode => (\n <Animated.View style={[{ transform: [{ scale }] }, innerStyle]}>\n {typeof children === 'function' ? children(state) : children}\n </Animated.View>\n );\n\n return (\n <Pressable {...rest} testID={testID} onPressIn={handlePressIn} onPressOut={handlePressOut}>\n {renderInner}\n </Pressable>\n );\n};\n\nexport default PressableScale;\n","/**\n * Skeleton — a rounded placeholder block with a highlight band that sweeps across\n * it, upgrading the opacity-pulse skeleton in `@dloizides/ui-feedback` to a moving\n * shimmer (a stronger \"content is loading\" signal).\n *\n * The block measures its own pixel width via `onLayout`, then loops a highlight\n * `Animated.View` from just off the left edge to just off the right edge (transform\n * translateX — native-drivable on native, JS driver on web). Under reduced-motion\n * the sweep is suppressed and a static block is shown, so it never animates against\n * the user's OS preference.\n */\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\n\nimport {\n Animated,\n Easing,\n type LayoutChangeEvent,\n StyleSheet,\n type StyleProp,\n View,\n type ViewStyle,\n} from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { shouldUseNativeDriver, shimmerRange } from '../motionUtils';\nimport { DEFAULT_SKELETON_DURATION_MS, MOTION_TEST_IDS } from '../constants';\n\nconst DEFAULT_HEIGHT = 16;\nconst DEFAULT_BORDER_RADIUS = 4;\nconst DEFAULT_BG = 'rgba(0, 0, 0, 0.08)';\nconst DEFAULT_HIGHLIGHT = 'rgba(255, 255, 255, 0.35)';\nconst HIGHLIGHT_WIDTH_RATIO = 0.4;\n\nconst styles = StyleSheet.create({\n block: {\n overflow: 'hidden',\n },\n highlight: {\n position: 'absolute',\n top: 0,\n bottom: 0,\n },\n});\n\nexport interface SkeletonProps {\n /** Block width. Number (px) or percentage string. Default '100%'. */\n width?: number | string;\n /** Block height in px. Default 16. */\n height?: number;\n /** Corner radius in px. Default 4. */\n borderRadius?: number;\n /** Shimmer sweep duration in ms. Default 1200. */\n duration?: number;\n /** Base block colour. */\n backgroundColor?: string;\n /** Sweeping highlight colour. */\n highlightColor?: string;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nexport const Skeleton = ({\n width = '100%',\n height = DEFAULT_HEIGHT,\n borderRadius = DEFAULT_BORDER_RADIUS,\n duration = DEFAULT_SKELETON_DURATION_MS,\n backgroundColor = DEFAULT_BG,\n highlightColor = DEFAULT_HIGHLIGHT,\n style,\n testID = MOTION_TEST_IDS.skeleton,\n}: SkeletonProps): React.ReactElement => {\n const reduced = useReducedMotion();\n const shimmer = useRef(new Animated.Value(0)).current;\n const [measuredWidth, setMeasuredWidth] = useState(0);\n\n const onLayout = useCallback((event: LayoutChangeEvent): void => {\n const nextWidth = event.nativeEvent.layout.width;\n setMeasuredWidth((previous) => (previous === nextWidth ? previous : nextWidth));\n }, []);\n\n useEffect(() => {\n if (reduced || measuredWidth === 0) return undefined;\n shimmer.setValue(0);\n const anim = Animated.loop(\n Animated.timing(shimmer, {\n toValue: 1,\n duration,\n easing: Easing.linear,\n useNativeDriver: shouldUseNativeDriver(),\n }),\n );\n anim.start();\n return () => anim.stop();\n }, [reduced, measuredWidth, duration, shimmer]);\n\n const [from, to] = shimmerRange(measuredWidth);\n const translateX = shimmer.interpolate({ inputRange: [0, 1], outputRange: [from, to] });\n const highlightWidth = measuredWidth * HIGHLIGHT_WIDTH_RATIO;\n\n return (\n <View\n accessibilityState={{ busy: true }}\n onLayout={onLayout}\n style={[styles.block, { width: width as ViewStyle['width'], height, borderRadius, backgroundColor }, style]}\n testID={testID}\n >\n {reduced || measuredWidth === 0 ? null : (\n <Animated.View\n style={[styles.highlight, { width: highlightWidth, backgroundColor: highlightColor, transform: [{ translateX }] }]}\n />\n )}\n </View>\n );\n};\n\nexport default Skeleton;\n"]}
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { useReducedMotion } from '@dloizides/rn-web-hooks';
2
2
  export { prefersReducedMotion, useReducedMotion } from '@dloizides/rn-web-hooks';
3
- import { Easing, StyleSheet, Platform, Animated, View, Pressable } from 'react-native';
3
+ import { Easing, Platform, StyleSheet, Animated, View, Pressable } from 'react-native';
4
4
  import { DEFAULT_MOTION } from '@dloizides/design-tokens';
5
5
  export { DEFAULT_MOTION } from '@dloizides/design-tokens';
6
6
  import { useRef, useState, useEffect, useMemo, useCallback } from 'react';
@@ -24,6 +24,9 @@ function collapseTargetHeight(open, measured) {
24
24
  if (measured === null) return void 0;
25
25
  return open ? measured : 0;
26
26
  }
27
+ function collapseSettlesClosed(open, finished) {
28
+ return finished && !open;
29
+ }
27
30
  function resolveScale(pressed, reduced, pressedScale) {
28
31
  if (reduced) return 1;
29
32
  return pressed ? pressedScale : 1;
@@ -132,11 +135,13 @@ var FadeIn = ({
132
135
  }, [progress, translateY]);
133
136
  return /* @__PURE__ */ jsx(Animated.View, { style: [animatedStyle, style], testID, children });
134
137
  };
138
+ var IS_WEB = Platform.OS === "web";
135
139
  var styles = StyleSheet.create({
136
140
  outer: {
137
141
  overflow: "hidden"
138
142
  }
139
143
  });
144
+ var HIDDEN_WHEN_SETTLED_CLOSED = { visibility: "hidden" };
140
145
  var Collapse = ({
141
146
  open,
142
147
  children,
@@ -146,6 +151,7 @@ var Collapse = ({
146
151
  }) => {
147
152
  const reduced = useReducedMotion();
148
153
  const [measured, setMeasured] = useState(null);
154
+ const [settledClosed, setSettledClosed] = useState(!open);
149
155
  const heightAnim = useRef(new Animated.Value(0)).current;
150
156
  const initialized = useRef(false);
151
157
  const onContentLayout = useCallback((event) => {
@@ -153,16 +159,17 @@ var Collapse = ({
153
159
  setMeasured((previous) => previous === nextHeight ? previous : nextHeight);
154
160
  }, []);
155
161
  useEffect(() => {
162
+ if (open) setSettledClosed(false);
156
163
  const target = collapseTargetHeight(open, measured);
157
- if (target === void 0) return void 0;
158
- if (!initialized.current) {
159
- heightAnim.setValue(target);
160
- initialized.current = true;
164
+ if (target === void 0) {
165
+ if (!open) setSettledClosed(true);
161
166
  return void 0;
162
167
  }
163
168
  const effectiveDuration = resolveDuration(duration, reduced);
164
- if (effectiveDuration === 0) {
169
+ if (!initialized.current || effectiveDuration === 0) {
165
170
  heightAnim.setValue(target);
171
+ initialized.current = true;
172
+ if (!open) setSettledClosed(true);
166
173
  return void 0;
167
174
  }
168
175
  const anim = Animated.timing(heightAnim, {
@@ -172,11 +179,29 @@ var Collapse = ({
172
179
  // Height is a layout prop — the native driver cannot animate it.
173
180
  useNativeDriver: false
174
181
  });
175
- anim.start();
182
+ anim.start(({ finished }) => {
183
+ if (collapseSettlesClosed(open, finished)) setSettledClosed(true);
184
+ });
176
185
  return () => anim.stop();
177
186
  }, [open, measured, duration, reduced, heightAnim]);
178
187
  const outerHeight = measured === null ? void 0 : heightAnim;
179
- return /* @__PURE__ */ jsx(Animated.View, { style: [styles.outer, { height: outerHeight }, style], testID, children: /* @__PURE__ */ jsx(View, { onLayout: onContentLayout, children }) });
188
+ return /* @__PURE__ */ jsx(
189
+ Animated.View,
190
+ {
191
+ accessibilityElementsHidden: settledClosed,
192
+ "aria-hidden": settledClosed ? true : void 0,
193
+ importantForAccessibility: settledClosed ? "no-hide-descendants" : "auto",
194
+ pointerEvents: settledClosed ? "none" : "auto",
195
+ style: [
196
+ styles.outer,
197
+ { height: outerHeight },
198
+ style,
199
+ settledClosed && IS_WEB ? HIDDEN_WHEN_SETTLED_CLOSED : null
200
+ ],
201
+ testID,
202
+ children: /* @__PURE__ */ jsx(View, { onLayout: onContentLayout, children })
203
+ }
204
+ );
180
205
  };
181
206
  var PressableScale = ({
182
207
  pressedScale = DEFAULT_PRESSED_SCALE,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/motionTokens.ts","../src/motionUtils.ts","../src/constants.ts","../src/hooks/useEnterExit.ts","../src/FadeIn/FadeIn.tsx","../src/Collapse/Collapse.tsx","../src/PressableScale/PressableScale.tsx","../src/Skeleton/Skeleton.tsx"],"names":["anim","useReducedMotion","useRef","Animated","useEffect","useMemo","useState","jsx","useCallback","styles","StyleSheet","Easing","View"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * motionTokens — the RN-ready motion scale, DERIVED from `@dloizides/design-tokens`.\n *\n * design-tokens ships framework-agnostic VALUES (durations in ms + easings as a\n * `css` string plus `[x1,y1,x2,y2]` bezier control points). This module turns the\n * bezier tuples into RN `Easing.bezier(...)` functions ONCE, so every primitive in\n * this package animates on the same curves the web/CSS side uses — one source, two\n * runtimes. Nothing here is app-specific; motion is uniform across portals.\n */\nimport { Easing, type EasingFunction } from 'react-native';\n\nimport {\n DEFAULT_MOTION,\n type Motion,\n type MotionDurations,\n type MotionEasings,\n} from '@dloizides/design-tokens';\n\n/** Re-export the raw token group so consumers can read durations/css strings directly. */\nexport { DEFAULT_MOTION };\nexport type { Motion, MotionDurations, MotionEasings };\n\n/** Durations in ms (numbers). `instant` (0) is the reduced-motion collapse target. */\nexport const motionDurations: MotionDurations = DEFAULT_MOTION.durations;\n\n/** The easing token group in its raw (`css` + `bezier`) form. */\nexport const motionEasingTokens: MotionEasings = DEFAULT_MOTION.easings;\n\n/** The named RN easing curves, one per {@link MotionEasings} key. */\nexport type MotionEasingName = keyof MotionEasings;\n\n/**\n * RN `EasingFunction`s built from the design-token bezier tuples. Built once at\n * module load; the same three functions are reused by every primitive.\n */\nexport const motionEasings: Record<MotionEasingName, EasingFunction> = {\n standard: Easing.bezier(...motionEasingTokens.standard.bezier),\n decelerate: Easing.bezier(...motionEasingTokens.decelerate.bezier),\n accelerate: Easing.bezier(...motionEasingTokens.accelerate.bezier),\n};\n","/**\n * motionUtils — the pure decision logic behind the primitives, extracted so it can\n * be unit-tested WITHOUT driving RN `Animated` (which needs a running animation\n * frame loop). Each primitive imports these and only wires the result into an\n * `Animated.Value`; the interesting branching lives here where a plain assertion\n * can pin it.\n */\nimport { Platform } from 'react-native';\n\nimport { motionDurations } from './motionTokens';\n\n/**\n * The animation duration a primitive should actually use. Collapses to\n * `instant` (0) under reduced-motion — the single source of the \"every animation\n * collapses to instant\" rule the whole package promises. `0` means \"jump to the\n * end state on the next frame\", i.e. no visible motion.\n */\nexport function resolveDuration(duration: number, reduced: boolean): number {\n return reduced ? motionDurations.instant : duration;\n}\n\n/**\n * Whether RN `Animated` should use the native driver. Layout props (height) can\n * NEVER be native-driven, so callers animating height must pass `false` regardless\n * — this helper is for opacity/transform animations, which are native-drivable on\n * native and must fall back to the JS driver on web.\n */\nexport function shouldUseNativeDriver(): boolean {\n return Platform.OS !== 'web';\n}\n\n/**\n * The target height for a {@link Collapse}: the measured content height when open,\n * `0` when closed. `undefined` while the content has not been measured yet — the\n * caller leaves height `auto` for that first layout pass so the content can report\n * its natural size.\n */\nexport function collapseTargetHeight(open: boolean, measured: number | null): number | undefined {\n if (measured === null) return undefined;\n return open ? measured : 0;\n}\n\n/**\n * The scale a {@link PressableScale} should animate to. `1` (no scale) whenever\n * reduced-motion is on OR the control is not pressed; the pressed scale otherwise.\n */\nexport function resolveScale(pressed: boolean, reduced: boolean, pressedScale: number): number {\n if (reduced) return 1;\n return pressed ? pressedScale : 1;\n}\n\n/**\n * The `[from, to]` translateX range for a {@link Skeleton} shimmer sweep across a\n * block of the given pixel width — the highlight travels from just off the left\n * edge to just off the right edge.\n */\nexport function shimmerRange(width: number): readonly [number, number] {\n return [-width, width];\n}\n","/**\n * Default testID strings for the motion primitives. Consumers can override each\n * via the component's `testID` prop; these are the fallbacks so Playwright/RTL\n * selectors have a stable default.\n */\nexport const MOTION_TEST_IDS = {\n fadeIn: 'fade-in',\n collapse: 'collapse',\n pressableScale: 'pressable-scale',\n skeleton: 'skeleton',\n} as const;\n\n/** Default press-in scale for {@link PressableScale}. */\nexport const DEFAULT_PRESSED_SCALE = 0.96;\n\n/** Default enter translate-Y (px) for {@link FadeIn}. */\nexport const DEFAULT_FADE_TRANSLATE_Y = 8;\n\n/** Default shimmer sweep duration (ms) for {@link Skeleton}. */\nexport const DEFAULT_SKELETON_DURATION_MS = 1200;\n","/**\n * useEnterExit — the reusable enter/exit animation promoted from the ToastHost\n * pattern in `@dloizides/ui-feedback` (fade + optional translate/scale via\n * `Animated`, native driver off on web).\n *\n * The problem it solves: an exiting element must stay MOUNTED until its fade-out\n * finishes, otherwise it vanishes instantly. Callers can't easily track that, so\n * the hook owns a `mounted` flag alongside the animated `style` — keep the node in\n * the tree while `mounted` is true, and it will unmount itself once the exit\n * completes.\n *\n * Reduced-motion (via `@dloizides/rn-web-hooks`) collapses the duration to 0, so\n * both enter and exit are instant and `mounted` flips synchronously.\n */\nimport { useEffect, useMemo, useRef, useState } from 'react';\n\nimport { Animated } from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { motionDurations, motionEasings } from '../motionTokens';\nimport { resolveDuration, shouldUseNativeDriver } from '../motionUtils';\n\nexport interface UseEnterExitOptions {\n /** Whether the element should be visible (entered). */\n visible: boolean;\n /** Enter/exit duration in ms. Defaults to the `base` motion duration (200). */\n duration?: number;\n /**\n * Initial translate-Y offset (px) the element enters from / exits to. `0`\n * (default) disables the translate, leaving a pure fade.\n */\n translateY?: number;\n /**\n * Initial scale the element enters from / exits to. `1` (default) disables the\n * scale, leaving a pure fade.\n */\n fromScale?: number;\n}\n\nexport interface UseEnterExitResult {\n /** Animated style to spread onto an `Animated.View`. */\n style: {\n opacity: Animated.Value;\n transform: Animated.WithAnimatedArray<{ translateY: Animated.AnimatedInterpolation<number> } | { scale: Animated.AnimatedInterpolation<number> }>;\n };\n /** True while the element must remain in the tree (entered OR exiting). */\n mounted: boolean;\n}\n\nexport function useEnterExit(options: UseEnterExitOptions): UseEnterExitResult {\n const { visible, duration = motionDurations.base, translateY = 0, fromScale = 1 } = options;\n\n const reduced = useReducedMotion();\n const progress = useRef(new Animated.Value(visible ? 1 : 0)).current;\n const [mounted, setMounted] = useState(visible);\n\n useEffect(() => {\n const effectiveDuration = resolveDuration(duration, reduced);\n\n if (visible) {\n // Enter: mount immediately, then animate in.\n setMounted(true);\n if (effectiveDuration === 0) {\n progress.setValue(1);\n return undefined;\n }\n const anim = Animated.timing(progress, {\n toValue: 1,\n duration: effectiveDuration,\n easing: motionEasings.decelerate,\n useNativeDriver: shouldUseNativeDriver(),\n });\n anim.start();\n return () => anim.stop();\n }\n\n // Exit: animate out, then unmount once finished.\n if (effectiveDuration === 0) {\n progress.setValue(0);\n setMounted(false);\n return undefined;\n }\n const anim = Animated.timing(progress, {\n toValue: 0,\n duration: effectiveDuration,\n easing: motionEasings.accelerate,\n useNativeDriver: shouldUseNativeDriver(),\n });\n anim.start(({ finished }) => {\n if (finished) setMounted(false);\n });\n return () => anim.stop();\n }, [visible, duration, reduced, progress]);\n\n const style = useMemo(() => {\n const transform: Array<\n | { translateY: Animated.AnimatedInterpolation<number> }\n | { scale: Animated.AnimatedInterpolation<number> }\n > = [];\n if (translateY !== 0) {\n transform.push({\n translateY: progress.interpolate({ inputRange: [0, 1], outputRange: [translateY, 0] }),\n });\n }\n if (fromScale !== 1) {\n transform.push({\n scale: progress.interpolate({ inputRange: [0, 1], outputRange: [fromScale, 1] }),\n });\n }\n return { opacity: progress, transform };\n }, [progress, translateY, fromScale]);\n\n return { style, mounted };\n}\n","/**\n * FadeIn — fades its children in (with a small upward translate) once, on mount.\n * The lightweight, mount-only sibling of {@link useEnterExit} (which handles the\n * full enter/exit lifecycle). Under reduced-motion it renders its children at rest\n * immediately — no fade, no translate.\n */\nimport React, { useEffect, useMemo, useRef } from 'react';\n\nimport { Animated, type StyleProp, type ViewStyle } from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { motionDurations, motionEasings } from '../motionTokens';\nimport { shouldUseNativeDriver } from '../motionUtils';\nimport { DEFAULT_FADE_TRANSLATE_Y, MOTION_TEST_IDS } from '../constants';\n\nexport interface FadeInProps {\n children: React.ReactNode;\n /** Fade duration in ms. Defaults to the `base` motion duration (200). */\n duration?: number;\n /** Delay before the fade starts, in ms. Default 0. */\n delay?: number;\n /** Enter translate-Y offset (px). `0` disables the translate. Default 8. */\n translateY?: number;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nexport const FadeIn = ({\n children,\n duration = motionDurations.base,\n delay = 0,\n translateY = DEFAULT_FADE_TRANSLATE_Y,\n style,\n testID = MOTION_TEST_IDS.fadeIn,\n}: FadeInProps): React.ReactElement => {\n const reduced = useReducedMotion();\n const progress = useRef(new Animated.Value(reduced ? 1 : 0)).current;\n\n useEffect(() => {\n if (reduced) {\n progress.setValue(1);\n return undefined;\n }\n const anim = Animated.timing(progress, {\n toValue: 1,\n duration,\n delay,\n easing: motionEasings.decelerate,\n useNativeDriver: shouldUseNativeDriver(),\n });\n anim.start();\n return () => anim.stop();\n }, [reduced, duration, delay, progress]);\n\n const animatedStyle = useMemo((): Animated.WithAnimatedObject<ViewStyle> => {\n if (translateY === 0) {\n return { opacity: progress };\n }\n return {\n opacity: progress,\n transform: [{ translateY: progress.interpolate({ inputRange: [0, 1], outputRange: [translateY, 0] }) }],\n };\n }, [progress, translateY]);\n\n return (\n <Animated.View style={[animatedStyle, style]} testID={testID}>\n {children}\n </Animated.View>\n );\n};\n\nexport default FadeIn;\n","/**\n * Collapse — an animated height expand/collapse that WORKS ON WEB.\n *\n * WHY NOT LayoutAnimation: RN's `LayoutAnimation` (what ui-layout's Accordion uses)\n * is a NATIVE-only no-op under react-native-web, so an app that relied on it got no\n * animation on the web build. Collapse instead measures the content's natural height\n * via `onLayout` and animates a real `Animated.Value` height — the same code path on\n * web and native. Height is a layout prop, so the native driver can never be used\n * here (`useNativeDriver: false`).\n *\n * The children stay MOUNTED at all times (only clipped by `overflow: hidden`), so\n * their state is preserved across open/close and screen readers still reach them.\n * Reduced-motion snaps to the target height with no animation.\n */\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\n\nimport {\n Animated,\n type LayoutChangeEvent,\n StyleSheet,\n type StyleProp,\n View,\n type ViewStyle,\n} from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { motionDurations, motionEasings } from '../motionTokens';\nimport { collapseTargetHeight, resolveDuration } from '../motionUtils';\nimport { MOTION_TEST_IDS } from '../constants';\n\nconst styles = StyleSheet.create({\n outer: {\n overflow: 'hidden',\n },\n});\n\nexport interface CollapseProps {\n /** Whether the content is expanded. */\n open: boolean;\n children: React.ReactNode;\n /** Expand/collapse duration in ms. Defaults to the `base` motion duration (200). */\n duration?: number;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nexport const Collapse = ({\n open,\n children,\n duration = motionDurations.base,\n style,\n testID = MOTION_TEST_IDS.collapse,\n}: CollapseProps): React.ReactElement => {\n const reduced = useReducedMotion();\n const [measured, setMeasured] = useState<number | null>(null);\n const heightAnim = useRef(new Animated.Value(0)).current;\n // Skip the FIRST animation: on mount we snap to the initial state, then animate\n // only subsequent open/close changes.\n const initialized = useRef(false);\n\n const onContentLayout = useCallback((event: LayoutChangeEvent): void => {\n const nextHeight = event.nativeEvent.layout.height;\n setMeasured((previous) => (previous === nextHeight ? previous : nextHeight));\n }, []);\n\n useEffect(() => {\n const target = collapseTargetHeight(open, measured);\n if (target === undefined) return undefined;\n\n if (!initialized.current) {\n heightAnim.setValue(target);\n initialized.current = true;\n return undefined;\n }\n\n const effectiveDuration = resolveDuration(duration, reduced);\n if (effectiveDuration === 0) {\n heightAnim.setValue(target);\n return undefined;\n }\n\n const anim = Animated.timing(heightAnim, {\n toValue: target,\n duration: effectiveDuration,\n easing: motionEasings.standard,\n // Height is a layout prop — the native driver cannot animate it.\n useNativeDriver: false,\n });\n anim.start();\n return () => anim.stop();\n }, [open, measured, duration, reduced, heightAnim]);\n\n // Until measured, leave height `auto` (undefined) so the content reports its\n // natural size; after that, drive the animated height.\n const outerHeight = measured === null ? undefined : heightAnim;\n\n return (\n <Animated.View style={[styles.outer, { height: outerHeight }, style]} testID={testID}>\n <View onLayout={onContentLayout}>{children}</View>\n </Animated.View>\n );\n};\n\nexport default Collapse;\n","/**\n * PressableScale — a `Pressable` that scales down on press-in and springs back on\n * press-out, giving tactile feedback without the flat opacity dip of a bare\n * `activeOpacity`. Built to replace `activeOpacity` usage in `@dloizides/ui-buttons`.\n *\n * It forwards EVERY `Pressable` prop (onPress, accessibility*, disabled, hitSlop,\n * …) so it is a drop-in wrapper; the scale is applied to an inner `Animated.View`\n * so the outer `Pressable` keeps its full press/a11y behaviour. Under reduced-motion\n * the scale is suppressed entirely (the element stays at scale 1).\n */\nimport React, { useCallback, useRef } from 'react';\n\nimport {\n Animated,\n type GestureResponderEvent,\n Pressable,\n type PressableProps,\n type PressableStateCallbackType,\n type StyleProp,\n type ViewStyle,\n} from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { motionDurations, motionEasings } from '../motionTokens';\nimport { shouldUseNativeDriver } from '../motionUtils';\nimport { DEFAULT_PRESSED_SCALE, MOTION_TEST_IDS } from '../constants';\n\nexport interface PressableScaleProps extends PressableProps {\n /** Scale to shrink to on press-in. Default 0.96. */\n pressedScale?: number;\n /** Style applied to the inner animated wrapper that carries the scale transform. */\n innerStyle?: StyleProp<ViewStyle>;\n}\n\nexport const PressableScale = ({\n pressedScale = DEFAULT_PRESSED_SCALE,\n innerStyle,\n onPressIn,\n onPressOut,\n children,\n testID = MOTION_TEST_IDS.pressableScale,\n ...rest\n}: PressableScaleProps): React.ReactElement => {\n const reduced = useReducedMotion();\n const scale = useRef(new Animated.Value(1)).current;\n\n const animateTo = useCallback(\n (toValue: number): void => {\n if (reduced) {\n scale.setValue(1);\n return;\n }\n Animated.timing(scale, {\n toValue,\n duration: motionDurations.fast,\n easing: motionEasings.standard,\n useNativeDriver: shouldUseNativeDriver(),\n }).start();\n },\n [reduced, scale],\n );\n\n const handlePressIn = useCallback(\n (event: GestureResponderEvent): void => {\n animateTo(pressedScale);\n onPressIn?.(event);\n },\n [animateTo, pressedScale, onPressIn],\n );\n\n const handlePressOut = useCallback(\n (event: GestureResponderEvent): void => {\n animateTo(1);\n onPressOut?.(event);\n },\n [animateTo, onPressOut],\n );\n\n const renderInner = (state: PressableStateCallbackType): React.ReactNode => (\n <Animated.View style={[{ transform: [{ scale }] }, innerStyle]}>\n {typeof children === 'function' ? children(state) : children}\n </Animated.View>\n );\n\n return (\n <Pressable {...rest} testID={testID} onPressIn={handlePressIn} onPressOut={handlePressOut}>\n {renderInner}\n </Pressable>\n );\n};\n\nexport default PressableScale;\n","/**\n * Skeleton — a rounded placeholder block with a highlight band that sweeps across\n * it, upgrading the opacity-pulse skeleton in `@dloizides/ui-feedback` to a moving\n * shimmer (a stronger \"content is loading\" signal).\n *\n * The block measures its own pixel width via `onLayout`, then loops a highlight\n * `Animated.View` from just off the left edge to just off the right edge (transform\n * translateX — native-drivable on native, JS driver on web). Under reduced-motion\n * the sweep is suppressed and a static block is shown, so it never animates against\n * the user's OS preference.\n */\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\n\nimport {\n Animated,\n Easing,\n type LayoutChangeEvent,\n StyleSheet,\n type StyleProp,\n View,\n type ViewStyle,\n} from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { shouldUseNativeDriver, shimmerRange } from '../motionUtils';\nimport { DEFAULT_SKELETON_DURATION_MS, MOTION_TEST_IDS } from '../constants';\n\nconst DEFAULT_HEIGHT = 16;\nconst DEFAULT_BORDER_RADIUS = 4;\nconst DEFAULT_BG = 'rgba(0, 0, 0, 0.08)';\nconst DEFAULT_HIGHLIGHT = 'rgba(255, 255, 255, 0.35)';\nconst HIGHLIGHT_WIDTH_RATIO = 0.4;\n\nconst styles = StyleSheet.create({\n block: {\n overflow: 'hidden',\n },\n highlight: {\n position: 'absolute',\n top: 0,\n bottom: 0,\n },\n});\n\nexport interface SkeletonProps {\n /** Block width. Number (px) or percentage string. Default '100%'. */\n width?: number | string;\n /** Block height in px. Default 16. */\n height?: number;\n /** Corner radius in px. Default 4. */\n borderRadius?: number;\n /** Shimmer sweep duration in ms. Default 1200. */\n duration?: number;\n /** Base block colour. */\n backgroundColor?: string;\n /** Sweeping highlight colour. */\n highlightColor?: string;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nexport const Skeleton = ({\n width = '100%',\n height = DEFAULT_HEIGHT,\n borderRadius = DEFAULT_BORDER_RADIUS,\n duration = DEFAULT_SKELETON_DURATION_MS,\n backgroundColor = DEFAULT_BG,\n highlightColor = DEFAULT_HIGHLIGHT,\n style,\n testID = MOTION_TEST_IDS.skeleton,\n}: SkeletonProps): React.ReactElement => {\n const reduced = useReducedMotion();\n const shimmer = useRef(new Animated.Value(0)).current;\n const [measuredWidth, setMeasuredWidth] = useState(0);\n\n const onLayout = useCallback((event: LayoutChangeEvent): void => {\n const nextWidth = event.nativeEvent.layout.width;\n setMeasuredWidth((previous) => (previous === nextWidth ? previous : nextWidth));\n }, []);\n\n useEffect(() => {\n if (reduced || measuredWidth === 0) return undefined;\n shimmer.setValue(0);\n const anim = Animated.loop(\n Animated.timing(shimmer, {\n toValue: 1,\n duration,\n easing: Easing.linear,\n useNativeDriver: shouldUseNativeDriver(),\n }),\n );\n anim.start();\n return () => anim.stop();\n }, [reduced, measuredWidth, duration, shimmer]);\n\n const [from, to] = shimmerRange(measuredWidth);\n const translateX = shimmer.interpolate({ inputRange: [0, 1], outputRange: [from, to] });\n const highlightWidth = measuredWidth * HIGHLIGHT_WIDTH_RATIO;\n\n return (\n <View\n accessibilityState={{ busy: true }}\n onLayout={onLayout}\n style={[styles.block, { width: width as ViewStyle['width'], height, borderRadius, backgroundColor }, style]}\n testID={testID}\n >\n {reduced || measuredWidth === 0 ? null : (\n <Animated.View\n style={[styles.highlight, { width: highlightWidth, backgroundColor: highlightColor, transform: [{ translateX }] }]}\n />\n )}\n </View>\n );\n};\n\nexport default Skeleton;\n"]}
1
+ {"version":3,"sources":["../src/motionTokens.ts","../src/motionUtils.ts","../src/constants.ts","../src/hooks/useEnterExit.ts","../src/FadeIn/FadeIn.tsx","../src/Collapse/Collapse.tsx","../src/PressableScale/PressableScale.tsx","../src/Skeleton/Skeleton.tsx"],"names":["anim","useReducedMotion","useRef","Animated","useEffect","useMemo","Platform","useState","jsx","useCallback","styles","StyleSheet","Easing","View"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * motionTokens — the RN-ready motion scale, DERIVED from `@dloizides/design-tokens`.\n *\n * design-tokens ships framework-agnostic VALUES (durations in ms + easings as a\n * `css` string plus `[x1,y1,x2,y2]` bezier control points). This module turns the\n * bezier tuples into RN `Easing.bezier(...)` functions ONCE, so every primitive in\n * this package animates on the same curves the web/CSS side uses — one source, two\n * runtimes. Nothing here is app-specific; motion is uniform across portals.\n */\nimport { Easing, type EasingFunction } from 'react-native';\n\nimport {\n DEFAULT_MOTION,\n type Motion,\n type MotionDurations,\n type MotionEasings,\n} from '@dloizides/design-tokens';\n\n/** Re-export the raw token group so consumers can read durations/css strings directly. */\nexport { DEFAULT_MOTION };\nexport type { Motion, MotionDurations, MotionEasings };\n\n/** Durations in ms (numbers). `instant` (0) is the reduced-motion collapse target. */\nexport const motionDurations: MotionDurations = DEFAULT_MOTION.durations;\n\n/** The easing token group in its raw (`css` + `bezier`) form. */\nexport const motionEasingTokens: MotionEasings = DEFAULT_MOTION.easings;\n\n/** The named RN easing curves, one per {@link MotionEasings} key. */\nexport type MotionEasingName = keyof MotionEasings;\n\n/**\n * RN `EasingFunction`s built from the design-token bezier tuples. Built once at\n * module load; the same three functions are reused by every primitive.\n */\nexport const motionEasings: Record<MotionEasingName, EasingFunction> = {\n standard: Easing.bezier(...motionEasingTokens.standard.bezier),\n decelerate: Easing.bezier(...motionEasingTokens.decelerate.bezier),\n accelerate: Easing.bezier(...motionEasingTokens.accelerate.bezier),\n};\n","/**\n * motionUtils — the pure decision logic behind the primitives, extracted so it can\n * be unit-tested WITHOUT driving RN `Animated` (which needs a running animation\n * frame loop). Each primitive imports these and only wires the result into an\n * `Animated.Value`; the interesting branching lives here where a plain assertion\n * can pin it.\n */\nimport { Platform } from 'react-native';\n\nimport { motionDurations } from './motionTokens';\n\n/**\n * The animation duration a primitive should actually use. Collapses to\n * `instant` (0) under reduced-motion — the single source of the \"every animation\n * collapses to instant\" rule the whole package promises. `0` means \"jump to the\n * end state on the next frame\", i.e. no visible motion.\n */\nexport function resolveDuration(duration: number, reduced: boolean): number {\n return reduced ? motionDurations.instant : duration;\n}\n\n/**\n * Whether RN `Animated` should use the native driver. Layout props (height) can\n * NEVER be native-driven, so callers animating height must pass `false` regardless\n * — this helper is for opacity/transform animations, which are native-drivable on\n * native and must fall back to the JS driver on web.\n */\nexport function shouldUseNativeDriver(): boolean {\n return Platform.OS !== 'web';\n}\n\n/**\n * The target height for a {@link Collapse}: the measured content height when open,\n * `0` when closed. `undefined` while the content has not been measured yet — the\n * caller leaves height `auto` for that first layout pass so the content can report\n * its natural size.\n */\nexport function collapseTargetHeight(open: boolean, measured: number | null): number | undefined {\n if (measured === null) return undefined;\n return open ? measured : 0;\n}\n\n/**\n * Whether a finished collapse animation leaves the region SETTLED CLOSED — the state in which the\n * content is taken out of the accessibility tree, the tab order and hit-testing.\n *\n * Both halves matter. `!open` alone is true from the animation's FIRST frame, so hiding on it would\n * make the content vanish instead of collapse. `finished` alone is true for a completed EXPAND. And\n * `finished` is false when a reopen interrupted the collapse, where hiding would hide an element\n * that is already on its way back open.\n *\n * This lives here, and not inline in the animation callback, because that callback is unreachable\n * from a jsdom test: `onLayout` needs a `ResizeObserver`, so `Collapse` never measures and never\n * animates there. A test asserting the rule through the DOM would be asserting nothing.\n */\nexport function collapseSettlesClosed(open: boolean, finished: boolean): boolean {\n return finished && !open;\n}\n\n/**\n * The scale a {@link PressableScale} should animate to. `1` (no scale) whenever\n * reduced-motion is on OR the control is not pressed; the pressed scale otherwise.\n */\nexport function resolveScale(pressed: boolean, reduced: boolean, pressedScale: number): number {\n if (reduced) return 1;\n return pressed ? pressedScale : 1;\n}\n\n/**\n * The `[from, to]` translateX range for a {@link Skeleton} shimmer sweep across a\n * block of the given pixel width — the highlight travels from just off the left\n * edge to just off the right edge.\n */\nexport function shimmerRange(width: number): readonly [number, number] {\n return [-width, width];\n}\n","/**\n * Default testID strings for the motion primitives. Consumers can override each\n * via the component's `testID` prop; these are the fallbacks so Playwright/RTL\n * selectors have a stable default.\n */\nexport const MOTION_TEST_IDS = {\n fadeIn: 'fade-in',\n collapse: 'collapse',\n pressableScale: 'pressable-scale',\n skeleton: 'skeleton',\n} as const;\n\n/** Default press-in scale for {@link PressableScale}. */\nexport const DEFAULT_PRESSED_SCALE = 0.96;\n\n/** Default enter translate-Y (px) for {@link FadeIn}. */\nexport const DEFAULT_FADE_TRANSLATE_Y = 8;\n\n/** Default shimmer sweep duration (ms) for {@link Skeleton}. */\nexport const DEFAULT_SKELETON_DURATION_MS = 1200;\n","/**\n * useEnterExit — the reusable enter/exit animation promoted from the ToastHost\n * pattern in `@dloizides/ui-feedback` (fade + optional translate/scale via\n * `Animated`, native driver off on web).\n *\n * The problem it solves: an exiting element must stay MOUNTED until its fade-out\n * finishes, otherwise it vanishes instantly. Callers can't easily track that, so\n * the hook owns a `mounted` flag alongside the animated `style` — keep the node in\n * the tree while `mounted` is true, and it will unmount itself once the exit\n * completes.\n *\n * Reduced-motion (via `@dloizides/rn-web-hooks`) collapses the duration to 0, so\n * both enter and exit are instant and `mounted` flips synchronously.\n */\nimport { useEffect, useMemo, useRef, useState } from 'react';\n\nimport { Animated } from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { motionDurations, motionEasings } from '../motionTokens';\nimport { resolveDuration, shouldUseNativeDriver } from '../motionUtils';\n\nexport interface UseEnterExitOptions {\n /** Whether the element should be visible (entered). */\n visible: boolean;\n /** Enter/exit duration in ms. Defaults to the `base` motion duration (200). */\n duration?: number;\n /**\n * Initial translate-Y offset (px) the element enters from / exits to. `0`\n * (default) disables the translate, leaving a pure fade.\n */\n translateY?: number;\n /**\n * Initial scale the element enters from / exits to. `1` (default) disables the\n * scale, leaving a pure fade.\n */\n fromScale?: number;\n}\n\nexport interface UseEnterExitResult {\n /** Animated style to spread onto an `Animated.View`. */\n style: {\n opacity: Animated.Value;\n transform: Animated.WithAnimatedArray<{ translateY: Animated.AnimatedInterpolation<number> } | { scale: Animated.AnimatedInterpolation<number> }>;\n };\n /** True while the element must remain in the tree (entered OR exiting). */\n mounted: boolean;\n}\n\nexport function useEnterExit(options: UseEnterExitOptions): UseEnterExitResult {\n const { visible, duration = motionDurations.base, translateY = 0, fromScale = 1 } = options;\n\n const reduced = useReducedMotion();\n const progress = useRef(new Animated.Value(visible ? 1 : 0)).current;\n const [mounted, setMounted] = useState(visible);\n\n useEffect(() => {\n const effectiveDuration = resolveDuration(duration, reduced);\n\n if (visible) {\n // Enter: mount immediately, then animate in.\n setMounted(true);\n if (effectiveDuration === 0) {\n progress.setValue(1);\n return undefined;\n }\n const anim = Animated.timing(progress, {\n toValue: 1,\n duration: effectiveDuration,\n easing: motionEasings.decelerate,\n useNativeDriver: shouldUseNativeDriver(),\n });\n anim.start();\n return () => anim.stop();\n }\n\n // Exit: animate out, then unmount once finished.\n if (effectiveDuration === 0) {\n progress.setValue(0);\n setMounted(false);\n return undefined;\n }\n const anim = Animated.timing(progress, {\n toValue: 0,\n duration: effectiveDuration,\n easing: motionEasings.accelerate,\n useNativeDriver: shouldUseNativeDriver(),\n });\n anim.start(({ finished }) => {\n if (finished) setMounted(false);\n });\n return () => anim.stop();\n }, [visible, duration, reduced, progress]);\n\n const style = useMemo(() => {\n const transform: Array<\n | { translateY: Animated.AnimatedInterpolation<number> }\n | { scale: Animated.AnimatedInterpolation<number> }\n > = [];\n if (translateY !== 0) {\n transform.push({\n translateY: progress.interpolate({ inputRange: [0, 1], outputRange: [translateY, 0] }),\n });\n }\n if (fromScale !== 1) {\n transform.push({\n scale: progress.interpolate({ inputRange: [0, 1], outputRange: [fromScale, 1] }),\n });\n }\n return { opacity: progress, transform };\n }, [progress, translateY, fromScale]);\n\n return { style, mounted };\n}\n","/**\n * FadeIn — fades its children in (with a small upward translate) once, on mount.\n * The lightweight, mount-only sibling of {@link useEnterExit} (which handles the\n * full enter/exit lifecycle). Under reduced-motion it renders its children at rest\n * immediately — no fade, no translate.\n */\nimport React, { useEffect, useMemo, useRef } from 'react';\n\nimport { Animated, type StyleProp, type ViewStyle } from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { motionDurations, motionEasings } from '../motionTokens';\nimport { shouldUseNativeDriver } from '../motionUtils';\nimport { DEFAULT_FADE_TRANSLATE_Y, MOTION_TEST_IDS } from '../constants';\n\nexport interface FadeInProps {\n children: React.ReactNode;\n /** Fade duration in ms. Defaults to the `base` motion duration (200). */\n duration?: number;\n /** Delay before the fade starts, in ms. Default 0. */\n delay?: number;\n /** Enter translate-Y offset (px). `0` disables the translate. Default 8. */\n translateY?: number;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nexport const FadeIn = ({\n children,\n duration = motionDurations.base,\n delay = 0,\n translateY = DEFAULT_FADE_TRANSLATE_Y,\n style,\n testID = MOTION_TEST_IDS.fadeIn,\n}: FadeInProps): React.ReactElement => {\n const reduced = useReducedMotion();\n const progress = useRef(new Animated.Value(reduced ? 1 : 0)).current;\n\n useEffect(() => {\n if (reduced) {\n progress.setValue(1);\n return undefined;\n }\n const anim = Animated.timing(progress, {\n toValue: 1,\n duration,\n delay,\n easing: motionEasings.decelerate,\n useNativeDriver: shouldUseNativeDriver(),\n });\n anim.start();\n return () => anim.stop();\n }, [reduced, duration, delay, progress]);\n\n const animatedStyle = useMemo((): Animated.WithAnimatedObject<ViewStyle> => {\n if (translateY === 0) {\n return { opacity: progress };\n }\n return {\n opacity: progress,\n transform: [{ translateY: progress.interpolate({ inputRange: [0, 1], outputRange: [translateY, 0] }) }],\n };\n }, [progress, translateY]);\n\n return (\n <Animated.View style={[animatedStyle, style]} testID={testID}>\n {children}\n </Animated.View>\n );\n};\n\nexport default FadeIn;\n","/**\n * Collapse — an animated height expand/collapse that WORKS ON WEB.\n *\n * WHY NOT LayoutAnimation: RN's `LayoutAnimation` (what ui-layout's Accordion uses)\n * is a NATIVE-only no-op under react-native-web, so an app that relied on it got no\n * animation on the web build. Collapse instead measures the content's natural height\n * via `onLayout` and animates a real `Animated.Value` height — the same code path on\n * web and native. Height is a layout prop, so the native driver can never be used\n * here (`useNativeDriver: false`).\n *\n * The children stay MOUNTED at all times, so their state survives open/close and a\n * consumer's `aria-controls` always resolves to a real node. MOUNTED IS NOT EXPOSED,\n * though: once the collapse has settled shut the content is hidden from the\n * accessibility tree, the tab order and hit-testing.\n *\n * THAT DISTINCTION IS A BUG FIX, and this comment used to assert the bug (\"screen\n * readers still reach them\"). `overflow: hidden` + `height: 0` CLIPS; it does not hide.\n * The clipped child keeps its own non-empty box, so it stayed focusable by Tab,\n * announced by screen readers, and reported VISIBLE by Playwright — while the owning\n * header said `aria-expanded=\"false\"`. Found via an aml-v2 E2E test that could see a\n * collapsed settings panel.\n *\n * Reduced-motion snaps to the target height with no animation.\n */\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\n\nimport {\n Animated,\n type LayoutChangeEvent,\n Platform,\n StyleSheet,\n type StyleProp,\n View,\n type ViewStyle,\n} from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { motionDurations, motionEasings } from '../motionTokens';\nimport { collapseSettlesClosed, collapseTargetHeight, resolveDuration } from '../motionUtils';\nimport { MOTION_TEST_IDS } from '../constants';\n\nconst IS_WEB = Platform.OS === 'web';\n\nconst styles = StyleSheet.create({\n outer: {\n overflow: 'hidden',\n },\n});\n\n/**\n * WEB ONLY, and the whole point of the fix: `visibility: hidden` is what actually makes a collapsed\n * region CLOSED — it takes the content out of the accessibility tree AND the tab order, and both\n * browsers and Playwright then report it hidden. `overflow: hidden` + `height: 0` does none of that.\n *\n * Deliberately NOT `display: none`, which would achieve the same hiding and then zero the `onLayout`\n * measurement this component animates back to — so reopening would animate 0 → 0. `visibility`\n * keeps the layout box, and therefore the measurement, intact.\n */\n// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- `visibility` is a react-native-web style; RN's own ViewStyle does not declare it.\nconst HIDDEN_WHEN_SETTLED_CLOSED = { visibility: 'hidden' } as unknown as ViewStyle;\n\nexport interface CollapseProps {\n /** Whether the content is expanded. */\n open: boolean;\n children: React.ReactNode;\n /** Expand/collapse duration in ms. Defaults to the `base` motion duration (200). */\n duration?: number;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nexport const Collapse = ({\n open,\n children,\n duration = motionDurations.base,\n style,\n testID = MOTION_TEST_IDS.collapse,\n}: CollapseProps): React.ReactElement => {\n const reduced = useReducedMotion();\n const [measured, setMeasured] = useState<number | null>(null);\n // \"Closed AND finished closing\". Distinct from `!open`, which is true from the first frame of the\n // collapse animation — hiding then would make the content vanish instead of collapse.\n const [settledClosed, setSettledClosed] = useState(!open);\n const heightAnim = useRef(new Animated.Value(0)).current;\n // Skip the FIRST animation: on mount we snap to the initial state, then animate\n // only subsequent open/close changes.\n const initialized = useRef(false);\n\n const onContentLayout = useCallback((event: LayoutChangeEvent): void => {\n const nextHeight = event.nativeEvent.layout.height;\n setMeasured((previous) => (previous === nextHeight ? previous : nextHeight));\n }, []);\n\n useEffect(() => {\n // Opening is immediate — the content has to be reachable for the whole expand, not just after it.\n if (open) setSettledClosed(false);\n\n const target = collapseTargetHeight(open, measured);\n if (target === undefined) {\n // Nothing has been measured yet, so there is no height to animate and the close is\n // INSTANT — settled the moment it is asked for. Returning early without settling (as this\n // did first) left a closed region exposed for the whole pre-layout window, which is exactly\n // the defect this state exists to fix: hiding is a property of `open`, not of measurement.\n if (!open) setSettledClosed(true);\n return undefined;\n }\n\n // Two more instant paths: the FIRST effect snaps to the initial state rather than animating\n // it, and reduced motion resolves every duration to 0.\n const effectiveDuration = resolveDuration(duration, reduced);\n if (!initialized.current || effectiveDuration === 0) {\n heightAnim.setValue(target);\n initialized.current = true;\n if (!open) setSettledClosed(true);\n return undefined;\n }\n\n const anim = Animated.timing(heightAnim, {\n toValue: target,\n duration: effectiveDuration,\n easing: motionEasings.standard,\n // Height is a layout prop — the native driver cannot animate it.\n useNativeDriver: false,\n });\n anim.start(({ finished }) => {\n if (collapseSettlesClosed(open, finished)) setSettledClosed(true);\n });\n return () => anim.stop();\n }, [open, measured, duration, reduced, heightAnim]);\n\n // Until measured, leave height `auto` (undefined) so the content reports its\n // natural size; after that, drive the animated height.\n const outerHeight = measured === null ? undefined : heightAnim;\n\n return (\n <Animated.View\n accessibilityElementsHidden={settledClosed}\n aria-hidden={settledClosed ? true : undefined}\n importantForAccessibility={settledClosed ? 'no-hide-descendants' : 'auto'}\n pointerEvents={settledClosed ? 'none' : 'auto'}\n // The hide comes AFTER the caller's `style`: whether a closed region is exposed is a\n // correctness property of Collapse, not something a consumer should override by accident.\n style={[\n styles.outer,\n { height: outerHeight },\n style,\n settledClosed && IS_WEB ? HIDDEN_WHEN_SETTLED_CLOSED : null,\n ]}\n testID={testID}\n >\n <View onLayout={onContentLayout}>{children}</View>\n </Animated.View>\n );\n};\n\nexport default Collapse;\n","/**\n * PressableScale — a `Pressable` that scales down on press-in and springs back on\n * press-out, giving tactile feedback without the flat opacity dip of a bare\n * `activeOpacity`. Built to replace `activeOpacity` usage in `@dloizides/ui-buttons`.\n *\n * It forwards EVERY `Pressable` prop (onPress, accessibility*, disabled, hitSlop,\n * …) so it is a drop-in wrapper; the scale is applied to an inner `Animated.View`\n * so the outer `Pressable` keeps its full press/a11y behaviour. Under reduced-motion\n * the scale is suppressed entirely (the element stays at scale 1).\n */\nimport React, { useCallback, useRef } from 'react';\n\nimport {\n Animated,\n type GestureResponderEvent,\n Pressable,\n type PressableProps,\n type PressableStateCallbackType,\n type StyleProp,\n type ViewStyle,\n} from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { motionDurations, motionEasings } from '../motionTokens';\nimport { shouldUseNativeDriver } from '../motionUtils';\nimport { DEFAULT_PRESSED_SCALE, MOTION_TEST_IDS } from '../constants';\n\nexport interface PressableScaleProps extends PressableProps {\n /** Scale to shrink to on press-in. Default 0.96. */\n pressedScale?: number;\n /** Style applied to the inner animated wrapper that carries the scale transform. */\n innerStyle?: StyleProp<ViewStyle>;\n}\n\nexport const PressableScale = ({\n pressedScale = DEFAULT_PRESSED_SCALE,\n innerStyle,\n onPressIn,\n onPressOut,\n children,\n testID = MOTION_TEST_IDS.pressableScale,\n ...rest\n}: PressableScaleProps): React.ReactElement => {\n const reduced = useReducedMotion();\n const scale = useRef(new Animated.Value(1)).current;\n\n const animateTo = useCallback(\n (toValue: number): void => {\n if (reduced) {\n scale.setValue(1);\n return;\n }\n Animated.timing(scale, {\n toValue,\n duration: motionDurations.fast,\n easing: motionEasings.standard,\n useNativeDriver: shouldUseNativeDriver(),\n }).start();\n },\n [reduced, scale],\n );\n\n const handlePressIn = useCallback(\n (event: GestureResponderEvent): void => {\n animateTo(pressedScale);\n onPressIn?.(event);\n },\n [animateTo, pressedScale, onPressIn],\n );\n\n const handlePressOut = useCallback(\n (event: GestureResponderEvent): void => {\n animateTo(1);\n onPressOut?.(event);\n },\n [animateTo, onPressOut],\n );\n\n const renderInner = (state: PressableStateCallbackType): React.ReactNode => (\n <Animated.View style={[{ transform: [{ scale }] }, innerStyle]}>\n {typeof children === 'function' ? children(state) : children}\n </Animated.View>\n );\n\n return (\n <Pressable {...rest} testID={testID} onPressIn={handlePressIn} onPressOut={handlePressOut}>\n {renderInner}\n </Pressable>\n );\n};\n\nexport default PressableScale;\n","/**\n * Skeleton — a rounded placeholder block with a highlight band that sweeps across\n * it, upgrading the opacity-pulse skeleton in `@dloizides/ui-feedback` to a moving\n * shimmer (a stronger \"content is loading\" signal).\n *\n * The block measures its own pixel width via `onLayout`, then loops a highlight\n * `Animated.View` from just off the left edge to just off the right edge (transform\n * translateX — native-drivable on native, JS driver on web). Under reduced-motion\n * the sweep is suppressed and a static block is shown, so it never animates against\n * the user's OS preference.\n */\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\n\nimport {\n Animated,\n Easing,\n type LayoutChangeEvent,\n StyleSheet,\n type StyleProp,\n View,\n type ViewStyle,\n} from 'react-native';\n\nimport { useReducedMotion } from '@dloizides/rn-web-hooks';\n\nimport { shouldUseNativeDriver, shimmerRange } from '../motionUtils';\nimport { DEFAULT_SKELETON_DURATION_MS, MOTION_TEST_IDS } from '../constants';\n\nconst DEFAULT_HEIGHT = 16;\nconst DEFAULT_BORDER_RADIUS = 4;\nconst DEFAULT_BG = 'rgba(0, 0, 0, 0.08)';\nconst DEFAULT_HIGHLIGHT = 'rgba(255, 255, 255, 0.35)';\nconst HIGHLIGHT_WIDTH_RATIO = 0.4;\n\nconst styles = StyleSheet.create({\n block: {\n overflow: 'hidden',\n },\n highlight: {\n position: 'absolute',\n top: 0,\n bottom: 0,\n },\n});\n\nexport interface SkeletonProps {\n /** Block width. Number (px) or percentage string. Default '100%'. */\n width?: number | string;\n /** Block height in px. Default 16. */\n height?: number;\n /** Corner radius in px. Default 4. */\n borderRadius?: number;\n /** Shimmer sweep duration in ms. Default 1200. */\n duration?: number;\n /** Base block colour. */\n backgroundColor?: string;\n /** Sweeping highlight colour. */\n highlightColor?: string;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nexport const Skeleton = ({\n width = '100%',\n height = DEFAULT_HEIGHT,\n borderRadius = DEFAULT_BORDER_RADIUS,\n duration = DEFAULT_SKELETON_DURATION_MS,\n backgroundColor = DEFAULT_BG,\n highlightColor = DEFAULT_HIGHLIGHT,\n style,\n testID = MOTION_TEST_IDS.skeleton,\n}: SkeletonProps): React.ReactElement => {\n const reduced = useReducedMotion();\n const shimmer = useRef(new Animated.Value(0)).current;\n const [measuredWidth, setMeasuredWidth] = useState(0);\n\n const onLayout = useCallback((event: LayoutChangeEvent): void => {\n const nextWidth = event.nativeEvent.layout.width;\n setMeasuredWidth((previous) => (previous === nextWidth ? previous : nextWidth));\n }, []);\n\n useEffect(() => {\n if (reduced || measuredWidth === 0) return undefined;\n shimmer.setValue(0);\n const anim = Animated.loop(\n Animated.timing(shimmer, {\n toValue: 1,\n duration,\n easing: Easing.linear,\n useNativeDriver: shouldUseNativeDriver(),\n }),\n );\n anim.start();\n return () => anim.stop();\n }, [reduced, measuredWidth, duration, shimmer]);\n\n const [from, to] = shimmerRange(measuredWidth);\n const translateX = shimmer.interpolate({ inputRange: [0, 1], outputRange: [from, to] });\n const highlightWidth = measuredWidth * HIGHLIGHT_WIDTH_RATIO;\n\n return (\n <View\n accessibilityState={{ busy: true }}\n onLayout={onLayout}\n style={[styles.block, { width: width as ViewStyle['width'], height, borderRadius, backgroundColor }, style]}\n testID={testID}\n >\n {reduced || measuredWidth === 0 ? null : (\n <Animated.View\n style={[styles.highlight, { width: highlightWidth, backgroundColor: highlightColor, transform: [{ translateX }] }]}\n />\n )}\n </View>\n );\n};\n\nexport default Skeleton;\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dloizides/ui-motion",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Themable, brand-agnostic React Native (RN-web) motion primitives — useEnterExit, FadeIn, Collapse, PressableScale, Skeleton — built on RN Animated, driven by @dloizides/design-tokens motion tokens, and gated on @dloizides/rn-web-hooks useReducedMotion (every animation collapses to instant under reduced-motion). Part of the dloizides.com shared UI kit.",
5
5
  "keywords": [
6
6
  "react",