From 32951dd4bd548f3ab40d8d7f507ae1ec9a1c0249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrycja=20Kali=C5=84ska?= <59940332+patrycjakalinska@users.noreply.github.com> Date: Wed, 17 Jul 2024 12:41:57 +0200 Subject: [PATCH 1/3] fix: docs: Access before initialization on withSpring and withTiming (#6282) Before on docs using `withSpring` and `withTiming` playgrounds (Customising animations, withSpring, withTiming): image After: image ## Test plan 1. Run `yarn && yarn build && yarn serve` 2. Check `Customising animations`, `withSpring`, `withTiming` documentation pages (they are must, but full docs checkup is recommended) --- .../InteractivePlayground/useSpringPlayground/Example.tsx | 3 ++- .../InteractivePlayground/useTimingPlayground/Example.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/docs-reanimated/src/components/InteractivePlayground/useSpringPlayground/Example.tsx b/packages/docs-reanimated/src/components/InteractivePlayground/useSpringPlayground/Example.tsx index 48ab2dffc45..7eddedfd99b 100644 --- a/packages/docs-reanimated/src/components/InteractivePlayground/useSpringPlayground/Example.tsx +++ b/packages/docs-reanimated/src/components/InteractivePlayground/useSpringPlayground/Example.tsx @@ -23,6 +23,8 @@ export default function App({ width, options }: Props) { const shouldReduceMotion = options.reduceMotion === ReduceMotion.Always || (options.reduceMotion === ReduceMotion.System && reduceMotion); + const [buttonDisabled, setButtonDisabled] = useState(false); + const callback = (isFinished) => { setTimeout(() => { if (isFinished) { @@ -31,7 +33,6 @@ export default function App({ width, options }: Props) { } }, 1000); }; - const [buttonDisabled, setButtonDisabled] = useState(false); const animatedStyles = useAnimatedStyle(() => { return { diff --git a/packages/docs-reanimated/src/components/InteractivePlayground/useTimingPlayground/Example.tsx b/packages/docs-reanimated/src/components/InteractivePlayground/useTimingPlayground/Example.tsx index befc15a9e11..9f9fd22751f 100644 --- a/packages/docs-reanimated/src/components/InteractivePlayground/useTimingPlayground/Example.tsx +++ b/packages/docs-reanimated/src/components/InteractivePlayground/useTimingPlayground/Example.tsx @@ -23,6 +23,8 @@ export default function App({ width, options }: Props) { const shouldReduceMotion = options.reduceMotion === ReduceMotion.Always || (options.reduceMotion === ReduceMotion.System && reduceMotion); + const [buttonDisabled, setButtonDisabled] = useState(false); + const callback = (isFinished) => { setTimeout( () => { @@ -34,7 +36,6 @@ export default function App({ width, options }: Props) { shouldReduceMotion ? 1000 : options.duration ); }; - const [buttonDisabled, setButtonDisabled] = useState(false); const animatedStyles = useAnimatedStyle(() => { return { From a462b8910730779349f6e0c6ca0fb6fbbf48a747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrycja=20Kali=C5=84ska?= <59940332+patrycjakalinska@users.noreply.github.com> Date: Wed, 17 Jul 2024 12:43:35 +0200 Subject: [PATCH 2/3] docs: Rewrite getRelativeCoords documentation page (#6249) To be up to date we rewritten [getRelativeCoords](https://docs.swmansion.com/react-native-reanimated/docs/utilities/getRelativeCoords/) and implemented InteractiveExample. - [#6165](https://github.com/software-mansion/react-native-reanimated/pull/6165) - needs to be merged before - it bumped `react-native-reanimated` to latest where `getRelativeCoords` work Before: https://github.com/software-mansion/react-native-reanimated/assets/59940332/6372409d-6eb7-4d8c-b339-13ea8090b509 After: https://github.com/software-mansion/react-native-reanimated/assets/59940332/a4335d87-0dc3-4dab-93da-86f7ffbb9ba0 --- .../docs/utilities/getRelativeCoords.mdx | 89 +++++++++++++------ .../src/examples/RelativeCoords.tsx | 85 ++++++++++++++++++ 2 files changed, 148 insertions(+), 26 deletions(-) create mode 100644 packages/docs-reanimated/src/examples/RelativeCoords.tsx diff --git a/packages/docs-reanimated/docs/utilities/getRelativeCoords.mdx b/packages/docs-reanimated/docs/utilities/getRelativeCoords.mdx index fc04d0b2992..6d02fd2367f 100644 --- a/packages/docs-reanimated/docs/utilities/getRelativeCoords.mdx +++ b/packages/docs-reanimated/docs/utilities/getRelativeCoords.mdx @@ -4,25 +4,68 @@ sidebar_position: 4 # getRelativeCoords -import DocsCompatibilityInfo from '../_shared/_docs_compatibility_info.mdx'; +`getRelativeCoords` determines the location on the screen, relative to the given view. - +## Reference -Determines the location on the screen, relative to the given view. It might be useful when there are only absolute coordinates available and you need coordinates relative to the parent. +```tsx +import { getRelativeCoords } from 'react-native-reanimated'; + +const Comp = () => { + const animatedRef = useAnimatedRef(); + // ... + + const gestureHandler = useAnimatedGestureHandler({ + onEnd: (event) => { + const coords = getRelativeCoords( + animatedRef, + event.absoluteX, + event.absoluteY + ); + }, + }); + + return ( + + + + + + ); +}; +``` + +
+Type definitions + +```typescript +function getRelativeCoords( + animatedRef: AnimatedRef, + absoluteX: number, + absoluteY: number +): ComponentCoords | null; + +interface ComponentCoords { + x: number; + y: number; +} +``` + +
### Arguments -#### animatedRef +#### `animatedRef` The product of [`useAnimatedRef`](/docs/core/useAnimatedRef) is Reanimated's extension of a standard React ref (delivers the view tag on the UI thread). This ref should be passed as a prop to the view relative to which we want to know coordinates. -#### x +#### `absoluteX` -Absolute `x` coordinate. +Number which is an absolute `x` coordinate. -#### y +#### `absoluteY` -Absolute `y` coordinate +Number which is an absolute `y` coordinate. ### Returns @@ -33,23 +76,17 @@ Object which contains relative coordinates ### Example -```js -const Comp = () => { - const aref = useAnimatedRef(); - // ... +import RelativeCoords from '@site/src/examples/RelativeCoords'; +import RelativeCoordsSrc from '!!raw-loader!@site/src/examples/RelativeCoords'; - const gestureHandler = useAnimatedGestureHandler({ - onEnd: (event) => { - getRelativeCoords(aref, event.absoluteX, event.absoluteY); - }, - }); + - return ( - - - - - - ); -}; -``` +## Platform compatibility + +
+ +| Android | iOS | Web | +| ------- | --- | --- | +| ✅ | ✅ | ✅ | + +
diff --git a/packages/docs-reanimated/src/examples/RelativeCoords.tsx b/packages/docs-reanimated/src/examples/RelativeCoords.tsx new file mode 100644 index 00000000000..530ee0c2b3b --- /dev/null +++ b/packages/docs-reanimated/src/examples/RelativeCoords.tsx @@ -0,0 +1,85 @@ +import React, { useState } from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import Animated, { + useAnimatedRef, + getRelativeCoords, +} from 'react-native-reanimated'; +import { useColorScheme } from '@mui/material'; +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; + +const RelativeCoords = () => { + const animatedRef = useAnimatedRef(); + const { colorScheme } = useColorScheme(); + const [coords, setCoords] = useState({ x: 0, y: 0 }); + + const tapGesture = Gesture.Tap().onEnd((event) => { + const relativeCoords = getRelativeCoords( + animatedRef, + event.absoluteX, + event.absoluteY + ); + if (relativeCoords) { + setCoords(relativeCoords); + } + }); + + const textColor = + colorScheme === 'light' ? styles.darkText : styles.lightText; + + return ( + + + Relative coordinates to parent: + + + x={coords.x.toFixed()} y= + {coords.y.toFixed()} + + + + Tap anywhere inside. + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + innerView: { + width: 300, + height: 300, + backgroundColor: 'var(--swm-purple-light-80)', + justifyContent: 'center', + alignItems: 'center', + borderRadius: 40, + cursor: 'pointer', + }, + coordsData: { + fontSize: 20, + fontFamily: 'Aeonik', + color: 'var(--swm-navy-light-100)', + }, + coords: { + marginBottom: 16, + fontWeight: '500', + }, + text: { + color: 'var(--swm-off-white)', + fontFamily: 'Aeonik', + fontSize: 16, + fontWeight: 'bold', + }, + lightText: { + color: 'var(--swm-off-white)', + }, + darkText: { + color: 'var(--swm-navy-light-100)', + }, +}); + +export default RelativeCoords; From a4633869f1e487c966b3b6e2d4063ea693f2419e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBelawski?= <40713406+tjzel@users.noreply.github.com> Date: Wed, 17 Jul 2024 12:49:35 +0200 Subject: [PATCH 3/3] refactor: Improve UX of runtime tests menu (#6283) ## Summary - Lazy evaluation of imports; it was impossible to uncheck a test suite. - More explicit UI; it was hard to tell what test suites were enabled. - QoL information the user has to reload the app to re-run tests. - Minor code readability improvements. https://github.com/user-attachments/assets/2a3135ab-a3ce-4433-a8b1-eeb3cf3fde13 --- .../RuntimeTestsRunner.tsx | 263 +++++++++++++----- .../RuntimeTests/RuntimeTestsExample.tsx | 18 +- 2 files changed, 196 insertions(+), 85 deletions(-) diff --git a/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/RuntimeTestsRunner.tsx b/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/RuntimeTestsRunner.tsx index 162b810cf75..0a98617e7c8 100644 --- a/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/RuntimeTestsRunner.tsx +++ b/apps/common-app/src/examples/RuntimeTests/ReanimatedRuntimeTestsRunner/RuntimeTestsRunner.tsx @@ -1,16 +1,9 @@ -import { View, TouchableOpacity, StyleSheet, Text } from 'react-native'; +import { View, StyleSheet, Text, Pressable } from 'react-native'; import type { ReactNode } from 'react'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { runTests, configure } from './RuntimeTestsApi'; import { RenderLock } from './SyncUIRunner'; -interface ImportButton { - testSuiteName: string; - importTest: () => void; - skipByDefault?: boolean; -} - -let renderLock: RenderLock = new RenderLock(); export class ErrorBoundary extends React.Component< { children: React.JSX.Element | Array }, { hasError: boolean } @@ -34,77 +27,51 @@ export class ErrorBoundary extends React.Component< } } -function ImportButtons({ importButtons }: { importButtons: Array }) { - const [importedTests, setImportedTests] = useState>([]); - const [importedAll, setImportedAll] = useState(false); - - const handleImportAllClick = () => { - setImportedAll(true); - const newImportedTests = importedTests; - for (const button of importButtons) { - if (!button.skipByDefault) { - button.importTest(); - if (!importedTests.includes(button.testSuiteName)) { - newImportedTests.push(button.testSuiteName); - } - } - } - setImportedTests(newImportedTests); - }; +let renderLock: RenderLock = new RenderLock(); - const handleImportClick = (button: ImportButton) => { - button.importTest(); - if (!importedTests.includes(button.testSuiteName)) { - setImportedTests([...importedTests, button.testSuiteName]); - } - }; - return ( - - - Import all reanimated tests - - - - {importButtons.map(importButton => { - const { testSuiteName } = importButton; - return ( - handleImportClick(importButton)} - style={[styles.importButton, importedTests.includes(testSuiteName) ? styles.importButtonImported : {}]}> - {testSuiteName} - - ); - })} - - - ); +interface TestData { + testSuiteName: string; + importTest: () => void; + skipByDefault?: boolean; } -export default function RuntimeTestsRunner({ importButtons }: { importButtons: Array }) { +interface RuntimeTestRunnerProps { + tests: TestData[]; +} + +export default function RuntimeTestsRunner({ tests }: RuntimeTestRunnerProps) { const [component, setComponent] = useState(null); const [started, setStarted] = useState(false); + const testSelectionCallbacks = useRef void>>(new Set()); useEffect(() => { if (renderLock) { renderLock.unlock(); } }, [component]); + + async function run() { + renderLock = configure({ render: setComponent }); + await runTests(); + } + + function handleStartClick() { + testSelectionCallbacks.current.forEach(callback => callback()); + setStarted(true); + // eslint-disable-next-line no-void + void run(); + } + return ( - {started ? null : } - {started ? null : ( - { - setStarted(true); - renderLock = configure({ render: setComponent }); - await runTests(); - }} - style={styles.button}> - Run tests - + {started ? ( + Reload the app to run the tests again + ) : ( + <> + + + Run tests + + )} {/* Don't render anything if component is undefined to prevent blinking */} @@ -113,23 +80,142 @@ export default function RuntimeTestsRunner({ importButtons }: { importButtons: A ); } +interface TestSelectorProps { + tests: Array; + testSelectionCallbacks: React.RefObject void>>; +} + +function TestSelector({ tests, testSelectionCallbacks }: TestSelectorProps) { + const [selectedTests, setSelectedTests] = useState>( + tests.reduce((acc, testData) => { + acc.set(testData.testSuiteName, !testData.skipByDefault); + return acc; + }, new Map()), + ); + + function selectAllClick(select: boolean) { + tests.forEach(button => { + setSelectedTests(selectedTests => new Map(selectedTests.set(button.testSuiteName, select))); + if (select) { + testSelectionCallbacks.current!.add(button.importTest); + } else { + testSelectionCallbacks.current!.delete(button.importTest); + } + }); + } + + function selectClick(button: TestData) { + setSelectedTests(new Map(selectedTests.set(button.testSuiteName, !selectedTests.get(button.testSuiteName)))); + if (testSelectionCallbacks.current!.has(button.importTest)) { + testSelectionCallbacks.current!.delete(button.importTest); + } else { + testSelectionCallbacks.current!.add(button.importTest); + } + } + + return ( + + + + + + {tests.map(testData => { + return ( + selectClick(testData)} + selectedTests={selectedTests} + /> + ); + })} + + + ); +} + +interface SelectTestProps { + testSuiteName: string; + selectClick: () => void; + selectedTests: Map; +} + +function SelectTest({ testSuiteName, selectClick, selectedTests }: SelectTestProps) { + const [isPressed, setIsPressed] = useState(false); + + function handleSelectClickIn() { + setIsPressed(true); + } + + function handleSelectClickOut() { + selectClick(); + setIsPressed(false); + } + + return ( + handleSelectClickIn()} + onPressOut={() => handleSelectClickOut()}> + + + {testSuiteName} + + + ); +} + +interface SelectAllButtonProps { + handleSelectAllClick: (select: boolean) => void; + select: boolean; +} + +function SelectAllButtonProps({ handleSelectAllClick, select }: SelectAllButtonProps) { + const [isPressed, setIsPressed] = useState(false); + + function handleSelectAllClickIn() { + setIsPressed(true); + } + + function handleSelectAllClickOut() { + handleSelectAllClick(select); + setIsPressed(false); + } + + return ( + handleSelectAllClickOut()} + style={[styles.selectAllButton, isPressed ? styles.pressedButton : {}]}> + {select ? 'Select all' : 'Deselect all'} + + ); +} + const styles = StyleSheet.create({ container: { flex: 1, flexDirection: 'column', }, - importAllButton: { + selectAllButton: { + marginVertical: 5, marginHorizontal: 20, - marginTop: 20, + height: 40, + borderWidth: 2, + borderRadius: 10, + backgroundColor: 'white', + borderColor: 'navy', + justifyContent: 'center', + alignItems: 'center', }, - importButtonsFrame: { + selectButtonsFrame: { borderRadius: 10, backgroundColor: 'lightblue', margin: 20, - paddingHorizontal: 40, + paddingHorizontal: 10, paddingVertical: 10, }, - importButton: { + selectButton: { height: 40, borderWidth: 2, marginVertical: 5, @@ -138,15 +224,14 @@ const styles = StyleSheet.create({ borderColor: 'navy', justifyContent: 'center', alignItems: 'center', - }, - importButtonImported: { - backgroundColor: 'pink', + flex: 1, }, button: { height: 40, backgroundColor: 'navy', justifyContent: 'center', alignItems: 'center', + zIndex: 1, }, buttonText: { fontSize: 20, @@ -156,4 +241,30 @@ const styles = StyleSheet.create({ fontSize: 20, color: 'white', }, + buttonWrapper: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 10, + }, + checkbox: { + width: 20, + height: 20, + marginRight: 10, + borderWidth: 2, + backgroundColor: 'white', + }, + checkedCheckbox: { + backgroundColor: 'navy', + }, + reloadText: { + fontSize: 20, + color: 'navy', + alignSelf: 'center', + }, + pressedButton: { + zIndex: 2, + backgroundColor: '#FFFA', + borderRadius: 10, + borderColor: '#FFFF', + }, }); diff --git a/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx b/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx index 6df171e77c2..97a3a00961d 100644 --- a/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx +++ b/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx @@ -5,14 +5,7 @@ import { describe } from './ReanimatedRuntimeTestsRunner/RuntimeTestsApi'; export default function RuntimeTestsExample() { return ( { - require('./tests/TestsOfTestingFramework.test'); - }, - }, + tests={[ { testSuiteName: 'animations', importTest: () => { @@ -69,12 +62,19 @@ export default function RuntimeTestsExample() { }, }, { - testSuiteName: 'advancedAPI', + testSuiteName: 'advanced API', importTest: () => { require('./tests/advancedAPI/useFrameCallback.test'); // require('./tests/advancedAPI/measure.test'); // crash on Android }, }, + { + skipByDefault: true, + testSuiteName: 'self-tests', + importTest: () => { + require('./tests/TestsOfTestingFramework.test'); + }, + }, ]} /> );