From c3aef332465f01baf27fc0d3805caec676cc6862 Mon Sep 17 00:00:00 2001 From: Bartek Dybowski Date: Wed, 17 Jun 2026 09:12:11 +0200 Subject: [PATCH 01/18] refactor(navigation-bar): extract module + deprecate Bar alias Move BottomNavigationBar into a new NavigationBar module and expose it as a top-level `NavigationBar` export. `BottomNavigation.Bar` is kept as a deprecated alias re-exporting the same component. No behavior change. Re #4975 --- .../BottomNavigation/BottomNavigation.tsx | 5 + .../BottomNavigation/BottomNavigationBar.tsx | 866 +----------------- .../NavigationBar/NavigationBar.tsx | 862 +++++++++++++++++ src/components/NavigationBar/index.tsx | 2 + src/index.tsx | 5 + 5 files changed, 880 insertions(+), 860 deletions(-) create mode 100644 src/components/NavigationBar/NavigationBar.tsx create mode 100644 src/components/NavigationBar/index.tsx diff --git a/src/components/BottomNavigation/BottomNavigation.tsx b/src/components/BottomNavigation/BottomNavigation.tsx index df7e28cc76..fc821039df 100644 --- a/src/components/BottomNavigation/BottomNavigation.tsx +++ b/src/components/BottomNavigation/BottomNavigation.tsx @@ -612,6 +612,11 @@ BottomNavigation.SceneMap = (scenes: { ); }; +/** + * @deprecated Use the top-level `NavigationBar` export instead. + * `BottomNavigation.Bar` is the M3 "original" navigation bar, superseded by the + * flexible `NavigationBar`. Kept as an alias for backwards compatibility. + */ // @component ./BottomNavigationBar.tsx BottomNavigation.Bar = BottomNavigationBar; diff --git a/src/components/BottomNavigation/BottomNavigationBar.tsx b/src/components/BottomNavigation/BottomNavigationBar.tsx index f25bcf0170..e5fbbe0b4d 100644 --- a/src/components/BottomNavigation/BottomNavigationBar.tsx +++ b/src/components/BottomNavigation/BottomNavigationBar.tsx @@ -1,862 +1,8 @@ -import * as React from 'react'; -import { Animated, Platform, StyleSheet, Pressable, View } from 'react-native'; -import type { - ColorValue, - EasingFunction, - StyleProp, - ViewStyle, -} from 'react-native'; - -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -import { - getActiveTintColor, - getInactiveTintColor, - getLabelColor, -} from './utils'; -import { useInternalTheme } from '../../core/theming'; -import type { Theme, ThemeProp } from '../../types'; -import useAnimatedValue from '../../utils/useAnimatedValue'; -import useAnimatedValueArray from '../../utils/useAnimatedValueArray'; -import useIsKeyboardShown from '../../utils/useIsKeyboardShown'; -import useLayout from '../../utils/useLayout'; -import Badge from '../Badge'; -import Icon from '../Icon'; -import type { IconSource } from '../Icon'; -import Surface from '../Surface'; -import TouchableRipple from '../TouchableRipple/TouchableRipple'; -import type { Props as TouchableRippleProps } from '../TouchableRipple/TouchableRipple'; -import Text from '../Typography/Text'; - -type BaseRoute = { - key: string; - title?: string; - focusedIcon?: IconSource; - unfocusedIcon?: IconSource; - badge?: string | number | boolean; - /** - * Accessibility label for the tab. This is read by the screen reader when the user focuses the tab. - */ - 'aria-label'?: string; - testID?: string; - lazy?: boolean; -}; - -type NavigationState = { - index: number; - routes: Route[]; -}; - -type TabPressEvent = { - defaultPrevented: boolean; - preventDefault(): void; -}; - -type TouchableProps = TouchableRippleProps & { - key: string; - route: Route; - children: React.ReactNode; - borderless?: boolean; - centered?: boolean; - rippleColor?: ColorValue; -}; - -export type Props = { - /** - * Whether the shifting style is used, the active tab icon shifts up to show the label and the inactive tabs won't have a label. - * - * By default, this is `false` with theme version 3 and `true` when you have more than 3 tabs. - * Pass `shifting={false}` to explicitly disable this animation, or `shifting={true}` to always use this animation. - * Note that you need at least 2 tabs be able to run this animation. - */ - shifting?: boolean; - /** - * Whether to show labels in tabs. When `false`, only icons will be displayed. - */ - labeled?: boolean; - /** - * Whether tabs should be spread across the entire width. - */ - compact?: boolean; - /** - * State for the bottom navigation. The state should contain the following properties: - * - * - `index`: a number representing the index of the active route in the `routes` array - * - `routes`: an array containing a list of route objects used for rendering the tabs - * - * Each route object should contain the following properties: - * - * - `key`: a unique key to identify the route (required) - * - `title`: title of the route to use as the tab label - * - `focusedIcon`: icon to use as the focused tab icon, can be a string, an image source or a react component @renamed Renamed from 'icon' to 'focusedIcon' in v5.x - * - `unfocusedIcon`: icon to use as the unfocused tab icon, can be a string, an image source or a react component @supported Available in v5.x with theme version 3 - * - `badge`: badge to show on the tab icon, can be `true` to show a dot, `string` or `number` to show text. - * - `aria-label`: accessibility label for the tab button - * - `testID`: test id for the tab button - * - * Example: - * - * ```js - * { - * index: 1, - * routes: [ - * { key: 'music', title: 'Favorites', focusedIcon: 'heart', unfocusedIcon: 'heart-outline'}, - * { key: 'albums', title: 'Albums', focusedIcon: 'album' }, - * { key: 'recents', title: 'Recents', focusedIcon: 'history' }, - * { key: 'notifications', title: 'Notifications', focusedIcon: 'bell', unfocusedIcon: 'bell-outline' }, - * ] - * } - * ``` - * - * `BottomNavigation.Bar` is a controlled component, which means the `index` needs to be updated via the `onTabPress` callback. - */ - navigationState: NavigationState; - /** - * Callback which returns a React Element to be used as tab icon. - */ - renderIcon?: (props: { - route: Route; - focused: boolean; - color: ColorValue; - }) => React.ReactNode; - /** - * Callback which React Element to be used as tab label. - */ - renderLabel?: (props: { - route: Route; - focused: boolean; - color: ColorValue; - }) => React.ReactNode; - /** - * Callback which returns a React element to be used as the touchable for the tab item. - * Renders a `TouchableRipple` on Android and `Pressable` on iOS. - */ - renderTouchable?: (props: TouchableProps) => React.ReactNode; - /** - * Get accessibility label for the tab button. This is read by the screen reader when the user taps the tab. - * Uses `route['aria-label']` by default. - */ - getAccessibilityLabel?: (props: { route: Route }) => string | undefined; - /** - * Get badge for the tab, uses `route.badge` by default. - */ - getBadge?: (props: { route: Route }) => boolean | number | string | undefined; - /** - * Get label text for the tab, uses `route.title` by default. Use `renderLabel` to replace label component. - */ - getLabelText?: (props: { route: Route }) => string | undefined; - /** - * Get the id to locate this tab button in tests, uses `route.testID` by default. - */ - getTestID?: (props: { route: Route }) => string | undefined; - /** - * Function to execute on tab press. It receives the route for the pressed tab. Use this to update the navigation state. - */ - onTabPress: (props: { route: Route } & TabPressEvent) => void; - /** - * Function to execute on tab long press. It receives the route for the pressed tab - */ - onTabLongPress?: (props: { route: Route } & TabPressEvent) => void; - /** - * Custom color for icon and label in the active tab. - */ - activeColor?: string; - /** - * Custom color for icon and label in the inactive tab. - */ - inactiveColor?: string; - /** - * The scene animation Easing. - */ - animationEasing?: EasingFunction | undefined; - /** - * Whether the bottom navigation bar is hidden when keyboard is shown. - * On Android, this works best when [`windowSoftInputMode`](https://developer.android.com/guide/topics/manifest/activity-element#wsoft) is set to `adjustResize`. - */ - keyboardHidesNavigationBar?: boolean; - /** - * Safe area insets for the tab bar. This can be used to avoid elements like the navigation bar on Android and bottom safe area on iOS. - * The bottom insets for iOS is added by default. You can override the behavior with this option. - */ - safeAreaInsets?: { - top?: number; - right?: number; - bottom?: number; - left?: number; - }; - /** - * Specifies the largest possible scale a label font can reach. - */ - labelMaxFontSizeMultiplier?: number; - style?: Animated.WithAnimatedValue>; - activeIndicatorStyle?: StyleProp; - /** - * @optional - */ - theme?: ThemeProp; - /** - * TestID used for testing purposes - */ - testID?: string; -}; - -const MIN_TAB_WIDTH = 96; -const MAX_TAB_WIDTH = 168; -const BAR_HEIGHT = 56; -const OUTLINE_WIDTH = 64; - -const Touchable = ({ - route: _0, - style, - children, - borderless, - centered, - rippleColor, - ...rest -}: TouchableProps) => - TouchableRipple.supported ? ( - - {children} - - ) : ( - - {children} - - ); - /** - * A navigation bar which can easily be integrated with [React Navigation's Bottom Tabs Navigator](https://reactnavigation.org/docs/bottom-tab-navigator/). - * - * ## Usage - * ### without React Navigation - * ```js - * import React from 'react'; - * import { useState } from 'react'; - * import { View } from 'react-native'; - * import { BottomNavigation, Text, Provider } from 'react-native-paper'; - * import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons'; - * - * function HomeScreen() { - * return ( - * - * Home! - * - * ); - * } - * - * function SettingsScreen() { - * return ( - * - * Settings! - * - * ); - * } - * - * export default function MyComponent() { - * const [index, setIndex] = useState(0); - * - * const routes = [ - * { key: 'home', title: 'Home', icon: 'home' }, - * { key: 'settings', title: 'Settings', icon: 'cog' }, - * ]; - - * const renderScene = ({ route }) => { - * switch (route.key) { - * case 'home': - * return ; - * case 'settings': - * return ; - * default: - * return null; - * } - * }; - * - * return ( - * - * {renderScene({ route: routes[index] })} - * { - * const newIndex = routes.findIndex((r) => r.key === route.key); - * if (newIndex !== -1) { - * setIndex(newIndex); - * } - * }} - * renderIcon={({ route, color }) => ( - * - * )} - * getLabelText={({ route }) => route.title} - * /> - * - * ); - * } - * ``` + * @deprecated Use `NavigationBar` instead. `BottomNavigation.Bar` is the M3 + * "original" navigation bar and has been superseded by the flexible + * `NavigationBar`. This module re-exports `NavigationBar` for backwards + * compatibility and will be removed in a future major version. */ -const BottomNavigationBar = ({ - navigationState, - renderIcon, - renderLabel, - renderTouchable = ({ key, ...props }: TouchableProps) => ( - - ), - getLabelText = ({ route }: { route: Route }) => route.title, - getBadge = ({ route }: { route: Route }) => route.badge, - getAccessibilityLabel = ({ route }: { route: Route }) => route['aria-label'], - getTestID = ({ route }: { route: Route }) => route.testID, - activeColor, - inactiveColor, - keyboardHidesNavigationBar = Platform.OS === 'android', - style, - activeIndicatorStyle, - labeled = true, - animationEasing, - onTabPress, - onTabLongPress, - shifting: shiftingProp, - safeAreaInsets, - labelMaxFontSizeMultiplier = 1, - compact: compactProp, - testID = 'bottom-navigation-bar', - theme: themeOverrides, -}: Props) => { - const theme = useInternalTheme(themeOverrides); - const { colors } = theme as Theme; - const { bottom, left, right } = useSafeAreaInsets(); - const { scale } = theme.animation; - const compact = compactProp ?? false; - let shifting = shiftingProp ?? false; - - if (shifting && navigationState.routes.length < 2) { - shifting = false; - console.warn( - 'BottomNavigation.Bar needs at least 2 tabs to run shifting animation' - ); - } - - /** - * Visibility of the navigation bar, visible state is 1 and invisible is 0. - */ - const visibleAnim = useAnimatedValue(1); - - /** - * Active state of individual tab items, active state is 1 and inactive state is 0. - */ - const tabsAnims = useAnimatedValueArray( - navigationState.routes.map( - // focused === 1, unfocused === 0 - (_, i) => (i === navigationState.index ? 1 : 0) - ) - ); - - /** - * Layout of the navigation bar. - */ - const [layout, onLayout] = useLayout(); - - /** - * Track whether the keyboard is visible to show and hide the navigation bar. - */ - const [keyboardVisible, setKeyboardVisible] = React.useState(false); - - const handleKeyboardShow = React.useCallback(() => { - setKeyboardVisible(true); - Animated.timing(visibleAnim, { - toValue: 0, - duration: 150 * scale, - useNativeDriver: true, - }).start(); - }, [scale, visibleAnim]); - - const handleKeyboardHide = React.useCallback(() => { - Animated.timing(visibleAnim, { - toValue: 1, - duration: 100 * scale, - useNativeDriver: true, - }).start(() => { - setKeyboardVisible(false); - }); - }, [scale, visibleAnim]); - - const animateToIndex = React.useCallback( - (index: number) => { - Animated.parallel( - navigationState.routes.map((_, i) => - Animated.timing(tabsAnims[i], { - toValue: i === index ? 1 : 0, - duration: 150 * scale, - useNativeDriver: true, - easing: animationEasing, - }) - ) - ).start(() => { - // Workaround a bug in native animations where this is reset after first animation - tabsAnims.map((tab, i) => tab.setValue(i === index ? 1 : 0)); - }); - }, - [scale, navigationState.routes, tabsAnims, animationEasing] - ); - - React.useEffect(() => { - // Workaround for native animated bug in react-native@^0.57 - // Context: https://github.com/callstack/react-native-paper/pull/637 - animateToIndex(navigationState.index); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useIsKeyboardShown({ - onShow: handleKeyboardShow, - onHide: handleKeyboardHide, - }); - - React.useEffect(() => { - animateToIndex(navigationState.index); - }, [navigationState.index, animateToIndex]); - - const eventForIndex = (index: number) => { - const event = { - route: navigationState.routes[index], - defaultPrevented: false, - preventDefault: () => { - event.defaultPrevented = true; - }, - }; - - return event; - }; - - const { routes } = navigationState; - - const { backgroundColor: customBackground } = (StyleSheet.flatten(style) || - {}) as { - elevation?: number; - backgroundColor?: ColorValue; - }; - - const backgroundColor = customBackground || colors.surfaceContainer; - - const activeTintColor = getActiveTintColor({ - activeColor, - theme, - }); - - const inactiveTintColor = getInactiveTintColor({ - inactiveColor, - theme, - }); - - const maxTabWidth = routes.length > 3 ? MIN_TAB_WIDTH : MAX_TAB_WIDTH; - const maxTabBarWidth = maxTabWidth * routes.length; - - const insets = { - left: safeAreaInsets?.left ?? left, - right: safeAreaInsets?.right ?? right, - bottom: safeAreaInsets?.bottom ?? bottom, - }; - - return ( - - ); -}; - -BottomNavigationBar.displayName = 'BottomNavigation.Bar'; - -export default BottomNavigationBar; - -const styles = StyleSheet.create({ - bar: { - left: 0, - right: 0, - bottom: 0, - }, - barContent: { - alignItems: 'center', - overflow: 'hidden', - }, - items: { - flexDirection: 'row', - ...(Platform.OS === 'web' - ? { - width: '100%', - } - : null), - }, - item: { - flex: 1, - // Top padding is 6 and bottom padding is 10 - // The extra 4dp bottom padding is offset by label's height - paddingVertical: 6, - }, - v3Item: { - paddingVertical: 0, - }, - iconContainer: { - height: 24, - width: 24, - marginTop: 2, - marginHorizontal: 12, - alignSelf: 'center', - }, - v3IconContainer: { - height: 32, - width: 32, - marginBottom: 4, - marginTop: 0, - justifyContent: 'center', - }, - iconWrapper: { - ...StyleSheet.absoluteFill, - alignItems: 'center', - }, - v3IconWrapper: { - top: 4, - }, - labelContainer: { - height: 16, - paddingBottom: 2, - }, - labelWrapper: { - ...StyleSheet.absoluteFill, - }, - // eslint-disable-next-line react-native/no-color-literals - label: { - fontSize: 12, - height: BAR_HEIGHT, - textAlign: 'center', - backgroundColor: 'transparent', - ...(Platform.OS === 'web' - ? { - whiteSpace: 'nowrap', - alignSelf: 'center', - } - : null), - }, - badgeContainer: { - position: 'absolute', - left: 0, - }, - v3TouchableContainer: { - paddingTop: 12, - paddingBottom: 16, - }, - v3NoLabelContainer: { - height: 80, - justifyContent: 'center', - alignItems: 'center', - }, - outline: { - width: OUTLINE_WIDTH, - height: OUTLINE_WIDTH / 2, - borderRadius: OUTLINE_WIDTH / 4, - alignSelf: 'center', - }, -}); +export { default } from '../NavigationBar/NavigationBar'; +export type { Props, BaseRoute } from '../NavigationBar/NavigationBar'; \ No newline at end of file diff --git a/src/components/NavigationBar/NavigationBar.tsx b/src/components/NavigationBar/NavigationBar.tsx new file mode 100644 index 0000000000..decd29fc60 --- /dev/null +++ b/src/components/NavigationBar/NavigationBar.tsx @@ -0,0 +1,862 @@ +import * as React from 'react'; +import { Animated, Platform, StyleSheet, Pressable, View } from 'react-native'; +import type { + ColorValue, + EasingFunction, + StyleProp, + ViewStyle, +} from 'react-native'; + +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import { useInternalTheme } from '../../core/theming'; +import type { Theme, ThemeProp } from '../../types'; +import useAnimatedValue from '../../utils/useAnimatedValue'; +import useAnimatedValueArray from '../../utils/useAnimatedValueArray'; +import useIsKeyboardShown from '../../utils/useIsKeyboardShown'; +import useLayout from '../../utils/useLayout'; +import Badge from '../Badge'; +import { + getActiveTintColor, + getInactiveTintColor, + getLabelColor, +} from '../BottomNavigation/utils'; +import Icon from '../Icon'; +import type { IconSource } from '../Icon'; +import Surface from '../Surface'; +import TouchableRipple from '../TouchableRipple/TouchableRipple'; +import type { Props as TouchableRippleProps } from '../TouchableRipple/TouchableRipple'; +import Text from '../Typography/Text'; + +export type BaseRoute = { + key: string; + title?: string; + focusedIcon?: IconSource; + unfocusedIcon?: IconSource; + badge?: string | number | boolean; + accessibilityLabel?: string; + testID?: string; + lazy?: boolean; +}; + +type NavigationState = { + index: number; + routes: Route[]; +}; + +type TabPressEvent = { + defaultPrevented: boolean; + preventDefault(): void; +}; + +type TouchableProps = TouchableRippleProps & { + key: string; + route: Route; + children: React.ReactNode; + borderless?: boolean; + centered?: boolean; + rippleColor?: ColorValue; +}; + +export type Props = { + /** + * Whether the shifting style is used, the active tab icon shifts up to show the label and the inactive tabs won't have a label. + * + * By default, this is `false` with theme version 3 and `true` when you have more than 3 tabs. + * Pass `shifting={false}` to explicitly disable this animation, or `shifting={true}` to always use this animation. + * Note that you need at least 2 tabs be able to run this animation. + */ + shifting?: boolean; + /** + * Whether to show labels in tabs. When `false`, only icons will be displayed. + */ + labeled?: boolean; + /** + * Whether tabs should be spread across the entire width. + */ + compact?: boolean; + /** + * State for the bottom navigation. The state should contain the following properties: + * + * - `index`: a number representing the index of the active route in the `routes` array + * - `routes`: an array containing a list of route objects used for rendering the tabs + * + * Each route object should contain the following properties: + * + * - `key`: a unique key to identify the route (required) + * - `title`: title of the route to use as the tab label + * - `focusedIcon`: icon to use as the focused tab icon, can be a string, an image source or a react component @renamed Renamed from 'icon' to 'focusedIcon' in v5.x + * - `unfocusedIcon`: icon to use as the unfocused tab icon, can be a string, an image source or a react component @supported Available in v5.x with theme version 3 + * - `badge`: badge to show on the tab icon, can be `true` to show a dot, `string` or `number` to show text. + * - `accessibilityLabel`: accessibility label for the tab button + * - `testID`: test id for the tab button + * + * Example: + * + * ```js + * { + * index: 1, + * routes: [ + * { key: 'music', title: 'Favorites', focusedIcon: 'heart', unfocusedIcon: 'heart-outline'}, + * { key: 'albums', title: 'Albums', focusedIcon: 'album' }, + * { key: 'recents', title: 'Recents', focusedIcon: 'history' }, + * { key: 'notifications', title: 'Notifications', focusedIcon: 'bell', unfocusedIcon: 'bell-outline' }, + * ] + * } + * ``` + * + * `BottomNavigation.Bar` is a controlled component, which means the `index` needs to be updated via the `onTabPress` callback. + */ + navigationState: NavigationState; + /** + * Callback which returns a React Element to be used as tab icon. + */ + renderIcon?: (props: { + route: Route; + focused: boolean; + color: ColorValue; + }) => React.ReactNode; + /** + * Callback which React Element to be used as tab label. + */ + renderLabel?: (props: { + route: Route; + focused: boolean; + color: ColorValue; + }) => React.ReactNode; + /** + * Callback which returns a React element to be used as the touchable for the tab item. + * Renders a `TouchableRipple` on Android and `Pressable` on iOS. + */ + renderTouchable?: (props: TouchableProps) => React.ReactNode; + /** + * Get accessibility label for the tab button. This is read by the screen reader when the user taps the tab. + * Uses `route.accessibilityLabel` by default. + */ + getAccessibilityLabel?: (props: { route: Route }) => string | undefined; + /** + * Get badge for the tab, uses `route.badge` by default. + */ + getBadge?: (props: { route: Route }) => boolean | number | string | undefined; + /** + * Get label text for the tab, uses `route.title` by default. Use `renderLabel` to replace label component. + */ + getLabelText?: (props: { route: Route }) => string | undefined; + /** + * Get the id to locate this tab button in tests, uses `route.testID` by default. + */ + getTestID?: (props: { route: Route }) => string | undefined; + /** + * Function to execute on tab press. It receives the route for the pressed tab. Use this to update the navigation state. + */ + onTabPress: (props: { route: Route } & TabPressEvent) => void; + /** + * Function to execute on tab long press. It receives the route for the pressed tab + */ + onTabLongPress?: (props: { route: Route } & TabPressEvent) => void; + /** + * Custom color for icon and label in the active tab. + */ + activeColor?: string; + /** + * Custom color for icon and label in the inactive tab. + */ + inactiveColor?: string; + /** + * The scene animation Easing. + */ + animationEasing?: EasingFunction | undefined; + /** + * Whether the bottom navigation bar is hidden when keyboard is shown. + * On Android, this works best when [`windowSoftInputMode`](https://developer.android.com/guide/topics/manifest/activity-element#wsoft) is set to `adjustResize`. + */ + keyboardHidesNavigationBar?: boolean; + /** + * Safe area insets for the tab bar. This can be used to avoid elements like the navigation bar on Android and bottom safe area on iOS. + * The bottom insets for iOS is added by default. You can override the behavior with this option. + */ + safeAreaInsets?: { + top?: number; + right?: number; + bottom?: number; + left?: number; + }; + /** + * Specifies the largest possible scale a label font can reach. + */ + labelMaxFontSizeMultiplier?: number; + style?: Animated.WithAnimatedValue>; + activeIndicatorStyle?: StyleProp; + /** + * @optional + */ + theme?: ThemeProp; + /** + * TestID used for testing purposes + */ + testID?: string; +}; + +const MIN_TAB_WIDTH = 96; +const MAX_TAB_WIDTH = 168; +const BAR_HEIGHT = 56; +const OUTLINE_WIDTH = 64; + +const Touchable = ({ + route: _0, + style, + children, + borderless, + centered, + rippleColor, + ...rest +}: TouchableProps) => + TouchableRipple.supported ? ( + + {children} + + ) : ( + + {children} + + ); + +/** + * A navigation bar which can easily be integrated with [React Navigation's Bottom Tabs Navigator](https://reactnavigation.org/docs/bottom-tab-navigator/). + * + * ## Usage + * ### without React Navigation + * ```js + * import React from 'react'; + * import { useState } from 'react'; + * import { View } from 'react-native'; + * import { BottomNavigation, Text, Provider } from 'react-native-paper'; + * import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons'; + * + * function HomeScreen() { + * return ( + * + * Home! + * + * ); + * } + * + * function SettingsScreen() { + * return ( + * + * Settings! + * + * ); + * } + * + * export default function MyComponent() { + * const [index, setIndex] = useState(0); + * + * const routes = [ + * { key: 'home', title: 'Home', icon: 'home' }, + * { key: 'settings', title: 'Settings', icon: 'cog' }, + * ]; + + * const renderScene = ({ route }) => { + * switch (route.key) { + * case 'home': + * return ; + * case 'settings': + * return ; + * default: + * return null; + * } + * }; + * + * return ( + * + * {renderScene({ route: routes[index] })} + * { + * const newIndex = routes.findIndex((r) => r.key === route.key); + * if (newIndex !== -1) { + * setIndex(newIndex); + * } + * }} + * renderIcon={({ route, color }) => ( + * + * )} + * getLabelText={({ route }) => route.title} + * /> + * + * ); + * } + * ``` + */ +const NavigationBar = ({ + navigationState, + renderIcon, + renderLabel, + renderTouchable = ({ key, ...props }: TouchableProps) => ( + + ), + getLabelText = ({ route }: { route: Route }) => route.title, + getBadge = ({ route }: { route: Route }) => route.badge, + getAccessibilityLabel = ({ route }: { route: Route }) => + route.accessibilityLabel, + getTestID = ({ route }: { route: Route }) => route.testID, + activeColor, + inactiveColor, + keyboardHidesNavigationBar = Platform.OS === 'android', + style, + activeIndicatorStyle, + labeled = true, + animationEasing, + onTabPress, + onTabLongPress, + shifting: shiftingProp, + safeAreaInsets, + labelMaxFontSizeMultiplier = 1, + compact: compactProp, + testID = 'bottom-navigation-bar', + theme: themeOverrides, +}: Props) => { + const theme = useInternalTheme(themeOverrides); + const { colors } = theme as Theme; + const { bottom, left, right } = useSafeAreaInsets(); + const { scale } = theme.animation; + const compact = compactProp ?? false; + let shifting = shiftingProp ?? false; + + if (shifting && navigationState.routes.length < 2) { + shifting = false; + console.warn( + 'BottomNavigation.Bar needs at least 2 tabs to run shifting animation' + ); + } + + /** + * Visibility of the navigation bar, visible state is 1 and invisible is 0. + */ + const visibleAnim = useAnimatedValue(1); + + /** + * Active state of individual tab items, active state is 1 and inactive state is 0. + */ + const tabsAnims = useAnimatedValueArray( + navigationState.routes.map( + // focused === 1, unfocused === 0 + (_, i) => (i === navigationState.index ? 1 : 0) + ) + ); + + /** + * Layout of the navigation bar. + */ + const [layout, onLayout] = useLayout(); + + /** + * Track whether the keyboard is visible to show and hide the navigation bar. + */ + const [keyboardVisible, setKeyboardVisible] = React.useState(false); + + const handleKeyboardShow = React.useCallback(() => { + setKeyboardVisible(true); + Animated.timing(visibleAnim, { + toValue: 0, + duration: 150 * scale, + useNativeDriver: true, + }).start(); + }, [scale, visibleAnim]); + + const handleKeyboardHide = React.useCallback(() => { + Animated.timing(visibleAnim, { + toValue: 1, + duration: 100 * scale, + useNativeDriver: true, + }).start(() => { + setKeyboardVisible(false); + }); + }, [scale, visibleAnim]); + + const animateToIndex = React.useCallback( + (index: number) => { + Animated.parallel( + navigationState.routes.map((_, i) => + Animated.timing(tabsAnims[i], { + toValue: i === index ? 1 : 0, + duration: 150 * scale, + useNativeDriver: true, + easing: animationEasing, + }) + ) + ).start(() => { + // Workaround a bug in native animations where this is reset after first animation + tabsAnims.map((tab, i) => tab.setValue(i === index ? 1 : 0)); + }); + }, + [scale, navigationState.routes, tabsAnims, animationEasing] + ); + + React.useEffect(() => { + // Workaround for native animated bug in react-native@^0.57 + // Context: https://github.com/callstack/react-native-paper/pull/637 + animateToIndex(navigationState.index); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useIsKeyboardShown({ + onShow: handleKeyboardShow, + onHide: handleKeyboardHide, + }); + + React.useEffect(() => { + animateToIndex(navigationState.index); + }, [navigationState.index, animateToIndex]); + + const eventForIndex = (index: number) => { + const event = { + route: navigationState.routes[index], + defaultPrevented: false, + preventDefault: () => { + event.defaultPrevented = true; + }, + }; + + return event; + }; + + const { routes } = navigationState; + + const { backgroundColor: customBackground } = (StyleSheet.flatten(style) || + {}) as { + elevation?: number; + backgroundColor?: ColorValue; + }; + + const backgroundColor = customBackground || colors.surfaceContainer; + + const activeTintColor = getActiveTintColor({ + activeColor, + theme, + }); + + const inactiveTintColor = getInactiveTintColor({ + inactiveColor, + theme, + }); + + const maxTabWidth = routes.length > 3 ? MIN_TAB_WIDTH : MAX_TAB_WIDTH; + const maxTabBarWidth = maxTabWidth * routes.length; + + const insets = { + left: safeAreaInsets?.left ?? left, + right: safeAreaInsets?.right ?? right, + bottom: safeAreaInsets?.bottom ?? bottom, + }; + + return ( + + ); +}; + +NavigationBar.displayName = 'NavigationBar'; + +export default NavigationBar; + +const styles = StyleSheet.create({ + bar: { + left: 0, + right: 0, + bottom: 0, + }, + barContent: { + alignItems: 'center', + overflow: 'hidden', + }, + items: { + flexDirection: 'row', + ...(Platform.OS === 'web' + ? { + width: '100%', + } + : null), + }, + item: { + flex: 1, + // Top padding is 6 and bottom padding is 10 + // The extra 4dp bottom padding is offset by label's height + paddingVertical: 6, + }, + v3Item: { + paddingVertical: 0, + }, + iconContainer: { + height: 24, + width: 24, + marginTop: 2, + marginHorizontal: 12, + alignSelf: 'center', + }, + v3IconContainer: { + height: 32, + width: 32, + marginBottom: 4, + marginTop: 0, + justifyContent: 'center', + }, + iconWrapper: { + ...StyleSheet.absoluteFill, + alignItems: 'center', + }, + v3IconWrapper: { + top: 4, + }, + labelContainer: { + height: 16, + paddingBottom: 2, + }, + labelWrapper: { + ...StyleSheet.absoluteFill, + }, + // eslint-disable-next-line react-native/no-color-literals + label: { + fontSize: 12, + height: BAR_HEIGHT, + textAlign: 'center', + backgroundColor: 'transparent', + ...(Platform.OS === 'web' + ? { + whiteSpace: 'nowrap', + alignSelf: 'center', + } + : null), + }, + badgeContainer: { + position: 'absolute', + left: 0, + }, + v3TouchableContainer: { + paddingTop: 12, + paddingBottom: 16, + }, + v3NoLabelContainer: { + height: 80, + justifyContent: 'center', + alignItems: 'center', + }, + outline: { + width: OUTLINE_WIDTH, + height: OUTLINE_WIDTH / 2, + borderRadius: OUTLINE_WIDTH / 4, + alignSelf: 'center', + }, +}); diff --git a/src/components/NavigationBar/index.tsx b/src/components/NavigationBar/index.tsx new file mode 100644 index 0000000000..3567b88e01 --- /dev/null +++ b/src/components/NavigationBar/index.tsx @@ -0,0 +1,2 @@ +export { default } from './NavigationBar'; +export type { Props, BaseRoute } from './NavigationBar'; diff --git a/src/index.tsx b/src/index.tsx index 8863e2fa20..3bc5390de0 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -37,6 +37,7 @@ export { default as Icon } from './components/Icon'; export { default as IconButton } from './components/IconButton/IconButton'; export { default as Menu } from './components/Menu/Menu'; export { default as Modal } from './components/Modal'; +export { default as NavigationBar } from './components/NavigationBar'; export { default as Portal } from './components/Portal/Portal'; export { default as ProgressBar } from './components/ProgressBar'; export { default as RadioButton } from './components/RadioButton'; @@ -115,6 +116,10 @@ export type { Props as ListSubheaderProps } from './components/List/ListSubheade export type { Props as MenuProps } from './components/Menu/Menu'; export type { Props as MenuItemProps } from './components/Menu/MenuItem'; export type { Props as ModalProps } from './components/Modal'; +export type { + Props as NavigationBarProps, + BaseRoute as NavigationBarRoute, +} from './components/NavigationBar'; export type { Props as PortalProps } from './components/Portal/Portal'; export type { Props as PortalHostProps } from './components/Portal/PortalHost'; export type { Props as ProgressBarProps } from './components/ProgressBar'; From e0da53e79546f57d81af957a9e94da345a12ab42 Mon Sep 17 00:00:00 2001 From: Bartek Dybowski Date: Wed, 17 Jun 2026 10:12:31 +0200 Subject: [PATCH 02/18] feat(navigation-bar): apply MD3 token-based colors and sizing Extract NavigationBar/tokens.ts with resolved M3 spec values. Fix bar height (56->64), no-label container height (80->64) and active label color (onSurface->secondary, per M3 / Compose Material3 1.4.0). Re #4975 --- src/components/BottomNavigation/utils.ts | 3 +- .../NavigationBar/NavigationBar.tsx | 35 ++- src/components/NavigationBar/tokens.ts | 39 +++ .../__tests__/BottomNavigation.test.tsx | 2 +- .../BottomNavigation.test.tsx.snap | 261 ++++++++---------- 5 files changed, 184 insertions(+), 156 deletions(-) create mode 100644 src/components/NavigationBar/tokens.ts diff --git a/src/components/BottomNavigation/utils.ts b/src/components/BottomNavigation/utils.ts index ede88105e4..60f0545d69 100644 --- a/src/components/BottomNavigation/utils.ts +++ b/src/components/BottomNavigation/utils.ts @@ -47,7 +47,8 @@ export const getLabelColor = ({ } if (focused) { - return colors.onSurface; + // M3 active label color is `secondary` (changed from `onSurface`). + return colors.secondary; } return colors.onSurfaceVariant; }; diff --git a/src/components/NavigationBar/NavigationBar.tsx b/src/components/NavigationBar/NavigationBar.tsx index decd29fc60..3d138fd687 100644 --- a/src/components/NavigationBar/NavigationBar.tsx +++ b/src/components/NavigationBar/NavigationBar.tsx @@ -9,6 +9,16 @@ import type { import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { + BAR_HEIGHT, + ICON_LABEL_GAP, + INDICATOR_BORDER_RADIUS, + INDICATOR_HEIGHT, + INDICATOR_WIDTH, + MAX_TAB_WIDTH, + MIN_TAB_WIDTH, + NO_LABEL_BAR_HEIGHT, +} from './tokens'; import { useInternalTheme } from '../../core/theming'; import type { Theme, ThemeProp } from '../../types'; import useAnimatedValue from '../../utils/useAnimatedValue'; @@ -197,11 +207,6 @@ export type Props = { testID?: string; }; -const MIN_TAB_WIDTH = 96; -const MAX_TAB_WIDTH = 168; -const BAR_HEIGHT = 56; -const OUTLINE_WIDTH = 64; - const Touchable = ({ route: _0, style, @@ -807,9 +812,9 @@ const styles = StyleSheet.create({ alignSelf: 'center', }, v3IconContainer: { - height: 32, - width: 32, - marginBottom: 4, + height: INDICATOR_HEIGHT, + width: INDICATOR_HEIGHT, + marginBottom: ICON_LABEL_GAP, marginTop: 0, justifyContent: 'center', }, @@ -830,7 +835,6 @@ const styles = StyleSheet.create({ // eslint-disable-next-line react-native/no-color-literals label: { fontSize: 12, - height: BAR_HEIGHT, textAlign: 'center', backgroundColor: 'transparent', ...(Platform.OS === 'web' @@ -845,18 +849,19 @@ const styles = StyleSheet.create({ left: 0, }, v3TouchableContainer: { - paddingTop: 12, - paddingBottom: 16, + height: BAR_HEIGHT, + justifyContent: 'center', + alignItems: 'center', }, v3NoLabelContainer: { - height: 80, + height: NO_LABEL_BAR_HEIGHT, justifyContent: 'center', alignItems: 'center', }, outline: { - width: OUTLINE_WIDTH, - height: OUTLINE_WIDTH / 2, - borderRadius: OUTLINE_WIDTH / 4, + width: INDICATOR_WIDTH, + height: INDICATOR_HEIGHT, + borderRadius: INDICATOR_BORDER_RADIUS, alignSelf: 'center', }, }); diff --git a/src/components/NavigationBar/tokens.ts b/src/components/NavigationBar/tokens.ts new file mode 100644 index 0000000000..cdcca922b3 --- /dev/null +++ b/src/components/NavigationBar/tokens.ts @@ -0,0 +1,39 @@ +import type { ColorRole, TypescaleKey } from '../../theme/types'; + +/** + * Resolved M3 "flexible navigation bar" spec values. + * @see https://m3.material.io/components/navigation-bar/specs + */ + +// Container heights (dp). The flexible navigation bar is shorter than the +// original navigation bar (which was 80dp). +export const BAR_HEIGHT = 64; +export const NO_LABEL_BAR_HEIGHT = 64; + +// Active indicator / state-layer pill (dp). Matches the M3 `large` corner (16). +export const INDICATOR_WIDTH = 64; +export const INDICATOR_HEIGHT = 32; +export const INDICATOR_BORDER_RADIUS = 16; + +// Icon + spacing (dp). +export const ICON_SIZE = 24; +export const ICON_LABEL_GAP = 4; + +// Tab width clamps used by the `compact` layout (dp). +export const MIN_TAB_WIDTH = 96; +export const MAX_TAB_WIDTH = 168; + +export const LABEL_TYPESCALE: TypescaleKey = 'labelMedium'; + +/** + * Color roles per the M3 spec. `activeLabel` is `secondary` (changed from + * `onSurface` in Material 3 / Compose Material3 1.4.0). + */ +export const colorRoles = { + container: 'surfaceContainer', + activeIcon: 'onSecondaryContainer', + activeIndicator: 'secondaryContainer', + activeLabel: 'secondary', + inactiveIcon: 'onSurfaceVariant', + inactiveLabel: 'onSurfaceVariant', +} as const satisfies Record; diff --git a/src/components/__tests__/BottomNavigation.test.tsx b/src/components/__tests__/BottomNavigation.test.tsx index 21e495742a..6aa1e60f2a 100644 --- a/src/components/__tests__/BottomNavigation.test.tsx +++ b/src/components/__tests__/BottomNavigation.test.tsx @@ -569,7 +569,7 @@ describe('getLabelColor', () => { it.each([ { tintColor: '#FBF7DB', focused: true, expected: '#FBF7DB' }, { tintColor: '#853D4B', focused: true, expected: '#853D4B' }, - { tintColor: undefined, focused: true, expected: Palette.neutral10 }, + { tintColor: undefined, focused: true, expected: Palette.secondary40 }, { tintColor: undefined, focused: false, diff --git a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap index 33855bb467..14a82d59d9 100644 --- a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap @@ -184,8 +184,9 @@ exports[`allows customizing Route's type via generics 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -337,11 +338,10 @@ exports[`allows customizing Route's type via generics 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { - "color": "rgba(29, 27, 32, 1)", + "color": "rgba(98, 91, 113, 1)", "fontFamily": "System", "fontSize": 12, "fontWeight": "500", @@ -393,11 +393,10 @@ exports[`allows customizing Route's type via generics 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { - "color": "rgba(29, 27, 32, 1)", + "color": "rgba(98, 91, 113, 1)", "fontFamily": "System", "fontSize": 12, "fontWeight": "500", @@ -462,8 +461,9 @@ exports[`allows customizing Route's type via generics 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -598,7 +598,6 @@ exports[`allows customizing Route's type via generics 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -654,7 +653,6 @@ exports[`allows customizing Route's type via generics 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -868,7 +866,7 @@ exports[`hides labels in non-shifting bottom navigation 1`] = ` style={ { "alignItems": "center", - "height": 80, + "height": 64, "justifyContent": "center", } } @@ -1086,7 +1084,7 @@ exports[`hides labels in non-shifting bottom navigation 1`] = ` style={ { "alignItems": "center", - "height": 80, + "height": 64, "justifyContent": "center", } } @@ -1287,7 +1285,7 @@ exports[`hides labels in non-shifting bottom navigation 1`] = ` style={ { "alignItems": "center", - "height": 80, + "height": 64, "justifyContent": "center", } } @@ -1632,7 +1630,7 @@ exports[`hides labels in shifting bottom navigation 1`] = ` style={ { "alignItems": "center", - "height": 80, + "height": 64, "justifyContent": "center", } } @@ -1850,7 +1848,7 @@ exports[`hides labels in shifting bottom navigation 1`] = ` style={ { "alignItems": "center", - "height": 80, + "height": 64, "justifyContent": "center", } } @@ -2051,7 +2049,7 @@ exports[`hides labels in shifting bottom navigation 1`] = ` style={ { "alignItems": "center", - "height": 80, + "height": 64, "justifyContent": "center", } } @@ -2533,8 +2531,9 @@ exports[`renders bottom navigation with getLazy 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -2746,11 +2745,10 @@ exports[`renders bottom navigation with getLazy 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { - "color": "rgba(29, 27, 32, 1)", + "color": "rgba(98, 91, 113, 1)", "fontFamily": "System", "fontSize": 12, "fontWeight": "500", @@ -2802,11 +2800,10 @@ exports[`renders bottom navigation with getLazy 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { - "color": "rgba(29, 27, 32, 1)", + "color": "rgba(98, 91, 113, 1)", "fontFamily": "System", "fontSize": 12, "fontWeight": "500", @@ -2871,8 +2868,9 @@ exports[`renders bottom navigation with getLazy 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -3067,7 +3065,6 @@ exports[`renders bottom navigation with getLazy 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -3123,7 +3120,6 @@ exports[`renders bottom navigation with getLazy 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -3192,8 +3188,9 @@ exports[`renders bottom navigation with getLazy 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -3388,7 +3385,6 @@ exports[`renders bottom navigation with getLazy 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -3444,7 +3440,6 @@ exports[`renders bottom navigation with getLazy 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -3513,8 +3508,9 @@ exports[`renders bottom navigation with getLazy 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -3709,7 +3705,6 @@ exports[`renders bottom navigation with getLazy 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -3765,7 +3760,6 @@ exports[`renders bottom navigation with getLazy 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -3834,8 +3828,9 @@ exports[`renders bottom navigation with getLazy 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -4030,7 +4025,6 @@ exports[`renders bottom navigation with getLazy 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -4086,7 +4080,6 @@ exports[`renders bottom navigation with getLazy 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -4299,8 +4292,9 @@ exports[`renders bottom navigation with scene animation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -4517,11 +4511,10 @@ exports[`renders bottom navigation with scene animation 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { - "color": "rgba(29, 27, 32, 1)", + "color": "rgba(98, 91, 113, 1)", "fontFamily": "System", "fontSize": 12, "fontWeight": "500", @@ -4586,8 +4579,9 @@ exports[`renders bottom navigation with scene animation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -4787,7 +4781,6 @@ exports[`renders bottom navigation with scene animation 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -4856,8 +4849,9 @@ exports[`renders bottom navigation with scene animation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -5057,7 +5051,6 @@ exports[`renders bottom navigation with scene animation 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -5126,8 +5119,9 @@ exports[`renders bottom navigation with scene animation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -5327,7 +5321,6 @@ exports[`renders bottom navigation with scene animation 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -5396,8 +5389,9 @@ exports[`renders bottom navigation with scene animation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -5597,7 +5591,6 @@ exports[`renders bottom navigation with scene animation 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -5810,8 +5803,9 @@ exports[`renders custom icon and label in non-shifting bottom navigation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -5940,12 +5934,8 @@ exports[`renders custom icon and label in non-shifting bottom navigation 1`] = ` } } > - Route: 0 @@ -5963,12 +5953,8 @@ exports[`renders custom icon and label in non-shifting bottom navigation 1`] = ` } } > - Route: 0 @@ -6023,8 +6009,9 @@ exports[`renders custom icon and label in non-shifting bottom navigation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -6219,8 +6206,9 @@ exports[`renders custom icon and label in non-shifting bottom navigation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -6559,8 +6547,9 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -6694,12 +6683,8 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` } } > - Route: 0 @@ -6754,8 +6739,9 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -6932,8 +6918,9 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -7110,8 +7097,9 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -7288,8 +7276,9 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -7610,8 +7599,9 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -7823,7 +7813,6 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -7879,7 +7868,6 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -7948,8 +7936,9 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -8144,7 +8133,6 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -8200,7 +8188,6 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -8269,8 +8256,9 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -8465,7 +8453,6 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -8521,7 +8508,6 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -8734,8 +8720,9 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -8952,7 +8939,6 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -9021,8 +9007,9 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -9222,7 +9209,6 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -9291,8 +9277,9 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -9492,7 +9479,6 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -9705,8 +9691,9 @@ exports[`renders non-shifting bottom navigation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -9918,11 +9905,10 @@ exports[`renders non-shifting bottom navigation 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { - "color": "rgba(29, 27, 32, 1)", + "color": "rgba(98, 91, 113, 1)", "fontFamily": "System", "fontSize": 12, "fontWeight": "500", @@ -9974,11 +9960,10 @@ exports[`renders non-shifting bottom navigation 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { - "color": "rgba(29, 27, 32, 1)", + "color": "rgba(98, 91, 113, 1)", "fontFamily": "System", "fontSize": 12, "fontWeight": "500", @@ -10043,8 +10028,9 @@ exports[`renders non-shifting bottom navigation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -10239,7 +10225,6 @@ exports[`renders non-shifting bottom navigation 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -10295,7 +10280,6 @@ exports[`renders non-shifting bottom navigation 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -10364,8 +10348,9 @@ exports[`renders non-shifting bottom navigation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -10560,7 +10545,6 @@ exports[`renders non-shifting bottom navigation 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -10616,7 +10600,6 @@ exports[`renders non-shifting bottom navigation 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -10829,8 +10812,9 @@ exports[`renders shifting bottom navigation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -11047,11 +11031,10 @@ exports[`renders shifting bottom navigation 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { - "color": "rgba(29, 27, 32, 1)", + "color": "rgba(98, 91, 113, 1)", "fontFamily": "System", "fontSize": 12, "fontWeight": "500", @@ -11116,8 +11099,9 @@ exports[`renders shifting bottom navigation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -11317,7 +11301,6 @@ exports[`renders shifting bottom navigation 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -11386,8 +11369,9 @@ exports[`renders shifting bottom navigation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -11587,7 +11571,6 @@ exports[`renders shifting bottom navigation 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -11656,8 +11639,9 @@ exports[`renders shifting bottom navigation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -11857,7 +11841,6 @@ exports[`renders shifting bottom navigation 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { @@ -11926,8 +11909,9 @@ exports[`renders shifting bottom navigation 1`] = ` pointerEvents="none" style={ { - "paddingBottom": 16, - "paddingTop": 12, + "alignItems": "center", + "height": 64, + "justifyContent": "center", } } > @@ -12127,7 +12111,6 @@ exports[`renders shifting bottom navigation 1`] = ` { "backgroundColor": "transparent", "fontSize": 12, - "height": 56, "textAlign": "center", }, { From b384ecbd0f5b30319b6154bef7cad0cc3a7b6478 Mon Sep 17 00:00:00 2001 From: Bartek Dybowski Date: Wed, 17 Jun 2026 10:37:17 +0200 Subject: [PATCH 03/18] refactor(navigation-bar): remove MD2 shifting mode from the bar Remove the `shifting` prop and all its branches from NavigationBar; it is a Material Design 2 pattern with no MD3 equivalent. The wrapper no longer forwards it and keeps `shifting` as a deprecated no-op prop. Scene transition animation (sceneAnimationType) is unaffected. Re #4975 --- .../BottomNavigation/BottomNavigation.tsx | 19 +- .../NavigationBar/NavigationBar.tsx | 135 +-- .../__tests__/BottomNavigation.test.tsx | 34 +- .../BottomNavigation.test.tsx.snap | 992 +++++++++++++++--- 4 files changed, 914 insertions(+), 266 deletions(-) diff --git a/src/components/BottomNavigation/BottomNavigation.tsx b/src/components/BottomNavigation/BottomNavigation.tsx index fc821039df..1112a92e3d 100644 --- a/src/components/BottomNavigation/BottomNavigation.tsx +++ b/src/components/BottomNavigation/BottomNavigation.tsx @@ -49,11 +49,10 @@ type TouchableProps = TouchableRippleProps & { export type Props = { /** - * Whether the shifting style is used, the active tab icon shifts up to show the label and the inactive tabs won't have a label. - * - * By default, this is `false` with theme version 3 and `true` when you have more than 3 tabs. - * Pass `shifting={false}` to explicitly disable this animation, or `shifting={true}` to always use this animation. - * Note that you need at least 2 tabs be able to run this animation. + * @deprecated The `shifting` style is a Material Design 2 pattern that is not + * part of Material Design 3 and no longer has any effect. It will be removed + * in a future version. To animate scene transitions, use `sceneAnimationType` + * and `sceneAnimationEnabled` instead. */ shifting?: boolean; /** @@ -330,7 +329,6 @@ const BottomNavigation = ({ onTabPress, onTabLongPress, onIndexChange, - shifting: shiftingProp, safeAreaInsets, labelMaxFontSizeMultiplier = 1, compact: compactProp, @@ -341,14 +339,6 @@ const BottomNavigation = ({ const theme = useInternalTheme(themeOverrides); const { scale } = theme.animation; const compact = compactProp ?? false; - let shifting = shiftingProp ?? false; - - if (shifting && navigationState.routes.length < 2) { - shifting = false; - console.warn( - 'BottomNavigation needs at least 2 tabs to run shifting animation' - ); - } const focusedKey = navigationState.routes[navigationState.index].key; @@ -574,7 +564,6 @@ const BottomNavigation = ({ animationEasing={sceneAnimationEasing} onTabPress={handleTabPress} onTabLongPress={onTabLongPress} - shifting={shifting} safeAreaInsets={safeAreaInsets} labelMaxFontSizeMultiplier={labelMaxFontSizeMultiplier} compact={compact} diff --git a/src/components/NavigationBar/NavigationBar.tsx b/src/components/NavigationBar/NavigationBar.tsx index 3d138fd687..3eacd7bfae 100644 --- a/src/components/NavigationBar/NavigationBar.tsx +++ b/src/components/NavigationBar/NavigationBar.tsx @@ -69,14 +69,6 @@ type TouchableProps = TouchableRippleProps & { }; export type Props = { - /** - * Whether the shifting style is used, the active tab icon shifts up to show the label and the inactive tabs won't have a label. - * - * By default, this is `false` with theme version 3 and `true` when you have more than 3 tabs. - * Pass `shifting={false}` to explicitly disable this animation, or `shifting={true}` to always use this animation. - * Note that you need at least 2 tabs be able to run this animation. - */ - shifting?: boolean; /** * Whether to show labels in tabs. When `false`, only icons will be displayed. */ @@ -322,7 +314,6 @@ const NavigationBar = ({ animationEasing, onTabPress, onTabLongPress, - shifting: shiftingProp, safeAreaInsets, labelMaxFontSizeMultiplier = 1, compact: compactProp, @@ -334,14 +325,6 @@ const NavigationBar = ({ const { bottom, left, right } = useSafeAreaInsets(); const { scale } = theme.animation; const compact = compactProp ?? false; - let shifting = shiftingProp ?? false; - - if (shifting && navigationState.routes.length < 2) { - shifting = false; - console.warn( - 'BottomNavigation.Bar needs at least 2 tabs to run shifting animation' - ); - } /** * Visibility of the navigation bar, visible state is 1 and invisible is 0. @@ -519,31 +502,11 @@ const NavigationBar = ({ const focused = navigationState.index === index; const active = tabsAnims[index]; - // Move down the icon to account for no-label in shifting and smaller label in non-shifting. - const translateY = labeled - ? shifting - ? active.interpolate({ - inputRange: [0, 1], - outputRange: [7, 0], - }) - : 0 - : 7; - - // We render the active icon and label on top of inactive ones and cross-fade them on change. - // This trick gives the illusion that we are animating between active and inactive colors. - // This is to ensure that we can use native driver, as colors cannot be animated with native driver. - const activeOpacity = active; - const inactiveOpacity = active.interpolate({ - inputRange: [0, 1], - outputRange: [1, 0], - }); - - const v3ActiveOpacity = focused ? 1 : 0; - const v3InactiveOpacity = shifting - ? inactiveOpacity - : focused - ? 0 - : 1; + // We render the active icon and label on top of the inactive ones + // and toggle their opacity on change, so the active/inactive colors + // swap without animating color (which the native driver can't do). + const activeOpacity = focused ? 1 : 0; + const inactiveOpacity = focused ? 0 : 1; // Scale horizontally the outline pill const outlineScale = focused @@ -577,8 +540,6 @@ const NavigationBar = ({ : 0, }; - const isLegacyOrV3Shifting = shifting && labeled; - const font = (theme as Theme).fonts.labelMedium; return renderTouchable({ @@ -604,13 +565,7 @@ const NavigationBar = ({ } > {focused && ( ({ styles.iconWrapper, styles.v3IconWrapper, { - opacity: isLegacyOrV3Shifting - ? activeOpacity - : v3ActiveOpacity, + opacity: activeOpacity, }, ]} > @@ -658,9 +611,7 @@ const NavigationBar = ({ styles.iconWrapper, styles.v3IconWrapper, { - opacity: isLegacyOrV3Shifting - ? inactiveOpacity - : v3InactiveOpacity, + opacity: inactiveOpacity, }, ]} > @@ -698,9 +649,7 @@ const NavigationBar = ({ style={[ styles.labelWrapper, { - opacity: isLegacyOrV3Shifting - ? activeOpacity - : v3ActiveOpacity, + opacity: activeOpacity, }, ]} > @@ -726,41 +675,37 @@ const NavigationBar = ({ )} - {shifting ? null : ( - - {renderLabel ? ( - renderLabel({ - route, - focused: false, - color: inactiveLabelColor, - }) - ) : ( - - {getLabelText({ route })} - - )} - - )} + + {renderLabel ? ( + renderLabel({ + route, + focused: false, + color: inactiveLabelColor, + }) + ) : ( + + {getLabelText({ route })} + + )} + ) : null} diff --git a/src/components/__tests__/BottomNavigation.test.tsx b/src/components/__tests__/BottomNavigation.test.tsx index 6aa1e60f2a..2f1ca8d0e3 100644 --- a/src/components/__tests__/BottomNavigation.test.tsx +++ b/src/components/__tests__/BottomNavigation.test.tsx @@ -154,14 +154,13 @@ it('calls onIndexChange', async () => { renderScene={renderScene} /> ); - - await layoutNavigationBar(); - + // Both the active and inactive labels render per tab, so target the last + // match (the label inside the tab) to fire the tab press. // pressing same index as active navigation state does not call onIndexChange - await userEvent.press(getTab(0)); + fireEvent(screen.getAllByText('Route: 0').at(-1)!, 'onPress'); expect(onIndexChange).not.toHaveBeenCalled(); - await userEvent.press(getTab(1)); + fireEvent(screen.getAllByText('Route: 1').at(-1)!, 'onPress'); expect(onIndexChange).toHaveBeenCalledTimes(1); }); @@ -178,10 +177,7 @@ it('calls onTabPress', async () => { renderScene={renderScene} /> ); - - await layoutNavigationBar(); - - await userEvent.press(getTab(1)); + fireEvent(screen.getAllByText('Route: 1').at(-1)!, 'onPress'); expect(onTabPress).toHaveBeenCalled(); expect(onTabPress).toHaveBeenCalledTimes(1); expect(onTabPress).toHaveBeenLastCalledWith( @@ -208,10 +204,7 @@ it('calls onTabLongPress', async () => { renderScene={renderScene} /> ); - - await layoutNavigationBar(); - - await userEvent.longPress(getTab(2)); + fireEvent(screen.getAllByText('Route: 2').at(-1)!, 'onLongPress'); expect(onTabLongPress).toHaveBeenCalled(); expect(onTabLongPress).toHaveBeenCalledTimes(1); expect(onTabLongPress).toHaveBeenLastCalledWith( @@ -240,21 +233,22 @@ it('renders non-shifting bottom navigation', async () => { expect(tree).toMatchSnapshot(); }); -it('does not crash when shifting is true and the number of tabs in the navigationState is less than 2', async () => { - jest.spyOn(console, 'warn').mockImplementation(() => {}); +it('does not warn or crash when the deprecated shifting prop is passed with fewer than 2 tabs', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); - await render( + const { getByTestId } = render( route.title} + testID="bottom-navigation" /> ); - expect(console.warn).toHaveBeenCalledWith( - 'BottomNavigation needs at least 2 tabs to run shifting animation' - ); + // `shifting` is a deprecated no-op, so it no longer warns about tab count. + expect(getByTestId('bottom-navigation-bar')).toBeDefined(); + expect(warn).not.toHaveBeenCalled(); jest.restoreAllMocks(); }); diff --git a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap index 14a82d59d9..f8727c3192 100644 --- a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap @@ -4308,11 +4308,6 @@ exports[`renders bottom navigation with scene animation 1`] = ` "marginBottom": 4, "marginHorizontal": 12, "marginTop": 0, - "transform": [ - { - "translateY": 0, - }, - ], "width": 32, } } @@ -4529,6 +4524,61 @@ exports[`renders bottom navigation with scene animation 1`] = ` Route: 0 + + + Route: 0 + + @@ -4595,11 +4645,6 @@ exports[`renders bottom navigation with scene animation 1`] = ` "marginBottom": 4, "marginHorizontal": 12, "marginTop": 0, - "transform": [ - { - "translateY": 7, - }, - ], "width": 32, } } @@ -4799,6 +4844,61 @@ exports[`renders bottom navigation with scene animation 1`] = ` Route: 1 + + + Route: 1 + + @@ -4865,11 +4965,6 @@ exports[`renders bottom navigation with scene animation 1`] = ` "marginBottom": 4, "marginHorizontal": 12, "marginTop": 0, - "transform": [ - { - "translateY": 7, - }, - ], "width": 32, } } @@ -5069,6 +5164,61 @@ exports[`renders bottom navigation with scene animation 1`] = ` Route: 2 + + + Route: 2 + + @@ -5135,11 +5285,6 @@ exports[`renders bottom navigation with scene animation 1`] = ` "marginBottom": 4, "marginHorizontal": 12, "marginTop": 0, - "transform": [ - { - "translateY": 7, - }, - ], "width": 32, } } @@ -5339,6 +5484,61 @@ exports[`renders bottom navigation with scene animation 1`] = ` Route: 3 + + + Route: 3 + + @@ -5405,11 +5605,6 @@ exports[`renders bottom navigation with scene animation 1`] = ` "marginBottom": 4, "marginHorizontal": 12, "marginTop": 0, - "transform": [ - { - "translateY": 7, - }, - ], "width": 32, } } @@ -5609,32 +5804,87 @@ exports[`renders bottom navigation with scene animation 1`] = ` Route: 4 - - - - - - - - -`; - -exports[`renders custom icon and label in non-shifting bottom navigation 1`] = ` - - + + Route: 4 + + + + + + + + + + +`; + +exports[`renders custom icon and label in non-shifting bottom navigation 1`] = ` + + + + + Route: 0 + + @@ -6755,11 +7019,6 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` "marginBottom": 4, "marginHorizontal": 12, "marginTop": 0, - "transform": [ - { - "translateY": 7, - }, - ], "width": 32, } } @@ -6868,6 +7127,25 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` Route: 1 + + + Route: 1 + + @@ -6934,11 +7212,6 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` "marginBottom": 4, "marginHorizontal": 12, "marginTop": 0, - "transform": [ - { - "translateY": 7, - }, - ], "width": 32, } } @@ -7047,6 +7320,25 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` Route: 2 + + + Route: 2 + + @@ -7113,11 +7405,6 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` "marginBottom": 4, "marginHorizontal": 12, "marginTop": 0, - "transform": [ - { - "translateY": 7, - }, - ], "width": 32, } } @@ -7226,6 +7513,25 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` Route: 3 + + + Route: 3 + + @@ -7292,11 +7598,6 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` "marginBottom": 4, "marginHorizontal": 12, "marginTop": 0, - "transform": [ - { - "translateY": 7, - }, - ], "width": 32, } } @@ -7405,6 +7706,25 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` Route: 4 + + + Route: 4 + + @@ -8736,11 +9056,6 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav "marginBottom": 4, "marginHorizontal": 12, "marginTop": 0, - "transform": [ - { - "translateY": 0, - }, - ], "width": 32, } } @@ -8957,6 +9272,61 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav Route: 0 + + + Route: 0 + + @@ -9023,11 +9393,6 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav "marginBottom": 4, "marginHorizontal": 12, "marginTop": 0, - "transform": [ - { - "translateY": 7, - }, - ], "width": 32, } } @@ -9227,26 +9592,81 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav Route: 1 - - - - + + Route: 1 + + + + + + + + + Route: 2 + + @@ -10828,11 +11298,6 @@ exports[`renders shifting bottom navigation 1`] = ` "marginBottom": 4, "marginHorizontal": 12, "marginTop": 0, - "transform": [ - { - "translateY": 0, - }, - ], "width": 32, } } @@ -11049,6 +11514,61 @@ exports[`renders shifting bottom navigation 1`] = ` Route: 0 + + + Route: 0 + + @@ -11115,11 +11635,6 @@ exports[`renders shifting bottom navigation 1`] = ` "marginBottom": 4, "marginHorizontal": 12, "marginTop": 0, - "transform": [ - { - "translateY": 7, - }, - ], "width": 32, } } @@ -11319,6 +11834,61 @@ exports[`renders shifting bottom navigation 1`] = ` Route: 1 + + + Route: 1 + + @@ -11385,11 +11955,6 @@ exports[`renders shifting bottom navigation 1`] = ` "marginBottom": 4, "marginHorizontal": 12, "marginTop": 0, - "transform": [ - { - "translateY": 7, - }, - ], "width": 32, } } @@ -11589,6 +12154,61 @@ exports[`renders shifting bottom navigation 1`] = ` Route: 2 + + + Route: 2 + + @@ -11655,11 +12275,6 @@ exports[`renders shifting bottom navigation 1`] = ` "marginBottom": 4, "marginHorizontal": 12, "marginTop": 0, - "transform": [ - { - "translateY": 7, - }, - ], "width": 32, } } @@ -11859,6 +12474,61 @@ exports[`renders shifting bottom navigation 1`] = ` Route: 3 + + + Route: 3 + + @@ -11925,11 +12595,6 @@ exports[`renders shifting bottom navigation 1`] = ` "marginBottom": 4, "marginHorizontal": 12, "marginTop": 0, - "transform": [ - { - "translateY": 7, - }, - ], "width": 32, } } @@ -12129,6 +12794,61 @@ exports[`renders shifting bottom navigation 1`] = ` Route: 4 + + + Route: 4 + + From 33bdcb9f97daed5e342d1e7a7a0a5ddaee9b9e9c Mon Sep 17 00:00:00 2001 From: Bartek Dybowski Date: Wed, 17 Jun 2026 11:00:44 +0200 Subject: [PATCH 04/18] feat(navigation-bar): add visible MD3 state layers Extract NavigationBarItem and render a pill-shaped state-layer overlay driven by hover/focus/press interaction state via getStateLayer (8% hover, 10% focus/press), replacing the suppressed ripple feedback. Re #4975 --- .../NavigationBar/NavigationBar.tsx | 503 ++++---- .../__tests__/BottomNavigation.test.tsx | 38 + .../BottomNavigation.test.tsx.snap | 1114 ++++++++++++++++- 3 files changed, 1427 insertions(+), 228 deletions(-) diff --git a/src/components/NavigationBar/NavigationBar.tsx b/src/components/NavigationBar/NavigationBar.tsx index 3eacd7bfae..ee6c5cc40f 100644 --- a/src/components/NavigationBar/NavigationBar.tsx +++ b/src/components/NavigationBar/NavigationBar.tsx @@ -11,6 +11,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { BAR_HEIGHT, + colorRoles, ICON_LABEL_GAP, INDICATOR_BORDER_RADIUS, INDICATOR_HEIGHT, @@ -20,6 +21,7 @@ import { NO_LABEL_BAR_HEIGHT, } from './tokens'; import { useInternalTheme } from '../../core/theming'; +import { getStateLayer } from '../../theme/utils/state'; import type { Theme, ThemeProp } from '../../types'; import useAnimatedValue from '../../utils/useAnimatedValue'; import useAnimatedValueArray from '../../utils/useAnimatedValueArray'; @@ -66,6 +68,10 @@ type TouchableProps = TouchableRippleProps & { borderless?: boolean; centered?: boolean; rippleColor?: ColorValue; + onHoverIn?: () => void; + onHoverOut?: () => void; + onFocus?: () => void; + onBlur?: () => void; }; export type Props = { @@ -225,6 +231,256 @@ const Touchable = ({ ); +type ItemProps = { + route: Route; + focused: boolean; + active: Animated.Value; + labeled: boolean; + activeTintColor: ColorValue; + inactiveTintColor: ColorValue; + activeColor?: string; + inactiveColor?: string; + renderIcon?: Props['renderIcon']; + renderLabel?: Props['renderLabel']; + renderTouchable: NonNullable['renderTouchable']>; + getLabelText: NonNullable['getLabelText']>; + getBadge: NonNullable['getBadge']>; + getTestID: NonNullable['getTestID']>; + getAccessibilityLabel: NonNullable['getAccessibilityLabel']>; + onPress: () => void; + onLongPress: () => void; + activeIndicatorStyle?: StyleProp; + labelMaxFontSizeMultiplier?: number; + theme: Theme; +}; + +const NavigationBarItem = ({ + route, + focused, + active, + labeled, + activeTintColor, + inactiveTintColor, + activeColor, + inactiveColor, + renderIcon, + renderLabel, + renderTouchable, + getLabelText, + getBadge, + getTestID, + getAccessibilityLabel, + onPress, + onLongPress, + activeIndicatorStyle, + labelMaxFontSizeMultiplier = 1, + theme, +}: ItemProps) => { + const { colors } = theme; + + const [hovered, setHovered] = React.useState(false); + const [keyboardFocused, setKeyboardFocused] = React.useState(false); + const [pressed, setPressed] = React.useState(false); + + // We render the active icon and label on top of the inactive ones and toggle + // their opacity on change, so the active/inactive colors swap without + // animating color (which the native driver can't do). + const activeOpacity = focused ? 1 : 0; + const inactiveOpacity = focused ? 0 : 1; + + // Scale horizontally the active-indicator pill. + const outlineScale = focused + ? active.interpolate({ + inputRange: [0, 1], + outputRange: [0.5, 1], + }) + : 0; + + const badge = getBadge({ route }); + + const activeLabelColor = getLabelColor({ + tintColor: activeTintColor, + hasColor: Boolean(activeColor), + focused, + theme, + }); + + const inactiveLabelColor = getLabelColor({ + tintColor: inactiveTintColor, + hasColor: Boolean(inactiveColor), + focused, + theme, + }); + + const badgeStyle = { + top: typeof badge === 'boolean' ? 4 : 2, + right: + badge != null && typeof badge !== 'boolean' + ? String(badge).length * -2 + : 0, + }; + + const font = theme.fonts.labelMedium; + + // MD3 state layer: visible on hover (8%) and focus/press (10%), shaped like + // the active-indicator pill. Active items use the on-secondary-container + // role, inactive items the on-surface-variant role. + const stateLayerRole = focused + ? colorRoles.activeIcon + : colorRoles.inactiveIcon; + const stateLayer = pressed + ? getStateLayer(theme, stateLayerRole, 'pressed') + : keyboardFocused + ? getStateLayer(theme, stateLayerRole, 'focused') + : hovered + ? getStateLayer(theme, stateLayerRole, 'hovered') + : null; + + const itemTestID = getTestID({ route }); + + return renderTouchable({ + key: route.key, + route, + borderless: true, + centered: true, + rippleColor: 'transparent', + onPress, + onLongPress, + onPressIn: () => setPressed(true), + onPressOut: () => setPressed(false), + onHoverIn: () => setHovered(true), + onHoverOut: () => setHovered(false), + onFocus: () => setKeyboardFocused(true), + onBlur: () => setKeyboardFocused(false), + testID: itemTestID, + accessibilityLabel: getAccessibilityLabel({ route }), + accessibilityRole: Platform.OS === 'ios' ? 'button' : 'tab', + accessibilityState: { selected: focused }, + style: [styles.item, styles.v3Item], + children: ( + + + {focused && ( + + )} + + + + + {renderIcon ? ( + renderIcon({ route, focused: true, color: activeTintColor }) + ) : ( + + )} + + + {renderIcon ? ( + renderIcon({ route, focused: false, color: inactiveTintColor }) + ) : ( + + )} + + + {typeof badge === 'boolean' ? ( + + ) : ( + + {badge} + + )} + + + {labeled ? ( + + + {renderLabel ? ( + renderLabel({ route, focused: true, color: activeLabelColor }) + ) : ( + + {getLabelText({ route })} + + )} + + + {renderLabel ? ( + renderLabel({ + route, + focused: false, + color: inactiveLabelColor, + }) + ) : ( + + {getLabelText({ route })} + + )} + + + ) : null} + + ), + }); +}; + /** * A navigation bar which can easily be integrated with [React Navigation's Bottom Tabs Navigator](https://reactnavigation.org/docs/bottom-tab-navigator/). * @@ -500,217 +756,32 @@ const NavigationBar = ({ > {routes.map((route, index) => { const focused = navigationState.index === index; - const active = tabsAnims[index]; - - // We render the active icon and label on top of the inactive ones - // and toggle their opacity on change, so the active/inactive colors - // swap without animating color (which the native driver can't do). - const activeOpacity = focused ? 1 : 0; - const inactiveOpacity = focused ? 0 : 1; - - // Scale horizontally the outline pill - const outlineScale = focused - ? active.interpolate({ - inputRange: [0, 1], - outputRange: [0.5, 1], - }) - : 0; - - const badge = getBadge({ route }); - - const activeLabelColor = getLabelColor({ - tintColor: activeTintColor, - hasColor: Boolean(activeColor), - focused, - theme, - }); - - const inactiveLabelColor = getLabelColor({ - tintColor: inactiveTintColor, - hasColor: Boolean(inactiveColor), - focused, - theme, - }); - - const badgeStyle = { - top: typeof badge === 'boolean' ? 4 : 2, - right: - badge != null && typeof badge !== 'boolean' - ? String(badge).length * -2 - : 0, - }; - - const font = (theme as Theme).fonts.labelMedium; - - return renderTouchable({ - key: route.key, - route, - borderless: true, - centered: true, - rippleColor: 'transparent', - onPress: () => onTabPress(eventForIndex(index)), - onLongPress: () => onTabLongPress?.(eventForIndex(index)), - testID: getTestID({ route }), - accessibilityLabel: getAccessibilityLabel({ route }), - accessibilityRole: Platform.OS === 'ios' ? 'button' : 'tab', - accessibilityState: { selected: focused }, - style: [styles.item, styles.v3Item], - children: ( - - - {focused && ( - - )} - - {renderIcon ? ( - renderIcon({ - route, - focused: true, - color: activeTintColor, - }) - ) : ( - - )} - - - {renderIcon ? ( - renderIcon({ - route, - focused: false, - color: inactiveTintColor, - }) - ) : ( - - )} - - - {typeof badge === 'boolean' ? ( - - ) : ( - - {badge} - - )} - - - {labeled ? ( - - - {renderLabel ? ( - renderLabel({ - route, - focused: true, - color: activeLabelColor, - }) - ) : ( - - {getLabelText({ route })} - - )} - - - {renderLabel ? ( - renderLabel({ - route, - focused: false, - color: inactiveLabelColor, - }) - ) : ( - - {getLabelText({ route })} - - )} - - - ) : null} - - ), - }); + + return ( + onTabPress(eventForIndex(index))} + onLongPress={() => onTabLongPress?.(eventForIndex(index))} + activeIndicatorStyle={activeIndicatorStyle} + labelMaxFontSizeMultiplier={labelMaxFontSizeMultiplier} + theme={theme as Theme} + /> + ); })} @@ -809,4 +880,14 @@ const styles = StyleSheet.create({ borderRadius: INDICATOR_BORDER_RADIUS, alignSelf: 'center', }, + stateLayerWrapper: { + ...StyleSheet.absoluteFill, + alignItems: 'center', + justifyContent: 'center', + }, + stateLayer: { + width: INDICATOR_WIDTH, + height: INDICATOR_HEIGHT, + borderRadius: INDICATOR_BORDER_RADIUS, + }, }); diff --git a/src/components/__tests__/BottomNavigation.test.tsx b/src/components/__tests__/BottomNavigation.test.tsx index 2f1ca8d0e3..3cfd4ba5fe 100644 --- a/src/components/__tests__/BottomNavigation.test.tsx +++ b/src/components/__tests__/BottomNavigation.test.tsx @@ -14,6 +14,7 @@ import { getLabelColor, } from '../BottomNavigation/utils'; import Icon from '../Icon'; +import NavigationBar from '../NavigationBar/NavigationBar'; const styles = StyleSheet.create({ backgroundColor: { @@ -526,6 +527,43 @@ it('does not render legacy ripple overlay when shifting is disabled', async () = ).not.toBeOnTheScreen(); }); +it('renders MD3 state layers on hover, focus and press', () => { + const navigationState = { + index: 0, + routes: [ + { key: 'a', title: 'Route: 0', focusedIcon: 'magnify', testID: 'tab-a' }, + { key: 'b', title: 'Route: 1', focusedIcon: 'camera', testID: 'tab-b' }, + ], + }; + + const { getByTestId } = render( + + ); + + const layerOpacity = () => + StyleSheet.flatten(getByTestId('tab-b-state-layer').props.style).opacity; + + // Idle: no visible state layer. + expect(layerOpacity()).toBeUndefined(); + + // Hovered: 8% state layer. + fireEvent(getByTestId('tab-b'), 'hoverIn'); + expect(layerOpacity()).toBe(0.08); + fireEvent(getByTestId('tab-b'), 'hoverOut'); + expect(layerOpacity()).toBeUndefined(); + + // Focused: 10% state layer. + fireEvent(getByTestId('tab-b'), 'focus'); + expect(layerOpacity()).toBe(0.1); + fireEvent(getByTestId('tab-b'), 'blur'); + + // Pressed: 10% state layer. + fireEvent(getByTestId('tab-b'), 'pressIn'); + expect(layerOpacity()).toBe(0.1); + fireEvent(getByTestId('tab-b'), 'pressOut'); + expect(layerOpacity()).toBeUndefined(); +}); + describe('getActiveTintColor', () => { it.each` activeColor | expected diff --git a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap index f8727c3192..3de73ae3c5 100644 --- a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap @@ -221,6 +221,33 @@ exports[`allows customizing Route's type via generics 1`] = ` } } /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + } + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Date: Wed, 17 Jun 2026 11:18:02 +0200 Subject: [PATCH 05/18] feat(navigation-bar): drive motion from tokens and spring the indicator Replace hardcoded durations/easings with motion tokens and animate the active indicator with the M3-Expressive spatial spring (Animated.spring). A custom animationEasing still opts into timed movement; reduce-motion jumps instantly. Re #4975 --- .../NavigationBar/NavigationBar.tsx | 57 ++++++++++++++----- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/src/components/NavigationBar/NavigationBar.tsx b/src/components/NavigationBar/NavigationBar.tsx index ee6c5cc40f..c3eab191f5 100644 --- a/src/components/NavigationBar/NavigationBar.tsx +++ b/src/components/NavigationBar/NavigationBar.tsx @@ -1,5 +1,12 @@ import * as React from 'react'; -import { Animated, Platform, StyleSheet, Pressable, View } from 'react-native'; +import { + Animated, + Easing, + Platform, + StyleSheet, + Pressable, + View, +} from 'react-native'; import type { ColorValue, EasingFunction, @@ -21,6 +28,7 @@ import { NO_LABEL_BAR_HEIGHT, } from './tokens'; import { useInternalTheme } from '../../core/theming'; +import { toRawSpring } from '../../theme/tokens/sys/motion'; import { getStateLayer } from '../../theme/utils/state'; import type { Theme, ThemeProp } from '../../types'; import useAnimatedValue from '../../utils/useAnimatedValue'; @@ -577,7 +585,7 @@ const NavigationBar = ({ theme: themeOverrides, }: Props) => { const theme = useInternalTheme(themeOverrides); - const { colors } = theme as Theme; + const { colors, motion } = theme as Theme; const { bottom, left, right } = useSafeAreaInsets(); const { scale } = theme.animation; const compact = compactProp ?? false; @@ -611,38 +619,57 @@ const NavigationBar = ({ setKeyboardVisible(true); Animated.timing(visibleAnim, { toValue: 0, - duration: 150 * scale, + // The bar slides out, so accelerate (exit). + duration: motion.duration.short3 * scale, + easing: Easing.bezier(...motion.easing.standardAccelerate), useNativeDriver: true, }).start(); - }, [scale, visibleAnim]); + }, [motion, scale, visibleAnim]); const handleKeyboardHide = React.useCallback(() => { Animated.timing(visibleAnim, { toValue: 1, - duration: 100 * scale, + // The bar slides back in, so decelerate (enter). + duration: motion.duration.short2 * scale, + easing: Easing.bezier(...motion.easing.standardDecelerate), useNativeDriver: true, }).start(() => { setKeyboardVisible(false); }); - }, [scale, visibleAnim]); + }, [motion, scale, visibleAnim]); const animateToIndex = React.useCallback( (index: number) => { + // When animations are disabled (e.g. reduce motion), jump to the value. + if (scale === 0) { + tabsAnims.forEach((tab, i) => tab.setValue(i === index ? 1 : 0)); + return; + } + Animated.parallel( - navigationState.routes.map((_, i) => - Animated.timing(tabsAnims[i], { - toValue: i === index ? 1 : 0, - duration: 150 * scale, - useNativeDriver: true, - easing: animationEasing, - }) - ) + navigationState.routes.map((_, i) => { + const toValue = i === index ? 1 : 0; + // Spring the active indicator for the M3-Expressive selection motion. + // A custom `animationEasing` opts back into timed (eased) movement. + return animationEasing + ? Animated.timing(tabsAnims[i], { + toValue, + duration: motion.duration.short4 * scale, + easing: animationEasing, + useNativeDriver: true, + }) + : Animated.spring(tabsAnims[i], { + toValue, + ...toRawSpring(motion.spring.fast.spatial), + useNativeDriver: true, + }); + }) ).start(() => { // Workaround a bug in native animations where this is reset after first animation tabsAnims.map((tab, i) => tab.setValue(i === index ? 1 : 0)); }); }, - [scale, navigationState.routes, tabsAnims, animationEasing] + [scale, navigationState.routes, tabsAnims, animationEasing, motion] ); React.useEffect(() => { From c56ceffd3b7ea5edbc9206cb2c6ec2a917f20cf6 Mon Sep 17 00:00:00 2001 From: Bartek Dybowski Date: Wed, 17 Jun 2026 11:35:07 +0200 Subject: [PATCH 06/18] feat(navigation-bar): add flexible horizontal variant Add a `variant` prop ('stacked' | 'horizontal'). The horizontal arrangement places the icon beside the label inside a content-hugging indicator pill, for medium-width windows. It is a no-op without labels. The indicator springs its opacity/scale in place. Re #4975 --- .../NavigationBar/NavigationBar.tsx | 365 +++++++--- .../__tests__/BottomNavigation.test.tsx | 27 + .../BottomNavigation.test.tsx.snap | 665 ++++++++++++++++++ 3 files changed, 940 insertions(+), 117 deletions(-) diff --git a/src/components/NavigationBar/NavigationBar.tsx b/src/components/NavigationBar/NavigationBar.tsx index c3eab191f5..724ef62950 100644 --- a/src/components/NavigationBar/NavigationBar.tsx +++ b/src/components/NavigationBar/NavigationBar.tsx @@ -87,6 +87,15 @@ export type Props = { * Whether to show labels in tabs. When `false`, only icons will be displayed. */ labeled?: boolean; + /** + * The item layout variant of the flexible navigation bar. + * + * - `stacked` (default): the icon sits above the label. + * - `horizontal`: the icon sits beside the label and the active indicator + * hugs both. Recommended for medium-width windows (e.g. foldables and + * tablets). Has no effect when `labeled` is `false`. + */ + variant?: 'stacked' | 'horizontal'; /** * Whether tabs should be spread across the entire width. */ @@ -244,6 +253,7 @@ type ItemProps = { focused: boolean; active: Animated.Value; labeled: boolean; + variant: 'stacked' | 'horizontal'; activeTintColor: ColorValue; inactiveTintColor: ColorValue; activeColor?: string; @@ -267,6 +277,7 @@ const NavigationBarItem = ({ focused, active, labeled, + variant, activeTintColor, inactiveTintColor, activeColor, @@ -345,96 +356,185 @@ const NavigationBarItem = ({ : null; const itemTestID = getTestID({ route }); + // The horizontal arrangement places the label beside the icon and only + // applies when labels are shown; otherwise it falls back to stacked icon-only. + const horizontal = variant === 'horizontal' && labeled; - return renderTouchable({ - key: route.key, - route, - borderless: true, - centered: true, - rippleColor: 'transparent', - onPress, - onLongPress, - onPressIn: () => setPressed(true), - onPressOut: () => setPressed(false), - onHoverIn: () => setHovered(true), - onHoverOut: () => setHovered(false), - onFocus: () => setKeyboardFocused(true), - onBlur: () => setKeyboardFocused(false), - testID: itemTestID, - accessibilityLabel: getAccessibilityLabel({ route }), - accessibilityRole: Platform.OS === 'ios' ? 'button' : 'tab', - accessibilityState: { selected: focused }, - style: [styles.item, styles.v3Item], - children: ( - - - {focused && ( - + + {focused && ( + + )} + + + + + {renderIcon ? ( + renderIcon({ route, focused: true, color: activeTintColor }) + ) : ( + )} - - + + {renderIcon ? ( + renderIcon({ route, focused: false, color: inactiveTintColor }) + ) : ( + - + )} + + + {typeof badge === 'boolean' ? ( + + ) : ( + + {badge} + + )} + + + {labeled ? ( + - {renderIcon ? ( - renderIcon({ route, focused: true, color: activeTintColor }) + {renderLabel ? ( + renderLabel({ route, focused: true, color: activeLabelColor }) ) : ( - + + {getLabelText({ route })} + )} - {renderIcon ? ( - renderIcon({ route, focused: false, color: inactiveTintColor }) + {renderLabel ? ( + renderLabel({ + route, + focused: false, + color: inactiveLabelColor, + }) ) : ( - + + {getLabelText({ route })} + )} + + ) : null} + + ); + + const horizontalContent = ( + + + {focused && ( + + )} + + + {renderIcon ? ( + renderIcon({ + route, + focused, + color: focused ? activeTintColor : inactiveTintColor, + }) + ) : ( + + )} {typeof badge === 'boolean' ? ( @@ -444,48 +544,53 @@ const NavigationBarItem = ({ )} - - {labeled ? ( - - - {renderLabel ? ( - renderLabel({ route, focused: true, color: activeLabelColor }) - ) : ( - - {getLabelText({ route })} - - )} - - - {renderLabel ? ( - renderLabel({ - route, - focused: false, - color: inactiveLabelColor, - }) - ) : ( - - {getLabelText({ route })} - - )} - - - ) : null} + + {renderLabel ? ( + renderLabel({ + route, + focused, + color: focused ? activeLabelColor : inactiveLabelColor, + }) + ) : ( + + {getLabelText({ route })} + + )} - ), + + ); + + return renderTouchable({ + key: route.key, + route, + borderless: true, + centered: true, + rippleColor: 'transparent', + onPress, + onLongPress, + onPressIn: () => setPressed(true), + onPressOut: () => setPressed(false), + onHoverIn: () => setHovered(true), + onHoverOut: () => setHovered(false), + onFocus: () => setKeyboardFocused(true), + onBlur: () => setKeyboardFocused(false), + testID: itemTestID, + accessibilityLabel: getAccessibilityLabel({ route }), + accessibilityRole: Platform.OS === 'ios' ? 'button' : 'tab', + accessibilityState: { selected: focused }, + style: [styles.item, styles.v3Item], + children: horizontal ? horizontalContent : stackedContent, }); }; @@ -575,6 +680,7 @@ const NavigationBar = ({ style, activeIndicatorStyle, labeled = true, + variant = 'stacked', animationEasing, onTabPress, onTabLongPress, @@ -791,6 +897,7 @@ const NavigationBar = ({ focused={focused} active={tabsAnims[index]} labeled={labeled} + variant={variant} activeTintColor={activeTintColor} inactiveTintColor={inactiveTintColor} activeColor={activeColor} @@ -917,4 +1024,28 @@ const styles = StyleSheet.create({ height: INDICATOR_HEIGHT, borderRadius: INDICATOR_BORDER_RADIUS, }, + horizontalContainer: { + height: BAR_HEIGHT, + justifyContent: 'center', + alignItems: 'center', + }, + horizontalItem: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + height: INDICATOR_HEIGHT, + paddingHorizontal: 16, + }, + horizontalIndicator: { + borderRadius: INDICATOR_BORDER_RADIUS, + }, + horizontalLabel: { + marginLeft: ICON_LABEL_GAP, + textAlign: 'center', + ...(Platform.OS === 'web' + ? { + whiteSpace: 'nowrap', + } + : null), + }, }); diff --git a/src/components/__tests__/BottomNavigation.test.tsx b/src/components/__tests__/BottomNavigation.test.tsx index 3cfd4ba5fe..0a38772136 100644 --- a/src/components/__tests__/BottomNavigation.test.tsx +++ b/src/components/__tests__/BottomNavigation.test.tsx @@ -527,6 +527,33 @@ it('does not render legacy ripple overlay when shifting is disabled', async () = ).not.toBeOnTheScreen(); }); +it('renders the horizontal (flexible) variant', () => { + const tree = render( + + ).toJSON(); + + expect(tree).toMatchSnapshot(); +}); + +it('falls back to icon-only when horizontal is combined with labeled=false', () => { + const { queryByText } = render( + + ); + + // `horizontal` is a no-op without labels, so no label text is rendered. + expect(queryByText('Route: 0')).toBeNull(); + expect(queryByText('Route: 1')).toBeNull(); +}); + it('renders MD3 state layers on hover, focus and press', () => { const navigationState = { index: 0, diff --git a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap index 3de73ae3c5..b3f448537d 100644 --- a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap @@ -13938,3 +13938,668 @@ exports[`renders shifting bottom navigation 1`] = ` `; + +exports[`renders the horizontal (flexible) variant 1`] = ` + + + + + + + + + + + + magnify + + + + + + + Route: 0 + + + + + + + + + + + camera + + + + + + + Route: 1 + + + + + + + + + + + inbox + + + + + + + Route: 2 + + + + + + + + +`; From 39b2f82b3f9eea4e6667698d47fb8a2917c15797 Mon Sep 17 00:00:00 2001 From: Bartek Dybowski Date: Wed, 17 Jun 2026 11:47:43 +0200 Subject: [PATCH 07/18] docs(navigation-bar): add example and update JSDoc Add a NavigationBarExample with a responsive stacked/horizontal toggle, label toggle and badges, registered in the example list. Rewrite the NavigationBar JSDoc with variant docs and a migration note from the deprecated BottomNavigation.Bar. Re #4975 --- example/src/ExampleList.tsx | 2 + example/src/Examples/NavigationBarExample.tsx | 108 ++++++++++++++++++ .../NavigationBar/NavigationBar.tsx | 33 +++--- 3 files changed, 129 insertions(+), 14 deletions(-) create mode 100644 example/src/Examples/NavigationBarExample.tsx diff --git a/example/src/ExampleList.tsx b/example/src/ExampleList.tsx index 53132e299f..5c8aba19da 100644 --- a/example/src/ExampleList.tsx +++ b/example/src/ExampleList.tsx @@ -27,6 +27,7 @@ import ListAccordionExampleGroup from './Examples/ListAccordionGroupExample'; import ListItemExample from './Examples/ListItemExample'; import ListSectionExample from './Examples/ListSectionExample'; import MenuExample from './Examples/MenuExample'; +import NavigationBarExample from './Examples/NavigationBarExample'; import ProgressBarExample from './Examples/ProgressBarExample'; import RadioButtonExample from './Examples/RadioButtonExample'; import RadioButtonGroupExample from './Examples/RadioButtonGroupExample'; @@ -72,6 +73,7 @@ export const mainExamples = { ListSection: ListSectionExample, ListItem: ListItemExample, Menu: MenuExample, + NavigationBar: NavigationBarExample, Progressbar: ProgressBarExample, Radio: RadioButtonExample, RadioGroup: RadioButtonGroupExample, diff --git a/example/src/Examples/NavigationBarExample.tsx b/example/src/Examples/NavigationBarExample.tsx new file mode 100644 index 0000000000..070bbc28c2 --- /dev/null +++ b/example/src/Examples/NavigationBarExample.tsx @@ -0,0 +1,108 @@ +import * as React from 'react'; +import { StyleSheet, View, useWindowDimensions } from 'react-native'; + +import { + NavigationBar, + SegmentedButtons, + Switch, + Text, +} from 'react-native-paper'; + +type VariantMode = 'auto' | 'stacked' | 'horizontal'; + +// The flexible navigation bar switches to a horizontal item arrangement in +// medium-width windows. M3 recommends ~600dp as the breakpoint, but the +// component leaves the decision to the consumer — here it is driven by +// `useWindowDimensions`. +const MEDIUM_WINDOW_WIDTH = 600; + +const routes = [ + { key: 'album', title: 'Album', focusedIcon: 'image-album', badge: 3 }, + { key: 'library', title: 'Library', focusedIcon: 'bookshelf' }, + { + key: 'favorites', + title: 'Favorites', + focusedIcon: 'heart', + unfocusedIcon: 'heart-outline', + }, + { + key: 'settings', + title: 'Settings', + focusedIcon: 'cog', + unfocusedIcon: 'cog-outline', + }, +]; + +const NavigationBarExample = () => { + const [index, setIndex] = React.useState(0); + const [labeled, setLabeled] = React.useState(true); + const [variantMode, setVariantMode] = React.useState('auto'); + + const { width } = useWindowDimensions(); + const autoVariant = width >= MEDIUM_WINDOW_WIDTH ? 'horizontal' : 'stacked'; + const variant = variantMode === 'auto' ? autoVariant : variantMode; + + return ( + + + {routes[index].title} + + + + Show labels + + + + setVariantMode(value as VariantMode)} + buttons={[ + { value: 'auto', label: `Auto (${autoVariant})` }, + { value: 'stacked', label: 'Stacked' }, + { value: 'horizontal', label: 'Horizontal' }, + ]} + /> + + + + { + const nextIndex = routes.findIndex((r) => r.key === route.key); + if (nextIndex !== -1) { + setIndex(nextIndex); + } + }} + /> + + ); +}; + +NavigationBarExample.title = 'Navigation Bar (flexible)'; + +export default NavigationBarExample; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + content: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 16, + }, + controls: { + marginTop: 32, + width: '100%', + maxWidth: 480, + gap: 24, + }, + row: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, +}); diff --git a/src/components/NavigationBar/NavigationBar.tsx b/src/components/NavigationBar/NavigationBar.tsx index 724ef62950..42dbbc15cc 100644 --- a/src/components/NavigationBar/NavigationBar.tsx +++ b/src/components/NavigationBar/NavigationBar.tsx @@ -130,7 +130,7 @@ export type Props = { * } * ``` * - * `BottomNavigation.Bar` is a controlled component, which means the `index` needs to be updated via the `onTabPress` callback. + * `NavigationBar` is a controlled component, which means the `index` needs to be updated via the `onTabPress` callback. */ navigationState: NavigationState; /** @@ -595,16 +595,24 @@ const NavigationBarItem = ({ }; /** - * A navigation bar which can easily be integrated with [React Navigation's Bottom Tabs Navigator](https://reactnavigation.org/docs/bottom-tab-navigator/). + * The Material Design 3 flexible navigation bar. It can easily be integrated + * with [React Navigation's Bottom Tabs Navigator](https://reactnavigation.org/docs/bottom-tab-navigator/). + * + * The flexible navigation bar replaces the original (now deprecated) navigation + * bar exposed as `BottomNavigation.Bar`. Set the `variant` prop to `'horizontal'` + * to lay items out horizontally (icon beside label) in medium-width windows. + * + * Migrating from `BottomNavigation.Bar`: it is deprecated in favor of + * `NavigationBar`. The Material Design 2 `shifting` prop has been removed (it + * has no MD3 equivalent), tab interactions now show MD3 state layers instead of + * suppressing feedback, and the bar height follows the 64dp spec. * * ## Usage * ### without React Navigation * ```js - * import React from 'react'; - * import { useState } from 'react'; + * import * as React from 'react'; * import { View } from 'react-native'; - * import { BottomNavigation, Text, Provider } from 'react-native-paper'; - * import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons'; + * import { NavigationBar, Text, Provider } from 'react-native-paper'; * * function HomeScreen() { * return ( @@ -623,13 +631,13 @@ const NavigationBarItem = ({ * } * * export default function MyComponent() { - * const [index, setIndex] = useState(0); + * const [index, setIndex] = React.useState(0); * * const routes = [ - * { key: 'home', title: 'Home', icon: 'home' }, - * { key: 'settings', title: 'Settings', icon: 'cog' }, + * { key: 'home', title: 'Home', focusedIcon: 'home' }, + * { key: 'settings', title: 'Settings', focusedIcon: 'cog' }, * ]; - + * * const renderScene = ({ route }) => { * switch (route.key) { * case 'home': @@ -644,7 +652,7 @@ const NavigationBarItem = ({ * return ( * * {renderScene({ route: routes[index] })} - * { * const newIndex = routes.findIndex((r) => r.key === route.key); @@ -652,9 +660,6 @@ const NavigationBarItem = ({ * setIndex(newIndex); * } * }} - * renderIcon={({ route, color }) => ( - * - * )} * getLabelText={({ route }) => route.title} * /> * From 98346d30ad8f28d4d62238697abb513d80e62a83 Mon Sep 17 00:00:00 2001 From: Bartek Dybowski Date: Wed, 17 Jun 2026 13:28:22 +0200 Subject: [PATCH 08/18] fix(navigation-bar): restore labels and active indicator on tab change Stacked labels were collapsed to zero width by `alignItems: center` on the item container; remove it (the icon centers via alignSelf). The active indicator was mounted only while focused and driven by a native value, so it never appeared on a newly-selected tab; always mount it and drive opacity from `active` so it cross-fades between tabs. Re #4975 --- .../NavigationBar/NavigationBar.tsx | 78 +-- .../__tests__/BottomNavigation.test.tsx | 19 + .../BottomNavigation.test.tsx.snap | 635 ++++++++++++++++-- 3 files changed, 643 insertions(+), 89 deletions(-) diff --git a/src/components/NavigationBar/NavigationBar.tsx b/src/components/NavigationBar/NavigationBar.tsx index 42dbbc15cc..791588b96b 100644 --- a/src/components/NavigationBar/NavigationBar.tsx +++ b/src/components/NavigationBar/NavigationBar.tsx @@ -307,13 +307,13 @@ const NavigationBarItem = ({ const activeOpacity = focused ? 1 : 0; const inactiveOpacity = focused ? 0 : 1; - // Scale horizontally the active-indicator pill. - const outlineScale = focused - ? active.interpolate({ - inputRange: [0, 1], - outputRange: [0.5, 1], - }) - : 0; + // Scale horizontally the active-indicator pill. The indicator is always + // mounted and its visibility is driven by `active` opacity, so it cross-fades + // between tabs instead of remounting (which breaks native-driven animations). + const outlineScale = active.interpolate({ + inputRange: [0, 1], + outputRange: [0.5, 1], + }); const badge = getBadge({ route }); @@ -366,18 +366,17 @@ const NavigationBarItem = ({ style={labeled ? styles.v3TouchableContainer : styles.v3NoLabelContainer} > - {focused && ( - - )} + ({ const horizontalContent = ( - {focused && ( - - )} + { + const { getAllByText } = render( + + ); + + // Each tab renders an active and inactive label layer, so both match. + expect(getAllByText('Alpha').length).toBeGreaterThan(0); + expect(getAllByText('Beta').length).toBeGreaterThan(0); +}); + it('renders the horizontal (flexible) variant', () => { const tree = render( + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + Date: Wed, 17 Jun 2026 13:43:37 +0200 Subject: [PATCH 09/18] test(navigation-bar): drop redundant shifting test variants `shifting` is now a no-op, so the "shifting"/"non-shifting" test pairs were duplicates. Merge them, remove the dead `shifting` props, rename to neutral titles, and prune obsolete snapshots. Keep the deprecation test and the scene-animation coverage. Re #4975 --- .../__tests__/BottomNavigation.test.tsx | 89 +- .../BottomNavigation.test.tsx.snap | 15144 ---------------- 2 files changed, 7 insertions(+), 15226 deletions(-) delete mode 100644 src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap diff --git a/src/components/__tests__/BottomNavigation.test.tsx b/src/components/__tests__/BottomNavigation.test.tsx index c2c4ad7249..e009366527 100644 --- a/src/components/__tests__/BottomNavigation.test.tsx +++ b/src/components/__tests__/BottomNavigation.test.tsx @@ -49,11 +49,10 @@ const layoutNavigationBar = async () => { }); }; -it('renders shifting bottom navigation', async () => { +it('renders bottom navigation', async () => { const tree = ( await render( { const tree = ( await render( { await render( { const onIndexChange = jest.fn(); await render( { await render( { await render( { ); }); -it('renders non-shifting bottom navigation', async () => { +it('renders bottom navigation with three tabs', async () => { const tree = ( await render( { +it('renders custom icon and label', async () => { const tree = ( await render( { expect(tree).toMatchSnapshot(); }); -it('renders custom icon and label in non-shifting bottom navigation', async () => { +it('renders with custom active and inactive colors', async () => { const tree = ( await render( ( - - )} - renderLabel={({ route, color }) => ( - - {route.title} - - )} - /> - ) - ).toJSON(); - - expect(tree).toMatchSnapshot(); -}); - -it('renders custom icon and label with custom colors in shifting bottom navigation', async () => { - const tree = ( - await render( - - ) - ).toJSON(); - - expect(tree).toMatchSnapshot(); -}); - -it('renders custom icon and label with custom colors in non-shifting bottom navigation', async () => { - const tree = ( - await render( - { - const tree = ( - await render( - - ) - ).toJSON(); - - expect(tree).toMatchSnapshot(); -}); - -it('hides labels in non-shifting bottom navigation', async () => { +it('hides labels when labeled is false', async () => { const tree = ( await render( { const labelMaxFontSizeMultiplier = 2; await render( { it('renders custom background color passed to barStyle property', async () => { await render( { it('renders a single tab', async () => { await render( { onIndexChange={jest.fn()} renderScene={renderScene} getLazy={({ route }) => route.key === 'key-2'} - shifting={false} testID="bottom-navigation" compact /> @@ -482,7 +410,6 @@ it('does not apply maxTabBarWidth styling if compact prop is falsy', async () => onIndexChange={jest.fn()} renderScene={renderScene} getLazy={({ route }) => route.key === 'key-2'} - shifting={false} testID="bottom-navigation" compact={false} /> @@ -495,7 +422,7 @@ it('does not apply maxTabBarWidth styling if compact prop is falsy', async () => }); }); -it('renders bar content when shifting is enabled', async () => { +it('renders bar content', async () => { await render( { renderScene={renderScene} getLazy={({ route }) => route.key === 'key-2'} testID="bottom-navigation" - shifting /> ); expect(screen.getByTestId('bottom-navigation-bar-content')).toBeOnTheScreen(); }); -it('does not render legacy ripple overlay when shifting is disabled', async () => { +it('does not render the legacy ripple overlay', async () => { await render( route.key === 'key-2'} testID="bottom-navigation" - shifting={false} /> ); diff --git a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap deleted file mode 100644 index 2f5962c331..0000000000 --- a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap +++ /dev/null @@ -1,15144 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`allows customizing Route's type via generics 1`] = ` - - - - - - First - - - - - - - - - - - - - - - - - - - - - - - - - First - - - - - First - - - - - - - - - - - - - - - - - - - - - - Second - - - - - Second - - - - - - - - - - -`; - -exports[`hides labels in non-shifting bottom navigation 1`] = ` - - - - - - Route: 0 - - - - - - - - - - - - - - - - - - magnify - - - - - magnify - - - - - - - - - - - - - - - - - - camera - - - - - camera - - - - - - - - - - - - - - - - - - inbox - - - - - inbox - - - - - - - - - - - - - -`; - -exports[`hides labels in shifting bottom navigation 1`] = ` - - - - - - Route: 0 - - - - - - - - - - - - - - - - - - magnify - - - - - magnify - - - - - - - - - - - - - - - - - - camera - - - - - camera - - - - - - - - - - - - - - - - - - inbox - - - - - inbox - - - - - - - - - - - - - -`; - -exports[`renders bottom navigation with getLazy 1`] = ` - - - - - - Route: 0 - - - - - - - Route: 1 - - - - - - - Route: 3 - - - - - - - Route: 4 - - - - - - - - - - - - - - - - - - magnify - - - - - magnify - - - - - - - - - - Route: 0 - - - - - Route: 0 - - - - - - - - - - - - - - - camera - - - - - camera - - - - - - - - - - Route: 1 - - - - - Route: 1 - - - - - - - - - - - - - - - inbox - - - - - inbox - - - - - - - - - - Route: 2 - - - - - Route: 2 - - - - - - - - - - - - - - - heart - - - - - heart - - - - - - - - - - Route: 3 - - - - - Route: 3 - - - - - - - - - - - - - - - shopping-music - - - - - shopping-music - - - - - - - - - - Route: 4 - - - - - Route: 4 - - - - - - - - - - -`; - -exports[`renders bottom navigation with scene animation 1`] = ` - - - - - - Route: 0 - - - - - - - - - - - - - - - - - - magnify - - - - - magnify - - - - - - - - - - Route: 0 - - - - - Route: 0 - - - - - - - - - - - - - - - camera - - - - - camera - - - - - - - - - - Route: 1 - - - - - Route: 1 - - - - - - - - - - - - - - - inbox - - - - - inbox - - - - - - - - - - Route: 2 - - - - - Route: 2 - - - - - - - - - - - - - - - heart - - - - - heart - - - - - - - - - - Route: 3 - - - - - Route: 3 - - - - - - - - - - - - - - - shopping-music - - - - - shopping-music - - - - - - - - - - Route: 4 - - - - - Route: 4 - - - - - - - - - - -`; - -exports[`renders custom icon and label in non-shifting bottom navigation 1`] = ` - - - - - - Route: 0 - - - - - - - - - - - - - - - - - - - - - - - - - Route: 0 - - - - - Route: 0 - - - - - - - - - - - - - - - - - - - - - - Route: 1 - - - - - Route: 1 - - - - - - - - - - - - - - - - - - - - - - Route: 2 - - - - - Route: 2 - - - - - - - - - - -`; - -exports[`renders custom icon and label in shifting bottom navigation 1`] = ` - - - - - - Route: 0 - - - - - - - - - - - - - - - - - - - - - - - - - Route: 0 - - - - - Route: 0 - - - - - - - - - - - - - - - - - - - - - - Route: 1 - - - - - Route: 1 - - - - - - - - - - - - - - - - - - - - - - Route: 2 - - - - - Route: 2 - - - - - - - - - - - - - - - - - - - - - - Route: 3 - - - - - Route: 3 - - - - - - - - - - - - - - - - - - - - - - Route: 4 - - - - - Route: 4 - - - - - - - - - - -`; - -exports[`renders custom icon and label with custom colors in non-shifting bottom navigation 1`] = ` - - - - - - Route: 0 - - - - - - - - - - - - - - - - - - magnify - - - - - magnify - - - - - - - - - - Route: 0 - - - - - Route: 0 - - - - - - - - - - - - - - - camera - - - - - camera - - - - - - - - - - Route: 1 - - - - - Route: 1 - - - - - - - - - - - - - - - inbox - - - - - inbox - - - - - - - - - - Route: 2 - - - - - Route: 2 - - - - - - - - - - -`; - -exports[`renders custom icon and label with custom colors in shifting bottom navigation 1`] = ` - - - - - - Route: 0 - - - - - - - - - - - - - - - - - - magnify - - - - - magnify - - - - - - - - - - Route: 0 - - - - - Route: 0 - - - - - - - - - - - - - - - camera - - - - - camera - - - - - - - - - - Route: 1 - - - - - Route: 1 - - - - - - - - - - - - - - - inbox - - - - - inbox - - - - - - - - - - Route: 2 - - - - - Route: 2 - - - - - - - - - - -`; - -exports[`renders non-shifting bottom navigation 1`] = ` - - - - - - Route: 0 - - - - - - - - - - - - - - - - - - magnify - - - - - magnify - - - - - - - - - - Route: 0 - - - - - Route: 0 - - - - - - - - - - - - - - - camera - - - - - camera - - - - - - - - - - Route: 1 - - - - - Route: 1 - - - - - - - - - - - - - - - inbox - - - - - inbox - - - - - - - - - - Route: 2 - - - - - Route: 2 - - - - - - - - - - -`; - -exports[`renders shifting bottom navigation 1`] = ` - - - - - - Route: 0 - - - - - - - - - - - - - - - - - - magnify - - - - - magnify - - - - - - - - - - Route: 0 - - - - - Route: 0 - - - - - - - - - - - - - - - camera - - - - - camera - - - - - - - - - - Route: 1 - - - - - Route: 1 - - - - - - - - - - - - - - - inbox - - - - - inbox - - - - - - - - - - Route: 2 - - - - - Route: 2 - - - - - - - - - - - - - - - heart - - - - - heart - - - - - - - - - - Route: 3 - - - - - Route: 3 - - - - - - - - - - - - - - - shopping-music - - - - - shopping-music - - - - - - - - - - Route: 4 - - - - - Route: 4 - - - - - - - - - - -`; - -exports[`renders the horizontal (flexible) variant 1`] = ` - - - - - - - - - - - - magnify - - - - - - - Route: 0 - - - - - - - - - - - - camera - - - - - - - Route: 1 - - - - - - - - - - - - inbox - - - - - - - Route: 2 - - - - - - - - -`; From 57d75066feb8656295e781aca56a8e9812fa8250 Mon Sep 17 00:00:00 2001 From: Bartek Dybowski Date: Thu, 18 Jun 2026 07:25:03 +0200 Subject: [PATCH 10/18] test(navigation-bar): add characterization tests before refactor Add a -active-indicator testID and tests pinning the per-state label colors, active indicator color, and badge rendering, so the upcoming item refactor is guarded by explicit behavior rather than snapshots alone. Re #4975 --- .../NavigationBar/NavigationBar.tsx | 2 + .../__tests__/BottomNavigation.test.tsx | 57 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/components/NavigationBar/NavigationBar.tsx b/src/components/NavigationBar/NavigationBar.tsx index 791588b96b..d4d1695b0f 100644 --- a/src/components/NavigationBar/NavigationBar.tsx +++ b/src/components/NavigationBar/NavigationBar.tsx @@ -367,6 +367,7 @@ const NavigationBarItem = ({ > ({ { expect(layerOpacity()).toBeUndefined(); }); +it('colors the focused tab label with secondary and others with onSurfaceVariant', () => { + const navigationState = { + index: 0, + routes: [ + { key: 'a', title: 'Alpha', focusedIcon: 'magnify' }, + { key: 'b', title: 'Beta', focusedIcon: 'camera' }, + ], + }; + + const { getAllByText } = render( + + ); + + const colorsOf = (text: string) => + getAllByText(text).map( + (node) => StyleSheet.flatten(node.props.style).color + ); + + expect(colorsOf('Alpha')).toContain(getTheme().colors.secondary); + expect(colorsOf('Beta')).toContain(getTheme().colors.onSurfaceVariant); +}); + +it('renders the active indicator with the secondaryContainer color', () => { + const navigationState = { + index: 0, + routes: [ + { key: 'a', title: 'Alpha', focusedIcon: 'magnify', testID: 'tab-a' }, + { key: 'b', title: 'Beta', focusedIcon: 'camera', testID: 'tab-b' }, + ], + }; + + const { getByTestId } = render( + + ); + + expect( + StyleSheet.flatten(getByTestId('tab-a-active-indicator').props.style) + .backgroundColor + ).toBe(getTheme().colors.secondaryContainer); +}); + +it('renders a badge for routes that define one', () => { + const navigationState = { + index: 0, + routes: [ + { key: 'a', title: 'Alpha', focusedIcon: 'magnify', badge: 3 }, + { key: 'b', title: 'Beta', focusedIcon: 'camera' }, + ], + }; + + const { getByText } = render( + + ); + + expect(getByText('3')).toBeTruthy(); +}); + describe('getActiveTintColor', () => { it.each` activeColor | expected From 6937a1779474767ce30f52cab016b4361f3ca2c4 Mon Sep 17 00:00:00 2001 From: Bartek Dybowski Date: Thu, 18 Jun 2026 07:37:27 +0200 Subject: [PATCH 11/18] refactor(navigation-bar): collapse cross-fade, dedupe item render The stacked active/inactive layers were toggled instantly (not animated), so render a single icon + label using the focused color, matching the horizontal layout. Extract shared icon/badge/label rendering across both layouts and merge the vestigial v3* duplicate styles. Behavior-preserving; characterization tests unchanged. Re #4975 --- .../NavigationBar/NavigationBar.tsx | 288 +- .../BottomNavigation.test.tsx.snap | 8738 +++++++++++++++++ 2 files changed, 8826 insertions(+), 200 deletions(-) create mode 100644 src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap diff --git a/src/components/NavigationBar/NavigationBar.tsx b/src/components/NavigationBar/NavigationBar.tsx index d4d1695b0f..f2bc59cf00 100644 --- a/src/components/NavigationBar/NavigationBar.tsx +++ b/src/components/NavigationBar/NavigationBar.tsx @@ -11,6 +11,7 @@ import type { ColorValue, EasingFunction, StyleProp, + TextStyle, ViewStyle, } from 'react-native'; @@ -20,6 +21,7 @@ import { BAR_HEIGHT, colorRoles, ICON_LABEL_GAP, + ICON_SIZE, INDICATOR_BORDER_RADIUS, INDICATOR_HEIGHT, INDICATOR_WIDTH, @@ -301,36 +303,23 @@ const NavigationBarItem = ({ const [keyboardFocused, setKeyboardFocused] = React.useState(false); const [pressed, setPressed] = React.useState(false); - // We render the active icon and label on top of the inactive ones and toggle - // their opacity on change, so the active/inactive colors swap without - // animating color (which the native driver can't do). - const activeOpacity = focused ? 1 : 0; - const inactiveOpacity = focused ? 0 : 1; - - // Scale horizontally the active-indicator pill. The indicator is always - // mounted and its visibility is driven by `active` opacity, so it cross-fades - // between tabs instead of remounting (which breaks native-driven animations). + // The active indicator is always mounted and cross-fades via `active` opacity + // (remounting it on focus change breaks native-driven animations). In the + // stacked layout it also scales horizontally from 0.5 → 1 on selection. const outlineScale = active.interpolate({ inputRange: [0, 1], outputRange: [0.5, 1], }); - const badge = getBadge({ route }); - - const activeLabelColor = getLabelColor({ - tintColor: activeTintColor, - hasColor: Boolean(activeColor), - focused, - theme, - }); - - const inactiveLabelColor = getLabelColor({ - tintColor: inactiveTintColor, - hasColor: Boolean(inactiveColor), + const iconColor = focused ? activeTintColor : inactiveTintColor; + const labelColor = getLabelColor({ + tintColor: iconColor, + hasColor: Boolean(focused ? activeColor : inactiveColor), focused, theme, }); + const badge = getBadge({ route }); const badgeStyle = { top: typeof badge === 'boolean' ? 4 : 2, right: @@ -341,9 +330,8 @@ const NavigationBarItem = ({ const font = theme.fonts.labelMedium; - // MD3 state layer: visible on hover (8%) and focus/press (10%), shaped like - // the active-indicator pill. Active items use the on-secondary-container - // role, inactive items the on-surface-variant role. + // MD3 state layer: visible on hover (8%) and focus/press (10%). Active items + // use the on-secondary-container role, inactive the on-surface-variant role. const stateLayerRole = focused ? colorRoles.activeIcon : colorRoles.inactiveIcon; @@ -354,22 +342,72 @@ const NavigationBarItem = ({ : hovered ? getStateLayer(theme, stateLayerRole, 'hovered') : null; + const stateLayerColor = stateLayer + ? { backgroundColor: stateLayer.color, opacity: stateLayer.opacity } + : null; const itemTestID = getTestID({ route }); + const indicatorTestID = itemTestID + ? `${itemTestID}-active-indicator` + : undefined; + const stateLayerTestID = itemTestID ? `${itemTestID}-state-layer` : undefined; + // The horizontal arrangement places the label beside the icon and only // applies when labels are shown; otherwise it falls back to stacked icon-only. const horizontal = variant === 'horizontal' && labeled; + // Item pieces shared across both layouts. The active/inactive distinction is + // a plain color swap (no cross-fade), so a single icon and label suffice. + const icon = renderIcon ? ( + renderIcon({ route, focused, color: iconColor }) + ) : ( + + ); + + const tabBadge = ( + + {typeof badge === 'boolean' ? ( + + ) : ( + + {badge} + + )} + + ); + + const renderTabLabel = (labelStyle: StyleProp) => + renderLabel ? ( + renderLabel({ route, focused, color: labelColor }) + ) : ( + + {getLabelText({ route })} + + ); + const stackedContent = ( - + ({ /> - - {renderIcon ? ( - renderIcon({ route, focused: true, color: activeTintColor }) - ) : ( - - )} - - - {renderIcon ? ( - renderIcon({ route, focused: false, color: inactiveTintColor }) - ) : ( - - )} - - - {typeof badge === 'boolean' ? ( - - ) : ( - - {badge} - - )} - - + {icon} + {tabBadge} + {labeled ? ( - - - {renderLabel ? ( - renderLabel({ route, focused: true, color: activeLabelColor }) - ) : ( - - {getLabelText({ route })} - - )} - - - {renderLabel ? ( - renderLabel({ - route, - focused: false, - color: inactiveLabelColor, - }) - ) : ( - - {getLabelText({ route })} - - )} - - + + {renderTabLabel(styles.label)} + ) : null} ); @@ -484,7 +437,7 @@ const NavigationBarItem = ({ ({ ]} /> - {renderIcon ? ( - renderIcon({ - route, - focused, - color: focused ? activeTintColor : inactiveTintColor, - }) - ) : ( - - )} - - {typeof badge === 'boolean' ? ( - - ) : ( - - {badge} - - )} - + {icon} + {tabBadge} - {renderLabel ? ( - renderLabel({ - route, - focused, - color: focused ? activeLabelColor : inactiveLabelColor, - }) - ) : ( - - {getLabelText({ route })} - - )} + {renderTabLabel(styles.horizontalLabel)} ); @@ -588,7 +491,7 @@ const NavigationBarItem = ({ accessibilityLabel: getAccessibilityLabel({ route }), accessibilityRole: Platform.OS === 'ios' ? 'button' : 'tab', accessibilityState: { selected: focused }, - style: [styles.item, styles.v3Item], + style: styles.item, children: horizontal ? horizontalContent : stackedContent, }); }; @@ -951,41 +854,26 @@ const styles = StyleSheet.create({ }, item: { flex: 1, - // Top padding is 6 and bottom padding is 10 - // The extra 4dp bottom padding is offset by label's height - paddingVertical: 6, - }, - v3Item: { paddingVertical: 0, }, iconContainer: { - height: 24, - width: 24, - marginTop: 2, - marginHorizontal: 12, - alignSelf: 'center', - }, - v3IconContainer: { height: INDICATOR_HEIGHT, width: INDICATOR_HEIGHT, - marginBottom: ICON_LABEL_GAP, marginTop: 0, + marginBottom: ICON_LABEL_GAP, + marginHorizontal: 12, + alignSelf: 'center', justifyContent: 'center', }, iconWrapper: { ...StyleSheet.absoluteFill, - alignItems: 'center', - }, - v3IconWrapper: { top: 4, + alignItems: 'center', }, labelContainer: { height: 16, paddingBottom: 2, }, - labelWrapper: { - ...StyleSheet.absoluteFill, - }, // eslint-disable-next-line react-native/no-color-literals label: { fontSize: 12, @@ -1002,16 +890,16 @@ const styles = StyleSheet.create({ position: 'absolute', left: 0, }, - v3TouchableContainer: { + stackedContainer: { height: BAR_HEIGHT, justifyContent: 'center', }, - v3NoLabelContainer: { + noLabelContainer: { height: NO_LABEL_BAR_HEIGHT, justifyContent: 'center', alignItems: 'center', }, - outline: { + stackedIndicator: { width: INDICATOR_WIDTH, height: INDICATOR_HEIGHT, borderRadius: INDICATOR_BORDER_RADIUS, diff --git a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap new file mode 100644 index 0000000000..e2b4116595 --- /dev/null +++ b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap @@ -0,0 +1,8738 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`allows customizing Route's type via generics 1`] = ` + + + + + First + + + + + + + + + + + + + + + + + + + + + + First + + + + + + + + + + + + + + + + + + + Second + + + + + + + + + +`; + +exports[`hides labels when labeled is false 1`] = ` + + + + + Route: 0 + + + + + + + + + + + + + + + + + magnify + + + + + + + + + + + + + + + + + + camera + + + + + + + + + + + + + + + + + + inbox + + + + + + + + + + + + + +`; + +exports[`renders bottom navigation 1`] = ` + + + + + Route: 0 + + + + + + + + + + + + + + + + + magnify + + + + + + + + + Route: 0 + + + + + + + + + + + + + + camera + + + + + + + + + Route: 1 + + + + + + + + + + + + + + inbox + + + + + + + + + Route: 2 + + + + + + + + + + + + + + heart + + + + + + + + + Route: 3 + + + + + + + + + + + + + + shopping-music + + + + + + + + + Route: 4 + + + + + + + + + +`; + +exports[`renders bottom navigation with getLazy 1`] = ` + + + + + Route: 0 + + + + + Route: 1 + + + + + Route: 3 + + + + + Route: 4 + + + + + + + + + + + + + + + + + magnify + + + + + + + + + Route: 0 + + + + + + + + + + + + + + camera + + + + + + + + + Route: 1 + + + + + + + + + + + + + + inbox + + + + + + + + + Route: 2 + + + + + + + + + + + + + + heart + + + + + + + + + Route: 3 + + + + + + + + + + + + + + shopping-music + + + + + + + + + Route: 4 + + + + + + + + + +`; + +exports[`renders bottom navigation with scene animation 1`] = ` + + + + + Route: 0 + + + + + + + + + + + + + + + + + magnify + + + + + + + + + Route: 0 + + + + + + + + + + + + + + camera + + + + + + + + + Route: 1 + + + + + + + + + + + + + + inbox + + + + + + + + + Route: 2 + + + + + + + + + + + + + + heart + + + + + + + + + Route: 3 + + + + + + + + + + + + + + shopping-music + + + + + + + + + Route: 4 + + + + + + + + + +`; + +exports[`renders bottom navigation with three tabs 1`] = ` + + + + + Route: 0 + + + + + + + + + + + + + + + + + magnify + + + + + + + + + Route: 0 + + + + + + + + + + + + + + camera + + + + + + + + + Route: 1 + + + + + + + + + + + + + + inbox + + + + + + + + + Route: 2 + + + + + + + + + +`; + +exports[`renders custom icon and label 1`] = ` + + + + + Route: 0 + + + + + + + + + + + + + + + + + + + + + + Route: 0 + + + + + + + + + + + + + + + + + + + Route: 1 + + + + + + + + + + + + + + + + + + + Route: 2 + + + + + + + + + + + + + + + + + + + Route: 3 + + + + + + + + + + + + + + + + + + + Route: 4 + + + + + + + + + +`; + +exports[`renders the horizontal (flexible) variant 1`] = ` + + + + + + + + + + + + magnify + + + + + + + Route: 0 + + + + + + + + + + + + camera + + + + + + + Route: 1 + + + + + + + + + + + + inbox + + + + + + + Route: 2 + + + + + + + + +`; + +exports[`renders with custom active and inactive colors 1`] = ` + + + + + Route: 0 + + + + + + + + + + + + + + + + + magnify + + + + + + + + + Route: 0 + + + + + + + + + + + + + + camera + + + + + + + + + Route: 1 + + + + + + + + + + + + + + inbox + + + + + + + + + Route: 2 + + + + + + + + + +`; From b19e402d59ebec891d5c8d0e318ad513e9a7301f Mon Sep 17 00:00:00 2001 From: Bartek Dybowski Date: Thu, 18 Jun 2026 07:51:48 +0200 Subject: [PATCH 12/18] refactor(navigation-bar): unify color source on colorRoles Move the color resolvers from BottomNavigation/utils into the NavigationBar module (reversing the cross-module dependency) and express them via colorRoles. Use colors[colorRoles.activeIndicator] for the indicator background instead of a hardcoded role. Same resolved colors; no behavior change. Re #4975 --- src/components/NavigationBar/NavigationBar.tsx | 14 +++++++------- .../{BottomNavigation => NavigationBar}/utils.ts | 11 ++++------- src/components/__tests__/BottomNavigation.test.tsx | 10 +++++----- 3 files changed, 16 insertions(+), 19 deletions(-) rename src/components/{BottomNavigation => NavigationBar}/utils.ts (74%) diff --git a/src/components/NavigationBar/NavigationBar.tsx b/src/components/NavigationBar/NavigationBar.tsx index f2bc59cf00..724e73f1c3 100644 --- a/src/components/NavigationBar/NavigationBar.tsx +++ b/src/components/NavigationBar/NavigationBar.tsx @@ -29,6 +29,11 @@ import { MIN_TAB_WIDTH, NO_LABEL_BAR_HEIGHT, } from './tokens'; +import { + getActiveTintColor, + getInactiveTintColor, + getLabelColor, +} from './utils'; import { useInternalTheme } from '../../core/theming'; import { toRawSpring } from '../../theme/tokens/sys/motion'; import { getStateLayer } from '../../theme/utils/state'; @@ -38,11 +43,6 @@ import useAnimatedValueArray from '../../utils/useAnimatedValueArray'; import useIsKeyboardShown from '../../utils/useIsKeyboardShown'; import useLayout from '../../utils/useLayout'; import Badge from '../Badge'; -import { - getActiveTintColor, - getInactiveTintColor, - getLabelColor, -} from '../BottomNavigation/utils'; import Icon from '../Icon'; import type { IconSource } from '../Icon'; import Surface from '../Surface'; @@ -411,7 +411,7 @@ const NavigationBarItem = ({ { opacity: active, transform: [{ scaleX: outlineScale }], - backgroundColor: colors.secondaryContainer, + backgroundColor: colors[colorRoles.activeIndicator], }, activeIndicatorStyle, ]} @@ -442,7 +442,7 @@ const NavigationBarItem = ({ StyleSheet.absoluteFill, styles.horizontalIndicator, { - backgroundColor: colors.secondaryContainer, + backgroundColor: colors[colorRoles.activeIndicator], opacity: active, transform: [ { diff --git a/src/components/BottomNavigation/utils.ts b/src/components/NavigationBar/utils.ts similarity index 74% rename from src/components/BottomNavigation/utils.ts rename to src/components/NavigationBar/utils.ts index 60f0545d69..3e10c84f5e 100644 --- a/src/components/BottomNavigation/utils.ts +++ b/src/components/NavigationBar/utils.ts @@ -1,5 +1,6 @@ import type { ColorValue } from 'react-native'; +import { colorRoles } from './tokens'; import type { InternalTheme, Theme } from '../../types'; export const getActiveTintColor = ({ @@ -13,7 +14,7 @@ export const getActiveTintColor = ({ return activeColor; } - return (theme as Theme).colors.onSecondaryContainer; + return (theme as Theme).colors[colorRoles.activeIcon]; }; export const getInactiveTintColor = ({ @@ -27,7 +28,7 @@ export const getInactiveTintColor = ({ return inactiveColor; } - return (theme as Theme).colors.onSurfaceVariant; + return (theme as Theme).colors[colorRoles.inactiveIcon]; }; export const getLabelColor = ({ @@ -46,9 +47,5 @@ export const getLabelColor = ({ return tintColor; } - if (focused) { - // M3 active label color is `secondary` (changed from `onSurface`). - return colors.secondary; - } - return colors.onSurfaceVariant; + return colors[focused ? colorRoles.activeLabel : colorRoles.inactiveLabel]; }; diff --git a/src/components/__tests__/BottomNavigation.test.tsx b/src/components/__tests__/BottomNavigation.test.tsx index dc5b44c88f..46c2ad3451 100644 --- a/src/components/__tests__/BottomNavigation.test.tsx +++ b/src/components/__tests__/BottomNavigation.test.tsx @@ -8,13 +8,13 @@ import { render, screen } from '../../test-utils'; import { Palette } from '../../theme/tokens'; import BottomNavigation from '../BottomNavigation/BottomNavigation'; import BottomNavigationRouteScreen from '../BottomNavigation/BottomNavigationRouteScreen'; +import Icon from '../Icon'; +import NavigationBar from '../NavigationBar/NavigationBar'; import { getActiveTintColor, getInactiveTintColor, getLabelColor, -} from '../BottomNavigation/utils'; -import Icon from '../Icon'; -import NavigationBar from '../NavigationBar/NavigationBar'; +} from '../NavigationBar/utils'; const styles = StyleSheet.create({ backgroundColor: { @@ -151,8 +151,8 @@ it('calls onIndexChange', async () => { renderScene={renderScene} /> ); - // Both the active and inactive labels render per tab, so target the last - // match (the label inside the tab) to fire the tab press. + // The active scene also renders the route title, so target the last match + // (the tab label) to fire the tab press. // pressing same index as active navigation state does not call onIndexChange fireEvent(screen.getAllByText('Route: 0').at(-1)!, 'onPress'); expect(onIndexChange).not.toHaveBeenCalled(); From 83b1a8b9a870264b17ad76ed246f0ed8089a3575 Mon Sep 17 00:00:00 2001 From: Bartek Dybowski Date: Thu, 18 Jun 2026 08:15:18 +0200 Subject: [PATCH 13/18] docs(navigation-bar): register NavigationBar page, fix docs build Register NavigationBar in the docs component map and drop the deprecated BottomNavigationBar entry (now a re-export that react-docgen can't parse). Point the BottomNavigation JSDoc link at NavigationBar. Re #4975 --- .../NavigationBar/NavigationBar.mdx | 222 ++++++++++++++++++ .../docs/components/NavigationBar/_meta.json | 1 + docs/6.x/docs/components/_meta.json | 7 + docs/component-docs.config.ts | 4 +- .../BottomNavigation/BottomNavigation.tsx | 2 +- 5 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 docs/6.x/docs/components/NavigationBar/NavigationBar.mdx create mode 100644 docs/6.x/docs/components/NavigationBar/_meta.json diff --git a/docs/6.x/docs/components/NavigationBar/NavigationBar.mdx b/docs/6.x/docs/components/NavigationBar/NavigationBar.mdx new file mode 100644 index 0000000000..985d33b890 --- /dev/null +++ b/docs/6.x/docs/components/NavigationBar/NavigationBar.mdx @@ -0,0 +1,222 @@ +--- +title: NavigationBar +--- + +import PropTable from '@docs/components/PropTable.tsx'; +import ExtendsLink from '@docs/components/ExtendsLink.tsx'; +import ThemeColorsTable from '@docs/components/ThemeColorsTable.tsx'; +import ScreenshotTabs from '@docs/components/ScreenshotTabs.tsx'; +import ExtendedExample from '@docs/components/ExtendedExample.tsx'; + +The Material Design 3 flexible navigation bar. It can easily be integrated with [React Navigation's Bottom Tabs Navigator](https://reactnavigation.org/docs/bottom-tab-navigator/). + +The flexible navigation bar replaces the original (now deprecated) navigation bar exposed as `BottomNavigation.Bar`. Set the `variant` prop to `'horizontal'` to lay items out horizontally (icon beside label) in medium-width windows. + +## Usage +```js +import * as React from 'react'; +import { NavigationBar } from 'react-native-paper'; + +const MyComponent = () => { + const [index, setIndex] = React.useState(0); + const [routes] = React.useState([ + { key: 'music', title: 'Favorites', focusedIcon: 'heart' }, + { key: 'albums', title: 'Albums', focusedIcon: 'album' }, + { key: 'recents', title: 'Recents', focusedIcon: 'history' }, + ]); + + return ( + { + const newIndex = routes.findIndex((r) => r.key === route.key); + setIndex(newIndex); + }} + /> + ); +}; + +export default MyComponent; +``` + + + ## Props + + + + +
+ +### labeled + +
+ + + +
+ +### variant + +
+ + + +
+ +### compact + +
+ + + +
+ +### navigationState (required) + +
+ + + +
+ +### onTabPress (required) + +
+ + + +
+ +### onTabLongPress + +
+ + + +
+ +### renderIcon + +
+ + + +
+ +### renderLabel + +
+ + + +
+ +### renderTouchable + +
+ + + +
+ +### getAccessibilityLabel + +
+ + + +
+ +### getBadge + +
+ + + +
+ +### getLabelText + +
+ + + +
+ +### getTestID + +
+ + + +
+ +### activeColor + +
+ + + +
+ +### inactiveColor + +
+ + + +
+ +### animationEasing + +
+ + + +
+ +### keyboardHidesNavigationBar + +
+ + + +
+ +### safeAreaInsets + +
+ + + +
+ +### labelMaxFontSizeMultiplier + +
+ + + +
+ +### style + +
+ + + +
+ +### theme + +
+ + + + + + + + + + \ No newline at end of file diff --git a/docs/6.x/docs/components/NavigationBar/_meta.json b/docs/6.x/docs/components/NavigationBar/_meta.json new file mode 100644 index 0000000000..3f9533bfe1 --- /dev/null +++ b/docs/6.x/docs/components/NavigationBar/_meta.json @@ -0,0 +1 @@ +["NavigationBar"] \ No newline at end of file diff --git a/docs/6.x/docs/components/_meta.json b/docs/6.x/docs/components/_meta.json index 6f793d41a3..d30d12c6ee 100644 --- a/docs/6.x/docs/components/_meta.json +++ b/docs/6.x/docs/components/_meta.json @@ -103,6 +103,13 @@ "collapsed": false }, "Modal", + { + "type": "dir", + "name": "NavigationBar", + "label": "NavigationBar", + "collapsible": true, + "collapsed": false + }, { "type": "dir", "name": "Portal", diff --git a/docs/component-docs.config.ts b/docs/component-docs.config.ts index bad25d4eec..e588213f1b 100644 --- a/docs/component-docs.config.ts +++ b/docs/component-docs.config.ts @@ -40,7 +40,6 @@ const pages = { Banner: 'Banner', BottomNavigation: { BottomNavigation: 'BottomNavigation/BottomNavigation', - BottomNavigationBar: 'BottomNavigation/BottomNavigationBar', }, Button: { Button: 'Button/Button', @@ -103,6 +102,9 @@ const pages = { MenuItem: 'Menu/MenuItem', }, Modal: 'Modal', + NavigationBar: { + NavigationBar: 'NavigationBar/NavigationBar', + }, Portal: { Portal: 'Portal/Portal', PortalHost: 'Portal/PortalHost', diff --git a/src/components/BottomNavigation/BottomNavigation.tsx b/src/components/BottomNavigation/BottomNavigation.tsx index 1112a92e3d..6097628bb6 100644 --- a/src/components/BottomNavigation/BottomNavigation.tsx +++ b/src/components/BottomNavigation/BottomNavigation.tsx @@ -260,7 +260,7 @@ const SceneComponent = React.memo(({ component, ...rest }: any) => /** * BottomNavigation provides quick navigation between top-level views of an app with a bottom navigation bar. - * It is primarily designed for use on mobile. If you want to use the navigation bar only see [`BottomNavigation.Bar`](BottomNavigationBar). + * It is primarily designed for use on mobile. If you want to use the navigation bar only see [`NavigationBar`](../NavigationBar). * * By default BottomNavigation uses primary color as a background, in dark theme with `adaptive` mode it will use surface colour instead. * See [Dark Theme](https://callstack.github.io/react-native-paper/docs/guides/theming#dark-theme) for more information. From 20576123f671a33f7926ada7798d302f2413963f Mon Sep 17 00:00:00 2001 From: Bartek Dybowski Date: Thu, 18 Jun 2026 15:57:18 +0200 Subject: [PATCH 14/18] refactor(navigation-bar): drop deprecations per review - remove deprecated BottomNavigation.Bar alias + BottomNavigationBar module - remove dead no-op shifting prop from BottomNavigation - BottomNavigation renders NavigationBar directly - migrate react-navigation example to top-level NavigationBar Addresses satya164: no deprecations, remove old code. --- .../Examples/BottomNavigationBarExample.tsx | 6 ++--- .../BottomNavigation/BottomNavigation.tsx | 23 ++++--------------- .../BottomNavigation/BottomNavigationBar.tsx | 8 ------- .../__tests__/BottomNavigation.test.tsx | 22 +----------------- 4 files changed, 8 insertions(+), 51 deletions(-) delete mode 100644 src/components/BottomNavigation/BottomNavigationBar.tsx diff --git a/example/src/Examples/BottomNavigationBarExample.tsx b/example/src/Examples/BottomNavigationBarExample.tsx index 9fbddf7f23..4b4d98d94d 100644 --- a/example/src/Examples/BottomNavigationBarExample.tsx +++ b/example/src/Examples/BottomNavigationBarExample.tsx @@ -11,7 +11,7 @@ import { SFSymbol, MaterialSymbol, } from '@react-navigation/native'; -import { Text, BottomNavigation } from 'react-native-paper'; +import { Text, NavigationBar } from 'react-native-paper'; function HomeScreen() { return ( @@ -34,7 +34,7 @@ const BottomNavigationBarExample = createBottomTabNavigator({ headerShown: false, }, tabBar: ({ navigation, state, descriptors }) => ( - { const event = navigation.emit({ @@ -119,7 +119,7 @@ const BottomNavigationBarExample = createBottomTabNavigator({ }); export default Object.assign(BottomNavigationBarExample, { - title: 'Bottom Navigation Bar', + title: 'Navigation Bar (React Navigation)', }); const styles = StyleSheet.create({ diff --git a/src/components/BottomNavigation/BottomNavigation.tsx b/src/components/BottomNavigation/BottomNavigation.tsx index 6097628bb6..54246fa448 100644 --- a/src/components/BottomNavigation/BottomNavigation.tsx +++ b/src/components/BottomNavigation/BottomNavigation.tsx @@ -9,12 +9,12 @@ import type { import useLatestCallback from 'use-latest-callback'; -import BottomNavigationBar from './BottomNavigationBar'; import BottomNavigationRouteScreen from './BottomNavigationRouteScreen'; import { useInternalTheme } from '../../core/theming'; import type { ThemeProp } from '../../types'; import useAnimatedValueArray from '../../utils/useAnimatedValueArray'; import type { IconSource } from '../Icon'; +import NavigationBar from '../NavigationBar/NavigationBar'; import type { Props as TouchableRippleProps } from '../TouchableRipple/TouchableRipple'; export type BaseRoute = { @@ -48,13 +48,6 @@ type TouchableProps = TouchableRippleProps & { }; export type Props = { - /** - * @deprecated The `shifting` style is a Material Design 2 pattern that is not - * part of Material Design 3 and no longer has any effect. It will be removed - * in a future version. To animate scene transitions, use `sceneAnimationType` - * and `sceneAnimationEnabled` instead. - */ - shifting?: boolean; /** * Whether to show labels in tabs. When `false`, only icons will be displayed. */ @@ -199,8 +192,8 @@ export type Props = { */ inactiveColor?: string; /** - * Whether animation is enabled for scenes transitions in `shifting` mode. - * By default, the scenes cross-fade during tab change when `shifting` is enabled. + * Whether animation is enabled for scene transitions. + * By default, the scenes cross-fade during tab change. * Specify `sceneAnimationEnabled` as `false` to disable the animation. */ sceneAnimationEnabled?: boolean; @@ -546,7 +539,7 @@ const BottomNavigation = ({ ); })}
- (scenes: { ); }; -/** - * @deprecated Use the top-level `NavigationBar` export instead. - * `BottomNavigation.Bar` is the M3 "original" navigation bar, superseded by the - * flexible `NavigationBar`. Kept as an alias for backwards compatibility. - */ -// @component ./BottomNavigationBar.tsx -BottomNavigation.Bar = BottomNavigationBar; - export default BottomNavigation; const styles = StyleSheet.create({ diff --git a/src/components/BottomNavigation/BottomNavigationBar.tsx b/src/components/BottomNavigation/BottomNavigationBar.tsx deleted file mode 100644 index e5fbbe0b4d..0000000000 --- a/src/components/BottomNavigation/BottomNavigationBar.tsx +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @deprecated Use `NavigationBar` instead. `BottomNavigation.Bar` is the M3 - * "original" navigation bar and has been superseded by the flexible - * `NavigationBar`. This module re-exports `NavigationBar` for backwards - * compatibility and will be removed in a future major version. - */ -export { default } from '../NavigationBar/NavigationBar'; -export type { Props, BaseRoute } from '../NavigationBar/NavigationBar'; \ No newline at end of file diff --git a/src/components/__tests__/BottomNavigation.test.tsx b/src/components/__tests__/BottomNavigation.test.tsx index 46c2ad3451..673851d593 100644 --- a/src/components/__tests__/BottomNavigation.test.tsx +++ b/src/components/__tests__/BottomNavigation.test.tsx @@ -227,26 +227,6 @@ it('renders bottom navigation with three tabs', async () => { expect(tree).toMatchSnapshot(); }); -it('does not warn or crash when the deprecated shifting prop is passed with fewer than 2 tabs', () => { - const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); - - const { getByTestId } = render( - route.title} - testID="bottom-navigation" - /> - ); - - // `shifting` is a deprecated no-op, so it no longer warns about tab count. - expect(getByTestId('bottom-navigation-bar')).toBeDefined(); - expect(warn).not.toHaveBeenCalled(); - - jest.restoreAllMocks(); -}); - it('renders custom icon and label', async () => { const tree = ( await render( @@ -466,7 +446,7 @@ it('renders tab labels when labeled', () => { /> ); - // Each tab renders an active and inactive label layer, so both match. + // Each tab renders a single label (no cross-fade layers). expect(getAllByText('Alpha').length).toBeGreaterThan(0); expect(getAllByText('Beta').length).toBeGreaterThan(0); }); From 8fe9cf71b874bdcb67f8bc3a06371c72a50f60f2 Mon Sep 17 00:00:00 2001 From: Bartek Dybowski Date: Thu, 18 Jun 2026 16:20:42 +0200 Subject: [PATCH 15/18] refactor(navigation-bar): migrate motion to reanimated - replace RN Animated with react-native-reanimated (shared values + worklets) - each item springs its own selection progress from its focused prop, removing the central tabsAnims array, animateToIndex, and the native-driver reset workaround - keyboard slide uses withTiming + useAnimatedStyle on a wrapper view - honor reduce motion via ReduceMotion (drops theme.animation.scale gate) - remove customizable animationEasing prop (+ its BottomNavigation passthrough) Addresses satya164: move to reanimated, drop animationEasing. Also Copilot: map-for-side-effects removed with animateToIndex. --- .../BottomNavigation/BottomNavigation.tsx | 1 - .../NavigationBar/NavigationBar.tsx | 329 +- .../BottomNavigation.test.tsx.snap | 11736 ++++++++-------- 3 files changed, 6174 insertions(+), 5892 deletions(-) diff --git a/src/components/BottomNavigation/BottomNavigation.tsx b/src/components/BottomNavigation/BottomNavigation.tsx index 54246fa448..209a21b05b 100644 --- a/src/components/BottomNavigation/BottomNavigation.tsx +++ b/src/components/BottomNavigation/BottomNavigation.tsx @@ -554,7 +554,6 @@ const BottomNavigation = ({ style={barStyle} activeIndicatorStyle={activeIndicatorStyle} labeled={labeled} - animationEasing={sceneAnimationEasing} onTabPress={handleTabPress} onTabLongPress={onTabLongPress} safeAreaInsets={safeAreaInsets} diff --git a/src/components/NavigationBar/NavigationBar.tsx b/src/components/NavigationBar/NavigationBar.tsx index 724e73f1c3..2ff7379aa4 100644 --- a/src/components/NavigationBar/NavigationBar.tsx +++ b/src/components/NavigationBar/NavigationBar.tsx @@ -1,20 +1,22 @@ import * as React from 'react'; -import { - Animated, - Easing, - Platform, - StyleSheet, - Pressable, - View, -} from 'react-native'; +import { Platform, StyleSheet, Pressable, View } from 'react-native'; import type { + Animated as RNAnimated, ColorValue, - EasingFunction, StyleProp, TextStyle, ViewStyle, } from 'react-native'; +import Animated, { + Easing, + ReduceMotion, + runOnJS, + useAnimatedStyle, + useSharedValue, + withSpring, + withTiming, +} from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { @@ -35,11 +37,10 @@ import { getLabelColor, } from './utils'; import { useInternalTheme } from '../../core/theming'; +import { useReduceMotion } from '../../theme/accessibility/ReduceMotionContext'; import { toRawSpring } from '../../theme/tokens/sys/motion'; import { getStateLayer } from '../../theme/utils/state'; import type { Theme, ThemeProp } from '../../types'; -import useAnimatedValue from '../../utils/useAnimatedValue'; -import useAnimatedValueArray from '../../utils/useAnimatedValueArray'; import useIsKeyboardShown from '../../utils/useIsKeyboardShown'; import useLayout from '../../utils/useLayout'; import Badge from '../Badge'; @@ -189,10 +190,6 @@ export type Props = { * Custom color for icon and label in the inactive tab. */ inactiveColor?: string; - /** - * The scene animation Easing. - */ - animationEasing?: EasingFunction | undefined; /** * Whether the bottom navigation bar is hidden when keyboard is shown. * On Android, this works best when [`windowSoftInputMode`](https://developer.android.com/guide/topics/manifest/activity-element#wsoft) is set to `adjustResize`. @@ -212,7 +209,7 @@ export type Props = { * Specifies the largest possible scale a label font can reach. */ labelMaxFontSizeMultiplier?: number; - style?: Animated.WithAnimatedValue>; + style?: RNAnimated.WithAnimatedValue>; activeIndicatorStyle?: StyleProp; /** * @optional @@ -253,7 +250,6 @@ const Touchable = ({ type ItemProps = { route: Route; focused: boolean; - active: Animated.Value; labeled: boolean; variant: 'stacked' | 'horizontal'; activeTintColor: ColorValue; @@ -277,7 +273,6 @@ type ItemProps = { const NavigationBarItem = ({ route, focused, - active, labeled, variant, activeTintColor, @@ -303,13 +298,39 @@ const NavigationBarItem = ({ const [keyboardFocused, setKeyboardFocused] = React.useState(false); const [pressed, setPressed] = React.useState(false); - // The active indicator is always mounted and cross-fades via `active` opacity - // (remounting it on focus change breaks native-driven animations). In the - // stacked layout it also scales horizontally from 0.5 → 1 on selection. - const outlineScale = active.interpolate({ - inputRange: [0, 1], - outputRange: [0.5, 1], - }); + const reduceMotion = useReduceMotion(); + const reanimatedReduceMotion = reduceMotion + ? ReduceMotion.Always + : ReduceMotion.Never; + + // Selection progress for the active indicator: 1 when focused, 0 otherwise. + // Each item springs its own progress from its `focused` prop, so there's no + // shared animation array to keep in sync. + const progress = useSharedValue(focused ? 1 : 0); + const isFirstRender = React.useRef(true); + + React.useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + progress.value = withSpring(focused ? 1 : 0, { + ...toRawSpring(theme.motion.spring.fast.spatial), + reduceMotion: reanimatedReduceMotion, + }); + }, [focused, progress, theme.motion, reanimatedReduceMotion]); + + // The active indicator is always mounted and cross-fades via opacity (the + // stacked layout also scales it horizontally 0.5 → 1, the horizontal layout + // scales it 0.8 → 1). + const stackedIndicatorAnimatedStyle = useAnimatedStyle(() => ({ + opacity: progress.value, + transform: [{ scaleX: 0.5 + progress.value * 0.5 }], + })); + const horizontalIndicatorAnimatedStyle = useAnimatedStyle(() => ({ + opacity: progress.value, + transform: [{ scale: 0.8 + progress.value * 0.2 }], + })); const iconColor = focused ? activeTintColor : inactiveTintColor; const labelColor = getLabelColor({ @@ -408,11 +429,8 @@ const NavigationBarItem = ({ testID={indicatorTestID} style={[ styles.stackedIndicator, - { - opacity: active, - transform: [{ scaleX: outlineScale }], - backgroundColor: colors[colorRoles.activeIndicator], - }, + { backgroundColor: colors[colorRoles.activeIndicator] }, + stackedIndicatorAnimatedStyle, activeIndicatorStyle, ]} /> @@ -441,18 +459,8 @@ const NavigationBarItem = ({ style={[ StyleSheet.absoluteFill, styles.horizontalIndicator, - { - backgroundColor: colors[colorRoles.activeIndicator], - opacity: active, - transform: [ - { - scale: active.interpolate({ - inputRange: [0, 1], - outputRange: [0.8, 1], - }), - }, - ], - }, + { backgroundColor: colors[colorRoles.activeIndicator] }, + horizontalIndicatorAnimatedStyle, activeIndicatorStyle, ]} /> @@ -500,14 +508,8 @@ const NavigationBarItem = ({ * The Material Design 3 flexible navigation bar. It can easily be integrated * with [React Navigation's Bottom Tabs Navigator](https://reactnavigation.org/docs/bottom-tab-navigator/). * - * The flexible navigation bar replaces the original (now deprecated) navigation - * bar exposed as `BottomNavigation.Bar`. Set the `variant` prop to `'horizontal'` - * to lay items out horizontally (icon beside label) in medium-width windows. - * - * Migrating from `BottomNavigation.Bar`: it is deprecated in favor of - * `NavigationBar`. The Material Design 2 `shifting` prop has been removed (it - * has no MD3 equivalent), tab interactions now show MD3 state layers instead of - * suppressing feedback, and the bar height follows the 64dp spec. + * Set the `variant` prop to `'horizontal'` to lay items out horizontally + * (icon beside label) in medium-width windows. * * ## Usage * ### without React Navigation @@ -588,7 +590,6 @@ const NavigationBar = ({ activeIndicatorStyle, labeled = true, variant = 'stacked', - animationEasing, onTabPress, onTabLongPress, safeAreaInsets, @@ -600,23 +601,17 @@ const NavigationBar = ({ const theme = useInternalTheme(themeOverrides); const { colors, motion } = theme as Theme; const { bottom, left, right } = useSafeAreaInsets(); - const { scale } = theme.animation; const compact = compactProp ?? false; - /** - * Visibility of the navigation bar, visible state is 1 and invisible is 0. - */ - const visibleAnim = useAnimatedValue(1); + const reduceMotion = useReduceMotion(); + const reanimatedReduceMotion = reduceMotion + ? ReduceMotion.Always + : ReduceMotion.Never; /** - * Active state of individual tab items, active state is 1 and inactive state is 0. + * Visibility of the navigation bar, visible state is 1 and invisible is 0. */ - const tabsAnims = useAnimatedValueArray( - navigationState.routes.map( - // focused === 1, unfocused === 0 - (_, i) => (i === navigationState.index ? 1 : 0) - ) - ); + const visible = useSharedValue(1); /** * Layout of the navigation bar. @@ -630,77 +625,41 @@ const NavigationBar = ({ const handleKeyboardShow = React.useCallback(() => { setKeyboardVisible(true); - Animated.timing(visibleAnim, { - toValue: 0, + visible.value = withTiming(0, { // The bar slides out, so accelerate (exit). - duration: motion.duration.short3 * scale, + duration: motion.duration.short3, easing: Easing.bezier(...motion.easing.standardAccelerate), - useNativeDriver: true, - }).start(); - }, [motion, scale, visibleAnim]); + reduceMotion: reanimatedReduceMotion, + }); + }, [motion, reanimatedReduceMotion, visible]); const handleKeyboardHide = React.useCallback(() => { - Animated.timing(visibleAnim, { - toValue: 1, - // The bar slides back in, so decelerate (enter). - duration: motion.duration.short2 * scale, - easing: Easing.bezier(...motion.easing.standardDecelerate), - useNativeDriver: true, - }).start(() => { - setKeyboardVisible(false); - }); - }, [motion, scale, visibleAnim]); - - const animateToIndex = React.useCallback( - (index: number) => { - // When animations are disabled (e.g. reduce motion), jump to the value. - if (scale === 0) { - tabsAnims.forEach((tab, i) => tab.setValue(i === index ? 1 : 0)); - return; + visible.value = withTiming( + 1, + { + // The bar slides back in, so decelerate (enter). + duration: motion.duration.short2, + easing: Easing.bezier(...motion.easing.standardDecelerate), + reduceMotion: reanimatedReduceMotion, + }, + (finished) => { + if (finished) { + runOnJS(setKeyboardVisible)(false); + } } + ); + }, [motion, reanimatedReduceMotion, visible]); - Animated.parallel( - navigationState.routes.map((_, i) => { - const toValue = i === index ? 1 : 0; - // Spring the active indicator for the M3-Expressive selection motion. - // A custom `animationEasing` opts back into timed (eased) movement. - return animationEasing - ? Animated.timing(tabsAnims[i], { - toValue, - duration: motion.duration.short4 * scale, - easing: animationEasing, - useNativeDriver: true, - }) - : Animated.spring(tabsAnims[i], { - toValue, - ...toRawSpring(motion.spring.fast.spatial), - useNativeDriver: true, - }); - }) - ).start(() => { - // Workaround a bug in native animations where this is reset after first animation - tabsAnims.map((tab, i) => tab.setValue(i === index ? 1 : 0)); - }); - }, - [scale, navigationState.routes, tabsAnims, animationEasing, motion] - ); - - React.useEffect(() => { - // Workaround for native animated bug in react-native@^0.57 - // Context: https://github.com/callstack/react-native-paper/pull/637 - animateToIndex(navigationState.index); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + // Slide the bar down by its own height when the keyboard hides it. + const barAnimatedStyle = useAnimatedStyle(() => ({ + transform: [{ translateY: (1 - visible.value) * layout.height }], + })); useIsKeyboardShown({ onShow: handleKeyboardShow, onHide: handleKeyboardHide, }); - React.useEffect(() => { - animateToIndex(navigationState.index); - }, [navigationState.index, animateToIndex]); - const eventForIndex = (index: number) => { const event = { route: navigationState.routes[index], @@ -743,28 +702,13 @@ const NavigationBar = ({ }; return ( - + +
); }; @@ -840,6 +784,9 @@ const styles = StyleSheet.create({ right: 0, bottom: 0, }, + absolute: { + position: 'absolute', + }, barContent: { alignItems: 'center', overflow: 'hidden', diff --git a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap index e2b4116595..5b8c5cbaeb 100644 --- a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap @@ -71,32 +71,25 @@ exports[`allows customizing Route's type via generics 1`] = `
- @@ -231,205 +212,211 @@ exports[`allows customizing Route's type via generics 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 1, + "transform": [ + { + "scaleX": 1, + }, + ], + }, + undefined, ] } /> - - - - + + + + + + - - - + - First - + ] + } + > + First + + - - - - @@ -437,113 +424,152 @@ exports[`allows customizing Route's type via generics 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, - ] - } - /> - - - + - + + + + + + - - - + - Second - + ] + } + > + Second + + @@ -625,32 +651,25 @@ exports[`hides labels when labeled is false 1`] = ` - @@ -786,186 +793,192 @@ exports[`hides labels when labeled is false 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 1, + "transform": [ + { + "scaleX": 1, + }, + ], + }, + undefined, ] } /> - - - + - magnify - - - + + - + > + + magnify + + + + + - - - - @@ -973,186 +986,192 @@ exports[`hides labels when labeled is false 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, ] } /> - - - + - camera - - - + + - + > + + camera + + + + + - - - - @@ -1160,92 +1179,131 @@ exports[`hides labels when labeled is false 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, ] } /> - - - + - inbox - - - + + - + > + + inbox + + + + + @@ -1328,32 +1386,25 @@ exports[`renders bottom navigation 1`] = ` - @@ -1488,235 +1527,483 @@ exports[`renders bottom navigation 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 1, + "transform": [ + { + "scaleX": 1, + }, + ], + }, + undefined, ] } /> + + + + + + magnify + + + + + - magnify + Route: 0 - - - + + - + + + + + + + camera + + + + + + + + - Route: 0 - + ] + } + > + Route: 1 + + - - - - @@ -1724,471 +2011,241 @@ exports[`renders bottom navigation 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, ] } /> + + + + + + inbox + + + + + - camera - - - - - - - - - Route: 1 - - - - - - - - - - - - - - inbox - - - - - - - - - Route: 2 - - - - - + > + Route: 2 +
+
+
+
- @@ -2196,235 +2253,241 @@ exports[`renders bottom navigation 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, ] } /> - - - + + + + + heart + + + - heart - + + - - - - - Route: 3 - + ] + } + > + Route: 3 + +
-
- - - @@ -2432,143 +2495,182 @@ exports[`renders bottom navigation 1`] = ` style={ [ { - "borderRadius": 16, - "height": 32, - "width": 64, + "alignSelf": "center", + "borderRadius": 16, + "height": 32, + "width": 64, + }, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, + ] + } + /> + + + + + + shopping-music + + + + > + + - shopping-music - - - - - - - - - Route: 4 - + ] + } + > + Route: 4 + + @@ -2782,32 +2884,25 @@ exports[`renders bottom navigation with getLazy 1`] = `
- @@ -2942,235 +3025,241 @@ exports[`renders bottom navigation with getLazy 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 1, + "transform": [ + { + "scaleX": 1, + }, + ], + }, + undefined, ] } /> - - - + + + + + magnify + + + - magnify - + + - - - - - Route: 0 - + ] + } + > + Route: 0 + + - - - - @@ -3178,235 +3267,241 @@ exports[`renders bottom navigation with getLazy 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, ] } /> - - - + + + + + camera + + + - camera - + + - - - - - Route: 1 - + ] + } + > + Route: 1 + + - - - - @@ -3414,235 +3509,241 @@ exports[`renders bottom navigation with getLazy 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, ] } /> - - - + + + + + inbox + + + - inbox - + + - - - - - Route: 2 - + ] + } + > + Route: 2 + + - - - - @@ -3650,235 +3751,241 @@ exports[`renders bottom navigation with getLazy 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, ] } /> - - - + + + + + heart + + + - heart - + + - - - - - Route: 3 - + ] + } + > + Route: 3 + + - - - - @@ -3886,143 +3993,182 @@ exports[`renders bottom navigation with getLazy 1`] = ` style={ [ { - "borderRadius": 16, - "height": 32, - "width": 64, + "alignSelf": "center", + "borderRadius": 16, + "height": 32, + "width": 64, + }, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, + ] + } + /> + + + + + + shopping-music + + + + > + + - shopping-music - - - - - - - - - Route: 4 - + ] + } + > + Route: 4 + + @@ -4104,32 +4250,25 @@ exports[`renders bottom navigation with scene animation 1`] = `
- @@ -4264,235 +4391,241 @@ exports[`renders bottom navigation with scene animation 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 1, + "transform": [ + { + "scaleX": 1, + }, + ], + }, + undefined, ] } /> - - - + + + + + magnify + + + - magnify - + + - - - - - Route: 0 - + ] + } + > + Route: 0 + + - - - - @@ -4500,235 +4633,241 @@ exports[`renders bottom navigation with scene animation 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, ] } /> - - - + + + + + camera + + + - camera - + + - - - - - Route: 1 - + ] + } + > + Route: 1 + + - - - - @@ -4736,235 +4875,241 @@ exports[`renders bottom navigation with scene animation 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, ] } /> - - - + + + + + inbox + + + - inbox - + + - - - - - Route: 2 - + ] + } + > + Route: 2 + + - - - - @@ -4972,235 +5117,241 @@ exports[`renders bottom navigation with scene animation 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, ] } /> - - - + + + + + heart + + + - heart - + + - - - - - Route: 3 - + ] + } + > + Route: 3 + + - - - - @@ -5208,143 +5359,182 @@ exports[`renders bottom navigation with scene animation 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, ] } /> - - - + + + + + shopping-music + + + - shopping-music - + + - - - - - Route: 4 - + ] + } + > + Route: 4 + +
@@ -5426,32 +5616,25 @@ exports[`renders bottom navigation with three tabs 1`] = `
- @@ -5586,235 +5757,241 @@ exports[`renders bottom navigation with three tabs 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 1, + "transform": [ + { + "scaleX": 1, + }, + ], + }, + undefined, ] } /> - - - + - magnify - - - + + - + > + + magnify + + + + + - - - + - Route: 0 - + ] + } + > + Route: 0 + + - - - - @@ -5822,235 +5999,241 @@ exports[`renders bottom navigation with three tabs 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, ] } /> - - - + + + + + camera + + + - camera - + + - - - - - Route: 1 - + ] + } + > + Route: 1 + + - - - + - - @@ -6058,143 +6241,182 @@ exports[`renders bottom navigation with three tabs 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, ] } /> - - - + + + + + inbox + + + - inbox - + + - - - - - Route: 2 - + ] + } + > + Route: 2 + + @@ -6276,32 +6498,25 @@ exports[`renders custom icon and label 1`] = ` - - - + + + - @@ -6436,169 +6639,175 @@ exports[`renders custom icon and label 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 1, + "transform": [ + { + "scaleX": 1, + }, + ], + }, + undefined, ] } /> - - - - + + + + + + - - - - Route: 0 - + + Route: 0 + + - - - - @@ -6606,169 +6815,175 @@ exports[`renders custom icon and label 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, ] } /> - - - - + + + + + + - - - - Route: 1 - - - - - + + Route: 1 + + + + + - - @@ -6776,169 +6991,175 @@ exports[`renders custom icon and label 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, ] } /> - - - - + + + + + + - - - - Route: 2 - + + Route: 2 + + - - - - @@ -6946,169 +7167,175 @@ exports[`renders custom icon and label 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, ] } /> - - - - + + + + + + - - - - Route: 3 - + + Route: 3 + + - - - - @@ -7116,77 +7343,116 @@ exports[`renders custom icon and label 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, ] } /> - - - - + + + + + + - - - - Route: 4 - + + Route: 4 + + @@ -7199,32 +7465,25 @@ exports[`renders custom icon and label 1`] = ` exports[`renders the horizontal (flexible) variant 1`] = ` - - - + - magnify - + /> + /> + + magnify + + + > + + - - - Route: 0 - + ] + } + > + Route: 0 + + - - - - - - + - camera - + /> + /> + + > + camera + + + + - - - Route: 1 - + ] + } + > + Route: 1 + + - - - - - - + - inbox - + /> + /> + + > + inbox + + + + - - - Route: 2 - + ] + } + > + Route: 2 + + @@ -7958,32 +8262,25 @@ exports[`renders with custom active and inactive colors 1`] = ` + - + + + + + > + + magnify + + + > + + - magnify - - - - - - - - - Route: 0 - + ] + } + > + Route: 0 + + - - - - @@ -8354,235 +8645,241 @@ exports[`renders with custom active and inactive colors 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, ] } /> - - - + + + + + camera + + + - camera - - - + + + - - - - - Route: 1 - + ] + } + > + Route: 1 + + - - - - @@ -8590,143 +8887,182 @@ exports[`renders with custom active and inactive colors 1`] = ` style={ [ { + "alignSelf": "center", "borderRadius": 16, "height": 32, "width": 64, }, - null, + { + "backgroundColor": "rgba(232, 222, 248, 1)", + }, + { + "opacity": 0, + "transform": [ + { + "scaleX": 0.5, + }, + ], + }, + undefined, ] } /> - - - + + + + + inbox + + + - inbox - + + - - - - - Route: 2 - + ] + } + > + Route: 2 + + From 4e9a651153db130ebbe673dc0dad877ff1c6e2a8 Mon Sep 17 00:00:00 2001 From: Bartek Dybowski Date: Fri, 3 Jul 2026 12:13:56 +0200 Subject: [PATCH 16/18] fix(tests): migrate BottomNavigation tests to RNTL v14 patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix calls on{IndexChange,TabPress,TabLongPress}: add layoutNavigationBar() before userEvent.press/longPress — the outer Animated.View has pointerEvents="none" until layout is measured, blocking userEvent - Replace fireEvent(text, 'on{Press,LongPress}') with userEvent on role-based tabs, per ESLint no-restricted-syntax rule - Replace .props.style reads with toHaveStyle() assertions - Migrate remaining sync render() calls to async/await + screen.* - Fix NavigationBar.tsx: remove Badge size prop (no longer in API), fix prettier formatting on nested ternary and ?? expression --- .../NavigationBar/NavigationBar.tsx | 16 +- .../__tests__/BottomNavigation.test.tsx | 110 +- .../BottomNavigation.test.tsx.snap | 1442 ++++++++++------- 3 files changed, 923 insertions(+), 645 deletions(-) diff --git a/src/components/NavigationBar/NavigationBar.tsx b/src/components/NavigationBar/NavigationBar.tsx index 2ff7379aa4..ae84a4b367 100644 --- a/src/components/NavigationBar/NavigationBar.tsx +++ b/src/components/NavigationBar/NavigationBar.tsx @@ -359,10 +359,10 @@ const NavigationBarItem = ({ const stateLayer = pressed ? getStateLayer(theme, stateLayerRole, 'pressed') : keyboardFocused - ? getStateLayer(theme, stateLayerRole, 'focused') - : hovered - ? getStateLayer(theme, stateLayerRole, 'hovered') - : null; + ? getStateLayer(theme, stateLayerRole, 'focused') + : hovered + ? getStateLayer(theme, stateLayerRole, 'hovered') + : null; const stateLayerColor = stateLayer ? { backgroundColor: stateLayer.color, opacity: stateLayer.opacity } : null; @@ -386,7 +386,7 @@ const NavigationBarItem = ({ source={ (focused ? route.focusedIcon - : route.unfocusedIcon ?? route.focusedIcon) as IconSource + : (route.unfocusedIcon ?? route.focusedIcon)) as IconSource } color={iconColor} size={ICON_SIZE} @@ -396,11 +396,9 @@ const NavigationBarItem = ({ const tabBadge = ( {typeof badge === 'boolean' ? ( - + ) : ( - - {badge} - + {badge} )} ); diff --git a/src/components/__tests__/BottomNavigation.test.tsx b/src/components/__tests__/BottomNavigation.test.tsx index 673851d593..a808f1d521 100644 --- a/src/components/__tests__/BottomNavigation.test.tsx +++ b/src/components/__tests__/BottomNavigation.test.tsx @@ -151,13 +151,12 @@ it('calls onIndexChange', async () => { renderScene={renderScene} /> ); - // The active scene also renders the route title, so target the last match - // (the tab label) to fire the tab press. + await layoutNavigationBar(); // pressing same index as active navigation state does not call onIndexChange - fireEvent(screen.getAllByText('Route: 0').at(-1)!, 'onPress'); + await userEvent.press(getTab(0)); expect(onIndexChange).not.toHaveBeenCalled(); - fireEvent(screen.getAllByText('Route: 1').at(-1)!, 'onPress'); + await userEvent.press(getTab(1)); expect(onIndexChange).toHaveBeenCalledTimes(1); }); @@ -173,7 +172,8 @@ it('calls onTabPress', async () => { renderScene={renderScene} /> ); - fireEvent(screen.getAllByText('Route: 1').at(-1)!, 'onPress'); + await layoutNavigationBar(); + await userEvent.press(getTab(1)); expect(onTabPress).toHaveBeenCalled(); expect(onTabPress).toHaveBeenCalledTimes(1); expect(onTabPress).toHaveBeenLastCalledWith( @@ -199,7 +199,8 @@ it('calls onTabLongPress', async () => { renderScene={renderScene} /> ); - fireEvent(screen.getAllByText('Route: 2').at(-1)!, 'onLongPress'); + await layoutNavigationBar(); + await userEvent.longPress(getTab(2)); expect(onTabLongPress).toHaveBeenCalled(); expect(onTabLongPress).toHaveBeenCalledTimes(1); expect(onTabLongPress).toHaveBeenLastCalledWith( @@ -432,8 +433,8 @@ it('does not render the legacy ripple overlay', async () => { ).not.toBeOnTheScreen(); }); -it('renders tab labels when labeled', () => { - const { getAllByText } = render( +it('renders tab labels when labeled', async () => { + await render( { ); // Each tab renders a single label (no cross-fade layers). - expect(getAllByText('Alpha').length).toBeGreaterThan(0); - expect(getAllByText('Beta').length).toBeGreaterThan(0); + expect(screen.getAllByText('Alpha').length).toBeGreaterThan(0); + expect(screen.getAllByText('Beta').length).toBeGreaterThan(0); }); -it('renders the horizontal (flexible) variant', () => { - const tree = render( - +it('renders the horizontal (flexible) variant', async () => { + const tree = ( + await render( + + ) ).toJSON(); expect(tree).toMatchSnapshot(); }); -it('falls back to icon-only when horizontal is combined with labeled=false', () => { - const { queryByText } = render( +it('falls back to icon-only when horizontal is combined with labeled=false', async () => { + await render( { +it('renders MD3 state layers on hover, focus and press', async () => { const navigationState = { index: 0, routes: [ @@ -487,35 +490,34 @@ it('renders MD3 state layers on hover, focus and press', () => { ], }; - const { getByTestId } = render( + await render( ); - const layerOpacity = () => - StyleSheet.flatten(getByTestId('tab-b-state-layer').props.style).opacity; + const stateLayer = () => screen.getByTestId('tab-b-state-layer'); // Idle: no visible state layer. - expect(layerOpacity()).toBeUndefined(); + expect(stateLayer()).toHaveStyle({ opacity: undefined }); // Hovered: 8% state layer. - fireEvent(getByTestId('tab-b'), 'hoverIn'); - expect(layerOpacity()).toBe(0.08); - fireEvent(getByTestId('tab-b'), 'hoverOut'); - expect(layerOpacity()).toBeUndefined(); + await fireEvent(screen.getByTestId('tab-b'), 'hoverIn'); + expect(stateLayer()).toHaveStyle({ opacity: 0.08 }); + await fireEvent(screen.getByTestId('tab-b'), 'hoverOut'); + expect(stateLayer()).toHaveStyle({ opacity: undefined }); // Focused: 10% state layer. - fireEvent(getByTestId('tab-b'), 'focus'); - expect(layerOpacity()).toBe(0.1); - fireEvent(getByTestId('tab-b'), 'blur'); + await fireEvent(screen.getByTestId('tab-b'), 'focus'); + expect(stateLayer()).toHaveStyle({ opacity: 0.1 }); + await fireEvent(screen.getByTestId('tab-b'), 'blur'); // Pressed: 10% state layer. - fireEvent(getByTestId('tab-b'), 'pressIn'); - expect(layerOpacity()).toBe(0.1); - fireEvent(getByTestId('tab-b'), 'pressOut'); - expect(layerOpacity()).toBeUndefined(); + await fireEvent(screen.getByTestId('tab-b'), 'pressIn'); + expect(stateLayer()).toHaveStyle({ opacity: 0.1 }); + await fireEvent(screen.getByTestId('tab-b'), 'pressOut'); + expect(stateLayer()).toHaveStyle({ opacity: undefined }); }); -it('colors the focused tab label with secondary and others with onSurfaceVariant', () => { +it('colors the focused tab label with secondary and others with onSurfaceVariant', async () => { const navigationState = { index: 0, routes: [ @@ -524,20 +526,19 @@ it('colors the focused tab label with secondary and others with onSurfaceVariant ], }; - const { getAllByText } = render( + await render( ); - const colorsOf = (text: string) => - getAllByText(text).map( - (node) => StyleSheet.flatten(node.props.style).color - ); - - expect(colorsOf('Alpha')).toContain(getTheme().colors.secondary); - expect(colorsOf('Beta')).toContain(getTheme().colors.onSurfaceVariant); + expect(screen.getAllByText('Alpha').at(-1)).toHaveStyle({ + color: getTheme().colors.secondary, + }); + expect(screen.getAllByText('Beta').at(-1)).toHaveStyle({ + color: getTheme().colors.onSurfaceVariant, + }); }); -it('renders the active indicator with the secondaryContainer color', () => { +it('renders the active indicator with the secondaryContainer color', async () => { const navigationState = { index: 0, routes: [ @@ -546,17 +547,16 @@ it('renders the active indicator with the secondaryContainer color', () => { ], }; - const { getByTestId } = render( + await render( ); - expect( - StyleSheet.flatten(getByTestId('tab-a-active-indicator').props.style) - .backgroundColor - ).toBe(getTheme().colors.secondaryContainer); + expect(screen.getByTestId('tab-a-active-indicator')).toHaveStyle({ + backgroundColor: getTheme().colors.secondaryContainer, + }); }); -it('renders a badge for routes that define one', () => { +it('renders a badge for routes that define one', async () => { const navigationState = { index: 0, routes: [ @@ -565,11 +565,11 @@ it('renders a badge for routes that define one', () => { ], }; - const { getByText } = render( + await render( ); - expect(getByText('3')).toBeTruthy(); + expect(screen.getByText('3')).toBeTruthy(); }); describe('getActiveTintColor', () => { diff --git a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap index 5b8c5cbaeb..8aa6c7a010 100644 --- a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap @@ -26,7 +26,7 @@ exports[`allows customizing Route's type via generics 1`] = ` } > - First + + First + @@ -286,24 +288,31 @@ exports[`allows customizing Route's type via generics 1`] = ` } > @@ -498,24 +507,31 @@ exports[`allows customizing Route's type via generics 1`] = ` } > @@ -606,7 +622,7 @@ exports[`hides labels when labeled is false 1`] = ` } > - Route: 0 + + Route: 0 + @@ -897,24 +915,31 @@ exports[`hides labels when labeled is false 1`] = ` } > @@ -1090,24 +1115,31 @@ exports[`hides labels when labeled is false 1`] = ` } > @@ -1283,24 +1315,31 @@ exports[`hides labels when labeled is false 1`] = ` } > @@ -1341,7 +1380,7 @@ exports[`renders bottom navigation 1`] = ` } > - Route: 0 + + Route: 0 + @@ -1631,24 +1672,31 @@ exports[`renders bottom navigation 1`] = ` } > @@ -1873,24 +1921,31 @@ exports[`renders bottom navigation 1`] = ` } > @@ -2115,24 +2170,31 @@ exports[`renders bottom navigation 1`] = ` } > @@ -2357,24 +2419,31 @@ exports[`renders bottom navigation 1`] = ` } > @@ -2599,24 +2668,31 @@ exports[`renders bottom navigation 1`] = ` } > @@ -2707,7 +2783,7 @@ exports[`renders bottom navigation with getLazy 1`] = ` } > - Route: 0 + + Route: 0 + - Route: 1 + + Route: 1 + - Route: 3 + + Route: 3 + - Route: 4 + + Route: 4 + @@ -3129,24 +3213,31 @@ exports[`renders bottom navigation with getLazy 1`] = ` } > @@ -3371,24 +3462,31 @@ exports[`renders bottom navigation with getLazy 1`] = ` } > @@ -3613,24 +3711,31 @@ exports[`renders bottom navigation with getLazy 1`] = ` } > @@ -3855,24 +3960,31 @@ exports[`renders bottom navigation with getLazy 1`] = ` } > @@ -4097,24 +4209,31 @@ exports[`renders bottom navigation with getLazy 1`] = ` } > @@ -4205,7 +4324,7 @@ exports[`renders bottom navigation with scene animation 1`] = ` } > - Route: 0 + + Route: 0 + @@ -4495,24 +4616,31 @@ exports[`renders bottom navigation with scene animation 1`] = ` } >
@@ -4737,24 +4865,31 @@ exports[`renders bottom navigation with scene animation 1`] = ` } >
@@ -4979,24 +5114,31 @@ exports[`renders bottom navigation with scene animation 1`] = ` } >
@@ -5221,24 +5363,31 @@ exports[`renders bottom navigation with scene animation 1`] = ` } >
@@ -5463,24 +5612,31 @@ exports[`renders bottom navigation with scene animation 1`] = ` } >
@@ -5571,7 +5727,7 @@ exports[`renders bottom navigation with three tabs 1`] = ` } > - Route: 0 + + Route: 0 +
@@ -5861,24 +6019,31 @@ exports[`renders bottom navigation with three tabs 1`] = ` } >
@@ -6103,24 +6268,31 @@ exports[`renders bottom navigation with three tabs 1`] = ` } >
@@ -6345,24 +6517,31 @@ exports[`renders bottom navigation with three tabs 1`] = ` } >
@@ -6453,7 +6632,7 @@ exports[`renders custom icon and label 1`] = ` } > - Route: 0 + + Route: 0 +
@@ -6713,24 +6894,31 @@ exports[`renders custom icon and label 1`] = ` } >
@@ -6743,11 +6931,15 @@ exports[`renders custom icon and label 1`] = ` } } > - Route: 0 - +
@@ -6889,24 +7081,31 @@ exports[`renders custom icon and label 1`] = ` } > @@ -6919,11 +7118,15 @@ exports[`renders custom icon and label 1`] = ` } } > - Route: 1 - + @@ -7065,24 +7268,31 @@ exports[`renders custom icon and label 1`] = ` } > @@ -7095,11 +7305,15 @@ exports[`renders custom icon and label 1`] = ` } } > - Route: 2 - + @@ -7241,24 +7455,31 @@ exports[`renders custom icon and label 1`] = ` } > @@ -7271,11 +7492,15 @@ exports[`renders custom icon and label 1`] = ` } } > - Route: 3 - + @@ -7417,24 +7642,31 @@ exports[`renders custom icon and label 1`] = ` } > @@ -7447,11 +7679,15 @@ exports[`renders custom icon and label 1`] = ` } } > - Route: 4 - + @@ -7691,24 +7927,31 @@ exports[`renders the horizontal (flexible) variant 1`] = ` } > @@ -7905,24 +8148,31 @@ exports[`renders the horizontal (flexible) variant 1`] = ` } > @@ -8119,24 +8369,31 @@ exports[`renders the horizontal (flexible) variant 1`] = ` } > @@ -8217,7 +8474,7 @@ exports[`renders with custom active and inactive colors 1`] = ` } > - Route: 0 + + Route: 0 + @@ -8507,24 +8766,31 @@ exports[`renders with custom active and inactive colors 1`] = ` } > @@ -8749,24 +9015,31 @@ exports[`renders with custom active and inactive colors 1`] = ` } > @@ -8991,24 +9264,31 @@ exports[`renders with custom active and inactive colors 1`] = ` } > From fb3c9538b85f1970fa5f7b052445e4180f36d78e Mon Sep 17 00:00:00 2001 From: Bartek Dybowski Date: Fri, 3 Jul 2026 12:27:07 +0200 Subject: [PATCH 17/18] fix(docs): fix CI build for NavigationBar as top-level component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filter 5.x sidebar generation to only include components that exist in the 5.x directory — NavigationBar is 6.x-only so the generate script was injecting it into the 5.x _meta.json, causing rspress to fail when it couldn't find the directory. Also fix the BottomNavigation JSDoc link from `../NavigationBar` to `../NavigationBar/NavigationBar` — rspress requires pointing at a page inside a subdirectory, not the directory itself. Regenerate 6.x component docs. --- .../BottomNavigation/BottomNavigation.mdx | 10 +- .../BottomNavigation/BottomNavigationBar.mdx | 285 - .../components/BottomNavigation/_meta.json | 3 +- .../NavigationBar/NavigationBar.mdx | 131 +- .../docs/components/NavigationBar/_meta.json | 4 +- docs/scripts/generate-rspress-content.ts | 12 +- docs/src/data/componentDocs6x.json | 7879 +++++++++-------- .../BottomNavigation/BottomNavigation.tsx | 2 +- 8 files changed, 4071 insertions(+), 4255 deletions(-) delete mode 100644 docs/6.x/docs/components/BottomNavigation/BottomNavigationBar.mdx diff --git a/docs/6.x/docs/components/BottomNavigation/BottomNavigation.mdx b/docs/6.x/docs/components/BottomNavigation/BottomNavigation.mdx index c4fe55af84..2c8e93be1a 100644 --- a/docs/6.x/docs/components/BottomNavigation/BottomNavigation.mdx +++ b/docs/6.x/docs/components/BottomNavigation/BottomNavigation.mdx @@ -9,7 +9,7 @@ import ScreenshotTabs from '@docs/components/ScreenshotTabs.tsx'; import ExtendedExample from '@docs/components/ExtendedExample.tsx'; BottomNavigation provides quick navigation between top-level views of an app with a bottom navigation bar. -It is primarily designed for use on mobile. If you want to use the navigation bar only see [`BottomNavigation.Bar`](BottomNavigationBar). +It is primarily designed for use on mobile. If you want to use the navigation bar only see [`NavigationBar`](../NavigationBar/NavigationBar). By default BottomNavigation uses primary color as a background, in dark theme with `adaptive` mode it will use surface colour instead. See [Dark Theme](https://callstack.github.io/react-native-paper/docs/guides/theming#dark-theme) for more information. @@ -67,14 +67,6 @@ export default MyComponent; -
- -### shifting - -
- - -
### labeled diff --git a/docs/6.x/docs/components/BottomNavigation/BottomNavigationBar.mdx b/docs/6.x/docs/components/BottomNavigation/BottomNavigationBar.mdx deleted file mode 100644 index a93d41ee74..0000000000 --- a/docs/6.x/docs/components/BottomNavigation/BottomNavigationBar.mdx +++ /dev/null @@ -1,285 +0,0 @@ ---- -title: BottomNavigation.Bar ---- - -import PropTable from '@docs/components/PropTable.tsx'; -import ExtendsLink from '@docs/components/ExtendsLink.tsx'; -import ThemeColorsTable from '@docs/components/ThemeColorsTable.tsx'; -import ScreenshotTabs from '@docs/components/ScreenshotTabs.tsx'; -import ExtendedExample from '@docs/components/ExtendedExample.tsx'; - -A navigation bar which can easily be integrated with [React Navigation's Bottom Tabs Navigator](https://reactnavigation.org/docs/bottom-tab-navigator/). - - - - - - - - - ## Usage -### without React Navigation -```js -import React from 'react'; -import { useState } from 'react'; -import { View } from 'react-native'; -import { BottomNavigation, Text, Provider } from 'react-native-paper'; -import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons'; - -function HomeScreen() { - return ( - - Home! - - ); -} - -function SettingsScreen() { - return ( - - Settings! - - ); -} - -export default function MyComponent() { - const [index, setIndex] = useState(0); - - const routes = [ - { key: 'home', title: 'Home', icon: 'home' }, - { key: 'settings', title: 'Settings', icon: 'cog' }, - ]; - - const renderScene = ({ route }) => { - switch (route.key) { - case 'home': - return ; - case 'settings': - return ; - default: - return null; - } - }; - - return ( - - {renderScene({ route: routes[index] })} - { - const newIndex = routes.findIndex((r) => r.key === route.key); - if (newIndex !== -1) { - setIndex(newIndex); - } - }} - renderIcon={({ route, color }) => ( - - )} - getLabelText={({ route }) => route.title} - /> - - ); -} -``` - - ### with React Navigation - \n Home!\n \n );\n}\n\nfunction SettingsScreen() {\n return (\n \n Settings!\n \n );\n}\n\nconst MyTabs = createBottomTabNavigator({\n screenOptions: {\n animation: 'shift',\n },\n tabBar: ({ navigation, state, descriptors, insets }) => (\n {\n const event = navigation.emit({\n type: 'tabPress',\n target: route.key,\n canPreventDefault: true,\n });\n\n if (event.defaultPrevented) {\n preventDefault();\n } else {\n navigation.dispatch({\n ...CommonActions.navigate(route.name, route.params),\n target: state.key,\n });\n }\n }}\n renderIcon={({ route, focused, color }) =>\n descriptors[route.key].options.tabBarIcon?.({\n focused,\n color,\n size: 24,\n }) || null\n }\n getLabelText={({ route }) => {\n const { options } = descriptors[route.key];\n const label =\n typeof options.tabBarLabel === 'string'\n ? options.tabBarLabel\n : typeof options.title === 'string'\n ? options.title\n : route.name;\n\n return label;\n }}\n />\n ),\n screens: {\n Home: {\n screen: HomeScreen,\n options: {\n tabBarIcon: ({ color }) => (\n \n ),\n },\n },\n Settings: {\n screen: SettingsScreen,\n options: {\n tabBarIcon: ({ color }) => (\n \n ),\n },\n },\n },\n});\n\nconst Navigation = createStaticNavigation(MyTabs);\n\nexport default function App() {\n return (\n \n \n \n \n \n );\n}","dynamic":"import { Text, View } from 'react-native';\nimport { NavigationContainer, CommonActions } from '@react-navigation/native';\nimport { createBottomTabNavigator } from '@react-navigation/bottom-tabs';\nimport { Provider, BottomNavigation } from 'react-native-paper';\nimport MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons';\n\nfunction HomeScreen() {\n return (\n \n Home!\n \n );\n}\n\nfunction SettingsScreen() {\n return (\n \n Settings!\n \n );\n}\n\nconst Tab = createBottomTabNavigator();\n\nexport default function App() {\n return (\n \n \n (\n {\n const event = navigation.emit({\n type: 'tabPress',\n target: route.key,\n canPreventDefault: true,\n });\n\n if (event.defaultPrevented) {\n preventDefault();\n } else {\n navigation.dispatch({\n ...CommonActions.navigate(route.name, route.params),\n target: state.key,\n });\n }\n }}\n renderIcon={({ route, focused, color }) =>\n descriptors[route.key].options.tabBarIcon?.({\n focused,\n color,\n size: 24,\n }) || null\n }\n getLabelText={({ route }) => {\n const { options } = descriptors[route.key];\n const label =\n typeof options.tabBarLabel === 'string'\n ? options.tabBarLabel\n : typeof options.title === 'string'\n ? options.title\n : route.name;\n\n return label;\n }}\n />\n )}>\n (\n \n ),\n }}\n />\n (\n \n ),\n }}\n />\n \n \n \n );\n}\n"}}} /> - - - - ## Props - - - - -
- -### shifting - -
- - - -
- -### labeled - -
- - - -
- -### compact - -
- - - -
- -### navigationState (required) - -
- - - -
- -### renderIcon - -
- - - -
- -### renderLabel - -
- - - -
- -### renderTouchable - -
- - - -
- -### getAccessibilityLabel - -
- - - -
- -### getBadge - -
- - - -
- -### getLabelText - -
- - - -
- -### getTestID - -
- - - -
- -### onTabPress (required) - -
- - - -
- -### onTabLongPress - -
- - - -
- -### activeColor - -
- - - -
- -### inactiveColor - -
- - - -
- -### animationEasing - -
- - - -
- -### keyboardHidesNavigationBar - -
- - - -
- -### safeAreaInsets - -
- - - -
- -### labelMaxFontSizeMultiplier - -
- - - -
- -### style - -
- - - -
- -### activeIndicatorStyle - -
- - - -
- -### theme - -
- - - -
- -### testID - -
- - - - - - - - - - diff --git a/docs/6.x/docs/components/BottomNavigation/_meta.json b/docs/6.x/docs/components/BottomNavigation/_meta.json index 94a0b010f3..7988479ebd 100644 --- a/docs/6.x/docs/components/BottomNavigation/_meta.json +++ b/docs/6.x/docs/components/BottomNavigation/_meta.json @@ -1,4 +1,3 @@ [ - "BottomNavigation", - "BottomNavigationBar" + "BottomNavigation" ] diff --git a/docs/6.x/docs/components/NavigationBar/NavigationBar.mdx b/docs/6.x/docs/components/NavigationBar/NavigationBar.mdx index 985d33b890..457c250ccc 100644 --- a/docs/6.x/docs/components/NavigationBar/NavigationBar.mdx +++ b/docs/6.x/docs/components/NavigationBar/NavigationBar.mdx @@ -8,35 +8,74 @@ import ThemeColorsTable from '@docs/components/ThemeColorsTable.tsx'; import ScreenshotTabs from '@docs/components/ScreenshotTabs.tsx'; import ExtendedExample from '@docs/components/ExtendedExample.tsx'; -The Material Design 3 flexible navigation bar. It can easily be integrated with [React Navigation's Bottom Tabs Navigator](https://reactnavigation.org/docs/bottom-tab-navigator/). +The Material Design 3 flexible navigation bar. It can easily be integrated +with [React Navigation's Bottom Tabs Navigator](https://reactnavigation.org/docs/bottom-tab-navigator/). -The flexible navigation bar replaces the original (now deprecated) navigation bar exposed as `BottomNavigation.Bar`. Set the `variant` prop to `'horizontal'` to lay items out horizontally (icon beside label) in medium-width windows. +Set the `variant` prop to `'horizontal'` to lay items out horizontally +(icon beside label) in medium-width windows. + + + + ## Usage +### without React Navigation ```js import * as React from 'react'; -import { NavigationBar } from 'react-native-paper'; +import { View } from 'react-native'; +import { NavigationBar, Text, Provider } from 'react-native-paper'; -const MyComponent = () => { - const [index, setIndex] = React.useState(0); - const [routes] = React.useState([ - { key: 'music', title: 'Favorites', focusedIcon: 'heart' }, - { key: 'albums', title: 'Albums', focusedIcon: 'album' }, - { key: 'recents', title: 'Recents', focusedIcon: 'history' }, - ]); +function HomeScreen() { + return ( + + Home! + + ); +} +function SettingsScreen() { return ( - { - const newIndex = routes.findIndex((r) => r.key === route.key); - setIndex(newIndex); - }} - /> + + Settings! + ); -}; +} -export default MyComponent; +export default function MyComponent() { + const [index, setIndex] = React.useState(0); + + const routes = [ + { key: 'home', title: 'Home', focusedIcon: 'home' }, + { key: 'settings', title: 'Settings', focusedIcon: 'cog' }, + ]; + + const renderScene = ({ route }) => { + switch (route.key) { + case 'home': + return ; + case 'settings': + return ; + default: + return null; + } + }; + + return ( + + {renderScene({ route: routes[index] })} + { + const newIndex = routes.findIndex((r) => r.key === route.key); + if (newIndex !== -1) { + setIndex(newIndex); + } + }} + getLabelText={({ route }) => route.title} + /> + + ); +} ``` @@ -79,22 +118,6 @@ export default MyComponent;
-### onTabPress (required) - -
- - - -
- -### onTabLongPress - -
- - - -
- ### renderIcon
@@ -151,27 +174,35 @@ export default MyComponent;
-### activeColor +### onTabPress (required)
- +
-### inactiveColor +### onTabLongPress
- +
-### animationEasing +### activeColor
- + + +
+ +### inactiveColor + +
+ +
@@ -207,16 +238,32 @@ export default MyComponent;
+### activeIndicatorStyle + +
+ + + +
+ ### theme
+
+ +### testID + +
+ + + - \ No newline at end of file + diff --git a/docs/6.x/docs/components/NavigationBar/_meta.json b/docs/6.x/docs/components/NavigationBar/_meta.json index 3f9533bfe1..78af3034f5 100644 --- a/docs/6.x/docs/components/NavigationBar/_meta.json +++ b/docs/6.x/docs/components/NavigationBar/_meta.json @@ -1 +1,3 @@ -["NavigationBar"] \ No newline at end of file +[ + "NavigationBar" +] diff --git a/docs/scripts/generate-rspress-content.ts b/docs/scripts/generate-rspress-content.ts index 1c9accd93a..552c995b7f 100644 --- a/docs/scripts/generate-rspress-content.ts +++ b/docs/scripts/generate-rspress-content.ts @@ -128,8 +128,18 @@ const getVersionComponentOrder = (version: string): MetaEntry[] => { return fromConfig; } + const componentsDir = path.join(getVersionDocsDir(version), 'components'); + const filteredFromConfig = fromConfig.filter((entry) => { + const name = typeof entry === 'string' ? entry : entry.name; + return ( + fs.existsSync(path.join(componentsDir, name)) || + fs.existsSync(path.join(componentsDir, `${name}.mdx`)) || + fs.existsSync(path.join(componentsDir, `${name}.md`)) + ); + }); + return [ - ...fromConfig, + ...filteredFromConfig, { type: 'dir', name: 'HelperText', diff --git a/docs/src/data/componentDocs6x.json b/docs/src/data/componentDocs6x.json index 56b356912b..6794adf984 100644 --- a/docs/src/data/componentDocs6x.json +++ b/docs/src/data/componentDocs6x.json @@ -1263,10 +1263,10 @@ "BottomNavigation/BottomNavigation": { "filepath": "BottomNavigation/BottomNavigation.tsx", "title": "BottomNavigation", - "description": "BottomNavigation provides quick navigation between top-level views of an app with a bottom navigation bar.\nIt is primarily designed for use on mobile. If you want to use the navigation bar only see [`BottomNavigation.Bar`](BottomNavigationBar).\n\nBy default BottomNavigation uses primary color as a background, in dark theme with `adaptive` mode it will use surface colour instead.\nSee [Dark Theme](https://callstack.github.io/react-native-paper/docs/guides/theming#dark-theme) for more information.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { BottomNavigation, Text } from 'react-native-paper';\n\nconst MusicRoute = () => Music;\n\nconst AlbumsRoute = () => Albums;\n\nconst RecentsRoute = () => Recents;\n\nconst NotificationsRoute = () => Notifications;\n\nconst MyComponent = () => {\n const [index, setIndex] = React.useState(0);\n const [routes] = React.useState([\n { key: 'music', title: 'Favorites', focusedIcon: 'heart', unfocusedIcon: 'heart-outline'},\n { key: 'albums', title: 'Albums', focusedIcon: 'album' },\n { key: 'recents', title: 'Recents', focusedIcon: 'history' },\n { key: 'notifications', title: 'Notifications', focusedIcon: 'bell', unfocusedIcon: 'bell-outline' },\n ]);\n\n const renderScene = BottomNavigation.SceneMap({\n music: MusicRoute,\n albums: AlbumsRoute,\n recents: RecentsRoute,\n notifications: NotificationsRoute,\n });\n\n return (\n \n );\n};\n\nexport default MyComponent;\n```", + "description": "BottomNavigation provides quick navigation between top-level views of an app with a bottom navigation bar.\nIt is primarily designed for use on mobile. If you want to use the navigation bar only see [`NavigationBar`](../NavigationBar/NavigationBar).\n\nBy default BottomNavigation uses primary color as a background, in dark theme with `adaptive` mode it will use surface colour instead.\nSee [Dark Theme](https://callstack.github.io/react-native-paper/docs/guides/theming#dark-theme) for more information.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { BottomNavigation, Text } from 'react-native-paper';\n\nconst MusicRoute = () => Music;\n\nconst AlbumsRoute = () => Albums;\n\nconst RecentsRoute = () => Recents;\n\nconst NotificationsRoute = () => Notifications;\n\nconst MyComponent = () => {\n const [index, setIndex] = React.useState(0);\n const [routes] = React.useState([\n { key: 'music', title: 'Favorites', focusedIcon: 'heart', unfocusedIcon: 'heart-outline'},\n { key: 'albums', title: 'Albums', focusedIcon: 'album' },\n { key: 'recents', title: 'Recents', focusedIcon: 'history' },\n { key: 'notifications', title: 'Notifications', focusedIcon: 'bell', unfocusedIcon: 'bell-outline' },\n ]);\n\n const renderScene = BottomNavigation.SceneMap({\n music: MusicRoute,\n albums: AlbumsRoute,\n recents: RecentsRoute,\n notifications: NotificationsRoute,\n });\n\n return (\n \n );\n};\n\nexport default MyComponent;\n```", "link": "bottom-navigation", "data": { - "description": "BottomNavigation provides quick navigation between top-level views of an app with a bottom navigation bar.\nIt is primarily designed for use on mobile. If you want to use the navigation bar only see [`BottomNavigation.Bar`](BottomNavigationBar).\n\nBy default BottomNavigation uses primary color as a background, in dark theme with `adaptive` mode it will use surface colour instead.\nSee [Dark Theme](https://callstack.github.io/react-native-paper/docs/guides/theming#dark-theme) for more information.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { BottomNavigation, Text } from 'react-native-paper';\n\nconst MusicRoute = () => Music;\n\nconst AlbumsRoute = () => Albums;\n\nconst RecentsRoute = () => Recents;\n\nconst NotificationsRoute = () => Notifications;\n\nconst MyComponent = () => {\n const [index, setIndex] = React.useState(0);\n const [routes] = React.useState([\n { key: 'music', title: 'Favorites', focusedIcon: 'heart', unfocusedIcon: 'heart-outline'},\n { key: 'albums', title: 'Albums', focusedIcon: 'album' },\n { key: 'recents', title: 'Recents', focusedIcon: 'history' },\n { key: 'notifications', title: 'Notifications', focusedIcon: 'bell', unfocusedIcon: 'bell-outline' },\n ]);\n\n const renderScene = BottomNavigation.SceneMap({\n music: MusicRoute,\n albums: AlbumsRoute,\n recents: RecentsRoute,\n notifications: NotificationsRoute,\n });\n\n return (\n \n );\n};\n\nexport default MyComponent;\n```", + "description": "BottomNavigation provides quick navigation between top-level views of an app with a bottom navigation bar.\nIt is primarily designed for use on mobile. If you want to use the navigation bar only see [`NavigationBar`](../NavigationBar/NavigationBar).\n\nBy default BottomNavigation uses primary color as a background, in dark theme with `adaptive` mode it will use surface colour instead.\nSee [Dark Theme](https://callstack.github.io/react-native-paper/docs/guides/theming#dark-theme) for more information.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { BottomNavigation, Text } from 'react-native-paper';\n\nconst MusicRoute = () => Music;\n\nconst AlbumsRoute = () => Albums;\n\nconst RecentsRoute = () => Recents;\n\nconst NotificationsRoute = () => Notifications;\n\nconst MyComponent = () => {\n const [index, setIndex] = React.useState(0);\n const [routes] = React.useState([\n { key: 'music', title: 'Favorites', focusedIcon: 'heart', unfocusedIcon: 'heart-outline'},\n { key: 'albums', title: 'Albums', focusedIcon: 'album' },\n { key: 'recents', title: 'Recents', focusedIcon: 'history' },\n { key: 'notifications', title: 'Notifications', focusedIcon: 'bell', unfocusedIcon: 'bell-outline' },\n ]);\n\n const renderScene = BottomNavigation.SceneMap({\n music: MusicRoute,\n albums: AlbumsRoute,\n recents: RecentsRoute,\n notifications: NotificationsRoute,\n });\n\n return (\n \n );\n};\n\nexport default MyComponent;\n```", "displayName": "BottomNavigation", "methods": [ { @@ -1346,13 +1346,6 @@ ], "statics": [], "props": { - "shifting": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "Whether the shifting style is used, the active tab icon shifts up to show the label and the inactive tabs won't have a label.\n\nBy default, this is `false` with theme version 3 and `true` when you have more than 3 tabs.\nPass `shifting={false}` to explicitly disable this animation, or `shifting={true}` to always use this animation.\nNote that you need at least 2 tabs be able to run this animation." - }, "labeled": { "required": false, "tsType": { @@ -2051,7 +2044,7 @@ "tsType": { "name": "boolean" }, - "description": "Whether animation is enabled for scenes transitions in `shifting` mode.\nBy default, the scenes cross-fade during tab change when `shifting` is enabled.\nSpecify `sceneAnimationEnabled` as `false` to disable the animation.", + "description": "Whether animation is enabled for scene transitions.\nBy default, the scenes cross-fade during tab change.\nSpecify `sceneAnimationEnabled` as `false` to disable the animation.", "defaultValue": { "value": "false", "computed": false @@ -2228,512 +2221,425 @@ "src/components/BottomNavigation/BottomNavigation.tsx" ] }, - "BottomNavigation/BottomNavigationBar": { - "filepath": "BottomNavigation/BottomNavigationBar.tsx", - "title": "BottomNavigation.Bar", - "description": "A navigation bar which can easily be integrated with [React Navigation's Bottom Tabs Navigator](https://reactnavigation.org/docs/bottom-tab-navigator/).\n\n## Usage\n### without React Navigation\n```js\nimport React from 'react';\nimport { useState } from 'react';\nimport { View } from 'react-native';\nimport { BottomNavigation, Text, Provider } from 'react-native-paper';\nimport MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons';\n\nfunction HomeScreen() {\n return (\n \n Home!\n \n );\n}\n\nfunction SettingsScreen() {\n return (\n \n Settings!\n \n );\n}\n\nexport default function MyComponent() {\n const [index, setIndex] = useState(0);\n\n const routes = [\n { key: 'home', title: 'Home', icon: 'home' },\n { key: 'settings', title: 'Settings', icon: 'cog' },\n ];\n\n const renderScene = ({ route }) => {\n switch (route.key) {\n case 'home':\n return ;\n case 'settings':\n return ;\n default:\n return null;\n }\n };\n\n return (\n \n {renderScene({ route: routes[index] })}\n {\n const newIndex = routes.findIndex((r) => r.key === route.key);\n if (newIndex !== -1) {\n setIndex(newIndex);\n }\n }}\n renderIcon={({ route, color }) => (\n \n )}\n getLabelText={({ route }) => route.title}\n />\n \n );\n}\n```", - "link": "bottom-navigation-bar", + "Button/Button": { + "filepath": "Button/Button.tsx", + "title": "Button", + "description": "A button is component that the user can press to trigger an action.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Button } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n);\n\nexport default MyComponent;\n```", + "link": "button", "data": { - "description": "A navigation bar which can easily be integrated with [React Navigation's Bottom Tabs Navigator](https://reactnavigation.org/docs/bottom-tab-navigator/).\n\n## Usage\n### without React Navigation\n```js\nimport React from 'react';\nimport { useState } from 'react';\nimport { View } from 'react-native';\nimport { BottomNavigation, Text, Provider } from 'react-native-paper';\nimport MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons';\n\nfunction HomeScreen() {\n return (\n \n Home!\n \n );\n}\n\nfunction SettingsScreen() {\n return (\n \n Settings!\n \n );\n}\n\nexport default function MyComponent() {\n const [index, setIndex] = useState(0);\n\n const routes = [\n { key: 'home', title: 'Home', icon: 'home' },\n { key: 'settings', title: 'Settings', icon: 'cog' },\n ];\n\n const renderScene = ({ route }) => {\n switch (route.key) {\n case 'home':\n return ;\n case 'settings':\n return ;\n default:\n return null;\n }\n };\n\n return (\n \n {renderScene({ route: routes[index] })}\n {\n const newIndex = routes.findIndex((r) => r.key === route.key);\n if (newIndex !== -1) {\n setIndex(newIndex);\n }\n }}\n renderIcon={({ route, color }) => (\n \n )}\n getLabelText={({ route }) => route.title}\n />\n \n );\n}\n```", - "displayName": "BottomNavigation.Bar", + "description": "A button is component that the user can press to trigger an action.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Button } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n);\n\nexport default MyComponent;\n```", + "displayName": "Button", "methods": [], "statics": [], "props": { - "shifting": { + "mode": { "required": false, "tsType": { - "name": "boolean" + "name": "union", + "raw": "'text' | 'outlined' | 'contained' | 'elevated' | 'contained-tonal'", + "elements": [ + { + "name": "literal", + "value": "'text'" + }, + { + "name": "literal", + "value": "'outlined'" + }, + { + "name": "literal", + "value": "'contained'" + }, + { + "name": "literal", + "value": "'elevated'" + }, + { + "name": "literal", + "value": "'contained-tonal'" + } + ] }, - "description": "Whether the shifting style is used, the active tab icon shifts up to show the label and the inactive tabs won't have a label.\n\nBy default, this is `false` with theme version 3 and `true` when you have more than 3 tabs.\nPass `shifting={false}` to explicitly disable this animation, or `shifting={true}` to always use this animation.\nNote that you need at least 2 tabs be able to run this animation." + "description": "Mode of the button. You can change the mode to adjust the styling to give it desired emphasis.\n- `text` - flat button without background or outline, used for the lowest priority actions, especially when presenting multiple options.\n- `outlined` - button with an outline without background, typically used for important, but not primary action – represents medium emphasis.\n- `contained` - button with a background color, used for important action, have the most visual impact and high emphasis.\n- `elevated` - button with a background color and elevation, used when absolutely necessary e.g. button requires visual separation from a patterned background. @supported Available in v5.x with theme version 3\n- `contained-tonal` - button with a secondary background color, an alternative middle ground between contained and outlined buttons. @supported Available in v5.x with theme version 3", + "defaultValue": { + "value": "'text'", + "computed": false + } }, - "labeled": { + "dark": { "required": false, "tsType": { "name": "boolean" }, - "description": "Whether to show labels in tabs. When `false`, only icons will be displayed.", - "defaultValue": { - "value": "true", - "computed": false - } + "description": "Whether the color is a dark color. A dark button will render light text and vice-versa. Only applicable for:\n * `contained` mode for theme version 2\n * `contained`, `contained-tonal` and `elevated` modes for theme version 3." }, "compact": { "required": false, "tsType": { "name": "boolean" }, - "description": "Whether tabs should be spread across the entire width." + "description": "Use a compact look, useful for `text` buttons in a row." }, - "navigationState": { + "buttonColor": { + "required": false, + "tsType": { + "name": "ColorValue" + }, + "description": "Custom button's background color." + }, + "textColor": { + "required": false, + "tsType": { + "name": "ColorValue" + }, + "description": "Custom button's text color." + }, + "loading": { + "required": false, + "tsType": { + "name": "boolean" + }, + "description": "Whether to show a loading indicator." + }, + "icon": { + "required": false, + "tsType": { + "name": "IconSource" + }, + "description": "Icon to display for the `Button`." + }, + "disabled": { + "required": false, + "tsType": { + "name": "boolean" + }, + "description": "Whether the button is disabled. A disabled button is greyed out and `onPress` is not called on touch." + }, + "children": { "required": true, + "tsType": { + "name": "ReactReactNode", + "raw": "React.ReactNode" + }, + "description": "Label text of the button." + }, + "uppercase": { + "required": false, + "tsType": { + "name": "boolean" + }, + "description": "Make the label text uppercased. Note that this won't work if you pass React elements as children." + }, + "background": { + "required": false, + "tsType": { + "name": "PressableAndroidRippleConfig" + }, + "description": "Type of background drawabale to display the feedback (Android).\nhttps://reactnative.dev/docs/pressable#rippleconfig" + }, + "aria-label": { + "required": false, + "tsType": { + "name": "string" + }, + "description": "Accessibility label for the button. This is read by the screen reader when the user taps the button." + }, + "accessibilityHint": { + "required": false, + "tsType": { + "name": "string" + }, + "description": "Accessibility hint for the button. This is read by the screen reader when the user taps the button." + }, + "role": { + "required": false, + "tsType": { + "name": "Role" + }, + "description": "Accessibility role for the button. The \"button\" role is set by default.", + "defaultValue": { + "value": "'button'", + "computed": false + } + }, + "onPress": { + "required": false, "tsType": { "name": "signature", - "type": "object", - "raw": "{\n index: number;\n routes: Route[];\n}", + "type": "function", + "raw": "(e: GestureResponderEvent) => void", "signature": { - "properties": [ - { - "key": "index", - "value": { - "name": "number", - "required": true - } - }, + "arguments": [ { - "key": "routes", - "value": { - "name": "Array", - "elements": [ - { - "name": "Route" - } - ], - "raw": "Route[]", - "required": true + "name": "e", + "type": { + "name": "GestureResponderEvent" } } - ] + ], + "return": { + "name": "void" + } } }, - "description": "State for the bottom navigation. The state should contain the following properties:\n\n- `index`: a number representing the index of the active route in the `routes` array\n- `routes`: an array containing a list of route objects used for rendering the tabs\n\nEach route object should contain the following properties:\n\n- `key`: a unique key to identify the route (required)\n- `title`: title of the route to use as the tab label\n- `focusedIcon`: icon to use as the focused tab icon, can be a string, an image source or a react component @renamed Renamed from 'icon' to 'focusedIcon' in v5.x\n- `unfocusedIcon`: icon to use as the unfocused tab icon, can be a string, an image source or a react component @supported Available in v5.x with theme version 3\n- `badge`: badge to show on the tab icon, can be `true` to show a dot, `string` or `number` to show text.\n- `aria-label`: accessibility label for the tab button\n- `testID`: test id for the tab button\n\nExample:\n\n```js\n{\n index: 1,\n routes: [\n { key: 'music', title: 'Favorites', focusedIcon: 'heart', unfocusedIcon: 'heart-outline'},\n { key: 'albums', title: 'Albums', focusedIcon: 'album' },\n { key: 'recents', title: 'Recents', focusedIcon: 'history' },\n { key: 'notifications', title: 'Notifications', focusedIcon: 'bell', unfocusedIcon: 'bell-outline' },\n ]\n}\n```\n\n`BottomNavigation.Bar` is a controlled component, which means the `index` needs to be updated via the `onTabPress` callback." + "description": "Function to execute on press." }, - "renderIcon": { + "onPressIn": { "required": false, "tsType": { "name": "signature", "type": "function", - "raw": "(props: {\n route: Route;\n focused: boolean;\n color: ColorValue;\n}) => React.ReactNode", + "raw": "(e: GestureResponderEvent) => void", "signature": { "arguments": [ { - "name": "props", + "name": "e", "type": { - "name": "signature", - "type": "object", - "raw": "{\n route: Route;\n focused: boolean;\n color: ColorValue;\n}", - "signature": { - "properties": [ - { - "key": "route", - "value": { - "name": "Route", - "required": true - } - }, - { - "key": "focused", - "value": { - "name": "boolean", - "required": true - } - }, - { - "key": "color", - "value": { - "name": "ColorValue", - "required": true - } - } - ] - } + "name": "GestureResponderEvent" } } ], "return": { - "name": "ReactReactNode", - "raw": "React.ReactNode" + "name": "void" } } }, - "description": "Callback which returns a React Element to be used as tab icon." + "description": "Function to execute as soon as the touchable element is pressed and invoked even before onPress." }, - "renderLabel": { + "onPressOut": { "required": false, "tsType": { "name": "signature", "type": "function", - "raw": "(props: {\n route: Route;\n focused: boolean;\n color: ColorValue;\n}) => React.ReactNode", + "raw": "(e: GestureResponderEvent) => void", "signature": { "arguments": [ { - "name": "props", + "name": "e", "type": { - "name": "signature", - "type": "object", - "raw": "{\n route: Route;\n focused: boolean;\n color: ColorValue;\n}", - "signature": { - "properties": [ - { - "key": "route", - "value": { - "name": "Route", - "required": true - } - }, - { - "key": "focused", - "value": { - "name": "boolean", - "required": true - } - }, - { - "key": "color", - "value": { - "name": "ColorValue", - "required": true - } - } - ] - } + "name": "GestureResponderEvent" } } ], "return": { - "name": "ReactReactNode", - "raw": "React.ReactNode" + "name": "void" } } }, - "description": "Callback which React Element to be used as tab label." + "description": "Function to execute as soon as the touch is released even before onPress." }, - "renderTouchable": { + "onLongPress": { "required": false, "tsType": { "name": "signature", "type": "function", - "raw": "(props: TouchableProps) => React.ReactNode", + "raw": "(e: GestureResponderEvent) => void", "signature": { "arguments": [ { - "name": "props", + "name": "e", "type": { - "name": "intersection", - "raw": "TouchableRippleProps & {\n key: string;\n route: Route;\n children: React.ReactNode;\n borderless?: boolean;\n centered?: boolean;\n rippleColor?: ColorValue;\n}", - "elements": [ - { - "name": "TouchableRippleProps" - }, - { - "name": "signature", - "type": "object", - "raw": "{\n key: string;\n route: Route;\n children: React.ReactNode;\n borderless?: boolean;\n centered?: boolean;\n rippleColor?: ColorValue;\n}", - "signature": { - "properties": [ - { - "key": "key", - "value": { - "name": "string", - "required": true - } - }, - { - "key": "route", - "value": { - "name": "Route", - "required": true - } - }, - { - "key": "children", - "value": { - "name": "ReactReactNode", - "raw": "React.ReactNode", - "required": true - } - }, - { - "key": "borderless", - "value": { - "name": "boolean", - "required": false - } - }, - { - "key": "centered", - "value": { - "name": "boolean", - "required": false - } - }, - { - "key": "rippleColor", - "value": { - "name": "ColorValue", - "required": false - } - } - ] - } - } - ] + "name": "GestureResponderEvent" } } ], "return": { - "name": "ReactReactNode", - "raw": "React.ReactNode" + "name": "void" } } }, - "description": "Callback which returns a React element to be used as the touchable for the tab item.\nRenders a `TouchableRipple` on Android and `Pressable` on iOS.", - "defaultValue": { - "value": "({ key, ...props }: TouchableProps) => (\n \n)", - "computed": false - } + "description": "Function to execute on long press." }, - "getAccessibilityLabel": { + "delayLongPress": { "required": false, "tsType": { - "name": "signature", - "type": "function", - "raw": "(props: { route: Route }) => string | undefined", - "signature": { - "arguments": [ - { - "name": "props", - "type": { - "name": "signature", - "type": "object", - "raw": "{ route: Route }", - "signature": { - "properties": [ - { - "key": "route", - "value": { - "name": "Route", - "required": true - } - } - ] - } - } - } - ], - "return": { - "name": "union", - "raw": "string | undefined", - "elements": [ - { - "name": "string" - }, - { - "name": "undefined" - } - ] + "name": "number" + }, + "description": "The number of milliseconds a user must touch the element before executing `onLongPress`." + }, + "contentStyle": { + "required": false, + "tsType": { + "name": "StyleProp", + "elements": [ + { + "name": "ViewStyle" } - } + ], + "raw": "StyleProp" }, - "description": "Get accessibility label for the tab button. This is read by the screen reader when the user taps the tab.\nUses `route['aria-label']` by default.", - "defaultValue": { - "value": "({ route }: { route: Route }) => route['aria-label']", - "computed": false - } + "description": "Style of button's inner content.\nUse this prop to apply custom height and width, to set a custom padding or to set the icon on the right with `flexDirection: 'row-reverse'`." }, - "getBadge": { + "maxFontSizeMultiplier": { "required": false, "tsType": { - "name": "signature", - "type": "function", - "raw": "(props: { route: Route }) => boolean | number | string | undefined", - "signature": { - "arguments": [ - { - "name": "props", - "type": { - "name": "signature", - "type": "object", - "raw": "{ route: Route }", - "signature": { - "properties": [ - { - "key": "route", - "value": { - "name": "Route", - "required": true - } - } - ] - } - } - } - ], - "return": { - "name": "union", - "raw": "boolean | number | string | undefined", + "name": "number" + }, + "description": "Specifies the largest possible scale a text font can reach." + }, + "hitSlop": { + "required": false, + "tsType": { + "name": "TouchableRippleProps['hitSlop']", + "raw": "TouchableRippleProps['hitSlop']" + }, + "description": "Sets additional distance outside of element in which a press can be detected." + }, + "style": { + "required": false, + "tsType": { + "name": "Animated.WithAnimatedValue", + "elements": [ + { + "name": "StyleProp", "elements": [ { - "name": "boolean" - }, - { - "name": "number" - }, - { - "name": "string" - }, - { - "name": "undefined" + "name": "ViewStyle" } - ] + ], + "raw": "StyleProp" } - } + ], + "raw": "Animated.WithAnimatedValue>" }, - "description": "Get badge for the tab, uses `route.badge` by default.", - "defaultValue": { - "value": "({ route }: { route: Route }) => route.badge", - "computed": false - } + "description": "" }, - "getLabelText": { + "labelStyle": { "required": false, "tsType": { - "name": "signature", - "type": "function", - "raw": "(props: { route: Route }) => string | undefined", - "signature": { - "arguments": [ - { - "name": "props", - "type": { - "name": "signature", - "type": "object", - "raw": "{ route: Route }", - "signature": { - "properties": [ - { - "key": "route", - "value": { - "name": "Route", - "required": true - } - } - ] - } - } - } - ], - "return": { - "name": "union", - "raw": "string | undefined", - "elements": [ - { - "name": "string" - }, - { - "name": "undefined" - } - ] + "name": "StyleProp", + "elements": [ + { + "name": "TextStyle" } - } + ], + "raw": "StyleProp" }, - "description": "Get label text for the tab, uses `route.title` by default. Use `renderLabel` to replace label component.", - "defaultValue": { - "value": "({ route }: { route: Route }) => route.title", - "computed": false - } + "description": "Style for the button text." }, - "getTestID": { + "theme": { "required": false, "tsType": { - "name": "signature", - "type": "function", - "raw": "(props: { route: Route }) => string | undefined", - "signature": { - "arguments": [ - { - "name": "props", - "type": { - "name": "signature", - "type": "object", - "raw": "{ route: Route }", - "signature": { - "properties": [ - { - "key": "route", - "value": { - "name": "Route", - "required": true - } - } - ] - } - } - } - ], - "return": { - "name": "union", - "raw": "string | undefined", - "elements": [ - { - "name": "string" - }, - { - "name": "undefined" - } - ] + "name": "ThemeProp" + }, + "description": "" + }, + "touchableRef": { + "required": false, + "tsType": { + "name": "ReactRefObject", + "raw": "React.RefObject", + "elements": [ + { + "name": "View" } - } + ] }, - "description": "Get the id to locate this tab button in tests, uses `route.testID` by default.", + "description": "Reference for the touchable" + }, + "ref": { + "required": false, + "tsType": { + "name": "ReactRef", + "raw": "React.Ref", + "elements": [ + { + "name": "View" + } + ] + }, + "description": "" + }, + "testID": { + "required": false, + "tsType": { + "name": "string" + }, + "description": "testID to be used on tests.", "defaultValue": { - "value": "({ route }: { route: Route }) => route.testID", + "value": "'button'", + "computed": false + } + } + } + }, + "type": "component", + "dependencies": [ + "src/components/Button/Button.tsx" + ] + }, + "Card/Card": { + "filepath": "Card/Card.tsx", + "title": "Card", + "description": "A card is a sheet of material that serves as an entry point to more detailed information.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Avatar, Button, Card, Text } from 'react-native-paper';\n\nconst LeftContent = props => \n\nconst MyComponent = () => (\n \n \n \n Card title\n Card content\n \n \n \n \n \n \n \n);\n\nexport default MyComponent;\n```", + "link": "card", + "data": { + "description": "A card is a sheet of material that serves as an entry point to more detailed information.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Avatar, Button, Card, Text } from 'react-native-paper';\n\nconst LeftContent = props => \n\nconst MyComponent = () => (\n \n \n \n Card title\n Card content\n \n \n \n \n \n \n \n);\n\nexport default MyComponent;\n```", + "displayName": "Card", + "methods": [], + "statics": [], + "props": { + "mode": { + "required": false, + "tsType": { + "name": "union", + "raw": "'elevated' | 'outlined' | 'contained'", + "elements": [ + { + "name": "literal", + "value": "'elevated'" + }, + { + "name": "literal", + "value": "'outlined'" + }, + { + "name": "literal", + "value": "'contained'" + } + ] + }, + "description": "Mode of the Card.\n- `elevated` - Card with elevation.\n- `contained` - Card without outline and elevation @supported Available in v5.x with theme version 3\n- `outlined` - Card with an outline.", + "defaultValue": { + "value": "'elevated'", "computed": false } }, - "onTabPress": { + "children": { "required": true, + "tsType": { + "name": "ReactReactNode", + "raw": "React.ReactNode" + }, + "description": "Content of the `Card`." + }, + "onLongPress": { + "required": false, "tsType": { "name": "signature", "type": "function", - "raw": "(props: { route: Route } & TabPressEvent) => void", + "raw": "() => void", + "signature": { + "arguments": [], + "return": { + "name": "void" + } + } + }, + "description": "Function to execute on long press." + }, + "onPress": { + "required": false, + "tsType": { + "name": "signature", + "type": "function", + "raw": "(e: GestureResponderEvent) => void", "signature": { "arguments": [ { - "name": "props", + "name": "e", "type": { - "name": "intersection", - "raw": "{ route: Route } & TabPressEvent", - "elements": [ - { - "name": "signature", - "type": "object", - "raw": "{ route: Route }", - "signature": { - "properties": [ - { - "key": "route", - "value": { - "name": "Route", - "required": true - } - } - ] - } - }, - { - "name": "signature", - "type": "object", - "raw": "{\n defaultPrevented: boolean;\n preventDefault(): void;\n}", - "signature": { - "properties": [ - { - "key": "defaultPrevented", - "value": { - "name": "boolean", - "required": true - } - }, - { - "key": "preventDefault", - "value": { - "name": "void", - "required": true - } - } - ] - } - } - ] + "name": "GestureResponderEvent" } } ], @@ -2742,62 +2648,20 @@ } } }, - "description": "Function to execute on tab press. It receives the route for the pressed tab. Use this to update the navigation state." + "description": "Function to execute on press." }, - "onTabLongPress": { + "onPressIn": { "required": false, "tsType": { "name": "signature", "type": "function", - "raw": "(props: { route: Route } & TabPressEvent) => void", + "raw": "(e: GestureResponderEvent) => void", "signature": { "arguments": [ { - "name": "props", + "name": "e", "type": { - "name": "intersection", - "raw": "{ route: Route } & TabPressEvent", - "elements": [ - { - "name": "signature", - "type": "object", - "raw": "{ route: Route }", - "signature": { - "properties": [ - { - "key": "route", - "value": { - "name": "Route", - "required": true - } - } - ] - } - }, - { - "name": "signature", - "type": "object", - "raw": "{\n defaultPrevented: boolean;\n preventDefault(): void;\n}", - "signature": { - "properties": [ - { - "key": "defaultPrevented", - "value": { - "name": "boolean", - "required": true - } - }, - { - "key": "preventDefault", - "value": { - "name": "void", - "required": true - } - } - ] - } - } - ] + "name": "GestureResponderEvent" } } ], @@ -2806,101 +2670,98 @@ } } }, - "description": "Function to execute on tab long press. It receives the route for the pressed tab" - }, - "activeColor": { - "required": false, - "tsType": { - "name": "string" - }, - "description": "Custom color for icon and label in the active tab." - }, - "inactiveColor": { - "required": false, - "tsType": { - "name": "string" - }, - "description": "Custom color for icon and label in the inactive tab." + "description": "Function to execute as soon as the touchable element is pressed and invoked even before onPress." }, - "animationEasing": { + "onPressOut": { "required": false, "tsType": { - "name": "union", - "raw": "EasingFunction | undefined", - "elements": [ - { - "name": "EasingFunction" - }, - { - "name": "undefined" + "name": "signature", + "type": "function", + "raw": "(e: GestureResponderEvent) => void", + "signature": { + "arguments": [ + { + "name": "e", + "type": { + "name": "GestureResponderEvent" + } + } + ], + "return": { + "name": "void" } - ] + } }, - "description": "The scene animation Easing." + "description": "Function to execute as soon as the touch is released even before onPress." }, - "keyboardHidesNavigationBar": { + "delayLongPress": { "required": false, "tsType": { - "name": "boolean" + "name": "number" }, - "description": "Whether the bottom navigation bar is hidden when keyboard is shown.\nOn Android, this works best when [`windowSoftInputMode`](https://developer.android.com/guide/topics/manifest/activity-element#wsoft) is set to `adjustResize`.", - "defaultValue": { - "value": "Platform.OS === 'android'", - "computed": false - } + "description": "The number of milliseconds a user must touch the element before executing `onLongPress`." }, - "safeAreaInsets": { + "disabled": { "required": false, "tsType": { - "name": "signature", - "type": "object", - "raw": "{\n top?: number;\n right?: number;\n bottom?: number;\n left?: number;\n}", - "signature": { - "properties": [ - { - "key": "top", - "value": { - "name": "number", - "required": false - } - }, - { - "key": "right", - "value": { - "name": "number", - "required": false - } - }, - { - "key": "bottom", - "value": { - "name": "number", - "required": false - } - }, - { - "key": "left", - "value": { - "name": "number", - "required": false - } - } - ] - } + "name": "boolean" }, - "description": "Safe area insets for the tab bar. This can be used to avoid elements like the navigation bar on Android and bottom safe area on iOS.\nThe bottom insets for iOS is added by default. You can override the behavior with this option." + "description": "If true, disable all interactions for this component." }, - "labelMaxFontSizeMultiplier": { + "elevation": { "required": false, "tsType": { - "name": "number" + "name": "union", + "raw": "0 | 1 | 2 | 3 | 4 | 5 | Animated.Value", + "elements": [ + { + "name": "literal", + "value": "0" + }, + { + "name": "literal", + "value": "1" + }, + { + "name": "literal", + "value": "2" + }, + { + "name": "literal", + "value": "3" + }, + { + "name": "literal", + "value": "4" + }, + { + "name": "literal", + "value": "5" + }, + { + "name": "Animated.Value" + } + ] }, - "description": "Specifies the largest possible scale a label font can reach.", + "description": "Changes Card shadow and background on iOS and Android.", "defaultValue": { "value": "1", "computed": false } }, + "contentStyle": { + "required": false, + "tsType": { + "name": "StyleProp", + "elements": [ + { + "name": "ViewStyle" + } + ], + "raw": "StyleProp" + }, + "description": "Style of card's inner content." + }, "style": { "required": false, "tsType": { @@ -2920,19 +2781,6 @@ }, "description": "" }, - "activeIndicatorStyle": { - "required": false, - "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" - }, - "description": "" - }, "theme": { "required": false, "tsType": { @@ -2945,346 +2793,531 @@ "tsType": { "name": "string" }, - "description": "TestID used for testing purposes", + "description": "Pass down testID from card props to touchable", "defaultValue": { - "value": "'bottom-navigation-bar'", + "value": "'card'", "computed": false } + }, + "accessible": { + "required": false, + "tsType": { + "name": "boolean" + }, + "description": "Pass down accessible from card props to touchable" } } }, "type": "component", "dependencies": [ - "src/components/BottomNavigation/BottomNavigationBar.tsx" - ], - "group": "BottomNavigation" + "src/components/Card/Card.tsx" + ] }, - "Button/Button": { - "filepath": "Button/Button.tsx", - "title": "Button", - "description": "A button is component that the user can press to trigger an action.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Button } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n);\n\nexport default MyComponent;\n```", - "link": "button", + "Card/CardActions": { + "filepath": "Card/CardActions.tsx", + "title": "Card.Actions", + "description": "A component to show a list of actions inside a Card.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Card, Button } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n \n \n \n);\n\nexport default MyComponent;\n```", + "link": "card-actions", "data": { - "description": "A button is component that the user can press to trigger an action.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Button } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n);\n\nexport default MyComponent;\n```", - "displayName": "Button", + "description": "A component to show a list of actions inside a Card.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Card, Button } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n \n \n \n);\n\nexport default MyComponent;\n```", + "displayName": "Card.Actions", "methods": [], "statics": [], "props": { - "mode": { + "children": { + "required": true, + "tsType": { + "name": "ReactReactNode", + "raw": "React.ReactNode" + }, + "description": "Items inside the `CardActions`." + }, + "style": { "required": false, "tsType": { - "name": "union", - "raw": "'text' | 'outlined' | 'contained' | 'elevated' | 'contained-tonal'", + "name": "StyleProp", "elements": [ { - "name": "literal", - "value": "'text'" - }, - { - "name": "literal", - "value": "'outlined'" - }, - { - "name": "literal", - "value": "'contained'" - }, - { - "name": "literal", - "value": "'elevated'" - }, - { - "name": "literal", - "value": "'contained-tonal'" + "name": "ViewStyle" } - ] + ], + "raw": "StyleProp" }, - "description": "Mode of the button. You can change the mode to adjust the styling to give it desired emphasis.\n- `text` - flat button without background or outline, used for the lowest priority actions, especially when presenting multiple options.\n- `outlined` - button with an outline without background, typically used for important, but not primary action – represents medium emphasis.\n- `contained` - button with a background color, used for important action, have the most visual impact and high emphasis.\n- `elevated` - button with a background color and elevation, used when absolutely necessary e.g. button requires visual separation from a patterned background. @supported Available in v5.x with theme version 3\n- `contained-tonal` - button with a secondary background color, an alternative middle ground between contained and outlined buttons. @supported Available in v5.x with theme version 3", - "defaultValue": { - "value": "'text'", - "computed": false - } + "description": "" }, - "dark": { + "theme": { "required": false, "tsType": { - "name": "boolean" + "name": "ThemeProp" }, - "description": "Whether the color is a dark color. A dark button will render light text and vice-versa. Only applicable for:\n * `contained` mode for theme version 2\n * `contained`, `contained-tonal` and `elevated` modes for theme version 3." - }, - "compact": { - "required": false, + "description": "" + } + } + }, + "type": "component", + "dependencies": [ + "src/components/Card/CardActions.tsx" + ], + "group": "Card" + }, + "Card/CardContent": { + "filepath": "Card/CardContent.tsx", + "title": "Card.Content", + "description": "A component to show content inside a Card.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Card, Text } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n Card title\n Card content\n \n \n);\n\nexport default MyComponent;\n```", + "link": "card-content", + "data": { + "description": "A component to show content inside a Card.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Card, Text } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n Card title\n Card content\n \n \n);\n\nexport default MyComponent;\n```", + "displayName": "Card.Content", + "methods": [], + "statics": [], + "props": { + "children": { + "required": true, "tsType": { - "name": "boolean" + "name": "ReactReactNode", + "raw": "React.ReactNode" }, - "description": "Use a compact look, useful for `text` buttons in a row." + "description": "Items inside the `Card.Content`." }, - "buttonColor": { + "index": { "required": false, "tsType": { - "name": "ColorValue" + "name": "number" }, - "description": "Custom button's background color." + "description": "@internal" }, - "textColor": { + "total": { "required": false, "tsType": { - "name": "ColorValue" + "name": "number" }, - "description": "Custom button's text color." + "description": "@internal" }, - "loading": { + "siblings": { "required": false, "tsType": { - "name": "boolean" - }, - "description": "Whether to show a loading indicator." + "name": "Array", + "elements": [ + { + "name": "string" + } + ], + "raw": "Array" + }, + "description": "@internal" }, - "icon": { + "style": { "required": false, "tsType": { - "name": "IconSource" + "name": "StyleProp", + "elements": [ + { + "name": "ViewStyle" + } + ], + "raw": "StyleProp" }, - "description": "Icon to display for the `Button`." + "description": "" + } + } + }, + "type": "component", + "dependencies": [ + "src/components/Card/CardContent.tsx" + ], + "group": "Card" + }, + "Card/CardCover": { + "filepath": "Card/CardCover.tsx", + "title": "Card.Cover", + "description": "A component to show a cover image inside a Card.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Card } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n);\n\nexport default MyComponent;\n```\n\n@extends Image props https://reactnative.dev/docs/image#props", + "link": "card-cover", + "data": { + "description": "A component to show a cover image inside a Card.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Card } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n);\n\nexport default MyComponent;\n```\n\n@extends Image props https://reactnative.dev/docs/image#props", + "displayName": "Card.Cover", + "methods": [], + "statics": [], + "props": { + "index": { + "required": false, + "tsType": { + "name": "number" + }, + "description": "@internal" }, - "disabled": { + "total": { "required": false, "tsType": { - "name": "boolean" + "name": "number" }, - "description": "Whether the button is disabled. A disabled button is greyed out and `onPress` is not called on touch." + "description": "@internal" }, - "children": { + "style": { + "required": false, + "tsType": { + "name": "StyleProp", + "elements": [ + { + "name": "ViewStyle" + } + ], + "raw": "StyleProp" + }, + "description": "" + }, + "theme": { + "required": false, + "tsType": { + "name": "ThemeProp" + }, + "description": "" + } + } + }, + "type": "component", + "dependencies": [ + "src/components/Card/CardCover.tsx" + ], + "group": "Card" + }, + "Card/CardTitle": { + "filepath": "Card/CardTitle.tsx", + "title": "Card.Title", + "description": "A component to show a title, subtitle and an avatar inside a Card.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Avatar, Card, IconButton } from 'react-native-paper';\n\nconst MyComponent = () => (\n }\n right={(props) => {}} />}\n />\n);\n\nexport default MyComponent;\n```", + "link": "card-title", + "data": { + "description": "A component to show a title, subtitle and an avatar inside a Card.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Avatar, Card, IconButton } from 'react-native-paper';\n\nconst MyComponent = () => (\n }\n right={(props) => {}} />}\n />\n);\n\nexport default MyComponent;\n```", + "displayName": "Card.Title", + "methods": [], + "statics": [], + "props": { + "title": { "required": true, "tsType": { "name": "ReactReactNode", "raw": "React.ReactNode" }, - "description": "Label text of the button." + "description": "Text for the title. Note that this will only accept a string or ``-based node." }, - "uppercase": { + "titleStyle": { "required": false, "tsType": { - "name": "boolean" + "name": "StyleProp", + "elements": [ + { + "name": "TextStyle" + } + ], + "raw": "StyleProp" }, - "description": "Make the label text uppercased. Note that this won't work if you pass React elements as children." + "description": "Style for the title." }, - "background": { + "titleNumberOfLines": { "required": false, "tsType": { - "name": "PressableAndroidRippleConfig" + "name": "number" }, - "description": "Type of background drawabale to display the feedback (Android).\nhttps://reactnative.dev/docs/pressable#rippleconfig" + "description": "Number of lines for the title.", + "defaultValue": { + "value": "1", + "computed": false + } }, - "aria-label": { + "titleVariant": { "required": false, "tsType": { - "name": "string" + "name": "TypescaleKey" }, - "description": "Accessibility label for the button. This is read by the screen reader when the user taps the button." + "description": "@supported Available in v5.x with theme version 3\n\nTitle text variant defines appropriate text styles for type role and its size.\nAvailable variants:\n\n Display: `displayLarge`, `displayMedium`, `displaySmall`\n\n Headline: `headlineLarge`, `headlineMedium`, `headlineSmall`\n\n Title: `titleLarge`, `titleMedium`, `titleSmall`\n\n Label: `labelLarge`, `labelMedium`, `labelSmall`\n\n Body: `bodyLarge`, `bodyMedium`, `bodySmall`", + "defaultValue": { + "value": "'bodyLarge'", + "computed": false + } }, - "accessibilityHint": { + "subtitle": { "required": false, "tsType": { - "name": "string" + "name": "ReactReactNode", + "raw": "React.ReactNode" }, - "description": "Accessibility hint for the button. This is read by the screen reader when the user taps the button." + "description": "Text for the subtitle. Note that this will only accept a string or ``-based node." }, - "role": { + "subtitleStyle": { "required": false, "tsType": { - "name": "Role" + "name": "StyleProp", + "elements": [ + { + "name": "TextStyle" + } + ], + "raw": "StyleProp" }, - "description": "Accessibility role for the button. The \"button\" role is set by default.", + "description": "Style for the subtitle." + }, + "subtitleNumberOfLines": { + "required": false, + "tsType": { + "name": "number" + }, + "description": "Number of lines for the subtitle.", "defaultValue": { - "value": "'button'", + "value": "1", "computed": false } }, - "onPress": { + "subtitleVariant": { + "required": false, + "tsType": { + "name": "TypescaleKey" + }, + "description": "@supported Available in v5.x with theme version 3\n\nSubtitle text variant defines appropriate text styles for type role and its size.\nAvailable variants:\n\n Display: `displayLarge`, `displayMedium`, `displaySmall`\n\n Headline: `headlineLarge`, `headlineMedium`, `headlineSmall`\n\n Title: `titleLarge`, `titleMedium`, `titleSmall`\n\n Label: `labelLarge`, `labelMedium`, `labelSmall`\n\n Body: `bodyLarge`, `bodyMedium`, `bodySmall`", + "defaultValue": { + "value": "'bodyMedium'", + "computed": false + } + }, + "left": { "required": false, "tsType": { "name": "signature", "type": "function", - "raw": "(e: GestureResponderEvent) => void", + "raw": "(props: { size: number }) => React.ReactNode", "signature": { "arguments": [ { - "name": "e", + "name": "props", "type": { - "name": "GestureResponderEvent" + "name": "signature", + "type": "object", + "raw": "{ size: number }", + "signature": { + "properties": [ + { + "key": "size", + "value": { + "name": "number", + "required": true + } + } + ] + } } } ], "return": { - "name": "void" + "name": "ReactReactNode", + "raw": "React.ReactNode" } } }, - "description": "Function to execute on press." + "description": "Callback which returns a React element to display on the left side." }, - "onPressIn": { + "leftStyle": { "required": false, "tsType": { - "name": "signature", - "type": "function", - "raw": "(e: GestureResponderEvent) => void", - "signature": { - "arguments": [ - { - "name": "e", - "type": { - "name": "GestureResponderEvent" - } - } - ], - "return": { - "name": "void" + "name": "StyleProp", + "elements": [ + { + "name": "ViewStyle" } - } + ], + "raw": "StyleProp" }, - "description": "Function to execute as soon as the touchable element is pressed and invoked even before onPress." + "description": "Style for the left element wrapper." }, - "onPressOut": { + "right": { "required": false, "tsType": { "name": "signature", "type": "function", - "raw": "(e: GestureResponderEvent) => void", + "raw": "(props: { size: number }) => React.ReactNode", "signature": { "arguments": [ { - "name": "e", + "name": "props", "type": { - "name": "GestureResponderEvent" + "name": "signature", + "type": "object", + "raw": "{ size: number }", + "signature": { + "properties": [ + { + "key": "size", + "value": { + "name": "number", + "required": true + } + } + ] + } } } ], "return": { - "name": "void" + "name": "ReactReactNode", + "raw": "React.ReactNode" } } }, - "description": "Function to execute as soon as the touch is released even before onPress." + "description": "Callback which returns a React element to display on the right side." }, - "onLongPress": { + "rightStyle": { "required": false, "tsType": { - "name": "signature", - "type": "function", - "raw": "(e: GestureResponderEvent) => void", - "signature": { - "arguments": [ - { - "name": "e", - "type": { - "name": "GestureResponderEvent" - } - } - ], - "return": { - "name": "void" + "name": "StyleProp", + "elements": [ + { + "name": "ViewStyle" } - } + ], + "raw": "StyleProp" }, - "description": "Function to execute on long press." + "description": "Style for the right element wrapper." }, - "delayLongPress": { + "index": { "required": false, "tsType": { "name": "number" }, - "description": "The number of milliseconds a user must touch the element before executing `onLongPress`." + "description": "@internal" }, - "contentStyle": { + "total": { "required": false, "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" + "name": "number" }, - "description": "Style of button's inner content.\nUse this prop to apply custom height and width, to set a custom padding or to set the icon on the right with `flexDirection: 'row-reverse'`." + "description": "@internal" }, - "maxFontSizeMultiplier": { + "titleMaxFontSizeMultiplier": { "required": false, "tsType": { "name": "number" }, - "description": "Specifies the largest possible scale a text font can reach." + "description": "Specifies the largest possible scale a title font can reach." }, - "hitSlop": { + "subtitleMaxFontSizeMultiplier": { "required": false, "tsType": { - "name": "TouchableRippleProps['hitSlop']", - "raw": "TouchableRippleProps['hitSlop']" + "name": "number" }, - "description": "Sets additional distance outside of element in which a press can be detected." + "description": "Specifies the largest possible scale a subtitle font can reach." }, "style": { "required": false, "tsType": { - "name": "Animated.WithAnimatedValue", + "name": "StyleProp", "elements": [ { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" + "name": "ViewStyle" } ], - "raw": "Animated.WithAnimatedValue>" + "raw": "StyleProp" }, "description": "" }, - "labelStyle": { + "theme": { "required": false, "tsType": { - "name": "StyleProp", + "name": "ThemeProp" + }, + "description": "" + } + } + }, + "type": "component", + "dependencies": [ + "src/components/Card/CardTitle.tsx" + ], + "group": "Card" + }, + "Checkbox/Checkbox": { + "filepath": "Checkbox/Checkbox.tsx", + "title": "Checkbox", + "description": "Checkboxes allow the selection of multiple options from a set.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Checkbox } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [checked, setChecked] = React.useState(false);\n\n return (\n {\n setChecked(!checked);\n }}\n />\n );\n};\n\nexport default MyComponent;\n```", + "link": "checkbox", + "data": { + "description": "Checkboxes allow the selection of multiple options from a set.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Checkbox } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [checked, setChecked] = React.useState(false);\n\n return (\n {\n setChecked(!checked);\n }}\n />\n );\n};\n\nexport default MyComponent;\n```", + "displayName": "Checkbox", + "methods": [], + "statics": [], + "props": { + "status": { + "required": true, + "tsType": { + "name": "union", + "raw": "'checked' | 'unchecked' | 'indeterminate'", "elements": [ { - "name": "TextStyle" + "name": "literal", + "value": "'checked'" + }, + { + "name": "literal", + "value": "'unchecked'" + }, + { + "name": "literal", + "value": "'indeterminate'" } - ], - "raw": "StyleProp" + ] }, - "description": "Style for the button text." + "description": "Status of checkbox." }, - "theme": { + "disabled": { "required": false, "tsType": { - "name": "ThemeProp" + "name": "boolean" }, - "description": "" + "description": "Whether checkbox is disabled." }, - "touchableRef": { + "onPress": { "required": false, "tsType": { - "name": "ReactRefObject", - "raw": "React.RefObject", - "elements": [ - { - "name": "View" + "name": "signature", + "type": "function", + "raw": "(e: GestureResponderEvent) => void", + "signature": { + "arguments": [ + { + "name": "e", + "type": { + "name": "GestureResponderEvent" + } + } + ], + "return": { + "name": "void" } - ] + } }, - "description": "Reference for the touchable" + "description": "Function to execute on press." }, - "ref": { + "uncheckedColor": { "required": false, "tsType": { - "name": "ReactRef", - "raw": "React.Ref", - "elements": [ - { - "name": "View" - } - ] + "name": "ColorValue" + }, + "description": "Custom color for unchecked checkbox." + }, + "color": { + "required": false, + "tsType": { + "name": "ColorValue" + }, + "description": "Custom color for checkbox." + }, + "error": { + "required": false, + "tsType": { + "name": "boolean" + }, + "description": "Whether the checkbox is in an error state. When true, the outline\n(unchecked) and container (selected) use `theme.colors.error`.\n`disabled` and explicit `color`/`uncheckedColor` overrides take\nprecedence." + }, + "theme": { + "required": false, + "tsType": { + "name": "ThemeProp" }, "description": "" }, @@ -3293,78 +3326,74 @@ "tsType": { "name": "string" }, - "description": "testID to be used on tests.", - "defaultValue": { - "value": "'button'", - "computed": false - } + "description": "testID to be used on tests." + }, + "style": { + "required": false, + "tsType": { + "name": "StyleProp", + "elements": [ + { + "name": "ViewStyle" + } + ], + "raw": "StyleProp" + }, + "description": "Custom style to override the default tap target. Passed through to\nthe underlying `TouchableRipple`." } } }, "type": "component", "dependencies": [ - "src/components/Button/Button.tsx" + "src/components/Checkbox/Checkbox.tsx" ] }, - "Card/Card": { - "filepath": "Card/Card.tsx", - "title": "Card", - "description": "A card is a sheet of material that serves as an entry point to more detailed information.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Avatar, Button, Card, Text } from 'react-native-paper';\n\nconst LeftContent = props => \n\nconst MyComponent = () => (\n \n \n \n Card title\n Card content\n \n \n \n \n \n \n \n);\n\nexport default MyComponent;\n```", - "link": "card", + "Checkbox/CheckboxItem": { + "filepath": "Checkbox/CheckboxItem.tsx", + "title": "Checkbox.Item", + "description": "Checkbox.Item allows you to press the whole row (item) instead of only the Checkbox.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Checkbox } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n);\n\nexport default MyComponent;\n```", + "link": "checkbox-item", "data": { - "description": "A card is a sheet of material that serves as an entry point to more detailed information.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Avatar, Button, Card, Text } from 'react-native-paper';\n\nconst LeftContent = props => \n\nconst MyComponent = () => (\n \n \n \n Card title\n Card content\n \n \n \n \n \n \n \n);\n\nexport default MyComponent;\n```", - "displayName": "Card", + "description": "Checkbox.Item allows you to press the whole row (item) instead of only the Checkbox.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Checkbox } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n);\n\nexport default MyComponent;\n```", + "displayName": "Checkbox.Item", "methods": [], "statics": [], "props": { - "mode": { - "required": false, + "status": { + "required": true, "tsType": { "name": "union", - "raw": "'elevated' | 'outlined' | 'contained'", + "raw": "'checked' | 'unchecked' | 'indeterminate'", "elements": [ { "name": "literal", - "value": "'elevated'" + "value": "'checked'" }, { "name": "literal", - "value": "'outlined'" + "value": "'unchecked'" }, { "name": "literal", - "value": "'contained'" + "value": "'indeterminate'" } ] }, - "description": "Mode of the Card.\n- `elevated` - Card with elevation.\n- `contained` - Card without outline and elevation @supported Available in v5.x with theme version 3\n- `outlined` - Card with an outline.", - "defaultValue": { - "value": "'elevated'", - "computed": false - } + "description": "Status of checkbox." }, - "children": { - "required": true, + "disabled": { + "required": false, "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" + "name": "boolean" }, - "description": "Content of the `Card`." + "description": "Whether checkbox is disabled." }, - "onLongPress": { - "required": false, + "label": { + "required": true, "tsType": { - "name": "signature", - "type": "function", - "raw": "() => void", - "signature": { - "arguments": [], - "return": { - "name": "void" - } - } + "name": "string" }, - "description": "Function to execute on long press." + "description": "Label to be displayed on the item." }, "onPress": { "required": false, @@ -3388,7 +3417,7 @@ }, "description": "Function to execute on press." }, - "onPressIn": { + "onLongPress": { "required": false, "tsType": { "name": "signature", @@ -3408,86 +3437,41 @@ } } }, - "description": "Function to execute as soon as the touchable element is pressed and invoked even before onPress." + "description": "Function to execute on long press." }, - "onPressOut": { + "background": { "required": false, "tsType": { - "name": "signature", - "type": "function", - "raw": "(e: GestureResponderEvent) => void", - "signature": { - "arguments": [ - { - "name": "e", - "type": { - "name": "GestureResponderEvent" - } - } - ], - "return": { - "name": "void" - } - } + "name": "PressableAndroidRippleConfig" }, - "description": "Function to execute as soon as the touch is released even before onPress." + "description": "Type of background drawabale to display the feedback (Android).\nhttps://reactnative.dev/docs/pressable#rippleconfig" }, - "delayLongPress": { + "aria-label": { "required": false, "tsType": { - "name": "number" + "name": "string" }, - "description": "The number of milliseconds a user must touch the element before executing `onLongPress`." + "description": "Accessibility label for the touchable. This is read by the screen reader when the user taps the touchable.", + "defaultValue": { + "value": "label", + "computed": true + } }, - "disabled": { + "uncheckedColor": { "required": false, "tsType": { - "name": "boolean" + "name": "string" }, - "description": "If true, disable all interactions for this component." + "description": "Custom color for unchecked checkbox." }, - "elevation": { + "color": { "required": false, "tsType": { - "name": "union", - "raw": "0 | 1 | 2 | 3 | 4 | 5 | Animated.Value", - "elements": [ - { - "name": "literal", - "value": "0" - }, - { - "name": "literal", - "value": "1" - }, - { - "name": "literal", - "value": "2" - }, - { - "name": "literal", - "value": "3" - }, - { - "name": "literal", - "value": "4" - }, - { - "name": "literal", - "value": "5" - }, - { - "name": "Animated.Value" - } - ] + "name": "string" }, - "description": "Changes Card shadow and background on iOS and Android.", - "defaultValue": { - "value": "1", - "computed": false - } + "description": "Custom color for checkbox." }, - "contentStyle": { + "style": { "required": false, "tsType": { "name": "StyleProp", @@ -3498,26 +3482,42 @@ ], "raw": "StyleProp" }, - "description": "Style of card's inner content." + "description": "Additional styles for container View." }, - "style": { + "labelMaxFontSizeMultiplier": { "required": false, "tsType": { - "name": "Animated.WithAnimatedValue", + "name": "number" + }, + "description": "Specifies the largest possible scale a label font can reach.", + "defaultValue": { + "value": "1.5", + "computed": false + } + }, + "labelStyle": { + "required": false, + "tsType": { + "name": "StyleProp", "elements": [ { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" + "name": "TextStyle" } ], - "raw": "Animated.WithAnimatedValue>" + "raw": "StyleProp" }, - "description": "" + "description": "Style that is passed to Label element." + }, + "labelVariant": { + "required": false, + "tsType": { + "name": "TypescaleKey" + }, + "description": "@supported Available in v5.x with theme version 3\n\nLabel text variant defines appropriate text styles for type role and its size.\nAvailable variants:\n\n Display: `displayLarge`, `displayMedium`, `displaySmall`\n\n Headline: `headlineLarge`, `headlineMedium`, `headlineSmall`\n\n Title: `titleLarge`, `titleMedium`, `titleSmall`\n\n Label: `labelLarge`, `labelMedium`, `labelSmall`\n\n Body: `bodyLarge`, `bodyMedium`, `bodySmall`", + "defaultValue": { + "value": "'bodyLarge'", + "computed": false + } }, "theme": { "required": false, @@ -3531,540 +3531,407 @@ "tsType": { "name": "string" }, - "description": "Pass down testID from card props to touchable", + "description": "testID to be used on tests." + }, + "position": { + "required": false, + "tsType": { + "name": "union", + "raw": "'leading' | 'trailing'", + "elements": [ + { + "name": "literal", + "value": "'leading'" + }, + { + "name": "literal", + "value": "'trailing'" + } + ] + }, + "description": "Checkbox control position.", "defaultValue": { - "value": "'card'", + "value": "'trailing'", "computed": false } }, - "accessible": { + "hitSlop": { "required": false, "tsType": { - "name": "boolean" + "name": "TouchableRippleProps['hitSlop']", + "raw": "TouchableRippleProps['hitSlop']" }, - "description": "Pass down accessible from card props to touchable" + "description": "Sets additional distance outside of element in which a press can be detected." } } }, "type": "component", "dependencies": [ - "src/components/Card/Card.tsx" - ] + "src/components/Checkbox/CheckboxItem.tsx" + ], + "group": "Checkbox" }, - "Card/CardActions": { - "filepath": "Card/CardActions.tsx", - "title": "Card.Actions", - "description": "A component to show a list of actions inside a Card.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Card, Button } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n \n \n \n);\n\nexport default MyComponent;\n```", - "link": "card-actions", + "Chip/Chip": { + "filepath": "Chip/Chip.tsx", + "title": "Chip", + "description": "Chips are compact elements that can represent inputs, attributes, or actions.\nThey can have an icon or avatar on the left, and a close button icon on the right.\nThey are typically used to:\n
    \n
  • Present multiple options
  • \n
  • Represent attributes active or chosen
  • \n
  • Present filter options
  • \n
  • Trigger actions related to primary content
  • \n
\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Chip } from 'react-native-paper';\n\nconst MyComponent = () => (\n console.log('Pressed')}>Example Chip\n);\n\nexport default MyComponent;\n```", + "link": "chip", "data": { - "description": "A component to show a list of actions inside a Card.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Card, Button } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n \n \n \n);\n\nexport default MyComponent;\n```", - "displayName": "Card.Actions", + "description": "Chips are compact elements that can represent inputs, attributes, or actions.\nThey can have an icon or avatar on the left, and a close button icon on the right.\nThey are typically used to:\n
    \n
  • Present multiple options
  • \n
  • Represent attributes active or chosen
  • \n
  • Present filter options
  • \n
  • Trigger actions related to primary content
  • \n
\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Chip } from 'react-native-paper';\n\nconst MyComponent = () => (\n console.log('Pressed')}>Example Chip\n);\n\nexport default MyComponent;\n```", + "displayName": "Chip", "methods": [], "statics": [], "props": { + "mode": { + "required": false, + "tsType": { + "name": "union", + "raw": "'flat' | 'outlined'", + "elements": [ + { + "name": "literal", + "value": "'flat'" + }, + { + "name": "literal", + "value": "'outlined'" + } + ] + }, + "description": "Mode of the chip.\n- `flat` - flat chip without outline.\n- `outlined` - chip with an outline.", + "defaultValue": { + "value": "'flat'", + "computed": false + } + }, "children": { "required": true, "tsType": { "name": "ReactReactNode", "raw": "React.ReactNode" }, - "description": "Items inside the `CardActions`." + "description": "Text content of the `Chip`." }, - "style": { + "icon": { "required": false, "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" + "name": "IconSource" }, - "description": "" + "description": "Icon to display for the `Chip`. Both icon and avatar cannot be specified." }, - "theme": { + "avatar": { "required": false, - "tsType": { - "name": "ThemeProp" - }, - "description": "" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/Card/CardActions.tsx" - ], - "group": "Card" - }, - "Card/CardContent": { - "filepath": "Card/CardContent.tsx", - "title": "Card.Content", - "description": "A component to show content inside a Card.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Card, Text } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n Card title\n Card content\n \n \n);\n\nexport default MyComponent;\n```", - "link": "card-content", - "data": { - "description": "A component to show content inside a Card.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Card, Text } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n Card title\n Card content\n \n \n);\n\nexport default MyComponent;\n```", - "displayName": "Card.Content", - "methods": [], - "statics": [], - "props": { - "children": { - "required": true, "tsType": { "name": "ReactReactNode", "raw": "React.ReactNode" }, - "description": "Items inside the `Card.Content`." + "description": "Avatar to display for the `Chip`. Both icon and avatar cannot be specified." }, - "index": { + "closeIcon": { "required": false, "tsType": { - "name": "number" + "name": "IconSource" }, - "description": "@internal" + "description": "Icon to display as the close button for the `Chip`. The icon appears only when the onClose prop is specified." }, - "total": { + "selected": { "required": false, "tsType": { - "name": "number" + "name": "boolean" }, - "description": "@internal" + "description": "Whether chip is selected.", + "defaultValue": { + "value": "false", + "computed": false + } }, - "siblings": { + "selectedColor": { "required": false, "tsType": { - "name": "Array", - "elements": [ - { - "name": "string" - } - ], - "raw": "Array" + "name": "ColorValue" }, - "description": "@internal" + "description": "Whether to style the chip color as selected.\nNote: With theme version 3 `selectedColor` doesn't apply to the `icon`.\n If you want specify custom color for the `icon`, render your own `Icon` component." }, - "style": { + "showSelectedOverlay": { "required": false, "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" - }, - "description": "" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/Card/CardContent.tsx" - ], - "group": "Card" - }, - "Card/CardCover": { - "filepath": "Card/CardCover.tsx", - "title": "Card.Cover", - "description": "A component to show a cover image inside a Card.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Card } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n);\n\nexport default MyComponent;\n```\n\n@extends Image props https://reactnative.dev/docs/image#props", - "link": "card-cover", - "data": { - "description": "A component to show a cover image inside a Card.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Card } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n);\n\nexport default MyComponent;\n```\n\n@extends Image props https://reactnative.dev/docs/image#props", - "displayName": "Card.Cover", - "methods": [], - "statics": [], - "props": { - "index": { - "required": false, - "tsType": { - "name": "number" - }, - "description": "@internal" - }, - "total": { - "required": false, - "tsType": { - "name": "number" - }, - "description": "@internal" - }, - "style": { - "required": false, - "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" - }, - "description": "" - }, - "theme": { - "required": false, - "tsType": { - "name": "ThemeProp" - }, - "description": "" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/Card/CardCover.tsx" - ], - "group": "Card" - }, - "Card/CardTitle": { - "filepath": "Card/CardTitle.tsx", - "title": "Card.Title", - "description": "A component to show a title, subtitle and an avatar inside a Card.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Avatar, Card, IconButton } from 'react-native-paper';\n\nconst MyComponent = () => (\n }\n right={(props) => {}} />}\n />\n);\n\nexport default MyComponent;\n```", - "link": "card-title", - "data": { - "description": "A component to show a title, subtitle and an avatar inside a Card.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Avatar, Card, IconButton } from 'react-native-paper';\n\nconst MyComponent = () => (\n }\n right={(props) => {}} />}\n />\n);\n\nexport default MyComponent;\n```", - "displayName": "Card.Title", - "methods": [], - "statics": [], - "props": { - "title": { - "required": true, - "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" - }, - "description": "Text for the title. Note that this will only accept a string or ``-based node." - }, - "titleStyle": { - "required": false, - "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "TextStyle" - } - ], - "raw": "StyleProp" + "name": "boolean" }, - "description": "Style for the title." + "description": "@supported Available in v5.x with theme version 3\nWhether to display overlay on selected chip" }, - "titleNumberOfLines": { + "showSelectedCheck": { "required": false, "tsType": { - "name": "number" + "name": "boolean" }, - "description": "Number of lines for the title.", + "description": "Whether to display default check icon on selected chip.\nNote: Check will not be shown if `icon` is specified. If specified, `icon` will be shown regardless of `selected`.", "defaultValue": { - "value": "1", + "value": "true", "computed": false } }, - "titleVariant": { + "disabled": { "required": false, "tsType": { - "name": "TypescaleKey" + "name": "boolean" }, - "description": "@supported Available in v5.x with theme version 3\n\nTitle text variant defines appropriate text styles for type role and its size.\nAvailable variants:\n\n Display: `displayLarge`, `displayMedium`, `displaySmall`\n\n Headline: `headlineLarge`, `headlineMedium`, `headlineSmall`\n\n Title: `titleLarge`, `titleMedium`, `titleSmall`\n\n Label: `labelLarge`, `labelMedium`, `labelSmall`\n\n Body: `bodyLarge`, `bodyMedium`, `bodySmall`", + "description": "Whether the chip is disabled. A disabled chip is greyed out and `onPress` is not called on touch.", "defaultValue": { - "value": "'bodyLarge'", + "value": "false", "computed": false } }, - "subtitle": { - "required": false, - "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" - }, - "description": "Text for the subtitle. Note that this will only accept a string or ``-based node." - }, - "subtitleStyle": { + "background": { "required": false, "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "TextStyle" - } - ], - "raw": "StyleProp" + "name": "PressableAndroidRippleConfig" }, - "description": "Style for the subtitle." + "description": "Type of background drawabale to display the feedback (Android).\nhttps://reactnative.dev/docs/pressable#rippleconfig" }, - "subtitleNumberOfLines": { + "aria-label": { "required": false, "tsType": { - "name": "number" + "name": "string" }, - "description": "Number of lines for the subtitle.", - "defaultValue": { - "value": "1", - "computed": false - } + "description": "Accessibility label for the chip. This is read by the screen reader when the user taps the chip." }, - "subtitleVariant": { + "closeIconAccessibilityLabel": { "required": false, "tsType": { - "name": "TypescaleKey" + "name": "string" }, - "description": "@supported Available in v5.x with theme version 3\n\nSubtitle text variant defines appropriate text styles for type role and its size.\nAvailable variants:\n\n Display: `displayLarge`, `displayMedium`, `displaySmall`\n\n Headline: `headlineLarge`, `headlineMedium`, `headlineSmall`\n\n Title: `titleLarge`, `titleMedium`, `titleSmall`\n\n Label: `labelLarge`, `labelMedium`, `labelSmall`\n\n Body: `bodyLarge`, `bodyMedium`, `bodySmall`", + "description": "Accessibility label for the close icon. This is read by the screen reader when the user taps the close icon.", "defaultValue": { - "value": "'bodyMedium'", + "value": "'Close'", "computed": false } }, - "left": { + "onPress": { "required": false, "tsType": { "name": "signature", "type": "function", - "raw": "(props: { size: number }) => React.ReactNode", + "raw": "(e: GestureResponderEvent) => void", "signature": { "arguments": [ { - "name": "props", + "name": "e", "type": { - "name": "signature", - "type": "object", - "raw": "{ size: number }", - "signature": { - "properties": [ - { - "key": "size", - "value": { - "name": "number", - "required": true - } - } - ] - } + "name": "GestureResponderEvent" } } ], "return": { - "name": "ReactReactNode", - "raw": "React.ReactNode" + "name": "void" } } }, - "description": "Callback which returns a React element to display on the left side." + "description": "Function to execute on press." }, - "leftStyle": { + "onLongPress": { "required": false, "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" + "name": "signature", + "type": "function", + "raw": "() => void", + "signature": { + "arguments": [], + "return": { + "name": "void" } - ], - "raw": "StyleProp" + } }, - "description": "Style for the left element wrapper." + "description": "Function to execute on long press." }, - "right": { + "onPressIn": { "required": false, "tsType": { "name": "signature", "type": "function", - "raw": "(props: { size: number }) => React.ReactNode", + "raw": "(e: GestureResponderEvent) => void", "signature": { "arguments": [ { - "name": "props", + "name": "e", "type": { - "name": "signature", - "type": "object", - "raw": "{ size: number }", - "signature": { - "properties": [ - { - "key": "size", - "value": { - "name": "number", - "required": true - } - } - ] - } + "name": "GestureResponderEvent" } } ], "return": { - "name": "ReactReactNode", - "raw": "React.ReactNode" + "name": "void" } } }, - "description": "Callback which returns a React element to display on the right side." + "description": "Function to execute as soon as the touchable element is pressed and invoked even before onPress." }, - "rightStyle": { + "onPressOut": { "required": false, "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" + "name": "signature", + "type": "function", + "raw": "(e: GestureResponderEvent) => void", + "signature": { + "arguments": [ + { + "name": "e", + "type": { + "name": "GestureResponderEvent" + } + } + ], + "return": { + "name": "void" } - ], - "raw": "StyleProp" - }, - "description": "Style for the right element wrapper." - }, - "index": { - "required": false, - "tsType": { - "name": "number" + } }, - "description": "@internal" + "description": "Function to execute as soon as the touch is released even before onPress." }, - "total": { + "onClose": { "required": false, "tsType": { - "name": "number" + "name": "signature", + "type": "function", + "raw": "() => void", + "signature": { + "arguments": [], + "return": { + "name": "void" + } + } }, - "description": "@internal" + "description": "Function to execute on close button press. The close button appears only when this prop is specified." }, - "titleMaxFontSizeMultiplier": { + "delayLongPress": { "required": false, "tsType": { "name": "number" }, - "description": "Specifies the largest possible scale a title font can reach." + "description": "The number of milliseconds a user must touch the element before executing `onLongPress`." }, - "subtitleMaxFontSizeMultiplier": { + "compact": { "required": false, "tsType": { - "name": "number" + "name": "boolean" }, - "description": "Specifies the largest possible scale a subtitle font can reach." + "description": "@supported Available in v5.x with theme version 3\nSets smaller horizontal paddings `12dp` around label, when there is only label." }, - "style": { + "elevated": { + "required": false, + "tsType": { + "name": "boolean" + }, + "description": "@supported Available in v5.x with theme version 3\nWhether chip should have the elevation.", + "defaultValue": { + "value": "false", + "computed": false + } + }, + "textStyle": { "required": false, "tsType": { "name": "StyleProp", "elements": [ { - "name": "ViewStyle" + "name": "TextStyle" } ], - "raw": "StyleProp" + "raw": "StyleProp" }, - "description": "" + "description": "Style of chip's text" }, - "theme": { + "style": { "required": false, "tsType": { - "name": "ThemeProp" - }, - "description": "" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/Card/CardTitle.tsx" - ], - "group": "Card" - }, - "Checkbox/Checkbox": { - "filepath": "Checkbox/Checkbox.tsx", - "title": "Checkbox", - "description": "Checkboxes allow the selection of multiple options from a set.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Checkbox } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [checked, setChecked] = React.useState(false);\n\n return (\n {\n setChecked(!checked);\n }}\n />\n );\n};\n\nexport default MyComponent;\n```", - "link": "checkbox", - "data": { - "description": "Checkboxes allow the selection of multiple options from a set.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Checkbox } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [checked, setChecked] = React.useState(false);\n\n return (\n {\n setChecked(!checked);\n }}\n />\n );\n};\n\nexport default MyComponent;\n```", - "displayName": "Checkbox", - "methods": [], - "statics": [], - "props": { - "status": { - "required": true, - "tsType": { - "name": "union", - "raw": "'checked' | 'unchecked' | 'indeterminate'", + "name": "Animated.WithAnimatedValue", "elements": [ { - "name": "literal", - "value": "'checked'" - }, - { - "name": "literal", - "value": "'unchecked'" - }, - { - "name": "literal", - "value": "'indeterminate'" + "name": "StyleProp", + "elements": [ + { + "name": "ViewStyle" + } + ], + "raw": "StyleProp" } - ] + ], + "raw": "Animated.WithAnimatedValue>" }, - "description": "Status of checkbox." + "description": "" }, - "disabled": { + "hitSlop": { "required": false, "tsType": { - "name": "boolean" + "name": "TouchableRippleProps['hitSlop']", + "raw": "TouchableRippleProps['hitSlop']" }, - "description": "Whether checkbox is disabled." + "description": "Sets additional distance outside of element in which a press can be detected." }, - "onPress": { + "theme": { "required": false, "tsType": { - "name": "signature", - "type": "function", - "raw": "(e: GestureResponderEvent) => void", - "signature": { - "arguments": [ - { - "name": "e", - "type": { - "name": "GestureResponderEvent" - } - } - ], - "return": { - "name": "void" - } - } + "name": "ThemeProp" }, - "description": "Function to execute on press." + "description": "" }, - "uncheckedColor": { + "testID": { "required": false, "tsType": { - "name": "ColorValue" + "name": "string" }, - "description": "Custom color for unchecked checkbox." + "description": "Pass down testID from chip props to touchable for Detox tests.", + "defaultValue": { + "value": "'chip'", + "computed": false + } }, - "color": { + "ellipsizeMode": { "required": false, "tsType": { - "name": "ColorValue" + "name": "EllipsizeProp" }, - "description": "Custom color for checkbox." + "description": "Ellipsize Mode for the children text" }, - "error": { + "maxFontSizeMultiplier": { "required": false, "tsType": { - "name": "boolean" + "name": "number" }, - "description": "Whether the checkbox is in an error state. When true, the outline\n(unchecked) and container (selected) use `theme.colors.error`.\n`disabled` and explicit `color`/`uncheckedColor` overrides take\nprecedence." + "description": "Specifies the largest possible scale a text font can reach." }, - "theme": { - "required": false, - "tsType": { - "name": "ThemeProp" + "role": { + "defaultValue": { + "value": "'button'", + "computed": false }, - "description": "" - }, - "testID": { "required": false, + "description": "" + } + } + }, + "type": "component", + "dependencies": [ + "src/components/Chip/Chip.tsx" + ] + }, + "DataTable/DataTable": { + "filepath": "DataTable/DataTable.tsx", + "title": "DataTable", + "description": "Data tables allow displaying sets of data.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [page, setPage] = React.useState(0);\n const [numberOfItemsPerPageList] = React.useState([2, 3, 4]);\n const [itemsPerPage, onItemsPerPageChange] = React.useState(\n numberOfItemsPerPageList[0]\n );\n\n const [items] = React.useState([\n {\n key: 1,\n name: 'Cupcake',\n calories: 356,\n fat: 16,\n },\n {\n key: 2,\n name: 'Eclair',\n calories: 262,\n fat: 16,\n },\n {\n key: 3,\n name: 'Frozen yogurt',\n calories: 159,\n fat: 6,\n },\n {\n key: 4,\n name: 'Gingerbread',\n calories: 305,\n fat: 3.7,\n },\n ]);\n\n const from = page * itemsPerPage;\n const to = Math.min((page + 1) * itemsPerPage, items.length);\n\n React.useEffect(() => {\n setPage(0);\n }, [itemsPerPage]);\n\n return (\n \n \n Dessert\n Calories\n Fat\n \n\n {items.slice(from, to).map((item) => (\n \n {item.name}\n {item.calories}\n {item.fat}\n \n ))}\n\n setPage(page)}\n label={`${from + 1}-${to} of ${items.length}`}\n numberOfItemsPerPageList={numberOfItemsPerPageList}\n numberOfItemsPerPage={itemsPerPage}\n onItemsPerPageChange={onItemsPerPageChange}\n showFastPaginationControls\n selectPageDropdownLabel={'Rows per page'}\n />\n \n );\n};\n\nexport default MyComponent;\n```", + "link": "data-table", + "data": { + "description": "Data tables allow displaying sets of data.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [page, setPage] = React.useState(0);\n const [numberOfItemsPerPageList] = React.useState([2, 3, 4]);\n const [itemsPerPage, onItemsPerPageChange] = React.useState(\n numberOfItemsPerPageList[0]\n );\n\n const [items] = React.useState([\n {\n key: 1,\n name: 'Cupcake',\n calories: 356,\n fat: 16,\n },\n {\n key: 2,\n name: 'Eclair',\n calories: 262,\n fat: 16,\n },\n {\n key: 3,\n name: 'Frozen yogurt',\n calories: 159,\n fat: 6,\n },\n {\n key: 4,\n name: 'Gingerbread',\n calories: 305,\n fat: 3.7,\n },\n ]);\n\n const from = page * itemsPerPage;\n const to = Math.min((page + 1) * itemsPerPage, items.length);\n\n React.useEffect(() => {\n setPage(0);\n }, [itemsPerPage]);\n\n return (\n \n \n Dessert\n Calories\n Fat\n \n\n {items.slice(from, to).map((item) => (\n \n {item.name}\n {item.calories}\n {item.fat}\n \n ))}\n\n setPage(page)}\n label={`${from + 1}-${to} of ${items.length}`}\n numberOfItemsPerPageList={numberOfItemsPerPageList}\n numberOfItemsPerPage={itemsPerPage}\n onItemsPerPageChange={onItemsPerPageChange}\n showFastPaginationControls\n selectPageDropdownLabel={'Rows per page'}\n />\n \n );\n};\n\nexport default MyComponent;\n```", + "displayName": "DataTable", + "methods": [], + "statics": [], + "props": { + "children": { + "required": true, "tsType": { - "name": "string" + "name": "ReactReactNode", + "raw": "React.ReactNode" }, - "description": "testID to be used on tests." + "description": "Content of the `DataTable`." }, "style": { "required": false, @@ -4077,61 +3944,40 @@ ], "raw": "StyleProp" }, - "description": "Custom style to override the default tap target. Passed through to\nthe underlying `TouchableRipple`." + "description": "" } } }, "type": "component", "dependencies": [ - "src/components/Checkbox/Checkbox.tsx" + "src/components/DataTable/DataTable.tsx" ] }, - "Checkbox/CheckboxItem": { - "filepath": "Checkbox/CheckboxItem.tsx", - "title": "Checkbox.Item", - "description": "Checkbox.Item allows you to press the whole row (item) instead of only the Checkbox.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Checkbox } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n);\n\nexport default MyComponent;\n```", - "link": "checkbox-item", + "DataTable/DataTableCell": { + "filepath": "DataTable/DataTableCell.tsx", + "title": "DataTable.Cell", + "description": "A component to show a single cell inside of a table.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n 1\n 2\n 3\n 4\n \n);\n\nexport default MyComponent;\n```\n\nIf you want to support multiline text, please use View instead, as multiline text doesn't comply with\nMD Guidelines (https://github.com/callstack/react-native-paper/issues/2381).\n\n@extends TouchableRipple props https://callstack.github.io/react-native-paper/docs/components/TouchableRipple", + "link": "data-table-cell", "data": { - "description": "Checkbox.Item allows you to press the whole row (item) instead of only the Checkbox.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Checkbox } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n);\n\nexport default MyComponent;\n```", - "displayName": "Checkbox.Item", + "description": "A component to show a single cell inside of a table.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n 1\n 2\n 3\n 4\n \n);\n\nexport default MyComponent;\n```\n\nIf you want to support multiline text, please use View instead, as multiline text doesn't comply with\nMD Guidelines (https://github.com/callstack/react-native-paper/issues/2381).\n\n@extends TouchableRipple props https://callstack.github.io/react-native-paper/docs/components/TouchableRipple", + "displayName": "DataTable.Cell", "methods": [], "statics": [], "props": { - "status": { + "children": { "required": true, "tsType": { - "name": "union", - "raw": "'checked' | 'unchecked' | 'indeterminate'", - "elements": [ - { - "name": "literal", - "value": "'checked'" - }, - { - "name": "literal", - "value": "'unchecked'" - }, - { - "name": "literal", - "value": "'indeterminate'" - } - ] + "name": "ReactReactNode", + "raw": "React.ReactNode" }, - "description": "Status of checkbox." + "description": "Content of the `DataTableCell`." }, - "disabled": { + "numeric": { "required": false, "tsType": { "name": "boolean" }, - "description": "Whether checkbox is disabled." - }, - "label": { - "required": true, - "tsType": { - "name": "string" - }, - "description": "Label to be displayed on the item." + "description": "Align the text to the right. Generally monetary or number fields are aligned to right." }, "onPress": { "required": false, @@ -4155,59 +4001,72 @@ }, "description": "Function to execute on press." }, - "onLongPress": { + "style": { "required": false, "tsType": { - "name": "signature", - "type": "function", - "raw": "(e: GestureResponderEvent) => void", - "signature": { - "arguments": [ - { - "name": "e", - "type": { - "name": "GestureResponderEvent" - } - } - ], - "return": { - "name": "void" + "name": "StyleProp", + "elements": [ + { + "name": "ViewStyle" } - } + ], + "raw": "StyleProp" }, - "description": "Function to execute on long press." + "description": "" }, - "background": { + "textStyle": { "required": false, "tsType": { - "name": "PressableAndroidRippleConfig" + "name": "StyleProp", + "elements": [ + { + "name": "TextStyle" + } + ], + "raw": "StyleProp" }, - "description": "Type of background drawabale to display the feedback (Android).\nhttps://reactnative.dev/docs/pressable#rippleconfig" + "description": "Text content style of the `DataTableCell`." }, - "aria-label": { + "maxFontSizeMultiplier": { "required": false, "tsType": { - "name": "string" + "name": "number" }, - "description": "Accessibility label for the touchable. This is read by the screen reader when the user taps the touchable.", - "defaultValue": { - "value": "label", - "computed": true - } + "description": "Specifies the largest possible scale a text font can reach." }, - "uncheckedColor": { + "testID": { "required": false, "tsType": { "name": "string" }, - "description": "Custom color for unchecked checkbox." - }, - "color": { - "required": false, + "description": "testID to be used on tests." + } + } + }, + "type": "component", + "dependencies": [ + "src/components/DataTable/DataTableCell.tsx" + ], + "group": "DataTable" + }, + "DataTable/DataTableHeader": { + "filepath": "DataTable/DataTableHeader.tsx", + "title": "DataTable.Header", + "description": "A component to display title in table header.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n Dessert\n \n Calories\n Fat (g)\n \n \n);\n\nexport default MyComponent;\n```", + "link": "data-table-header", + "data": { + "description": "A component to display title in table header.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n Dessert\n \n Calories\n Fat (g)\n \n \n);\n\nexport default MyComponent;\n```", + "displayName": "DataTable.Header", + "methods": [], + "statics": [], + "props": { + "children": { + "required": true, "tsType": { - "name": "string" + "name": "ReactReactNode", + "raw": "React.ReactNode" }, - "description": "Custom color for checkbox." + "description": "Content of the `DataTableHeader`." }, "style": { "required": false, @@ -4220,40 +4079,78 @@ ], "raw": "StyleProp" }, - "description": "Additional styles for container View." + "description": "" }, - "labelMaxFontSizeMultiplier": { + "theme": { "required": false, + "tsType": { + "name": "ThemeProp" + }, + "description": "" + } + } + }, + "type": "component", + "dependencies": [ + "src/components/DataTable/DataTableHeader.tsx" + ], + "group": "DataTable" + }, + "DataTable/DataTablePagination": { + "filepath": "DataTable/DataTablePagination.tsx", + "title": "DataTable.Pagination", + "description": "A component to show pagination for data table.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst numberOfItemsPerPageList = [2, 3, 4];\n\nconst items = [\n {\n key: 1,\n name: 'Page 1',\n },\n {\n key: 2,\n name: 'Page 2',\n },\n {\n key: 3,\n name: 'Page 3',\n },\n];\n\nconst MyComponent = () => {\n const [page, setPage] = React.useState(0);\n const [numberOfItemsPerPage, onItemsPerPageChange] = React.useState(numberOfItemsPerPageList[0]);\n const from = page * numberOfItemsPerPage;\n const to = Math.min((page + 1) * numberOfItemsPerPage, items.length);\n\n React.useEffect(() => {\n setPage(0);\n }, [numberOfItemsPerPage]);\n\n return (\n \n setPage(page)}\n label={`${from + 1}-${to} of ${items.length}`}\n showFastPaginationControls\n numberOfItemsPerPageList={numberOfItemsPerPageList}\n numberOfItemsPerPage={numberOfItemsPerPage}\n onItemsPerPageChange={onItemsPerPageChange}\n selectPageDropdownLabel={'Rows per page'}\n />\n \n );\n};\n\nexport default MyComponent;\n```", + "link": "data-table-pagination", + "data": { + "description": "A component to show pagination for data table.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst numberOfItemsPerPageList = [2, 3, 4];\n\nconst items = [\n {\n key: 1,\n name: 'Page 1',\n },\n {\n key: 2,\n name: 'Page 2',\n },\n {\n key: 3,\n name: 'Page 3',\n },\n];\n\nconst MyComponent = () => {\n const [page, setPage] = React.useState(0);\n const [numberOfItemsPerPage, onItemsPerPageChange] = React.useState(numberOfItemsPerPageList[0]);\n const from = page * numberOfItemsPerPage;\n const to = Math.min((page + 1) * numberOfItemsPerPage, items.length);\n\n React.useEffect(() => {\n setPage(0);\n }, [numberOfItemsPerPage]);\n\n return (\n \n setPage(page)}\n label={`${from + 1}-${to} of ${items.length}`}\n showFastPaginationControls\n numberOfItemsPerPageList={numberOfItemsPerPageList}\n numberOfItemsPerPage={numberOfItemsPerPage}\n onItemsPerPageChange={onItemsPerPageChange}\n selectPageDropdownLabel={'Rows per page'}\n />\n \n );\n};\n\nexport default MyComponent;\n```", + "displayName": "DataTable.Pagination", + "methods": [], + "statics": [], + "props": { + "page": { + "required": true, "tsType": { "name": "number" }, - "description": "Specifies the largest possible scale a label font can reach.", - "defaultValue": { - "value": "1.5", - "computed": false - } + "description": "The currently visible page (starting with 0)." }, - "labelStyle": { - "required": false, + "numberOfPages": { + "required": true, "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "TextStyle" + "name": "number" + }, + "description": "The total number of pages." + }, + "onPageChange": { + "required": true, + "tsType": { + "name": "signature", + "type": "function", + "raw": "(page: number) => void", + "signature": { + "arguments": [ + { + "name": "page", + "type": { + "name": "number" + } + } + ], + "return": { + "name": "void" } - ], - "raw": "StyleProp" + } }, - "description": "Style that is passed to Label element." + "description": "Function to execute on page change." }, - "labelVariant": { + "showFastPaginationControls": { "required": false, "tsType": { - "name": "TypescaleKey" + "name": "boolean" }, - "description": "@supported Available in v5.x with theme version 3\n\nLabel text variant defines appropriate text styles for type role and its size.\nAvailable variants:\n\n Display: `displayLarge`, `displayMedium`, `displaySmall`\n\n Headline: `headlineLarge`, `headlineMedium`, `headlineSmall`\n\n Title: `titleLarge`, `titleMedium`, `titleSmall`\n\n Label: `labelLarge`, `labelMedium`, `labelSmall`\n\n Body: `bodyLarge`, `bodyMedium`, `bodySmall`", + "description": "Whether to show fast forward and fast rewind buttons in pagination. False by default.", "defaultValue": { - "value": "'bodyLarge'", + "value": "false", "computed": false } }, @@ -4264,183 +4161,228 @@ }, "description": "" }, - "testID": { + "numberOfItemsPerPage": { "required": false, "tsType": { - "name": "string" + "name": "number" }, - "description": "testID to be used on tests." + "description": "The current number of rows per page." }, - "position": { + "numberOfItemsPerPageList": { "required": false, "tsType": { - "name": "union", - "raw": "'leading' | 'trailing'", + "name": "Array", "elements": [ { - "name": "literal", - "value": "'leading'" - }, - { - "name": "literal", - "value": "'trailing'" + "name": "number" } - ] + ], + "raw": "Array" }, - "description": "Checkbox control position.", - "defaultValue": { - "value": "'trailing'", - "computed": false - } + "description": "Options for a number of rows per page to choose from." }, - "hitSlop": { - "required": false, - "tsType": { - "name": "TouchableRippleProps['hitSlop']", - "raw": "TouchableRippleProps['hitSlop']" - }, - "description": "Sets additional distance outside of element in which a press can be detected." - } - } - }, - "type": "component", - "dependencies": [ - "src/components/Checkbox/CheckboxItem.tsx" - ], - "group": "Checkbox" - }, - "Chip/Chip": { - "filepath": "Chip/Chip.tsx", - "title": "Chip", - "description": "Chips are compact elements that can represent inputs, attributes, or actions.\nThey can have an icon or avatar on the left, and a close button icon on the right.\nThey are typically used to:\n
    \n
  • Present multiple options
  • \n
  • Represent attributes active or chosen
  • \n
  • Present filter options
  • \n
  • Trigger actions related to primary content
  • \n
\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Chip } from 'react-native-paper';\n\nconst MyComponent = () => (\n console.log('Pressed')}>Example Chip\n);\n\nexport default MyComponent;\n```", - "link": "chip", - "data": { - "description": "Chips are compact elements that can represent inputs, attributes, or actions.\nThey can have an icon or avatar on the left, and a close button icon on the right.\nThey are typically used to:\n
    \n
  • Present multiple options
  • \n
  • Represent attributes active or chosen
  • \n
  • Present filter options
  • \n
  • Trigger actions related to primary content
  • \n
\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Chip } from 'react-native-paper';\n\nconst MyComponent = () => (\n console.log('Pressed')}>Example Chip\n);\n\nexport default MyComponent;\n```", - "displayName": "Chip", - "methods": [], - "statics": [], - "props": { - "mode": { + "onItemsPerPageChange": { "required": false, "tsType": { - "name": "union", - "raw": "'flat' | 'outlined'", - "elements": [ - { - "name": "literal", - "value": "'flat'" - }, - { - "name": "literal", - "value": "'outlined'" + "name": "signature", + "type": "function", + "raw": "(numberOfItemsPerPage: number) => void", + "signature": { + "arguments": [ + { + "name": "numberOfItemsPerPage", + "type": { + "name": "number" + } + } + ], + "return": { + "name": "void" } - ] + } }, - "description": "Mode of the chip.\n- `flat` - flat chip without outline.\n- `outlined` - chip with an outline.", - "defaultValue": { - "value": "'flat'", - "computed": false - } + "description": "The function to set the number of rows per page." }, - "children": { - "required": true, + "selectPageDropdownLabel": { + "required": false, "tsType": { "name": "ReactReactNode", "raw": "React.ReactNode" }, - "description": "Text content of the `Chip`." + "description": "Label text for select page dropdown to display." }, - "icon": { + "selectPageDropdownAccessibilityLabel": { "required": false, "tsType": { - "name": "IconSource" + "name": "string" }, - "description": "Icon to display for the `Chip`. Both icon and avatar cannot be specified." + "description": "AccessibilityLabel for `selectPageDropdownLabel`." }, - "avatar": { + "label": { "required": false, "tsType": { "name": "ReactReactNode", "raw": "React.ReactNode" }, - "description": "Avatar to display for the `Chip`. Both icon and avatar cannot be specified." + "description": "Label text to display which indicates current pagination." }, - "closeIcon": { + "aria-label": { "required": false, "tsType": { - "name": "IconSource" + "name": "string" }, - "description": "Icon to display as the close button for the `Chip`. The icon appears only when the onClose prop is specified." + "description": "AccessibilityLabel for `label`." }, - "selected": { + "style": { "required": false, "tsType": { - "name": "boolean" + "name": "StyleProp", + "elements": [ + { + "name": "ViewStyle" + } + ], + "raw": "StyleProp" }, - "description": "Whether chip is selected.", - "defaultValue": { - "value": "false", - "computed": false - } + "description": "" + } + } + }, + "type": "component", + "dependencies": [ + "src/components/DataTable/DataTablePagination.tsx" + ], + "group": "DataTable" + }, + "DataTable/DataTableRow": { + "filepath": "DataTable/DataTableRow.tsx", + "title": "DataTable.Row", + "description": "A component to show a single row inside of a table.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n 1\n 2\n 3\n 4\n \n);\n\nexport default MyComponent;\n```\n\n@extends TouchableRipple props https://callstack.github.io/react-native-paper/docs/components/TouchableRipple", + "link": "data-table-row", + "data": { + "description": "A component to show a single row inside of a table.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n 1\n 2\n 3\n 4\n \n);\n\nexport default MyComponent;\n```\n\n@extends TouchableRipple props https://callstack.github.io/react-native-paper/docs/components/TouchableRipple", + "displayName": "DataTable.Row", + "methods": [], + "statics": [], + "props": { + "children": { + "required": true, + "tsType": { + "name": "ReactReactNode", + "raw": "React.ReactNode" + }, + "description": "Content of the `DataTableRow`." }, - "selectedColor": { + "onPress": { "required": false, "tsType": { - "name": "ColorValue" + "name": "signature", + "type": "function", + "raw": "(e: GestureResponderEvent) => void", + "signature": { + "arguments": [ + { + "name": "e", + "type": { + "name": "GestureResponderEvent" + } + } + ], + "return": { + "name": "void" + } + } }, - "description": "Whether to style the chip color as selected.\nNote: With theme version 3 `selectedColor` doesn't apply to the `icon`.\n If you want specify custom color for the `icon`, render your own `Icon` component." + "description": "Function to execute on press." }, - "showSelectedOverlay": { + "style": { "required": false, "tsType": { - "name": "boolean" + "name": "StyleProp", + "elements": [ + { + "name": "ViewStyle" + } + ], + "raw": "StyleProp" }, - "description": "@supported Available in v5.x with theme version 3\nWhether to display overlay on selected chip" + "description": "" }, - "showSelectedCheck": { + "theme": { "required": false, "tsType": { - "name": "boolean" + "name": "ThemeProp" }, - "description": "Whether to display default check icon on selected chip.\nNote: Check will not be shown if `icon` is specified. If specified, `icon` will be shown regardless of `selected`.", - "defaultValue": { - "value": "true", - "computed": false - } + "description": "" }, - "disabled": { + "pointerEvents": { "required": false, "tsType": { - "name": "boolean" + "name": "ViewProps['pointerEvents']", + "raw": "ViewProps['pointerEvents']" }, - "description": "Whether the chip is disabled. A disabled chip is greyed out and `onPress` is not called on touch.", - "defaultValue": { - "value": "false", - "computed": false - } + "description": "`pointerEvents` passed to the `View` container, which is wrapping children within `TouchableRipple`." + } + } + }, + "type": "component", + "dependencies": [ + "src/components/DataTable/DataTableRow.tsx" + ], + "group": "DataTable" + }, + "DataTable/DataTableTitle": { + "filepath": "DataTable/DataTableTitle.tsx", + "title": "DataTable.Title", + "description": "A component to display title in table header.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n Dessert\n \n Calories\n Fat (g)\n \n \n);\n\nexport default MyComponent;\n```", + "link": "data-table-title", + "data": { + "description": "A component to display title in table header.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n Dessert\n \n Calories\n Fat (g)\n \n \n);\n\nexport default MyComponent;\n```", + "displayName": "DataTable.Title", + "methods": [], + "statics": [], + "props": { + "children": { + "required": true, + "tsType": { + "name": "ReactReactNode", + "raw": "React.ReactNode" + }, + "description": "Text content of the `DataTableTitle`." }, - "background": { + "numeric": { "required": false, "tsType": { - "name": "PressableAndroidRippleConfig" + "name": "boolean" }, - "description": "Type of background drawabale to display the feedback (Android).\nhttps://reactnative.dev/docs/pressable#rippleconfig" + "description": "Align the text to the right. Generally monetary or number fields are aligned to right." }, - "aria-label": { + "sortDirection": { "required": false, "tsType": { - "name": "string" + "name": "union", + "raw": "'ascending' | 'descending'", + "elements": [ + { + "name": "literal", + "value": "'ascending'" + }, + { + "name": "literal", + "value": "'descending'" + } + ] }, - "description": "Accessibility label for the chip. This is read by the screen reader when the user taps the chip." + "description": "Direction of sorting. An arrow indicating the direction is displayed when this is given." }, - "closeIconAccessibilityLabel": { + "numberOfLines": { "required": false, "tsType": { - "name": "string" + "name": "number" }, - "description": "Accessibility label for the close icon. This is read by the screen reader when the user taps the close icon.", + "description": "The number of lines to show.", "defaultValue": { - "value": "'Close'", + "value": "1", "computed": false } }, @@ -4466,117 +4408,120 @@ }, "description": "Function to execute on press." }, - "onLongPress": { + "style": { "required": false, "tsType": { - "name": "signature", - "type": "function", - "raw": "() => void", - "signature": { - "arguments": [], - "return": { - "name": "void" + "name": "StyleProp", + "elements": [ + { + "name": "ViewStyle" } - } + ], + "raw": "StyleProp" }, - "description": "Function to execute on long press." + "description": "" }, - "onPressIn": { + "textStyle": { "required": false, "tsType": { - "name": "signature", - "type": "function", - "raw": "(e: GestureResponderEvent) => void", - "signature": { - "arguments": [ - { - "name": "e", - "type": { - "name": "GestureResponderEvent" - } - } - ], - "return": { - "name": "void" + "name": "StyleProp", + "elements": [ + { + "name": "TextStyle" } - } + ], + "raw": "StyleProp" }, - "description": "Function to execute as soon as the touchable element is pressed and invoked even before onPress." + "description": "Text content style of the `DataTableTitle`." }, - "onPressOut": { + "maxFontSizeMultiplier": { "required": false, "tsType": { - "name": "signature", - "type": "function", - "raw": "(e: GestureResponderEvent) => void", - "signature": { - "arguments": [ - { - "name": "e", - "type": { - "name": "GestureResponderEvent" - } - } - ], - "return": { - "name": "void" - } - } + "name": "number" }, - "description": "Function to execute as soon as the touch is released even before onPress." + "description": "Specifies the largest possible scale a text font can reach." }, - "onClose": { + "theme": { "required": false, "tsType": { - "name": "signature", - "type": "function", - "raw": "() => void", - "signature": { - "arguments": [], - "return": { - "name": "void" - } - } + "name": "ThemeProp" }, - "description": "Function to execute on close button press. The close button appears only when this prop is specified." - }, - "delayLongPress": { + "description": "" + } + } + }, + "type": "component", + "dependencies": [ + "src/components/DataTable/DataTableTitle.tsx" + ], + "group": "DataTable" + }, + "Dialog/Dialog": { + "filepath": "Dialog/Dialog.tsx", + "title": "Dialog", + "description": "Dialogs inform users about a specific task and may contain critical information, require decisions, or involve multiple tasks.\nTo render the `Dialog` above other components, you'll need to wrap it with the [`Portal`](../Portal) component.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Button, Dialog, Portal, PaperProvider, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const showDialog = () => setVisible(true);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n \n \n Alert\n \n This is simple dialog\n \n \n \n \n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", + "link": "dialog", + "data": { + "description": "Dialogs inform users about a specific task and may contain critical information, require decisions, or involve multiple tasks.\nTo render the `Dialog` above other components, you'll need to wrap it with the [`Portal`](../Portal) component.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Button, Dialog, Portal, PaperProvider, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const showDialog = () => setVisible(true);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n \n \n Alert\n \n This is simple dialog\n \n \n \n \n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", + "displayName": "Dialog", + "methods": [], + "statics": [], + "props": { + "dismissable": { "required": false, "tsType": { - "name": "number" + "name": "boolean" }, - "description": "The number of milliseconds a user must touch the element before executing `onLongPress`." + "description": "Determines whether clicking outside the dialog dismiss it.", + "defaultValue": { + "value": "true", + "computed": false + } }, - "compact": { + "dismissableBackButton": { "required": false, "tsType": { "name": "boolean" }, - "description": "@supported Available in v5.x with theme version 3\nSets smaller horizontal paddings `12dp` around label, when there is only label." + "description": "Determines whether clicking Android hardware back button dismiss dialog.", + "defaultValue": { + "value": "dismissable", + "computed": true + } }, - "elevated": { + "onDismiss": { + "required": false, + "tsType": { + "name": "signature", + "type": "function", + "raw": "() => void", + "signature": { + "arguments": [], + "return": { + "name": "void" + } + } + }, + "description": "Callback that is called when the user dismisses the dialog." + }, + "visible": { "required": false, "tsType": { "name": "boolean" }, - "description": "@supported Available in v5.x with theme version 3\nWhether chip should have the elevation.", + "description": "Determines Whether the dialog is visible.", "defaultValue": { "value": "false", "computed": false } }, - "textStyle": { - "required": false, + "children": { + "required": true, "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "TextStyle" - } - ], - "raw": "StyleProp" + "name": "ReactReactNode", + "raw": "React.ReactNode" }, - "description": "Style of chip's text" + "description": "Content of the `Dialog`." }, "style": { "required": false, @@ -4597,14 +4542,6 @@ }, "description": "" }, - "hitSlop": { - "required": false, - "tsType": { - "name": "TouchableRippleProps['hitSlop']", - "raw": "TouchableRippleProps['hitSlop']" - }, - "description": "Sets additional distance outside of element in which a press can be detected." - }, "theme": { "required": false, "tsType": { @@ -4617,49 +4554,23 @@ "tsType": { "name": "string" }, - "description": "Pass down testID from chip props to touchable for Detox tests.", - "defaultValue": { - "value": "'chip'", - "computed": false - } - }, - "ellipsizeMode": { - "required": false, - "tsType": { - "name": "EllipsizeProp" - }, - "description": "Ellipsize Mode for the children text" - }, - "maxFontSizeMultiplier": { - "required": false, - "tsType": { - "name": "number" - }, - "description": "Specifies the largest possible scale a text font can reach." - }, - "role": { - "defaultValue": { - "value": "'button'", - "computed": false - }, - "required": false, - "description": "" + "description": "testID to be used on tests." } } }, "type": "component", "dependencies": [ - "src/components/Chip/Chip.tsx" + "src/components/Dialog/Dialog.tsx" ] }, - "DataTable/DataTable": { - "filepath": "DataTable/DataTable.tsx", - "title": "DataTable", - "description": "Data tables allow displaying sets of data.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [page, setPage] = React.useState(0);\n const [numberOfItemsPerPageList] = React.useState([2, 3, 4]);\n const [itemsPerPage, onItemsPerPageChange] = React.useState(\n numberOfItemsPerPageList[0]\n );\n\n const [items] = React.useState([\n {\n key: 1,\n name: 'Cupcake',\n calories: 356,\n fat: 16,\n },\n {\n key: 2,\n name: 'Eclair',\n calories: 262,\n fat: 16,\n },\n {\n key: 3,\n name: 'Frozen yogurt',\n calories: 159,\n fat: 6,\n },\n {\n key: 4,\n name: 'Gingerbread',\n calories: 305,\n fat: 3.7,\n },\n ]);\n\n const from = page * itemsPerPage;\n const to = Math.min((page + 1) * itemsPerPage, items.length);\n\n React.useEffect(() => {\n setPage(0);\n }, [itemsPerPage]);\n\n return (\n \n \n Dessert\n Calories\n Fat\n \n\n {items.slice(from, to).map((item) => (\n \n {item.name}\n {item.calories}\n {item.fat}\n \n ))}\n\n setPage(page)}\n label={`${from + 1}-${to} of ${items.length}`}\n numberOfItemsPerPageList={numberOfItemsPerPageList}\n numberOfItemsPerPage={itemsPerPage}\n onItemsPerPageChange={onItemsPerPageChange}\n showFastPaginationControls\n selectPageDropdownLabel={'Rows per page'}\n />\n \n );\n};\n\nexport default MyComponent;\n```", - "link": "data-table", + "Dialog/DialogActions": { + "filepath": "Dialog/DialogActions.tsx", + "title": "Dialog.Actions", + "description": "A component to show a list of actions in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Button, Dialog, Portal } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n \n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", + "link": "dialog-actions", "data": { - "description": "Data tables allow displaying sets of data.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [page, setPage] = React.useState(0);\n const [numberOfItemsPerPageList] = React.useState([2, 3, 4]);\n const [itemsPerPage, onItemsPerPageChange] = React.useState(\n numberOfItemsPerPageList[0]\n );\n\n const [items] = React.useState([\n {\n key: 1,\n name: 'Cupcake',\n calories: 356,\n fat: 16,\n },\n {\n key: 2,\n name: 'Eclair',\n calories: 262,\n fat: 16,\n },\n {\n key: 3,\n name: 'Frozen yogurt',\n calories: 159,\n fat: 6,\n },\n {\n key: 4,\n name: 'Gingerbread',\n calories: 305,\n fat: 3.7,\n },\n ]);\n\n const from = page * itemsPerPage;\n const to = Math.min((page + 1) * itemsPerPage, items.length);\n\n React.useEffect(() => {\n setPage(0);\n }, [itemsPerPage]);\n\n return (\n \n \n Dessert\n Calories\n Fat\n \n\n {items.slice(from, to).map((item) => (\n \n {item.name}\n {item.calories}\n {item.fat}\n \n ))}\n\n setPage(page)}\n label={`${from + 1}-${to} of ${items.length}`}\n numberOfItemsPerPageList={numberOfItemsPerPageList}\n numberOfItemsPerPage={itemsPerPage}\n onItemsPerPageChange={onItemsPerPageChange}\n showFastPaginationControls\n selectPageDropdownLabel={'Rows per page'}\n />\n \n );\n};\n\nexport default MyComponent;\n```", - "displayName": "DataTable", + "description": "A component to show a list of actions in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Button, Dialog, Portal } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n \n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", + "displayName": "Dialog.Actions", "methods": [], "statics": [], "props": { @@ -4669,7 +4580,7 @@ "name": "ReactReactNode", "raw": "React.ReactNode" }, - "description": "Content of the `DataTable`." + "description": "Content of the `DialogActions`." }, "style": { "required": false, @@ -4683,22 +4594,30 @@ "raw": "StyleProp" }, "description": "" + }, + "theme": { + "required": false, + "tsType": { + "name": "ThemeProp" + }, + "description": "" } } }, "type": "component", "dependencies": [ - "src/components/DataTable/DataTable.tsx" - ] + "src/components/Dialog/DialogActions.tsx" + ], + "group": "Dialog" }, - "DataTable/DataTableCell": { - "filepath": "DataTable/DataTableCell.tsx", - "title": "DataTable.Cell", - "description": "A component to show a single cell inside of a table.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n 1\n 2\n 3\n 4\n \n);\n\nexport default MyComponent;\n```\n\nIf you want to support multiline text, please use View instead, as multiline text doesn't comply with\nMD Guidelines (https://github.com/callstack/react-native-paper/issues/2381).\n\n@extends TouchableRipple props https://callstack.github.io/react-native-paper/docs/components/TouchableRipple", - "link": "data-table-cell", + "Dialog/DialogContent": { + "filepath": "Dialog/DialogContent.tsx", + "title": "Dialog.Content", + "description": "A component to show content in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n This is simple dialog\n \n \n \n );\n};\n\nexport default MyComponent;\n```", + "link": "dialog-content", "data": { - "description": "A component to show a single cell inside of a table.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n 1\n 2\n 3\n 4\n \n);\n\nexport default MyComponent;\n```\n\nIf you want to support multiline text, please use View instead, as multiline text doesn't comply with\nMD Guidelines (https://github.com/callstack/react-native-paper/issues/2381).\n\n@extends TouchableRipple props https://callstack.github.io/react-native-paper/docs/components/TouchableRipple", - "displayName": "DataTable.Cell", + "description": "A component to show content in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n This is simple dialog\n \n \n \n );\n};\n\nexport default MyComponent;\n```", + "displayName": "Dialog.Content", "methods": [], "statics": [], "props": { @@ -4708,36 +4627,7 @@ "name": "ReactReactNode", "raw": "React.ReactNode" }, - "description": "Content of the `DataTableCell`." - }, - "numeric": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "Align the text to the right. Generally monetary or number fields are aligned to right." - }, - "onPress": { - "required": false, - "tsType": { - "name": "signature", - "type": "function", - "raw": "(e: GestureResponderEvent) => void", - "signature": { - "arguments": [ - { - "name": "e", - "type": { - "name": "GestureResponderEvent" - } - } - ], - "return": { - "name": "void" - } - } - }, - "description": "Function to execute on press." + "description": "Content of the `DialogContent`." }, "style": { "required": false, @@ -4751,50 +4641,74 @@ "raw": "StyleProp" }, "description": "" - }, - "textStyle": { + } + } + }, + "type": "component", + "dependencies": [ + "src/components/Dialog/DialogContent.tsx" + ], + "group": "Dialog" + }, + "Dialog/DialogIcon": { + "filepath": "Dialog/DialogIcon.tsx", + "title": "Dialog.Icon", + "description": "@supported Available in v5.x with theme version 3\nA component to show an icon in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { StyleSheet } from 'react-native';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n This is a title\n \n This is simple dialog\n \n \n \n );\n};\n\nconst styles = StyleSheet.create({\n title: {\n textAlign: 'center',\n },\n})\n\nexport default MyComponent;\n```", + "link": "dialog-icon", + "data": { + "description": "@supported Available in v5.x with theme version 3\nA component to show an icon in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { StyleSheet } from 'react-native';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n This is a title\n \n This is simple dialog\n \n \n \n );\n};\n\nconst styles = StyleSheet.create({\n title: {\n textAlign: 'center',\n },\n})\n\nexport default MyComponent;\n```", + "displayName": "Dialog.Icon", + "methods": [], + "statics": [], + "props": { + "color": { "required": false, "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "TextStyle" - } - ], - "raw": "StyleProp" + "name": "ColorValue" }, - "description": "Text content style of the `DataTableCell`." + "description": "Custom color for action icon." }, - "maxFontSizeMultiplier": { + "icon": { + "required": true, + "tsType": { + "name": "IconSource" + }, + "description": "Name of the icon to show." + }, + "size": { "required": false, "tsType": { "name": "number" }, - "description": "Specifies the largest possible scale a text font can reach." + "description": "Optional icon size.", + "defaultValue": { + "value": "24", + "computed": false + } }, - "testID": { + "theme": { "required": false, "tsType": { - "name": "string" + "name": "ThemeProp" }, - "description": "testID to be used on tests." + "description": "" } } }, "type": "component", "dependencies": [ - "src/components/DataTable/DataTableCell.tsx" + "src/components/Dialog/DialogIcon.tsx" ], - "group": "DataTable" + "group": "Dialog" }, - "DataTable/DataTableHeader": { - "filepath": "DataTable/DataTableHeader.tsx", - "title": "DataTable.Header", - "description": "A component to display title in table header.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n Dessert\n \n Calories\n Fat (g)\n \n \n);\n\nexport default MyComponent;\n```", - "link": "data-table-header", + "Dialog/DialogScrollArea": { + "filepath": "Dialog/DialogScrollArea.tsx", + "title": "Dialog.ScrollArea", + "description": "A component to show a scrollable content in a Dialog. The component only provides appropriate styling.\nFor the scrollable content you can use `ScrollView`, `FlatList` etc. depending on your requirement.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { ScrollView } from 'react-native';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n \n This is a scrollable area\n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", + "link": "dialog-scroll-area", "data": { - "description": "A component to display title in table header.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n Dessert\n \n Calories\n Fat (g)\n \n \n);\n\nexport default MyComponent;\n```", - "displayName": "DataTable.Header", + "description": "A component to show a scrollable content in a Dialog. The component only provides appropriate styling.\nFor the scrollable content you can use `ScrollView`, `FlatList` etc. depending on your requirement.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { ScrollView } from 'react-native';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n \n This is a scrollable area\n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", + "displayName": "Dialog.ScrollArea", "methods": [], "statics": [], "props": { @@ -4804,7 +4718,7 @@ "name": "ReactReactNode", "raw": "React.ReactNode" }, - "description": "Content of the `DataTableHeader`." + "description": "Content of the `DialogScrollArea`." }, "style": { "required": false, @@ -4830,186 +4744,192 @@ }, "type": "component", "dependencies": [ - "src/components/DataTable/DataTableHeader.tsx" + "src/components/Dialog/DialogScrollArea.tsx" ], - "group": "DataTable" + "group": "Dialog" }, - "DataTable/DataTablePagination": { - "filepath": "DataTable/DataTablePagination.tsx", - "title": "DataTable.Pagination", - "description": "A component to show pagination for data table.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst numberOfItemsPerPageList = [2, 3, 4];\n\nconst items = [\n {\n key: 1,\n name: 'Page 1',\n },\n {\n key: 2,\n name: 'Page 2',\n },\n {\n key: 3,\n name: 'Page 3',\n },\n];\n\nconst MyComponent = () => {\n const [page, setPage] = React.useState(0);\n const [numberOfItemsPerPage, onItemsPerPageChange] = React.useState(numberOfItemsPerPageList[0]);\n const from = page * numberOfItemsPerPage;\n const to = Math.min((page + 1) * numberOfItemsPerPage, items.length);\n\n React.useEffect(() => {\n setPage(0);\n }, [numberOfItemsPerPage]);\n\n return (\n \n setPage(page)}\n label={`${from + 1}-${to} of ${items.length}`}\n showFastPaginationControls\n numberOfItemsPerPageList={numberOfItemsPerPageList}\n numberOfItemsPerPage={numberOfItemsPerPage}\n onItemsPerPageChange={onItemsPerPageChange}\n selectPageDropdownLabel={'Rows per page'}\n />\n \n );\n};\n\nexport default MyComponent;\n```", - "link": "data-table-pagination", + "Dialog/DialogTitle": { + "filepath": "Dialog/DialogTitle.tsx", + "title": "Dialog.Title", + "description": "A component to show a title in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n This is a title\n \n This is simple dialog\n \n \n \n );\n};\n\nexport default MyComponent;\n```", + "link": "dialog-title", "data": { - "description": "A component to show pagination for data table.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst numberOfItemsPerPageList = [2, 3, 4];\n\nconst items = [\n {\n key: 1,\n name: 'Page 1',\n },\n {\n key: 2,\n name: 'Page 2',\n },\n {\n key: 3,\n name: 'Page 3',\n },\n];\n\nconst MyComponent = () => {\n const [page, setPage] = React.useState(0);\n const [numberOfItemsPerPage, onItemsPerPageChange] = React.useState(numberOfItemsPerPageList[0]);\n const from = page * numberOfItemsPerPage;\n const to = Math.min((page + 1) * numberOfItemsPerPage, items.length);\n\n React.useEffect(() => {\n setPage(0);\n }, [numberOfItemsPerPage]);\n\n return (\n \n setPage(page)}\n label={`${from + 1}-${to} of ${items.length}`}\n showFastPaginationControls\n numberOfItemsPerPageList={numberOfItemsPerPageList}\n numberOfItemsPerPage={numberOfItemsPerPage}\n onItemsPerPageChange={onItemsPerPageChange}\n selectPageDropdownLabel={'Rows per page'}\n />\n \n );\n};\n\nexport default MyComponent;\n```", - "displayName": "DataTable.Pagination", + "description": "A component to show a title in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n This is a title\n \n This is simple dialog\n \n \n \n );\n};\n\nexport default MyComponent;\n```", + "displayName": "Dialog.Title", "methods": [], "statics": [], "props": { - "page": { + "children": { "required": true, "tsType": { - "name": "number" + "name": "ReactReactNode", + "raw": "React.ReactNode" }, - "description": "The currently visible page (starting with 0)." + "description": "Title text for the `DialogTitle`." }, - "numberOfPages": { - "required": true, + "style": { + "required": false, "tsType": { - "name": "number" - }, - "description": "The total number of pages." + "name": "StyleProp", + "elements": [ + { + "name": "TextStyle" + } + ], + "raw": "StyleProp" + }, + "description": "" }, - "onPageChange": { - "required": true, + "theme": { + "required": false, "tsType": { - "name": "signature", - "type": "function", - "raw": "(page: number) => void", - "signature": { - "arguments": [ - { - "name": "page", - "type": { - "name": "number" - } - } - ], - "return": { - "name": "void" - } - } + "name": "ThemeProp" }, - "description": "Function to execute on page change." + "description": "" + } + } + }, + "type": "component", + "dependencies": [ + "src/components/Dialog/DialogTitle.tsx" + ], + "group": "Dialog" + }, + "Divider": { + "filepath": "Divider.tsx", + "title": "Divider", + "description": "A divider is a thin, lightweight separator that groups content in lists and page layouts.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Divider, Text } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n Lemon\n \n Mango\n \n \n);\n\nexport default MyComponent;\n```", + "link": "divider", + "data": { + "description": "A divider is a thin, lightweight separator that groups content in lists and page layouts.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Divider, Text } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n Lemon\n \n Mango\n \n \n);\n\nexport default MyComponent;\n```", + "displayName": "Divider", + "methods": [], + "statics": [], + "props": { + "leftInset": { + "required": false, + "tsType": { + "name": "boolean" + }, + "description": "@renamed Renamed from 'inset' to 'leftInset` in v5.x\nWhether divider has a left inset." }, - "showFastPaginationControls": { + "horizontalInset": { "required": false, "tsType": { "name": "boolean" }, - "description": "Whether to show fast forward and fast rewind buttons in pagination. False by default.", + "description": "@supported Available in v5.x with theme version 3\n Whether divider has a horizontal inset on both sides.", "defaultValue": { "value": "false", "computed": false } }, - "theme": { - "required": false, - "tsType": { - "name": "ThemeProp" - }, - "description": "" - }, - "numberOfItemsPerPage": { + "bold": { "required": false, "tsType": { - "name": "number" + "name": "boolean" }, - "description": "The current number of rows per page." + "description": "@supported Available in v5.x with theme version 3\n Whether divider should be bolded.", + "defaultValue": { + "value": "false", + "computed": false + } }, - "numberOfItemsPerPageList": { + "style": { "required": false, "tsType": { - "name": "Array", + "name": "StyleProp", "elements": [ { - "name": "number" + "name": "ViewStyle" } ], - "raw": "Array" + "raw": "StyleProp" }, - "description": "Options for a number of rows per page to choose from." + "description": "" }, - "onItemsPerPageChange": { + "theme": { "required": false, "tsType": { - "name": "signature", - "type": "function", - "raw": "(numberOfItemsPerPage: number) => void", - "signature": { - "arguments": [ - { - "name": "numberOfItemsPerPage", - "type": { - "name": "number" - } - } - ], - "return": { - "name": "void" - } - } + "name": "ThemeProp" }, - "description": "The function to set the number of rows per page." - }, - "selectPageDropdownLabel": { + "description": "" + } + } + }, + "type": "component", + "dependencies": [ + "src/components/Divider.tsx" + ] + }, + "Drawer/DrawerCollapsedItem": { + "filepath": "Drawer/DrawerCollapsedItem.tsx", + "title": "Drawer.CollapsedItem", + "description": "Note: Available in v5.x with theme version 3\n\nCollapsed component used to show an action item with an icon and optionally label in a navigation drawer.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Drawer } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n);\n\nexport default MyComponent;\n```", + "link": "drawer-collapsed-item", + "data": { + "description": "Note: Available in v5.x with theme version 3\n\nCollapsed component used to show an action item with an icon and optionally label in a navigation drawer.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Drawer } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n);\n\nexport default MyComponent;\n```", + "displayName": "Drawer.CollapsedItem", + "methods": [], + "statics": [], + "props": { + "label": { "required": false, "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" + "name": "string" }, - "description": "Label text for select page dropdown to display." + "description": "The label text of the item." }, - "selectPageDropdownAccessibilityLabel": { + "badge": { "required": false, "tsType": { - "name": "string" + "name": "union", + "raw": "string | number | boolean", + "elements": [ + { + "name": "string" + }, + { + "name": "number" + }, + { + "name": "boolean" + } + ] }, - "description": "AccessibilityLabel for `selectPageDropdownLabel`." + "description": "Badge to show on the icon, can be `true` to show a dot, `string` or `number` to show text.", + "defaultValue": { + "value": "false", + "computed": false + } }, - "label": { + "disabled": { "required": false, "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" + "name": "boolean" }, - "description": "Label text to display which indicates current pagination." + "description": "Whether the item is disabled." }, - "aria-label": { + "focusedIcon": { "required": false, "tsType": { - "name": "string" + "name": "IconSource" }, - "description": "AccessibilityLabel for `label`." + "description": "@renamed Renamed from 'icon' to 'focusedIcon' in v5.x\nIcon to use as the focused destination icon, can be a string, an image source or a react component" }, - "style": { + "unfocusedIcon": { "required": false, "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" + "name": "IconSource" }, - "description": "" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/DataTable/DataTablePagination.tsx" - ], - "group": "DataTable" - }, - "DataTable/DataTableRow": { - "filepath": "DataTable/DataTableRow.tsx", - "title": "DataTable.Row", - "description": "A component to show a single row inside of a table.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n 1\n 2\n 3\n 4\n \n);\n\nexport default MyComponent;\n```\n\n@extends TouchableRipple props https://callstack.github.io/react-native-paper/docs/components/TouchableRipple", - "link": "data-table-row", - "data": { - "description": "A component to show a single row inside of a table.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n 1\n 2\n 3\n 4\n \n);\n\nexport default MyComponent;\n```\n\n@extends TouchableRipple props https://callstack.github.io/react-native-paper/docs/components/TouchableRipple", - "displayName": "DataTable.Row", - "methods": [], - "statics": [], - "props": { - "children": { - "required": true, + "description": "@renamed Renamed from 'icon' to 'focusedIcon' in v5.x\nIcon to use as the unfocused destination icon, can be a string, an image source or a react component" + }, + "active": { + "required": false, "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" + "name": "boolean" }, - "description": "Content of the `DataTableRow`." + "description": "Whether to highlight the drawer item as active." }, "onPress": { "required": false, @@ -5033,6 +4953,20 @@ }, "description": "Function to execute on press." }, + "labelMaxFontSizeMultiplier": { + "required": false, + "tsType": { + "name": "number" + }, + "description": "Specifies the largest possible scale a label font can reach." + }, + "aria-label": { + "required": false, + "tsType": { + "name": "string" + }, + "description": "Accessibility label for the button. This is read by the screen reader when the user taps the button." + }, "style": { "required": false, "tsType": { @@ -5053,76 +4987,63 @@ }, "description": "" }, - "pointerEvents": { + "testID": { "required": false, "tsType": { - "name": "ViewProps['pointerEvents']", - "raw": "ViewProps['pointerEvents']" + "name": "string" }, - "description": "`pointerEvents` passed to the `View` container, which is wrapping children within `TouchableRipple`." + "description": "TestID used for testing purposes", + "defaultValue": { + "value": "'drawer-collapsed-item'", + "computed": false + } } } }, "type": "component", "dependencies": [ - "src/components/DataTable/DataTableRow.tsx" + "src/components/Drawer/DrawerCollapsedItem.tsx" ], - "group": "DataTable" + "group": "Drawer" }, - "DataTable/DataTableTitle": { - "filepath": "DataTable/DataTableTitle.tsx", - "title": "DataTable.Title", - "description": "A component to display title in table header.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n Dessert\n \n Calories\n Fat (g)\n \n \n);\n\nexport default MyComponent;\n```", - "link": "data-table-title", + "Drawer/DrawerItem": { + "filepath": "Drawer/DrawerItem.tsx", + "title": "Drawer.Item", + "description": "A component used to show an action item with an icon and a label in a navigation drawer.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Drawer } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n);\n\nexport default MyComponent;\n```", + "link": "drawer-item", "data": { - "description": "A component to display title in table header.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { DataTable } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n Dessert\n \n Calories\n Fat (g)\n \n \n);\n\nexport default MyComponent;\n```", - "displayName": "DataTable.Title", + "description": "A component used to show an action item with an icon and a label in a navigation drawer.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Drawer } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n);\n\nexport default MyComponent;\n```", + "displayName": "Drawer.Item", "methods": [], "statics": [], "props": { - "children": { + "label": { "required": true, "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" + "name": "string" }, - "description": "Text content of the `DataTableTitle`." + "description": "The label text of the item." }, - "numeric": { + "icon": { "required": false, "tsType": { - "name": "boolean" + "name": "IconSource" }, - "description": "Align the text to the right. Generally monetary or number fields are aligned to right." + "description": "Icon to display for the `DrawerItem`." }, - "sortDirection": { + "active": { "required": false, "tsType": { - "name": "union", - "raw": "'ascending' | 'descending'", - "elements": [ - { - "name": "literal", - "value": "'ascending'" - }, - { - "name": "literal", - "value": "'descending'" - } - ] + "name": "boolean" }, - "description": "Direction of sorting. An arrow indicating the direction is displayed when this is given." + "description": "Whether to highlight the drawer item as active." }, - "numberOfLines": { + "disabled": { "required": false, "tsType": { - "name": "number" + "name": "boolean" }, - "description": "The number of lines to show.", - "defaultValue": { - "value": "1", - "computed": false - } + "description": "Whether the item is disabled." }, "onPress": { "required": false, @@ -5146,137 +5067,81 @@ }, "description": "Function to execute on press." }, - "style": { - "required": false, - "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" - }, - "description": "" - }, - "textStyle": { - "required": false, - "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "TextStyle" - } - ], - "raw": "StyleProp" - }, - "description": "Text content style of the `DataTableTitle`." - }, - "maxFontSizeMultiplier": { - "required": false, - "tsType": { - "name": "number" - }, - "description": "Specifies the largest possible scale a text font can reach." - }, - "theme": { - "required": false, - "tsType": { - "name": "ThemeProp" - }, - "description": "" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/DataTable/DataTableTitle.tsx" - ], - "group": "DataTable" - }, - "Dialog/Dialog": { - "filepath": "Dialog/Dialog.tsx", - "title": "Dialog", - "description": "Dialogs inform users about a specific task and may contain critical information, require decisions, or involve multiple tasks.\nTo render the `Dialog` above other components, you'll need to wrap it with the [`Portal`](../Portal) component.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Button, Dialog, Portal, PaperProvider, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const showDialog = () => setVisible(true);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n \n \n Alert\n \n This is simple dialog\n \n \n \n \n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", - "link": "dialog", - "data": { - "description": "Dialogs inform users about a specific task and may contain critical information, require decisions, or involve multiple tasks.\nTo render the `Dialog` above other components, you'll need to wrap it with the [`Portal`](../Portal) component.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Button, Dialog, Portal, PaperProvider, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const showDialog = () => setVisible(true);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n \n \n Alert\n \n This is simple dialog\n \n \n \n \n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", - "displayName": "Dialog", - "methods": [], - "statics": [], - "props": { - "dismissable": { + "background": { "required": false, "tsType": { - "name": "boolean" + "name": "PressableAndroidRippleConfig" }, - "description": "Determines whether clicking outside the dialog dismiss it.", - "defaultValue": { - "value": "true", - "computed": false - } + "description": "Type of background drawabale to display the feedback (Android).\nhttps://reactnative.dev/docs/pressable#rippleconfig" }, - "dismissableBackButton": { + "aria-label": { "required": false, "tsType": { - "name": "boolean" + "name": "string" }, - "description": "Determines whether clicking Android hardware back button dismiss dialog.", - "defaultValue": { - "value": "dismissable", - "computed": true - } + "description": "Accessibility label for the button. This is read by the screen reader when the user taps the button." }, - "onDismiss": { + "right": { "required": false, "tsType": { "name": "signature", "type": "function", - "raw": "() => void", + "raw": "(props: { color: ColorValue }) => React.ReactNode", "signature": { - "arguments": [], + "arguments": [ + { + "name": "props", + "type": { + "name": "signature", + "type": "object", + "raw": "{ color: ColorValue }", + "signature": { + "properties": [ + { + "key": "color", + "value": { + "name": "ColorValue", + "required": true + } + } + ] + } + } + } + ], "return": { - "name": "void" + "name": "ReactReactNode", + "raw": "React.ReactNode" } } }, - "description": "Callback that is called when the user dismisses the dialog." + "description": "Callback which returns a React element to display on the right side. For instance a Badge." }, - "visible": { + "labelMaxFontSizeMultiplier": { "required": false, "tsType": { - "name": "boolean" + "name": "number" }, - "description": "Determines Whether the dialog is visible.", - "defaultValue": { - "value": "false", - "computed": false - } + "description": "Specifies the largest possible scale a label font can reach." }, - "children": { - "required": true, + "hitSlop": { + "required": false, "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" + "name": "TouchableRippleProps['hitSlop']", + "raw": "TouchableRippleProps['hitSlop']" }, - "description": "Content of the `Dialog`." + "description": "Sets additional distance outside of element in which a press can be detected." }, "style": { "required": false, "tsType": { - "name": "Animated.WithAnimatedValue", + "name": "StyleProp", "elements": [ { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" + "name": "ViewStyle" } ], - "raw": "Animated.WithAnimatedValue>" + "raw": "StyleProp" }, "description": "" }, @@ -5286,39 +5151,58 @@ "name": "ThemeProp" }, "description": "" - }, - "testID": { - "required": false, - "tsType": { - "name": "string" - }, - "description": "testID to be used on tests." } } }, "type": "component", "dependencies": [ - "src/components/Dialog/Dialog.tsx" - ] + "src/components/Drawer/DrawerItem.tsx" + ], + "group": "Drawer" }, - "Dialog/DialogActions": { - "filepath": "Dialog/DialogActions.tsx", - "title": "Dialog.Actions", - "description": "A component to show a list of actions in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Button, Dialog, Portal } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n \n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", - "link": "dialog-actions", + "Drawer/DrawerSection": { + "filepath": "Drawer/DrawerSection.tsx", + "title": "Drawer.Section", + "description": "A component to group content inside a navigation drawer.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Drawer } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [active, setActive] = React.useState('');\n\n return (\n \n setActive('first')}\n />\n setActive('second')}\n />\n \n );\n};\n\nexport default MyComponent;\n```", + "link": "drawer-section", "data": { - "description": "A component to show a list of actions in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Button, Dialog, Portal } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n \n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", - "displayName": "Dialog.Actions", + "description": "A component to group content inside a navigation drawer.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Drawer } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [active, setActive] = React.useState('');\n\n return (\n \n setActive('first')}\n />\n setActive('second')}\n />\n \n );\n};\n\nexport default MyComponent;\n```", + "displayName": "Drawer.Section", "methods": [], "statics": [], "props": { + "title": { + "required": false, + "tsType": { + "name": "string" + }, + "description": "Title to show as the header for the section." + }, "children": { "required": true, "tsType": { "name": "ReactReactNode", "raw": "React.ReactNode" }, - "description": "Content of the `DialogActions`." + "description": "Content of the `Drawer.Section`." + }, + "showDivider": { + "required": false, + "tsType": { + "name": "boolean" + }, + "description": "Whether to show `Divider` at the end of the section. True by default.", + "defaultValue": { + "value": "true", + "computed": false + } + }, + "titleMaxFontSizeMultiplier": { + "required": false, + "tsType": { + "name": "number" + }, + "description": "Specifies the largest possible scale a title font can reach." }, "style": { "required": false, @@ -5344,234 +5228,148 @@ }, "type": "component", "dependencies": [ - "src/components/Dialog/DialogActions.tsx" + "src/components/Drawer/DrawerSection.tsx" ], - "group": "Dialog" + "group": "Drawer" }, - "Dialog/DialogContent": { - "filepath": "Dialog/DialogContent.tsx", - "title": "Dialog.Content", - "description": "A component to show content in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n This is simple dialog\n \n \n \n );\n};\n\nexport default MyComponent;\n```", - "link": "dialog-content", + "FAB/FAB": { + "filepath": "FAB/FAB.tsx", + "title": "FAB", + "description": "A floating action button represents the primary action on a screen.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { StyleSheet } from 'react-native';\nimport { FAB } from 'react-native-paper';\n\nconst MyComponent = () => (\n console.log('Pressed')}\n />\n);\n\nconst styles = StyleSheet.create({\n fab: {\n position: 'absolute',\n margin: 16,\n right: 0,\n bottom: 0,\n },\n});\n\nexport default MyComponent;\n```", + "link": "fab", "data": { - "description": "A component to show content in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n This is simple dialog\n \n \n \n );\n};\n\nexport default MyComponent;\n```", - "displayName": "Dialog.Content", + "description": "A floating action button represents the primary action on a screen.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { StyleSheet } from 'react-native';\nimport { FAB } from 'react-native-paper';\n\nconst MyComponent = () => (\n console.log('Pressed')}\n />\n);\n\nconst styles = StyleSheet.create({\n fab: {\n position: 'absolute',\n margin: 16,\n right: 0,\n bottom: 0,\n },\n});\n\nexport default MyComponent;\n```", + "displayName": "FAB", "methods": [], "statics": [], "props": { - "children": { + "icon": { "required": true, "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" + "name": "IconSource" }, - "description": "Content of the `DialogContent`." + "description": "Icon to display inside the FAB." }, - "style": { + "variant": { "required": false, "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" + "name": "Variant" }, - "description": "" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/Dialog/DialogContent.tsx" - ], - "group": "Dialog" - }, - "Dialog/DialogIcon": { - "filepath": "Dialog/DialogIcon.tsx", - "title": "Dialog.Icon", - "description": "@supported Available in v5.x with theme version 3\nA component to show an icon in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { StyleSheet } from 'react-native';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n This is a title\n \n This is simple dialog\n \n \n \n );\n};\n\nconst styles = StyleSheet.create({\n title: {\n textAlign: 'center',\n },\n})\n\nexport default MyComponent;\n```", - "link": "dialog-icon", - "data": { - "description": "@supported Available in v5.x with theme version 3\nA component to show an icon in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { StyleSheet } from 'react-native';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n This is a title\n \n This is simple dialog\n \n \n \n );\n};\n\nconst styles = StyleSheet.create({\n title: {\n textAlign: 'center',\n },\n})\n\nexport default MyComponent;\n```", - "displayName": "Dialog.Icon", - "methods": [], - "statics": [], - "props": { - "color": { + "description": "Role-color preset. Defaults to `tonalPrimary`.", + "defaultValue": { + "value": "'tonalPrimary'", + "computed": false + } + }, + "containerColor": { "required": false, "tsType": { "name": "ColorValue" }, - "description": "Custom color for action icon." + "description": "Override the container (background) color." }, - "icon": { - "required": true, + "contentColor": { + "required": false, "tsType": { - "name": "IconSource" + "name": "ColorValue" }, - "description": "Name of the icon to show." + "description": "Override the content (icon) color." }, "size": { "required": false, "tsType": { - "name": "number" + "name": "Size" }, - "description": "Optional icon size.", + "description": "Spec size. Defaults to `default`.", "defaultValue": { - "value": "24", + "value": "'default'", "computed": false } }, - "theme": { + "visible": { "required": false, "tsType": { - "name": "ThemeProp" - }, - "description": "" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/Dialog/DialogIcon.tsx" - ], - "group": "Dialog" - }, - "Dialog/DialogScrollArea": { - "filepath": "Dialog/DialogScrollArea.tsx", - "title": "Dialog.ScrollArea", - "description": "A component to show a scrollable content in a Dialog. The component only provides appropriate styling.\nFor the scrollable content you can use `ScrollView`, `FlatList` etc. depending on your requirement.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { ScrollView } from 'react-native';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n \n This is a scrollable area\n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", - "link": "dialog-scroll-area", - "data": { - "description": "A component to show a scrollable content in a Dialog. The component only provides appropriate styling.\nFor the scrollable content you can use `ScrollView`, `FlatList` etc. depending on your requirement.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { ScrollView } from 'react-native';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n \n \n This is a scrollable area\n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", - "displayName": "Dialog.ScrollArea", - "methods": [], - "statics": [], - "props": { - "children": { - "required": true, - "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" + "name": "boolean" }, - "description": "Content of the `DialogScrollArea`." + "description": "Whether the FAB is currently visible. Toggling animates the spec'd enter\nand exit (scale + alpha) on the FAB itself.", + "defaultValue": { + "value": "true", + "computed": false + } }, - "style": { + "onPress": { "required": false, "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" + "name": "signature", + "type": "function", + "raw": "(e: GestureResponderEvent) => void", + "signature": { + "arguments": [ + { + "name": "e", + "type": { + "name": "GestureResponderEvent" + } + } + ], + "return": { + "name": "void" } - ], - "raw": "StyleProp" + } }, - "description": "" + "description": "Function to execute on press." }, - "theme": { + "aria-label": { "required": false, "tsType": { - "name": "ThemeProp" - }, - "description": "" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/Dialog/DialogScrollArea.tsx" - ], - "group": "Dialog" - }, - "Dialog/DialogTitle": { - "filepath": "Dialog/DialogTitle.tsx", - "title": "Dialog.Title", - "description": "A component to show a title in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n This is a title\n \n This is simple dialog\n \n \n \n );\n};\n\nexport default MyComponent;\n```", - "link": "dialog-title", - "data": { - "description": "A component to show a title in a Dialog.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Dialog, Portal, Text } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const hideDialog = () => setVisible(false);\n\n return (\n \n \n This is a title\n \n This is simple dialog\n \n \n \n );\n};\n\nexport default MyComponent;\n```", - "displayName": "Dialog.Title", - "methods": [], - "statics": [], - "props": { - "children": { - "required": true, - "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" + "name": "string" }, - "description": "Title text for the `DialogTitle`." + "description": "Accessibility label. Falls back to nothing if unset." }, - "style": { + "aria-checked": { "required": false, "tsType": { - "name": "StyleProp", + "name": "union", + "raw": "boolean | 'mixed'", "elements": [ { - "name": "TextStyle" + "name": "boolean" + }, + { + "name": "literal", + "value": "'mixed'" } - ], - "raw": "StyleProp" + ] }, - "description": "" + "description": "Indicates whether the element is checked. Accepts `true`, `false`,\nor `'mixed'` for an indeterminate state." }, - "theme": { + "aria-selected": { "required": false, "tsType": { - "name": "ThemeProp" + "name": "boolean" }, - "description": "" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/Dialog/DialogTitle.tsx" - ], - "group": "Dialog" - }, - "Divider": { - "filepath": "Divider.tsx", - "title": "Divider", - "description": "A divider is a thin, lightweight separator that groups content in lists and page layouts.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Divider, Text } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n Lemon\n \n Mango\n \n \n);\n\nexport default MyComponent;\n```", - "link": "divider", - "data": { - "description": "A divider is a thin, lightweight separator that groups content in lists and page layouts.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Divider, Text } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n Lemon\n \n Mango\n \n \n);\n\nexport default MyComponent;\n```", - "displayName": "Divider", - "methods": [], - "statics": [], - "props": { - "leftInset": { + "description": "Indicates whether the element is selected." + }, + "aria-busy": { "required": false, "tsType": { "name": "boolean" }, - "description": "@renamed Renamed from 'inset' to 'leftInset` in v5.x\nWhether divider has a left inset." + "description": "Indicates whether the element is currently busy (e.g. loading)." }, - "horizontalInset": { + "aria-expanded": { "required": false, "tsType": { "name": "boolean" }, - "description": "@supported Available in v5.x with theme version 3\n Whether divider has a horizontal inset on both sides.", - "defaultValue": { - "value": "false", - "computed": false - } + "description": "Indicates whether the element's controlled content is expanded." }, - "bold": { + "background": { "required": false, "tsType": { - "name": "boolean" + "name": "PressableAndroidRippleConfig" }, - "description": "@supported Available in v5.x with theme version 3\n Whether divider should be bolded.", - "defaultValue": { - "value": "false", - "computed": false - } + "description": "Type of background drawable to display the feedback (Android).\nhttps://reactnative.dev/docs/pressable#rippleconfig" }, "style": { "required": false, @@ -5584,7 +5382,18 @@ ], "raw": "StyleProp" }, - "description": "" + "description": "Style for positioning the FAB. The visual treatment (size, shape, color)\nis driven by `variant` and `size`." + }, + "testID": { + "required": false, + "tsType": { + "name": "string" + }, + "description": "TestID used for testing purposes.", + "defaultValue": { + "value": "'floating-action-button'", + "computed": false + } }, "theme": { "required": false, @@ -5592,82 +5401,105 @@ "name": "ThemeProp" }, "description": "" + }, + "ref": { + "required": false, + "tsType": { + "name": "ReactRef", + "raw": "React.Ref", + "elements": [ + { + "name": "View" + } + ] + }, + "description": "" } } }, "type": "component", "dependencies": [ - "src/components/Divider.tsx" + "src/components/FAB/FAB.tsx" ] }, - "Drawer/DrawerCollapsedItem": { - "filepath": "Drawer/DrawerCollapsedItem.tsx", - "title": "Drawer.CollapsedItem", - "description": "Note: Available in v5.x with theme version 3\n\nCollapsed component used to show an action item with an icon and optionally label in a navigation drawer.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Drawer } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n);\n\nexport default MyComponent;\n```", - "link": "drawer-collapsed-item", - "data": { - "description": "Note: Available in v5.x with theme version 3\n\nCollapsed component used to show an action item with an icon and optionally label in a navigation drawer.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Drawer } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n);\n\nexport default MyComponent;\n```", - "displayName": "Drawer.CollapsedItem", + "FAB/Extended": { + "filepath": "FAB/Extended.tsx", + "title": "Extended", + "description": "An extended floating action button represents the primary action on a screen\nand shows a label next to the icon. Animates between expanded (icon + label)\nand collapsed (icon only) states.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { StyleSheet } from 'react-native';\nimport { FAB } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [expanded, setExpanded] = React.useState(true);\n\n return (\n setExpanded((v) => !v)}\n style={styles.fab}\n />\n );\n};\n\nconst styles = StyleSheet.create({\n fab: {\n position: 'absolute',\n margin: 16,\n left: 0,\n bottom: 0,\n },\n});\n\nexport default MyComponent;\n```", + "link": "extended", + "data": { + "description": "An extended floating action button represents the primary action on a screen\nand shows a label next to the icon. Animates between expanded (icon + label)\nand collapsed (icon only) states.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { StyleSheet } from 'react-native';\nimport { FAB } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [expanded, setExpanded] = React.useState(true);\n\n return (\n setExpanded((v) => !v)}\n style={styles.fab}\n />\n );\n};\n\nconst styles = StyleSheet.create({\n fab: {\n position: 'absolute',\n margin: 16,\n left: 0,\n bottom: 0,\n },\n});\n\nexport default MyComponent;\n```", + "displayName": "Extended", "methods": [], "statics": [], "props": { + "icon": { + "required": true, + "tsType": { + "name": "IconSource" + }, + "description": "Icon to display inside the FAB." + }, "label": { - "required": false, + "required": true, "tsType": { "name": "string" }, - "description": "The label text of the item." + "description": "Label rendered next to the icon when expanded." }, - "badge": { + "variant": { "required": false, "tsType": { - "name": "union", - "raw": "string | number | boolean", - "elements": [ - { - "name": "string" - }, - { - "name": "number" - }, - { - "name": "boolean" - } - ] + "name": "Variant" }, - "description": "Badge to show on the icon, can be `true` to show a dot, `string` or `number` to show text.", + "description": "Role-color preset. Defaults to `tonalPrimary`.", "defaultValue": { - "value": "false", + "value": "'tonalPrimary'", "computed": false } }, - "disabled": { + "containerColor": { "required": false, "tsType": { - "name": "boolean" + "name": "ColorValue" }, - "description": "Whether the item is disabled." + "description": "Override the container (background) color. When set without `contentColor`,\nthe icon and label colors are derived automatically via `contentColorFor`." }, - "focusedIcon": { + "contentColor": { "required": false, "tsType": { - "name": "IconSource" + "name": "ColorValue" }, - "description": "@renamed Renamed from 'icon' to 'focusedIcon' in v5.x\nIcon to use as the focused destination icon, can be a string, an image source or a react component" + "description": "Override the content (icon + label) color." }, - "unfocusedIcon": { + "size": { "required": false, "tsType": { - "name": "IconSource" + "name": "Size" }, - "description": "@renamed Renamed from 'icon' to 'focusedIcon' in v5.x\nIcon to use as the unfocused destination icon, can be a string, an image source or a react component" + "description": "Spec size. Defaults to `default`.", + "defaultValue": { + "value": "'default'", + "computed": false + } }, - "active": { + "expanded": { + "required": true, + "tsType": { + "name": "boolean" + }, + "description": "Whether the FAB is expanded (icon + label) or collapsed (icon only). The\nwidth and label opacity animate per the MD3 Expressive spec on change." + }, + "visible": { "required": false, "tsType": { "name": "boolean" }, - "description": "Whether to highlight the drawer item as active." + "description": "Whether the FAB is currently visible. Toggling animates the spec'd enter\nand exit (scale + alpha) on the FAB itself.", + "defaultValue": { + "value": "true", + "computed": false + } }, "onPress": { "required": false, @@ -5691,6 +5523,55 @@ }, "description": "Function to execute on press." }, + "aria-label": { + "required": false, + "tsType": { + "name": "string" + }, + "description": "Accessibility label. Falls back to `label` if unset.", + "defaultValue": { + "value": "label", + "computed": true + } + }, + "aria-checked": { + "required": false, + "tsType": { + "name": "union", + "raw": "boolean | 'mixed'", + "elements": [ + { + "name": "boolean" + }, + { + "name": "literal", + "value": "'mixed'" + } + ] + }, + "description": "Indicates whether the element is checked. Accepts `true`, `false`,\nor `'mixed'` for an indeterminate state." + }, + "aria-selected": { + "required": false, + "tsType": { + "name": "boolean" + }, + "description": "Indicates whether the element is selected." + }, + "aria-busy": { + "required": false, + "tsType": { + "name": "boolean" + }, + "description": "Indicates whether the element is currently busy (e.g. loading)." + }, + "aria-expanded": { + "required": false, + "tsType": { + "name": "boolean" + }, + "description": "Indicates whether the element's controlled content is expanded." + }, "labelMaxFontSizeMultiplier": { "required": false, "tsType": { @@ -5698,12 +5579,12 @@ }, "description": "Specifies the largest possible scale a label font can reach." }, - "aria-label": { + "background": { "required": false, "tsType": { - "name": "string" + "name": "PressableAndroidRippleConfig" }, - "description": "Accessibility label for the button. This is read by the screen reader when the user taps the button." + "description": "Type of background drawable to display the feedback (Android).\nhttps://reactnative.dev/docs/pressable#rippleconfig" }, "style": { "required": false, @@ -5716,7 +5597,18 @@ ], "raw": "StyleProp" }, - "description": "" + "description": "Style for positioning the FAB. The visual treatment (size, shape, color)\nis driven by `variant` and `size`." + }, + "testID": { + "required": false, + "tsType": { + "name": "string" + }, + "description": "TestID used for testing purposes.", + "defaultValue": { + "value": "'extended-floating-action-button'", + "computed": false + } }, "theme": { "required": false, @@ -5725,819 +5617,189 @@ }, "description": "" }, - "testID": { + "ref": { "required": false, "tsType": { - "name": "string" + "name": "ReactRef", + "raw": "React.Ref", + "elements": [ + { + "name": "View" + } + ] }, - "description": "TestID used for testing purposes", - "defaultValue": { - "value": "'drawer-collapsed-item'", - "computed": false - } + "description": "" } } }, "type": "component", "dependencies": [ - "src/components/Drawer/DrawerCollapsedItem.tsx" - ], - "group": "Drawer" + "src/components/FAB/Extended.tsx" + ] }, - "Drawer/DrawerItem": { - "filepath": "Drawer/DrawerItem.tsx", - "title": "Drawer.Item", - "description": "A component used to show an action item with an icon and a label in a navigation drawer.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Drawer } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n);\n\nexport default MyComponent;\n```", - "link": "drawer-item", + "FAB/Menu": { + "filepath": "FAB/Menu.tsx", + "title": "Menu", + "description": "Floating action button menu. Wraps a trigger FAB; when `expanded` is true,\nitems appear stacked above and the trigger morphs into the spec'd close\nbutton (`shape: 'full'`, 56 dp, saturated role color).\n\nNo visual backdrop and no outside-tap dismiss — that matches the MD3 spec\nand lets the user keep interacting with the content underneath. Dismiss\nvia the close button or by tapping an item.\n\n## Usage\n```tsx\nconst [open, setOpen] = React.useState(false);\n\n\n setOpen(false)}\n trigger={{ icon: 'plus', variant: 'primary', onPress: () => setOpen(true) }}\n items={[\n { icon: 'email', label: 'Send', onPress: () => {} },\n { icon: 'bell', label: 'Remind', onPress: () => {} },\n ]}\n />\n\n```", + "link": "menu", "data": { - "description": "A component used to show an action item with an icon and a label in a navigation drawer.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Drawer } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n);\n\nexport default MyComponent;\n```", - "displayName": "Drawer.Item", + "description": "Floating action button menu. Wraps a trigger FAB; when `expanded` is true,\nitems appear stacked above and the trigger morphs into the spec'd close\nbutton (`shape: 'full'`, 56 dp, saturated role color).\n\nNo visual backdrop and no outside-tap dismiss — that matches the MD3 spec\nand lets the user keep interacting with the content underneath. Dismiss\nvia the close button or by tapping an item.\n\n## Usage\n```tsx\nconst [open, setOpen] = React.useState(false);\n\n\n setOpen(false)}\n trigger={{ icon: 'plus', variant: 'primary', onPress: () => setOpen(true) }}\n items={[\n { icon: 'email', label: 'Send', onPress: () => {} },\n { icon: 'bell', label: 'Remind', onPress: () => {} },\n ]}\n />\n\n```", + "displayName": "Menu", "methods": [], "statics": [], "props": { - "label": { + "expanded": { "required": true, - "tsType": { - "name": "string" - }, - "description": "The label text of the item." - }, - "icon": { - "required": false, - "tsType": { - "name": "IconSource" - }, - "description": "Icon to display for the `DrawerItem`." - }, - "active": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "Whether to highlight the drawer item as active." - }, - "disabled": { - "required": false, "tsType": { "name": "boolean" }, - "description": "Whether the item is disabled." + "description": "Whether the menu is open." }, - "onPress": { - "required": false, + "onDismiss": { + "required": true, "tsType": { "name": "signature", "type": "function", - "raw": "(e: GestureResponderEvent) => void", + "raw": "() => void", "signature": { - "arguments": [ - { - "name": "e", - "type": { - "name": "GestureResponderEvent" - } - } - ], + "arguments": [], "return": { "name": "void" } } }, - "description": "Function to execute on press." - }, - "background": { - "required": false, - "tsType": { - "name": "PressableAndroidRippleConfig" - }, - "description": "Type of background drawabale to display the feedback (Android).\nhttps://reactnative.dev/docs/pressable#rippleconfig" - }, - "aria-label": { - "required": false, - "tsType": { - "name": "string" - }, - "description": "Accessibility label for the button. This is read by the screen reader when the user taps the button." + "description": "Called when the user taps the close button or taps an item." }, - "right": { - "required": false, + "trigger": { + "required": true, "tsType": { "name": "signature", - "type": "function", - "raw": "(props: { color: ColorValue }) => React.ReactNode", + "type": "object", + "raw": "{\n /**\n * Icon displayed in the trigger FAB (and cross-faded to `closeIcon` when\n * the menu is open).\n */\n icon: IconSource;\n variant?: Variant;\n size?: Size;\n containerColor?: ColorValue;\n contentColor?: ColorValue;\n visible?: boolean;\n onPress?: (e: GestureResponderEvent) => void;\n /**\n * Accessibility label for the trigger FAB.\n */\n 'aria-label'?: string;\n testID?: string;\n}", "signature": { - "arguments": [ + "properties": [ { - "name": "props", - "type": { - "name": "signature", - "type": "object", - "raw": "{ color: ColorValue }", - "signature": { - "properties": [ - { - "key": "color", - "value": { - "name": "ColorValue", - "required": true + "key": "icon", + "value": { + "name": "IconSource", + "required": true + } + }, + { + "key": "variant", + "value": { + "name": "Variant", + "required": false + } + }, + { + "key": "size", + "value": { + "name": "Size", + "required": false + } + }, + { + "key": "containerColor", + "value": { + "name": "ColorValue", + "required": false + } + }, + { + "key": "contentColor", + "value": { + "name": "ColorValue", + "required": false + } + }, + { + "key": "visible", + "value": { + "name": "boolean", + "required": false + } + }, + { + "key": "onPress", + "value": { + "name": "signature", + "type": "function", + "raw": "(e: GestureResponderEvent) => void", + "signature": { + "arguments": [ + { + "name": "e", + "type": { + "name": "GestureResponderEvent" } } - ] - } + ], + "return": { + "name": "void" + } + }, + "required": false + } + }, + { + "key": "aria-label", + "value": { + "name": "string", + "required": false + } + }, + { + "key": "testID", + "value": { + "name": "string", + "required": false } } - ], - "return": { - "name": "ReactReactNode", - "raw": "React.ReactNode" - } + ] } }, - "description": "Callback which returns a React element to display on the right side. For instance a Badge." - }, - "labelMaxFontSizeMultiplier": { - "required": false, - "tsType": { - "name": "number" - }, - "description": "Specifies the largest possible scale a label font can reach." - }, - "hitSlop": { - "required": false, - "tsType": { - "name": "TouchableRippleProps['hitSlop']", - "raw": "TouchableRippleProps['hitSlop']" - }, - "description": "Sets additional distance outside of element in which a press can be detected." + "description": "Trigger FAB configuration. The menu renders a morphing FAB that\nanimates between the trigger appearance and the spec'd close button." }, - "style": { + "alignment": { "required": false, "tsType": { - "name": "StyleProp", + "name": "union", + "raw": "'start' | 'center' | 'end'", "elements": [ { - "name": "ViewStyle" + "name": "literal", + "value": "'start'" + }, + { + "name": "literal", + "value": "'center'" + }, + { + "name": "literal", + "value": "'end'" } - ], - "raw": "StyleProp" - }, - "description": "" - }, - "theme": { - "required": false, - "tsType": { - "name": "ThemeProp" - }, - "description": "" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/Drawer/DrawerItem.tsx" - ], - "group": "Drawer" - }, - "Drawer/DrawerSection": { - "filepath": "Drawer/DrawerSection.tsx", - "title": "Drawer.Section", - "description": "A component to group content inside a navigation drawer.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Drawer } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [active, setActive] = React.useState('');\n\n return (\n \n setActive('first')}\n />\n setActive('second')}\n />\n \n );\n};\n\nexport default MyComponent;\n```", - "link": "drawer-section", - "data": { - "description": "A component to group content inside a navigation drawer.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Drawer } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [active, setActive] = React.useState('');\n\n return (\n \n setActive('first')}\n />\n setActive('second')}\n />\n \n );\n};\n\nexport default MyComponent;\n```", - "displayName": "Drawer.Section", - "methods": [], - "statics": [], - "props": { - "title": { - "required": false, - "tsType": { - "name": "string" - }, - "description": "Title to show as the header for the section." - }, - "children": { - "required": true, - "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" - }, - "description": "Content of the `Drawer.Section`." - }, - "showDivider": { - "required": false, - "tsType": { - "name": "boolean" + ] }, - "description": "Whether to show `Divider` at the end of the section. True by default.", + "description": "Horizontal side the menu sits on. Default `'end'`.", "defaultValue": { - "value": "true", + "value": "'end'", "computed": false } }, - "titleMaxFontSizeMultiplier": { - "required": false, - "tsType": { - "name": "number" - }, - "description": "Specifies the largest possible scale a title font can reach." - }, - "style": { - "required": false, - "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" - }, - "description": "" - }, - "theme": { + "closeIcon": { "required": false, - "tsType": { - "name": "ThemeProp" - }, - "description": "" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/Drawer/DrawerSection.tsx" - ], - "group": "Drawer" - }, - "FAB/FAB": { - "filepath": "FAB/FAB.tsx", - "title": "FAB", - "description": "A floating action button represents the primary action on a screen.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { StyleSheet } from 'react-native';\nimport { FAB } from 'react-native-paper';\n\nconst MyComponent = () => (\n console.log('Pressed')}\n />\n);\n\nconst styles = StyleSheet.create({\n fab: {\n position: 'absolute',\n margin: 16,\n right: 0,\n bottom: 0,\n },\n});\n\nexport default MyComponent;\n```", - "link": "fab", - "data": { - "description": "A floating action button represents the primary action on a screen.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { StyleSheet } from 'react-native';\nimport { FAB } from 'react-native-paper';\n\nconst MyComponent = () => (\n console.log('Pressed')}\n />\n);\n\nconst styles = StyleSheet.create({\n fab: {\n position: 'absolute',\n margin: 16,\n right: 0,\n bottom: 0,\n },\n});\n\nexport default MyComponent;\n```", - "displayName": "FAB", - "methods": [], - "statics": [], - "props": { - "icon": { - "required": true, "tsType": { "name": "IconSource" }, - "description": "Icon to display inside the FAB." - }, - "variant": { - "required": false, - "tsType": { - "name": "Variant" - }, - "description": "Role-color preset. Defaults to `tonalPrimary`.", + "description": "Icon used by the close button when the menu is expanded. Default\n`'close'`.", "defaultValue": { - "value": "'tonalPrimary'", + "value": "'close'", "computed": false } }, - "containerColor": { - "required": false, - "tsType": { - "name": "ColorValue" - }, - "description": "Override the container (background) color." - }, - "contentColor": { - "required": false, - "tsType": { - "name": "ColorValue" - }, - "description": "Override the content (icon) color." - }, - "size": { - "required": false, - "tsType": { - "name": "Size" - }, - "description": "Spec size. Defaults to `default`.", - "defaultValue": { - "value": "'default'", - "computed": false - } - }, - "visible": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "Whether the FAB is currently visible. Toggling animates the spec'd enter\nand exit (scale + alpha) on the FAB itself.", - "defaultValue": { - "value": "true", - "computed": false - } - }, - "onPress": { - "required": false, - "tsType": { - "name": "signature", - "type": "function", - "raw": "(e: GestureResponderEvent) => void", - "signature": { - "arguments": [ - { - "name": "e", - "type": { - "name": "GestureResponderEvent" - } - } - ], - "return": { - "name": "void" - } - } - }, - "description": "Function to execute on press." - }, - "aria-label": { - "required": false, - "tsType": { - "name": "string" - }, - "description": "Accessibility label. Falls back to nothing if unset." - }, - "aria-checked": { - "required": false, - "tsType": { - "name": "union", - "raw": "boolean | 'mixed'", - "elements": [ - { - "name": "boolean" - }, - { - "name": "literal", - "value": "'mixed'" - } - ] - }, - "description": "Indicates whether the element is checked. Accepts `true`, `false`,\nor `'mixed'` for an indeterminate state." - }, - "aria-selected": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "Indicates whether the element is selected." - }, - "aria-busy": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "Indicates whether the element is currently busy (e.g. loading)." - }, - "aria-expanded": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "Indicates whether the element's controlled content is expanded." - }, - "background": { - "required": false, - "tsType": { - "name": "PressableAndroidRippleConfig" - }, - "description": "Type of background drawable to display the feedback (Android).\nhttps://reactnative.dev/docs/pressable#rippleconfig" - }, - "style": { - "required": false, - "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" - }, - "description": "Style for positioning the FAB. The visual treatment (size, shape, color)\nis driven by `variant` and `size`." - }, - "testID": { - "required": false, - "tsType": { - "name": "string" - }, - "description": "TestID used for testing purposes.", - "defaultValue": { - "value": "'floating-action-button'", - "computed": false - } - }, - "theme": { - "required": false, - "tsType": { - "name": "ThemeProp" - }, - "description": "" - }, - "ref": { - "required": false, - "tsType": { - "name": "ReactRef", - "raw": "React.Ref", - "elements": [ - { - "name": "View" - } - ] - }, - "description": "" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/FAB/FAB.tsx" - ] - }, - "FAB/Extended": { - "filepath": "FAB/Extended.tsx", - "title": "Extended", - "description": "An extended floating action button represents the primary action on a screen\nand shows a label next to the icon. Animates between expanded (icon + label)\nand collapsed (icon only) states.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { StyleSheet } from 'react-native';\nimport { FAB } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [expanded, setExpanded] = React.useState(true);\n\n return (\n setExpanded((v) => !v)}\n style={styles.fab}\n />\n );\n};\n\nconst styles = StyleSheet.create({\n fab: {\n position: 'absolute',\n margin: 16,\n left: 0,\n bottom: 0,\n },\n});\n\nexport default MyComponent;\n```", - "link": "extended", - "data": { - "description": "An extended floating action button represents the primary action on a screen\nand shows a label next to the icon. Animates between expanded (icon + label)\nand collapsed (icon only) states.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { StyleSheet } from 'react-native';\nimport { FAB } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [expanded, setExpanded] = React.useState(true);\n\n return (\n setExpanded((v) => !v)}\n style={styles.fab}\n />\n );\n};\n\nconst styles = StyleSheet.create({\n fab: {\n position: 'absolute',\n margin: 16,\n left: 0,\n bottom: 0,\n },\n});\n\nexport default MyComponent;\n```", - "displayName": "Extended", - "methods": [], - "statics": [], - "props": { - "icon": { - "required": true, - "tsType": { - "name": "IconSource" - }, - "description": "Icon to display inside the FAB." - }, - "label": { - "required": true, - "tsType": { - "name": "string" - }, - "description": "Label rendered next to the icon when expanded." - }, - "variant": { - "required": false, - "tsType": { - "name": "Variant" - }, - "description": "Role-color preset. Defaults to `tonalPrimary`.", - "defaultValue": { - "value": "'tonalPrimary'", - "computed": false - } - }, - "containerColor": { - "required": false, - "tsType": { - "name": "ColorValue" - }, - "description": "Override the container (background) color. When set without `contentColor`,\nthe icon and label colors are derived automatically via `contentColorFor`." - }, - "contentColor": { - "required": false, - "tsType": { - "name": "ColorValue" - }, - "description": "Override the content (icon + label) color." - }, - "size": { - "required": false, - "tsType": { - "name": "Size" - }, - "description": "Spec size. Defaults to `default`.", - "defaultValue": { - "value": "'default'", - "computed": false - } - }, - "expanded": { - "required": true, - "tsType": { - "name": "boolean" - }, - "description": "Whether the FAB is expanded (icon + label) or collapsed (icon only). The\nwidth and label opacity animate per the MD3 Expressive spec on change." - }, - "visible": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "Whether the FAB is currently visible. Toggling animates the spec'd enter\nand exit (scale + alpha) on the FAB itself.", - "defaultValue": { - "value": "true", - "computed": false - } - }, - "onPress": { - "required": false, - "tsType": { - "name": "signature", - "type": "function", - "raw": "(e: GestureResponderEvent) => void", - "signature": { - "arguments": [ - { - "name": "e", - "type": { - "name": "GestureResponderEvent" - } - } - ], - "return": { - "name": "void" - } - } - }, - "description": "Function to execute on press." - }, - "aria-label": { - "required": false, - "tsType": { - "name": "string" - }, - "description": "Accessibility label. Falls back to `label` if unset.", - "defaultValue": { - "value": "label", - "computed": true - } - }, - "aria-checked": { - "required": false, - "tsType": { - "name": "union", - "raw": "boolean | 'mixed'", - "elements": [ - { - "name": "boolean" - }, - { - "name": "literal", - "value": "'mixed'" - } - ] - }, - "description": "Indicates whether the element is checked. Accepts `true`, `false`,\nor `'mixed'` for an indeterminate state." - }, - "aria-selected": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "Indicates whether the element is selected." - }, - "aria-busy": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "Indicates whether the element is currently busy (e.g. loading)." - }, - "aria-expanded": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "Indicates whether the element's controlled content is expanded." - }, - "labelMaxFontSizeMultiplier": { - "required": false, - "tsType": { - "name": "number" - }, - "description": "Specifies the largest possible scale a label font can reach." - }, - "background": { - "required": false, - "tsType": { - "name": "PressableAndroidRippleConfig" - }, - "description": "Type of background drawable to display the feedback (Android).\nhttps://reactnative.dev/docs/pressable#rippleconfig" - }, - "style": { - "required": false, - "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" - }, - "description": "Style for positioning the FAB. The visual treatment (size, shape, color)\nis driven by `variant` and `size`." - }, - "testID": { - "required": false, - "tsType": { - "name": "string" - }, - "description": "TestID used for testing purposes.", - "defaultValue": { - "value": "'extended-floating-action-button'", - "computed": false - } - }, - "theme": { - "required": false, - "tsType": { - "name": "ThemeProp" - }, - "description": "" - }, - "ref": { - "required": false, - "tsType": { - "name": "ReactRef", - "raw": "React.Ref", - "elements": [ - { - "name": "View" - } - ] - }, - "description": "" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/FAB/Extended.tsx" - ] - }, - "FAB/Menu": { - "filepath": "FAB/Menu.tsx", - "title": "Menu", - "description": "Floating action button menu. Wraps a trigger FAB; when `expanded` is true,\nitems appear stacked above and the trigger morphs into the spec'd close\nbutton (`shape: 'full'`, 56 dp, saturated role color).\n\nNo visual backdrop and no outside-tap dismiss — that matches the MD3 spec\nand lets the user keep interacting with the content underneath. Dismiss\nvia the close button or by tapping an item.\n\n## Usage\n```tsx\nconst [open, setOpen] = React.useState(false);\n\n\n setOpen(false)}\n trigger={{ icon: 'plus', variant: 'primary', onPress: () => setOpen(true) }}\n items={[\n { icon: 'email', label: 'Send', onPress: () => {} },\n { icon: 'bell', label: 'Remind', onPress: () => {} },\n ]}\n />\n\n```", - "link": "menu", - "data": { - "description": "Floating action button menu. Wraps a trigger FAB; when `expanded` is true,\nitems appear stacked above and the trigger morphs into the spec'd close\nbutton (`shape: 'full'`, 56 dp, saturated role color).\n\nNo visual backdrop and no outside-tap dismiss — that matches the MD3 spec\nand lets the user keep interacting with the content underneath. Dismiss\nvia the close button or by tapping an item.\n\n## Usage\n```tsx\nconst [open, setOpen] = React.useState(false);\n\n\n setOpen(false)}\n trigger={{ icon: 'plus', variant: 'primary', onPress: () => setOpen(true) }}\n items={[\n { icon: 'email', label: 'Send', onPress: () => {} },\n { icon: 'bell', label: 'Remind', onPress: () => {} },\n ]}\n />\n\n```", - "displayName": "Menu", - "methods": [], - "statics": [], - "props": { - "expanded": { - "required": true, - "tsType": { - "name": "boolean" - }, - "description": "Whether the menu is open." - }, - "onDismiss": { - "required": true, - "tsType": { - "name": "signature", - "type": "function", - "raw": "() => void", - "signature": { - "arguments": [], - "return": { - "name": "void" - } - } - }, - "description": "Called when the user taps the close button or taps an item." - }, - "trigger": { - "required": true, - "tsType": { - "name": "signature", - "type": "object", - "raw": "{\n /**\n * Icon displayed in the trigger FAB (and cross-faded to `closeIcon` when\n * the menu is open).\n */\n icon: IconSource;\n variant?: Variant;\n size?: Size;\n containerColor?: ColorValue;\n contentColor?: ColorValue;\n visible?: boolean;\n onPress?: (e: GestureResponderEvent) => void;\n /**\n * Accessibility label for the trigger FAB.\n */\n 'aria-label'?: string;\n testID?: string;\n}", - "signature": { - "properties": [ - { - "key": "icon", - "value": { - "name": "IconSource", - "required": true - } - }, - { - "key": "variant", - "value": { - "name": "Variant", - "required": false - } - }, - { - "key": "size", - "value": { - "name": "Size", - "required": false - } - }, - { - "key": "containerColor", - "value": { - "name": "ColorValue", - "required": false - } - }, - { - "key": "contentColor", - "value": { - "name": "ColorValue", - "required": false - } - }, - { - "key": "visible", - "value": { - "name": "boolean", - "required": false - } - }, - { - "key": "onPress", - "value": { - "name": "signature", - "type": "function", - "raw": "(e: GestureResponderEvent) => void", - "signature": { - "arguments": [ - { - "name": "e", - "type": { - "name": "GestureResponderEvent" - } - } - ], - "return": { - "name": "void" - } - }, - "required": false - } - }, - { - "key": "aria-label", - "value": { - "name": "string", - "required": false - } - }, - { - "key": "testID", - "value": { - "name": "string", - "required": false - } - } - ] - } - }, - "description": "Trigger FAB configuration. The menu renders a morphing FAB that\nanimates between the trigger appearance and the spec'd close button." - }, - "alignment": { - "required": false, - "tsType": { - "name": "union", - "raw": "'start' | 'center' | 'end'", - "elements": [ - { - "name": "literal", - "value": "'start'" - }, - { - "name": "literal", - "value": "'center'" - }, - { - "name": "literal", - "value": "'end'" - } - ] - }, - "description": "Horizontal side the menu sits on. Default `'end'`.", - "defaultValue": { - "value": "'end'", - "computed": false - } - }, - "closeIcon": { - "required": false, - "tsType": { - "name": "IconSource" - }, - "description": "Icon used by the close button when the menu is expanded. Default\n`'close'`.", - "defaultValue": { - "value": "'close'", - "computed": false - } - }, - "items": { - "required": true, + "items": { + "required": true, "tsType": { "name": "union", "raw": "| [T, T]\n| [T, T, T]\n| [T, T, T, T]\n| [T, T, T, T, T]\n| [T, T, T, T, T, T]", @@ -6846,13 +6108,131 @@ } ] } - } - ] - }, - { - "name": "tuple", - "raw": "[T, T, T, T]", - "elements": [ + } + ] + }, + { + "name": "tuple", + "raw": "[T, T, T, T]", + "elements": [ + { + "name": "signature", + "type": "object", + "raw": "{\n /**\n * Optional icon for the item.\n */\n icon?: IconSource;\n /**\n * Mandatory label.\n */\n label: string;\n /**\n * Called when the item is pressed. The menu is dismissed automatically\n * after `onPress` runs.\n */\n onPress: (e: GestureResponderEvent) => void;\n /**\n * Accessibility label. Falls back to `label`.\n */\n 'aria-label'?: string;\n testID?: string;\n}", + "signature": { + "properties": [ + { + "key": "icon", + "value": { + "name": "IconSource", + "required": false + } + }, + { + "key": "label", + "value": { + "name": "string", + "required": true + } + }, + { + "key": "onPress", + "value": { + "name": "signature", + "type": "function", + "raw": "(e: GestureResponderEvent) => void", + "signature": { + "arguments": [ + { + "name": "e", + "type": { + "name": "GestureResponderEvent" + } + } + ], + "return": { + "name": "void" + } + }, + "required": true + } + }, + { + "key": "aria-label", + "value": { + "name": "string", + "required": false + } + }, + { + "key": "testID", + "value": { + "name": "string", + "required": false + } + } + ] + } + }, + { + "name": "signature", + "type": "object", + "raw": "{\n /**\n * Optional icon for the item.\n */\n icon?: IconSource;\n /**\n * Mandatory label.\n */\n label: string;\n /**\n * Called when the item is pressed. The menu is dismissed automatically\n * after `onPress` runs.\n */\n onPress: (e: GestureResponderEvent) => void;\n /**\n * Accessibility label. Falls back to `label`.\n */\n 'aria-label'?: string;\n testID?: string;\n}", + "signature": { + "properties": [ + { + "key": "icon", + "value": { + "name": "IconSource", + "required": false + } + }, + { + "key": "label", + "value": { + "name": "string", + "required": true + } + }, + { + "key": "onPress", + "value": { + "name": "signature", + "type": "function", + "raw": "(e: GestureResponderEvent) => void", + "signature": { + "arguments": [ + { + "name": "e", + "type": { + "name": "GestureResponderEvent" + } + } + ], + "return": { + "name": "void" + } + }, + "required": true + } + }, + { + "key": "aria-label", + "value": { + "name": "string", + "required": false + } + }, + { + "key": "testID", + "value": { + "name": "string", + "required": false + } + } + ] + } + }, { "name": "signature", "type": "object", @@ -6970,7 +6350,13 @@ } ] } - }, + } + ] + }, + { + "name": "tuple", + "raw": "[T, T, T, T, T]", + "elements": [ { "name": "signature", "type": "object", @@ -7088,13 +6474,7 @@ } ] } - } - ] - }, - { - "name": "tuple", - "raw": "[T, T, T, T, T]", - "elements": [ + }, { "name": "signature", "type": "object", @@ -7271,7 +6651,13 @@ } ] } - }, + } + ] + }, + { + "name": "tuple", + "raw": "[T, T, T, T, T, T]", + "elements": [ { "name": "signature", "type": "object", @@ -7389,13 +6775,7 @@ } ] } - } - ] - }, - { - "name": "tuple", - "raw": "[T, T, T, T, T, T]", - "elements": [ + }, { "name": "signature", "type": "object", @@ -7631,141 +7011,473 @@ } ] } - }, + } + ] + } + ] + }, + "description": "Menu items. Spec calls for 2 to 6 items." + }, + "testID": { + "required": false, + "tsType": { + "name": "string" + }, + "description": "", + "defaultValue": { + "value": "'floating-action-button-menu'", + "computed": false + } + }, + "theme": { + "required": false, + "tsType": { + "name": "ThemeProp" + }, + "description": "" + } + } + }, + "type": "component", + "dependencies": [ + "src/components/FAB/Menu.tsx" + ] + }, + "IconButton/IconButton": { + "filepath": "IconButton/IconButton.tsx", + "title": "IconButton", + "description": "An icon button is a button which displays only an icon without a label.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { IconButton, Palette } from 'react-native-paper';\n\nconst MyComponent = () => (\n console.log('Pressed')}\n />\n);\n\nexport default MyComponent;\n```\n\n@extends TouchableRipple props https://callstack.github.io/react-native-paper/docs/components/TouchableRipple", + "link": "icon-button", + "data": { + "description": "An icon button is a button which displays only an icon without a label.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { IconButton, Palette } from 'react-native-paper';\n\nconst MyComponent = () => (\n console.log('Pressed')}\n />\n);\n\nexport default MyComponent;\n```\n\n@extends TouchableRipple props https://callstack.github.io/react-native-paper/docs/components/TouchableRipple", + "displayName": "IconButton", + "methods": [], + "statics": [], + "props": { + "icon": { + "required": true, + "tsType": { + "name": "IconSource" + }, + "description": "Icon to display." + }, + "mode": { + "required": false, + "tsType": { + "name": "union", + "raw": "'outlined' | 'contained' | 'contained-tonal'", + "elements": [ + { + "name": "literal", + "value": "'outlined'" + }, + { + "name": "literal", + "value": "'contained'" + }, + { + "name": "literal", + "value": "'contained-tonal'" + } + ] + }, + "description": "@supported Available in v5.x with theme version 3\nMode of the icon button. By default there is no specified mode - only pressable icon will be rendered." + }, + "iconColor": { + "required": false, + "tsType": { + "name": "ColorValue" + }, + "description": "@renamed Renamed from 'color' to 'iconColor' in v5.x\nColor of the icon." + }, + "containerColor": { + "required": false, + "tsType": { + "name": "ColorValue" + }, + "description": "Background color of the icon container." + }, + "selected": { + "required": false, + "tsType": { + "name": "boolean" + }, + "description": "Whether icon button is selected. A selected button receives alternative combination of icon and container colors.", + "defaultValue": { + "value": "false", + "computed": false + } + }, + "size": { + "required": false, + "tsType": { + "name": "number" + }, + "description": "Size of the icon.", + "defaultValue": { + "value": "24", + "computed": false + } + }, + "disabled": { + "required": false, + "tsType": { + "name": "boolean" + }, + "description": "Whether the button is disabled. A disabled button is greyed out and `onPress` is not called on touch." + }, + "animated": { + "required": false, + "tsType": { + "name": "boolean" + }, + "description": "Whether an icon change is animated.", + "defaultValue": { + "value": "false", + "computed": false + } + }, + "aria-label": { + "required": false, + "tsType": { + "name": "string" + }, + "description": "Accessibility label for the button. This is read by the screen reader when the user taps the button." + }, + "contentStyle": { + "required": false, + "tsType": { + "name": "StyleProp", + "elements": [ + { + "name": "ViewStyle" + } + ], + "raw": "StyleProp" + }, + "description": "Style of button's inner content.\nUse this prop to apply custom height and width or to set a custom padding`." + }, + "onPress": { + "required": false, + "tsType": { + "name": "signature", + "type": "function", + "raw": "(e: GestureResponderEvent) => void", + "signature": { + "arguments": [ + { + "name": "e", + "type": { + "name": "GestureResponderEvent" + } + } + ], + "return": { + "name": "void" + } + } + }, + "description": "Function to execute on press." + }, + "style": { + "required": false, + "tsType": { + "name": "Animated.WithAnimatedValue", + "elements": [ + { + "name": "StyleProp", + "elements": [ { + "name": "ViewStyle" + } + ], + "raw": "StyleProp" + } + ], + "raw": "Animated.WithAnimatedValue>" + }, + "description": "" + }, + "ref": { + "required": false, + "tsType": { + "name": "ReactRef", + "raw": "React.Ref", + "elements": [ + { + "name": "View" + } + ] + }, + "description": "" + }, + "testID": { + "required": false, + "tsType": { + "name": "string" + }, + "description": "TestID used for testing purposes", + "defaultValue": { + "value": "'icon-button'", + "computed": false + } + }, + "theme": { + "required": false, + "tsType": { + "name": "ThemeProp" + }, + "description": "" + }, + "loading": { + "required": false, + "tsType": { + "name": "boolean" + }, + "description": "Whether to show a loading indicator.", + "defaultValue": { + "value": "false", + "computed": false + } + } + } + }, + "type": "component", + "dependencies": [ + "src/components/IconButton/IconButton.tsx" + ] + }, + "Icon": { + "filepath": "Icon.tsx", + "title": "Icon", + "description": "An icon component which renders icon from vector library.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Icon, Palette } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n);\n\nexport default MyComponent;\n```", + "link": "icon", + "data": { + "description": "An icon component which renders icon from vector library.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Icon, Palette } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n);\n\nexport default MyComponent;\n```", + "displayName": "Icon", + "methods": [], + "statics": [], + "props": { + "size": { + "required": true, + "tsType": { + "name": "number" + }, + "description": "Size of icon." + }, + "allowFontScaling": { + "required": false, + "tsType": { + "name": "boolean" + }, + "description": "" + }, + "source": { + "required": true, + "tsType": { + "name": "any" + }, + "description": "Icon to display." + }, + "color": { + "required": false, + "tsType": { + "name": "ColorValue" + }, + "description": "Color of the icon." + }, + "testID": { + "required": false, + "tsType": { + "name": "string" + }, + "description": "TestID used for testing purposes" + }, + "theme": { + "required": false, + "tsType": { + "name": "ThemeProp" + }, + "description": "" + } + } + }, + "type": "component", + "dependencies": [ + "src/components/Icon.tsx" + ] + }, + "List/ListAccordion": { + "filepath": "List/ListAccordion.tsx", + "title": "List.Accordion", + "description": "A component used to display an expandable list item.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [expanded, setExpanded] = React.useState(true);\n\n const handlePress = () => setExpanded(!expanded);\n\n return (\n \n }>\n \n \n \n\n }\n expanded={expanded}\n onPress={handlePress}>\n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", + "link": "list-accordion", + "data": { + "description": "A component used to display an expandable list item.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [expanded, setExpanded] = React.useState(true);\n\n const handlePress = () => setExpanded(!expanded);\n\n return (\n \n }>\n \n \n \n\n }\n expanded={expanded}\n onPress={handlePress}>\n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", + "displayName": "List.Accordion", + "methods": [], + "statics": [], + "props": { + "title": { + "required": true, + "tsType": { + "name": "ReactReactNode", + "raw": "React.ReactNode" + }, + "description": "Title text for the list accordion." + }, + "description": { + "required": false, + "tsType": { + "name": "ReactReactNode", + "raw": "React.ReactNode" + }, + "description": "Description text for the list accordion." + }, + "left": { + "required": false, + "tsType": { + "name": "signature", + "type": "function", + "raw": "(props: { color: ColorValue; style: Style }) => React.ReactNode", + "signature": { + "arguments": [ + { + "name": "props", + "type": { "name": "signature", "type": "object", - "raw": "{\n /**\n * Optional icon for the item.\n */\n icon?: IconSource;\n /**\n * Mandatory label.\n */\n label: string;\n /**\n * Called when the item is pressed. The menu is dismissed automatically\n * after `onPress` runs.\n */\n onPress: (e: GestureResponderEvent) => void;\n /**\n * Accessibility label. Falls back to `label`.\n */\n 'aria-label'?: string;\n testID?: string;\n}", + "raw": "{ color: ColorValue; style: Style }", "signature": { "properties": [ { - "key": "icon", - "value": { - "name": "IconSource", - "required": false - } - }, - { - "key": "label", + "key": "color", "value": { - "name": "string", + "name": "ColorValue", "required": true } }, { - "key": "onPress", + "key": "style", "value": { - "name": "signature", - "type": "function", - "raw": "(e: GestureResponderEvent) => void", - "signature": { - "arguments": [ - { - "name": "e", - "type": { - "name": "GestureResponderEvent" - } - } - ], - "return": { - "name": "void" - } - }, + "name": "Style", "required": true } - }, - { - "key": "aria-label", - "value": { - "name": "string", - "required": false - } - }, - { - "key": "testID", - "value": { - "name": "string", - "required": false - } } ] } - }, - { + } + } + ], + "return": { + "name": "ReactReactNode", + "raw": "React.ReactNode" + } + } + }, + "description": "Callback which returns a React element to display on the left side." + }, + "right": { + "required": false, + "tsType": { + "name": "signature", + "type": "function", + "raw": "(props: { isExpanded: boolean }) => React.ReactNode", + "signature": { + "arguments": [ + { + "name": "props", + "type": { "name": "signature", "type": "object", - "raw": "{\n /**\n * Optional icon for the item.\n */\n icon?: IconSource;\n /**\n * Mandatory label.\n */\n label: string;\n /**\n * Called when the item is pressed. The menu is dismissed automatically\n * after `onPress` runs.\n */\n onPress: (e: GestureResponderEvent) => void;\n /**\n * Accessibility label. Falls back to `label`.\n */\n 'aria-label'?: string;\n testID?: string;\n}", + "raw": "{ isExpanded: boolean }", "signature": { "properties": [ { - "key": "icon", - "value": { - "name": "IconSource", - "required": false - } - }, - { - "key": "label", - "value": { - "name": "string", - "required": true - } - }, - { - "key": "onPress", + "key": "isExpanded", "value": { - "name": "signature", - "type": "function", - "raw": "(e: GestureResponderEvent) => void", - "signature": { - "arguments": [ - { - "name": "e", - "type": { - "name": "GestureResponderEvent" - } - } - ], - "return": { - "name": "void" - } - }, + "name": "boolean", "required": true } - }, - { - "key": "aria-label", - "value": { - "name": "string", - "required": false - } - }, - { - "key": "testID", - "value": { - "name": "string", - "required": false - } } ] } } - ] + } + ], + "return": { + "name": "ReactReactNode", + "raw": "React.ReactNode" + } + } + }, + "description": "Callback which returns a React element to display on the right side." + }, + "expanded": { + "required": false, + "tsType": { + "name": "boolean" + }, + "description": "Whether the accordion is expanded\nIf this prop is provided, the accordion will behave as a \"controlled component\".\nYou'll need to update this prop when you want to toggle the component or on `onPress`." + }, + "onPress": { + "required": false, + "tsType": { + "name": "signature", + "type": "function", + "raw": "(e: GestureResponderEvent) => void", + "signature": { + "arguments": [ + { + "name": "e", + "type": { + "name": "GestureResponderEvent" + } + } + ], + "return": { + "name": "void" } - ] + } }, - "description": "Menu items. Spec calls for 2 to 6 items." + "description": "Function to execute on press." }, - "testID": { + "onLongPress": { "required": false, "tsType": { - "name": "string" + "name": "signature", + "type": "function", + "raw": "(e: GestureResponderEvent) => void", + "signature": { + "arguments": [ + { + "name": "e", + "type": { + "name": "GestureResponderEvent" + } + } + ], + "return": { + "name": "void" + } + } }, - "description": "", - "defaultValue": { - "value": "'floating-action-button-menu'", - "computed": false - } + "description": "Function to execute on long press." + }, + "delayLongPress": { + "required": false, + "tsType": { + "name": "number" + }, + "description": "The number of milliseconds a user must touch the element before executing `onLongPress`." + }, + "children": { + "required": true, + "tsType": { + "name": "ReactReactNode", + "raw": "React.ReactNode" + }, + "description": "Content of the section." }, "theme": { "required": false, @@ -7773,140 +7485,205 @@ "name": "ThemeProp" }, "description": "" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/FAB/Menu.tsx" - ] - }, - "IconButton/IconButton": { - "filepath": "IconButton/IconButton.tsx", - "title": "IconButton", - "description": "An icon button is a button which displays only an icon without a label.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { IconButton, Palette } from 'react-native-paper';\n\nconst MyComponent = () => (\n console.log('Pressed')}\n />\n);\n\nexport default MyComponent;\n```\n\n@extends TouchableRipple props https://callstack.github.io/react-native-paper/docs/components/TouchableRipple", - "link": "icon-button", - "data": { - "description": "An icon button is a button which displays only an icon without a label.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { IconButton, Palette } from 'react-native-paper';\n\nconst MyComponent = () => (\n console.log('Pressed')}\n />\n);\n\nexport default MyComponent;\n```\n\n@extends TouchableRipple props https://callstack.github.io/react-native-paper/docs/components/TouchableRipple", - "displayName": "IconButton", - "methods": [], - "statics": [], - "props": { - "icon": { - "required": true, + }, + "background": { + "required": false, "tsType": { - "name": "IconSource" + "name": "PressableAndroidRippleConfig" }, - "description": "Icon to display." + "description": "Type of background drawabale to display the feedback (Android).\nhttps://reactnative.dev/docs/pressable#rippleconfig" }, - "mode": { + "style": { "required": false, "tsType": { - "name": "union", - "raw": "'outlined' | 'contained' | 'contained-tonal'", + "name": "StyleProp", "elements": [ { - "name": "literal", - "value": "'outlined'" - }, + "name": "ViewStyle" + } + ], + "raw": "StyleProp" + }, + "description": "Style that is passed to the root TouchableRipple container." + }, + "containerStyle": { + "required": false, + "tsType": { + "name": "StyleProp", + "elements": [ { - "name": "literal", - "value": "'contained'" - }, + "name": "ViewStyle" + } + ], + "raw": "StyleProp" + }, + "description": "Style that is passed to the outermost container that wraps the entire content, including left and right items and both title and description." + }, + "contentStyle": { + "required": false, + "tsType": { + "name": "StyleProp", + "elements": [ { - "name": "literal", - "value": "'contained-tonal'" + "name": "ViewStyle" } - ] + ], + "raw": "StyleProp" }, - "description": "@supported Available in v5.x with theme version 3\nMode of the icon button. By default there is no specified mode - only pressable icon will be rendered." + "description": "Style that is passed to the content container, which wraps the title and description." }, - "iconColor": { + "titleStyle": { "required": false, "tsType": { - "name": "ColorValue" + "name": "StyleProp", + "elements": [ + { + "name": "TextStyle" + } + ], + "raw": "StyleProp" }, - "description": "@renamed Renamed from 'color' to 'iconColor' in v5.x\nColor of the icon." + "description": "Style that is passed to Title element." }, - "containerColor": { + "descriptionStyle": { "required": false, "tsType": { - "name": "ColorValue" + "name": "StyleProp", + "elements": [ + { + "name": "TextStyle" + } + ], + "raw": "StyleProp" }, - "description": "Background color of the icon container." + "description": "Style that is passed to Description element." }, - "selected": { + "titleNumberOfLines": { "required": false, "tsType": { - "name": "boolean" + "name": "number" }, - "description": "Whether icon button is selected. A selected button receives alternative combination of icon and container colors.", + "description": "Truncate Title text such that the total number of lines does not\nexceed this number.", "defaultValue": { - "value": "false", + "value": "1", "computed": false } }, - "size": { + "descriptionNumberOfLines": { "required": false, "tsType": { "name": "number" }, - "description": "Size of the icon.", + "description": "Truncate Description text such that the total number of lines does not\nexceed this number.", "defaultValue": { - "value": "24", + "value": "2", "computed": false } }, - "disabled": { + "titleMaxFontSizeMultiplier": { "required": false, "tsType": { - "name": "boolean" + "name": "number" }, - "description": "Whether the button is disabled. A disabled button is greyed out and `onPress` is not called on touch." + "description": "Specifies the largest possible scale a title font can reach." }, - "animated": { + "descriptionMaxFontSizeMultiplier": { "required": false, "tsType": { - "name": "boolean" + "name": "number" }, - "description": "Whether an icon change is animated.", - "defaultValue": { - "value": "false", - "computed": false - } + "description": "Specifies the largest possible scale a description font can reach." + }, + "id": { + "required": false, + "tsType": { + "name": "union", + "raw": "string | number", + "elements": [ + { + "name": "string" + }, + { + "name": "number" + } + ] + }, + "description": "Id is used for distinguishing specific accordion when using List.AccordionGroup. Property is required when using List.AccordionGroup and has no impact on behavior when using standalone List.Accordion." + }, + "testID": { + "required": false, + "tsType": { + "name": "string" + }, + "description": "TestID used for testing purposes" }, "aria-label": { "required": false, "tsType": { "name": "string" }, - "description": "Accessibility label for the button. This is read by the screen reader when the user taps the button." + "description": "Accessibility label for the TouchableRipple. This is read by the screen reader when the user taps the touchable." }, - "contentStyle": { + "pointerEvents": { "required": false, "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" + "name": "ViewProps['pointerEvents']", + "raw": "ViewProps['pointerEvents']" }, - "description": "Style of button's inner content.\nUse this prop to apply custom height and width or to set a custom padding`." + "description": "`pointerEvents` passed to the `View` container", + "defaultValue": { + "value": "'none'", + "computed": false + } }, - "onPress": { + "hitSlop": { + "required": false, + "tsType": { + "name": "TouchableRippleProps['hitSlop']", + "raw": "TouchableRippleProps['hitSlop']" + }, + "description": "Amount of space between the touchable area and the edge of the component.\nThis can be used to enlarge the touchable area beyond the visible component." + } + } + }, + "type": "component", + "dependencies": [ + "src/components/List/ListAccordion.tsx" + ], + "group": "List" + }, + "List/ListAccordionGroup": { + "filepath": "List/ListAccordionGroup.tsx", + "title": "List.AccordionGroup", + "description": "List.AccordionGroup allows to control a group of List Accordions. `id` prop for List.Accordion is required in order for group to work.\nList.AccordionGroup can be a controlled or uncontrolled component. The example shows the uncontrolled version.\nAt most one Accordion can be expanded at a given time.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View, Text } from 'react-native';\nimport { List } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n \n \n \n \n \n \n List.Accordion can be wrapped because implementation uses React.Context.\n \n \n \n \n \n \n);\n\nexport default MyComponent;\n```", + "link": "list-accordion-group", + "data": { + "description": "List.AccordionGroup allows to control a group of List Accordions. `id` prop for List.Accordion is required in order for group to work.\nList.AccordionGroup can be a controlled or uncontrolled component. The example shows the uncontrolled version.\nAt most one Accordion can be expanded at a given time.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View, Text } from 'react-native';\nimport { List } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n \n \n \n \n \n \n List.Accordion can be wrapped because implementation uses React.Context.\n \n \n \n \n \n \n);\n\nexport default MyComponent;\n```", + "displayName": "List.AccordionGroup", + "methods": [], + "statics": [], + "props": { + "onAccordionPress": { "required": false, "tsType": { "name": "signature", "type": "function", - "raw": "(e: GestureResponderEvent) => void", + "raw": "(expandedId: string | number) => void", "signature": { "arguments": [ { - "name": "e", + "name": "expandedId", "type": { - "name": "GestureResponderEvent" + "name": "union", + "raw": "string | number", + "elements": [ + { + "name": "string" + }, + { + "name": "number" + } + ] } } ], @@ -7915,121 +7692,77 @@ } } }, - "description": "Function to execute on press." + "description": "Function to execute on selection change." }, - "style": { + "expandedId": { "required": false, "tsType": { - "name": "Animated.WithAnimatedValue", + "name": "union", + "raw": "string | number", "elements": [ { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" - } - ], - "raw": "Animated.WithAnimatedValue>" - }, - "description": "" - }, - "ref": { - "required": false, - "tsType": { - "name": "ReactRef", - "raw": "React.Ref", - "elements": [ + "name": "string" + }, { - "name": "View" + "name": "number" } ] }, - "description": "" - }, - "testID": { - "required": false, - "tsType": { - "name": "string" - }, - "description": "TestID used for testing purposes", - "defaultValue": { - "value": "'icon-button'", - "computed": false - } - }, - "theme": { - "required": false, - "tsType": { - "name": "ThemeProp" - }, - "description": "" + "description": "Id of the currently expanded list accordion" }, - "loading": { - "required": false, + "children": { + "required": true, "tsType": { - "name": "boolean" + "name": "ReactReactNode", + "raw": "React.ReactNode" }, - "description": "Whether to show a loading indicator.", - "defaultValue": { - "value": "false", - "computed": false - } + "description": "React elements containing list accordions" } } }, "type": "component", "dependencies": [ - "src/components/IconButton/IconButton.tsx" - ] + "src/components/List/ListAccordionGroup.tsx" + ], + "group": "List" }, - "Icon": { - "filepath": "Icon.tsx", - "title": "Icon", - "description": "An icon component which renders icon from vector library.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Icon, Palette } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n);\n\nexport default MyComponent;\n```", - "link": "icon", + "List/ListIcon": { + "filepath": "List/ListIcon.tsx", + "title": "List.Icon", + "description": "A component to show an icon in a list item.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List, Palette } from 'react-native-paper';\n\nconst MyComponent = () => (\n <>\n \n \n \n \n);\n\nexport default MyComponent;\n```", + "link": "list-icon", "data": { - "description": "An icon component which renders icon from vector library.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Icon, Palette } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n);\n\nexport default MyComponent;\n```", - "displayName": "Icon", + "description": "A component to show an icon in a list item.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List, Palette } from 'react-native-paper';\n\nconst MyComponent = () => (\n <>\n \n \n \n \n);\n\nexport default MyComponent;\n```", + "displayName": "List.Icon", "methods": [], "statics": [], "props": { - "size": { - "required": true, - "tsType": { - "name": "number" - }, - "description": "Size of icon." - }, - "allowFontScaling": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "" - }, - "source": { + "icon": { "required": true, "tsType": { - "name": "any" + "name": "IconSource" }, - "description": "Icon to display." + "description": "Icon to show." }, "color": { "required": false, "tsType": { "name": "ColorValue" }, - "description": "Color of the icon." + "description": "Color for the icon." }, - "testID": { + "style": { "required": false, "tsType": { - "name": "string" + "name": "StyleProp", + "elements": [ + { + "name": "ViewStyle" + } + ], + "raw": "StyleProp" }, - "description": "TestID used for testing purposes" + "description": "" }, "theme": { "required": false, @@ -8042,35 +7775,54 @@ }, "type": "component", "dependencies": [ - "src/components/Icon.tsx" - ] + "src/components/List/ListIcon.tsx" + ], + "group": "List" }, - "List/ListAccordion": { - "filepath": "List/ListAccordion.tsx", - "title": "List.Accordion", - "description": "A component used to display an expandable list item.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [expanded, setExpanded] = React.useState(true);\n\n const handlePress = () => setExpanded(!expanded);\n\n return (\n \n }>\n \n \n \n\n }\n expanded={expanded}\n onPress={handlePress}>\n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", - "link": "list-accordion", + "List/ListItem": { + "filepath": "List/ListItem.tsx", + "title": "List.Item", + "description": "A component to show tiles inside a List.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List } from 'react-native-paper';\n\nconst MyComponent = () => (\n }\n />\n);\n\nexport default MyComponent;\n```\n\n@extends TouchableRipple props https://callstack.github.io/react-native-paper/docs/components/TouchableRipple", + "link": "list-item", "data": { - "description": "A component used to display an expandable list item.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [expanded, setExpanded] = React.useState(true);\n\n const handlePress = () => setExpanded(!expanded);\n\n return (\n \n }>\n \n \n \n\n }\n expanded={expanded}\n onPress={handlePress}>\n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", - "displayName": "List.Accordion", + "description": "A component to show tiles inside a List.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List } from 'react-native-paper';\n\nconst MyComponent = () => (\n }\n />\n);\n\nexport default MyComponent;\n```\n\n@extends TouchableRipple props https://callstack.github.io/react-native-paper/docs/components/TouchableRipple", + "displayName": "List.Item", "methods": [], "statics": [], "props": { "title": { "required": true, "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" + "name": "union", + "raw": "| React.ReactNode\n| ((props: {\n selectable: boolean;\n ellipsizeMode: EllipsizeProp | undefined;\n color: ColorValue;\n fontSize: number;\n }) => React.ReactNode)", + "elements": [ + { + "name": "ReactReactNode", + "raw": "React.ReactNode" + }, + { + "name": "unknown" + } + ] }, - "description": "Title text for the list accordion." + "description": "Title text for the list item." }, "description": { "required": false, "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" + "name": "union", + "raw": "| React.ReactNode\n| ((props: {\n selectable: boolean;\n ellipsizeMode: EllipsizeProp | undefined;\n color: ColorValue;\n fontSize: number;\n }) => React.ReactNode)", + "elements": [ + { + "name": "ReactReactNode", + "raw": "React.ReactNode" + }, + { + "name": "unknown" + } + ] }, - "description": "Description text for the list accordion." + "description": "Description text for the list item or callback which returns a React element to display the description." }, "left": { "required": false, @@ -8120,7 +7872,7 @@ "tsType": { "name": "signature", "type": "function", - "raw": "(props: { isExpanded: boolean }) => React.ReactNode", + "raw": "(props: { color: ColorValue; style?: Style }) => React.ReactNode", "signature": { "arguments": [ { @@ -8128,15 +7880,22 @@ "type": { "name": "signature", "type": "object", - "raw": "{ isExpanded: boolean }", + "raw": "{ color: ColorValue; style?: Style }", "signature": { "properties": [ { - "key": "isExpanded", + "key": "color", "value": { - "name": "boolean", + "name": "ColorValue", "required": true } + }, + { + "key": "style", + "value": { + "name": "Style", + "required": false + } } ] } @@ -8151,13 +7910,6 @@ }, "description": "Callback which returns a React element to display on the right side." }, - "expanded": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "Whether the accordion is expanded\nIf this prop is provided, the accordion will behave as a \"controlled component\".\nYou'll need to update this prop when you want to toggle the component or on `onPress`." - }, "onPress": { "required": false, "tsType": { @@ -8180,43 +7932,6 @@ }, "description": "Function to execute on press." }, - "onLongPress": { - "required": false, - "tsType": { - "name": "signature", - "type": "function", - "raw": "(e: GestureResponderEvent) => void", - "signature": { - "arguments": [ - { - "name": "e", - "type": { - "name": "GestureResponderEvent" - } - } - ], - "return": { - "name": "void" - } - } - }, - "description": "Function to execute on long press." - }, - "delayLongPress": { - "required": false, - "tsType": { - "name": "number" - }, - "description": "The number of milliseconds a user must touch the element before executing `onLongPress`." - }, - "children": { - "required": true, - "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" - }, - "description": "Content of the section." - }, "theme": { "required": false, "tsType": { @@ -8224,25 +7939,31 @@ }, "description": "" }, - "background": { + "style": { "required": false, "tsType": { - "name": "PressableAndroidRippleConfig" + "name": "StyleProp", + "elements": [ + { + "name": "ViewStyle" + } + ], + "raw": "StyleProp" }, - "description": "Type of background drawabale to display the feedback (Android).\nhttps://reactnative.dev/docs/pressable#rippleconfig" + "description": "Style that is passed to the root TouchableRipple container." }, - "style": { + "ref": { "required": false, "tsType": { - "name": "StyleProp", + "name": "ReactRef", + "raw": "React.Ref", "elements": [ { - "name": "ViewStyle" + "name": "View" } - ], - "raw": "StyleProp" + ] }, - "description": "Style that is passed to the root TouchableRipple container." + "description": "" }, "containerStyle": { "required": false, @@ -8318,6 +8039,20 @@ "computed": false } }, + "titleEllipsizeMode": { + "required": false, + "tsType": { + "name": "EllipsizeProp" + }, + "description": "Ellipsize Mode for the Title. One of `'head'`, `'middle'`, `'tail'`, `'clip'`.\n\nSee [`ellipsizeMode`](https://reactnative.dev/docs/text#ellipsizemode)" + }, + "descriptionEllipsizeMode": { + "required": false, + "tsType": { + "name": "EllipsizeProp" + }, + "description": "Ellipsize Mode for the Description. One of `'head'`, `'middle'`, `'tail'`, `'clip'`.\n\nSee [`ellipsizeMode`](https://reactnative.dev/docs/text#ellipsizemode)" + }, "titleMaxFontSizeMultiplier": { "required": false, "tsType": { @@ -8332,121 +8067,239 @@ }, "description": "Specifies the largest possible scale a description font can reach." }, - "id": { + "testID": { "required": false, "tsType": { - "name": "union", - "raw": "string | number", + "name": "string" + }, + "description": "TestID used for testing purposes" + } + } + }, + "type": "component", + "dependencies": [ + "src/components/List/ListItem.tsx" + ], + "group": "List" + }, + "List/ListSection": { + "filepath": "List/ListSection.tsx", + "title": "List.Section", + "description": "A component used to group list items.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List, Palette } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n Some title\n } />\n }\n />\n \n);\n\nexport default MyComponent;\n```", + "link": "list-section", + "data": { + "description": "A component used to group list items.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List, Palette } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n Some title\n } />\n }\n />\n \n);\n\nexport default MyComponent;\n```", + "displayName": "List.Section", + "methods": [], + "statics": [], + "props": { + "title": { + "required": false, + "tsType": { + "name": "string" + }, + "description": "Title text for the section." + }, + "children": { + "required": true, + "tsType": { + "name": "ReactReactNode", + "raw": "React.ReactNode" + }, + "description": "Content of the section." + }, + "theme": { + "required": false, + "tsType": { + "name": "ThemeProp" + }, + "description": "" + }, + "titleStyle": { + "required": false, + "tsType": { + "name": "StyleProp", "elements": [ { - "name": "string" - }, - { - "name": "number" + "name": "TextStyle" } - ] + ], + "raw": "StyleProp" }, - "description": "Id is used for distinguishing specific accordion when using List.AccordionGroup. Property is required when using List.AccordionGroup and has no impact on behavior when using standalone List.Accordion." + "description": "Style that is passed to Title element." }, - "testID": { + "style": { "required": false, "tsType": { - "name": "string" + "name": "StyleProp", + "elements": [ + { + "name": "ViewStyle" + } + ], + "raw": "StyleProp" }, - "description": "TestID used for testing purposes" - }, - "aria-label": { + "description": "" + } + } + }, + "type": "component", + "dependencies": [ + "src/components/List/ListSection.tsx" + ], + "group": "List" + }, + "List/ListSubheader": { + "filepath": "List/ListSubheader.tsx", + "title": "List.Subheader", + "description": "A component used to display a header in lists.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List } from 'react-native-paper';\n\nconst MyComponent = () => My List Title;\n\nexport default MyComponent;\n```", + "link": "list-subheader", + "data": { + "description": "A component used to display a header in lists.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List } from 'react-native-paper';\n\nconst MyComponent = () => My List Title;\n\nexport default MyComponent;\n```", + "displayName": "List.Subheader", + "methods": [], + "statics": [], + "props": { + "theme": { "required": false, "tsType": { - "name": "string" + "name": "ThemeProp" }, - "description": "Accessibility label for the TouchableRipple. This is read by the screen reader when the user taps the touchable." + "description": "" }, - "pointerEvents": { + "style": { "required": false, "tsType": { - "name": "ViewProps['pointerEvents']", - "raw": "ViewProps['pointerEvents']" + "name": "StyleProp", + "elements": [ + { + "name": "TextStyle" + } + ], + "raw": "StyleProp" }, - "description": "`pointerEvents` passed to the `View` container", - "defaultValue": { - "value": "'none'", - "computed": false - } + "description": "Style that is passed to Text element." }, - "hitSlop": { + "maxFontSizeMultiplier": { "required": false, "tsType": { - "name": "TouchableRippleProps['hitSlop']", - "raw": "TouchableRippleProps['hitSlop']" + "name": "number" }, - "description": "Amount of space between the touchable area and the edge of the component.\nThis can be used to enlarge the touchable area beyond the visible component." + "description": "Specifies the largest possible scale a text font can reach." } } }, "type": "component", "dependencies": [ - "src/components/List/ListAccordion.tsx" + "src/components/List/ListSubheader.tsx" ], "group": "List" }, - "List/ListAccordionGroup": { - "filepath": "List/ListAccordionGroup.tsx", - "title": "List.AccordionGroup", - "description": "List.AccordionGroup allows to control a group of List Accordions. `id` prop for List.Accordion is required in order for group to work.\nList.AccordionGroup can be a controlled or uncontrolled component. The example shows the uncontrolled version.\nAt most one Accordion can be expanded at a given time.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View, Text } from 'react-native';\nimport { List } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n \n \n \n \n \n \n List.Accordion can be wrapped because implementation uses React.Context.\n \n \n \n \n \n \n);\n\nexport default MyComponent;\n```", - "link": "list-accordion-group", + "Menu/Menu": { + "filepath": "Menu/Menu.tsx", + "title": "Menu", + "description": "Menus display a list of choices on temporary elevated surfaces. Their placement varies based on the element that opens them.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Button, Menu, Divider, PaperProvider } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const openMenu = () => setVisible(true);\n\n const closeMenu = () => setVisible(false);\n\n return (\n \n \n Show menu}>\n {}} title=\"Item 1\" />\n {}} title=\"Item 2\" />\n \n {}} title=\"Item 3\" />\n \n \n \n );\n};\n\nexport default MyComponent;\n```\n\n### Note\nWhen using `Menu` within a React Native's `Modal` component, you need to wrap all\n`Modal` contents within a `PaperProvider` in order for the menu to show. This\nwrapping is not necessary if you use Paper's `Modal` instead.", + "link": "menu", "data": { - "description": "List.AccordionGroup allows to control a group of List Accordions. `id` prop for List.Accordion is required in order for group to work.\nList.AccordionGroup can be a controlled or uncontrolled component. The example shows the uncontrolled version.\nAt most one Accordion can be expanded at a given time.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View, Text } from 'react-native';\nimport { List } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n \n \n \n \n \n \n List.Accordion can be wrapped because implementation uses React.Context.\n \n \n \n \n \n \n);\n\nexport default MyComponent;\n```", - "displayName": "List.AccordionGroup", + "description": "Menus display a list of choices on temporary elevated surfaces. Their placement varies based on the element that opens them.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Button, Menu, Divider, PaperProvider } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const openMenu = () => setVisible(true);\n\n const closeMenu = () => setVisible(false);\n\n return (\n \n \n Show menu}>\n {}} title=\"Item 1\" />\n {}} title=\"Item 2\" />\n \n {}} title=\"Item 3\" />\n \n \n \n );\n};\n\nexport default MyComponent;\n```\n\n### Note\nWhen using `Menu` within a React Native's `Modal` component, you need to wrap all\n`Modal` contents within a `PaperProvider` in order for the menu to show. This\nwrapping is not necessary if you use Paper's `Modal` instead.", + "displayName": "Menu", "methods": [], "statics": [], "props": { - "onAccordionPress": { - "required": false, + "visible": { + "required": true, "tsType": { - "name": "signature", - "type": "function", - "raw": "(expandedId: string | number) => void", - "signature": { - "arguments": [ - { - "name": "expandedId", - "type": { - "name": "union", - "raw": "string | number", - "elements": [ - { - "name": "string" - }, - { - "name": "number" + "name": "boolean" + }, + "description": "Whether the Menu is currently visible." + }, + "anchor": { + "required": true, + "tsType": { + "name": "union", + "raw": "React.ReactNode | { x: number; y: number }", + "elements": [ + { + "name": "ReactReactNode", + "raw": "React.ReactNode" + }, + { + "name": "signature", + "type": "object", + "raw": "{ x: number; y: number }", + "signature": { + "properties": [ + { + "key": "x", + "value": { + "name": "number", + "required": true } - ] - } + }, + { + "key": "y", + "value": { + "name": "number", + "required": true + } + } + ] } - ], + } + ] + }, + "description": "The anchor to open the menu from. In most cases, it will be a button that opens the menu." + }, + "anchorPosition": { + "required": false, + "tsType": { + "name": "union", + "raw": "'top' | 'bottom'", + "elements": [ + { + "name": "literal", + "value": "'top'" + }, + { + "name": "literal", + "value": "'bottom'" + } + ] + }, + "description": "Whether the menu should open at the top of the anchor or at its bottom.\nApplied only when anchor is a node, not an x/y position." + }, + "statusBarHeight": { + "required": false, + "tsType": { + "name": "number" + }, + "description": "Extra margin to add at the top of the menu to account for translucent status bar on Android.\nIf you are using Expo, we assume translucent status bar and set a height for status bar automatically.\nPass `0` or a custom value to and customize it.\nThis is automatically handled on iOS." + }, + "onDismiss": { + "required": false, + "tsType": { + "name": "signature", + "type": "function", + "raw": "() => void", + "signature": { + "arguments": [], "return": { "name": "void" } } }, - "description": "Function to execute on selection change." + "description": "Callback called when Menu is dismissed. The `visible` prop needs to be updated when this is called." }, - "expandedId": { + "overlayAccessibilityLabel": { "required": false, "tsType": { - "name": "union", - "raw": "string | number", - "elements": [ - { - "name": "string" - }, - { - "name": "number" - } - ] + "name": "string" }, - "description": "Id of the currently expanded list accordion" + "description": "Accessibility label for the overlay. This is read by the screen reader when the user taps outside the menu.", + "defaultValue": { + "value": "'Close menu'", + "computed": false + } }, "children": { "required": true, @@ -8454,40 +8307,26 @@ "name": "ReactReactNode", "raw": "React.ReactNode" }, - "description": "React elements containing list accordions" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/List/ListAccordionGroup.tsx" - ], - "group": "List" - }, - "List/ListIcon": { - "filepath": "List/ListIcon.tsx", - "title": "List.Icon", - "description": "A component to show an icon in a list item.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List, Palette } from 'react-native-paper';\n\nconst MyComponent = () => (\n <>\n \n \n \n \n);\n\nexport default MyComponent;\n```", - "link": "list-icon", - "data": { - "description": "A component to show an icon in a list item.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List, Palette } from 'react-native-paper';\n\nconst MyComponent = () => (\n <>\n \n \n \n \n);\n\nexport default MyComponent;\n```", - "displayName": "List.Icon", - "methods": [], - "statics": [], - "props": { - "icon": { - "required": true, - "tsType": { - "name": "IconSource" - }, - "description": "Icon to show." + "description": "Content of the `Menu`." }, - "color": { + "contentStyle": { "required": false, "tsType": { - "name": "ColorValue" + "name": "Animated.WithAnimatedValue", + "elements": [ + { + "name": "StyleProp", + "elements": [ + { + "name": "ViewStyle" + } + ], + "raw": "StyleProp" + } + ], + "raw": "Animated.WithAnimatedValue>" }, - "description": "Color for the icon." + "description": "Style of menu's inner content." }, "style": { "required": false, @@ -8502,151 +8341,125 @@ }, "description": "" }, + "elevation": { + "required": false, + "tsType": { + "name": "Elevation" + }, + "description": "Elevation level of the menu's content. Shadow styles are calculated based on this value. Default `backgroundColor` is taken from the corresponding `theme.colors.elevation` property. By default equals `2`.\n@supported Available in v5.x with theme version 3", + "defaultValue": { + "value": "2", + "computed": false + } + }, + "mode": { + "required": false, + "tsType": { + "name": "union", + "raw": "'flat' | 'elevated'", + "elements": [ + { + "name": "literal", + "value": "'flat'" + }, + { + "name": "literal", + "value": "'elevated'" + } + ] + }, + "description": "Mode of the menu's content.\n- `elevated` - Surface with a shadow and background color corresponding to set `elevation` value.\n- `flat` - Surface without a shadow, with the background color corresponding to set `elevation` value.\n\n@supported Available in v5.x with theme version 3", + "defaultValue": { + "value": "'elevated'", + "computed": false + } + }, "theme": { "required": false, "tsType": { "name": "ThemeProp" }, "description": "" + }, + "keyboardShouldPersistTaps": { + "required": false, + "tsType": { + "name": "ScrollViewProps['keyboardShouldPersistTaps']", + "raw": "ScrollViewProps['keyboardShouldPersistTaps']" + }, + "description": "Inner ScrollView prop" + }, + "testID": { + "required": false, + "tsType": { + "name": "string" + }, + "description": "testID to be used on tests.", + "defaultValue": { + "value": "'menu'", + "computed": false + } } } }, "type": "component", "dependencies": [ - "src/components/List/ListIcon.tsx" - ], - "group": "List" + "src/components/Menu/Menu.tsx" + ] }, - "List/ListItem": { - "filepath": "List/ListItem.tsx", - "title": "List.Item", - "description": "A component to show tiles inside a List.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List } from 'react-native-paper';\n\nconst MyComponent = () => (\n }\n />\n);\n\nexport default MyComponent;\n```\n\n@extends TouchableRipple props https://callstack.github.io/react-native-paper/docs/components/TouchableRipple", - "link": "list-item", + "Menu/MenuItem": { + "filepath": "Menu/MenuItem.tsx", + "title": "Menu.Item", + "description": "A component to show a single list item inside a Menu.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Menu } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n {}} title=\"Redo\" />\n {}} title=\"Undo\" />\n {}} title=\"Cut\" disabled />\n {}} title=\"Copy\" disabled />\n {}} title=\"Paste\" />\n \n);\n\nexport default MyComponent;\n```", + "link": "menu-item", "data": { - "description": "A component to show tiles inside a List.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List } from 'react-native-paper';\n\nconst MyComponent = () => (\n }\n />\n);\n\nexport default MyComponent;\n```\n\n@extends TouchableRipple props https://callstack.github.io/react-native-paper/docs/components/TouchableRipple", - "displayName": "List.Item", + "description": "A component to show a single list item inside a Menu.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Menu } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n {}} title=\"Redo\" />\n {}} title=\"Undo\" />\n {}} title=\"Cut\" disabled />\n {}} title=\"Copy\" disabled />\n {}} title=\"Paste\" />\n \n);\n\nexport default MyComponent;\n```", + "displayName": "Menu.Item", "methods": [], "statics": [], "props": { "title": { "required": true, "tsType": { - "name": "union", - "raw": "| React.ReactNode\n| ((props: {\n selectable: boolean;\n ellipsizeMode: EllipsizeProp | undefined;\n color: ColorValue;\n fontSize: number;\n }) => React.ReactNode)", - "elements": [ - { - "name": "ReactReactNode", - "raw": "React.ReactNode" - }, - { - "name": "unknown" - } - ] + "name": "ReactReactNode", + "raw": "React.ReactNode" }, - "description": "Title text for the list item." + "description": "Title text for the `MenuItem`." }, - "description": { + "leadingIcon": { "required": false, "tsType": { - "name": "union", - "raw": "| React.ReactNode\n| ((props: {\n selectable: boolean;\n ellipsizeMode: EllipsizeProp | undefined;\n color: ColorValue;\n fontSize: number;\n }) => React.ReactNode)", - "elements": [ - { - "name": "ReactReactNode", - "raw": "React.ReactNode" - }, - { - "name": "unknown" - } - ] + "name": "IconSource" }, - "description": "Description text for the list item or callback which returns a React element to display the description." + "description": "@renamed Renamed from 'icon' to 'leadingIcon' in v5.x\n\nLeading icon to display for the `MenuItem`." }, - "left": { + "trailingIcon": { "required": false, "tsType": { - "name": "signature", - "type": "function", - "raw": "(props: { color: ColorValue; style: Style }) => React.ReactNode", - "signature": { - "arguments": [ - { - "name": "props", - "type": { - "name": "signature", - "type": "object", - "raw": "{ color: ColorValue; style: Style }", - "signature": { - "properties": [ - { - "key": "color", - "value": { - "name": "ColorValue", - "required": true - } - }, - { - "key": "style", - "value": { - "name": "Style", - "required": true - } - } - ] - } - } - } - ], - "return": { - "name": "ReactReactNode", - "raw": "React.ReactNode" - } - } + "name": "IconSource" }, - "description": "Callback which returns a React element to display on the left side." + "description": "@supported Available in v5.x with theme version 3\n\nTrailing icon to display for the `MenuItem`." }, - "right": { - "required": false, - "tsType": { - "name": "signature", - "type": "function", - "raw": "(props: { color: ColorValue; style?: Style }) => React.ReactNode", - "signature": { - "arguments": [ - { - "name": "props", - "type": { - "name": "signature", - "type": "object", - "raw": "{ color: ColorValue; style?: Style }", - "signature": { - "properties": [ - { - "key": "color", - "value": { - "name": "ColorValue", - "required": true - } - }, - { - "key": "style", - "value": { - "name": "Style", - "required": false - } - } - ] - } - } - } - ], - "return": { - "name": "ReactReactNode", - "raw": "React.ReactNode" - } - } + "disabled": { + "required": false, + "tsType": { + "name": "boolean" }, - "description": "Callback which returns a React element to display on the right side." + "description": "Whether the 'item' is disabled. A disabled 'item' is greyed out and `onPress` is not called on touch." + }, + "dense": { + "required": false, + "tsType": { + "name": "boolean" + }, + "description": "@supported Available in v5.x with theme version 3\n\nSets min height with densed layout." + }, + "background": { + "required": false, + "tsType": { + "name": "PressableAndroidRippleConfig" + }, + "description": "Type of background drawabale to display the feedback (Android).\nhttps://reactnative.dev/docs/pressable#rippleconfig" }, "onPress": { "required": false, @@ -8670,12 +8483,16 @@ }, "description": "Function to execute on press." }, - "theme": { + "titleMaxFontSizeMultiplier": { "required": false, "tsType": { - "name": "ThemeProp" + "name": "number" }, - "description": "" + "description": "Specifies the largest possible scale a title font can reach.", + "defaultValue": { + "value": "1.5", + "computed": false + } }, "style": { "required": false, @@ -8688,20 +8505,7 @@ ], "raw": "StyleProp" }, - "description": "Style that is passed to the root TouchableRipple container." - }, - "ref": { - "required": false, - "tsType": { - "name": "ReactRef", - "raw": "React.Ref", - "elements": [ - { - "name": "View" - } - ] - }, - "description": "" + "description": "Style that is passed to the root TouchableRipple container.\n" }, "containerStyle": { "required": false, @@ -8714,7 +8518,7 @@ ], "raw": "StyleProp" }, - "description": "Style that is passed to the outermost container that wraps the entire content, including left and right items and both title and description." + "description": "Style that is passed to the outermost container that wraps the entire content, including leading and trailing icons and title text." }, "contentStyle": { "required": false, @@ -8727,7 +8531,7 @@ ], "raw": "StyleProp" }, - "description": "Style that is passed to the content container, which wraps the title and description." + "description": "Style that is passed to the content container, which wraps the title text." }, "titleStyle": { "required": false, @@ -8740,170 +8544,187 @@ ], "raw": "StyleProp" }, - "description": "Style that is passed to Title element." + "description": "Style that is passed to the Title element." }, - "descriptionStyle": { + "theme": { "required": false, "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "TextStyle" - } - ], - "raw": "StyleProp" + "name": "ThemeProp" }, - "description": "Style that is passed to Description element." + "description": "" }, - "titleNumberOfLines": { + "hitSlop": { "required": false, "tsType": { - "name": "number" + "name": "TouchableRippleProps['hitSlop']", + "raw": "TouchableRippleProps['hitSlop']" }, - "description": "Truncate Title text such that the total number of lines does not\nexceed this number.", - "defaultValue": { - "value": "1", - "computed": false - } + "description": "Sets additional distance outside of element in which a press can be detected." }, - "descriptionNumberOfLines": { + "testID": { "required": false, "tsType": { - "name": "number" + "name": "string" }, - "description": "Truncate Description text such that the total number of lines does not\nexceed this number.", + "description": "TestID used for testing purposes", "defaultValue": { - "value": "2", + "value": "'menu-item'", "computed": false } }, - "titleEllipsizeMode": { + "aria-label": { "required": false, "tsType": { - "name": "EllipsizeProp" + "name": "string" }, - "description": "Ellipsize Mode for the Title. One of `'head'`, `'middle'`, `'tail'`, `'clip'`.\n\nSee [`ellipsizeMode`](https://reactnative.dev/docs/text#ellipsizemode)" + "description": "Accessibility label for the Touchable. This is read by the screen reader when the user taps the component." }, - "descriptionEllipsizeMode": { + "aria-checked": { "required": false, "tsType": { - "name": "EllipsizeProp" + "name": "union", + "raw": "boolean | 'mixed'", + "elements": [ + { + "name": "boolean" + }, + { + "name": "literal", + "value": "'mixed'" + } + ] }, - "description": "Ellipsize Mode for the Description. One of `'head'`, `'middle'`, `'tail'`, `'clip'`.\n\nSee [`ellipsizeMode`](https://reactnative.dev/docs/text#ellipsizemode)" + "description": "Indicates whether the element is checked. Accepts `true`, `false`,\nor `'mixed'` for an indeterminate state." }, - "titleMaxFontSizeMultiplier": { + "aria-selected": { "required": false, "tsType": { - "name": "number" + "name": "boolean" }, - "description": "Specifies the largest possible scale a title font can reach." + "description": "Indicates whether the element is selected." }, - "descriptionMaxFontSizeMultiplier": { + "aria-busy": { "required": false, "tsType": { - "name": "number" + "name": "boolean" }, - "description": "Specifies the largest possible scale a description font can reach." + "description": "Indicates whether the element is currently busy (e.g. loading)." }, - "testID": { + "aria-expanded": { "required": false, "tsType": { - "name": "string" + "name": "boolean" }, - "description": "TestID used for testing purposes" + "description": "Indicates whether the element's controlled content is expanded." } } }, "type": "component", "dependencies": [ - "src/components/List/ListItem.tsx" + "src/components/Menu/MenuItem.tsx" ], - "group": "List" + "group": "Menu" }, - "List/ListSection": { - "filepath": "List/ListSection.tsx", - "title": "List.Section", - "description": "A component used to group list items.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List, Palette } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n Some title\n } />\n }\n />\n \n);\n\nexport default MyComponent;\n```", - "link": "list-section", + "Modal": { + "filepath": "Modal.tsx", + "title": "Modal", + "description": "The Modal component is a simple way to present content above an enclosing view.\nTo render the `Modal` above other components, you'll need to wrap it with the [`Portal`](./Portal) component.\nNote that this modal is NOT accessible by default; if you need an accessible modal, please use the React Native Modal.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Modal, Portal, Text, Button, PaperProvider } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const showModal = () => setVisible(true);\n const hideModal = () => setVisible(false);\n const containerStyle = { backgroundColor: 'white', padding: 20 };\n\n return (\n \n \n \n Example Modal. Click outside this area to dismiss.\n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", + "link": "modal", "data": { - "description": "A component used to group list items.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List, Palette } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n Some title\n } />\n }\n />\n \n);\n\nexport default MyComponent;\n```", - "displayName": "List.Section", + "description": "The Modal component is a simple way to present content above an enclosing view.\nTo render the `Modal` above other components, you'll need to wrap it with the [`Portal`](./Portal) component.\nNote that this modal is NOT accessible by default; if you need an accessible modal, please use the React Native Modal.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Modal, Portal, Text, Button, PaperProvider } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const showModal = () => setVisible(true);\n const hideModal = () => setVisible(false);\n const containerStyle = { backgroundColor: 'white', padding: 20 };\n\n return (\n \n \n \n Example Modal. Click outside this area to dismiss.\n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", + "displayName": "Modal", "methods": [], "statics": [], "props": { - "title": { + "dismissable": { "required": false, "tsType": { - "name": "string" + "name": "boolean" }, - "description": "Title text for the section." + "description": "Determines whether clicking outside the modal dismisses it.", + "defaultValue": { + "value": "true", + "computed": false + } }, - "children": { - "required": true, + "dismissableBackButton": { + "required": false, "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" + "name": "boolean" }, - "description": "Content of the section." + "description": "Determines whether clicking Android hardware back button dismisses the dialog.", + "defaultValue": { + "value": "dismissable", + "computed": true + } }, - "theme": { + "onDismiss": { "required": false, "tsType": { - "name": "ThemeProp" + "name": "signature", + "type": "function", + "raw": "() => void", + "signature": { + "arguments": [], + "return": { + "name": "void" + } + } }, - "description": "" + "description": "Callback that is called when the user dismisses the modal.", + "defaultValue": { + "value": "() => {}", + "computed": false + } }, - "titleStyle": { + "overlayAccessibilityLabel": { "required": false, "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "TextStyle" - } - ], - "raw": "StyleProp" + "name": "string" }, - "description": "Style that is passed to Title element." + "description": "Accessibility label for the overlay. This is read by the screen reader when the user taps outside the modal.", + "defaultValue": { + "value": "'Close modal'", + "computed": false + } }, - "style": { + "visible": { "required": false, "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" + "name": "boolean" }, - "description": "" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/List/ListSection.tsx" - ], - "group": "List" - }, - "List/ListSubheader": { - "filepath": "List/ListSubheader.tsx", - "title": "List.Subheader", - "description": "A component used to display a header in lists.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List } from 'react-native-paper';\n\nconst MyComponent = () => My List Title;\n\nexport default MyComponent;\n```", - "link": "list-subheader", - "data": { - "description": "A component used to display a header in lists.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { List } from 'react-native-paper';\n\nconst MyComponent = () => My List Title;\n\nexport default MyComponent;\n```", - "displayName": "List.Subheader", - "methods": [], - "statics": [], - "props": { - "theme": { + "description": "Determines Whether the modal is visible.", + "defaultValue": { + "value": "false", + "computed": false + } + }, + "children": { + "required": true, + "tsType": { + "name": "ReactReactNode", + "raw": "React.ReactNode" + }, + "description": "Content of the `Modal`." + }, + "contentContainerStyle": { "required": false, "tsType": { - "name": "ThemeProp" + "name": "Animated.WithAnimatedValue", + "elements": [ + { + "name": "StyleProp", + "elements": [ + { + "name": "ViewStyle" + } + ], + "raw": "StyleProp" + } + ], + "raw": "Animated.WithAnimatedValue>" }, - "description": "" + "description": "Style for the content of the modal" }, "style": { "required": false, @@ -8911,306 +8732,683 @@ "name": "StyleProp", "elements": [ { - "name": "TextStyle" + "name": "ViewStyle" } ], - "raw": "StyleProp" + "raw": "StyleProp" }, - "description": "Style that is passed to Text element." + "description": "Style for the wrapper of the modal.\nUse this prop to change the default wrapper style or to override safe area insets with marginTop and marginBottom." }, - "maxFontSizeMultiplier": { + "theme": { "required": false, "tsType": { - "name": "number" + "name": "ThemeProp" }, - "description": "Specifies the largest possible scale a text font can reach." + "description": "" + }, + "testID": { + "required": false, + "tsType": { + "name": "string" + }, + "description": "testID to be used on tests.", + "defaultValue": { + "value": "'modal'", + "computed": false + } } } }, "type": "component", "dependencies": [ - "src/components/List/ListSubheader.tsx" - ], - "group": "List" + "src/components/Modal.tsx" + ] }, - "Menu/Menu": { - "filepath": "Menu/Menu.tsx", - "title": "Menu", - "description": "Menus display a list of choices on temporary elevated surfaces. Their placement varies based on the element that opens them.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Button, Menu, Divider, PaperProvider } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const openMenu = () => setVisible(true);\n\n const closeMenu = () => setVisible(false);\n\n return (\n \n \n Show menu}>\n {}} title=\"Item 1\" />\n {}} title=\"Item 2\" />\n \n {}} title=\"Item 3\" />\n \n \n \n );\n};\n\nexport default MyComponent;\n```\n\n### Note\nWhen using `Menu` within a React Native's `Modal` component, you need to wrap all\n`Modal` contents within a `PaperProvider` in order for the menu to show. This\nwrapping is not necessary if you use Paper's `Modal` instead.", - "link": "menu", + "NavigationBar/NavigationBar": { + "filepath": "NavigationBar/NavigationBar.tsx", + "title": "NavigationBar", + "description": "The Material Design 3 flexible navigation bar. It can easily be integrated\nwith [React Navigation's Bottom Tabs Navigator](https://reactnavigation.org/docs/bottom-tab-navigator/).\n\nSet the `variant` prop to `'horizontal'` to lay items out horizontally\n(icon beside label) in medium-width windows.\n\n## Usage\n### without React Navigation\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { NavigationBar, Text, Provider } from 'react-native-paper';\n\nfunction HomeScreen() {\n return (\n \n Home!\n \n );\n}\n\nfunction SettingsScreen() {\n return (\n \n Settings!\n \n );\n}\n\nexport default function MyComponent() {\n const [index, setIndex] = React.useState(0);\n\n const routes = [\n { key: 'home', title: 'Home', focusedIcon: 'home' },\n { key: 'settings', title: 'Settings', focusedIcon: 'cog' },\n ];\n\n const renderScene = ({ route }) => {\n switch (route.key) {\n case 'home':\n return ;\n case 'settings':\n return ;\n default:\n return null;\n }\n };\n\n return (\n \n {renderScene({ route: routes[index] })}\n {\n const newIndex = routes.findIndex((r) => r.key === route.key);\n if (newIndex !== -1) {\n setIndex(newIndex);\n }\n }}\n getLabelText={({ route }) => route.title}\n />\n \n );\n}\n```", + "link": "navigation-bar", "data": { - "description": "Menus display a list of choices on temporary elevated surfaces. Their placement varies based on the element that opens them.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Button, Menu, Divider, PaperProvider } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const openMenu = () => setVisible(true);\n\n const closeMenu = () => setVisible(false);\n\n return (\n \n \n Show menu}>\n {}} title=\"Item 1\" />\n {}} title=\"Item 2\" />\n \n {}} title=\"Item 3\" />\n \n \n \n );\n};\n\nexport default MyComponent;\n```\n\n### Note\nWhen using `Menu` within a React Native's `Modal` component, you need to wrap all\n`Modal` contents within a `PaperProvider` in order for the menu to show. This\nwrapping is not necessary if you use Paper's `Modal` instead.", - "displayName": "Menu", + "description": "The Material Design 3 flexible navigation bar. It can easily be integrated\nwith [React Navigation's Bottom Tabs Navigator](https://reactnavigation.org/docs/bottom-tab-navigator/).\n\nSet the `variant` prop to `'horizontal'` to lay items out horizontally\n(icon beside label) in medium-width windows.\n\n## Usage\n### without React Navigation\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { NavigationBar, Text, Provider } from 'react-native-paper';\n\nfunction HomeScreen() {\n return (\n \n Home!\n \n );\n}\n\nfunction SettingsScreen() {\n return (\n \n Settings!\n \n );\n}\n\nexport default function MyComponent() {\n const [index, setIndex] = React.useState(0);\n\n const routes = [\n { key: 'home', title: 'Home', focusedIcon: 'home' },\n { key: 'settings', title: 'Settings', focusedIcon: 'cog' },\n ];\n\n const renderScene = ({ route }) => {\n switch (route.key) {\n case 'home':\n return ;\n case 'settings':\n return ;\n default:\n return null;\n }\n };\n\n return (\n \n {renderScene({ route: routes[index] })}\n {\n const newIndex = routes.findIndex((r) => r.key === route.key);\n if (newIndex !== -1) {\n setIndex(newIndex);\n }\n }}\n getLabelText={({ route }) => route.title}\n />\n \n );\n}\n```", + "displayName": "NavigationBar", "methods": [], "statics": [], "props": { - "visible": { - "required": true, + "labeled": { + "required": false, "tsType": { "name": "boolean" }, - "description": "Whether the Menu is currently visible." + "description": "Whether to show labels in tabs. When `false`, only icons will be displayed.", + "defaultValue": { + "value": "true", + "computed": false + } }, - "anchor": { - "required": true, + "variant": { + "required": false, "tsType": { "name": "union", - "raw": "React.ReactNode | { x: number; y: number }", + "raw": "'stacked' | 'horizontal'", "elements": [ { - "name": "ReactReactNode", - "raw": "React.ReactNode" + "name": "literal", + "value": "'stacked'" }, { - "name": "signature", - "type": "object", - "raw": "{ x: number; y: number }", - "signature": { - "properties": [ - { - "key": "x", - "value": { - "name": "number", - "required": true + "name": "literal", + "value": "'horizontal'" + } + ] + }, + "description": "The item layout variant of the flexible navigation bar.\n\n- `stacked` (default): the icon sits above the label.\n- `horizontal`: the icon sits beside the label and the active indicator\n hugs both. Recommended for medium-width windows (e.g. foldables and\n tablets). Has no effect when `labeled` is `false`.", + "defaultValue": { + "value": "'stacked'", + "computed": false + } + }, + "compact": { + "required": false, + "tsType": { + "name": "boolean" + }, + "description": "Whether tabs should be spread across the entire width." + }, + "navigationState": { + "required": true, + "tsType": { + "name": "signature", + "type": "object", + "raw": "{\n index: number;\n routes: Route[];\n}", + "signature": { + "properties": [ + { + "key": "index", + "value": { + "name": "number", + "required": true + } + }, + { + "key": "routes", + "value": { + "name": "Array", + "elements": [ + { + "name": "Route" } - }, - { - "key": "y", - "value": { - "name": "number", - "required": true + ], + "raw": "Route[]", + "required": true + } + } + ] + } + }, + "description": "State for the bottom navigation. The state should contain the following properties:\n\n- `index`: a number representing the index of the active route in the `routes` array\n- `routes`: an array containing a list of route objects used for rendering the tabs\n\nEach route object should contain the following properties:\n\n- `key`: a unique key to identify the route (required)\n- `title`: title of the route to use as the tab label\n- `focusedIcon`: icon to use as the focused tab icon, can be a string, an image source or a react component @renamed Renamed from 'icon' to 'focusedIcon' in v5.x\n- `unfocusedIcon`: icon to use as the unfocused tab icon, can be a string, an image source or a react component @supported Available in v5.x with theme version 3\n- `badge`: badge to show on the tab icon, can be `true` to show a dot, `string` or `number` to show text.\n- `accessibilityLabel`: accessibility label for the tab button\n- `testID`: test id for the tab button\n\nExample:\n\n```js\n{\n index: 1,\n routes: [\n { key: 'music', title: 'Favorites', focusedIcon: 'heart', unfocusedIcon: 'heart-outline'},\n { key: 'albums', title: 'Albums', focusedIcon: 'album' },\n { key: 'recents', title: 'Recents', focusedIcon: 'history' },\n { key: 'notifications', title: 'Notifications', focusedIcon: 'bell', unfocusedIcon: 'bell-outline' },\n ]\n}\n```\n\n`NavigationBar` is a controlled component, which means the `index` needs to be updated via the `onTabPress` callback." + }, + "renderIcon": { + "required": false, + "tsType": { + "name": "signature", + "type": "function", + "raw": "(props: {\n route: Route;\n focused: boolean;\n color: ColorValue;\n}) => React.ReactNode", + "signature": { + "arguments": [ + { + "name": "props", + "type": { + "name": "signature", + "type": "object", + "raw": "{\n route: Route;\n focused: boolean;\n color: ColorValue;\n}", + "signature": { + "properties": [ + { + "key": "route", + "value": { + "name": "Route", + "required": true + } + }, + { + "key": "focused", + "value": { + "name": "boolean", + "required": true + } + }, + { + "key": "color", + "value": { + "name": "ColorValue", + "required": true + } + } + ] + } + } + } + ], + "return": { + "name": "ReactReactNode", + "raw": "React.ReactNode" + } + } + }, + "description": "Callback which returns a React Element to be used as tab icon." + }, + "renderLabel": { + "required": false, + "tsType": { + "name": "signature", + "type": "function", + "raw": "(props: {\n route: Route;\n focused: boolean;\n color: ColorValue;\n}) => React.ReactNode", + "signature": { + "arguments": [ + { + "name": "props", + "type": { + "name": "signature", + "type": "object", + "raw": "{\n route: Route;\n focused: boolean;\n color: ColorValue;\n}", + "signature": { + "properties": [ + { + "key": "route", + "value": { + "name": "Route", + "required": true + } + }, + { + "key": "focused", + "value": { + "name": "boolean", + "required": true + } + }, + { + "key": "color", + "value": { + "name": "ColorValue", + "required": true + } + } + ] + } + } + } + ], + "return": { + "name": "ReactReactNode", + "raw": "React.ReactNode" + } + } + }, + "description": "Callback which React Element to be used as tab label." + }, + "renderTouchable": { + "required": false, + "tsType": { + "name": "signature", + "type": "function", + "raw": "(props: TouchableProps) => React.ReactNode", + "signature": { + "arguments": [ + { + "name": "props", + "type": { + "name": "intersection", + "raw": "TouchableRippleProps & {\n key: string;\n route: Route;\n children: React.ReactNode;\n borderless?: boolean;\n centered?: boolean;\n rippleColor?: ColorValue;\n onHoverIn?: () => void;\n onHoverOut?: () => void;\n onFocus?: () => void;\n onBlur?: () => void;\n}", + "elements": [ + { + "name": "TouchableRippleProps" + }, + { + "name": "signature", + "type": "object", + "raw": "{\n key: string;\n route: Route;\n children: React.ReactNode;\n borderless?: boolean;\n centered?: boolean;\n rippleColor?: ColorValue;\n onHoverIn?: () => void;\n onHoverOut?: () => void;\n onFocus?: () => void;\n onBlur?: () => void;\n}", + "signature": { + "properties": [ + { + "key": "key", + "value": { + "name": "string", + "required": true + } + }, + { + "key": "route", + "value": { + "name": "Route", + "required": true + } + }, + { + "key": "children", + "value": { + "name": "ReactReactNode", + "raw": "React.ReactNode", + "required": true + } + }, + { + "key": "borderless", + "value": { + "name": "boolean", + "required": false + } + }, + { + "key": "centered", + "value": { + "name": "boolean", + "required": false + } + }, + { + "key": "rippleColor", + "value": { + "name": "ColorValue", + "required": false + } + }, + { + "key": "onHoverIn", + "value": { + "name": "signature", + "type": "function", + "raw": "() => void", + "signature": { + "arguments": [], + "return": { + "name": "void" + } + }, + "required": false + } + }, + { + "key": "onHoverOut", + "value": { + "name": "signature", + "type": "function", + "raw": "() => void", + "signature": { + "arguments": [], + "return": { + "name": "void" + } + }, + "required": false + } + }, + { + "key": "onFocus", + "value": { + "name": "signature", + "type": "function", + "raw": "() => void", + "signature": { + "arguments": [], + "return": { + "name": "void" + } + }, + "required": false + } + }, + { + "key": "onBlur", + "value": { + "name": "signature", + "type": "function", + "raw": "() => void", + "signature": { + "arguments": [], + "return": { + "name": "void" + } + }, + "required": false + } + } + ] + } } - } - ] + ] + } } + ], + "return": { + "name": "ReactReactNode", + "raw": "React.ReactNode" } - ] - }, - "description": "The anchor to open the menu from. In most cases, it will be a button that opens the menu." - }, - "anchorPosition": { - "required": false, - "tsType": { - "name": "union", - "raw": "'top' | 'bottom'", - "elements": [ - { - "name": "literal", - "value": "'top'" - }, - { - "name": "literal", - "value": "'bottom'" - } - ] - }, - "description": "Whether the menu should open at the top of the anchor or at its bottom.\nApplied only when anchor is a node, not an x/y position." - }, - "statusBarHeight": { - "required": false, - "tsType": { - "name": "number" + } }, - "description": "Extra margin to add at the top of the menu to account for translucent status bar on Android.\nIf you are using Expo, we assume translucent status bar and set a height for status bar automatically.\nPass `0` or a custom value to and customize it.\nThis is automatically handled on iOS." + "description": "Callback which returns a React element to be used as the touchable for the tab item.\nRenders a `TouchableRipple` on Android and `Pressable` on iOS.", + "defaultValue": { + "value": "({ key, ...props }: TouchableProps) => (\n \n)", + "computed": false + } }, - "onDismiss": { + "getAccessibilityLabel": { "required": false, "tsType": { "name": "signature", "type": "function", - "raw": "() => void", + "raw": "(props: { route: Route }) => string | undefined", "signature": { - "arguments": [], + "arguments": [ + { + "name": "props", + "type": { + "name": "signature", + "type": "object", + "raw": "{ route: Route }", + "signature": { + "properties": [ + { + "key": "route", + "value": { + "name": "Route", + "required": true + } + } + ] + } + } + } + ], "return": { - "name": "void" + "name": "union", + "raw": "string | undefined", + "elements": [ + { + "name": "string" + }, + { + "name": "undefined" + } + ] } } }, - "description": "Callback called when Menu is dismissed. The `visible` prop needs to be updated when this is called." - }, - "overlayAccessibilityLabel": { - "required": false, - "tsType": { - "name": "string" - }, - "description": "Accessibility label for the overlay. This is read by the screen reader when the user taps outside the menu.", + "description": "Get accessibility label for the tab button. This is read by the screen reader when the user taps the tab.\nUses `route.accessibilityLabel` by default.", "defaultValue": { - "value": "'Close menu'", + "value": "({ route }: { route: Route }) =>\nroute.accessibilityLabel", "computed": false } }, - "children": { - "required": true, - "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" - }, - "description": "Content of the `Menu`." - }, - "contentStyle": { + "getBadge": { "required": false, "tsType": { - "name": "Animated.WithAnimatedValue", - "elements": [ - { - "name": "StyleProp", + "name": "signature", + "type": "function", + "raw": "(props: { route: Route }) => boolean | number | string | undefined", + "signature": { + "arguments": [ + { + "name": "props", + "type": { + "name": "signature", + "type": "object", + "raw": "{ route: Route }", + "signature": { + "properties": [ + { + "key": "route", + "value": { + "name": "Route", + "required": true + } + } + ] + } + } + } + ], + "return": { + "name": "union", + "raw": "boolean | number | string | undefined", "elements": [ { - "name": "ViewStyle" + "name": "boolean" + }, + { + "name": "number" + }, + { + "name": "string" + }, + { + "name": "undefined" } - ], - "raw": "StyleProp" - } - ], - "raw": "Animated.WithAnimatedValue>" - }, - "description": "Style of menu's inner content." - }, - "style": { - "required": false, - "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" + ] } - ], - "raw": "StyleProp" + } }, - "description": "" + "description": "Get badge for the tab, uses `route.badge` by default.", + "defaultValue": { + "value": "({ route }: { route: Route }) => route.badge", + "computed": false + } }, - "elevation": { + "getLabelText": { "required": false, "tsType": { - "name": "Elevation" + "name": "signature", + "type": "function", + "raw": "(props: { route: Route }) => string | undefined", + "signature": { + "arguments": [ + { + "name": "props", + "type": { + "name": "signature", + "type": "object", + "raw": "{ route: Route }", + "signature": { + "properties": [ + { + "key": "route", + "value": { + "name": "Route", + "required": true + } + } + ] + } + } + } + ], + "return": { + "name": "union", + "raw": "string | undefined", + "elements": [ + { + "name": "string" + }, + { + "name": "undefined" + } + ] + } + } }, - "description": "Elevation level of the menu's content. Shadow styles are calculated based on this value. Default `backgroundColor` is taken from the corresponding `theme.colors.elevation` property. By default equals `2`.\n@supported Available in v5.x with theme version 3", + "description": "Get label text for the tab, uses `route.title` by default. Use `renderLabel` to replace label component.", "defaultValue": { - "value": "2", + "value": "({ route }: { route: Route }) => route.title", "computed": false } }, - "mode": { + "getTestID": { "required": false, "tsType": { - "name": "union", - "raw": "'flat' | 'elevated'", - "elements": [ - { - "name": "literal", - "value": "'flat'" - }, - { - "name": "literal", - "value": "'elevated'" + "name": "signature", + "type": "function", + "raw": "(props: { route: Route }) => string | undefined", + "signature": { + "arguments": [ + { + "name": "props", + "type": { + "name": "signature", + "type": "object", + "raw": "{ route: Route }", + "signature": { + "properties": [ + { + "key": "route", + "value": { + "name": "Route", + "required": true + } + } + ] + } + } + } + ], + "return": { + "name": "union", + "raw": "string | undefined", + "elements": [ + { + "name": "string" + }, + { + "name": "undefined" + } + ] } - ] + } }, - "description": "Mode of the menu's content.\n- `elevated` - Surface with a shadow and background color corresponding to set `elevation` value.\n- `flat` - Surface without a shadow, with the background color corresponding to set `elevation` value.\n\n@supported Available in v5.x with theme version 3", + "description": "Get the id to locate this tab button in tests, uses `route.testID` by default.", "defaultValue": { - "value": "'elevated'", + "value": "({ route }: { route: Route }) => route.testID", "computed": false } }, - "theme": { - "required": false, - "tsType": { - "name": "ThemeProp" - }, - "description": "" - }, - "keyboardShouldPersistTaps": { - "required": false, - "tsType": { - "name": "ScrollViewProps['keyboardShouldPersistTaps']", - "raw": "ScrollViewProps['keyboardShouldPersistTaps']" - }, - "description": "Inner ScrollView prop" - }, - "testID": { - "required": false, - "tsType": { - "name": "string" - }, - "description": "testID to be used on tests.", - "defaultValue": { - "value": "'menu'", - "computed": false - } - } - } - }, - "type": "component", - "dependencies": [ - "src/components/Menu/Menu.tsx" - ] - }, - "Menu/MenuItem": { - "filepath": "Menu/MenuItem.tsx", - "title": "Menu.Item", - "description": "A component to show a single list item inside a Menu.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Menu } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n {}} title=\"Redo\" />\n {}} title=\"Undo\" />\n {}} title=\"Cut\" disabled />\n {}} title=\"Copy\" disabled />\n {}} title=\"Paste\" />\n \n);\n\nexport default MyComponent;\n```", - "link": "menu-item", - "data": { - "description": "A component to show a single list item inside a Menu.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { View } from 'react-native';\nimport { Menu } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n {}} title=\"Redo\" />\n {}} title=\"Undo\" />\n {}} title=\"Cut\" disabled />\n {}} title=\"Copy\" disabled />\n {}} title=\"Paste\" />\n \n);\n\nexport default MyComponent;\n```", - "displayName": "Menu.Item", - "methods": [], - "statics": [], - "props": { - "title": { + "onTabPress": { "required": true, "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" - }, - "description": "Title text for the `MenuItem`." - }, - "leadingIcon": { - "required": false, - "tsType": { - "name": "IconSource" - }, - "description": "@renamed Renamed from 'icon' to 'leadingIcon' in v5.x\n\nLeading icon to display for the `MenuItem`." - }, - "trailingIcon": { - "required": false, - "tsType": { - "name": "IconSource" - }, - "description": "@supported Available in v5.x with theme version 3\n\nTrailing icon to display for the `MenuItem`." - }, - "disabled": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "Whether the 'item' is disabled. A disabled 'item' is greyed out and `onPress` is not called on touch." - }, - "dense": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "@supported Available in v5.x with theme version 3\n\nSets min height with densed layout." - }, - "background": { - "required": false, - "tsType": { - "name": "PressableAndroidRippleConfig" + "name": "signature", + "type": "function", + "raw": "(props: { route: Route } & TabPressEvent) => void", + "signature": { + "arguments": [ + { + "name": "props", + "type": { + "name": "intersection", + "raw": "{ route: Route } & TabPressEvent", + "elements": [ + { + "name": "signature", + "type": "object", + "raw": "{ route: Route }", + "signature": { + "properties": [ + { + "key": "route", + "value": { + "name": "Route", + "required": true + } + } + ] + } + }, + { + "name": "signature", + "type": "object", + "raw": "{\n defaultPrevented: boolean;\n preventDefault(): void;\n}", + "signature": { + "properties": [ + { + "key": "defaultPrevented", + "value": { + "name": "boolean", + "required": true + } + }, + { + "key": "preventDefault", + "value": { + "name": "void", + "required": true + } + } + ] + } + } + ] + } + } + ], + "return": { + "name": "void" + } + } }, - "description": "Type of background drawabale to display the feedback (Android).\nhttps://reactnative.dev/docs/pressable#rippleconfig" + "description": "Function to execute on tab press. It receives the route for the pressed tab. Use this to update the navigation state." }, - "onPress": { + "onTabLongPress": { "required": false, "tsType": { "name": "signature", "type": "function", - "raw": "(e: GestureResponderEvent) => void", + "raw": "(props: { route: Route } & TabPressEvent) => void", "signature": { "arguments": [ { - "name": "e", + "name": "props", "type": { - "name": "GestureResponderEvent" + "name": "intersection", + "raw": "{ route: Route } & TabPressEvent", + "elements": [ + { + "name": "signature", + "type": "object", + "raw": "{ route: Route }", + "signature": { + "properties": [ + { + "key": "route", + "value": { + "name": "Route", + "required": true + } + } + ] + } + }, + { + "name": "signature", + "type": "object", + "raw": "{\n defaultPrevented: boolean;\n preventDefault(): void;\n}", + "signature": { + "properties": [ + { + "key": "defaultPrevented", + "value": { + "name": "boolean", + "required": true + } + }, + { + "key": "preventDefault", + "value": { + "name": "void", + "required": true + } + } + ] + } + } + ] } } ], @@ -9219,236 +9417,89 @@ } } }, - "description": "Function to execute on press." - }, - "titleMaxFontSizeMultiplier": { - "required": false, - "tsType": { - "name": "number" - }, - "description": "Specifies the largest possible scale a title font can reach.", - "defaultValue": { - "value": "1.5", - "computed": false - } - }, - "style": { - "required": false, - "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" - }, - "description": "Style that is passed to the root TouchableRipple container.\n" - }, - "containerStyle": { - "required": false, - "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" - }, - "description": "Style that is passed to the outermost container that wraps the entire content, including leading and trailing icons and title text." - }, - "contentStyle": { - "required": false, - "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" - }, - "description": "Style that is passed to the content container, which wraps the title text." - }, - "titleStyle": { - "required": false, - "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "TextStyle" - } - ], - "raw": "StyleProp" - }, - "description": "Style that is passed to the Title element." - }, - "theme": { - "required": false, - "tsType": { - "name": "ThemeProp" - }, - "description": "" - }, - "hitSlop": { - "required": false, - "tsType": { - "name": "TouchableRippleProps['hitSlop']", - "raw": "TouchableRippleProps['hitSlop']" - }, - "description": "Sets additional distance outside of element in which a press can be detected." + "description": "Function to execute on tab long press. It receives the route for the pressed tab" }, - "testID": { + "activeColor": { "required": false, "tsType": { "name": "string" }, - "description": "TestID used for testing purposes", - "defaultValue": { - "value": "'menu-item'", - "computed": false - } + "description": "Custom color for icon and label in the active tab." }, - "aria-label": { + "inactiveColor": { "required": false, "tsType": { "name": "string" }, - "description": "Accessibility label for the Touchable. This is read by the screen reader when the user taps the component." - }, - "aria-checked": { - "required": false, - "tsType": { - "name": "union", - "raw": "boolean | 'mixed'", - "elements": [ - { - "name": "boolean" - }, - { - "name": "literal", - "value": "'mixed'" - } - ] - }, - "description": "Indicates whether the element is checked. Accepts `true`, `false`,\nor `'mixed'` for an indeterminate state." - }, - "aria-selected": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "Indicates whether the element is selected." - }, - "aria-busy": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "Indicates whether the element is currently busy (e.g. loading)." + "description": "Custom color for icon and label in the inactive tab." }, - "aria-expanded": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "Indicates whether the element's controlled content is expanded." - } - } - }, - "type": "component", - "dependencies": [ - "src/components/Menu/MenuItem.tsx" - ], - "group": "Menu" - }, - "Modal": { - "filepath": "Modal.tsx", - "title": "Modal", - "description": "The Modal component is a simple way to present content above an enclosing view.\nTo render the `Modal` above other components, you'll need to wrap it with the [`Portal`](./Portal) component.\nNote that this modal is NOT accessible by default; if you need an accessible modal, please use the React Native Modal.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Modal, Portal, Text, Button, PaperProvider } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const showModal = () => setVisible(true);\n const hideModal = () => setVisible(false);\n const containerStyle = { backgroundColor: 'white', padding: 20 };\n\n return (\n \n \n \n Example Modal. Click outside this area to dismiss.\n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", - "link": "modal", - "data": { - "description": "The Modal component is a simple way to present content above an enclosing view.\nTo render the `Modal` above other components, you'll need to wrap it with the [`Portal`](./Portal) component.\nNote that this modal is NOT accessible by default; if you need an accessible modal, please use the React Native Modal.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Modal, Portal, Text, Button, PaperProvider } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const showModal = () => setVisible(true);\n const hideModal = () => setVisible(false);\n const containerStyle = { backgroundColor: 'white', padding: 20 };\n\n return (\n \n \n \n Example Modal. Click outside this area to dismiss.\n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", - "displayName": "Modal", - "methods": [], - "statics": [], - "props": { - "dismissable": { + "keyboardHidesNavigationBar": { "required": false, "tsType": { "name": "boolean" }, - "description": "Determines whether clicking outside the modal dismisses it.", + "description": "Whether the bottom navigation bar is hidden when keyboard is shown.\nOn Android, this works best when [`windowSoftInputMode`](https://developer.android.com/guide/topics/manifest/activity-element#wsoft) is set to `adjustResize`.", "defaultValue": { - "value": "true", + "value": "Platform.OS === 'android'", "computed": false } }, - "dismissableBackButton": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "Determines whether clicking Android hardware back button dismisses the dialog.", - "defaultValue": { - "value": "dismissable", - "computed": true - } - }, - "onDismiss": { + "safeAreaInsets": { "required": false, "tsType": { "name": "signature", - "type": "function", - "raw": "() => void", + "type": "object", + "raw": "{\n top?: number;\n right?: number;\n bottom?: number;\n left?: number;\n}", "signature": { - "arguments": [], - "return": { - "name": "void" - } + "properties": [ + { + "key": "top", + "value": { + "name": "number", + "required": false + } + }, + { + "key": "right", + "value": { + "name": "number", + "required": false + } + }, + { + "key": "bottom", + "value": { + "name": "number", + "required": false + } + }, + { + "key": "left", + "value": { + "name": "number", + "required": false + } + } + ] } }, - "description": "Callback that is called when the user dismisses the modal.", - "defaultValue": { - "value": "() => {}", - "computed": false - } - }, - "overlayAccessibilityLabel": { - "required": false, - "tsType": { - "name": "string" - }, - "description": "Accessibility label for the overlay. This is read by the screen reader when the user taps outside the modal.", - "defaultValue": { - "value": "'Close modal'", - "computed": false - } + "description": "Safe area insets for the tab bar. This can be used to avoid elements like the navigation bar on Android and bottom safe area on iOS.\nThe bottom insets for iOS is added by default. You can override the behavior with this option." }, - "visible": { + "labelMaxFontSizeMultiplier": { "required": false, "tsType": { - "name": "boolean" + "name": "number" }, - "description": "Determines Whether the modal is visible.", + "description": "Specifies the largest possible scale a label font can reach.", "defaultValue": { - "value": "false", + "value": "1", "computed": false } }, - "children": { - "required": true, - "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" - }, - "description": "Content of the `Modal`." - }, - "contentContainerStyle": { + "style": { "required": false, "tsType": { - "name": "Animated.WithAnimatedValue", + "name": "RNAnimated.WithAnimatedValue", "elements": [ { "name": "StyleProp", @@ -9460,11 +9511,11 @@ "raw": "StyleProp" } ], - "raw": "Animated.WithAnimatedValue>" + "raw": "RNAnimated.WithAnimatedValue>" }, - "description": "Style for the content of the modal" + "description": "" }, - "style": { + "activeIndicatorStyle": { "required": false, "tsType": { "name": "StyleProp", @@ -9475,7 +9526,7 @@ ], "raw": "StyleProp" }, - "description": "Style for the wrapper of the modal.\nUse this prop to change the default wrapper style or to override safe area insets with marginTop and marginBottom." + "description": "" }, "theme": { "required": false, @@ -9489,9 +9540,9 @@ "tsType": { "name": "string" }, - "description": "testID to be used on tests.", + "description": "TestID used for testing purposes", "defaultValue": { - "value": "'modal'", + "value": "'bottom-navigation-bar'", "computed": false } } @@ -9499,7 +9550,7 @@ }, "type": "component", "dependencies": [ - "src/components/Modal.tsx" + "src/components/NavigationBar/NavigationBar.tsx" ] }, "Portal/Portal": { diff --git a/src/components/BottomNavigation/BottomNavigation.tsx b/src/components/BottomNavigation/BottomNavigation.tsx index 209a21b05b..61ca44fa07 100644 --- a/src/components/BottomNavigation/BottomNavigation.tsx +++ b/src/components/BottomNavigation/BottomNavigation.tsx @@ -253,7 +253,7 @@ const SceneComponent = React.memo(({ component, ...rest }: any) => /** * BottomNavigation provides quick navigation between top-level views of an app with a bottom navigation bar. - * It is primarily designed for use on mobile. If you want to use the navigation bar only see [`NavigationBar`](../NavigationBar). + * It is primarily designed for use on mobile. If you want to use the navigation bar only see [`NavigationBar`](../NavigationBar/NavigationBar). * * By default BottomNavigation uses primary color as a background, in dark theme with `adaptive` mode it will use surface colour instead. * See [Dark Theme](https://callstack.github.io/react-native-paper/docs/guides/theming#dark-theme) for more information. From c1bf91195a181bf7e498733835355dfc9adf6012 Mon Sep 17 00:00:00 2001 From: Bartek Dybowski Date: Fri, 3 Jul 2026 12:48:33 +0200 Subject: [PATCH 18/18] refactor(navigation-bar): remove BottomNavigation scene wrapper satya164's review feedback: BottomNavigation (the scene-transition wrapper) is niche and unnecessary since most users integrate with React Navigation. NavigationBar is now the sole public surface. - Delete BottomNavigation component + BottomNavigationRouteScreen - Remove BottomNavigation/BottomNavigationRoute from public index exports - Replace BottomNavigation in TeamDetails example with NavigationBar + local scene state - Move NavigationBar tests to NavigationBar.test.tsx, drop all BottomNavigation-specific tests - Remove BottomNavigation from docs and example list - Update babel import-rewriter fixture to drop BottomNavigation --- .../BottomNavigation/BottomNavigation.mdx | 300 - .../components/BottomNavigation/_meta.json | 3 - docs/6.x/docs/components/_meta.json | 7 - docs/component-docs.config.ts | 3 - example/src/ExampleList.tsx | 2 - .../src/Examples/BottomNavigationExample.tsx | 187 - example/src/Examples/TeamDetails.tsx | 27 +- .../__fixtures__/rewrite-imports/code.js | 1 - .../__fixtures__/rewrite-imports/output.js | 1 - .../BottomNavigation/BottomNavigation.tsx | 606 -- .../BottomNavigationRouteScreen.tsx | 33 - .../__tests__/BottomNavigation.test.tsx | 693 -- .../__tests__/NavigationBar.test.tsx | 221 + .../BottomNavigation.test.tsx.snap | 9354 ----------------- .../__snapshots__/NavigationBar.test.tsx.snap | 750 ++ src/index.tsx | 5 - 16 files changed, 985 insertions(+), 11208 deletions(-) delete mode 100644 docs/6.x/docs/components/BottomNavigation/BottomNavigation.mdx delete mode 100644 docs/6.x/docs/components/BottomNavigation/_meta.json delete mode 100644 example/src/Examples/BottomNavigationExample.tsx delete mode 100644 src/components/BottomNavigation/BottomNavigation.tsx delete mode 100644 src/components/BottomNavigation/BottomNavigationRouteScreen.tsx delete mode 100644 src/components/__tests__/BottomNavigation.test.tsx create mode 100644 src/components/__tests__/NavigationBar.test.tsx delete mode 100644 src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap create mode 100644 src/components/__tests__/__snapshots__/NavigationBar.test.tsx.snap diff --git a/docs/6.x/docs/components/BottomNavigation/BottomNavigation.mdx b/docs/6.x/docs/components/BottomNavigation/BottomNavigation.mdx deleted file mode 100644 index 2c8e93be1a..0000000000 --- a/docs/6.x/docs/components/BottomNavigation/BottomNavigation.mdx +++ /dev/null @@ -1,300 +0,0 @@ ---- -title: BottomNavigation ---- - -import PropTable from '@docs/components/PropTable.tsx'; -import ExtendsLink from '@docs/components/ExtendsLink.tsx'; -import ThemeColorsTable from '@docs/components/ThemeColorsTable.tsx'; -import ScreenshotTabs from '@docs/components/ScreenshotTabs.tsx'; -import ExtendedExample from '@docs/components/ExtendedExample.tsx'; - -BottomNavigation provides quick navigation between top-level views of an app with a bottom navigation bar. -It is primarily designed for use on mobile. If you want to use the navigation bar only see [`NavigationBar`](../NavigationBar/NavigationBar). - -By default BottomNavigation uses primary color as a background, in dark theme with `adaptive` mode it will use surface colour instead. -See [Dark Theme](https://callstack.github.io/react-native-paper/docs/guides/theming#dark-theme) for more information. - - - - - - - -## Usage -```js -import * as React from 'react'; -import { BottomNavigation, Text } from 'react-native-paper'; - -const MusicRoute = () => Music; - -const AlbumsRoute = () => Albums; - -const RecentsRoute = () => Recents; - -const NotificationsRoute = () => Notifications; - -const MyComponent = () => { - const [index, setIndex] = React.useState(0); - const [routes] = React.useState([ - { key: 'music', title: 'Favorites', focusedIcon: 'heart', unfocusedIcon: 'heart-outline'}, - { key: 'albums', title: 'Albums', focusedIcon: 'album' }, - { key: 'recents', title: 'Recents', focusedIcon: 'history' }, - { key: 'notifications', title: 'Notifications', focusedIcon: 'bell', unfocusedIcon: 'bell-outline' }, - ]); - - const renderScene = BottomNavigation.SceneMap({ - music: MusicRoute, - albums: AlbumsRoute, - recents: RecentsRoute, - notifications: NotificationsRoute, - }); - - return ( - - ); -}; - -export default MyComponent; -``` - - - ## Props - - - - -
- -### labeled - -
- - - -
- -### compact - -
- - - -
- -### navigationState (required) - -
- - - -
- -### onIndexChange (required) - -
- - - -
- -### renderScene (required) - -
- - - -
- -### renderIcon - -
- - - -
- -### renderLabel - -
- - - -
- -### renderTouchable - -
- - - -
- -### getAccessibilityLabel - -
- - - -
- -### getBadge - -
- - - -
- -### getLabelText - -
- - - -
- -### getLazy - -
- - - -
- -### getTestID - -
- - - -
- -### onTabPress - -
- - - -
- -### onTabLongPress - -
- - - -
- -### activeColor - -
- - - -
- -### inactiveColor - -
- - - -
- -### sceneAnimationEnabled - -
- - - -
- -### sceneAnimationType - -
- - - -
- -### sceneAnimationEasing - -
- - - -
- -### keyboardHidesNavigationBar - -
- - - -
- -### safeAreaInsets - -
- - - -
- -### barStyle - -
- - - -
- -### labelMaxFontSizeMultiplier - -
- - - -
- -### style - -
- - - -
- -### activeIndicatorStyle - -
- - - -
- -### theme - -
- - - -
- -### testID - -
- - - - - - - - - - diff --git a/docs/6.x/docs/components/BottomNavigation/_meta.json b/docs/6.x/docs/components/BottomNavigation/_meta.json deleted file mode 100644 index 7988479ebd..0000000000 --- a/docs/6.x/docs/components/BottomNavigation/_meta.json +++ /dev/null @@ -1,3 +0,0 @@ -[ - "BottomNavigation" -] diff --git a/docs/6.x/docs/components/_meta.json b/docs/6.x/docs/components/_meta.json index d30d12c6ee..9c240c0452 100644 --- a/docs/6.x/docs/components/_meta.json +++ b/docs/6.x/docs/components/_meta.json @@ -16,13 +16,6 @@ }, "Badge", "Banner", - { - "type": "dir", - "name": "BottomNavigation", - "label": "BottomNavigation", - "collapsible": true, - "collapsed": false - }, { "type": "dir", "name": "Button", diff --git a/docs/component-docs.config.ts b/docs/component-docs.config.ts index e588213f1b..4bc2d8e404 100644 --- a/docs/component-docs.config.ts +++ b/docs/component-docs.config.ts @@ -38,9 +38,6 @@ const pages = { }, Badge: 'Badge', Banner: 'Banner', - BottomNavigation: { - BottomNavigation: 'BottomNavigation/BottomNavigation', - }, Button: { Button: 'Button/Button', }, diff --git a/example/src/ExampleList.tsx b/example/src/ExampleList.tsx index 5c8aba19da..2dc994243c 100644 --- a/example/src/ExampleList.tsx +++ b/example/src/ExampleList.tsx @@ -10,7 +10,6 @@ import AvatarExample from './Examples/AvatarExample'; import BadgeExample from './Examples/BadgeExample'; import BannerExample from './Examples/BannerExample'; import BottomNavigationBarExample from './Examples/BottomNavigationBarExample'; -import BottomNavigationExample from './Examples/BottomNavigationExample'; import ButtonExample from './Examples/ButtonExample'; import CardExample from './Examples/CardExample'; import CheckboxExample from './Examples/CheckboxExample'; @@ -56,7 +55,6 @@ export const mainExamples = { Badge: BadgeExample, Banner: BannerExample, BottomNavigationBarExample, - BottomNavigation: BottomNavigationExample, Button: ButtonExample, Card: CardExample, Checkbox: CheckboxExample, diff --git a/example/src/Examples/BottomNavigationExample.tsx b/example/src/Examples/BottomNavigationExample.tsx deleted file mode 100644 index 8b2495fdbc..0000000000 --- a/example/src/Examples/BottomNavigationExample.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import * as React from 'react'; -import { - Dimensions, - Easing, - Image, - Platform, - StyleSheet, - View, -} from 'react-native'; - -import { useNavigation } from '@react-navigation/native'; -import { Appbar, BottomNavigation, Menu } from 'react-native-paper'; -import type { BottomNavigationRoute } from 'react-native-paper'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -import ScreenWrapper from '../ScreenWrapper'; - -type Route = { route: { key: string } }; - -const MORE_ICON = Platform.OS === 'ios' ? 'dots-horizontal' : 'dots-vertical'; - -const PhotoGallery = ({ route }: Route) => { - const PHOTOS = Array.from({ length: 24 }).map( - (_, i) => `https://unsplash.it/300/300/?random&__id=${route.key}${i}` - ); - - return ( - - {PHOTOS.map((uri) => ( - - - - ))} - - ); -}; - -const BottomNavigationExample = () => { - const navigation = useNavigation('BottomNavigation'); - - const insets = useSafeAreaInsets(); - const [index, setIndex] = React.useState(0); - const [menuVisible, setMenuVisible] = React.useState(false); - const [sceneAnimation, setSceneAnimation] = - React.useState< - React.ComponentProps['sceneAnimationType'] - >(); - - const [routes] = React.useState([ - { - key: 'album', - title: 'Album', - focusedIcon: 'image-album', - }, - { - key: 'library', - title: 'Library', - focusedIcon: 'inbox', - unfocusedIcon: 'inbox-outline', - badge: true, - }, - { - key: 'favorites', - title: 'Favorites', - focusedIcon: 'heart', - unfocusedIcon: 'heart-outline', - }, - { - key: 'purchased', - title: 'Purchased', - focusedIcon: 'shopping', - unfocusedIcon: 'shopping-outline', - }, - ]); - - React.useLayoutEffect(() => { - navigation.setOptions({ - headerShown: false, - }); - }, [navigation]); - - return ( - - - navigation.goBack()} /> - - setMenuVisible(false)} - anchor={ - setMenuVisible(true)} - /> - } - > - { - setSceneAnimation(undefined); - setMenuVisible(false); - }} - title="Scene animation: none" - /> - { - setSceneAnimation('shifting'); - setMenuVisible(false); - }} - title="Scene animation: shifting" - /> - { - setSceneAnimation('opacity'); - setMenuVisible(false); - }} - title="Scene animation: opacity" - /> - - - route.key !== 'album'} - /> - - ); -}; - -BottomNavigationExample.title = 'Bottom Navigation'; - -export default BottomNavigationExample; - -const styles = StyleSheet.create({ - ...Platform.select({ - web: { - content: { - // there is no 'grid' type in RN :( - display: 'grid' as 'none', - gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', - gridRowGap: '8px', - gridColumnGap: '8px', - padding: 8, - }, - item: { - width: '100%', - height: 150, - }, - }, - default: { - content: { - flexDirection: 'row', - flexWrap: 'wrap', - padding: 4, - }, - item: { - height: Dimensions.get('window').width / 2, - width: '50%', - padding: 4, - }, - }, - }), - photo: { - flex: 1, - }, - screen: { - flex: 1, - }, -}); diff --git a/example/src/Examples/TeamDetails.tsx b/example/src/Examples/TeamDetails.tsx index 5970274f31..f032fdbd36 100644 --- a/example/src/Examples/TeamDetails.tsx +++ b/example/src/Examples/TeamDetails.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { - Easing, FlatList, ScrollView, StyleSheet, @@ -11,7 +10,7 @@ import { import { useNavigation, useRoute } from '@react-navigation/native'; import { Appbar, - BottomNavigation, + NavigationBar, Card, Button, Text, @@ -211,20 +210,19 @@ const ThemeBasedOnSourceColor = () => { navigation.goBack()} /> - + {routes[index].key === 'news' && } + {routes[index].key === 'results' && } + {routes[index].key === 'roster' && } +
+ { + const i = routes.findIndex((r) => r.key === route.key); + if (i !== -1 && i !== index) setIndex(i); + }} labelMaxFontSizeMultiplier={2} - renderScene={BottomNavigation.SceneMap({ - news: News, - results: Results, - roster: Roster, - })} - sceneAnimationEnabled - sceneAnimationType={'opacity'} - sceneAnimationEasing={Easing.ease} - getLazy={({ route }) => route.key !== 'album'} />
@@ -239,6 +237,9 @@ const styles = StyleSheet.create({ screen: { flex: 1, }, + scene: { + flex: 1, + }, winner: { fontWeight: '700', }, diff --git a/src/babel/__fixtures__/rewrite-imports/code.js b/src/babel/__fixtures__/rewrite-imports/code.js index f1253a5e08..a1028ff585 100644 --- a/src/babel/__fixtures__/rewrite-imports/code.js +++ b/src/babel/__fixtures__/rewrite-imports/code.js @@ -1,7 +1,6 @@ /* eslint-disable prettier/prettier */ import { PaperProvider, - BottomNavigation, Button, FAB, Appbar, diff --git a/src/babel/__fixtures__/rewrite-imports/output.js b/src/babel/__fixtures__/rewrite-imports/output.js index bbe342ad0d..5e04ea94dd 100644 --- a/src/babel/__fixtures__/rewrite-imports/output.js +++ b/src/babel/__fixtures__/rewrite-imports/output.js @@ -1,6 +1,5 @@ /* eslint-disable prettier/prettier */ import PaperProvider from "react-native-paper/lib/module/core/PaperProvider"; -import BottomNavigation from "react-native-paper/lib/module/components/BottomNavigation/BottomNavigation"; import Button from "react-native-paper/lib/module/components/Button/Button"; import FAB from "react-native-paper/lib/module/components/FAB"; import Appbar from "react-native-paper/lib/module/components/Appbar"; diff --git a/src/components/BottomNavigation/BottomNavigation.tsx b/src/components/BottomNavigation/BottomNavigation.tsx deleted file mode 100644 index 61ca44fa07..0000000000 --- a/src/components/BottomNavigation/BottomNavigation.tsx +++ /dev/null @@ -1,606 +0,0 @@ -import * as React from 'react'; -import { Animated, Platform, StyleSheet, View } from 'react-native'; -import type { - ColorValue, - EasingFunction, - StyleProp, - ViewStyle, -} from 'react-native'; - -import useLatestCallback from 'use-latest-callback'; - -import BottomNavigationRouteScreen from './BottomNavigationRouteScreen'; -import { useInternalTheme } from '../../core/theming'; -import type { ThemeProp } from '../../types'; -import useAnimatedValueArray from '../../utils/useAnimatedValueArray'; -import type { IconSource } from '../Icon'; -import NavigationBar from '../NavigationBar/NavigationBar'; -import type { Props as TouchableRippleProps } from '../TouchableRipple/TouchableRipple'; - -export type BaseRoute = { - key: string; - title?: string; - focusedIcon?: IconSource; - unfocusedIcon?: IconSource; - badge?: string | number | boolean; - 'aria-label'?: string; - testID?: string; - lazy?: boolean; -}; - -type NavigationState = { - index: number; - routes: Route[]; -}; - -type TabPressEvent = { - defaultPrevented: boolean; - preventDefault(): void; -}; - -type TouchableProps = TouchableRippleProps & { - key: string; - route: Route; - children: React.ReactNode; - borderless?: boolean; - centered?: boolean; - rippleColor?: ColorValue; -}; - -export type Props = { - /** - * Whether to show labels in tabs. When `false`, only icons will be displayed. - */ - labeled?: boolean; - /** - * Whether tabs should be spread across the entire width. - */ - compact?: boolean; - /** - * State for the bottom navigation. The state should contain the following properties: - * - * - `index`: a number representing the index of the active route in the `routes` array - * - `routes`: an array containing a list of route objects used for rendering the tabs - * - * Each route object should contain the following properties: - * - * - `key`: a unique key to identify the route (required) - * - `title`: title of the route to use as the tab label - * - `focusedIcon`: icon to use as the focused tab icon, can be a string, an image source or a react component @renamed Renamed from 'icon' to 'focusedIcon' in v5.x - * - `unfocusedIcon`: icon to use as the unfocused tab icon, can be a string, an image source or a react component @supported Available in v5.x with theme version 3 - * - `badge`: badge to show on the tab icon, can be `true` to show a dot, `string` or `number` to show text. - * - `aria-label`: accessibility label for the tab button - * - `testID`: test id for the tab button - * - * Example: - * - * ```js - * { - * index: 1, - * routes: [ - * { key: 'music', title: 'Favorites', focusedIcon: 'heart', unfocusedIcon: 'heart-outline'}, - * { key: 'albums', title: 'Albums', focusedIcon: 'album' }, - * { key: 'recents', title: 'Recents', focusedIcon: 'history' }, - * { key: 'notifications', title: 'Notifications', focusedIcon: 'bell', unfocusedIcon: 'bell-outline' }, - * ] - * } - * ``` - * - * `BottomNavigation` is a controlled component, which means the `index` needs to be updated via the `onIndexChange` callback. - */ - navigationState: NavigationState; - /** - * Callback which is called on tab change, receives the index of the new tab as argument. - * The navigation state needs to be updated when it's called, otherwise the change is dropped. - */ - onIndexChange: (index: number) => void; - /** - * Callback which returns a react element to render as the page for the tab. Receives an object containing the route as the argument: - * - * ```js - * renderScene = ({ route, jumpTo }) => { - * switch (route.key) { - * case 'music': - * return ; - * case 'albums': - * return ; - * } - * } - * ``` - * - * Pages are lazily rendered, which means that a page will be rendered the first time you navigate to it. - * After initial render, all the pages stay rendered to preserve their state. - * - * You need to make sure that your individual routes implement a `shouldComponentUpdate` to improve the performance. - * To make it easier to specify the components, you can use the `SceneMap` helper: - * - * ```js - * renderScene = BottomNavigation.SceneMap({ - * music: MusicRoute, - * albums: AlbumsRoute, - * }); - * ``` - * - * Specifying the components this way is easier and takes care of implementing a `shouldComponentUpdate` method. - * Each component will receive the current route and a `jumpTo` method as it's props. - * The `jumpTo` method can be used to navigate to other tabs programmatically: - * - * ```js - * this.props.jumpTo('albums') - * ``` - */ - renderScene: (props: { - route: Route; - jumpTo: (key: string) => void; - }) => React.ReactNode | null; - /** - * Callback which returns a React Element to be used as tab icon. - */ - renderIcon?: (props: { - route: Route; - focused: boolean; - color: ColorValue; - }) => React.ReactNode; - /** - * Callback which React Element to be used as tab label. - */ - renderLabel?: (props: { - route: Route; - focused: boolean; - color: ColorValue; - }) => React.ReactNode; - /** - * Callback which returns a React element to be used as the touchable for the tab item. - * Renders a `TouchableRipple` on Android and `Pressable` on iOS. - */ - renderTouchable?: (props: TouchableProps) => React.ReactNode; - /** - * Get accessibility label for the tab button. This is read by the screen reader when the user taps the tab. - * Uses `route['aria-label']` by default. - */ - getAccessibilityLabel?: (props: { route: Route }) => string | undefined; - /** - * Get badge for the tab, uses `route.badge` by default. - */ - getBadge?: (props: { route: Route }) => boolean | number | string | undefined; - /** - * Get label text for the tab, uses `route.title` by default. Use `renderLabel` to replace label component. - */ - getLabelText?: (props: { route: Route }) => string | undefined; - /** - * Get lazy for the current screen. Uses true by default. - */ - getLazy?: (props: { route: Route }) => boolean | undefined; - /** - * Get the id to locate this tab button in tests, uses `route.testID` by default. - */ - getTestID?: (props: { route: Route }) => string | undefined; - /** - * Function to execute on tab press. It receives the route for the pressed tab, useful for things like scroll to top. - */ - onTabPress?: (props: { route: Route } & TabPressEvent) => void; - /** - * Function to execute on tab long press. It receives the route for the pressed tab, useful for things like custom action when longed pressed. - */ - onTabLongPress?: (props: { route: Route } & TabPressEvent) => void; - /** - * Custom color for icon and label in the active tab. - */ - activeColor?: string; - /** - * Custom color for icon and label in the inactive tab. - */ - inactiveColor?: string; - /** - * Whether animation is enabled for scene transitions. - * By default, the scenes cross-fade during tab change. - * Specify `sceneAnimationEnabled` as `false` to disable the animation. - */ - sceneAnimationEnabled?: boolean; - /** - * The scene animation effect. Specify `'shifting'` for a different effect. - * By default, 'opacity' will be used. - */ - sceneAnimationType?: 'opacity' | 'shifting'; - /** - * The scene animation Easing. - */ - sceneAnimationEasing?: EasingFunction | undefined; - /** - * Whether the bottom navigation bar is hidden when keyboard is shown. - * On Android, this works best when [`windowSoftInputMode`](https://developer.android.com/guide/topics/manifest/activity-element#wsoft) is set to `adjustResize`. - */ - keyboardHidesNavigationBar?: boolean; - /** - * Safe area insets for the tab bar. This can be used to avoid elements like the navigation bar on Android and bottom safe area on iOS. - * The bottom insets for iOS is added by default. You can override the behavior with this option. - */ - safeAreaInsets?: { - top?: number; - right?: number; - bottom?: number; - left?: number; - }; - /** - * Style for the bottom navigation bar. You can pass a custom background color here: - * - * ```js - * barStyle={{ backgroundColor: '#694fad' }} - * ``` - */ - barStyle?: Animated.WithAnimatedValue>; - /** - * Specifies the largest possible scale a label font can reach. - */ - labelMaxFontSizeMultiplier?: number; - style?: StyleProp; - activeIndicatorStyle?: StyleProp; - /** - * @optional - */ - theme?: ThemeProp; - /** - * TestID used for testing purposes - */ - testID?: string; -}; - -const FAR_FAR_AWAY = Platform.OS === 'web' ? 0 : 9999; - -const SceneComponent = React.memo(({ component, ...rest }: any) => - React.createElement(component, rest) -); - -/** - * BottomNavigation provides quick navigation between top-level views of an app with a bottom navigation bar. - * It is primarily designed for use on mobile. If you want to use the navigation bar only see [`NavigationBar`](../NavigationBar/NavigationBar). - * - * By default BottomNavigation uses primary color as a background, in dark theme with `adaptive` mode it will use surface colour instead. - * See [Dark Theme](https://callstack.github.io/react-native-paper/docs/guides/theming#dark-theme) for more information. - * - * ## Usage - * ```js - * import * as React from 'react'; - * import { BottomNavigation, Text } from 'react-native-paper'; - * - * const MusicRoute = () => Music; - * - * const AlbumsRoute = () => Albums; - * - * const RecentsRoute = () => Recents; - * - * const NotificationsRoute = () => Notifications; - * - * const MyComponent = () => { - * const [index, setIndex] = React.useState(0); - * const [routes] = React.useState([ - * { key: 'music', title: 'Favorites', focusedIcon: 'heart', unfocusedIcon: 'heart-outline'}, - * { key: 'albums', title: 'Albums', focusedIcon: 'album' }, - * { key: 'recents', title: 'Recents', focusedIcon: 'history' }, - * { key: 'notifications', title: 'Notifications', focusedIcon: 'bell', unfocusedIcon: 'bell-outline' }, - * ]); - * - * const renderScene = BottomNavigation.SceneMap({ - * music: MusicRoute, - * albums: AlbumsRoute, - * recents: RecentsRoute, - * notifications: NotificationsRoute, - * }); - * - * return ( - * - * ); - * }; - * - * export default MyComponent; - * ``` - */ -const BottomNavigation = ({ - navigationState, - renderScene, - renderIcon, - renderLabel, - renderTouchable, - getLabelText, - getBadge, - getAccessibilityLabel, - getTestID, - activeColor, - inactiveColor, - keyboardHidesNavigationBar = Platform.OS === 'android', - barStyle, - labeled = true, - style, - activeIndicatorStyle, - sceneAnimationEnabled = false, - sceneAnimationType = 'opacity', - sceneAnimationEasing, - onTabPress, - onTabLongPress, - onIndexChange, - safeAreaInsets, - labelMaxFontSizeMultiplier = 1, - compact: compactProp, - testID = 'bottom-navigation', - theme: themeOverrides, - getLazy = ({ route }: { route: Route }) => route.lazy, -}: Props) => { - const theme = useInternalTheme(themeOverrides); - const { scale } = theme.animation; - const compact = compactProp ?? false; - - const focusedKey = navigationState.routes[navigationState.index].key; - - /** - * Active state of individual tab item positions: - * -1 if they're before the active tab, 0 if they're active, 1 if they're after the active tab - */ - const tabsPositionAnims = useAnimatedValueArray( - navigationState.routes.map((_, i) => - i === navigationState.index ? 0 : i >= navigationState.index ? 1 : -1 - ) - ); - - /** - * The top offset for each tab item to position it offscreen. - * Placing items offscreen helps to save memory usage for inactive screens with removeClippedSubviews. - * We use animated values for this to prevent unnecessary re-renders. - */ - const offsetsAnims = useAnimatedValueArray( - navigationState.routes.map( - // offscreen === 1, normal === 0 - (_, i) => (i === navigationState.index ? 0 : 1) - ) - ); - - /** - * List of loaded tabs, tabs will be loaded when navigated to. - */ - const [loaded, setLoaded] = React.useState([focusedKey]); - - if (!loaded.includes(focusedKey)) { - // Set the current tab to be loaded if it was not loaded before - setLoaded((loaded) => [...loaded, focusedKey]); - } - - const animateToIndex = React.useCallback( - (index: number) => { - Animated.parallel([ - ...navigationState.routes.map((_, i) => - Animated.timing(tabsPositionAnims[i], { - toValue: i === index ? 0 : i >= index ? 1 : -1, - duration: 150 * scale, - useNativeDriver: true, - easing: sceneAnimationEasing, - }) - ), - ]).start(({ finished }) => { - if (finished) { - // Position all inactive screens offscreen to save memory usage - // Only do it when animation has finished to avoid glitches mid-transition if switching fast - offsetsAnims.forEach((offset, i) => { - if (i === index) { - offset.setValue(0); - } else { - offset.setValue(1); - } - }); - } - }); - }, - [ - navigationState.routes, - offsetsAnims, - scale, - tabsPositionAnims, - sceneAnimationEasing, - ] - ); - - React.useEffect(() => { - // Workaround for native animated bug in react-native@^0.57 - // Context: https://github.com/callstack/react-native-paper/pull/637 - animateToIndex(navigationState.index); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const prevNavigationState = React.useRef | undefined>( - undefined - ); - - React.useEffect(() => { - // Reset offsets of previous and current tabs before animation - offsetsAnims.forEach((offset, i) => { - if ( - i === navigationState.index || - i === prevNavigationState.current?.index - ) { - offset.setValue(0); - } - }); - - animateToIndex(navigationState.index); - }, [navigationState.index, animateToIndex, offsetsAnims]); - - const handleTabPress = useLatestCallback( - (event: { route: Route } & TabPressEvent) => { - onTabPress?.(event); - - if (event.defaultPrevented) { - return; - } - - const index = navigationState.routes.findIndex( - (route) => event.route.key === route.key - ); - - if (index !== navigationState.index) { - prevNavigationState.current = navigationState; - onIndexChange(index); - } - } - ); - - const jumpTo = useLatestCallback((key: string) => { - const index = navigationState.routes.findIndex( - (route) => route.key === key - ); - - prevNavigationState.current = navigationState; - onIndexChange(index); - }); - - const { routes } = navigationState; - const { colors } = theme; - - return ( - - - {routes.map((route, index) => { - if (getLazy({ route }) !== false && !loaded.includes(route.key)) { - // Don't render a screen if we've never navigated to it - return null; - } - - const focused = navigationState.index === index; - const previouslyFocused = - prevNavigationState.current?.index === index; - const countAlphaOffscreen = - sceneAnimationEnabled && (focused || previouslyFocused); - const renderToHardwareTextureAndroid = - sceneAnimationEnabled && focused; - - const opacity = sceneAnimationEnabled - ? tabsPositionAnims[index].interpolate({ - inputRange: [-1, 0, 1], - outputRange: [0, 1, 0], - }) - : focused - ? 1 - : 0; - - const offsetTarget = focused ? 0 : FAR_FAR_AWAY; - - const top = sceneAnimationEnabled - ? offsetsAnims[index].interpolate({ - inputRange: [0, 1], - outputRange: [0, offsetTarget], - }) - : offsetTarget; - - const left = - sceneAnimationType === 'shifting' - ? tabsPositionAnims[index].interpolate({ - inputRange: [-1, 0, 1], - outputRange: [-50, 0, 50], - }) - : 0; - - const zIndex = focused ? 1 : 0; - - return ( - - - {renderScene({ route, jumpTo })} - - - ); - })} - - - - ); -}; - -/** - * Function which takes a map of route keys to components. - * Pure components are used to minimize re-rendering of the pages. - * This drastically improves the animation performance. - */ -BottomNavigation.SceneMap = (scenes: { - [key: string]: React.ComponentType<{ - route: Route; - jumpTo: (key: string) => void; - }>; -}) => { - return ({ - route, - jumpTo, - }: { - route: Route; - jumpTo: (key: string) => void; - }) => ( - - ); -}; - -export default BottomNavigation; - -const styles = StyleSheet.create({ - container: { - flex: 1, - overflow: 'hidden', - }, - content: { - flex: 1, - }, -}); diff --git a/src/components/BottomNavigation/BottomNavigationRouteScreen.tsx b/src/components/BottomNavigation/BottomNavigationRouteScreen.tsx deleted file mode 100644 index 5773e2e1fd..0000000000 --- a/src/components/BottomNavigation/BottomNavigationRouteScreen.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import type { ReactNode } from 'react'; -import { Animated, Platform, View } from 'react-native'; -import type { ViewProps } from 'react-native'; - -interface Props extends ViewProps { - visibility?: 0 | 1 | Animated.AnimatedInterpolation; - index: number; -} - -class BottomNavigationRouteScreen extends React.Component { - render(): ReactNode { - const { style, index, children, visibility, ...rest } = this.props; - - // On Web, the unfocused tab screens can still be clicked since they are transparent, but still there - // Hiding them with `display: none` makes sure that they won't receive clicks - // We only set it on Web since on native, react-native-pager-view's breaks due to layout changing - const display = - Platform.OS === 'web' ? (visibility === 0 ? 'none' : 'flex') : undefined; - - return ( - - {children} - - ); - } -} - -export default Animated.createAnimatedComponent(BottomNavigationRouteScreen); diff --git a/src/components/__tests__/BottomNavigation.test.tsx b/src/components/__tests__/BottomNavigation.test.tsx deleted file mode 100644 index a808f1d521..0000000000 --- a/src/components/__tests__/BottomNavigation.test.tsx +++ /dev/null @@ -1,693 +0,0 @@ -import { Animated, Easing, Platform, StyleSheet, Text } from 'react-native'; - -import { describe, expect, it, jest } from '@jest/globals'; -import { act, fireEvent, userEvent } from '@testing-library/react-native'; - -import { getTheme } from '../../core/theming'; -import { render, screen } from '../../test-utils'; -import { Palette } from '../../theme/tokens'; -import BottomNavigation from '../BottomNavigation/BottomNavigation'; -import BottomNavigationRouteScreen from '../BottomNavigation/BottomNavigationRouteScreen'; -import Icon from '../Icon'; -import NavigationBar from '../NavigationBar/NavigationBar'; -import { - getActiveTintColor, - getInactiveTintColor, - getLabelColor, -} from '../NavigationBar/utils'; - -const styles = StyleSheet.create({ - backgroundColor: { - backgroundColor: Palette.error60, - }, -}); - -const icons = ['magnify', 'camera', 'inbox', 'heart', 'shopping-music']; - -const createState = (index: number, length: number) => ({ - index, - routes: Array.from({ length }, (_, i) => ({ - key: `key-${i}`, - focusedIcon: icons[i], - unfocusedIcon: undefined, - title: `Route: ${i}`, - })), -}); - -const renderScene = ({ route }: { route: { title: string } }) => ( - {route.title} -); - -const getTab = (index: number) => - screen.getAllByRole(Platform.OS === 'ios' ? 'button' : 'tab')[index]; - -const layoutNavigationBar = async () => { - await fireEvent(screen.getByTestId('bottom-navigation-bar'), 'layout', { - nativeEvent: { - layout: { height: 56, width: 360 }, - }, - }); -}; - -it('renders bottom navigation', async () => { - const tree = ( - await render( - - ) - ).toJSON(); - - expect(tree).toMatchSnapshot(); -}); - -it('renders bottom navigation with scene animation', async () => { - const tree = ( - await render( - - ) - ).toJSON(); - - expect(tree).toMatchSnapshot(); -}); - -// eslint-disable-next-line jest/no-disabled-tests -it.skip('sceneAnimationEnabled matches animation requirements', async () => { - const ease = Easing.ease; - - await render( - - ); - - // Simulate the button press - await userEvent.press(screen.getAllByRole('button')[1]); - - // Expect the calls to Animated.parallel - expect(Animated.parallel).toHaveBeenCalledTimes(2); - - // Expect the first call to Animated.parallel - expect(Animated.parallel).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - // ripple - config: expect.objectContaining({ toValue: 1, duration: 400 }), - }), - ]) - ); - - // Expect the second call to Animated.parallel - expect(Animated.parallel).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - // previous position anims, shifting to the left - config: expect.objectContaining({ - toValue: -1, - duration: 150, - easing: ease, - }), - }), - expect.objectContaining({ - // active page visibility - config: expect.objectContaining({ - toValue: 1, - duration: 150, - easing: ease, - }), - }), - expect.objectContaining({ - // next position anims, shifting to the right - config: expect.objectContaining({ - toValue: 1, - duration: 150, - easing: ease, - }), - }), - ]) - ); -}); - -it('calls onIndexChange', async () => { - const onIndexChange = jest.fn(); - await render( - - ); - await layoutNavigationBar(); - // pressing same index as active navigation state does not call onIndexChange - await userEvent.press(getTab(0)); - expect(onIndexChange).not.toHaveBeenCalled(); - - await userEvent.press(getTab(1)); - expect(onIndexChange).toHaveBeenCalledTimes(1); -}); - -it('calls onTabPress', async () => { - const onTabPress = jest.fn(); - const onIndexChange = jest.fn(); - - await render( - - ); - await layoutNavigationBar(); - await userEvent.press(getTab(1)); - expect(onTabPress).toHaveBeenCalled(); - expect(onTabPress).toHaveBeenCalledTimes(1); - expect(onTabPress).toHaveBeenLastCalledWith( - expect.objectContaining({ - route: expect.objectContaining({ - key: 'key-1', - }), - defaultPrevented: expect.any(Boolean), - preventDefault: expect.any(Function), - }) - ); -}); - -it('calls onTabLongPress', async () => { - const onTabLongPress = jest.fn(); - const onIndexChange = jest.fn(); - - await render( - - ); - await layoutNavigationBar(); - await userEvent.longPress(getTab(2)); - expect(onTabLongPress).toHaveBeenCalled(); - expect(onTabLongPress).toHaveBeenCalledTimes(1); - expect(onTabLongPress).toHaveBeenLastCalledWith( - expect.objectContaining({ - route: expect.objectContaining({ - key: 'key-2', - }), - defaultPrevented: expect.any(Boolean), - preventDefault: expect.any(Function), - }) - ); -}); - -it('renders bottom navigation with three tabs', async () => { - const tree = ( - await render( - - ) - ).toJSON(); - - expect(tree).toMatchSnapshot(); -}); - -it('renders custom icon and label', async () => { - const tree = ( - await render( - ( - - )} - renderLabel={({ route, color }) => ( - - {route.title} - - )} - /> - ) - ).toJSON(); - - expect(tree).toMatchSnapshot(); -}); - -it('renders with custom active and inactive colors', async () => { - const tree = ( - await render( - - ) - ).toJSON(); - - expect(tree).toMatchSnapshot(); -}); - -it('hides labels when labeled is false', async () => { - const tree = ( - await render( - - ) - ).toJSON(); - - expect(tree).toMatchSnapshot(); -}); - -it('should have appropriate display style according to the visibility on web', async () => { - const originalPlatform = Platform.OS; - Platform.OS = 'web'; - - const { rerender } = await render( - - ); - - const wrapper = screen.getByTestId('RouteScreen: 0'); - - expect(wrapper).toHaveStyle({ display: 'flex' }); - - await rerender(); - expect(wrapper).toHaveStyle({ display: 'none' }); - - Platform.OS = originalPlatform; -}); - -it('should have labelMaxFontSizeMultiplier passed to label', async () => { - const labelMaxFontSizeMultiplier = 2; - await render( - - ); - - const label = screen.getAllByText('Route: 0').find( - // eslint-disable-next-line no-restricted-syntax -- TODO: replace TestInstance props access with a user-visible assertion. - (item) => item.props.maxFontSizeMultiplier === labelMaxFontSizeMultiplier - ); - - expect(label).toBeTruthy(); -}); - -it('renders custom background color passed to barStyle property', async () => { - await render( - - ); - - const wrapper = screen.getByTestId('bottom-navigation-bar-content'); - expect(wrapper).toHaveStyle({ backgroundColor: Palette.error60 }); -}); - -it('renders a single tab', async () => { - await render( - - ); - - expect(screen.getByTestId('bottom-navigation')).toBeOnTheScreen(); -}); - -it('renders bottom navigation with getLazy', async () => { - const view = await render( - route.key === 'key-2'} - /> - ); - - expect(view).toMatchSnapshot(); - - expect(screen.queryByTestId('RouteScreen: 2')).not.toBeOnTheScreen(); -}); - -it('applies maxTabBarWidth styling if compact prop is truthy', async () => { - await render( - route.key === 'key-2'} - testID="bottom-navigation" - compact - /> - ); - - expect( - screen.getByTestId('bottom-navigation-bar-content-wrapper') - ).toHaveStyle({ - maxWidth: 480, - }); -}); - -it('does not apply maxTabBarWidth styling if compact prop is falsy', async () => { - await render( - route.key === 'key-2'} - testID="bottom-navigation" - compact={false} - /> - ); - - expect( - screen.getByTestId('bottom-navigation-bar-content-wrapper') - ).not.toHaveStyle({ - maxWidth: 480, - }); -}); - -it('renders bar content', async () => { - await render( - route.key === 'key-2'} - testID="bottom-navigation" - /> - ); - - expect(screen.getByTestId('bottom-navigation-bar-content')).toBeOnTheScreen(); -}); - -it('does not render the legacy ripple overlay', async () => { - await render( - route.key === 'key-2'} - testID="bottom-navigation" - /> - ); - - expect( - screen.queryByTestId('bottom-navigation-bar-content-ripple') - ).not.toBeOnTheScreen(); -}); - -it('renders tab labels when labeled', async () => { - await render( - - ); - - // Each tab renders a single label (no cross-fade layers). - expect(screen.getAllByText('Alpha').length).toBeGreaterThan(0); - expect(screen.getAllByText('Beta').length).toBeGreaterThan(0); -}); - -it('renders the horizontal (flexible) variant', async () => { - const tree = ( - await render( - - ) - ).toJSON(); - - expect(tree).toMatchSnapshot(); -}); - -it('falls back to icon-only when horizontal is combined with labeled=false', async () => { - await render( - - ); - - // `horizontal` is a no-op without labels, so no label text is rendered. - expect(screen.queryByText('Route: 0')).toBeNull(); - expect(screen.queryByText('Route: 1')).toBeNull(); -}); - -it('renders MD3 state layers on hover, focus and press', async () => { - const navigationState = { - index: 0, - routes: [ - { key: 'a', title: 'Route: 0', focusedIcon: 'magnify', testID: 'tab-a' }, - { key: 'b', title: 'Route: 1', focusedIcon: 'camera', testID: 'tab-b' }, - ], - }; - - await render( - - ); - - const stateLayer = () => screen.getByTestId('tab-b-state-layer'); - - // Idle: no visible state layer. - expect(stateLayer()).toHaveStyle({ opacity: undefined }); - - // Hovered: 8% state layer. - await fireEvent(screen.getByTestId('tab-b'), 'hoverIn'); - expect(stateLayer()).toHaveStyle({ opacity: 0.08 }); - await fireEvent(screen.getByTestId('tab-b'), 'hoverOut'); - expect(stateLayer()).toHaveStyle({ opacity: undefined }); - - // Focused: 10% state layer. - await fireEvent(screen.getByTestId('tab-b'), 'focus'); - expect(stateLayer()).toHaveStyle({ opacity: 0.1 }); - await fireEvent(screen.getByTestId('tab-b'), 'blur'); - - // Pressed: 10% state layer. - await fireEvent(screen.getByTestId('tab-b'), 'pressIn'); - expect(stateLayer()).toHaveStyle({ opacity: 0.1 }); - await fireEvent(screen.getByTestId('tab-b'), 'pressOut'); - expect(stateLayer()).toHaveStyle({ opacity: undefined }); -}); - -it('colors the focused tab label with secondary and others with onSurfaceVariant', async () => { - const navigationState = { - index: 0, - routes: [ - { key: 'a', title: 'Alpha', focusedIcon: 'magnify' }, - { key: 'b', title: 'Beta', focusedIcon: 'camera' }, - ], - }; - - await render( - - ); - - expect(screen.getAllByText('Alpha').at(-1)).toHaveStyle({ - color: getTheme().colors.secondary, - }); - expect(screen.getAllByText('Beta').at(-1)).toHaveStyle({ - color: getTheme().colors.onSurfaceVariant, - }); -}); - -it('renders the active indicator with the secondaryContainer color', async () => { - const navigationState = { - index: 0, - routes: [ - { key: 'a', title: 'Alpha', focusedIcon: 'magnify', testID: 'tab-a' }, - { key: 'b', title: 'Beta', focusedIcon: 'camera', testID: 'tab-b' }, - ], - }; - - await render( - - ); - - expect(screen.getByTestId('tab-a-active-indicator')).toHaveStyle({ - backgroundColor: getTheme().colors.secondaryContainer, - }); -}); - -it('renders a badge for routes that define one', async () => { - const navigationState = { - index: 0, - routes: [ - { key: 'a', title: 'Alpha', focusedIcon: 'magnify', badge: 3 }, - { key: 'b', title: 'Beta', focusedIcon: 'camera' }, - ], - }; - - await render( - - ); - - expect(screen.getByText('3')).toBeTruthy(); -}); - -describe('getActiveTintColor', () => { - it.each` - activeColor | expected - ${'#FBF7DB'} | ${'#FBF7DB'} - ${undefined} | ${Palette.secondary10} - `( - 'returns $expected when activeColor: $activeColor', - ({ activeColor, expected }) => { - const theme = getTheme(false); - const result = getActiveTintColor({ activeColor, theme }); - expect(result).toBe(expected); - } - ); -}); - -describe('getInactiveTintColor', () => { - it.each` - inactiveColor | expected - ${'#853D4B'} | ${'#853D4B'} - ${undefined} | ${Palette.neutralVariant30} - `( - 'returns $expected when inactiveColor: $inactiveColor', - ({ inactiveColor, expected }) => { - const theme = getTheme(false); - const result = getInactiveTintColor({ - inactiveColor, - theme, - }); - expect(result).toBe(expected); - } - ); -}); - -describe('getLabelColor', () => { - it.each([ - { tintColor: '#FBF7DB', focused: true, expected: '#FBF7DB' }, - { tintColor: '#853D4B', focused: true, expected: '#853D4B' }, - { tintColor: undefined, focused: true, expected: Palette.secondary40 }, - { - tintColor: undefined, - focused: false, - expected: Palette.neutralVariant30, - }, - ])( - 'returns $expected when tintColor: $tintColor, focused: $focused', - ({ tintColor, focused, expected }) => { - const theme = getTheme(false); - const result = getLabelColor({ - tintColor: tintColor ?? '', - hasColor: Boolean(tintColor), - focused, - theme, - }); - expect(result).toBe(expected); - } - ); -}); - -it('barStyle animated value changes correctly', async () => { - const value = new Animated.Value(1); - await render( - {}} - renderScene={renderScene} - testID={'bottom-navigation'} - barStyle={[{ transform: [{ scale: value }] }]} - /> - ); - expect(screen.getByTestId('bottom-navigation-bar-outer-layer')).toHaveStyle({ - transform: [{ scale: 1 }], - }); - - Animated.timing(value, { - toValue: 1.5, - useNativeDriver: false, - duration: 200, - }).start(); - - await act(() => { - jest.advanceTimersByTime(200); - }); - expect(screen.getByTestId('bottom-navigation-bar-outer-layer')).toHaveStyle({ - transform: [{ scale: 1.5 }], - }); -}); - -it("allows customizing Route's type via generics", async () => { - type CustomRoute = { - key: string; - customPropertyName: string; - }; - - type CustomState = { - index: number; - routes: CustomRoute[]; - }; - - const state: CustomState = { - index: 0, - routes: [ - { key: 'a', customPropertyName: 'First' }, - { key: 'b', customPropertyName: 'Second' }, - ], - }; - - const tree = ( - await render( - route.customPropertyName} - renderScene={({ route }) => {route.customPropertyName}} - /> - ) - ).toJSON(); - - expect(tree).toMatchSnapshot(); -}); diff --git a/src/components/__tests__/NavigationBar.test.tsx b/src/components/__tests__/NavigationBar.test.tsx new file mode 100644 index 0000000000..18a4431fd5 --- /dev/null +++ b/src/components/__tests__/NavigationBar.test.tsx @@ -0,0 +1,221 @@ +import { describe, expect, it, jest } from '@jest/globals'; +import { fireEvent } from '@testing-library/react-native'; + +import { getTheme } from '../../core/theming'; +import { render, screen } from '../../test-utils'; +import { Palette } from '../../theme/tokens'; +import NavigationBar from '../NavigationBar/NavigationBar'; +import { + getActiveTintColor, + getInactiveTintColor, + getLabelColor, +} from '../NavigationBar/utils'; + +const icons = ['magnify', 'camera', 'inbox', 'heart', 'shopping-music']; + +const createState = (index: number, length: number) => ({ + index, + routes: Array.from({ length }, (_, i) => ({ + key: `key-${i}`, + focusedIcon: icons[i], + unfocusedIcon: undefined, + title: `Route: ${i}`, + })), +}); + +it('renders tab labels when labeled', async () => { + await render( + + ); + + // Each tab renders a single label (no cross-fade layers). + expect(screen.getAllByText('Alpha').length).toBeGreaterThan(0); + expect(screen.getAllByText('Beta').length).toBeGreaterThan(0); +}); + +it('renders the horizontal (flexible) variant', async () => { + const tree = ( + await render( + + ) + ).toJSON(); + + expect(tree).toMatchSnapshot(); +}); + +it('falls back to icon-only when horizontal is combined with labeled=false', async () => { + await render( + + ); + + // `horizontal` is a no-op without labels, so no label text is rendered. + expect(screen.queryByText('Route: 0')).toBeNull(); + expect(screen.queryByText('Route: 1')).toBeNull(); +}); + +it('renders MD3 state layers on hover, focus and press', async () => { + const navigationState = { + index: 0, + routes: [ + { key: 'a', title: 'Route: 0', focusedIcon: 'magnify', testID: 'tab-a' }, + { key: 'b', title: 'Route: 1', focusedIcon: 'camera', testID: 'tab-b' }, + ], + }; + + await render( + + ); + + const stateLayer = () => screen.getByTestId('tab-b-state-layer'); + + // Idle: no visible state layer. + expect(stateLayer()).toHaveStyle({ opacity: undefined }); + + // Hovered: 8% state layer. + await fireEvent(screen.getByTestId('tab-b'), 'hoverIn'); + expect(stateLayer()).toHaveStyle({ opacity: 0.08 }); + await fireEvent(screen.getByTestId('tab-b'), 'hoverOut'); + expect(stateLayer()).toHaveStyle({ opacity: undefined }); + + // Focused: 10% state layer. + await fireEvent(screen.getByTestId('tab-b'), 'focus'); + expect(stateLayer()).toHaveStyle({ opacity: 0.1 }); + await fireEvent(screen.getByTestId('tab-b'), 'blur'); + + // Pressed: 10% state layer. + await fireEvent(screen.getByTestId('tab-b'), 'pressIn'); + expect(stateLayer()).toHaveStyle({ opacity: 0.1 }); + await fireEvent(screen.getByTestId('tab-b'), 'pressOut'); + expect(stateLayer()).toHaveStyle({ opacity: undefined }); +}); + +it('colors the focused tab label with secondary and others with onSurfaceVariant', async () => { + const navigationState = { + index: 0, + routes: [ + { key: 'a', title: 'Alpha', focusedIcon: 'magnify' }, + { key: 'b', title: 'Beta', focusedIcon: 'camera' }, + ], + }; + + await render( + + ); + + expect(screen.getAllByText('Alpha').at(-1)).toHaveStyle({ + color: getTheme().colors.secondary, + }); + expect(screen.getAllByText('Beta').at(-1)).toHaveStyle({ + color: getTheme().colors.onSurfaceVariant, + }); +}); + +it('renders the active indicator with the secondaryContainer color', async () => { + const navigationState = { + index: 0, + routes: [ + { key: 'a', title: 'Alpha', focusedIcon: 'magnify', testID: 'tab-a' }, + { key: 'b', title: 'Beta', focusedIcon: 'camera', testID: 'tab-b' }, + ], + }; + + await render( + + ); + + expect(screen.getByTestId('tab-a-active-indicator')).toHaveStyle({ + backgroundColor: getTheme().colors.secondaryContainer, + }); +}); + +it('renders a badge for routes that define one', async () => { + const navigationState = { + index: 0, + routes: [ + { key: 'a', title: 'Alpha', focusedIcon: 'magnify', badge: 3 }, + { key: 'b', title: 'Beta', focusedIcon: 'camera' }, + ], + }; + + await render( + + ); + + expect(screen.getByText('3')).toBeTruthy(); +}); + +describe('getActiveTintColor', () => { + it.each` + activeColor | expected + ${'#FBF7DB'} | ${'#FBF7DB'} + ${undefined} | ${Palette.secondary10} + `( + 'returns $expected when activeColor: $activeColor', + ({ activeColor, expected }) => { + const theme = getTheme(false); + const result = getActiveTintColor({ activeColor, theme }); + expect(result).toBe(expected); + } + ); +}); + +describe('getInactiveTintColor', () => { + it.each` + inactiveColor | expected + ${'#853D4B'} | ${'#853D4B'} + ${undefined} | ${Palette.neutralVariant30} + `( + 'returns $expected when inactiveColor: $inactiveColor', + ({ inactiveColor, expected }) => { + const theme = getTheme(false); + const result = getInactiveTintColor({ + inactiveColor, + theme, + }); + expect(result).toBe(expected); + } + ); +}); + +describe('getLabelColor', () => { + it.each([ + { tintColor: '#FBF7DB', focused: true, expected: '#FBF7DB' }, + { tintColor: '#853D4B', focused: true, expected: '#853D4B' }, + { tintColor: undefined, focused: true, expected: Palette.secondary40 }, + { + tintColor: undefined, + focused: false, + expected: Palette.neutralVariant30, + }, + ])( + 'returns $expected when tintColor: $tintColor, focused: $focused', + ({ tintColor, focused, expected }) => { + const theme = getTheme(false); + const result = getLabelColor({ + tintColor: tintColor ?? '', + hasColor: Boolean(tintColor), + focused, + theme, + }); + expect(result).toBe(expected); + } + ); +}); diff --git a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap deleted file mode 100644 index 8aa6c7a010..0000000000 --- a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap +++ /dev/null @@ -1,9354 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`allows customizing Route's type via generics 1`] = ` - - - - - - First - - - - - - - - - - - - - - - - - - - - - - - - First - - - - - - - - - - - - - - - - - - - Second - - - - - - - - - - -`; - -exports[`hides labels when labeled is false 1`] = ` - - - - - - Route: 0 - - - - - - - - - - - - - - - - - - - magnify - - - - - - - - - - - - - - - - - - camera - - - - - - - - - - - - - - - - - - inbox - - - - - - - - - - - - - - -`; - -exports[`renders bottom navigation 1`] = ` - - - - - - Route: 0 - - - - - - - - - - - - - - - - - - - magnify - - - - - - - - - Route: 0 - - - - - - - - - - - - - - camera - - - - - - - - - Route: 1 - - - - - - - - - - - - - - inbox - - - - - - - - - Route: 2 - - - - - - - - - - - - - - heart - - - - - - - - - Route: 3 - - - - - - - - - - - - - - shopping-music - - - - - - - - - Route: 4 - - - - - - - - - - -`; - -exports[`renders bottom navigation with getLazy 1`] = ` - - - - - - Route: 0 - - - - - - - Route: 1 - - - - - - - Route: 3 - - - - - - - Route: 4 - - - - - - - - - - - - - - - - - - - magnify - - - - - - - - - Route: 0 - - - - - - - - - - - - - - camera - - - - - - - - - Route: 1 - - - - - - - - - - - - - - inbox - - - - - - - - - Route: 2 - - - - - - - - - - - - - - heart - - - - - - - - - Route: 3 - - - - - - - - - - - - - - shopping-music - - - - - - - - - Route: 4 - - - - - - - - - - -`; - -exports[`renders bottom navigation with scene animation 1`] = ` - - - - - - Route: 0 - - - - - - - - - - - - - - - - - - - magnify - - - - - - - - - Route: 0 - - - - - - - - - - - - - - camera - - - - - - - - - Route: 1 - - - - - - - - - - - - - - inbox - - - - - - - - - Route: 2 - - - - - - - - - - - - - - heart - - - - - - - - - Route: 3 - - - - - - - - - - - - - - shopping-music - - - - - - - - - Route: 4 - - - - - - - - - - -`; - -exports[`renders bottom navigation with three tabs 1`] = ` - - - - - - Route: 0 - - - - - - - - - - - - - - - - - - - magnify - - - - - - - - - Route: 0 - - - - - - - - - - - - - - camera - - - - - - - - - Route: 1 - - - - - - - - - - - - - - inbox - - - - - - - - - Route: 2 - - - - - - - - - - -`; - -exports[`renders custom icon and label 1`] = ` - - - - - - Route: 0 - - - - - - - - - - - - - - - - - - - - - - - - Route: 0 - - - - - - - - - - - - - - - - - - - Route: 1 - - - - - - - - - - - - - - - - - - - Route: 2 - - - - - - - - - - - - - - - - - - - Route: 3 - - - - - - - - - - - - - - - - - - - Route: 4 - - - - - - - - - - -`; - -exports[`renders the horizontal (flexible) variant 1`] = ` - - - - - - - - - - - - - magnify - - - - - - - Route: 0 - - - - - - - - - - - - camera - - - - - - - Route: 1 - - - - - - - - - - - - inbox - - - - - - - Route: 2 - - - - - - - - - -`; - -exports[`renders with custom active and inactive colors 1`] = ` - - - - - - Route: 0 - - - - - - - - - - - - - - - - - - - magnify - - - - - - - - - Route: 0 - - - - - - - - - - - - - - camera - - - - - - - - - Route: 1 - - - - - - - - - - - - - - inbox - - - - - - - - - Route: 2 - - - - - - - - - - -`; diff --git a/src/components/__tests__/__snapshots__/NavigationBar.test.tsx.snap b/src/components/__tests__/__snapshots__/NavigationBar.test.tsx.snap new file mode 100644 index 0000000000..34b6835173 --- /dev/null +++ b/src/components/__tests__/__snapshots__/NavigationBar.test.tsx.snap @@ -0,0 +1,750 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders the horizontal (flexible) variant 1`] = ` + + + + + + + + + + + + + magnify + + + + + + + Route: 0 + + + + + + + + + + + + camera + + + + + + + Route: 1 + + + + + + + + + + + + inbox + + + + + + + Route: 2 + + + + + + + + + +`; diff --git a/src/index.tsx b/src/index.tsx index 3bc5390de0..3b580962c3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -24,7 +24,6 @@ export { Avatar, List, Drawer }; export { default as Badge } from './components/Badge'; export { default as ActivityIndicator } from './components/ActivityIndicator'; export { default as Banner } from './components/Banner'; -export { default as BottomNavigation } from './components/BottomNavigation/BottomNavigation'; export { default as Button } from './components/Button/Button'; export { default as Card } from './components/Card/Card'; export { default as Checkbox } from './components/Checkbox'; @@ -66,10 +65,6 @@ export type { Props as AvatarImageProps } from './components/Avatar/AvatarImage' export type { Props as AvatarTextProps } from './components/Avatar/AvatarText'; export type { Props as BadgeProps } from './components/Badge'; export type { Props as BannerProps } from './components/Banner'; -export type { - Props as BottomNavigationProps, - BaseRoute as BottomNavigationRoute, -} from './components/BottomNavigation/BottomNavigation'; export type { Props as ButtonProps } from './components/Button/Button'; export type { Props as CardProps } from './components/Card/Card'; export type { Props as CardActionsProps } from './components/Card/CardActions';