diff --git a/.changeset/no-native-animated-style-deps.md b/.changeset/no-native-animated-style-deps.md new file mode 100644 index 0000000000..95b3062ffe --- /dev/null +++ b/.changeset/no-native-animated-style-deps.md @@ -0,0 +1,5 @@ +--- +'@storybook/react-native-ui': patch +--- + +The addons panel no longer passes `useAnimatedStyle` dependencies on native, where Reanimated 4.6+ ignores them and warns ("Dependencies should only be used on the web"). diff --git a/packages/react-native-ui/__mocks__/@gorhom/bottom-sheet.js b/packages/react-native-ui/__mocks__/@gorhom/bottom-sheet.js index 159d67a596..4fb2e96460 100644 --- a/packages/react-native-ui/__mocks__/@gorhom/bottom-sheet.js +++ b/packages/react-native-ui/__mocks__/@gorhom/bottom-sheet.js @@ -9,5 +9,6 @@ module.exports = { __esModule: true, default: renderChildren(), BottomSheetBackdrop: jest.fn(() => null), + BottomSheetModal: jest.fn(() => null), BottomSheetScrollView: jest.fn(() => null), }; diff --git a/packages/react-native-ui/__mocks__/@storybook/react-native-theming.js b/packages/react-native-ui/__mocks__/@storybook/react-native-theming.js index a4b286d681..0bd2c5a96d 100644 --- a/packages/react-native-ui/__mocks__/@storybook/react-native-theming.js +++ b/packages/react-native-ui/__mocks__/@storybook/react-native-theming.js @@ -1,8 +1,13 @@ const theme = { + appBorderColor: '#e0e0e0', + barSelectedColor: '#1b71ba', + color: { mediumdark: '#999999' }, background: { content: '#ffffff' }, textMutedColor: '#999999', }; module.exports = { + // `styled.X(styleFn)` returns a component; host names keep the elements renderable. + styled: { Text: () => 'Text', TouchableOpacity: () => 'TouchableOpacity' }, useTheme: jest.fn(() => theme), }; diff --git a/packages/react-native-ui/__mocks__/@storybook/react-native-ui-common.js b/packages/react-native-ui/__mocks__/@storybook/react-native-ui-common.js new file mode 100644 index 0000000000..b9c45472f6 --- /dev/null +++ b/packages/react-native-ui/__mocks__/@storybook/react-native-ui-common.js @@ -0,0 +1,5 @@ +// `useStyle` memoizes in the real package; recomputing is enough here. +module.exports = { + IconButton: 'IconButton', + useStyle: (styleFactory) => styleFactory(), +}; diff --git a/packages/react-native-ui/__mocks__/react-native-gesture-handler.js b/packages/react-native-ui/__mocks__/react-native-gesture-handler.js new file mode 100644 index 0000000000..18218db7f9 --- /dev/null +++ b/packages/react-native-ui/__mocks__/react-native-gesture-handler.js @@ -0,0 +1,3 @@ +module.exports = { + ScrollView: 'ScrollView', +}; diff --git a/packages/react-native-ui/__mocks__/react-native-reanimated.js b/packages/react-native-ui/__mocks__/react-native-reanimated.js index 45bbd9f0d5..f6db54c864 100644 --- a/packages/react-native-ui/__mocks__/react-native-reanimated.js +++ b/packages/react-native-ui/__mocks__/react-native-reanimated.js @@ -1,4 +1,6 @@ module.exports = { + useAnimatedKeyboard: jest.fn(), useAnimatedStyle: jest.fn(() => ({})), useReducedMotion: jest.fn(() => false), + useSharedValue: jest.fn((initial) => ({ value: initial })), }; diff --git a/packages/react-native-ui/__mocks__/react-native-svg.js b/packages/react-native-ui/__mocks__/react-native-svg.js new file mode 100644 index 0000000000..6fd2745e15 --- /dev/null +++ b/packages/react-native-ui/__mocks__/react-native-svg.js @@ -0,0 +1,4 @@ +module.exports = { + Path: 'Path', + Svg: 'Svg', +}; diff --git a/packages/react-native-ui/__mocks__/react-native.js b/packages/react-native-ui/__mocks__/react-native.js index fb6091e68d..fe7c6b50ba 100644 --- a/packages/react-native-ui/__mocks__/react-native.js +++ b/packages/react-native-ui/__mocks__/react-native.js @@ -1,4 +1,7 @@ module.exports = { Keyboard: { dismiss: jest.fn() }, Platform: { OS: 'ios' }, + Text: 'Text', + View: 'View', + useWindowDimensions: jest.fn(() => ({ width: 390, height: 844, scale: 3, fontScale: 1 })), }; diff --git a/packages/react-native-ui/__mocks__/storybook/manager-api.js b/packages/react-native-ui/__mocks__/storybook/manager-api.js new file mode 100644 index 0000000000..739eb9ac10 --- /dev/null +++ b/packages/react-native-ui/__mocks__/storybook/manager-api.js @@ -0,0 +1,3 @@ +module.exports = { + addons: { getElements: jest.fn(() => ({})) }, +}; diff --git a/packages/react-native-ui/src/MobileAddonsPanel.test.tsx b/packages/react-native-ui/src/MobileAddonsPanel.test.tsx new file mode 100644 index 0000000000..7857b7eb7a --- /dev/null +++ b/packages/react-native-ui/src/MobileAddonsPanel.test.tsx @@ -0,0 +1,60 @@ +import { act } from 'react'; +import { Platform } from 'react-native'; +import { useAnimatedStyle } from 'react-native-reanimated'; +import { createRoot } from 'test-renderer'; +import { MobileAddonsPanel } from './MobileAddonsPanel'; + +const animatedStyle = useAnimatedStyle as unknown as jest.Mock; +const platform = Platform as { OS: string }; + +// shared value, then the window height from the react-native mock and the bottom inset from +// __mocks__/react-native-safe-area-context.js +const deps = [expect.any(Object), 844, 34]; + +const renderPanel = async () => { + const root = createRoot(); + + await act(async () => { + root.render(); + }); + + return root; +}; + +const depsArgument = () => animatedStyle.mock.calls[0][1]; + +describe('MobileAddonsPanel', () => { + const originalOS = platform.OS; + + afterEach(() => { + platform.OS = originalOS; + animatedStyle.mockClear(); + }); + + it.each(['ios', 'android'])( + 'does not pass dependencies to useAnimatedStyle on %s', + async (os) => { + platform.OS = os; + const root = await renderPanel(); + + expect(animatedStyle).toHaveBeenCalled(); + expect(depsArgument()).toBeUndefined(); + + await act(async () => { + root.unmount(); + }); + } + ); + + it('passes dependencies to useAnimatedStyle on web', async () => { + platform.OS = 'web'; + const root = await renderPanel(); + + // web builds without the worklets babel plugin only recompute the style when deps change + expect(depsArgument()).toEqual(deps); + + await act(async () => { + root.unmount(); + }); + }); +}); diff --git a/packages/react-native-ui/src/MobileAddonsPanel.tsx b/packages/react-native-ui/src/MobileAddonsPanel.tsx index 65af43fc40..b22ee18c14 100644 --- a/packages/react-native-ui/src/MobileAddonsPanel.tsx +++ b/packages/react-native-ui/src/MobileAddonsPanel.tsx @@ -60,12 +60,16 @@ export const MobileAddonsPanel = forwardRef { - const extraPadding = Platform.OS === 'android' ? 32 : 16 + insets.bottom; - return { - maxHeight: height - animatedPosition.value - extraPadding, - }; - }, [animatedPosition, height, insets.bottom]); + const adjustedBottomSheetSize = useAnimatedStyle( + () => { + const extraPadding = Platform.OS === 'android' ? 32 : 16 + insets.bottom; + return { + maxHeight: height - animatedPosition.value - extraPadding, + }; + }, + // ignored (and warned about) on native; web without the babel plugin needs them + Platform.OS === 'web' ? [animatedPosition, height, insets.bottom] : undefined + ); const backgroundStyle = useStyle(() => { return {