diff --git a/docs/src/components/DynamicColorTheme.tsx b/docs/src/components/DynamicColorTheme.tsx index 3861013caf..24e1191b21 100644 --- a/docs/src/components/DynamicColorTheme.tsx +++ b/docs/src/components/DynamicColorTheme.tsx @@ -2,6 +2,7 @@ import React, { useState, ReactNode } from 'react'; import Color from 'color'; //@ts-ignore +// eslint-disable-next-line import/no-unresolved import { BlockPicker } from 'react-color'; import Switch from './Switch'; diff --git a/docs/src/components/ScreenshotTabs.tsx b/docs/src/components/ScreenshotTabs.tsx index ddd775d54d..e62220b148 100644 --- a/docs/src/components/ScreenshotTabs.tsx +++ b/docs/src/components/ScreenshotTabs.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { JSX } from 'react'; //@ts-ignore import TabItem from '@theme/TabItem'; diff --git a/docs/src/components/ThemeColorsTable.tsx b/docs/src/components/ThemeColorsTable.tsx index 85510db500..f30ccea03e 100644 --- a/docs/src/components/ThemeColorsTable.tsx +++ b/docs/src/components/ThemeColorsTable.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { JSX } from 'react'; //@ts-ignore import Admonition from '@theme/Admonition'; diff --git a/docs/src/utils/themes.tsx b/docs/src/utils/themes.tsx index dfbdeac529..a6c0d1cf9f 100644 --- a/docs/src/utils/themes.tsx +++ b/docs/src/utils/themes.tsx @@ -2,6 +2,7 @@ import { argbFromHex, themeFromSourceColor, //@ts-ignore + // eslint-disable-next-line import/no-unresolved } from '@material/material-color-utilities'; import camelCase from 'camelcase'; import Color from 'color'; diff --git a/example/package.json b/example/package.json index 284cc2a8cc..667c56cb19 100644 --- a/example/package.json +++ b/example/package.json @@ -14,42 +14,43 @@ "web": "EXPO_NO_TYPESCRIPT_SETUP=1 expo start --web" }, "dependencies": { - "@expo/vector-icons": "^14.1.0", + "@expo/vector-icons": "^15.0.2", "@expo/webpack-config": "~19.0.1", "@pchmn/expo-material3-theme": "^1.3.2", - "@react-native-async-storage/async-storage": "1.23.1", + "@react-native-async-storage/async-storage": "2.2.0", "@react-native-masked-view/masked-view": "0.3.2", "@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/drawer": "^7.3.9", "@react-navigation/native": "^7.1.6", "@react-navigation/stack": "^7.2.10", - "expo": "~52.0.46", - "expo-crypto": "~14.0.2", - "expo-dev-client": "~5.0.20", - "expo-font": "~13.0.4", - "expo-keep-awake": "~14.0.3", - "expo-splash-screen": "~0.29.24", - "expo-status-bar": "~2.0.1", - "expo-updates": "~0.27.4", + "expo": "^54.0.0", + "expo-crypto": "~15.0.7", + "expo-dev-client": "~6.0.14", + "expo-font": "~14.0.9", + "expo-keep-awake": "~15.0.7", + "expo-splash-screen": "~31.0.10", + "expo-status-bar": "~3.0.8", + "expo-updates": "~29.0.12", "file-loader": "^6.2.0", - "react": "18.3.1", - "react-dom": "18.3.1", - "react-native": "0.77.2", - "react-native-gesture-handler": "~2.22.0", + "react": "19.1.0", + "react-dom": "19.1.0", + "react-native": "0.81.4", + "react-native-gesture-handler": "~2.28.0", "react-native-monorepo-config": "^0.1.6", - "react-native-reanimated": "~3.16.7", - "react-native-safe-area-context": "5.1.0", - "react-native-screens": "~4.8.0", - "react-native-web": "~0.19.13", + "react-native-reanimated": "~4.1.1", + "react-native-safe-area-context": "~5.6.0", + "react-native-screens": "~4.16.0", + "react-native-web": "^0.21.0", + "react-native-worklets": "0.5.1", "typeface-roboto": "^1.1.13" }, "devDependencies": { "@babel/core": "^7.25.2", "babel-plugin-module-resolver": "^5.0.0", - "babel-preset-expo": "~12.0.0", + "babel-preset-expo": "~54.0.0", "url-loader": "^4.1.1" }, "engines": { - "node": ">=18" + "node": ">=20" } } diff --git a/example/src/Examples/AnimatedFABExample/CustomFAB.tsx b/example/src/Examples/AnimatedFABExample/CustomFAB.tsx index ec206bbffc..3495b00a86 100644 --- a/example/src/Examples/AnimatedFABExample/CustomFAB.tsx +++ b/example/src/Examples/AnimatedFABExample/CustomFAB.tsx @@ -1,11 +1,5 @@ import React from 'react'; -import { - Animated, - Platform, - StyleProp, - StyleSheet, - ViewStyle, -} from 'react-native'; +import { Animated, Platform, StyleSheet, ViewStyle } from 'react-native'; import { AnimatedFAB } from 'react-native-paper'; @@ -18,7 +12,7 @@ type CustomFABProps = { label: string; animateFrom: 'left' | 'right'; iconMode?: 'static' | 'dynamic'; - style?: StyleProp; + style?: ViewStyle; }; const CustomFAB = ({ diff --git a/package.json b/package.json index b51bcb248f..cbf3cf8f94 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ }, "dependencies": { "@callstack/react-theme-provider": "^3.0.9", + "@react-native/babel-preset": "^0.82.1", "color": "^3.1.2", "use-latest-callback": "^0.2.3" }, @@ -69,9 +70,9 @@ "@types/color": "^3.0.0", "@types/jest": "^29.2.1", "@types/node": "^13.1.0", - "@types/react-dom": "^18.3.1", + "@types/react-dom": "^19.1.1", "@types/react-native-vector-icons": "^6.4.18", - "@types/react-test-renderer": "^18.3.0", + "@types/react-test-renderer": "^19.1.0", "@typescript-eslint/eslint-plugin": "^5.41.0", "@typescript-eslint/parser": "^5.41.0", "all-contributors-cli": "^6.24.0", @@ -92,15 +93,15 @@ "jest": "^29.6.3", "jest-file-snapshot": "^0.3.2", "metro-react-native-babel-preset": "0.73.9", - "react": "18.3.1", + "react": "19.1.1", "react-dom": "18.3.1", - "react-native": "0.77.0", + "react-native": "0.82.1", "react-native-builder-bob": "^0.21.3", - "react-native-safe-area-context": "5.1.0", - "react-test-renderer": "18.3.1", + "react-native-safe-area-context": "5.5.2", + "react-test-renderer": "19.1.1", "release-it": "^13.4.0", "rimraf": "^3.0.2", - "typescript": "5.0.4" + "typescript": "5.8.3" }, "peerDependencies": { "react": "*", diff --git a/src/components/BottomNavigation/BottomNavigationRouteScreen.tsx b/src/components/BottomNavigation/BottomNavigationRouteScreen.tsx index dbdd7ac7e7..d9a3dbe136 100644 --- a/src/components/BottomNavigation/BottomNavigationRouteScreen.tsx +++ b/src/components/BottomNavigation/BottomNavigationRouteScreen.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { JSX } from 'react'; import { Animated, Platform, View, ViewProps } from 'react-native'; interface Props extends ViewProps { diff --git a/src/components/Typography/AnimatedText.tsx b/src/components/Typography/AnimatedText.tsx index bab41a617c..f988390b67 100644 --- a/src/components/Typography/AnimatedText.tsx +++ b/src/components/Typography/AnimatedText.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { JSX } from 'react'; import { Animated, I18nManager, diff --git a/src/components/Typography/Text.tsx b/src/components/Typography/Text.tsx index e34c4b5054..4c808b4394 100644 --- a/src/components/Typography/Text.tsx +++ b/src/components/Typography/Text.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { JSX } from 'react'; import { I18nManager, StyleProp, diff --git a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap index cf1ddf0dd8..4a62c376d8 100644 --- a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap +++ b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap @@ -565,23 +565,19 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A @@ -812,23 +808,19 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A diff --git a/src/components/__tests__/Dialog.test.tsx b/src/components/__tests__/Dialog.test.tsx index 26b2f34aa5..8f7e327a72 100644 --- a/src/components/__tests__/Dialog.test.tsx +++ b/src/components/__tests__/Dialog.test.tsx @@ -12,11 +12,6 @@ import { act, fireEvent, render } from '@testing-library/react-native'; import Dialog from '../../components/Dialog/Dialog'; import Button from '../Button/Button'; -jest.mock('react-native/Libraries/Utilities/BackHandler', () => - // eslint-disable-next-line jest/no-mocks-import - require('react-native/Libraries/Utilities/__mocks__/BackHandler') -); - interface BackHandlerStatic extends RNBackHandlerStatic { mockPressBack(): void; } diff --git a/src/components/__tests__/Menu.test.tsx b/src/components/__tests__/Menu.test.tsx index b8f7c0d7fe..68e73661e6 100644 --- a/src/components/__tests__/Menu.test.tsx +++ b/src/components/__tests__/Menu.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Animated, StyleSheet, View } from 'react-native'; +import { Animated, Dimensions, StyleSheet, View } from 'react-native'; import { act, render, screen, waitFor } from '@testing-library/react-native'; @@ -93,6 +93,16 @@ it('renders menu with content styles', () => { ); it('uses the default anchorPosition of top', async () => { + const dimensionsSpy = jest.spyOn(Dimensions, 'get').mockReturnValue({ + width: 400, + height: 800, + scale: 2, + fontScale: 2, + }); + const measureSpy = jest + .spyOn(View.prototype, 'measureInWindow') + .mockImplementation((fn) => fn(100, 100, 80, 32)); + function makeMenu(visible: boolean) { return ( @@ -115,15 +125,13 @@ it('uses the default anchorPosition of top', async () => { render(makeMenu(false)); - jest - .spyOn(View.prototype, 'measureInWindow') - .mockImplementation((fn) => fn(100, 100, 80, 32)); - // You must update instead of creating directly and using it because // componentDidUpdate isn't called by default in jest. Forcing the update // than triggers measureInWindow, which is how Menu decides where to show // itself. - screen.update(makeMenu(true)); + await act(async () => { + screen.update(makeMenu(true)); + }); await waitFor(() => { const menu = screen.getByTestId('menu-view'); @@ -133,9 +141,22 @@ it('uses the default anchorPosition of top', async () => { top: 100, }); }); + + measureSpy.mockRestore(); + dimensionsSpy.mockRestore(); }); it('respects anchorPosition bottom', async () => { + const dimensionsSpy = jest.spyOn(Dimensions, 'get').mockReturnValue({ + width: 400, + height: 800, + scale: 2, + fontScale: 2, + }); + const measureSpy = jest + .spyOn(View.prototype, 'measureInWindow') + .mockImplementation((fn) => fn(100, 100, 80, 32)); + function makeMenu(visible: boolean) { return ( @@ -159,11 +180,9 @@ it('respects anchorPosition bottom', async () => { render(makeMenu(false)); - jest - .spyOn(View.prototype, 'measureInWindow') - .mockImplementation((fn) => fn(100, 100, 80, 32)); - - screen.update(makeMenu(true)); + await act(async () => { + screen.update(makeMenu(true)); + }); await waitFor(() => { const menu = screen.getByTestId('menu-view'); @@ -173,6 +192,9 @@ it('respects anchorPosition bottom', async () => { top: 132, }); }); + + measureSpy.mockRestore(); + dimensionsSpy.mockRestore(); }); it('animated value changes correctly', () => { diff --git a/src/components/__tests__/Modal.test.tsx b/src/components/__tests__/Modal.test.tsx index 0535d712ab..32161d5b84 100644 --- a/src/components/__tests__/Modal.test.tsx +++ b/src/components/__tests__/Modal.test.tsx @@ -21,11 +21,6 @@ interface BackHandlerStatic extends RNBackHandlerStatic { const BackHandler = RNBackHandler as BackHandlerStatic; -jest.mock('react-native/Libraries/Utilities/BackHandler', () => - // eslint-disable-next-line jest/no-mocks-import - require('react-native/Libraries/Utilities/__mocks__/BackHandler') -); - describe('Modal', () => { beforeAll(() => { jest.useFakeTimers(); diff --git a/src/components/__tests__/ProgressBar.test.tsx b/src/components/__tests__/ProgressBar.test.tsx index d995facdb9..9aac20ac91 100644 --- a/src/components/__tests__/ProgressBar.test.tsx +++ b/src/components/__tests__/ProgressBar.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Animated, Platform, StyleSheet } from 'react-native'; -import { render, waitFor } from '@testing-library/react-native'; +import { act, render } from '@testing-library/react-native'; import ProgressBar, { Props } from '../ProgressBar'; @@ -20,6 +20,13 @@ const styles = StyleSheet.create({ }); const a11yRole = 'progressbar'; +const triggerLayout = async ( + tree: ReturnType +): Promise => { + await act(async () => { + tree.getByRole(a11yRole).props.onLayout(layoutEvent); + }); +}; class ClassProgressBar extends React.Component { render() { @@ -35,7 +42,7 @@ afterEach(() => { it('renders progress bar with animated value', async () => { const tree = render(); - await waitFor(() => tree.getByRole(a11yRole).props.onLayout(layoutEvent)); + await triggerLayout(tree); tree.update(); @@ -44,28 +51,28 @@ it('renders progress bar with animated value', async () => { it('renders progress bar with specific progress', async () => { const tree = render(); - await waitFor(() => tree.getByRole(a11yRole).props.onLayout(layoutEvent)); + await triggerLayout(tree); expect(tree.toJSON()).toMatchSnapshot(); }); it('renders hidden progress bar', async () => { const tree = render(); - await waitFor(() => tree.getByRole(a11yRole).props.onLayout(layoutEvent)); + await triggerLayout(tree); expect(tree.toJSON()).toMatchSnapshot(); }); it('renders colored progress bar', async () => { const tree = render(); - await waitFor(() => tree.getByRole(a11yRole).props.onLayout(layoutEvent)); + await triggerLayout(tree); expect(tree.toJSON()).toMatchSnapshot(); }); it('renders indeterminate progress bar', async () => { const tree = render(); - await waitFor(() => tree.getByRole(a11yRole).props.onLayout(layoutEvent)); + await triggerLayout(tree); expect(tree.toJSON()).toMatchSnapshot(); }); @@ -84,7 +91,7 @@ it('renders progress bar with custom style of filled part', async () => { const tree = render( ); - await waitFor(() => tree.getByRole(a11yRole).props.onLayout(layoutEvent)); + await triggerLayout(tree); expect(tree.getByTestId('progress-bar-fill')).toHaveStyle({ borderRadius: 4, diff --git a/src/components/__tests__/Tooltip.test.tsx b/src/components/__tests__/Tooltip.test.tsx index f0b423169f..075b534ba6 100644 --- a/src/components/__tests__/Tooltip.test.tsx +++ b/src/components/__tests__/Tooltip.test.tsx @@ -1,11 +1,8 @@ import React, { RefObject } from 'react'; import { Dimensions, Text, View, Platform } from 'react-native'; -import { - fireEvent, - render, - waitForElementToBeRemoved, -} from '@testing-library/react-native'; +import { act, fireEvent, render } from '@testing-library/react-native'; +import type { ReactTestInstance } from 'react-test-renderer'; import PaperProvider from '../../core/PaperProvider'; import Tooltip from '../Tooltip/Tooltip'; @@ -25,6 +22,19 @@ const DummyComponent = React.forwardRef((props, ref) => ( )); describe('Tooltip', () => { + const getTrigger = (getByText: (text: string) => ReactTestInstance) => + getByText('dummy component').parent as ReactTestInstance; + + const runTimers = (ms?: number) => { + act(() => { + if (ms === undefined) { + jest.runOnlyPendingTimers(); + } else { + jest.advanceTimersByTime(ms); + } + }); + }; + const setup = ( propOverrides?: Partial>, measure = {} @@ -74,7 +84,7 @@ describe('Tooltip', () => { wrapper: { getByText, unmount }, } = setup({ enterTouchDelay: 5000 }); - fireEvent(getByText('dummy component'), 'pressOut'); + fireEvent(getTrigger(getByText), 'pressOut'); unmount(); @@ -86,7 +96,7 @@ describe('Tooltip', () => { wrapper: { getByText, findByText, unmount }, } = setup(); - fireEvent(getByText('dummy component'), 'longPress'); + fireEvent(getTrigger(getByText), 'longPress'); await findByText('some tooltip text'); @@ -107,9 +117,10 @@ describe('Tooltip', () => { wrapper: { getByText }, } = setup(); - fireEvent(getByText('dummy component'), 'longPress'); - fireEvent(getByText('dummy component'), 'pressOut'); - fireEvent(getByText('dummy component'), 'longPress'); + const trigger = getTrigger(getByText); + fireEvent(trigger, 'longPress'); + fireEvent(trigger, 'pressOut'); + fireEvent(trigger, 'longPress'); expect(global.clearTimeout).toHaveBeenCalledTimes(1); }); @@ -122,13 +133,12 @@ describe('Tooltip', () => { wrapper: { queryByText, getByText, findByText }, } = setup({ enterTouchDelay: 50, leaveTouchDelay: 0 }); - fireEvent(getByText('dummy component'), 'longPress'); + fireEvent(getTrigger(getByText), 'longPress'); await findByText('some tooltip text'); - fireEvent(getByText('dummy component'), 'pressOut'); - - await waitForElementToBeRemoved(() => getByText('some tooltip text')); + fireEvent(getTrigger(getByText), 'pressOut'); + runTimers(); expect(queryByText('some tooltip text')).toBeNull(); }); @@ -155,7 +165,7 @@ describe('Tooltip', () => { wrapper: { getByText, getByTestId, findByText }, } = setup(); - fireEvent(getByText('dummy component'), 'longPress'); + fireEvent(getTrigger(getByText), 'longPress'); fireEvent(await findByText('some tooltip text'), 'layout', { nativeEvent: { @@ -179,7 +189,7 @@ describe('Tooltip', () => { wrapper: { getByText, getByTestId, findByText }, } = setup({}, { pageX: 0 }); // Component starting at the starting 0 X coord - fireEvent(getByText('dummy component'), 'longPress'); + fireEvent(getTrigger(getByText), 'longPress'); fireEvent(await findByText('some tooltip text'), 'layout', { nativeEvent: { @@ -203,7 +213,7 @@ describe('Tooltip', () => { wrapper: { getByText, getByTestId, findByText }, } = setup({}, { pageX: 900, width: 150 }); // Component close to the screen limit - fireEvent(getByText('dummy component'), 'longPress'); + fireEvent(getTrigger(getByText), 'longPress'); fireEvent(await findByText('some tooltip text'), 'layout', { nativeEvent: { @@ -227,7 +237,7 @@ describe('Tooltip', () => { wrapper: { getByText, getByTestId, findByText }, } = setup({}, { pageY: 600, height: 50 }); - fireEvent(getByText('dummy component'), 'longPress'); + fireEvent(getTrigger(getByText), 'longPress'); fireEvent(await findByText('some tooltip text'), 'layout', { nativeEvent: { @@ -260,7 +270,7 @@ describe('Tooltip', () => { wrapper: { getByText, unmount }, } = setup({ enterTouchDelay: 5000 }); - fireEvent(getByText('dummy component'), 'hoverIn'); + fireEvent(getTrigger(getByText), 'hoverIn'); unmount(); @@ -272,7 +282,7 @@ describe('Tooltip', () => { wrapper: { getByText, unmount }, } = setup({ enterTouchDelay: 5000 }); - fireEvent(getByText('dummy component'), 'hoverOut'); + fireEvent(getTrigger(getByText), 'hoverOut'); unmount(); @@ -284,7 +294,8 @@ describe('Tooltip', () => { wrapper: { getByText, findByText, unmount }, } = setup(); - fireEvent(getByText('dummy component'), 'hoverIn'); + fireEvent(getTrigger(getByText), 'hoverIn'); + runTimers(500); await findByText('some tooltip text'); @@ -305,9 +316,10 @@ describe('Tooltip', () => { wrapper: { getByText }, } = setup(); - fireEvent(getByText('dummy component'), 'hoverIn'); - fireEvent(getByText('dummy component'), 'hoverOut'); - fireEvent(getByText('dummy component'), 'hoverIn'); + const trigger = getTrigger(getByText); + fireEvent(trigger, 'hoverIn'); + fireEvent(trigger, 'hoverOut'); + fireEvent(trigger, 'hoverIn'); expect(global.clearTimeout).toHaveBeenCalledTimes(2); }); @@ -320,13 +332,13 @@ describe('Tooltip', () => { wrapper: { queryByText, getByText, findByText }, } = setup({ enterTouchDelay: 50, leaveTouchDelay: 0 }); - fireEvent(getByText('dummy component'), 'hoverIn'); + fireEvent(getTrigger(getByText), 'hoverIn'); + runTimers(50); await findByText('some tooltip text'); - fireEvent(getByText('dummy component'), 'hoverOut'); - - await waitForElementToBeRemoved(() => getByText('some tooltip text')); + fireEvent(getTrigger(getByText), 'hoverOut'); + runTimers(); expect(queryByText('some tooltip text')).toBeNull(); }); @@ -353,7 +365,8 @@ describe('Tooltip', () => { wrapper: { getByText, getByTestId, findByText }, } = setup(); - fireEvent(getByText('dummy component'), 'hoverIn'); + fireEvent(getTrigger(getByText), 'hoverIn'); + runTimers(500); fireEvent(await findByText('some tooltip text'), 'layout', { nativeEvent: { @@ -377,7 +390,8 @@ describe('Tooltip', () => { wrapper: { getByText, getByTestId, findByText }, } = setup({}, { pageX: 0 }); // Component starting at the starting 0 X coord - fireEvent(getByText('dummy component'), 'hoverIn'); + fireEvent(getTrigger(getByText), 'hoverIn'); + runTimers(500); fireEvent(await findByText('some tooltip text'), 'layout', { nativeEvent: { @@ -401,7 +415,8 @@ describe('Tooltip', () => { wrapper: { getByText, getByTestId, findByText }, } = setup({}, { pageX: 900, width: 150 }); // Component close to the screen limit - fireEvent(getByText('dummy component'), 'hoverIn'); + fireEvent(getTrigger(getByText), 'hoverIn'); + runTimers(500); fireEvent(await findByText('some tooltip text'), 'layout', { nativeEvent: { @@ -425,7 +440,8 @@ describe('Tooltip', () => { wrapper: { getByText, getByTestId, findByText }, } = setup({}, { pageY: 600, height: 50 }); - fireEvent(getByText('dummy component'), 'hoverIn'); + fireEvent(getTrigger(getByText), 'hoverIn'); + runTimers(500); fireEvent(await findByText('some tooltip text'), 'layout', { nativeEvent: { diff --git a/src/components/__tests__/__snapshots__/ActivityIndicator.test.tsx.snap b/src/components/__tests__/__snapshots__/ActivityIndicator.test.tsx.snap index 204f865524..43b4eac37e 100644 --- a/src/components/__tests__/__snapshots__/ActivityIndicator.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/ActivityIndicator.test.tsx.snap @@ -32,17 +32,15 @@ exports[`renders colored indicator 1`] = `