From 43488d327412f8bd7b10d5ecb9526751131af6fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Wed, 19 Aug 2026 14:52:09 +0200 Subject: [PATCH 01/34] feat(appbar): reintroduction of subtitle --- example/src/Examples/AppbarExample.tsx | 6 +- src/components/Appbar/Appbar.tsx | 2 +- src/components/Appbar/AppbarContent.tsx | 80 ++++++++++++++----- src/components/Appbar/utils.ts | 15 +++- .../__tests__/Appbar/Appbar.test.tsx | 31 +++++++ .../Appbar/__snapshots__/Appbar.test.tsx.snap | 8 +- 6 files changed, 113 insertions(+), 29 deletions(-) diff --git a/example/src/Examples/AppbarExample.tsx b/example/src/Examples/AppbarExample.tsx index 2d4eb0bbc4..10f4b0bb92 100644 --- a/example/src/Examples/AppbarExample.tsx +++ b/example/src/Examples/AppbarExample.tsx @@ -52,7 +52,11 @@ const AppbarExample = () => { {showLeftIcon && ( navigation.goBack()} /> )} - setShowSnackbar(true)} /> + setShowSnackbar(true)} + /> {isCenterAlignedMode ? false : showCalendarIcon && ( diff --git a/src/components/Appbar/Appbar.tsx b/src/components/Appbar/Appbar.tsx index 8ef9965819..c42304765b 100644 --- a/src/components/Appbar/Appbar.tsx +++ b/src/components/Appbar/Appbar.tsx @@ -220,7 +220,7 @@ const Appbar = ({ { backgroundColor }, styles.appbar, { - height: modeAppbarHeight[mode], + minHeight: modeAppbarHeight[mode], }, insets, restStyle, diff --git a/src/components/Appbar/AppbarContent.tsx b/src/components/Appbar/AppbarContent.tsx index 5270aa271d..826477619d 100644 --- a/src/components/Appbar/AppbarContent.tsx +++ b/src/components/Appbar/AppbarContent.tsx @@ -8,8 +8,13 @@ import type { ViewProps, } from 'react-native'; -import { modeTextVariant } from './utils'; +import { + modeTextVariant, + modeSubtitleTextVariant, + modeSubtitleTextSpacing, +} from './utils'; import { useInternalTheme } from '../../core/theming'; +import type { TypescaleKey } from '../../theme/types'; import type { $RemoveChildren, ThemeProp } from '../../types'; import Text from '../Typography/Text'; import type { TextRef } from '../Typography/Text'; @@ -36,6 +41,14 @@ export type Props = $RemoveChildren & { * Reference for the title. */ titleRef?: React.RefObject; + /** + * Text or component for the subtitle. + */ + subtitle?: React.ReactNode; + /** + * Style for the subtitle. + */ + subtitleStyle?: StyleProp; /** * Function to execute on press. */ @@ -68,7 +81,7 @@ export type Props = $RemoveChildren & { } & (TitleString | TitleElement); /** - * A component used to display a title in an appbar. + * A component used to display a title and optional subtitle in an appbar. * * ## Usage * ```js @@ -77,7 +90,7 @@ export type Props = $RemoveChildren & { * * const MyComponent = () => ( * - * + * * * ); * @@ -86,6 +99,8 @@ export type Props = $RemoveChildren & { */ const AppbarContent = ({ color: titleColor, + subtitle, + subtitleStyle, onPress, disabled, style, @@ -110,7 +125,9 @@ const AppbarContent = ({ 'center-aligned': styles.v3DefaultContainer, }; - const variant = modeTextVariant[mode]; + const variant = modeTextVariant[mode] as TypescaleKey; + const subtitleVariant = modeSubtitleTextVariant[mode] as TypescaleKey; + const subtitleTopPadding = modeSubtitleTextSpacing[mode]; const contentWrapperProps = { pointerEvents: 'box-none', @@ -122,24 +139,43 @@ const AppbarContent = ({ const content = ( <> {typeof title === 'string' ? ( - - {title} - + <> + + {title} + + {subtitle ? ( + + {subtitle} + + ) : null} + ) : ( title )} diff --git a/src/components/Appbar/utils.ts b/src/components/Appbar/utils.ts index a6ea546b34..85c5eecc7a 100644 --- a/src/components/Appbar/utils.ts +++ b/src/components/Appbar/utils.ts @@ -86,7 +86,6 @@ type RenderAppbarContentProps = BaseProps & { theme?: ThemeProp; }; -export const DEFAULT_APPBAR_HEIGHT = 56; const MD3_DEFAULT_APPBAR_HEIGHT = 64; export const modeAppbarHeight = { @@ -103,6 +102,20 @@ export const modeTextVariant = { 'center-aligned': 'titleLarge', } as const; +export const modeSubtitleTextVariant = { + small: 'labelSmall', + medium: 'labelMedium', + large: 'labelLarge', + 'center-aligned': 'labelSmall', +} as const; + +export const modeSubtitleTextSpacing = { + small: undefined, + medium: 4, + large: 8, + 'center-aligned': undefined, +} as const satisfies Record; + export const filterAppbarActions = ( children: React.ReactNode, isLeading = false diff --git a/src/components/__tests__/Appbar/Appbar.test.tsx b/src/components/__tests__/Appbar/Appbar.test.tsx index 27cd573f9d..e18fc08671 100644 --- a/src/components/__tests__/Appbar/Appbar.test.tsx +++ b/src/components/__tests__/Appbar/Appbar.test.tsx @@ -277,6 +277,37 @@ describe('AppbarContent', () => { expect(screen.getByText('Title')).toBeOnTheScreen(); }); + + it('should render subtitle with MD3 typography and color tokens', async () => { + await render( + + + + ); + + expect(screen.getByTestId('appbar-content-subtitle-text')).toHaveStyle({ + ...getTheme().fonts.labelSmall, + color: getTheme().colors.onSurfaceVariant, + }); + }); + + it('should apply a custom subtitle style', async () => { + const customSubtitleColor = getTheme().colors.error; + + await render( + + + + ); + + expect(screen.getByTestId('appbar-content-subtitle-text')).toHaveStyle({ + color: customSubtitleColor, + }); + }); }); describe('getAppbarColors', () => { diff --git a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap index a5d9d95766..59c778e4be 100644 --- a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap +++ b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap @@ -6,7 +6,6 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` style={ { "backgroundColor": "rgba(254, 247, 255, 1)", - "height": 64, "shadowColor": "rgba(0, 0, 0, 1)", "shadowOffset": { "height": 0, @@ -24,8 +23,9 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` { "alignItems": "center", "backgroundColor": "rgba(254, 247, 255, 1)", - "flex": 1, + "flex": undefined, "flexDirection": "row", + "minHeight": 64, "paddingBottom": undefined, "paddingHorizontal": 4, "paddingLeft": undefined, @@ -422,7 +422,6 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A style={ { "backgroundColor": "rgba(254, 247, 255, 1)", - "height": 64, "shadowColor": "rgba(0, 0, 0, 1)", "shadowOffset": { "height": 0, @@ -440,8 +439,9 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A { "alignItems": "center", "backgroundColor": "rgba(254, 247, 255, 1)", - "flex": 1, + "flex": undefined, "flexDirection": "row", + "minHeight": 64, "paddingBottom": undefined, "paddingHorizontal": 4, "paddingLeft": undefined, From 1b82ff5608a2ac513698f3e62e26c7c988d8d254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Fri, 21 Aug 2026 11:10:28 +0200 Subject: [PATCH 02/34] feat(appbar): flat structure + expressive material design --- src/components/AppbarV3/Appbar.tsx | 200 ++++++++++++++++++++++ src/components/AppbarV3/AppbarAction.tsx | 70 ++++++++ src/components/AppbarV3/AppbarContent.tsx | 171 ++++++++++++++++++ src/components/AppbarV3/index.ts | 12 ++ src/components/AppbarV3/types.ts | 130 ++++++++++++++ src/components/AppbarV3/utils.ts | 29 ++++ src/index.tsx | 11 ++ 7 files changed, 623 insertions(+) create mode 100644 src/components/AppbarV3/Appbar.tsx create mode 100644 src/components/AppbarV3/AppbarAction.tsx create mode 100644 src/components/AppbarV3/AppbarContent.tsx create mode 100644 src/components/AppbarV3/index.ts create mode 100644 src/components/AppbarV3/types.ts create mode 100644 src/components/AppbarV3/utils.ts diff --git a/src/components/AppbarV3/Appbar.tsx b/src/components/AppbarV3/Appbar.tsx new file mode 100644 index 0000000000..b7d8070d8b --- /dev/null +++ b/src/components/AppbarV3/Appbar.tsx @@ -0,0 +1,200 @@ +import { StyleSheet, View } from 'react-native'; +import type { ColorValue, ViewStyle } from 'react-native'; + +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import AppbarAction from './AppbarAction'; +import AppbarContent from './AppbarContent'; +import type { AppbarAction as AppbarActionConfig, Props } from './types'; +import { APPBAR_ACTION_SIZE, getActionsWidth, getAppbarHeight } from './utils'; +import { useInternalTheme } from '../../core/theming'; +import type { Theme } from '../../types'; +import { getAppbarBorders } from '../Appbar/utils'; +import Surface from '../Surface'; + +const Appbar = ({ + actions = [], + contentStyle, + isScrolled = false, + leadingAction, + onTitlePress, + safeAreaInsets, + statusBarHeight, + style, + subtitle, + subtitleColor, + subtitleMaxFontSizeMultiplier, + subtitleStyle, + testID = 'appbar', + theme: themeOverrides, + title, + titleAlignment = 'leading', + titleColor, + titleDisabled, + titleImage, + titleMaxFontSizeMultiplier, + titleRef, + titleStyle, + variant, + ref, + ...rest +}: Props) => { + const theme = useInternalTheme(themeOverrides) as Theme; + const detectedInsets = useSafeAreaInsets(); + const flattenedStyle = StyleSheet.flatten(style); + const { backgroundColor: customBackground, ...restStyle } = (flattenedStyle || + {}) as Exclude & { + backgroundColor?: ColorValue; + }; + const backgroundColor = + customBackground ?? + (isScrolled ? theme.colors.surfaceContainer : theme.colors.surface); + const resolvedTitleColor = titleColor ?? theme.colors.onSurface; + const resolvedSubtitleColor = subtitleColor ?? theme.colors.onSurfaceVariant; + const hasSubtitle = typeof subtitle === 'string' && subtitle.length > 0; + const minHeight = getAppbarHeight(variant, hasSubtitle); + const topInset = statusBarHeight ?? safeAreaInsets?.top ?? detectedInsets.top; + const leftInset = safeAreaInsets?.left ?? detectedInsets.left; + const rightInset = safeAreaInsets?.right ?? detectedInsets.right; + const horizontalInset = Math.max(leftInset, rightInset); + const borderRadius = getAppbarBorders(restStyle as ViewStyle); + const centered = titleAlignment === 'center'; + + const renderLeadingAction = () => + leadingAction ? ( + + ) : null; + + const renderActions = () => + actions.map((action, index) => ( + + )); + + const contentProps = { + alignment: titleAlignment, + contentStyle, + onTitlePress, + subtitle, + subtitleColor: resolvedSubtitleColor, + subtitleMaxFontSizeMultiplier, + subtitleStyle, + testID: `${testID}-content`, + theme, + title, + titleColor: resolvedTitleColor, + titleDisabled, + titleImage, + titleMaxFontSizeMultiplier, + titleRef, + titleStyle, + variant, + }; + + const renderSmallAppbar = () => { + if (centered) { + const sideWidth = Math.max( + leadingAction ? APPBAR_ACTION_SIZE : 0, + getActionsWidth(actions as readonly AppbarActionConfig[]) + ); + + return ( + + + {renderLeadingAction()} + + + + {renderActions()} + + + ); + } + + return ( + + {renderLeadingAction()} + + {renderActions()} + + ); + }; + + const renderFlexibleAppbar = () => ( + + + {renderLeadingAction()} + {renderActions()} + + + + ); + + return ( + + + {variant === 'small' ? renderSmallAppbar() : renderFlexibleAppbar()} + + + ); +}; + +const styles = StyleSheet.create({ + appbar: { + paddingHorizontal: 4, + }, + smallRow: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + }, + flexibleContainer: { + flex: 1, + }, + controlsRow: { + minHeight: 56, + flexDirection: 'row', + alignItems: 'flex-start', + justifyContent: 'space-between', + }, + trailingActions: { + flexDirection: 'row', + justifyContent: 'flex-end', + }, + side: { + flexDirection: 'row', + }, + titleLeading: { + marginStart: 12, + }, + titleWithLeading: { + marginStart: 4, + }, +}); + +export default Appbar; + +// @component-docs ignore-next-line +export { Appbar }; diff --git a/src/components/AppbarV3/AppbarAction.tsx b/src/components/AppbarV3/AppbarAction.tsx new file mode 100644 index 0000000000..b9eb1a2cca --- /dev/null +++ b/src/components/AppbarV3/AppbarAction.tsx @@ -0,0 +1,70 @@ +import { StyleSheet } from 'react-native'; + +import type { + AppbarAction as AppbarActionConfig, + AppbarLeadingAction, +} from './types'; +import type { Theme, ThemeProp } from '../../types'; +import AppbarBackIcon from '../Appbar/AppbarBackIcon'; +import IconButton from '../IconButton/IconButton'; + +type Props = { + action: AppbarActionConfig | AppbarLeadingAction; + leading?: boolean; + theme: ThemeProp; +}; + +const AppbarAction = ({ action, leading = false, theme }: Props) => { + const { + color, + key: _key, + style, + variant = 'standard', + width, + ...rest + } = action; + const { type: _type, ...buttonProps } = rest as typeof rest & { + type?: 'back' | 'icon'; + }; + const isBackAction = 'type' in action && action.type === 'back'; + const resolvedTheme = theme as Theme; + const mode = + variant === 'filled' + ? 'contained' + : variant === 'tonal' + ? 'contained-tonal' + : undefined; + + return ( + + ); +}; + +const styles = StyleSheet.create({ + action: { + margin: 4, + }, + wideAction: { + width: 56, + }, +}); + +export default AppbarAction; diff --git a/src/components/AppbarV3/AppbarContent.tsx b/src/components/AppbarV3/AppbarContent.tsx new file mode 100644 index 0000000000..67f98583b0 --- /dev/null +++ b/src/components/AppbarV3/AppbarContent.tsx @@ -0,0 +1,171 @@ +import { Pressable, StyleSheet, View } from 'react-native'; +import type { ColorValue, StyleProp, ViewStyle } from 'react-native'; + +import type { + AppbarTitleAlignment, + AppbarVariant, + Props as AppbarProps, +} from './types'; +import type { Theme, TypescaleKey } from '../../types'; +import Text from '../Typography/Text'; + +type Props = Pick< + AppbarProps, + | 'contentStyle' + | 'onTitlePress' + | 'subtitle' + | 'subtitleMaxFontSizeMultiplier' + | 'subtitleStyle' + | 'title' + | 'titleDisabled' + | 'titleImage' + | 'titleMaxFontSizeMultiplier' + | 'titleRef' + | 'titleStyle' +> & { + alignment: AppbarTitleAlignment; + subtitleColor: ColorValue; + theme: Theme; + titleColor: ColorValue; + variant: AppbarVariant; + style?: StyleProp; + testID: string; +}; + +const titleVariants = { + small: 'titleLarge', + 'medium-flexible': 'headlineMedium', + 'large-flexible': 'displaySmall', +} as const satisfies Record; + +const subtitleVariants = { + small: 'labelSmall', + 'medium-flexible': 'labelLarge', + 'large-flexible': 'titleMedium', +} as const satisfies Record; + +const subtitleSpacing = { + small: 0, + 'medium-flexible': 4, + 'large-flexible': 8, +} as const satisfies Record; + +const AppbarContent = ({ + alignment, + contentStyle, + onTitlePress, + subtitle, + subtitleColor, + subtitleMaxFontSizeMultiplier, + subtitleStyle, + testID, + theme, + title, + titleColor, + titleDisabled, + titleImage, + titleMaxFontSizeMultiplier, + titleRef, + titleStyle, + variant, + style, +}: Props) => { + const titleVariant = titleVariants[variant]; + const subtitleVariant = subtitleVariants[variant]; + const centered = alignment === 'center'; + + const content = titleImage ? ( + titleImage + ) : ( + <> + + {title} + + {subtitle ? ( + + {subtitle} + + ) : null} + + ); + + const wrapperProps = { + testID, + style: [ + styles.container, + variant !== 'small' && styles.flexibleContainer, + centered && styles.centeredContainer, + style, + contentStyle, + ], + }; + + if (onTitlePress) { + return ( + + {content} + + ); + } + + return {content}; +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + minWidth: 0, + }, + flexibleContainer: { + flex: 0, + paddingHorizontal: 16, + paddingTop: 8, + paddingBottom: 12, + }, + centeredContainer: { + alignItems: 'center', + }, + text: { + alignSelf: 'stretch', + }, + centeredText: { + textAlign: 'center', + }, +}); + +export default AppbarContent; diff --git a/src/components/AppbarV3/index.ts b/src/components/AppbarV3/index.ts new file mode 100644 index 0000000000..7891d3b6a0 --- /dev/null +++ b/src/components/AppbarV3/index.ts @@ -0,0 +1,12 @@ +export { default } from './Appbar'; +export { Appbar } from './Appbar'; +export type { + AppbarAction, + AppbarActions, + AppbarFilledAction, + AppbarLeadingAction, + AppbarStandardAction, + AppbarTitleAlignment, + AppbarVariant, + Props, +} from './types'; diff --git a/src/components/AppbarV3/types.ts b/src/components/AppbarV3/types.ts new file mode 100644 index 0000000000..2f6e9874e7 --- /dev/null +++ b/src/components/AppbarV3/types.ts @@ -0,0 +1,130 @@ +import * as React from 'react'; +import type { + Animated, + ColorValue, + GestureResponderEvent, + StyleProp, + TextStyle, + View, + ViewProps, + ViewStyle, +} from 'react-native'; + +import type { ThemeProp } from '../../types'; +import type { IconSource } from '../Icon'; +import type { Props as IconButtonProps } from '../IconButton/IconButton'; +import type { TextRef } from '../Typography/Text'; + +export type AppbarVariant = 'small' | 'medium-flexible' | 'large-flexible'; + +export type AppbarTitleAlignment = 'leading' | 'center'; + +type AppbarActionBase = Omit< + IconButtonProps, + 'icon' | 'iconColor' | 'mode' | 'selected' | 'size' | 'theme' | 'aria-label' +> & { + /** Stable key used when rendering an action from the actions array. */ + key?: React.Key; + /** Icon displayed by the action. */ + icon: IconSource; + /** Accessible label announced for the icon button. */ + 'aria-label': string; + /** Custom action icon color. */ + color?: ColorValue; +}; + +export type AppbarStandardAction = AppbarActionBase & { + variant?: 'standard'; + width?: never; +}; + +export type AppbarFilledAction = AppbarActionBase & { + /** Filled primary and tonal actions use the corresponding IconButton colors. */ + variant: 'filled' | 'tonal'; + /** Expressive filled actions can use the default or wide container. */ + width?: 'default' | 'wide'; +}; + +export type AppbarAction = AppbarStandardAction | AppbarFilledAction; + +/** A filled action replaces the standard action group and is valid on its own. */ +export type AppbarActions = + | readonly AppbarStandardAction[] + | readonly [AppbarFilledAction]; + +type AppbarLeadingIconAction = AppbarStandardAction & { + type?: 'icon'; +}; + +type AppbarBackAction = Omit< + AppbarStandardAction, + 'aria-label' | 'icon' | 'type' +> & { + /** Uses the platform-aware Paper back icon. */ + type: 'back'; + icon?: never; + 'aria-label'?: string; +}; + +export type AppbarLeadingAction = AppbarLeadingIconAction | AppbarBackAction; + +type AppbarTextTitle = { + /** Written headline displayed by the app bar. */ + title: string; + titleImage?: never; + /** Optional supporting text displayed below the headline. */ + subtitle?: string; + titleStyle?: StyleProp; + subtitleStyle?: StyleProp; + titleRef?: React.RefObject; + titleMaxFontSizeMultiplier?: number; + subtitleMaxFontSizeMultiplier?: number; +}; + +type AppbarImageTitle = { + title?: never; + /** Image or logo which replaces the written headline. */ + titleImage: React.ReactElement; + subtitle?: never; + titleStyle?: never; + subtitleStyle?: never; + titleRef?: never; + titleMaxFontSizeMultiplier?: never; + subtitleMaxFontSizeMultiplier?: never; +}; + +type AppbarBaseProps = Omit & { + /** Expressive app bar size variant. */ + variant: AppbarVariant; + /** Headline and subtitle alignment. */ + titleAlignment?: AppbarTitleAlignment; + /** Optional leading navigation action. */ + leadingAction?: AppbarLeadingAction; + /** Trailing icon actions. */ + actions?: AppbarActions; + /** Uses the on-scroll container color when true. */ + isScrolled?: boolean; + /** Override for the automatic top safe-area inset. */ + statusBarHeight?: number; + /** Safe-area inset overrides. Unspecified values use the detected insets. */ + safeAreaInsets?: { + top?: number; + left?: number; + right?: number; + }; + /** Called when the title area is pressed. */ + onTitlePress?: (event: GestureResponderEvent) => void; + /** Disables title-area interaction. */ + titleDisabled?: boolean; + /** Custom headline color. */ + titleColor?: ColorValue; + /** Custom subtitle color. */ + subtitleColor?: ColorValue; + /** Style applied to the title area. */ + contentStyle?: StyleProp; + style?: Animated.WithAnimatedValue>; + ref?: React.Ref; + theme?: ThemeProp; +}; + +export type Props = AppbarBaseProps & (AppbarTextTitle | AppbarImageTitle); diff --git a/src/components/AppbarV3/utils.ts b/src/components/AppbarV3/utils.ts new file mode 100644 index 0000000000..b8ac83ab64 --- /dev/null +++ b/src/components/AppbarV3/utils.ts @@ -0,0 +1,29 @@ +import type { AppbarAction, AppbarVariant } from './types'; + +export const APPBAR_ACTION_SIZE = 48; +export const APPBAR_WIDE_ACTION_SIZE = 64; + +export const getAppbarHeight = ( + variant: AppbarVariant, + hasSubtitle: boolean +) => { + if (variant === 'small') { + return 64; + } + + if (variant === 'medium-flexible') { + return hasSubtitle ? 136 : 112; + } + + return hasSubtitle ? 152 : 120; +}; + +export const getActionsWidth = (actions: readonly AppbarAction[]) => + actions.reduce( + (width, action) => + width + + (action.variant !== 'standard' && action.width === 'wide' + ? APPBAR_WIDE_ACTION_SIZE + : APPBAR_ACTION_SIZE), + 0 + ); diff --git a/src/index.tsx b/src/index.tsx index 8863e2fa20..7469301a7b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -45,6 +45,7 @@ export { default as Snackbar } from './components/Snackbar'; export { default as Surface } from './components/Surface'; export { default as Switch } from './components/Switch/Switch'; export { default as Appbar } from './components/Appbar'; +export { default as AppbarV3 } from './components/AppbarV3'; export { default as TouchableRipple } from './components/TouchableRipple/TouchableRipple'; export { default as TextInput } from './components/TextInput'; export { default as ToggleButton } from './components/ToggleButton'; @@ -60,6 +61,16 @@ export type { Props as AppbarActionProps } from './components/Appbar/AppbarActio export type { Props as AppbarBackActionProps } from './components/Appbar/AppbarBackAction'; export type { Props as AppbarContentProps } from './components/Appbar/AppbarContent'; export type { Props as AppbarHeaderProps } from './components/Appbar/AppbarHeader'; +export type { + Props as AppbarV3Props, + AppbarAction as AppbarV3Action, + AppbarActions as AppbarV3Actions, + AppbarFilledAction as AppbarV3FilledAction, + AppbarLeadingAction as AppbarV3LeadingAction, + AppbarStandardAction as AppbarV3StandardAction, + AppbarTitleAlignment as AppbarV3TitleAlignment, + AppbarVariant as AppbarV3Variant, +} from './components/AppbarV3'; export type { Props as AvatarIconProps } from './components/Avatar/AvatarIcon'; export type { Props as AvatarImageProps } from './components/Avatar/AvatarImage'; export type { Props as AvatarTextProps } from './components/Avatar/AvatarText'; From 025005d9b4aba92f72a2bac65688e264136f70a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Fri, 21 Aug 2026 11:10:57 +0200 Subject: [PATCH 03/34] feat(example): example app update --- example/src/ExampleList.tsx | 2 + example/src/Examples/AppbarV3Example.tsx | 269 +++++++++++++++++++++++ 2 files changed, 271 insertions(+) create mode 100644 example/src/Examples/AppbarV3Example.tsx diff --git a/example/src/ExampleList.tsx b/example/src/ExampleList.tsx index 8a2646d798..bd30b8feb6 100644 --- a/example/src/ExampleList.tsx +++ b/example/src/ExampleList.tsx @@ -6,6 +6,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import ActivityIndicatorExample from './Examples/ActivityIndicatorExample'; import AppbarExample from './Examples/AppbarExample'; +import AppbarV3Example from './Examples/AppbarV3Example'; import AvatarExample from './Examples/AvatarExample'; import BadgeExample from './Examples/BadgeExample'; import BannerExample from './Examples/BannerExample'; @@ -51,6 +52,7 @@ import TouchableRippleExample from './Examples/TouchableRippleExample'; export const mainExamples = { ActivityIndicator: ActivityIndicatorExample, Appbar: AppbarExample, + AppbarV3: AppbarV3Example, Avatar: AvatarExample, Badge: BadgeExample, Banner: BannerExample, diff --git a/example/src/Examples/AppbarV3Example.tsx b/example/src/Examples/AppbarV3Example.tsx new file mode 100644 index 0000000000..3e4a8fe1c8 --- /dev/null +++ b/example/src/Examples/AppbarV3Example.tsx @@ -0,0 +1,269 @@ +import * as React from 'react'; +import { Platform, StyleSheet, View } from 'react-native'; + +import { useNavigation } from '@react-navigation/native'; +import { + AppbarV3, + FAB, + IconButton, + List, + Palette, + RadioButton, + Snackbar, + Surface, + Switch, + Text, + useTheme, +} from 'react-native-paper'; +import type { + AppbarV3StandardAction, + AppbarV3TitleAlignment, + AppbarV3Variant, +} from 'react-native-paper'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import ScreenWrapper from '../ScreenWrapper'; + +type AppbarConfiguration = AppbarV3Variant | 'small-centered'; + +const MORE_ICON = Platform.OS === 'ios' ? 'dots-horizontal' : 'dots-vertical'; +const BOTTOM_APPBAR_HEIGHT = 80; +const MEDIUM_FAB_HEIGHT = 56; + +const AppbarV3Example = () => { + const navigation = useNavigation('AppbarV3'); + + const [showLeftIcon, setShowLeftIcon] = React.useState(true); + const [showSubtitle, setShowSubtitle] = React.useState(true); + const [showSearchIcon, setShowSearchIcon] = React.useState(true); + const [showMoreIcon, setShowMoreIcon] = React.useState(true); + const [showCustomColor, setShowCustomColor] = React.useState(false); + const [appbarConfiguration, setAppbarConfiguration] = + React.useState('small'); + const [showCalendarIcon, setShowCalendarIcon] = React.useState(false); + const [showScrolled, setShowScrolled] = React.useState(false); + const [showSnackbar, setShowSnackbar] = React.useState(false); + + const theme = useTheme(); + const { bottom, left, right } = useSafeAreaInsets(); + const isCentered = appbarConfiguration === 'small-centered'; + const variant: AppbarV3Variant = isCentered ? 'small' : appbarConfiguration; + const titleAlignment: AppbarV3TitleAlignment = isCentered + ? 'center' + : 'leading'; + + React.useLayoutEffect(() => { + const actions: AppbarV3StandardAction[] = []; + + if (!isCentered && showCalendarIcon) { + actions.push({ + icon: 'calendar', + 'aria-label': 'Calendar', + onPress: () => {}, + }); + } + + if (showSearchIcon) { + actions.push({ + icon: 'magnify', + 'aria-label': 'Search', + onPress: () => {}, + }); + } + + if (showMoreIcon) { + actions.push({ + icon: MORE_ICON, + 'aria-label': 'More options', + onPress: () => {}, + }); + } + + navigation.setOptions({ + header: () => ( + setShowSnackbar(true)} + isScrolled={showScrolled} + leadingAction={ + showLeftIcon + ? { + type: 'back', + onPress: () => navigation.goBack(), + } + : undefined + } + actions={actions} + /> + ), + }); + }, [ + isCentered, + navigation, + showCalendarIcon, + showCustomColor, + showLeftIcon, + showMoreIcon, + showScrolled, + showSearchIcon, + showSubtitle, + titleAlignment, + variant, + ]); + + const renderFAB = () => ( + {}} + style={[ + styles.fab, + { + right: 16 + right, + top: (BOTTOM_APPBAR_HEIGHT - MEDIUM_FAB_HEIGHT) / 2, + }, + ]} + /> + ); + + const renderDefaultOptions = () => ( + <> + + Left icon + + + + Subtitle + + + + Search icon + + + + More icon + + + + Calendar icon + + + + Custom Color + + + + Scrolled + + + + ); + + return ( + <> + + + {renderDefaultOptions()} + + + + setAppbarConfiguration(value as AppbarConfiguration) + } + > + + Small (default) + + + + Medium flexible + + + + Large flexible + + + + Small (centered) + + + + + + + + {}} /> + {}} /> + {}} /> + {}} /> + + {renderFAB()} + + setShowSnackbar(false)} + duration={Snackbar.DURATION_SHORT} + > + Heading pressed + + + ); +}; + +AppbarV3Example.title = 'Appbar V3'; + +export default AppbarV3Example; + +const styles = StyleSheet.create({ + contentContainer: { + paddingVertical: 8, + }, + row: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 8, + paddingHorizontal: 16, + }, + bottom: { + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + }, + bottomActions: { + height: BOTTOM_APPBAR_HEIGHT, + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 4, + }, + fab: { + position: 'absolute', + }, + customColor: { + backgroundColor: Palette.secondary80, + }, +}); From d2cbdc48fd6701d9d2b4c2918772265138fd1fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Fri, 21 Aug 2026 11:23:55 +0200 Subject: [PATCH 04/34] feat(example): handling scrolling behavior --- example/src/Examples/AppbarV3Example.tsx | 42 +++++++++++++++++++----- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/example/src/Examples/AppbarV3Example.tsx b/example/src/Examples/AppbarV3Example.tsx index 3e4a8fe1c8..ba488ddef2 100644 --- a/example/src/Examples/AppbarV3Example.tsx +++ b/example/src/Examples/AppbarV3Example.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; -import { Platform, StyleSheet, View } from 'react-native'; +import { Platform, StyleSheet, useWindowDimensions, View } from 'react-native'; +import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { @@ -41,11 +42,12 @@ const AppbarV3Example = () => { const [appbarConfiguration, setAppbarConfiguration] = React.useState('small'); const [showCalendarIcon, setShowCalendarIcon] = React.useState(false); - const [showScrolled, setShowScrolled] = React.useState(false); + const [isScrolled, setIsScrolled] = React.useState(false); const [showSnackbar, setShowSnackbar] = React.useState(false); const theme = useTheme(); const { bottom, left, right } = useSafeAreaInsets(); + const { height: windowHeight } = useWindowDimensions(); const isCentered = appbarConfiguration === 'small-centered'; const variant: AppbarV3Variant = isCentered ? 'small' : appbarConfiguration; const titleAlignment: AppbarV3TitleAlignment = isCentered @@ -88,7 +90,7 @@ const AppbarV3Example = () => { title="Title" subtitle={showSubtitle ? 'Subtitle' : undefined} onTitlePress={() => setShowSnackbar(true)} - isScrolled={showScrolled} + isScrolled={isScrolled} leadingAction={ showLeftIcon ? { @@ -108,13 +110,26 @@ const AppbarV3Example = () => { showCustomColor, showLeftIcon, showMoreIcon, - showScrolled, + isScrolled, showSearchIcon, showSubtitle, titleAlignment, variant, ]); + const handleScroll = React.useCallback( + ({ nativeEvent }: NativeSyntheticEvent) => { + const nextIsScrolled = nativeEvent.contentOffset.y > 1; + + setIsScrolled((currentIsScrolled) => + currentIsScrolled === nextIsScrolled + ? currentIsScrolled + : nextIsScrolled + ); + }, + [] + ); + const renderFAB = () => ( { Custom Color - - Scrolled - - ); @@ -170,8 +181,21 @@ const AppbarV3Example = () => { <> + + + {renderDefaultOptions()} From 8a34d7c2011da55de429170513c38ad0cc50f658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Fri, 21 Aug 2026 11:34:49 +0200 Subject: [PATCH 05/34] fix(appbar): unnecessary animations on actions change --- example/src/Examples/AppbarV3Example.tsx | 3 +++ src/components/AppbarV3/Appbar.tsx | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/example/src/Examples/AppbarV3Example.tsx b/example/src/Examples/AppbarV3Example.tsx index ba488ddef2..526e511090 100644 --- a/example/src/Examples/AppbarV3Example.tsx +++ b/example/src/Examples/AppbarV3Example.tsx @@ -59,6 +59,7 @@ const AppbarV3Example = () => { if (!isCentered && showCalendarIcon) { actions.push({ + key: 'calendar', icon: 'calendar', 'aria-label': 'Calendar', onPress: () => {}, @@ -67,6 +68,7 @@ const AppbarV3Example = () => { if (showSearchIcon) { actions.push({ + key: 'search', icon: 'magnify', 'aria-label': 'Search', onPress: () => {}, @@ -75,6 +77,7 @@ const AppbarV3Example = () => { if (showMoreIcon) { actions.push({ + key: 'more', icon: MORE_ICON, 'aria-label': 'More options', onPress: () => {}, diff --git a/src/components/AppbarV3/Appbar.tsx b/src/components/AppbarV3/Appbar.tsx index b7d8070d8b..03db31c423 100644 --- a/src/components/AppbarV3/Appbar.tsx +++ b/src/components/AppbarV3/Appbar.tsx @@ -66,8 +66,12 @@ const Appbar = ({ ) : null; const renderActions = () => - actions.map((action, index) => ( - + actions.map((action) => ( + )); const contentProps = { From 5a188f779fb40b2a85ba119d8efb03803cdfa1e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Fri, 21 Aug 2026 13:20:33 +0200 Subject: [PATCH 06/34] feat(appbar): filled icon button support --- example/src/Examples/AppbarV3Example.tsx | 89 +++++++++++++++++++++--- src/components/AppbarV3/AppbarAction.tsx | 14 ++-- 2 files changed, 89 insertions(+), 14 deletions(-) diff --git a/example/src/Examples/AppbarV3Example.tsx b/example/src/Examples/AppbarV3Example.tsx index 526e511090..8f3ed5b48d 100644 --- a/example/src/Examples/AppbarV3Example.tsx +++ b/example/src/Examples/AppbarV3Example.tsx @@ -17,6 +17,8 @@ import { useTheme, } from 'react-native-paper'; import type { + AppbarV3Actions, + AppbarV3FilledAction, AppbarV3StandardAction, AppbarV3TitleAlignment, AppbarV3Variant, @@ -26,6 +28,8 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import ScreenWrapper from '../ScreenWrapper'; type AppbarConfiguration = AppbarV3Variant | 'small-centered'; +type FilledActionVariant = AppbarV3FilledAction['variant']; +type FilledActionWidth = NonNullable; const MORE_ICON = Platform.OS === 'ios' ? 'dots-horizontal' : 'dots-vertical'; const BOTTOM_APPBAR_HEIGHT = 80; @@ -42,6 +46,11 @@ const AppbarV3Example = () => { const [appbarConfiguration, setAppbarConfiguration] = React.useState('small'); const [showCalendarIcon, setShowCalendarIcon] = React.useState(false); + const [showFilledAction, setShowFilledAction] = React.useState(false); + const [filledActionVariant, setFilledActionVariant] = + React.useState('filled'); + const [filledActionWidth, setFilledActionWidth] = + React.useState('default'); const [isScrolled, setIsScrolled] = React.useState(false); const [showSnackbar, setShowSnackbar] = React.useState(false); @@ -55,10 +64,10 @@ const AppbarV3Example = () => { : 'leading'; React.useLayoutEffect(() => { - const actions: AppbarV3StandardAction[] = []; + const standardActions: AppbarV3StandardAction[] = []; if (!isCentered && showCalendarIcon) { - actions.push({ + standardActions.push({ key: 'calendar', icon: 'calendar', 'aria-label': 'Calendar', @@ -67,7 +76,7 @@ const AppbarV3Example = () => { } if (showSearchIcon) { - actions.push({ + standardActions.push({ key: 'search', icon: 'magnify', 'aria-label': 'Search', @@ -76,7 +85,7 @@ const AppbarV3Example = () => { } if (showMoreIcon) { - actions.push({ + standardActions.push({ key: 'more', icon: MORE_ICON, 'aria-label': 'More options', @@ -84,13 +93,26 @@ const AppbarV3Example = () => { }); } + const actions: AppbarV3Actions = showFilledAction + ? [ + { + key: 'share', + icon: 'share-variant', + 'aria-label': 'Share', + variant: filledActionVariant, + width: filledActionWidth, + onPress: () => {}, + }, + ] + : standardActions; + navigation.setOptions({ header: () => ( setShowSnackbar(true)} isScrolled={isScrolled} @@ -108,9 +130,12 @@ const AppbarV3Example = () => { }); }, [ isCentered, + filledActionVariant, + filledActionWidth, navigation, showCalendarIcon, showCustomColor, + showFilledAction, showLeftIcon, showMoreIcon, isScrolled, @@ -157,19 +182,31 @@ const AppbarV3Example = () => { Subtitle + + Filled trailing action + + Search icon - + More icon - + Calendar icon @@ -202,6 +239,42 @@ const AppbarV3Example = () => { {renderDefaultOptions()} + {showFilledAction ? ( + + Style + + setFilledActionVariant(value as FilledActionVariant) + } + > + + Primary filled + + + + Tonal filled + + + + Width + + setFilledActionWidth(value as FilledActionWidth) + } + > + + Default + + + + Wide + + + + + ) : null} { : variant === 'tonal' ? 'contained-tonal' : undefined; + const iconColor = + color ?? + (mode + ? undefined + : leading + ? resolvedTheme.colors.onSurface + : resolvedTheme.colors.onSurfaceVariant); return ( { aria-label={ isBackAction ? (action['aria-label'] ?? 'Back') : action['aria-label'] } - iconColor={ - color ?? - (leading - ? resolvedTheme.colors.onSurface - : resolvedTheme.colors.onSurfaceVariant) - } + iconColor={iconColor} mode={mode} selected={mode ? true : undefined} size={24} From c2b5f60fe63923f6071459523900e15e6baff65d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Mon, 24 Aug 2026 08:32:32 +0200 Subject: [PATCH 07/34] feat(appbar): showcase title centering in example app --- example/src/Examples/AppbarV3Example.tsx | 55 +++++++++++++----------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/example/src/Examples/AppbarV3Example.tsx b/example/src/Examples/AppbarV3Example.tsx index 8f3ed5b48d..0cfc4fa753 100644 --- a/example/src/Examples/AppbarV3Example.tsx +++ b/example/src/Examples/AppbarV3Example.tsx @@ -27,7 +27,6 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import ScreenWrapper from '../ScreenWrapper'; -type AppbarConfiguration = AppbarV3Variant | 'small-centered'; type FilledActionVariant = AppbarV3FilledAction['variant']; type FilledActionWidth = NonNullable; @@ -44,7 +43,8 @@ const AppbarV3Example = () => { const [showMoreIcon, setShowMoreIcon] = React.useState(true); const [showCustomColor, setShowCustomColor] = React.useState(false); const [appbarConfiguration, setAppbarConfiguration] = - React.useState('small'); + React.useState('small'); + const [isTitleCentered, setIsTitleCentered] = React.useState(false); const [showCalendarIcon, setShowCalendarIcon] = React.useState(false); const [showFilledAction, setShowFilledAction] = React.useState(false); const [filledActionVariant, setFilledActionVariant] = @@ -57,16 +57,14 @@ const AppbarV3Example = () => { const theme = useTheme(); const { bottom, left, right } = useSafeAreaInsets(); const { height: windowHeight } = useWindowDimensions(); - const isCentered = appbarConfiguration === 'small-centered'; - const variant: AppbarV3Variant = isCentered ? 'small' : appbarConfiguration; - const titleAlignment: AppbarV3TitleAlignment = isCentered + const titleAlignment: AppbarV3TitleAlignment = isTitleCentered ? 'center' : 'leading'; React.useLayoutEffect(() => { const standardActions: AppbarV3StandardAction[] = []; - if (!isCentered && showCalendarIcon) { + if (showCalendarIcon) { standardActions.push({ key: 'calendar', icon: 'calendar', @@ -110,7 +108,7 @@ const AppbarV3Example = () => { header: () => ( { ), }); }, [ - isCentered, + appbarConfiguration, filledActionVariant, filledActionWidth, navigation, @@ -142,7 +140,6 @@ const AppbarV3Example = () => { showSearchIcon, showSubtitle, titleAlignment, - variant, ]); const handleScroll = React.useCallback( @@ -182,6 +179,10 @@ const AppbarV3Example = () => { Subtitle + + Center title and subtitle + + Filled trailing action @@ -205,8 +206,8 @@ const AppbarV3Example = () => { Calendar icon @@ -244,9 +245,11 @@ const AppbarV3Example = () => { Style - setFilledActionVariant(value as FilledActionVariant) - } + onValueChange={(value: string) => { + if (value === 'filled' || value === 'tonal') { + setFilledActionVariant(value); + } + }} > Primary filled @@ -260,9 +263,11 @@ const AppbarV3Example = () => { Width - setFilledActionWidth(value as FilledActionWidth) - } + onValueChange={(value: string) => { + if (value === 'default' || value === 'wide') { + setFilledActionWidth(value); + } + }} > Default @@ -278,9 +283,15 @@ const AppbarV3Example = () => { - setAppbarConfiguration(value as AppbarConfiguration) - } + onValueChange={(value: string) => { + if ( + value === 'small' || + value === 'medium-flexible' || + value === 'large-flexible' + ) { + setAppbarConfiguration(value); + } + }} > Small (default) @@ -294,10 +305,6 @@ const AppbarV3Example = () => { Large flexible - - Small (centered) - - From 9be328786927830e7684f9617c5a3a6d5fc20962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Mon, 24 Aug 2026 11:19:54 +0200 Subject: [PATCH 08/34] feat(appbar): title image support --- example/src/Examples/AppbarV3Example.tsx | 109 +++++++++++++++++----- src/components/AppbarV3/Appbar.tsx | 90 ++++++++++++++---- src/components/AppbarV3/AppbarContent.tsx | 34 ++++++- src/components/AppbarV3/types.ts | 29 ++++-- src/components/AppbarV3/utils.ts | 1 + 5 files changed, 211 insertions(+), 52 deletions(-) diff --git a/example/src/Examples/AppbarV3Example.tsx b/example/src/Examples/AppbarV3Example.tsx index 0cfc4fa753..2337ed6b61 100644 --- a/example/src/Examples/AppbarV3Example.tsx +++ b/example/src/Examples/AppbarV3Example.tsx @@ -1,5 +1,11 @@ import * as React from 'react'; -import { Platform, StyleSheet, useWindowDimensions, View } from 'react-native'; +import { + Image, + Platform, + StyleSheet, + useWindowDimensions, + View, +} from 'react-native'; import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native'; import { useNavigation } from '@react-navigation/native'; @@ -39,6 +45,7 @@ const AppbarV3Example = () => { const [showLeftIcon, setShowLeftIcon] = React.useState(true); const [showSubtitle, setShowSubtitle] = React.useState(true); + const [showTitleImage, setShowTitleImage] = React.useState(false); const [showSearchIcon, setShowSearchIcon] = React.useState(true); const [showMoreIcon, setShowMoreIcon] = React.useState(true); const [showCustomColor, setShowCustomColor] = React.useState(false); @@ -104,27 +111,72 @@ const AppbarV3Example = () => { ] : standardActions; + const leadingAction = showLeftIcon + ? { + type: 'back' as const, + onPress: () => navigation.goBack(), + } + : undefined; + const titleImage = ( + + ); + const commonProps = { + actions, + isScrolled, + leadingAction, + onTitlePress: () => setShowSnackbar(true), + style: showCustomColor ? styles.customColor : null, + titleAlignment, + }; + navigation.setOptions({ - header: () => ( - setShowSnackbar(true)} - isScrolled={isScrolled} - leadingAction={ - showLeftIcon - ? { - type: 'back', - onPress: () => navigation.goBack(), - } - : undefined - } - actions={actions} - /> - ), + header: () => { + if (appbarConfiguration === 'small') { + return showTitleImage ? ( + + ) : ( + + ); + } + + return showTitleImage ? ( + + ) : ( + + ); + }, }); }, [ appbarConfiguration, @@ -139,6 +191,7 @@ const AppbarV3Example = () => { isScrolled, showSearchIcon, showSubtitle, + showTitleImage, titleAlignment, ]); @@ -177,7 +230,15 @@ const AppbarV3Example = () => { Subtitle - + + + + Title image + Center title and subtitle @@ -373,4 +434,8 @@ const styles = StyleSheet.create({ customColor: { backgroundColor: Palette.secondary80, }, + titleImage: { + width: 32, + height: 32, + }, }); diff --git a/src/components/AppbarV3/Appbar.tsx b/src/components/AppbarV3/Appbar.tsx index 03db31c423..a4bbc7473c 100644 --- a/src/components/AppbarV3/Appbar.tsx +++ b/src/components/AppbarV3/Appbar.tsx @@ -1,14 +1,18 @@ import { StyleSheet, View } from 'react-native'; -import type { ColorValue, ViewStyle } from 'react-native'; +import type { ColorValue } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import AppbarAction from './AppbarAction'; import AppbarContent from './AppbarContent'; -import type { AppbarAction as AppbarActionConfig, Props } from './types'; -import { APPBAR_ACTION_SIZE, getActionsWidth, getAppbarHeight } from './utils'; +import type { Props } from './types'; +import { + APPBAR_ACTION_SIZE, + APPBAR_TITLE_IMAGE_HEIGHT, + getActionsWidth, + getAppbarHeight, +} from './utils'; import { useInternalTheme } from '../../core/theming'; -import type { Theme } from '../../types'; import { getAppbarBorders } from '../Appbar/utils'; import Surface from '../Surface'; @@ -39,13 +43,17 @@ const Appbar = ({ ref, ...rest }: Props) => { - const theme = useInternalTheme(themeOverrides) as Theme; + const theme = useInternalTheme(themeOverrides); const detectedInsets = useSafeAreaInsets(); const flattenedStyle = StyleSheet.flatten(style); - const { backgroundColor: customBackground, ...restStyle } = (flattenedStyle || - {}) as Exclude & { + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const resolvedStyle = (flattenedStyle || {}) as Exclude< + typeof flattenedStyle, + number + > & { backgroundColor?: ColorValue; }; + const { backgroundColor: customBackground, ...restStyle } = resolvedStyle; const backgroundColor = customBackground ?? (isScrolled ? theme.colors.surfaceContainer : theme.colors.surface); @@ -57,7 +65,7 @@ const Appbar = ({ const leftInset = safeAreaInsets?.left ?? detectedInsets.left; const rightInset = safeAreaInsets?.right ?? detectedInsets.right; const horizontalInset = Math.max(leftInset, rightInset); - const borderRadius = getAppbarBorders(restStyle as ViewStyle); + const borderRadius = getAppbarBorders(restStyle); const centered = titleAlignment === 'center'; const renderLeadingAction = () => @@ -94,11 +102,23 @@ const Appbar = ({ variant, }; + const renderFlexibleTitleImage = () => + titleImage ? ( + + {titleImage} + + ) : null; + const renderSmallAppbar = () => { - if (centered) { + if (centered || titleImage) { const sideWidth = Math.max( leadingAction ? APPBAR_ACTION_SIZE : 0, - getActionsWidth(actions as readonly AppbarActionConfig[]) + getActionsWidth(actions) ); return ( @@ -128,15 +148,40 @@ const Appbar = ({ ); }; - const renderFlexibleAppbar = () => ( - - - {renderLeadingAction()} - {renderActions()} + const renderFlexibleAppbar = () => { + const sideWidth = Math.max( + leadingAction ? APPBAR_ACTION_SIZE : 0, + getActionsWidth(actions) + ); + + return ( + + {titleImage ? ( + + + {renderLeadingAction()} + + {renderFlexibleTitleImage()} + + {renderActions()} + + + ) : ( + + {renderLeadingAction()} + {renderActions()} + + )} + - - - ); + ); + }; return ( ; const subtitleVariants = { - small: 'labelSmall', + small: 'labelMedium', 'medium-flexible': 'labelLarge', 'large-flexible': 'titleMedium', } as const satisfies Record; @@ -73,9 +74,17 @@ const AppbarContent = ({ const titleVariant = titleVariants[variant]; const subtitleVariant = subtitleVariants[variant]; const centered = alignment === 'center'; + const hasTitleImage = variant === 'small' && Boolean(titleImage); - const content = titleImage ? ( - titleImage + const content = hasTitleImage ? ( + + {titleImage} + ) : ( <> {content}; + return ( + + {content} + + ); }; const styles = StyleSheet.create({ @@ -166,6 +185,13 @@ const styles = StyleSheet.create({ centeredText: { textAlign: 'center', }, + titleImage: { + height: APPBAR_TITLE_IMAGE_HEIGHT, + maxWidth: '100%', + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + }, }); export default AppbarContent; diff --git a/src/components/AppbarV3/types.ts b/src/components/AppbarV3/types.ts index 2f6e9874e7..9fbc0eb3c8 100644 --- a/src/components/AppbarV3/types.ts +++ b/src/components/AppbarV3/types.ts @@ -68,10 +68,9 @@ type AppbarBackAction = Omit< export type AppbarLeadingAction = AppbarLeadingIconAction | AppbarBackAction; -type AppbarTextTitle = { +type AppbarWrittenTitle = { /** Written headline displayed by the app bar. */ title: string; - titleImage?: never; /** Optional supporting text displayed below the headline. */ subtitle?: string; titleStyle?: StyleProp; @@ -81,10 +80,20 @@ type AppbarTextTitle = { subtitleMaxFontSizeMultiplier?: number; }; -type AppbarImageTitle = { - title?: never; - /** Image or logo which replaces the written headline. */ +type AppbarTitleImage = { + /** Image or logo displayed in the app bar. It should fit within 32dp height. */ titleImage: React.ReactElement; +}; + +type AppbarTextTitle = AppbarWrittenTitle & { + variant: AppbarVariant; + titleImage?: never; +}; + +type AppbarSmallImageTitle = AppbarTitleImage & { + variant: 'small'; + /** Accessible page title. The image replaces this text visually. */ + title: string; subtitle?: never; titleStyle?: never; subtitleStyle?: never; @@ -93,9 +102,12 @@ type AppbarImageTitle = { subtitleMaxFontSizeMultiplier?: never; }; +type AppbarFlexibleImageTitle = AppbarWrittenTitle & + AppbarTitleImage & { + variant: Exclude; + }; + type AppbarBaseProps = Omit & { - /** Expressive app bar size variant. */ - variant: AppbarVariant; /** Headline and subtitle alignment. */ titleAlignment?: AppbarTitleAlignment; /** Optional leading navigation action. */ @@ -127,4 +139,5 @@ type AppbarBaseProps = Omit & { theme?: ThemeProp; }; -export type Props = AppbarBaseProps & (AppbarTextTitle | AppbarImageTitle); +export type Props = AppbarBaseProps & + (AppbarTextTitle | AppbarSmallImageTitle | AppbarFlexibleImageTitle); diff --git a/src/components/AppbarV3/utils.ts b/src/components/AppbarV3/utils.ts index b8ac83ab64..43f2478039 100644 --- a/src/components/AppbarV3/utils.ts +++ b/src/components/AppbarV3/utils.ts @@ -2,6 +2,7 @@ import type { AppbarAction, AppbarVariant } from './types'; export const APPBAR_ACTION_SIZE = 48; export const APPBAR_WIDE_ACTION_SIZE = 64; +export const APPBAR_TITLE_IMAGE_HEIGHT = 32; export const getAppbarHeight = ( variant: AppbarVariant, From 91c870bcfe7829da2b4c648b6d0ba64c41e7ca03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Mon, 24 Aug 2026 13:18:13 +0200 Subject: [PATCH 09/34] feat(appbar): initial search bar support --- example/src/Examples/AppbarV3Example.tsx | 76 +++++++++++- src/components/AppbarV3/Appbar.tsx | 141 +++++++++++++++++++--- src/components/AppbarV3/AppbarContent.tsx | 10 +- src/components/AppbarV3/index.ts | 1 + src/components/AppbarV3/types.ts | 67 ++++++++-- src/components/AppbarV3/utils.ts | 8 +- src/index.tsx | 1 + 7 files changed, 261 insertions(+), 43 deletions(-) diff --git a/example/src/Examples/AppbarV3Example.tsx b/example/src/Examples/AppbarV3Example.tsx index 2337ed6b61..5d18a207a8 100644 --- a/example/src/Examples/AppbarV3Example.tsx +++ b/example/src/Examples/AppbarV3Example.tsx @@ -25,6 +25,7 @@ import { import type { AppbarV3Actions, AppbarV3FilledAction, + AppbarV3LeadingAction, AppbarV3StandardAction, AppbarV3TitleAlignment, AppbarV3Variant, @@ -40,6 +41,38 @@ const MORE_ICON = Platform.OS === 'ios' ? 'dots-horizontal' : 'dots-vertical'; const BOTTOM_APPBAR_HEIGHT = 80; const MEDIUM_FAB_HEIGHT = 56; +type SearchAppbarHeaderProps = { + actions: AppbarV3StandardAction[]; + isScrolled: boolean; + leadingAction?: AppbarV3LeadingAction; + showCustomColor: boolean; +}; + +const SearchAppbarHeader = ({ + actions, + isScrolled, + leadingAction, + showCustomColor, +}: SearchAppbarHeaderProps) => { + const [searchQuery, setSearchQuery] = React.useState(''); + + return ( + + ); +}; + const AppbarV3Example = () => { const navigation = useNavigation('AppbarV3'); @@ -80,7 +113,7 @@ const AppbarV3Example = () => { }); } - if (showSearchIcon) { + if (showSearchIcon && appbarConfiguration !== 'search') { standardActions.push({ key: 'search', icon: 'magnify', @@ -136,6 +169,17 @@ const AppbarV3Example = () => { navigation.setOptions({ header: () => { + if (appbarConfiguration === 'search') { + return ( + + ); + } + if (appbarConfiguration === 'small') { return showTitleImage ? ( { Subtitle Title image - + Center title and subtitle - + Filled trailing action - + Search icon @@ -347,6 +406,7 @@ const AppbarV3Example = () => { onValueChange={(value: string) => { if ( value === 'small' || + value === 'search' || value === 'medium-flexible' || value === 'large-flexible' ) { @@ -354,6 +414,10 @@ const AppbarV3Example = () => { } }} > + + Search + + Small (default) diff --git a/src/components/AppbarV3/Appbar.tsx b/src/components/AppbarV3/Appbar.tsx index a4bbc7473c..595bcf9b7b 100644 --- a/src/components/AppbarV3/Appbar.tsx +++ b/src/components/AppbarV3/Appbar.tsx @@ -1,5 +1,6 @@ +import * as React from 'react'; import { StyleSheet, View } from 'react-native'; -import type { ColorValue } from 'react-native'; +import type { ColorValue, LayoutChangeEvent } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -11,9 +12,11 @@ import { APPBAR_TITLE_IMAGE_HEIGHT, getActionsWidth, getAppbarHeight, + getAppbarSearchWidth, } from './utils'; import { useInternalTheme } from '../../core/theming'; import { getAppbarBorders } from '../Appbar/utils'; +import Searchbar from '../Searchbar'; import Surface from '../Surface'; const Appbar = ({ @@ -23,6 +26,7 @@ const Appbar = ({ leadingAction, onTitlePress, safeAreaInsets, + searchBar, statusBarHeight, style, subtitle, @@ -45,6 +49,7 @@ const Appbar = ({ }: Props) => { const theme = useInternalTheme(themeOverrides); const detectedInsets = useSafeAreaInsets(); + const [searchSlotWidth, setSearchSlotWidth] = React.useState(0); const flattenedStyle = StyleSheet.flatten(style); // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const resolvedStyle = (flattenedStyle || {}) as Exclude< @@ -68,6 +73,17 @@ const Appbar = ({ const borderRadius = getAppbarBorders(restStyle); const centered = titleAlignment === 'center'; + const handleSearchSlotLayout = React.useCallback( + ({ nativeEvent }: LayoutChangeEvent) => { + const nextWidth = nativeEvent.layout.width; + + setSearchSlotWidth((currentWidth) => + currentWidth === nextWidth ? currentWidth : nextWidth + ); + }, + [] + ); + const renderLeadingAction = () => leadingAction ? ( @@ -82,26 +98,83 @@ const Appbar = ({ /> )); - const contentProps = { - alignment: titleAlignment, - contentStyle, - onTitlePress, - subtitle, - subtitleColor: resolvedSubtitleColor, - subtitleMaxFontSizeMultiplier, - subtitleStyle, - testID: `${testID}-content`, - theme, - title, - titleColor: resolvedTitleColor, - titleDisabled, - titleImage, - titleMaxFontSizeMultiplier, - titleRef, - titleStyle, - variant, + const renderSearchAppbar = () => { + if (!searchBar) { + return null; + } + + const { + inputStyle: searchInputStyle, + style: searchStyle, + testID: searchTestID = `${testID}-search`, + ...searchProps + } = searchBar; + const searchWidth = searchSlotWidth + ? getAppbarSearchWidth(searchSlotWidth) + : '100%'; + const searchBackgroundColor = isScrolled + ? theme.colors.surfaceContainerHighest + : theme.colors.surfaceContainer; + + return ( + + {renderLeadingAction()} + + + + {renderActions()} + + ); }; + const contentProps = + variant !== 'search' + ? { + alignment: titleAlignment, + contentStyle, + onTitlePress, + subtitle, + subtitleColor: resolvedSubtitleColor, + subtitleMaxFontSizeMultiplier, + subtitleStyle, + testID: `${testID}-content`, + theme, + title, + titleColor: resolvedTitleColor, + titleDisabled, + titleImage, + titleMaxFontSizeMultiplier, + titleRef, + titleStyle, + variant, + } + : null; + const renderFlexibleTitleImage = () => titleImage ? ( { + if (!contentProps) { + return null; + } + if (centered || titleImage) { const sideWidth = Math.max( leadingAction ? APPBAR_ACTION_SIZE : 0, @@ -149,6 +226,10 @@ const Appbar = ({ }; const renderFlexibleAppbar = () => { + if (!contentProps) { + return null; + } + const sideWidth = Math.max( leadingAction ? APPBAR_ACTION_SIZE : 0, getActionsWidth(actions) @@ -204,7 +285,11 @@ const Appbar = ({ testID={testID} style={[styles.appbar, { backgroundColor, minHeight }, restStyle]} > - {variant === 'small' ? renderSmallAppbar() : renderFlexibleAppbar()} + {variant === 'search' + ? renderSearchAppbar() + : variant === 'small' + ? renderSmallAppbar() + : renderFlexibleAppbar()} ); @@ -219,6 +304,22 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', }, + searchRow: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + }, + searchSlot: { + flex: 1, + marginHorizontal: 8, + alignItems: 'center', + }, + searchBar: { + maxWidth: '100%', + }, + searchInput: { + textAlign: 'center', + }, flexibleContainer: { flex: 1, }, diff --git a/src/components/AppbarV3/AppbarContent.tsx b/src/components/AppbarV3/AppbarContent.tsx index 27bcab24d4..e09e6b507d 100644 --- a/src/components/AppbarV3/AppbarContent.tsx +++ b/src/components/AppbarV3/AppbarContent.tsx @@ -3,7 +3,7 @@ import type { ColorValue, StyleProp, ViewStyle } from 'react-native'; import type { AppbarTitleAlignment, - AppbarVariant, + AppbarTitleVariant, Props as AppbarProps, } from './types'; import { APPBAR_TITLE_IMAGE_HEIGHT } from './utils'; @@ -28,7 +28,7 @@ type Props = Pick< subtitleColor: ColorValue; theme: Theme; titleColor: ColorValue; - variant: AppbarVariant; + variant: AppbarTitleVariant; style?: StyleProp; testID: string; }; @@ -37,19 +37,19 @@ const titleVariants = { small: 'titleLarge', 'medium-flexible': 'headlineMedium', 'large-flexible': 'displaySmall', -} as const satisfies Record; +} as const satisfies Record; const subtitleVariants = { small: 'labelMedium', 'medium-flexible': 'labelLarge', 'large-flexible': 'titleMedium', -} as const satisfies Record; +} as const satisfies Record; const subtitleSpacing = { small: 0, 'medium-flexible': 4, 'large-flexible': 8, -} as const satisfies Record; +} as const satisfies Record; const AppbarContent = ({ alignment, diff --git a/src/components/AppbarV3/index.ts b/src/components/AppbarV3/index.ts index 7891d3b6a0..9bed0afff1 100644 --- a/src/components/AppbarV3/index.ts +++ b/src/components/AppbarV3/index.ts @@ -5,6 +5,7 @@ export type { AppbarActions, AppbarFilledAction, AppbarLeadingAction, + AppbarSearchbarProps, AppbarStandardAction, AppbarTitleAlignment, AppbarVariant, diff --git a/src/components/AppbarV3/types.ts b/src/components/AppbarV3/types.ts index 9fbc0eb3c8..00dedd1a60 100644 --- a/src/components/AppbarV3/types.ts +++ b/src/components/AppbarV3/types.ts @@ -13,9 +13,16 @@ import type { import type { ThemeProp } from '../../types'; import type { IconSource } from '../Icon'; import type { Props as IconButtonProps } from '../IconButton/IconButton'; +import type { Props as SearchbarProps } from '../Searchbar'; import type { TextRef } from '../Typography/Text'; -export type AppbarVariant = 'small' | 'medium-flexible' | 'large-flexible'; +export type AppbarVariant = + | 'search' + | 'small' + | 'medium-flexible' + | 'large-flexible'; + +export type AppbarTitleVariant = Exclude; export type AppbarTitleAlignment = 'leading' | 'center'; @@ -86,7 +93,7 @@ type AppbarTitleImage = { }; type AppbarTextTitle = AppbarWrittenTitle & { - variant: AppbarVariant; + variant: AppbarTitleVariant; titleImage?: never; }; @@ -104,16 +111,12 @@ type AppbarSmallImageTitle = AppbarTitleImage & { type AppbarFlexibleImageTitle = AppbarWrittenTitle & AppbarTitleImage & { - variant: Exclude; + variant: Exclude; }; type AppbarBaseProps = Omit & { - /** Headline and subtitle alignment. */ - titleAlignment?: AppbarTitleAlignment; /** Optional leading navigation action. */ leadingAction?: AppbarLeadingAction; - /** Trailing icon actions. */ - actions?: AppbarActions; /** Uses the on-scroll container color when true. */ isScrolled?: boolean; /** Override for the automatic top safe-area inset. */ @@ -124,6 +127,16 @@ type AppbarBaseProps = Omit & { left?: number; right?: number; }; + style?: Animated.WithAnimatedValue>; + ref?: React.Ref; + theme?: ThemeProp; +}; + +type AppbarTitleProps = { + /** Headline and subtitle alignment. */ + titleAlignment?: AppbarTitleAlignment; + /** Trailing icon actions. */ + actions?: AppbarActions; /** Called when the title area is pressed. */ onTitlePress?: (event: GestureResponderEvent) => void; /** Disables title-area interaction. */ @@ -134,10 +147,42 @@ type AppbarBaseProps = Omit & { subtitleColor?: ColorValue; /** Style applied to the title area. */ contentStyle?: StyleProp; - style?: Animated.WithAnimatedValue>; - ref?: React.Ref; - theme?: ThemeProp; + searchBar?: never; +}; + +export type AppbarSearchbarProps = Omit< + SearchbarProps, + 'elevation' | 'mode' | 'showDivider' | 'theme' +> & { + /** Search hint. Material guidance recommends including the word “Search”. */ + placeholder: string; +}; + +type AppbarSearchProps = { + variant: 'search'; + /** Props forwarded to the existing Paper Searchbar. */ + searchBar: AppbarSearchbarProps; + /** Exterior trailing icon actions. */ + actions?: readonly AppbarStandardAction[]; + title?: never; + subtitle?: never; + titleImage?: never; + titleAlignment?: never; + titleStyle?: never; + subtitleStyle?: never; + titleRef?: never; + titleMaxFontSizeMultiplier?: never; + subtitleMaxFontSizeMultiplier?: never; + onTitlePress?: never; + titleDisabled?: never; + titleColor?: never; + subtitleColor?: never; + contentStyle?: never; }; export type Props = AppbarBaseProps & - (AppbarTextTitle | AppbarSmallImageTitle | AppbarFlexibleImageTitle); + ( + | (AppbarTitleProps & + (AppbarTextTitle | AppbarSmallImageTitle | AppbarFlexibleImageTitle)) + | AppbarSearchProps + ); diff --git a/src/components/AppbarV3/utils.ts b/src/components/AppbarV3/utils.ts index 43f2478039..2df8aa37bd 100644 --- a/src/components/AppbarV3/utils.ts +++ b/src/components/AppbarV3/utils.ts @@ -3,12 +3,18 @@ import type { AppbarAction, AppbarVariant } from './types'; export const APPBAR_ACTION_SIZE = 48; export const APPBAR_WIDE_ACTION_SIZE = 64; export const APPBAR_TITLE_IMAGE_HEIGHT = 32; +export const APPBAR_SEARCH_WIDTH_BREAKPOINT = 312; + +export const getAppbarSearchWidth = (availableWidth: number) => + availableWidth > APPBAR_SEARCH_WIDTH_BREAKPOINT + ? Math.max(APPBAR_SEARCH_WIDTH_BREAKPOINT, availableWidth / 2) + : availableWidth; export const getAppbarHeight = ( variant: AppbarVariant, hasSubtitle: boolean ) => { - if (variant === 'small') { + if (variant === 'search' || variant === 'small') { return 64; } diff --git a/src/index.tsx b/src/index.tsx index 7469301a7b..36a5eac3c9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -67,6 +67,7 @@ export type { AppbarActions as AppbarV3Actions, AppbarFilledAction as AppbarV3FilledAction, AppbarLeadingAction as AppbarV3LeadingAction, + AppbarSearchbarProps as AppbarV3SearchbarProps, AppbarStandardAction as AppbarV3StandardAction, AppbarTitleAlignment as AppbarV3TitleAlignment, AppbarVariant as AppbarV3Variant, From 033024d9f74b3a42fae406d6e1c17cbc5a5040b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Mon, 24 Aug 2026 13:59:10 +0200 Subject: [PATCH 10/34] refactor(appbar): drop titleDisabled --- src/components/AppbarV3/Appbar.tsx | 2 -- src/components/AppbarV3/AppbarContent.tsx | 4 ---- src/components/AppbarV3/types.ts | 3 --- 3 files changed, 9 deletions(-) diff --git a/src/components/AppbarV3/Appbar.tsx b/src/components/AppbarV3/Appbar.tsx index 595bcf9b7b..f2933aa920 100644 --- a/src/components/AppbarV3/Appbar.tsx +++ b/src/components/AppbarV3/Appbar.tsx @@ -38,7 +38,6 @@ const Appbar = ({ title, titleAlignment = 'leading', titleColor, - titleDisabled, titleImage, titleMaxFontSizeMultiplier, titleRef, @@ -166,7 +165,6 @@ const Appbar = ({ theme, title, titleColor: resolvedTitleColor, - titleDisabled, titleImage, titleMaxFontSizeMultiplier, titleRef, diff --git a/src/components/AppbarV3/AppbarContent.tsx b/src/components/AppbarV3/AppbarContent.tsx index e09e6b507d..6be4305324 100644 --- a/src/components/AppbarV3/AppbarContent.tsx +++ b/src/components/AppbarV3/AppbarContent.tsx @@ -18,7 +18,6 @@ type Props = Pick< | 'subtitleMaxFontSizeMultiplier' | 'subtitleStyle' | 'title' - | 'titleDisabled' | 'titleImage' | 'titleMaxFontSizeMultiplier' | 'titleRef' @@ -63,7 +62,6 @@ const AppbarContent = ({ theme, title, titleColor, - titleDisabled, titleImage, titleMaxFontSizeMultiplier, titleRef, @@ -143,8 +141,6 @@ const AppbarContent = ({ {...wrapperProps} role="button" accessibilityLabel={hasTitleImage ? title : undefined} - aria-disabled={titleDisabled} - disabled={titleDisabled} onPress={onTitlePress} > {content} diff --git a/src/components/AppbarV3/types.ts b/src/components/AppbarV3/types.ts index 00dedd1a60..575f89cd7f 100644 --- a/src/components/AppbarV3/types.ts +++ b/src/components/AppbarV3/types.ts @@ -139,8 +139,6 @@ type AppbarTitleProps = { actions?: AppbarActions; /** Called when the title area is pressed. */ onTitlePress?: (event: GestureResponderEvent) => void; - /** Disables title-area interaction. */ - titleDisabled?: boolean; /** Custom headline color. */ titleColor?: ColorValue; /** Custom subtitle color. */ @@ -174,7 +172,6 @@ type AppbarSearchProps = { titleMaxFontSizeMultiplier?: never; subtitleMaxFontSizeMultiplier?: never; onTitlePress?: never; - titleDisabled?: never; titleColor?: never; subtitleColor?: never; contentStyle?: never; From ca9c2f65a9b2246e2339bbee158857abffbfa619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Mon, 24 Aug 2026 14:27:44 +0200 Subject: [PATCH 11/34] feat(appbar): streamline props --- src/components/AppbarV3/Appbar.tsx | 20 +++-------- src/components/AppbarV3/AppbarContent.tsx | 34 +++++++------------ src/components/AppbarV3/index.ts | 2 ++ src/components/AppbarV3/types.ts | 41 +++++++++++------------ src/index.tsx | 2 ++ 5 files changed, 40 insertions(+), 59 deletions(-) diff --git a/src/components/AppbarV3/Appbar.tsx b/src/components/AppbarV3/Appbar.tsx index f2933aa920..e5351f909a 100644 --- a/src/components/AppbarV3/Appbar.tsx +++ b/src/components/AppbarV3/Appbar.tsx @@ -30,18 +30,13 @@ const Appbar = ({ statusBarHeight, style, subtitle, - subtitleColor, - subtitleMaxFontSizeMultiplier, - subtitleStyle, + subtitleProps, testID = 'appbar', theme: themeOverrides, title, titleAlignment = 'leading', - titleColor, titleImage, - titleMaxFontSizeMultiplier, - titleRef, - titleStyle, + titleProps, variant, ref, ...rest @@ -61,8 +56,6 @@ const Appbar = ({ const backgroundColor = customBackground ?? (isScrolled ? theme.colors.surfaceContainer : theme.colors.surface); - const resolvedTitleColor = titleColor ?? theme.colors.onSurface; - const resolvedSubtitleColor = subtitleColor ?? theme.colors.onSurfaceVariant; const hasSubtitle = typeof subtitle === 'string' && subtitle.length > 0; const minHeight = getAppbarHeight(variant, hasSubtitle); const topInset = statusBarHeight ?? safeAreaInsets?.top ?? detectedInsets.top; @@ -158,17 +151,12 @@ const Appbar = ({ contentStyle, onTitlePress, subtitle, - subtitleColor: resolvedSubtitleColor, - subtitleMaxFontSizeMultiplier, - subtitleStyle, + subtitleProps, testID: `${testID}-content`, theme, title, - titleColor: resolvedTitleColor, titleImage, - titleMaxFontSizeMultiplier, - titleRef, - titleStyle, + titleProps, variant, } : null; diff --git a/src/components/AppbarV3/AppbarContent.tsx b/src/components/AppbarV3/AppbarContent.tsx index 6be4305324..a24a5df401 100644 --- a/src/components/AppbarV3/AppbarContent.tsx +++ b/src/components/AppbarV3/AppbarContent.tsx @@ -1,5 +1,5 @@ import { Pressable, StyleSheet, View } from 'react-native'; -import type { ColorValue, StyleProp, ViewStyle } from 'react-native'; +import type { StyleProp, ViewStyle } from 'react-native'; import type { AppbarTitleAlignment, @@ -15,18 +15,13 @@ type Props = Pick< | 'contentStyle' | 'onTitlePress' | 'subtitle' - | 'subtitleMaxFontSizeMultiplier' - | 'subtitleStyle' + | 'subtitleProps' | 'title' | 'titleImage' - | 'titleMaxFontSizeMultiplier' - | 'titleRef' - | 'titleStyle' + | 'titleProps' > & { alignment: AppbarTitleAlignment; - subtitleColor: ColorValue; theme: Theme; - titleColor: ColorValue; variant: AppbarTitleVariant; style?: StyleProp; testID: string; @@ -55,17 +50,12 @@ const AppbarContent = ({ contentStyle, onTitlePress, subtitle, - subtitleColor, - subtitleMaxFontSizeMultiplier, - subtitleStyle, + subtitleProps, testID, theme, title, - titleColor, titleImage, - titleMaxFontSizeMultiplier, - titleRef, - titleStyle, + titleProps, variant, style, }: Props) => { @@ -86,19 +76,19 @@ const AppbarContent = ({ ) : ( <> {title} @@ -108,14 +98,14 @@ const AppbarContent = ({ variant={subtitleVariant} theme={theme} numberOfLines={1} - maxFontSizeMultiplier={subtitleMaxFontSizeMultiplier} + maxFontSizeMultiplier={subtitleProps?.maxFontSizeMultiplier} testID={`${testID}-subtitle-text`} style={[ styles.text, centered && styles.centeredText, { marginTop: subtitleSpacing[variant] }, - { color: subtitleColor }, - subtitleStyle, + { color: theme.colors.onSurfaceVariant }, + subtitleProps?.style, ]} > {subtitle} diff --git a/src/components/AppbarV3/index.ts b/src/components/AppbarV3/index.ts index 9bed0afff1..06fb7c8533 100644 --- a/src/components/AppbarV3/index.ts +++ b/src/components/AppbarV3/index.ts @@ -7,7 +7,9 @@ export type { AppbarLeadingAction, AppbarSearchbarProps, AppbarStandardAction, + AppbarTextProps, AppbarTitleAlignment, + AppbarTitleTextProps, AppbarVariant, Props, } from './types'; diff --git a/src/components/AppbarV3/types.ts b/src/components/AppbarV3/types.ts index 575f89cd7f..6f945f8156 100644 --- a/src/components/AppbarV3/types.ts +++ b/src/components/AppbarV3/types.ts @@ -26,6 +26,18 @@ export type AppbarTitleVariant = Exclude; export type AppbarTitleAlignment = 'leading' | 'center'; +export type AppbarTextProps = { + /** Style applied to the text. */ + style?: StyleProp; + /** Specifies the largest possible scale the font can reach. */ + maxFontSizeMultiplier?: number; +}; + +export type AppbarTitleTextProps = AppbarTextProps & { + /** Reference for the title heading. */ + ref?: React.RefObject; +}; + type AppbarActionBase = Omit< IconButtonProps, 'icon' | 'iconColor' | 'mode' | 'selected' | 'size' | 'theme' | 'aria-label' @@ -80,11 +92,10 @@ type AppbarWrittenTitle = { title: string; /** Optional supporting text displayed below the headline. */ subtitle?: string; - titleStyle?: StyleProp; - subtitleStyle?: StyleProp; - titleRef?: React.RefObject; - titleMaxFontSizeMultiplier?: number; - subtitleMaxFontSizeMultiplier?: number; + /** Props applied to the title heading. */ + titleProps?: AppbarTitleTextProps; + /** Props applied to the subtitle text. */ + subtitleProps?: AppbarTextProps; }; type AppbarTitleImage = { @@ -102,11 +113,8 @@ type AppbarSmallImageTitle = AppbarTitleImage & { /** Accessible page title. The image replaces this text visually. */ title: string; subtitle?: never; - titleStyle?: never; - subtitleStyle?: never; - titleRef?: never; - titleMaxFontSizeMultiplier?: never; - subtitleMaxFontSizeMultiplier?: never; + titleProps?: never; + subtitleProps?: never; }; type AppbarFlexibleImageTitle = AppbarWrittenTitle & @@ -139,10 +147,6 @@ type AppbarTitleProps = { actions?: AppbarActions; /** Called when the title area is pressed. */ onTitlePress?: (event: GestureResponderEvent) => void; - /** Custom headline color. */ - titleColor?: ColorValue; - /** Custom subtitle color. */ - subtitleColor?: ColorValue; /** Style applied to the title area. */ contentStyle?: StyleProp; searchBar?: never; @@ -166,14 +170,9 @@ type AppbarSearchProps = { subtitle?: never; titleImage?: never; titleAlignment?: never; - titleStyle?: never; - subtitleStyle?: never; - titleRef?: never; - titleMaxFontSizeMultiplier?: never; - subtitleMaxFontSizeMultiplier?: never; + titleProps?: never; + subtitleProps?: never; onTitlePress?: never; - titleColor?: never; - subtitleColor?: never; contentStyle?: never; }; diff --git a/src/index.tsx b/src/index.tsx index 36a5eac3c9..1726a497ec 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -69,7 +69,9 @@ export type { AppbarLeadingAction as AppbarV3LeadingAction, AppbarSearchbarProps as AppbarV3SearchbarProps, AppbarStandardAction as AppbarV3StandardAction, + AppbarTextProps as AppbarV3TextProps, AppbarTitleAlignment as AppbarV3TitleAlignment, + AppbarTitleTextProps as AppbarV3TitleTextProps, AppbarVariant as AppbarV3Variant, } from './components/AppbarV3'; export type { Props as AvatarIconProps } from './components/Avatar/AvatarIcon'; From 996b71671a37fdfc699c8529cb0bcef22f750ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Mon, 24 Aug 2026 14:44:09 +0200 Subject: [PATCH 12/34] refactor(appbar): search width adjustments --- src/components/AppbarV3/utils.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/AppbarV3/utils.ts b/src/components/AppbarV3/utils.ts index 2df8aa37bd..219f0d6cca 100644 --- a/src/components/AppbarV3/utils.ts +++ b/src/components/AppbarV3/utils.ts @@ -3,12 +3,16 @@ import type { AppbarAction, AppbarVariant } from './types'; export const APPBAR_ACTION_SIZE = 48; export const APPBAR_WIDE_ACTION_SIZE = 64; export const APPBAR_TITLE_IMAGE_HEIGHT = 32; -export const APPBAR_SEARCH_WIDTH_BREAKPOINT = 312; +export const APPBAR_SEARCH_MIN_WIDTH = 360; +export const APPBAR_SEARCH_MAX_WIDTH = 720; -export const getAppbarSearchWidth = (availableWidth: number) => - availableWidth > APPBAR_SEARCH_WIDTH_BREAKPOINT - ? Math.max(APPBAR_SEARCH_WIDTH_BREAKPOINT, availableWidth / 2) - : availableWidth; +export const getAppbarSearchWidth = (availableWidth: number) => { + if (availableWidth < APPBAR_SEARCH_MIN_WIDTH) { + return availableWidth; + } + + return Math.min(availableWidth, APPBAR_SEARCH_MAX_WIDTH); +}; export const getAppbarHeight = ( variant: AppbarVariant, From a1b0a4debbd55842f0da488377da3ad05f49853d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Tue, 25 Aug 2026 08:19:10 +0200 Subject: [PATCH 13/34] feat(appbar): adjust v3 example --- example/src/Examples/AppbarV3Example.tsx | 61 +----------------------- 1 file changed, 2 insertions(+), 59 deletions(-) diff --git a/example/src/Examples/AppbarV3Example.tsx b/example/src/Examples/AppbarV3Example.tsx index 5d18a207a8..c3734b6712 100644 --- a/example/src/Examples/AppbarV3Example.tsx +++ b/example/src/Examples/AppbarV3Example.tsx @@ -11,16 +11,12 @@ import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { AppbarV3, - FAB, - IconButton, List, Palette, RadioButton, Snackbar, - Surface, Switch, Text, - useTheme, } from 'react-native-paper'; import type { AppbarV3Actions, @@ -38,8 +34,6 @@ type FilledActionVariant = AppbarV3FilledAction['variant']; type FilledActionWidth = NonNullable; const MORE_ICON = Platform.OS === 'ios' ? 'dots-horizontal' : 'dots-vertical'; -const BOTTOM_APPBAR_HEIGHT = 80; -const MEDIUM_FAB_HEIGHT = 56; type SearchAppbarHeaderProps = { actions: AppbarV3StandardAction[]; @@ -94,8 +88,7 @@ const AppbarV3Example = () => { const [isScrolled, setIsScrolled] = React.useState(false); const [showSnackbar, setShowSnackbar] = React.useState(false); - const theme = useTheme(); - const { bottom, left, right } = useSafeAreaInsets(); + const { bottom } = useSafeAreaInsets(); const { height: windowHeight } = useWindowDimensions(); const titleAlignment: AppbarV3TitleAlignment = isTitleCentered ? 'center' @@ -252,20 +245,6 @@ const AppbarV3Example = () => { [] ); - const renderFAB = () => ( - {}} - style={[ - styles.fab, - { - right: 16 + right, - top: (BOTTOM_APPBAR_HEIGHT - MEDIUM_FAB_HEIGHT) / 2, - }, - ]} - /> - ); - const renderDefaultOptions = () => ( <> @@ -341,7 +320,7 @@ const AppbarV3Example = () => { return ( <> { - - - {}} /> - {}} /> - {}} /> - {}} /> - - {renderFAB()} - setShowSnackbar(false)} @@ -480,21 +438,6 @@ const styles = StyleSheet.create({ paddingVertical: 8, paddingHorizontal: 16, }, - bottom: { - position: 'absolute', - left: 0, - right: 0, - bottom: 0, - }, - bottomActions: { - height: BOTTOM_APPBAR_HEIGHT, - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 4, - }, - fab: { - position: 'absolute', - }, customColor: { backgroundColor: Palette.secondary80, }, From 64cd517e8e362f3656bbac7bcc0ef3c98721a9ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Tue, 25 Aug 2026 08:29:00 +0200 Subject: [PATCH 14/34] refactor(appbar): remove code smells --- src/components/AppbarV3/AppbarAction.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/AppbarV3/AppbarAction.tsx b/src/components/AppbarV3/AppbarAction.tsx index 974aa395ed..6889d45948 100644 --- a/src/components/AppbarV3/AppbarAction.tsx +++ b/src/components/AppbarV3/AppbarAction.tsx @@ -4,14 +4,14 @@ import type { AppbarAction as AppbarActionConfig, AppbarLeadingAction, } from './types'; -import type { Theme, ThemeProp } from '../../types'; +import type { Theme } from '../../types'; import AppbarBackIcon from '../Appbar/AppbarBackIcon'; import IconButton from '../IconButton/IconButton'; type Props = { action: AppbarActionConfig | AppbarLeadingAction; leading?: boolean; - theme: ThemeProp; + theme: Theme; }; const AppbarAction = ({ action, leading = false, theme }: Props) => { @@ -27,7 +27,6 @@ const AppbarAction = ({ action, leading = false, theme }: Props) => { type?: 'back' | 'icon'; }; const isBackAction = 'type' in action && action.type === 'back'; - const resolvedTheme = theme as Theme; const mode = variant === 'filled' ? 'contained' @@ -39,13 +38,13 @@ const AppbarAction = ({ action, leading = false, theme }: Props) => { (mode ? undefined : leading - ? resolvedTheme.colors.onSurface - : resolvedTheme.colors.onSurfaceVariant); + ? theme.colors.onSurface + : theme.colors.onSurfaceVariant); return ( Date: Tue, 25 Aug 2026 09:20:35 +0200 Subject: [PATCH 15/34] feat(appbar): accessibility related adjustments --- src/components/AppbarV3/Appbar.tsx | 20 +++++--- src/components/AppbarV3/AppbarContent.tsx | 22 ++++++++- src/components/AppbarV3/index.ts | 1 + src/components/AppbarV3/types.ts | 56 ++++++++++++++++++++--- src/index.tsx | 1 + 5 files changed, 84 insertions(+), 16 deletions(-) diff --git a/src/components/AppbarV3/Appbar.tsx b/src/components/AppbarV3/Appbar.tsx index e5351f909a..69a208ee30 100644 --- a/src/components/AppbarV3/Appbar.tsx +++ b/src/components/AppbarV3/Appbar.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { StyleSheet, View } from 'react-native'; -import type { ColorValue, LayoutChangeEvent } from 'react-native'; +import type { ColorValue, LayoutChangeEvent, ViewProps } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -34,6 +34,7 @@ const Appbar = ({ testID = 'appbar', theme: themeOverrides, title, + titleActionProps, titleAlignment = 'leading', titleImage, titleProps, @@ -64,6 +65,14 @@ const Appbar = ({ const horizontalInset = Math.max(leftInset, rightInset); const borderRadius = getAppbarBorders(restStyle); const centered = titleAlignment === 'center'; + const { + accessibilityLabel: _accessibilityLabel, + accessibilityRole: _accessibilityRole, + accessible: _accessible, + 'aria-label': _ariaLabel, + role: _role, + ...viewProps + } = rest as ViewProps; const handleSearchSlotLayout = React.useCallback( ({ nativeEvent }: LayoutChangeEvent) => { @@ -83,11 +92,7 @@ const Appbar = ({ const renderActions = () => actions.map((action) => ( - + )); const renderSearchAppbar = () => { @@ -155,6 +160,7 @@ const Appbar = ({ testID: `${testID}-content`, theme, title, + titleActionProps, titleImage, titleProps, variant, @@ -267,7 +273,7 @@ const Appbar = ({ ]} > diff --git a/src/components/AppbarV3/AppbarContent.tsx b/src/components/AppbarV3/AppbarContent.tsx index a24a5df401..a1118a63c4 100644 --- a/src/components/AppbarV3/AppbarContent.tsx +++ b/src/components/AppbarV3/AppbarContent.tsx @@ -17,6 +17,7 @@ type Props = Pick< | 'subtitle' | 'subtitleProps' | 'title' + | 'titleActionProps' | 'titleImage' | 'titleProps' > & { @@ -54,6 +55,7 @@ const AppbarContent = ({ testID, theme, title, + titleActionProps, titleImage, titleProps, variant, @@ -80,7 +82,7 @@ const AppbarContent = ({ theme={theme} variant={titleVariant} numberOfLines={variant === 'small' ? 1 : 2} - role={onTitlePress ? 'none' : 'heading'} + role="heading" accessible maxFontSizeMultiplier={titleProps?.maxFontSizeMultiplier} testID={`${testID}-title-text`} @@ -126,11 +128,27 @@ const AppbarContent = ({ }; if (onTitlePress) { + const { + 'aria-label': ariaLabel, + accessibilityLabel, + disabled, + ...restTitleActionProps + } = titleActionProps ?? {}; + return ( {content} diff --git a/src/components/AppbarV3/index.ts b/src/components/AppbarV3/index.ts index 06fb7c8533..ec24e51cb1 100644 --- a/src/components/AppbarV3/index.ts +++ b/src/components/AppbarV3/index.ts @@ -8,6 +8,7 @@ export type { AppbarSearchbarProps, AppbarStandardAction, AppbarTextProps, + AppbarTitleActionProps, AppbarTitleAlignment, AppbarTitleTextProps, AppbarVariant, diff --git a/src/components/AppbarV3/types.ts b/src/components/AppbarV3/types.ts index 6f945f8156..e1356c45ef 100644 --- a/src/components/AppbarV3/types.ts +++ b/src/components/AppbarV3/types.ts @@ -38,12 +38,33 @@ export type AppbarTitleTextProps = AppbarTextProps & { ref?: React.RefObject; }; +export type AppbarTitleActionProps = Pick< + ViewProps, + | 'accessibilityActions' + | 'accessibilityHint' + | 'accessibilityLabel' + | 'accessibilityLabelledBy' + | 'accessibilityLanguage' + | 'accessibilityState' + | 'accessibilityValue' + | 'aria-busy' + | 'aria-disabled' + | 'aria-expanded' + | 'aria-label' + | 'aria-labelledby' + | 'aria-selected' + | 'onAccessibilityAction' +> & { + /** Whether the title action is disabled. */ + disabled?: boolean; +}; + type AppbarActionBase = Omit< IconButtonProps, 'icon' | 'iconColor' | 'mode' | 'selected' | 'size' | 'theme' | 'aria-label' > & { /** Stable key used when rendering an action from the actions array. */ - key?: React.Key; + key: React.Key; /** Icon displayed by the action. */ icon: IconSource; /** Accessible label announced for the icon button. */ @@ -71,17 +92,19 @@ export type AppbarActions = | readonly AppbarStandardAction[] | readonly [AppbarFilledAction]; -type AppbarLeadingIconAction = AppbarStandardAction & { +type AppbarLeadingIconAction = Omit & { + key?: never; type?: 'icon'; }; type AppbarBackAction = Omit< AppbarStandardAction, - 'aria-label' | 'icon' | 'type' + 'aria-label' | 'icon' | 'key' | 'type' > & { /** Uses the platform-aware Paper back icon. */ type: 'back'; icon?: never; + key?: never; 'aria-label'?: string; }; @@ -122,7 +145,16 @@ type AppbarFlexibleImageTitle = AppbarWrittenTitle & variant: Exclude; }; -type AppbarBaseProps = Omit & { +type AppbarBaseProps = Omit< + ViewProps, + | 'accessibilityLabel' + | 'accessibilityRole' + | 'accessible' + | 'aria-label' + | 'children' + | 'role' + | 'style' +> & { /** Optional leading navigation action. */ leadingAction?: AppbarLeadingAction; /** Uses the on-scroll container color when true. */ @@ -145,12 +177,21 @@ type AppbarTitleProps = { titleAlignment?: AppbarTitleAlignment; /** Trailing icon actions. */ actions?: AppbarActions; - /** Called when the title area is pressed. */ - onTitlePress?: (event: GestureResponderEvent) => void; /** Style applied to the title area. */ contentStyle?: StyleProp; searchBar?: never; -}; +} & ( + | { + /** Called when the title area is pressed. */ + onTitlePress: (event: GestureResponderEvent) => void; + /** Accessibility props applied to the interactive title area. */ + titleActionProps?: AppbarTitleActionProps; + } + | { + onTitlePress?: never; + titleActionProps?: never; + } +); export type AppbarSearchbarProps = Omit< SearchbarProps, @@ -173,6 +214,7 @@ type AppbarSearchProps = { titleProps?: never; subtitleProps?: never; onTitlePress?: never; + titleActionProps?: never; contentStyle?: never; }; diff --git a/src/index.tsx b/src/index.tsx index 1726a497ec..0f90db1c07 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -70,6 +70,7 @@ export type { AppbarSearchbarProps as AppbarV3SearchbarProps, AppbarStandardAction as AppbarV3StandardAction, AppbarTextProps as AppbarV3TextProps, + AppbarTitleActionProps as AppbarV3TitleActionProps, AppbarTitleAlignment as AppbarV3TitleAlignment, AppbarTitleTextProps as AppbarV3TitleTextProps, AppbarVariant as AppbarV3Variant, From 40d3947486d38e020d993b64906891f61c19b482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Tue, 25 Aug 2026 09:53:14 +0200 Subject: [PATCH 16/34] test(appbar): accessibility related tests --- .../__tests__/AppbarV3/AppbarV3.test.tsx | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 src/components/__tests__/AppbarV3/AppbarV3.test.tsx diff --git a/src/components/__tests__/AppbarV3/AppbarV3.test.tsx b/src/components/__tests__/AppbarV3/AppbarV3.test.tsx new file mode 100644 index 0000000000..287d449ed7 --- /dev/null +++ b/src/components/__tests__/AppbarV3/AppbarV3.test.tsx @@ -0,0 +1,259 @@ +import { Text, View } from 'react-native'; + +import { describe, expect, it, jest } from '@jest/globals'; + +import { render, screen } from '../../../test-utils'; +import AppbarV3 from '../../AppbarV3'; +import type { AppbarVariant } from '../../AppbarV3'; + +const writtenTitleVariants: Exclude[] = [ + 'small', + 'medium-flexible', + 'large-flexible', +]; + +const decorativeTitleImage = ( + + + + Brand words + + +); + +describe('AppbarV3 accessibility', () => { + it.each(writtenTitleVariants)( + 'exposes a written %s title as a heading', + async (variant) => { + await render(); + + expect(screen.getByRole('heading', { name: 'Inbox' })).toBeOnTheScreen(); + } + ); + + it('hides a small title image behind one named heading', async () => { + await render( + + ); + + expect(screen.getAllByRole('heading')).toHaveLength(1); + expect(screen.getByRole('heading', { name: 'Inbox' })).toBeOnTheScreen(); + expect(screen.queryByRole('img')).not.toBeOnTheScreen(); + expect(screen.queryByText('Brand words')).not.toBeOnTheScreen(); + }); + + it.each(['medium-flexible', 'large-flexible'] as const)( + 'does not expose duplicate content for a %s title image', + async (variant) => { + await render( + + ); + + expect(screen.getAllByRole('heading', { name: 'Inbox' })).toHaveLength(1); + expect(screen.queryByRole('img')).not.toBeOnTheScreen(); + expect(screen.queryByLabelText('Brand artwork')).not.toBeOnTheScreen(); + } + ); + + it('exposes a pressable written title as a named button and heading', async () => { + const onTitlePress = jest.fn(); + await render( + + ); + + expect(screen.getByRole('button', { name: 'Inbox' })).toBeOnTheScreen(); + expect(screen.getByRole('heading', { name: 'Inbox' })).toBeOnTheScreen(); + }); + + it('forwards custom title button accessibility props', async () => { + const onAccessibilityAction = jest.fn(); + await render( + {}} + titleActionProps={{ + accessibilityActions: [{ name: 'activate', label: 'Open inbox' }], + accessibilityHint: 'Opens the inbox menu', + accessibilityLabel: 'Inbox options', + accessibilityState: { busy: true, expanded: true }, + accessibilityValue: { text: 'Unread messages available' }, + onAccessibilityAction, + }} + /> + ); + + const titleButton = screen.getByRole('button', { + name: 'Inbox options', + busy: true, + expanded: true, + }); + + expect(titleButton).toHaveProp( + 'accessibilityActions', + expect.arrayContaining([ + expect.objectContaining({ name: 'activate', label: 'Open inbox' }), + ]) + ); + expect(titleButton).toHaveProp('accessibilityHint', 'Opens the inbox menu'); + expect(titleButton).toHaveAccessibilityValue({ + text: 'Unread messages available', + }); + expect(titleButton).toHaveProp( + 'onAccessibilityAction', + onAccessibilityAction + ); + }); + + it('uses default and custom labels for back actions', async () => { + await render( + + + + + ); + + expect(screen.getByRole('button', { name: 'Back' })).toBeOnTheScreen(); + expect( + screen.getByRole('button', { name: 'Return to inbox' }) + ).toBeOnTheScreen(); + }); + + it('preserves icon action labels, hints, disabled state, and busy state', async () => { + await render( + + ); + + const action = screen.getByRole('button', { + name: 'Save message', + busy: true, + disabled: true, + }); + + expect(action).toHaveProp('accessibilityHint', 'Saves the current draft'); + expect(action).toBeDisabled(); + expect(action).toBeBusy(); + }); + + it.each(['small', 'medium-flexible'] as const)( + 'hides decorative descendants of a %s title image', + async (variant) => { + await render( + variant === 'small' ? ( + + ) : ( + + ) + ); + + const imageContainer = screen.getByTestId('appbar-content-title-image', { + includeHiddenElements: true, + }); + + expect(imageContainer).toHaveProp('aria-hidden', true); + expect(imageContainer).toHaveProp( + 'importantForAccessibility', + 'no-hide-descendants' + ); + expect( + screen.queryByRole('heading', { name: 'Brand words' }) + ).not.toBeOnTheScreen(); + } + ); + + it('exposes leading, title, and trailing elements in source order', async () => { + await render( + + ); + + expect(screen.getAllByRole(/button|heading/)).toEqual([ + screen.getByRole('button', { name: 'Navigate back' }), + screen.getByRole('heading', { name: 'Inbox' }), + screen.getByRole('button', { name: 'Search inbox' }), + screen.getByRole('button', { name: 'More options' }), + ]); + }); + + it('does not remount a surviving action when its configuration changes', async () => { + const { rerender } = await render( + + ); + const survivingAction = screen.getByRole('button', { + name: 'Keep action', + }); + + await rerender( + + ); + + expect(screen.getByRole('button', { name: 'Keep action' })).toBe( + survivingAction + ); + }); +}); From 1b77102da6f7d91df69f802a4630cdf9ae2a87ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Tue, 25 Aug 2026 10:24:06 +0200 Subject: [PATCH 17/34] refactor(appbar): rename props to match M3 conventions --- example/src/Examples/AppbarV3Example.tsx | 158 +++++++++--------- src/components/AppbarV3/Appbar.tsx | 110 ++++++------ .../{AppbarAction.tsx => AppbarButton.tsx} | 25 ++- src/components/AppbarV3/AppbarContent.tsx | 90 +++++----- src/components/AppbarV3/index.ts | 16 +- src/components/AppbarV3/types.ts | 130 +++++++------- src/components/AppbarV3/utils.ts | 16 +- .../__tests__/AppbarV3/AppbarV3.test.tsx | 106 ++++++------ src/index.tsx | 16 +- 9 files changed, 347 insertions(+), 320 deletions(-) rename src/components/AppbarV3/{AppbarAction.tsx => AppbarButton.tsx} (67%) diff --git a/example/src/Examples/AppbarV3Example.tsx b/example/src/Examples/AppbarV3Example.tsx index c3734b6712..e737fcae64 100644 --- a/example/src/Examples/AppbarV3Example.tsx +++ b/example/src/Examples/AppbarV3Example.tsx @@ -19,33 +19,35 @@ import { Text, } from 'react-native-paper'; import type { - AppbarV3Actions, - AppbarV3FilledAction, - AppbarV3LeadingAction, - AppbarV3StandardAction, - AppbarV3TitleAlignment, + AppbarV3FilledTrailingAction, + AppbarV3HeadlineAlignment, + AppbarV3LeadingButton, + AppbarV3StandardTrailingAction, + AppbarV3TrailingActions, AppbarV3Variant, } from 'react-native-paper'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import ScreenWrapper from '../ScreenWrapper'; -type FilledActionVariant = AppbarV3FilledAction['variant']; -type FilledActionWidth = NonNullable; +type FilledTrailingActionVariant = AppbarV3FilledTrailingAction['variant']; +type FilledTrailingActionWidth = NonNullable< + AppbarV3FilledTrailingAction['width'] +>; const MORE_ICON = Platform.OS === 'ios' ? 'dots-horizontal' : 'dots-vertical'; type SearchAppbarHeaderProps = { - actions: AppbarV3StandardAction[]; + trailingActions: AppbarV3StandardTrailingAction[]; isScrolled: boolean; - leadingAction?: AppbarV3LeadingAction; + leadingButton?: AppbarV3LeadingButton; showCustomColor: boolean; }; const SearchAppbarHeader = ({ - actions, + trailingActions, isScrolled, - leadingAction, + leadingButton, showCustomColor, }: SearchAppbarHeaderProps) => { const [searchQuery, setSearchQuery] = React.useState(''); @@ -53,10 +55,10 @@ const SearchAppbarHeader = ({ return ( { const navigation = useNavigation('AppbarV3'); - const [showLeftIcon, setShowLeftIcon] = React.useState(true); + const [showLeadingButton, setShowLeadingButton] = React.useState(true); const [showSubtitle, setShowSubtitle] = React.useState(true); - const [showTitleImage, setShowTitleImage] = React.useState(false); + const [showHeadlineImage, setShowHeadlineImage] = React.useState(false); const [showSearchIcon, setShowSearchIcon] = React.useState(true); const [showMoreIcon, setShowMoreIcon] = React.useState(true); const [showCustomColor, setShowCustomColor] = React.useState(false); const [appbarConfiguration, setAppbarConfiguration] = React.useState('small'); - const [isTitleCentered, setIsTitleCentered] = React.useState(false); + const [isHeadlineCentered, setIsHeadlineCentered] = React.useState(false); const [showCalendarIcon, setShowCalendarIcon] = React.useState(false); - const [showFilledAction, setShowFilledAction] = React.useState(false); - const [filledActionVariant, setFilledActionVariant] = - React.useState('filled'); - const [filledActionWidth, setFilledActionWidth] = - React.useState('default'); + const [showFilledTrailingAction, setShowFilledTrailingAction] = + React.useState(false); + const [filledTrailingActionVariant, setFilledTrailingActionVariant] = + React.useState('filled'); + const [filledTrailingActionWidth, setFilledTrailingActionWidth] = + React.useState('default'); const [isScrolled, setIsScrolled] = React.useState(false); const [showSnackbar, setShowSnackbar] = React.useState(false); const { bottom } = useSafeAreaInsets(); const { height: windowHeight } = useWindowDimensions(); - const titleAlignment: AppbarV3TitleAlignment = isTitleCentered + const headlineAlignment: AppbarV3HeadlineAlignment = isHeadlineCentered ? 'center' : 'leading'; React.useLayoutEffect(() => { - const standardActions: AppbarV3StandardAction[] = []; + const standardTrailingActions: AppbarV3StandardTrailingAction[] = []; if (showCalendarIcon) { - standardActions.push({ + standardTrailingActions.push({ key: 'calendar', icon: 'calendar', 'aria-label': 'Calendar', @@ -107,7 +110,7 @@ const AppbarV3Example = () => { } if (showSearchIcon && appbarConfiguration !== 'search') { - standardActions.push({ + standardTrailingActions.push({ key: 'search', icon: 'magnify', 'aria-label': 'Search', @@ -116,7 +119,7 @@ const AppbarV3Example = () => { } if (showMoreIcon) { - standardActions.push({ + standardTrailingActions.push({ key: 'more', icon: MORE_ICON, 'aria-label': 'More options', @@ -124,40 +127,40 @@ const AppbarV3Example = () => { }); } - const actions: AppbarV3Actions = showFilledAction + const trailingActions: AppbarV3TrailingActions = showFilledTrailingAction ? [ { key: 'share', icon: 'share-variant', 'aria-label': 'Share', - variant: filledActionVariant, - width: filledActionWidth, + variant: filledTrailingActionVariant, + width: filledTrailingActionWidth, onPress: () => {}, }, ] - : standardActions; + : standardTrailingActions; - const leadingAction = showLeftIcon + const leadingButton = showLeadingButton ? { type: 'back' as const, onPress: () => navigation.goBack(), } : undefined; - const titleImage = ( + const headlineImage = ( ); const commonProps = { - actions, + headlineAlignment, isScrolled, - leadingAction, - onTitlePress: () => setShowSnackbar(true), + leadingButton, + onHeadlinePress: () => setShowSnackbar(true), style: showCustomColor ? styles.customColor : null, - titleAlignment, + trailingActions, }; navigation.setOptions({ @@ -165,27 +168,27 @@ const AppbarV3Example = () => { if (appbarConfiguration === 'search') { return ( ); } if (appbarConfiguration === 'small') { - return showTitleImage ? ( + return showHeadlineImage ? ( ) : ( { ); } - return showTitleImage ? ( + return showHeadlineImage ? ( { { }); }, [ appbarConfiguration, - filledActionVariant, - filledActionWidth, + filledTrailingActionVariant, + filledTrailingActionWidth, navigation, showCalendarIcon, showCustomColor, - showFilledAction, - showLeftIcon, + showFilledTrailingAction, + showLeadingButton, showMoreIcon, isScrolled, showSearchIcon, showSubtitle, - showTitleImage, - titleAlignment, + showHeadlineImage, + headlineAlignment, ]); const handleScroll = React.useCallback( @@ -248,8 +251,11 @@ const AppbarV3Example = () => { const renderDefaultOptions = () => ( <> - Left icon - + Leading button + Subtitle @@ -257,40 +263,42 @@ const AppbarV3Example = () => { value={showSubtitle} disabled={ appbarConfiguration === 'search' || - (showTitleImage && appbarConfiguration === 'small') + (showHeadlineImage && appbarConfiguration === 'small') } onValueChange={setShowSubtitle} /> - Title image + Headline image - Center title and subtitle + Center headline and subtitle Filled trailing action Search icon @@ -298,7 +306,7 @@ const AppbarV3Example = () => { More icon @@ -306,7 +314,7 @@ const AppbarV3Example = () => { Calendar icon @@ -339,14 +347,14 @@ const AppbarV3Example = () => { {renderDefaultOptions()} - {showFilledAction ? ( - + {showFilledTrailingAction ? ( + Style { if (value === 'filled' || value === 'tonal') { - setFilledActionVariant(value); + setFilledTrailingActionVariant(value); } }} > @@ -361,10 +369,10 @@ const AppbarV3Example = () => { Width { if (value === 'default' || value === 'wide') { - setFilledActionWidth(value); + setFilledTrailingActionWidth(value); } }} > @@ -417,7 +425,7 @@ const AppbarV3Example = () => { onDismiss={() => setShowSnackbar(false)} duration={Snackbar.DURATION_SHORT} > - Heading pressed + Headline pressed ); @@ -441,7 +449,7 @@ const styles = StyleSheet.create({ customColor: { backgroundColor: Palette.secondary80, }, - titleImage: { + headlineImage: { width: 32, height: 32, }, diff --git a/src/components/AppbarV3/Appbar.tsx b/src/components/AppbarV3/Appbar.tsx index 69a208ee30..b663f852b7 100644 --- a/src/components/AppbarV3/Appbar.tsx +++ b/src/components/AppbarV3/Appbar.tsx @@ -4,15 +4,15 @@ import type { ColorValue, LayoutChangeEvent, ViewProps } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import AppbarAction from './AppbarAction'; +import AppbarButton from './AppbarButton'; import AppbarContent from './AppbarContent'; import type { Props } from './types'; import { - APPBAR_ACTION_SIZE, - APPBAR_TITLE_IMAGE_HEIGHT, - getActionsWidth, + APPBAR_HEADLINE_IMAGE_HEIGHT, + APPBAR_ICON_BUTTON_SIZE, getAppbarHeight, getAppbarSearchWidth, + getTrailingActionsWidth, } from './utils'; import { useInternalTheme } from '../../core/theming'; import { getAppbarBorders } from '../Appbar/utils'; @@ -20,11 +20,15 @@ import Searchbar from '../Searchbar'; import Surface from '../Surface'; const Appbar = ({ - actions = [], contentStyle, + headline, + headlineAlignment = 'leading', + headlineImage, + headlinePressableProps, + headlineProps, isScrolled = false, - leadingAction, - onTitlePress, + leadingButton, + onHeadlinePress, safeAreaInsets, searchBar, statusBarHeight, @@ -33,11 +37,7 @@ const Appbar = ({ subtitleProps, testID = 'appbar', theme: themeOverrides, - title, - titleActionProps, - titleAlignment = 'leading', - titleImage, - titleProps, + trailingActions = [], variant, ref, ...rest @@ -64,7 +64,7 @@ const Appbar = ({ const rightInset = safeAreaInsets?.right ?? detectedInsets.right; const horizontalInset = Math.max(leftInset, rightInset); const borderRadius = getAppbarBorders(restStyle); - const centered = titleAlignment === 'center'; + const centered = headlineAlignment === 'center'; const { accessibilityLabel: _accessibilityLabel, accessibilityRole: _accessibilityRole, @@ -85,14 +85,14 @@ const Appbar = ({ [] ); - const renderLeadingAction = () => - leadingAction ? ( - + const renderLeadingButton = () => + leadingButton ? ( + ) : null; - const renderActions = () => - actions.map((action) => ( - + const renderTrailingActions = () => + trailingActions.map((action) => ( + )); const renderSearchAppbar = () => { @@ -115,7 +115,7 @@ const Appbar = ({ return ( - {renderLeadingAction()} + {renderLeadingButton()} - {renderActions()} + {renderTrailingActions()} ); }; @@ -152,30 +152,30 @@ const Appbar = ({ const contentProps = variant !== 'search' ? { - alignment: titleAlignment, + alignment: headlineAlignment, contentStyle, - onTitlePress, + headline, + headlineImage, + headlinePressableProps, + headlineProps, + onHeadlinePress, subtitle, subtitleProps, testID: `${testID}-content`, theme, - title, - titleActionProps, - titleImage, - titleProps, variant, } : null; - const renderFlexibleTitleImage = () => - titleImage ? ( + const renderFlexibleHeadlineImage = () => + headlineImage ? ( - {titleImage} + {headlineImage} ) : null; @@ -184,22 +184,22 @@ const Appbar = ({ return null; } - if (centered || titleImage) { + if (centered || headlineImage) { const sideWidth = Math.max( - leadingAction ? APPBAR_ACTION_SIZE : 0, - getActionsWidth(actions) + leadingButton ? APPBAR_ICON_BUTTON_SIZE : 0, + getTrailingActionsWidth(trailingActions) ); return ( - {renderLeadingAction()} + {renderLeadingButton()} - {renderActions()} + {renderTrailingActions()} ); @@ -207,12 +207,14 @@ const Appbar = ({ return ( - {renderLeadingAction()} + {renderLeadingButton()} - {renderActions()} + {renderTrailingActions()} ); }; @@ -223,18 +225,18 @@ const Appbar = ({ } const sideWidth = Math.max( - leadingAction ? APPBAR_ACTION_SIZE : 0, - getActionsWidth(actions) + leadingButton ? APPBAR_ICON_BUTTON_SIZE : 0, + getTrailingActionsWidth(trailingActions) ); return ( - {titleImage ? ( + {headlineImage ? ( - {renderLeadingAction()} + {renderLeadingButton()} - {renderFlexibleTitleImage()} + {renderFlexibleHeadlineImage()} - {renderActions()} + {renderTrailingActions()} ) : ( - {renderLeadingAction()} - {renderActions()} + {renderLeadingButton()} + + {renderTrailingActions()} + )} - + ); }; @@ -328,15 +332,15 @@ const styles = StyleSheet.create({ side: { flexDirection: 'row', }, - titleLeading: { + headlineLeading: { marginStart: 12, }, - titleWithLeading: { + headlineWithLeading: { marginStart: 4, }, - flexibleTitleImage: { + flexibleHeadlineImage: { flex: 1, - height: APPBAR_TITLE_IMAGE_HEIGHT, + height: APPBAR_HEADLINE_IMAGE_HEIGHT, maxWidth: '100%', marginTop: 12, alignItems: 'center', diff --git a/src/components/AppbarV3/AppbarAction.tsx b/src/components/AppbarV3/AppbarButton.tsx similarity index 67% rename from src/components/AppbarV3/AppbarAction.tsx rename to src/components/AppbarV3/AppbarButton.tsx index 6889d45948..251190aa91 100644 --- a/src/components/AppbarV3/AppbarAction.tsx +++ b/src/components/AppbarV3/AppbarButton.tsx @@ -1,20 +1,17 @@ import { StyleSheet } from 'react-native'; -import type { - AppbarAction as AppbarActionConfig, - AppbarLeadingAction, -} from './types'; +import type { AppbarLeadingButton, AppbarTrailingAction } from './types'; import type { Theme } from '../../types'; import AppbarBackIcon from '../Appbar/AppbarBackIcon'; import IconButton from '../IconButton/IconButton'; type Props = { - action: AppbarActionConfig | AppbarLeadingAction; + button: AppbarTrailingAction | AppbarLeadingButton; leading?: boolean; theme: Theme; }; -const AppbarAction = ({ action, leading = false, theme }: Props) => { +const AppbarButton = ({ button, leading = false, theme }: Props) => { const { color, key: _key, @@ -22,11 +19,11 @@ const AppbarAction = ({ action, leading = false, theme }: Props) => { variant = 'standard', width, ...rest - } = action; + } = button; const { type: _type, ...buttonProps } = rest as typeof rest & { type?: 'back' | 'icon'; }; - const isBackAction = 'type' in action && action.type === 'back'; + const isBackButton = 'type' in button && button.type === 'back'; const mode = variant === 'filled' ? 'contained' @@ -44,28 +41,28 @@ const AppbarAction = ({ action, leading = false, theme }: Props) => { return ( ); }; const styles = StyleSheet.create({ - action: { + button: { margin: 4, }, - wideAction: { + wideButton: { width: 56, }, }); -export default AppbarAction; +export default AppbarButton; diff --git a/src/components/AppbarV3/AppbarContent.tsx b/src/components/AppbarV3/AppbarContent.tsx index a1118a63c4..bba1db657e 100644 --- a/src/components/AppbarV3/AppbarContent.tsx +++ b/src/components/AppbarV3/AppbarContent.tsx @@ -2,98 +2,98 @@ import { Pressable, StyleSheet, View } from 'react-native'; import type { StyleProp, ViewStyle } from 'react-native'; import type { - AppbarTitleAlignment, - AppbarTitleVariant, + AppbarHeadlineAlignment, + AppbarHeadlineVariant, Props as AppbarProps, } from './types'; -import { APPBAR_TITLE_IMAGE_HEIGHT } from './utils'; +import { APPBAR_HEADLINE_IMAGE_HEIGHT } from './utils'; import type { Theme, TypescaleKey } from '../../types'; import Text from '../Typography/Text'; type Props = Pick< AppbarProps, | 'contentStyle' - | 'onTitlePress' + | 'headline' + | 'headlineImage' + | 'headlinePressableProps' + | 'headlineProps' + | 'onHeadlinePress' | 'subtitle' | 'subtitleProps' - | 'title' - | 'titleActionProps' - | 'titleImage' - | 'titleProps' > & { - alignment: AppbarTitleAlignment; + alignment: AppbarHeadlineAlignment; theme: Theme; - variant: AppbarTitleVariant; + variant: AppbarHeadlineVariant; style?: StyleProp; testID: string; }; -const titleVariants = { +const headlineVariants = { small: 'titleLarge', 'medium-flexible': 'headlineMedium', 'large-flexible': 'displaySmall', -} as const satisfies Record; +} as const satisfies Record; const subtitleVariants = { small: 'labelMedium', 'medium-flexible': 'labelLarge', 'large-flexible': 'titleMedium', -} as const satisfies Record; +} as const satisfies Record; const subtitleSpacing = { small: 0, 'medium-flexible': 4, 'large-flexible': 8, -} as const satisfies Record; +} as const satisfies Record; const AppbarContent = ({ alignment, contentStyle, - onTitlePress, + headline, + headlineImage, + headlinePressableProps, + headlineProps, + onHeadlinePress, subtitle, subtitleProps, testID, theme, - title, - titleActionProps, - titleImage, - titleProps, variant, style, }: Props) => { - const titleVariant = titleVariants[variant]; + const headlineVariant = headlineVariants[variant]; const subtitleVariant = subtitleVariants[variant]; const centered = alignment === 'center'; - const hasTitleImage = variant === 'small' && Boolean(titleImage); + const hasHeadlineImage = variant === 'small' && Boolean(headlineImage); - const content = hasTitleImage ? ( + const content = hasHeadlineImage ? ( - {titleImage} + {headlineImage} ) : ( <> - {title} + {headline} {subtitle ? ( {content} @@ -159,9 +161,9 @@ const AppbarContent = ({ return ( {content} @@ -189,8 +191,8 @@ const styles = StyleSheet.create({ centeredText: { textAlign: 'center', }, - titleImage: { - height: APPBAR_TITLE_IMAGE_HEIGHT, + headlineImage: { + height: APPBAR_HEADLINE_IMAGE_HEIGHT, maxWidth: '100%', alignItems: 'center', justifyContent: 'center', diff --git a/src/components/AppbarV3/index.ts b/src/components/AppbarV3/index.ts index ec24e51cb1..d8ba839263 100644 --- a/src/components/AppbarV3/index.ts +++ b/src/components/AppbarV3/index.ts @@ -1,16 +1,16 @@ export { default } from './Appbar'; export { Appbar } from './Appbar'; export type { - AppbarAction, - AppbarActions, - AppbarFilledAction, - AppbarLeadingAction, + AppbarFilledTrailingAction, + AppbarHeadlineAlignment, + AppbarHeadlinePressableProps, + AppbarHeadlineTextProps, + AppbarLeadingButton, AppbarSearchbarProps, - AppbarStandardAction, + AppbarStandardTrailingAction, AppbarTextProps, - AppbarTitleActionProps, - AppbarTitleAlignment, - AppbarTitleTextProps, + AppbarTrailingAction, + AppbarTrailingActions, AppbarVariant, Props, } from './types'; diff --git a/src/components/AppbarV3/types.ts b/src/components/AppbarV3/types.ts index e1356c45ef..4696a7bb27 100644 --- a/src/components/AppbarV3/types.ts +++ b/src/components/AppbarV3/types.ts @@ -22,9 +22,9 @@ export type AppbarVariant = | 'medium-flexible' | 'large-flexible'; -export type AppbarTitleVariant = Exclude; +export type AppbarHeadlineVariant = Exclude; -export type AppbarTitleAlignment = 'leading' | 'center'; +export type AppbarHeadlineAlignment = 'leading' | 'center'; export type AppbarTextProps = { /** Style applied to the text. */ @@ -33,12 +33,12 @@ export type AppbarTextProps = { maxFontSizeMultiplier?: number; }; -export type AppbarTitleTextProps = AppbarTextProps & { - /** Reference for the title heading. */ +export type AppbarHeadlineTextProps = AppbarTextProps & { + /** Reference for the headline heading. */ ref?: React.RefObject; }; -export type AppbarTitleActionProps = Pick< +export type AppbarHeadlinePressableProps = Pick< ViewProps, | 'accessibilityActions' | 'accessibilityHint' @@ -55,50 +55,52 @@ export type AppbarTitleActionProps = Pick< | 'aria-selected' | 'onAccessibilityAction' > & { - /** Whether the title action is disabled. */ + /** Whether the headline interaction is disabled. */ disabled?: boolean; }; -type AppbarActionBase = Omit< +type AppbarTrailingActionBase = Omit< IconButtonProps, 'icon' | 'iconColor' | 'mode' | 'selected' | 'size' | 'theme' | 'aria-label' > & { - /** Stable key used when rendering an action from the actions array. */ + /** Stable key used when rendering an action from the trailing actions array. */ key: React.Key; - /** Icon displayed by the action. */ + /** Icon displayed by the trailing action. */ icon: IconSource; /** Accessible label announced for the icon button. */ 'aria-label': string; - /** Custom action icon color. */ + /** Custom trailing action icon color. */ color?: ColorValue; }; -export type AppbarStandardAction = AppbarActionBase & { +export type AppbarStandardTrailingAction = AppbarTrailingActionBase & { variant?: 'standard'; width?: never; }; -export type AppbarFilledAction = AppbarActionBase & { - /** Filled primary and tonal actions use the corresponding IconButton colors. */ +export type AppbarFilledTrailingAction = AppbarTrailingActionBase & { + /** Filled primary and tonal icons use the corresponding IconButton colors. */ variant: 'filled' | 'tonal'; - /** Expressive filled actions can use the default or wide container. */ + /** Expressive filled icons can use the default or wide container. */ width?: 'default' | 'wide'; }; -export type AppbarAction = AppbarStandardAction | AppbarFilledAction; +export type AppbarTrailingAction = + | AppbarStandardTrailingAction + | AppbarFilledTrailingAction; -/** A filled action replaces the standard action group and is valid on its own. */ -export type AppbarActions = - | readonly AppbarStandardAction[] - | readonly [AppbarFilledAction]; +/** A filled trailing action replaces the standard action group and is valid on its own. */ +export type AppbarTrailingActions = + | readonly AppbarStandardTrailingAction[] + | readonly [AppbarFilledTrailingAction]; -type AppbarLeadingIconAction = Omit & { +type AppbarLeadingIconButton = Omit & { key?: never; type?: 'icon'; }; -type AppbarBackAction = Omit< - AppbarStandardAction, +type AppbarBackButton = Omit< + AppbarStandardTrailingAction, 'aria-label' | 'icon' | 'key' | 'type' > & { /** Uses the platform-aware Paper back icon. */ @@ -108,41 +110,41 @@ type AppbarBackAction = Omit< 'aria-label'?: string; }; -export type AppbarLeadingAction = AppbarLeadingIconAction | AppbarBackAction; +export type AppbarLeadingButton = AppbarLeadingIconButton | AppbarBackButton; -type AppbarWrittenTitle = { +type AppbarWrittenHeadline = { /** Written headline displayed by the app bar. */ - title: string; + headline: string; /** Optional supporting text displayed below the headline. */ subtitle?: string; - /** Props applied to the title heading. */ - titleProps?: AppbarTitleTextProps; + /** Props applied to the headline heading. */ + headlineProps?: AppbarHeadlineTextProps; /** Props applied to the subtitle text. */ subtitleProps?: AppbarTextProps; }; -type AppbarTitleImage = { +type AppbarHeadlineImage = { /** Image or logo displayed in the app bar. It should fit within 32dp height. */ - titleImage: React.ReactElement; + headlineImage: React.ReactElement; }; -type AppbarTextTitle = AppbarWrittenTitle & { - variant: AppbarTitleVariant; - titleImage?: never; +type AppbarTextHeadline = AppbarWrittenHeadline & { + variant: AppbarHeadlineVariant; + headlineImage?: never; }; -type AppbarSmallImageTitle = AppbarTitleImage & { +type AppbarSmallImageHeadline = AppbarHeadlineImage & { variant: 'small'; - /** Accessible page title. The image replaces this text visually. */ - title: string; + /** Accessible page headline. The image replaces this text visually. */ + headline: string; subtitle?: never; - titleProps?: never; + headlineProps?: never; subtitleProps?: never; }; -type AppbarFlexibleImageTitle = AppbarWrittenTitle & - AppbarTitleImage & { - variant: Exclude; +type AppbarFlexibleImageHeadline = AppbarWrittenHeadline & + AppbarHeadlineImage & { + variant: Exclude; }; type AppbarBaseProps = Omit< @@ -155,8 +157,8 @@ type AppbarBaseProps = Omit< | 'role' | 'style' > & { - /** Optional leading navigation action. */ - leadingAction?: AppbarLeadingAction; + /** Optional leading button. */ + leadingButton?: AppbarLeadingButton; /** Uses the on-scroll container color when true. */ isScrolled?: boolean; /** Override for the automatic top safe-area inset. */ @@ -172,24 +174,24 @@ type AppbarBaseProps = Omit< theme?: ThemeProp; }; -type AppbarTitleProps = { +type AppbarHeadlineProps = { /** Headline and subtitle alignment. */ - titleAlignment?: AppbarTitleAlignment; - /** Trailing icon actions. */ - actions?: AppbarActions; - /** Style applied to the title area. */ + headlineAlignment?: AppbarHeadlineAlignment; + /** Trailing actions. */ + trailingActions?: AppbarTrailingActions; + /** Style applied to the headline and subtitle area. */ contentStyle?: StyleProp; searchBar?: never; } & ( | { - /** Called when the title area is pressed. */ - onTitlePress: (event: GestureResponderEvent) => void; - /** Accessibility props applied to the interactive title area. */ - titleActionProps?: AppbarTitleActionProps; + /** Called when the headline area is pressed. */ + onHeadlinePress: (event: GestureResponderEvent) => void; + /** Props applied to the interactive headline area. */ + headlinePressableProps?: AppbarHeadlinePressableProps; } | { - onTitlePress?: never; - titleActionProps?: never; + onHeadlinePress?: never; + headlinePressableProps?: never; } ); @@ -205,22 +207,26 @@ type AppbarSearchProps = { variant: 'search'; /** Props forwarded to the existing Paper Searchbar. */ searchBar: AppbarSearchbarProps; - /** Exterior trailing icon actions. */ - actions?: readonly AppbarStandardAction[]; - title?: never; + /** Exterior trailing actions. */ + trailingActions?: readonly AppbarStandardTrailingAction[]; + headline?: never; subtitle?: never; - titleImage?: never; - titleAlignment?: never; - titleProps?: never; + headlineImage?: never; + headlineAlignment?: never; + headlineProps?: never; subtitleProps?: never; - onTitlePress?: never; - titleActionProps?: never; + onHeadlinePress?: never; + headlinePressableProps?: never; contentStyle?: never; }; export type Props = AppbarBaseProps & ( - | (AppbarTitleProps & - (AppbarTextTitle | AppbarSmallImageTitle | AppbarFlexibleImageTitle)) + | (AppbarHeadlineProps & + ( + | AppbarTextHeadline + | AppbarSmallImageHeadline + | AppbarFlexibleImageHeadline + )) | AppbarSearchProps ); diff --git a/src/components/AppbarV3/utils.ts b/src/components/AppbarV3/utils.ts index 219f0d6cca..6ab0c7b02f 100644 --- a/src/components/AppbarV3/utils.ts +++ b/src/components/AppbarV3/utils.ts @@ -1,8 +1,8 @@ -import type { AppbarAction, AppbarVariant } from './types'; +import type { AppbarTrailingAction, AppbarVariant } from './types'; -export const APPBAR_ACTION_SIZE = 48; -export const APPBAR_WIDE_ACTION_SIZE = 64; -export const APPBAR_TITLE_IMAGE_HEIGHT = 32; +export const APPBAR_ICON_BUTTON_SIZE = 48; +export const APPBAR_WIDE_ICON_BUTTON_SIZE = 64; +export const APPBAR_HEADLINE_IMAGE_HEIGHT = 32; export const APPBAR_SEARCH_MIN_WIDTH = 360; export const APPBAR_SEARCH_MAX_WIDTH = 720; @@ -29,12 +29,14 @@ export const getAppbarHeight = ( return hasSubtitle ? 152 : 120; }; -export const getActionsWidth = (actions: readonly AppbarAction[]) => +export const getTrailingActionsWidth = ( + actions: readonly AppbarTrailingAction[] +) => actions.reduce( (width, action) => width + (action.variant !== 'standard' && action.width === 'wide' - ? APPBAR_WIDE_ACTION_SIZE - : APPBAR_ACTION_SIZE), + ? APPBAR_WIDE_ICON_BUTTON_SIZE + : APPBAR_ICON_BUTTON_SIZE), 0 ); diff --git a/src/components/__tests__/AppbarV3/AppbarV3.test.tsx b/src/components/__tests__/AppbarV3/AppbarV3.test.tsx index 287d449ed7..89bb874239 100644 --- a/src/components/__tests__/AppbarV3/AppbarV3.test.tsx +++ b/src/components/__tests__/AppbarV3/AppbarV3.test.tsx @@ -6,13 +6,13 @@ import { render, screen } from '../../../test-utils'; import AppbarV3 from '../../AppbarV3'; import type { AppbarVariant } from '../../AppbarV3'; -const writtenTitleVariants: Exclude[] = [ +const writtenHeadlineVariants: Exclude[] = [ 'small', 'medium-flexible', 'large-flexible', ]; -const decorativeTitleImage = ( +const decorativeHeadlineImage = ( @@ -22,21 +22,21 @@ const decorativeTitleImage = ( ); describe('AppbarV3 accessibility', () => { - it.each(writtenTitleVariants)( - 'exposes a written %s title as a heading', + it.each(writtenHeadlineVariants)( + 'exposes a written %s headline as a heading', async (variant) => { - await render(); + await render(); expect(screen.getByRole('heading', { name: 'Inbox' })).toBeOnTheScreen(); } ); - it('hides a small title image behind one named heading', async () => { + it('hides a small headline image behind one named heading', async () => { await render( ); @@ -47,13 +47,13 @@ describe('AppbarV3 accessibility', () => { }); it.each(['medium-flexible', 'large-flexible'] as const)( - 'does not expose duplicate content for a %s title image', + 'does not expose duplicate content for a %s headline image', async (variant) => { await render( ); @@ -63,24 +63,28 @@ describe('AppbarV3 accessibility', () => { } ); - it('exposes a pressable written title as a named button and heading', async () => { - const onTitlePress = jest.fn(); + it('exposes a pressable written headline as a named button and heading', async () => { + const onHeadlinePress = jest.fn(); await render( - + ); expect(screen.getByRole('button', { name: 'Inbox' })).toBeOnTheScreen(); expect(screen.getByRole('heading', { name: 'Inbox' })).toBeOnTheScreen(); }); - it('forwards custom title button accessibility props', async () => { + it('forwards custom headline button accessibility props', async () => { const onAccessibilityAction = jest.fn(); await render( {}} - titleActionProps={{ + headline="Inbox" + onHeadlinePress={() => {}} + headlinePressableProps={{ accessibilityActions: [{ name: 'activate', label: 'Open inbox' }], accessibilityHint: 'Opens the inbox menu', accessibilityLabel: 'Inbox options', @@ -113,20 +117,20 @@ describe('AppbarV3 accessibility', () => { ); }); - it('uses default and custom labels for back actions', async () => { + it('uses default and custom labels for back buttons', async () => { await render( ); @@ -137,12 +141,12 @@ describe('AppbarV3 accessibility', () => { ).toBeOnTheScreen(); }); - it('preserves icon action labels, hints, disabled state, and busy state', async () => { + it('preserves trailing action labels, hints, disabled state, and busy state', async () => { await render( { /> ); - const action = screen.getByRole('button', { + const trailingAction = screen.getByRole('button', { name: 'Save message', busy: true, disabled: true, }); - expect(action).toHaveProp('accessibilityHint', 'Saves the current draft'); - expect(action).toBeDisabled(); - expect(action).toBeBusy(); + expect(trailingAction).toHaveProp( + 'accessibilityHint', + 'Saves the current draft' + ); + expect(trailingAction).toBeDisabled(); + expect(trailingAction).toBeBusy(); }); it.each(['small', 'medium-flexible'] as const)( - 'hides decorative descendants of a %s title image', + 'hides decorative descendants of a %s headline image', async (variant) => { await render( variant === 'small' ? ( ) : ( ) ); - const imageContainer = screen.getByTestId('appbar-content-title-image', { - includeHiddenElements: true, - }); + const imageContainer = screen.getByTestId( + 'appbar-content-headline-image', + { includeHiddenElements: true } + ); expect(imageContainer).toHaveProp('aria-hidden', true); expect(imageContainer).toHaveProp( @@ -200,13 +208,13 @@ describe('AppbarV3 accessibility', () => { } ); - it('exposes leading, title, and trailing elements in source order', async () => { + it('exposes the leading button, headline, and trailing actions in source order', async () => { await render( { ]); }); - it('does not remount a surviving action when its configuration changes', async () => { + it('does not remount a surviving trailing action when its configuration changes', async () => { const { rerender } = await render( ); - const survivingAction = screen.getByRole('button', { + const survivingTrailingAction = screen.getByRole('button', { name: 'Keep action', }); await rerender( { ); expect(screen.getByRole('button', { name: 'Keep action' })).toBe( - survivingAction + survivingTrailingAction ); }); }); diff --git a/src/index.tsx b/src/index.tsx index 0f90db1c07..a7b8e58f80 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -63,16 +63,16 @@ export type { Props as AppbarContentProps } from './components/Appbar/AppbarCont export type { Props as AppbarHeaderProps } from './components/Appbar/AppbarHeader'; export type { Props as AppbarV3Props, - AppbarAction as AppbarV3Action, - AppbarActions as AppbarV3Actions, - AppbarFilledAction as AppbarV3FilledAction, - AppbarLeadingAction as AppbarV3LeadingAction, + AppbarFilledTrailingAction as AppbarV3FilledTrailingAction, + AppbarHeadlineAlignment as AppbarV3HeadlineAlignment, + AppbarHeadlinePressableProps as AppbarV3HeadlinePressableProps, + AppbarHeadlineTextProps as AppbarV3HeadlineTextProps, + AppbarLeadingButton as AppbarV3LeadingButton, AppbarSearchbarProps as AppbarV3SearchbarProps, - AppbarStandardAction as AppbarV3StandardAction, + AppbarStandardTrailingAction as AppbarV3StandardTrailingAction, AppbarTextProps as AppbarV3TextProps, - AppbarTitleActionProps as AppbarV3TitleActionProps, - AppbarTitleAlignment as AppbarV3TitleAlignment, - AppbarTitleTextProps as AppbarV3TitleTextProps, + AppbarTrailingAction as AppbarV3TrailingAction, + AppbarTrailingActions as AppbarV3TrailingActions, AppbarVariant as AppbarV3Variant, } from './components/AppbarV3'; export type { Props as AvatarIconProps } from './components/Avatar/AvatarIcon'; From 5b5c4a53dcb5d865a644d94c172d198e22598391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Tue, 25 Aug 2026 11:03:30 +0200 Subject: [PATCH 18/34] test(appbar): appbar test suite --- .../__tests__/AppbarV3/AppbarV3.test.tsx | 433 +++++++++++++++++- .../__tests__/AppbarV3/utils.test.ts | 69 +++ 2 files changed, 501 insertions(+), 1 deletion(-) create mode 100644 src/components/__tests__/AppbarV3/utils.test.ts diff --git a/src/components/__tests__/AppbarV3/AppbarV3.test.tsx b/src/components/__tests__/AppbarV3/AppbarV3.test.tsx index 89bb874239..a86d3d1a8b 100644 --- a/src/components/__tests__/AppbarV3/AppbarV3.test.tsx +++ b/src/components/__tests__/AppbarV3/AppbarV3.test.tsx @@ -1,8 +1,11 @@ +import * as React from 'react'; import { Text, View } from 'react-native'; import { describe, expect, it, jest } from '@jest/globals'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; -import { render, screen } from '../../../test-utils'; +import { getTheme } from '../../../core/theming'; +import { fireEvent, render, screen, userEvent } from '../../../test-utils'; import AppbarV3 from '../../AppbarV3'; import type { AppbarVariant } from '../../AppbarV3'; @@ -21,6 +24,401 @@ const decorativeHeadlineImage = ( ); +describe('AppbarV3 content', () => { + it.each([ + { + variant: 'small', + headlineVariant: 'titleLarge', + headlineLines: 1, + subtitleVariant: 'labelMedium', + subtitleSpacing: 0, + }, + { + variant: 'medium-flexible', + headlineVariant: 'headlineMedium', + headlineLines: 2, + subtitleVariant: 'labelLarge', + subtitleSpacing: 4, + }, + { + variant: 'large-flexible', + headlineVariant: 'displaySmall', + headlineLines: 2, + subtitleVariant: 'titleMedium', + subtitleSpacing: 8, + }, + ] as const)( + 'uses the $variant content treatment', + async ({ + variant, + headlineVariant, + headlineLines, + subtitleVariant, + subtitleSpacing, + }) => { + await render( + + ); + + expect(screen.getByTestId('appbar-content-headline-text')).toHaveStyle( + getTheme().fonts[headlineVariant] + ); + expect(screen.getByTestId('appbar-content-headline-text')).toHaveProp( + 'numberOfLines', + headlineLines + ); + expect(screen.getByTestId('appbar-content-subtitle-text')).toHaveStyle({ + ...getTheme().fonts[subtitleVariant], + color: getTheme().colors.onSurfaceVariant, + marginTop: subtitleSpacing, + }); + } + ); + + it('centers text and balances asymmetric controls for centered and image layouts', async () => { + const { rerender } = await render( + + ); + + expect(screen.getByTestId('appbar-content-headline-text')).toHaveStyle({ + textAlign: 'center', + }); + expect(screen.getByTestId('appbar-content-subtitle-text')).toHaveStyle({ + textAlign: 'center', + }); + expect( + screen.getByTestId('leading-action-container-outer-layer').parent + ).toHaveStyle({ width: 96 }); + expect( + screen.getByTestId('trailing-action-container-outer-layer').parent + ).toHaveStyle({ width: 96 }); + + await rerender( + } + leadingButton={{ + type: 'back', + testID: 'leading-action', + }} + trailingActions={[ + { + key: 'more', + icon: 'dots-vertical', + 'aria-label': 'More options', + testID: 'trailing-action', + }, + { + key: 'search', + icon: 'magnify', + 'aria-label': 'Search inbox', + }, + ]} + /> + ); + + expect( + screen.getByTestId('leading-action-container-outer-layer').parent + ).toHaveStyle({ width: 96 }); + expect( + screen.getByTestId('trailing-action-container-outer-layer').parent + ).toHaveStyle({ width: 96 }); + expect( + screen.getByTestId('brand-mark', { includeHiddenElements: true }) + ).toBeOnTheScreen(); + }); + + it('adjusts leading headline spacing when a leading button is present', async () => { + const { rerender } = await render( + + ); + + expect(screen.getByTestId('appbar-content')).toHaveStyle({ + marginStart: 12, + }); + + await rerender( + + ); + + expect(screen.getByTestId('appbar-content')).toHaveStyle({ + marginStart: 4, + }); + }); +}); + +describe('AppbarV3 surface', () => { + it('uses scroll container colors unless a custom background is supplied', async () => { + const customBackground = 'rebeccapurple'; + const { rerender } = await render( + + ); + + expect(screen.getByTestId('appbar-root-layer')).toHaveStyle({ + backgroundColor: getTheme().colors.surface, + }); + + await rerender(); + + expect(screen.getByTestId('appbar-root-layer')).toHaveStyle({ + backgroundColor: getTheme().colors.surfaceContainer, + }); + + await rerender( + + ); + + expect(screen.getByTestId('appbar-root-layer')).toHaveStyle({ + backgroundColor: customBackground, + }); + }); + + it('applies border clipping and resolves safe-area overrides on the surface', async () => { + await render( + + + + ); + + expect(screen.getByTestId('appbar-root-layer')).toHaveStyle({ + borderBottomLeftRadius: 16, + borderBottomRightRadius: 16, + paddingTop: 20, + paddingHorizontal: 12, + }); + }); +}); + +describe('AppbarV3 actions', () => { + it('maps leading, trailing, and custom action colors', async () => { + await render( + ( + + ), + 'aria-label': 'Navigation', + }} + trailingActions={[ + { + key: 'default', + icon: ({ color }) => ( + + ), + 'aria-label': 'Default action', + }, + { + key: 'custom', + icon: ({ color }) => ( + + ), + 'aria-label': 'Custom action', + color: 'rebeccapurple', + }, + ]} + /> + ); + + expect(screen.getByTestId('leading-icon-color')).toHaveStyle({ + backgroundColor: getTheme().colors.onSurface, + }); + expect(screen.getByTestId('trailing-icon-color')).toHaveStyle({ + backgroundColor: getTheme().colors.onSurfaceVariant, + }); + expect(screen.getByTestId('custom-icon-color')).toHaveStyle({ + backgroundColor: 'rebeccapurple', + }); + }); + + it.each([ + { variant: 'filled', color: getTheme().colors.primary }, + { variant: 'tonal', color: getTheme().colors.secondaryContainer }, + ] as const)( + 'maps $variant actions to their selected container color', + async ({ variant, color }) => { + await render( + + ); + + expect(screen.getByTestId('expressive-action-container')).toHaveStyle({ + backgroundColor: color, + }); + expect( + screen.getByTestId('expressive-action-container-outer-layer') + ).toHaveStyle({ width: 56 }); + } + ); +}); + +describe('AppbarV3 search', () => { + it('uses the placeholder as the searchbox label unless an explicit label is supplied', async () => { + const { rerender } = await render( + + ); + + expect( + screen.getByRole('searchbox', { name: 'Search messages' }) + ).toBeOnTheScreen(); + expect(screen.getByTestId('appbar-search-container')).toHaveStyle({ + backgroundColor: getTheme().colors.surfaceContainer, + }); + + await rerender( + + ); + + expect( + screen.getByRole('searchbox', { name: 'Message search' }) + ).toBeOnTheScreen(); + }); + + it('configures the search field, forwards its behavior, and constrains its measured width', async () => { + const onChangeText = jest.fn(); + const SearchAppbar = () => { + const [value, setValue] = React.useState(''); + + return ( + { + setValue(nextValue); + onChangeText(nextValue); + }, + testID: 'message-search', + }} + trailingActions={[ + { + key: 'more', + icon: 'dots-vertical', + 'aria-label': 'More options', + }, + ]} + /> + ); + }; + + await render(); + + const searchbox = screen.getByRole('searchbox', { + name: 'Search messages', + }); + expect(searchbox).toHaveStyle({ + color: getTheme().colors.onSurface, + textAlign: 'center', + }); + expect(searchbox).toHaveProp( + 'placeholderTextColor', + getTheme().colors.onSurfaceVariant + ); + expect(screen.getByTestId('message-search-container')).toHaveStyle({ + backgroundColor: getTheme().colors.surfaceContainerHighest, + }); + expect( + screen.getByRole('button', { name: 'Navigate back' }) + ).toBeOnTheScreen(); + expect( + screen.getByRole('button', { name: 'More options' }) + ).toBeOnTheScreen(); + + await userEvent.type(searchbox, 'draft'); + expect(onChangeText).toHaveBeenLastCalledWith('draft'); + expect(searchbox).toHaveProp('value', 'draft'); + + await fireEvent(screen.getByTestId('appbar-search-slot'), 'layout', { + nativeEvent: { layout: { x: 0, y: 0, width: 800, height: 56 } }, + }); + + expect( + screen.getByTestId('message-search-container-outer-layer') + ).toHaveStyle({ width: 720 }); + }); +}); + describe('AppbarV3 accessibility', () => { it.each(writtenHeadlineVariants)( 'exposes a written %s headline as a heading', @@ -77,6 +475,39 @@ describe('AppbarV3 accessibility', () => { expect(screen.getByRole('heading', { name: 'Inbox' })).toBeOnTheScreen(); }); + it('does not invoke a disabled headline action', async () => { + const onHeadlinePress = jest.fn(); + await render( + + ); + + const headlineButton = screen.getByRole('button', { + name: 'Inbox', + disabled: true, + }); + await userEvent.press(headlineButton); + + expect(onHeadlinePress).not.toHaveBeenCalled(); + }); + + it('names a pressable small headline image from its fallback headline', async () => { + await render( + {}} + /> + ); + + expect(screen.getByRole('button', { name: 'Inbox' })).toBeOnTheScreen(); + }); + it('forwards custom headline button accessibility props', async () => { const onAccessibilityAction = jest.fn(); await render( diff --git a/src/components/__tests__/AppbarV3/utils.test.ts b/src/components/__tests__/AppbarV3/utils.test.ts new file mode 100644 index 0000000000..b3f0ee8e94 --- /dev/null +++ b/src/components/__tests__/AppbarV3/utils.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from '@jest/globals'; + +import { + getAppbarHeight, + getAppbarSearchWidth, + getTrailingActionsWidth, +} from '../../AppbarV3/utils'; + +describe('getAppbarHeight', () => { + it.each([ + { variant: 'search', subtitle: false, height: 64 }, + { variant: 'search', subtitle: true, height: 64 }, + { variant: 'small', subtitle: false, height: 64 }, + { variant: 'small', subtitle: true, height: 64 }, + { variant: 'medium-flexible', subtitle: false, height: 112 }, + { variant: 'medium-flexible', subtitle: true, height: 136 }, + { variant: 'large-flexible', subtitle: false, height: 120 }, + { variant: 'large-flexible', subtitle: true, height: 152 }, + ] as const)( + 'returns $height for $variant with subtitle=$subtitle', + ({ variant, subtitle, height }) => { + expect(getAppbarHeight(variant, subtitle)).toBe(height); + } + ); +}); + +describe('getAppbarSearchWidth', () => { + it.each([ + [320, 320], + [360, 360], + [600, 600], + [800, 720], + ])('constrains %i available pixels to %i', (availableWidth, width) => { + expect(getAppbarSearchWidth(availableWidth)).toBe(width); + }); +}); + +describe('getTrailingActionsWidth', () => { + it('accounts for standard, filled, and wide filled actions', () => { + expect(getTrailingActionsWidth([])).toBe(0); + expect( + getTrailingActionsWidth([ + { key: 'first', icon: 'star', 'aria-label': 'First' }, + { key: 'second', icon: 'heart', 'aria-label': 'Second' }, + ]) + ).toBe(96); + expect( + getTrailingActionsWidth([ + { + key: 'filled', + icon: 'star', + 'aria-label': 'Filled', + variant: 'filled', + }, + ]) + ).toBe(48); + expect( + getTrailingActionsWidth([ + { + key: 'wide', + icon: 'star', + 'aria-label': 'Wide', + variant: 'tonal', + width: 'wide', + }, + ]) + ).toBe(64); + }); +}); From ae3553849b3b1d1374daa72a86b55c4ecc7679b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Tue, 25 Aug 2026 12:01:18 +0200 Subject: [PATCH 19/34] refactor(appbar): optimizations --- src/components/AppbarV3/Appbar.tsx | 198 +++++++++--------- src/components/AppbarV3/AppbarButton.tsx | 38 +++- src/components/AppbarV3/AppbarContent.tsx | 55 +++-- src/components/AppbarV3/utils.ts | 9 - .../__tests__/AppbarV3/AppbarV3.test.tsx | 41 +++- .../__tests__/AppbarV3/utils.test.ts | 17 +- 6 files changed, 204 insertions(+), 154 deletions(-) diff --git a/src/components/AppbarV3/Appbar.tsx b/src/components/AppbarV3/Appbar.tsx index b663f852b7..4d756ca44f 100644 --- a/src/components/AppbarV3/Appbar.tsx +++ b/src/components/AppbarV3/Appbar.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { StyleSheet, View } from 'react-native'; -import type { ColorValue, LayoutChangeEvent, ViewProps } from 'react-native'; +import type { ColorValue, ViewProps } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -10,8 +10,8 @@ import type { Props } from './types'; import { APPBAR_HEADLINE_IMAGE_HEIGHT, APPBAR_ICON_BUTTON_SIZE, + APPBAR_SEARCH_MAX_WIDTH, getAppbarHeight, - getAppbarSearchWidth, getTrailingActionsWidth, } from './utils'; import { useInternalTheme } from '../../core/theming'; @@ -19,6 +19,8 @@ import { getAppbarBorders } from '../Appbar/utils'; import Searchbar from '../Searchbar'; import Surface from '../Surface'; +const EMPTY_TRAILING_ACTIONS = [] as const; + const Appbar = ({ contentStyle, headline, @@ -37,23 +39,30 @@ const Appbar = ({ subtitleProps, testID = 'appbar', theme: themeOverrides, - trailingActions = [], + trailingActions = EMPTY_TRAILING_ACTIONS, variant, ref, ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); const detectedInsets = useSafeAreaInsets(); - const [searchSlotWidth, setSearchSlotWidth] = React.useState(0); - const flattenedStyle = StyleSheet.flatten(style); - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - const resolvedStyle = (flattenedStyle || {}) as Exclude< - typeof flattenedStyle, - number - > & { - backgroundColor?: ColorValue; - }; - const { backgroundColor: customBackground, ...restStyle } = resolvedStyle; + const { customBackground, restStyle, borderRadius } = React.useMemo(() => { + const flattenedStyle = StyleSheet.flatten(style); + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const resolvedStyle = (flattenedStyle || {}) as Exclude< + typeof flattenedStyle, + number + > & { + backgroundColor?: ColorValue; + }; + const { backgroundColor, ...remainingStyle } = resolvedStyle; + + return { + customBackground: backgroundColor, + restStyle: remainingStyle, + borderRadius: getAppbarBorders(remainingStyle), + }; + }, [style]); const backgroundColor = customBackground ?? (isScrolled ? theme.colors.surfaceContainer : theme.colors.surface); @@ -63,8 +72,46 @@ const Appbar = ({ const leftInset = safeAreaInsets?.left ?? detectedInsets.left; const rightInset = safeAreaInsets?.right ?? detectedInsets.right; const horizontalInset = Math.max(leftInset, rightInset); - const borderRadius = getAppbarBorders(restStyle); const centered = headlineAlignment === 'center'; + const sideWidth = Math.max( + leadingButton ? APPBAR_ICON_BUTTON_SIZE : 0, + getTrailingActionsWidth(trailingActions) + ); + const sideStyle = React.useMemo(() => ({ width: sideWidth }), [sideWidth]); + const surfaceStyle = React.useMemo( + () => [ + { + backgroundColor, + paddingTop: topInset, + paddingHorizontal: horizontalInset, + }, + borderRadius, + ], + [backgroundColor, borderRadius, horizontalInset, topInset] + ); + const appbarStyle = React.useMemo( + () => [styles.appbar, { backgroundColor, minHeight }, restStyle], + [backgroundColor, minHeight, restStyle] + ); + const resolvedSearchInputStyle = React.useMemo( + () => [ + searchBar?.inputStyle, + styles.searchInput, + { color: theme.colors.onSurface }, + ], + [searchBar?.inputStyle, theme.colors.onSurface] + ); + const searchBackgroundColor = isScrolled + ? theme.colors.surfaceContainerHighest + : theme.colors.surfaceContainer; + const resolvedSearchStyle = React.useMemo( + () => [ + styles.searchBar, + { backgroundColor: searchBackgroundColor }, + searchBar?.style, + ], + [searchBackgroundColor, searchBar?.style] + ); const { accessibilityLabel: _accessibilityLabel, accessibilityRole: _accessibilityRole, @@ -74,17 +121,6 @@ const Appbar = ({ ...viewProps } = rest as ViewProps; - const handleSearchSlotLayout = React.useCallback( - ({ nativeEvent }: LayoutChangeEvent) => { - const nextWidth = nativeEvent.layout.width; - - setSearchSlotWidth((currentWidth) => - currentWidth === nextWidth ? currentWidth : nextWidth - ); - }, - [] - ); - const renderLeadingButton = () => leadingButton ? ( @@ -101,48 +137,35 @@ const Appbar = ({ } const { - inputStyle: searchInputStyle, - style: searchStyle, + inputStyle: _searchInputStyle, + style: _searchStyle, testID: searchTestID = `${testID}-search`, ...searchProps } = searchBar; - const searchWidth = searchSlotWidth - ? getAppbarSearchWidth(searchSlotWidth) - : '100%'; - const searchBackgroundColor = isScrolled - ? theme.colors.surfaceContainerHighest - : theme.colors.surfaceContainer; return ( {renderLeadingButton()} - - + + + + {renderTrailingActions()} @@ -185,20 +208,11 @@ const Appbar = ({ } if (centered || headlineImage) { - const sideWidth = Math.max( - leadingButton ? APPBAR_ICON_BUTTON_SIZE : 0, - getTrailingActionsWidth(trailingActions) - ); - return ( - - {renderLeadingButton()} - + {renderLeadingButton()} - + {renderTrailingActions()} @@ -224,26 +238,15 @@ const Appbar = ({ return null; } - const sideWidth = Math.max( - leadingButton ? APPBAR_ICON_BUTTON_SIZE : 0, - getTrailingActionsWidth(trailingActions) - ); - return ( {headlineImage ? ( - + {renderLeadingButton()} {renderFlexibleHeadlineImage()} - + {renderTrailingActions()} @@ -267,20 +270,9 @@ const Appbar = ({ elevation={0} container theme={theme} - style={[ - { - backgroundColor, - paddingTop: topInset, - paddingHorizontal: horizontalInset, - }, - borderRadius, - ]} + style={surfaceStyle} > - + {variant === 'search' ? renderSearchAppbar() : variant === 'small' @@ -311,7 +303,11 @@ const styles = StyleSheet.create({ alignItems: 'center', }, searchBar: { - maxWidth: '100%', + width: '100%', + }, + searchWidthLimiter: { + width: '100%', + maxWidth: APPBAR_SEARCH_MAX_WIDTH, }, searchInput: { textAlign: 'center', @@ -349,7 +345,9 @@ const styles = StyleSheet.create({ }, }); -export default Appbar; +const MemoizedAppbar = React.memo(Appbar); + +export default MemoizedAppbar; // @component-docs ignore-next-line -export { Appbar }; +export { MemoizedAppbar as Appbar }; diff --git a/src/components/AppbarV3/AppbarButton.tsx b/src/components/AppbarV3/AppbarButton.tsx index 251190aa91..b6e7d942aa 100644 --- a/src/components/AppbarV3/AppbarButton.tsx +++ b/src/components/AppbarV3/AppbarButton.tsx @@ -1,3 +1,4 @@ +import * as React from 'react'; import { StyleSheet } from 'react-native'; import type { AppbarLeadingButton, AppbarTrailingAction } from './types'; @@ -56,6 +57,41 @@ const AppbarButton = ({ button, leading = false, theme }: Props) => { ); }; +const hasEqualButtonProps = ( + previous: Props['button'], + next: Props['button'] +) => { + if (previous === next) { + return true; + } + + const previousKeys = Object.keys(previous); + const nextKeys = Object.keys(next); + + if (previousKeys.length !== nextKeys.length) { + return false; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const previousRecord = previous as unknown as Record; + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const nextRecord = next as unknown as Record; + + return previousKeys.every( + (key) => + Object.prototype.hasOwnProperty.call(nextRecord, key) && + Object.is(previousRecord[key], nextRecord[key]) + ); +}; + +const MemoizedAppbarButton = React.memo( + AppbarButton, + (previous, next) => + previous.leading === next.leading && + previous.theme === next.theme && + hasEqualButtonProps(previous.button, next.button) +); + const styles = StyleSheet.create({ button: { margin: 4, @@ -65,4 +101,4 @@ const styles = StyleSheet.create({ }, }); -export default AppbarButton; +export default MemoizedAppbarButton; diff --git a/src/components/AppbarV3/AppbarContent.tsx b/src/components/AppbarV3/AppbarContent.tsx index bba1db657e..ecc54513ae 100644 --- a/src/components/AppbarV3/AppbarContent.tsx +++ b/src/components/AppbarV3/AppbarContent.tsx @@ -1,3 +1,4 @@ +import * as React from 'react'; import { Pressable, StyleSheet, View } from 'react-native'; import type { StyleProp, ViewStyle } from 'react-native'; @@ -65,6 +66,35 @@ const AppbarContent = ({ const subtitleVariant = subtitleVariants[variant]; const centered = alignment === 'center'; const hasHeadlineImage = variant === 'small' && Boolean(headlineImage); + const headlineStyle = React.useMemo( + () => [ + styles.text, + centered && styles.centeredText, + { color: theme.colors.onSurface }, + headlineProps?.style, + ], + [centered, headlineProps?.style, theme.colors.onSurface] + ); + const subtitleStyle = React.useMemo( + () => [ + styles.text, + centered && styles.centeredText, + { marginTop: subtitleSpacing[variant] }, + { color: theme.colors.onSurfaceVariant }, + subtitleProps?.style, + ], + [centered, subtitleProps?.style, theme.colors.onSurfaceVariant, variant] + ); + const wrapperStyle = React.useMemo( + () => [ + styles.container, + variant !== 'small' && styles.flexibleContainer, + centered && styles.centeredContainer, + style, + contentStyle, + ], + [centered, contentStyle, style, variant] + ); const content = hasHeadlineImage ? ( {headline} @@ -102,13 +127,7 @@ const AppbarContent = ({ numberOfLines={1} maxFontSizeMultiplier={subtitleProps?.maxFontSizeMultiplier} testID={`${testID}-subtitle-text`} - style={[ - styles.text, - centered && styles.centeredText, - { marginTop: subtitleSpacing[variant] }, - { color: theme.colors.onSurfaceVariant }, - subtitleProps?.style, - ]} + style={subtitleStyle} > {subtitle} @@ -118,13 +137,7 @@ const AppbarContent = ({ const wrapperProps = { testID, - style: [ - styles.container, - variant !== 'small' && styles.flexibleContainer, - centered && styles.centeredContainer, - style, - contentStyle, - ], + style: wrapperStyle, }; if (onHeadlinePress) { @@ -200,4 +213,4 @@ const styles = StyleSheet.create({ }, }); -export default AppbarContent; +export default React.memo(AppbarContent); diff --git a/src/components/AppbarV3/utils.ts b/src/components/AppbarV3/utils.ts index 6ab0c7b02f..4677ffc43c 100644 --- a/src/components/AppbarV3/utils.ts +++ b/src/components/AppbarV3/utils.ts @@ -3,17 +3,8 @@ import type { AppbarTrailingAction, AppbarVariant } from './types'; export const APPBAR_ICON_BUTTON_SIZE = 48; export const APPBAR_WIDE_ICON_BUTTON_SIZE = 64; export const APPBAR_HEADLINE_IMAGE_HEIGHT = 32; -export const APPBAR_SEARCH_MIN_WIDTH = 360; export const APPBAR_SEARCH_MAX_WIDTH = 720; -export const getAppbarSearchWidth = (availableWidth: number) => { - if (availableWidth < APPBAR_SEARCH_MIN_WIDTH) { - return availableWidth; - } - - return Math.min(availableWidth, APPBAR_SEARCH_MAX_WIDTH); -}; - export const getAppbarHeight = ( variant: AppbarVariant, hasSubtitle: boolean diff --git a/src/components/__tests__/AppbarV3/AppbarV3.test.tsx b/src/components/__tests__/AppbarV3/AppbarV3.test.tsx index a86d3d1a8b..fc0ceb397b 100644 --- a/src/components/__tests__/AppbarV3/AppbarV3.test.tsx +++ b/src/components/__tests__/AppbarV3/AppbarV3.test.tsx @@ -5,7 +5,7 @@ import { describe, expect, it, jest } from '@jest/globals'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { getTheme } from '../../../core/theming'; -import { fireEvent, render, screen, userEvent } from '../../../test-utils'; +import { render, screen, userEvent } from '../../../test-utils'; import AppbarV3 from '../../AppbarV3'; import type { AppbarVariant } from '../../AppbarV3'; @@ -236,6 +236,33 @@ describe('AppbarV3 surface', () => { }); describe('AppbarV3 actions', () => { + it('skips rendering unchanged action configurations', async () => { + const icon = jest.fn(() => ); + const { rerender } = await render( + + ); + const initialRenderCount = icon.mock.calls.length; + + await rerender( + + ); + + expect(screen.getByTestId('stable-action-icon')).toBeOnTheScreen(); + expect(icon).toHaveBeenCalledTimes(initialRenderCount); + }); + it('maps leading, trailing, and custom action colors', async () => { await render( { ).toBeOnTheScreen(); }); - it('configures the search field, forwards its behavior, and constrains its measured width', async () => { + it('configures the search field, forwards its behavior, and constrains its width', async () => { const onChangeText = jest.fn(); const SearchAppbar = () => { const [value, setValue] = React.useState(''); @@ -409,13 +436,13 @@ describe('AppbarV3 search', () => { expect(onChangeText).toHaveBeenLastCalledWith('draft'); expect(searchbox).toHaveProp('value', 'draft'); - await fireEvent(screen.getByTestId('appbar-search-slot'), 'layout', { - nativeEvent: { layout: { x: 0, y: 0, width: 800, height: 56 } }, - }); - expect( screen.getByTestId('message-search-container-outer-layer') - ).toHaveStyle({ width: 720 }); + ).toHaveStyle({ width: '100%' }); + expect(screen.getByTestId('appbar-search-width-limiter')).toHaveStyle({ + width: '100%', + maxWidth: 720, + }); }); }); diff --git a/src/components/__tests__/AppbarV3/utils.test.ts b/src/components/__tests__/AppbarV3/utils.test.ts index b3f0ee8e94..d37a585372 100644 --- a/src/components/__tests__/AppbarV3/utils.test.ts +++ b/src/components/__tests__/AppbarV3/utils.test.ts @@ -1,10 +1,6 @@ import { describe, expect, it } from '@jest/globals'; -import { - getAppbarHeight, - getAppbarSearchWidth, - getTrailingActionsWidth, -} from '../../AppbarV3/utils'; +import { getAppbarHeight, getTrailingActionsWidth } from '../../AppbarV3/utils'; describe('getAppbarHeight', () => { it.each([ @@ -24,17 +20,6 @@ describe('getAppbarHeight', () => { ); }); -describe('getAppbarSearchWidth', () => { - it.each([ - [320, 320], - [360, 360], - [600, 600], - [800, 720], - ])('constrains %i available pixels to %i', (availableWidth, width) => { - expect(getAppbarSearchWidth(availableWidth)).toBe(width); - }); -}); - describe('getTrailingActionsWidth', () => { it('accounts for standard, filled, and wide filled actions', () => { expect(getTrailingActionsWidth([])).toBe(0); From 19b043ad318f9efb098faf7542a10a5d317eb481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Tue, 25 Aug 2026 12:42:12 +0200 Subject: [PATCH 20/34] feat(appbar): replace v2 with v3 + docs + example app update --- docs/6.x/docs/components/Appbar/Appbar.mdx | 199 ++-- .../docs/components/Appbar/AppbarAction.mdx | 134 --- .../components/Appbar/AppbarBackAction.mdx | 101 -- .../docs/components/Appbar/AppbarContent.mdx | 129 --- .../docs/components/Appbar/AppbarHeader.mdx | 125 --- docs/6.x/docs/components/Appbar/_meta.json | 6 +- docs/6.x/docs/guides/react-navigation.md | 65 +- .../guides/theming-with-react-navigation.md | 23 +- docs/6.x/docs/guides/theming.mdx | 6 +- docs/component-docs.config.ts | 4 - docs/src/data/componentDocs6x.json | 610 +---------- docs/src/data/screenshots.ts | 10 - docs/src/data/themeColors.ts | 23 +- example/src/ExampleList.tsx | 2 - example/src/Examples/AppbarExample.tsx | 453 ++++++-- example/src/Examples/AppbarV3Example.tsx | 456 -------- .../src/Examples/BottomNavigationExample.tsx | 86 +- example/src/Examples/MenuExample.tsx | 57 +- example/src/Examples/TeamDetails.tsx | 13 +- example/src/Examples/TooltipExample.tsx | 45 +- example/src/RootNavigator.tsx | 26 +- src/components/Appbar/Appbar.tsx | 641 +++++++----- src/components/Appbar/AppbarAction.tsx | 117 --- src/components/Appbar/AppbarBackAction.tsx | 79 -- .../{AppbarV3 => Appbar}/AppbarButton.tsx | 2 +- src/components/Appbar/AppbarContent.tsx | 368 ++++--- src/components/Appbar/AppbarHeader.tsx | 164 --- src/components/Appbar/index.ts | 38 +- src/components/{AppbarV3 => Appbar}/types.ts | 0 src/components/Appbar/utils.ts | 191 +--- src/components/AppbarV3/Appbar.tsx | 353 ------- src/components/AppbarV3/AppbarContent.tsx | 216 ---- src/components/AppbarV3/index.ts | 16 - src/components/AppbarV3/utils.ts | 33 - .../__tests__/Appbar/Appbar.test.tsx | 985 +++++++++++------- .../Appbar/__snapshots__/Appbar.test.tsx.snap | 881 ---------------- .../{AppbarV3 => Appbar}/utils.test.ts | 2 +- .../__tests__/AppbarV3/AppbarV3.test.tsx | 725 ------------- src/index.tsx | 32 +- 39 files changed, 1872 insertions(+), 5544 deletions(-) delete mode 100644 docs/6.x/docs/components/Appbar/AppbarAction.mdx delete mode 100644 docs/6.x/docs/components/Appbar/AppbarBackAction.mdx delete mode 100644 docs/6.x/docs/components/Appbar/AppbarContent.mdx delete mode 100644 docs/6.x/docs/components/Appbar/AppbarHeader.mdx delete mode 100644 example/src/Examples/AppbarV3Example.tsx delete mode 100644 src/components/Appbar/AppbarAction.tsx delete mode 100644 src/components/Appbar/AppbarBackAction.tsx rename src/components/{AppbarV3 => Appbar}/AppbarButton.tsx (97%) delete mode 100644 src/components/Appbar/AppbarHeader.tsx rename src/components/{AppbarV3 => Appbar}/types.ts (100%) delete mode 100644 src/components/AppbarV3/Appbar.tsx delete mode 100644 src/components/AppbarV3/AppbarContent.tsx delete mode 100644 src/components/AppbarV3/index.ts delete mode 100644 src/components/AppbarV3/utils.ts delete mode 100644 src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap rename src/components/__tests__/{AppbarV3 => Appbar}/utils.test.ts (98%) delete mode 100644 src/components/__tests__/AppbarV3/AppbarV3.test.tsx diff --git a/docs/6.x/docs/components/Appbar/Appbar.mdx b/docs/6.x/docs/components/Appbar/Appbar.mdx index 798b6dd136..0338f367ff 100644 --- a/docs/6.x/docs/components/Appbar/Appbar.mdx +++ b/docs/6.x/docs/components/Appbar/Appbar.mdx @@ -8,95 +8,100 @@ import ThemeColorsTable from '@docs/components/ThemeColorsTable.tsx'; import ScreenshotTabs from '@docs/components/ScreenshotTabs.tsx'; import ExtendedExample from '@docs/components/ExtendedExample.tsx'; -A component to display action items in a bar. It can be placed at the top or bottom. -The top bar usually contains the screen title, controls such as navigation buttons, menu button etc. -The bottom bar usually provides access to a drawer and up to four actions. - -By default Appbar 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 informations - - - - - - - +A Material Design app bar for displaying a page headline, navigation, and +contextual actions. Appbar supports small, medium flexible, large flexible, +and search variants. Its surface color automatically changes when content +has scrolled, and it accounts for safe-area insets. + +## Variants + +| `variant` | Purpose | +| --- | --- | +| `small` | A compact 64dp app bar with a one-line headline. | +| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. | +| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. | +| `search` | An app bar containing a centered Paper `Searchbar`. | + +## Main props + +| Prop | Description | +| --- | --- | +| `headline` | Required accessible headline for every non-search variant. | +| `subtitle` | Optional supporting text for written headlines. | +| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. | +| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. | +| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. | +| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. | +| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`. | +| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. | +| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. | +| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. | +| `statusBarHeight` | Overrides only the automatic top inset. | +| `contentStyle` | Styles the headline and subtitle area. | +| `style` | Styles the app bar and can override its background color. | + +## Migrating from the compound API + +`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and +`Appbar.BackAction` have been removed. Render one `Appbar` and provide its +headline, leading button, and trailing actions as props. The former +`mode="medium"` and `mode="large"` values are now +`variant="medium-flexible"` and `variant="large-flexible"`; centered +content uses `headlineAlignment="center"`. ## Usage -### Top bar + +### Small app bar ```js import * as React from 'react'; import { Appbar } from 'react-native-paper'; const MyComponent = () => ( - - {}} /> - - {}} /> - {}} /> - + {} }} + trailingActions={[ + { + key: 'search', + icon: 'magnify', + 'aria-label': 'Search', + onPress: () => {}, + }, + { + key: 'more', + icon: 'dots-vertical', + 'aria-label': 'More options', + onPress: () => {}, + }, + ]} + /> ); export default MyComponent; ``` -### Bottom bar +### Flexible app bar ```js -import * as React from 'react'; -import { StyleSheet } from 'react-native'; -import { Appbar, FAB, useTheme } from 'react-native-paper'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -const BOTTOM_APPBAR_HEIGHT = 80; -const MEDIUM_FAB_HEIGHT = 56; - -const MyComponent = () => { - const { bottom } = useSafeAreaInsets(); - const theme = useTheme(); - - return ( - - {}} /> - {}} /> - {}} /> - {}} /> - {}} - style={[ - styles.fab, - { top: (BOTTOM_APPBAR_HEIGHT - MEDIUM_FAB_HEIGHT) / 2 }, - ]} - /> - - ); -}; - -const styles = StyleSheet.create({ - bottom: { - backgroundColor: 'aquamarine', - position: 'absolute', - left: 0, - right: 0, - bottom: 0, - }, - fab: { - position: 'absolute', - right: 16, - }, -}); + +``` -export default MyComponent; +### Search app bar +```js + {} }} + searchBar={{ + placeholder: 'Search messages', + value: query, + onChangeText: setQuery, + }} +/> ``` @@ -107,59 +112,35 @@ export default MyComponent;
-### dark - -
- - - -
- -### children (required) - -
- - - -
- -### mode Available in v5.x with theme version 3 - -
- - - -
- -### elevated Available in v5.x with theme version 3 +### headlineAlignment
- +
-### safeAreaInsets +### isScrolled
- +
-### theme +### testID
- +
-### style +### trailingActions
- + @@ -168,7 +149,7 @@ export default MyComponent; ## Theme colors - + diff --git a/docs/6.x/docs/components/Appbar/AppbarAction.mdx b/docs/6.x/docs/components/Appbar/AppbarAction.mdx deleted file mode 100644 index 5db23d4ba9..0000000000 --- a/docs/6.x/docs/components/Appbar/AppbarAction.mdx +++ /dev/null @@ -1,134 +0,0 @@ ---- -title: Appbar.Action ---- - -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 component used to display an action item in the appbar. - - - - - - - -## Usage -```js -import * as React from 'react'; -import { Appbar } from 'react-native-paper'; -import { Platform } from 'react-native'; - -const MORE_ICON = Platform.OS === 'ios' ? 'dots-horizontal' : 'dots-vertical'; - -const MyComponent = () => ( - - - {}} /> - {}} /> - -); - -export default MyComponent; -``` - - - ## Props - - - - -
- -### color - -
- - - -
- -### icon (required) - -
- - - -
- -### size - -
- - - -
- -### disabled - -
- - - -
- -### aria-label - -
- - - -
- -### onPress - -
- - - -
- -### isLeading Available in v5.x with theme version 3 - -
- - - -
- -### style - -
- - - -
- -### ref - -
- - - -
- -### theme - -
- - - - - - - - - ## Theme colors - - - - - diff --git a/docs/6.x/docs/components/Appbar/AppbarBackAction.mdx b/docs/6.x/docs/components/Appbar/AppbarBackAction.mdx deleted file mode 100644 index 4faa4ec18d..0000000000 --- a/docs/6.x/docs/components/Appbar/AppbarBackAction.mdx +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: Appbar.BackAction ---- - -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 component used to display a back button in the appbar. - - - - - - - -## Usage -```js -import * as React from 'react'; -import { Appbar } from 'react-native-paper'; - -const MyComponent = () => ( - - {}} /> - -); - -export default MyComponent; -``` - - - ## Props - - - - -
- -### color - -
- - - -
- -### size - -
- - - -
- -### disabled - -
- - - -
- -### aria-label - -
- - - -
- -### onPress - -
- - - -
- -### style - -
- - - -
- -### ref - -
- - - - - - - - - - diff --git a/docs/6.x/docs/components/Appbar/AppbarContent.mdx b/docs/6.x/docs/components/Appbar/AppbarContent.mdx deleted file mode 100644 index a3c4953c66..0000000000 --- a/docs/6.x/docs/components/Appbar/AppbarContent.mdx +++ /dev/null @@ -1,129 +0,0 @@ ---- -title: Appbar.Content ---- - -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 component used to display a title in an appbar. - - - - - - - -## Usage -```js -import * as React from 'react'; -import { Appbar } from 'react-native-paper'; - -const MyComponent = () => ( - - - -); - -export default MyComponent; -``` - - - ## Props - - - - -
- -### title (required) - -
- - - -
- -### titleStyle - -
- - - -
- -### titleRef - -
- - - -
- -### onPress - -
- - - -
- -### disabled - -
- - - -
- -### color - -
- - - -
- -### titleMaxFontSizeMultiplier - -
- - - -
- -### style - -
- - - -
- -### theme - -
- - - -
- -### testID - -
- - - - - - - - - ## Theme colors - - - - - diff --git a/docs/6.x/docs/components/Appbar/AppbarHeader.mdx b/docs/6.x/docs/components/Appbar/AppbarHeader.mdx deleted file mode 100644 index f541042a00..0000000000 --- a/docs/6.x/docs/components/Appbar/AppbarHeader.mdx +++ /dev/null @@ -1,125 +0,0 @@ ---- -title: Appbar.Header ---- - -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 component to use as a header at the top of the screen. -It can contain the screen title, controls such as navigation buttons, menu button etc. - - - - - - - -## Usage -```js -import * as React from 'react'; -import { Appbar } from 'react-native-paper'; - -const MyComponent = () => { - const _goBack = () => console.log('Went back'); - - const _handleSearch = () => console.log('Searching'); - - const _handleMore = () => console.log('Shown more'); - - return ( - - - - - - - ); -}; - -export default MyComponent; -``` - - - ## Props - - - - -
- -### dark - -
- - - -
- -### statusBarHeight - -
- - - -
- -### children (required) - -
- - - -
- -### mode Available in v5.x with theme version 3 - -
- - - -
- -### elevated Available in v5.x with theme version 3 - -
- - - -
- -### theme - -
- - - -
- -### style - -
- - - -
- -### testID - -
- - - - - - - - - ## Theme colors - - - - - diff --git a/docs/6.x/docs/components/Appbar/_meta.json b/docs/6.x/docs/components/Appbar/_meta.json index d4243fea02..8350bf635e 100644 --- a/docs/6.x/docs/components/Appbar/_meta.json +++ b/docs/6.x/docs/components/Appbar/_meta.json @@ -1,7 +1,3 @@ [ - "Appbar", - "AppbarAction", - "AppbarBackAction", - "AppbarContent", - "AppbarHeader" + "Appbar" ] diff --git a/docs/6.x/docs/guides/react-navigation.md b/docs/6.x/docs/guides/react-navigation.md index 6c01200f11..2e655b2e87 100644 --- a/docs/6.x/docs/guides/react-navigation.md +++ b/docs/6.x/docs/guides/react-navigation.md @@ -136,11 +136,7 @@ Now we will implement `CustomNavigationBar` using `AppBar` component: import { Appbar } from 'react-native-paper'; export default function CustomNavigationBar() { - return ( - - - - ); + return ; } ``` @@ -154,11 +150,7 @@ import { getHeaderTitle } from '@react-navigation/elements'; export default function CustomNavigationBar({ route, options }) { const title = getHeaderTitle(options, route.name); - return ( - - - - ); + return ; } ``` @@ -179,10 +171,13 @@ export default function CustomNavigationBar({ const title = getHeaderTitle(options, route.name); return ( - - {back ? : null} - - + ); } ``` @@ -194,8 +189,8 @@ export default function CustomNavigationBar({ Another interesting pattern that can be implemented with `react-native-paper` and `react-navigation` is a "menu" button. Thanks to the `Menu` component we can add a nice looking pop-up to our `Appbar`. To implement this feature we need to make a couple of changes in `CustomNavigationBar`: - Render a `Menu` component -- Pass `Appbar.Action` to the anchor prop -- Add a state to control `Menu` visibility +- Add a trailing action to `Appbar` +- Store the action coordinates and menu visibility in state :::note To have properly working `Menu` component, remember to wrap your root component with the `PaperProvider`: @@ -236,21 +231,41 @@ export default function CustomNavigationBar({ back, }) { const [visible, setVisible] = React.useState(false); + const [menuAnchor, setMenuAnchor] = React.useState({ x: 0, y: 0 }); const openMenu = () => setVisible(true); const closeMenu = () => setVisible(false); const title = getHeaderTitle(options, route.name); return ( - - {back ? : null} - + <> + { + setMenuAnchor({ + x: event.nativeEvent.pageX, + y: event.nativeEvent.pageY, + }); + openMenu(); + }, + }, + ] + } + /> {!back ? ( - } - > + { console.log('Option 1 was pressed'); @@ -272,7 +287,7 @@ export default function CustomNavigationBar({ /> ) : null} - + ); } ``` diff --git a/docs/6.x/docs/guides/theming-with-react-navigation.md b/docs/6.x/docs/guides/theming-with-react-navigation.md index 06be61e5c8..8d28eb8440 100644 --- a/docs/6.x/docs/guides/theming-with-react-navigation.md +++ b/docs/6.x/docs/guides/theming-with-react-navigation.md @@ -249,24 +249,25 @@ Now that the Context is available at every component, all we need to do is impor ```js import React from 'react'; -import { useTheme, Appbar, TouchableRipple, Switch } from 'react-native-paper'; +import { Appbar } from 'react-native-paper'; import { PreferencesContext } from './PreferencesContext'; const Header = ({ scene }) => { - const theme = useTheme(); const { toggleTheme, isThemeDark } = React.useContext(PreferencesContext); return ( - - - - + ]} + /> ); }; ``` diff --git a/docs/6.x/docs/guides/theming.mdx b/docs/6.x/docs/guides/theming.mdx index 261fe128be..b7bfebacb3 100644 --- a/docs/6.x/docs/guides/theming.mdx +++ b/docs/6.x/docs/guides/theming.mdx @@ -503,14 +503,14 @@ Now you can use your `FancyButton` component everywhere instead of using `Button ## Dark Theme Since 3.0 we adapt dark theme to follow [Material design guidelines](https://material.io/design/color/dark-theme.html).
-In contrast to light theme, dark theme by default uses `surface` colour instead of `primary` on large components like `AppBar` or `BottomNavigation`.
+In contrast to light theme, dark theme by default uses `surface` colour instead of `primary` on large components like `BottomNavigation`.
The dark theme adds a white overlay with opacity depending on elevation of surfaces. It uses it for the better accentuation of surface elevation. Using only shadow is highly imperceptible on dark surfaces. We are aware that users often use dark theme in their own ways and may not want to use the default dark theme features from the guidelines.
That's why if you are using dark theme you can switch between two dark theme `mode`s: -- `exact` where everything is like it was before. `Appbar` and `BottomNavigation` will still use primary colour by default.
-- `adaptive` where we follow [Material design guidelines](https://material.io/design/color/dark-theme.html), the surface will use white overlay with opacity to show elevation, `Appbar` and `BottomNavigation` will use surface colour as a background. +- `exact` where everything is like it was before. `BottomNavigation` will still use primary colour by default.
+- `adaptive` where we follow [Material design guidelines](https://material.io/design/color/dark-theme.html), the surface will use white overlay with opacity to show elevation, and `BottomNavigation` will use surface colour as a background. If you don't use a custom theme, Paper will automatically change between the default theme and the default dark theme, depending on device settings. diff --git a/docs/component-docs.config.ts b/docs/component-docs.config.ts index bad25d4eec..bb57ff6940 100644 --- a/docs/component-docs.config.ts +++ b/docs/component-docs.config.ts @@ -26,10 +26,6 @@ const pages = { ActivityIndicator: 'ActivityIndicator', Appbar: { Appbar: 'Appbar/Appbar', - AppbarAction: 'Appbar/AppbarAction', - AppbarBackAction: 'Appbar/AppbarBackAction', - AppbarContent: 'Appbar/AppbarContent', - AppbarHeader: 'Appbar/AppbarHeader', }, Avatar: { AvatarIcon: 'Avatar/AvatarIcon', diff --git a/docs/src/data/componentDocs6x.json b/docs/src/data/componentDocs6x.json index 300c315b99..386d34d750 100644 --- a/docs/src/data/componentDocs6x.json +++ b/docs/src/data/componentDocs6x.json @@ -95,624 +95,41 @@ "Appbar/Appbar": { "filepath": "Appbar/Appbar.tsx", "title": "Appbar", - "description": "A component to display action items in a bar. It can be placed at the top or bottom.\nThe top bar usually contains the screen title, controls such as navigation buttons, menu button etc.\nThe bottom bar usually provides access to a drawer and up to four actions.\n\nBy default Appbar 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 informations\n\n## Usage\n### Top bar\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n {}} />\n \n {}} />\n {}} />\n \n);\n\nexport default MyComponent;\n```\n\n### Bottom bar\n```js\nimport * as React from 'react';\nimport { StyleSheet } from 'react-native';\nimport { Appbar, FAB, useTheme } from 'react-native-paper';\nimport { useSafeAreaInsets } from 'react-native-safe-area-context';\n\nconst BOTTOM_APPBAR_HEIGHT = 80;\nconst MEDIUM_FAB_HEIGHT = 56;\n\nconst MyComponent = () => {\n const { bottom } = useSafeAreaInsets();\n const theme = useTheme();\n\n return (\n \n {}} />\n {}} />\n {}} />\n {}} />\n {}}\n style={[\n styles.fab,\n { top: (BOTTOM_APPBAR_HEIGHT - MEDIUM_FAB_HEIGHT) / 2 },\n ]}\n />\n \n );\n};\n\nconst styles = StyleSheet.create({\n bottom: {\n backgroundColor: 'aquamarine',\n position: 'absolute',\n left: 0,\n right: 0,\n bottom: 0,\n },\n fab: {\n position: 'absolute',\n right: 16,\n },\n});\n\nexport default MyComponent;\n```", + "description": "A Material Design app bar for displaying a page headline, navigation, and\ncontextual actions. Appbar supports small, medium flexible, large flexible,\nand search variants. Its surface color automatically changes when content\nhas scrolled, and it accounts for safe-area insets.\n\n## Variants\n\n| `variant` | Purpose |\n| --- | --- |\n| `small` | A compact 64dp app bar with a one-line headline. |\n| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. |\n| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. |\n| `search` | An app bar containing a centered Paper `Searchbar`. |\n\n## Main props\n\n| Prop | Description |\n| --- | --- |\n| `headline` | Required accessible headline for every non-search variant. |\n| `subtitle` | Optional supporting text for written headlines. |\n| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. |\n| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. |\n| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. |\n| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. |\n| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`. |\n| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. |\n| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. |\n| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. |\n| `statusBarHeight` | Overrides only the automatic top inset. |\n| `contentStyle` | Styles the headline and subtitle area. |\n| `style` | Styles the app bar and can override its background color. |\n\n## Migrating from the compound API\n\n`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and\n`Appbar.BackAction` have been removed. Render one `Appbar` and provide its\nheadline, leading button, and trailing actions as props. The former\n`mode=\"medium\"` and `mode=\"large\"` values are now\n`variant=\"medium-flexible\"` and `variant=\"large-flexible\"`; centered\ncontent uses `headlineAlignment=\"center\"`.\n\n## Usage\n\n### Small app bar\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n {} }}\n trailingActions={[\n {\n key: 'search',\n icon: 'magnify',\n 'aria-label': 'Search',\n onPress: () => {},\n },\n {\n key: 'more',\n icon: 'dots-vertical',\n 'aria-label': 'More options',\n onPress: () => {},\n },\n ]}\n />\n);\n\nexport default MyComponent;\n```\n\n### Flexible app bar\n```js\n\n```\n\n### Search app bar\n```js\n {} }}\n searchBar={{\n placeholder: 'Search messages',\n value: query,\n onChangeText: setQuery,\n }}\n/>\n```", "link": "appbar", "data": { - "description": "A component to display action items in a bar. It can be placed at the top or bottom.\nThe top bar usually contains the screen title, controls such as navigation buttons, menu button etc.\nThe bottom bar usually provides access to a drawer and up to four actions.\n\nBy default Appbar 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 informations\n\n## Usage\n### Top bar\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n {}} />\n \n {}} />\n {}} />\n \n);\n\nexport default MyComponent;\n```\n\n### Bottom bar\n```js\nimport * as React from 'react';\nimport { StyleSheet } from 'react-native';\nimport { Appbar, FAB, useTheme } from 'react-native-paper';\nimport { useSafeAreaInsets } from 'react-native-safe-area-context';\n\nconst BOTTOM_APPBAR_HEIGHT = 80;\nconst MEDIUM_FAB_HEIGHT = 56;\n\nconst MyComponent = () => {\n const { bottom } = useSafeAreaInsets();\n const theme = useTheme();\n\n return (\n \n {}} />\n {}} />\n {}} />\n {}} />\n {}}\n style={[\n styles.fab,\n { top: (BOTTOM_APPBAR_HEIGHT - MEDIUM_FAB_HEIGHT) / 2 },\n ]}\n />\n \n );\n};\n\nconst styles = StyleSheet.create({\n bottom: {\n backgroundColor: 'aquamarine',\n position: 'absolute',\n left: 0,\n right: 0,\n bottom: 0,\n },\n fab: {\n position: 'absolute',\n right: 16,\n },\n});\n\nexport default MyComponent;\n```", + "description": "A Material Design app bar for displaying a page headline, navigation, and\ncontextual actions. Appbar supports small, medium flexible, large flexible,\nand search variants. Its surface color automatically changes when content\nhas scrolled, and it accounts for safe-area insets.\n\n## Variants\n\n| `variant` | Purpose |\n| --- | --- |\n| `small` | A compact 64dp app bar with a one-line headline. |\n| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. |\n| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. |\n| `search` | An app bar containing a centered Paper `Searchbar`. |\n\n## Main props\n\n| Prop | Description |\n| --- | --- |\n| `headline` | Required accessible headline for every non-search variant. |\n| `subtitle` | Optional supporting text for written headlines. |\n| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. |\n| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. |\n| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. |\n| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. |\n| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`. |\n| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. |\n| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. |\n| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. |\n| `statusBarHeight` | Overrides only the automatic top inset. |\n| `contentStyle` | Styles the headline and subtitle area. |\n| `style` | Styles the app bar and can override its background color. |\n\n## Migrating from the compound API\n\n`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and\n`Appbar.BackAction` have been removed. Render one `Appbar` and provide its\nheadline, leading button, and trailing actions as props. The former\n`mode=\"medium\"` and `mode=\"large\"` values are now\n`variant=\"medium-flexible\"` and `variant=\"large-flexible\"`; centered\ncontent uses `headlineAlignment=\"center\"`.\n\n## Usage\n\n### Small app bar\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n {} }}\n trailingActions={[\n {\n key: 'search',\n icon: 'magnify',\n 'aria-label': 'Search',\n onPress: () => {},\n },\n {\n key: 'more',\n icon: 'dots-vertical',\n 'aria-label': 'More options',\n onPress: () => {},\n },\n ]}\n />\n);\n\nexport default MyComponent;\n```\n\n### Flexible app bar\n```js\n\n```\n\n### Search app bar\n```js\n {} }}\n searchBar={{\n placeholder: 'Search messages',\n value: query,\n onChangeText: setQuery,\n }}\n/>\n```", "displayName": "Appbar", "methods": [], "statics": [], "props": { - "dark": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "Whether the background color is a dark color. A dark appbar will render light text and vice-versa." - }, - "children": { - "required": true, - "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" - }, - "description": "Content of the `Appbar`." - }, - "mode": { - "required": false, - "tsType": { - "name": "union", - "raw": "'small' | 'medium' | 'large' | 'center-aligned'", - "elements": [ - { - "name": "literal", - "value": "'small'" - }, - { - "name": "literal", - "value": "'medium'" - }, - { - "name": "literal", - "value": "'large'" - }, - { - "name": "literal", - "value": "'center-aligned'" - } - ] - }, - "description": "@supported Available in v5.x with theme version 3\n\nMode of the Appbar.\n- `small` - Appbar with default height (64).\n- `medium` - Appbar with medium height (112).\n- `large` - Appbar with large height (152).\n- `center-aligned` - Appbar with default height and center-aligned title.", - "defaultValue": { - "value": "'small'", - "computed": false - } - }, - "elevated": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "@supported Available in v5.x with theme version 3\nWhether Appbar background should have the elevation along with primary color pigment." - }, - "safeAreaInsets": { - "required": false, - "tsType": { - "name": "signature", - "type": "object", - "raw": "{\n bottom?: number;\n top?: number;\n left?: number;\n right?: number;\n}", - "signature": { - "properties": [ - { - "key": "bottom", - "value": { - "name": "number", - "required": false - } - }, - { - "key": "top", - "value": { - "name": "number", - "required": false - } - }, - { - "key": "left", - "value": { - "name": "number", - "required": false - } - }, - { - "key": "right", - "value": { - "name": "number", - "required": false - } - } - ] - } - }, - "description": "Safe area insets for the Appbar. This can be used to avoid elements like the navigation bar on Android and bottom safe area on iOS." - }, - "theme": { - "required": false, - "tsType": { - "name": "ThemeProp" - }, - "description": "" - }, - "style": { - "required": false, - "tsType": { - "name": "Animated.WithAnimatedValue", - "elements": [ - { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" - } - ], - "raw": "Animated.WithAnimatedValue>" - }, - "description": "" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/Appbar/Appbar.tsx" - ] - }, - "Appbar/AppbarAction": { - "filepath": "Appbar/AppbarAction.tsx", - "title": "Appbar.Action", - "description": "A component used to display an action item in the appbar.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\nimport { Platform } from 'react-native';\n\nconst MORE_ICON = Platform.OS === 'ios' ? 'dots-horizontal' : 'dots-vertical';\n\nconst MyComponent = () => (\n \n \n {}} />\n {}} />\n \n);\n\nexport default MyComponent;\n```", - "link": "appbar-action", - "data": { - "description": "A component used to display an action item in the appbar.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\nimport { Platform } from 'react-native';\n\nconst MORE_ICON = Platform.OS === 'ios' ? 'dots-horizontal' : 'dots-vertical';\n\nconst MyComponent = () => (\n \n \n {}} />\n {}} />\n \n);\n\nexport default MyComponent;\n```", - "displayName": "Appbar.Action", - "methods": [], - "statics": [], - "props": { - "color": { - "required": false, - "tsType": { - "name": "ColorValue" - }, - "description": "Custom color for action icon." - }, - "icon": { - "required": true, - "tsType": { - "name": "IconSource" - }, - "description": "Name of the icon to show." - }, - "size": { - "required": false, - "tsType": { - "name": "number" - }, - "description": "Optional icon size.", - "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." - }, - "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." - }, - "onPress": { - "required": false, - "tsType": { - "name": "signature", - "type": "function", - "raw": "() => void", - "signature": { - "arguments": [], - "return": { - "name": "void" - } - } - }, - "description": "Function to execute on press." - }, - "isLeading": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "@supported Available in v5.x with theme version 3\n\nWhether it's the leading button. Note: If `Appbar.BackAction` is present, it will be rendered before any `isLeading` icons." - }, - "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": "" - }, - "theme": { - "required": false, - "tsType": { - "name": "ThemeProp" - }, - "description": "" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/Appbar/AppbarAction.tsx" - ], - "group": "Appbar" - }, - "Appbar/AppbarBackAction": { - "filepath": "Appbar/AppbarBackAction.tsx", - "title": "Appbar.BackAction", - "description": "A component used to display a back button in the appbar.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n {}} />\n \n);\n\nexport default MyComponent;\n```", - "link": "appbar-back-action", - "data": { - "description": "A component used to display a back button in the appbar.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n {}} />\n \n);\n\nexport default MyComponent;\n```", - "displayName": "Appbar.BackAction", - "methods": [], - "statics": [], - "props": { - "color": { - "required": false, - "tsType": { - "name": "ColorValue" - }, - "description": "Custom color for back icon." - }, - "size": { - "required": false, - "tsType": { - "name": "number" - }, - "description": "Optional icon size." - }, - "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." - }, - "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.", - "defaultValue": { - "value": "'Back'", - "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." - }, - "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": "" - } - } - }, - "type": "component", - "dependencies": [ - "src/components/Appbar/AppbarBackAction.tsx" - ], - "group": "Appbar" - }, - "Appbar/AppbarContent": { - "filepath": "Appbar/AppbarContent.tsx", - "title": "Appbar.Content", - "description": "A component used to display a title in an appbar.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n);\n\nexport default MyComponent;\n```", - "link": "appbar-content", - "data": { - "description": "A component used to display a title in an appbar.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n \n);\n\nexport default MyComponent;\n```", - "displayName": "Appbar.Content", - "methods": [], - "statics": [], - "props": { - "title": { - "required": true, - "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" - }, - "description": "Text or component for the title." - }, - "titleStyle": { - "required": false, - "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "TextStyle" - } - ], - "raw": "StyleProp" - }, - "description": "Style for the title, if `title` is a string." - }, - "titleRef": { - "required": false, - "tsType": { - "name": "ReactRefObject", - "raw": "React.RefObject", - "elements": [ - { - "name": "TextRef" - } - ] - }, - "description": "Reference for the title." - }, - "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." - }, - "disabled": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "If true, disable all interactions for this component." - }, - "color": { - "required": false, - "tsType": { - "name": "string" - }, - "description": "Custom color for the text." - }, - "titleMaxFontSizeMultiplier": { - "required": false, - "tsType": { - "name": "number" - }, - "description": "Specifies the largest possible scale a title font can reach." - }, - "mode": { - "required": false, - "tsType": { - "name": "union", - "raw": "'small' | 'medium' | 'large' | 'center-aligned'", - "elements": [ - { - "name": "literal", - "value": "'small'" - }, - { - "name": "literal", - "value": "'medium'" - }, - { - "name": "literal", - "value": "'large'" - }, - { - "name": "literal", - "value": "'center-aligned'" - } - ] - }, - "description": "@internal", + "headlineAlignment": { "defaultValue": { - "value": "'small'", + "value": "'leading'", "computed": false - } - }, - "style": { - "required": false, - "tsType": { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" }, - "description": "" - }, - "theme": { "required": false, - "tsType": { - "name": "ThemeProp" - }, "description": "" }, - "testID": { - "required": false, - "tsType": { - "name": "string" - }, - "description": "testID to be used on tests.", + "isScrolled": { "defaultValue": { - "value": "'appbar-content'", + "value": "false", "computed": false - } - } - } - }, - "type": "component", - "dependencies": [ - "src/components/Appbar/AppbarContent.tsx" - ], - "group": "Appbar" - }, - "Appbar/AppbarHeader": { - "filepath": "Appbar/AppbarHeader.tsx", - "title": "Appbar.Header", - "description": "A component to use as a header at the top of the screen.\nIt can contain the screen title, controls such as navigation buttons, menu button etc.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => {\n const _goBack = () => console.log('Went back');\n\n const _handleSearch = () => console.log('Searching');\n\n const _handleMore = () => console.log('Shown more');\n\n return (\n \n \n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", - "link": "appbar-header", - "data": { - "description": "A component to use as a header at the top of the screen.\nIt can contain the screen title, controls such as navigation buttons, menu button etc.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => {\n const _goBack = () => console.log('Went back');\n\n const _handleSearch = () => console.log('Searching');\n\n const _handleMore = () => console.log('Shown more');\n\n return (\n \n \n \n \n \n \n );\n};\n\nexport default MyComponent;\n```", - "displayName": "Appbar.Header", - "methods": [], - "statics": [], - "props": { - "dark": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "Whether the background color is a dark color. A dark header will render light text and vice-versa." - }, - "statusBarHeight": { - "required": false, - "tsType": { - "name": "number" - }, - "description": "Extra padding to add at the top of header to account for translucent status bar.\nThis is automatically handled on iOS >= 11 including iPhone X using `SafeAreaView`.\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 disable the default behaviour, and customize the height." - }, - "children": { - "required": true, - "tsType": { - "name": "ReactReactNode", - "raw": "React.ReactNode" }, - "description": "Content of the header." - }, - "mode": { "required": false, - "tsType": { - "name": "union", - "raw": "'small' | 'medium' | 'large' | 'center-aligned'", - "elements": [ - { - "name": "literal", - "value": "'small'" - }, - { - "name": "literal", - "value": "'medium'" - }, - { - "name": "literal", - "value": "'large'" - }, - { - "name": "literal", - "value": "'center-aligned'" - } - ] - }, - "description": "@supported Available in v5.x with theme version 3\n\nMode of the Appbar.\n- `small` - Appbar with default height (64).\n- `medium` - Appbar with medium height (112).\n- `large` - Appbar with large height (152).\n- `center-aligned` - Appbar with default height and center-aligned title.", - "defaultValue": { - "value": "Platform.OS === 'ios' ? 'center-aligned' : 'small'", - "computed": false - } + "description": "" }, - "elevated": { - "required": false, - "tsType": { - "name": "boolean" - }, - "description": "@supported Available in v5.x with theme version 3\nWhether Appbar background should have the elevation along with primary color pigment.", + "testID": { "defaultValue": { - "value": "false", + "value": "'appbar'", "computed": false - } - }, - "theme": { - "required": false, - "tsType": { - "name": "ThemeProp" }, - "description": "" - }, - "style": { "required": false, - "tsType": { - "name": "Animated.WithAnimatedValue", - "elements": [ - { - "name": "StyleProp", - "elements": [ - { - "name": "ViewStyle" - } - ], - "raw": "StyleProp" - } - ], - "raw": "Animated.WithAnimatedValue>" - }, "description": "" }, - "testID": { + "trailingActions": { "defaultValue": { - "value": "'appbar-header'", + "value": "[]", "computed": false }, "required": false, @@ -722,9 +139,8 @@ }, "type": "component", "dependencies": [ - "src/components/Appbar/AppbarHeader.tsx" - ], - "group": "Appbar" + "src/components/Appbar/Appbar.tsx" + ] }, "Avatar/AvatarIcon": { "filepath": "Avatar/AvatarIcon.tsx", diff --git a/docs/src/data/screenshots.ts b/docs/src/data/screenshots.ts index 92bf8f2788..a30ca3bff6 100644 --- a/docs/src/data/screenshots.ts +++ b/docs/src/data/screenshots.ts @@ -1,15 +1,5 @@ export const screenshots = { ActivityIndicator: 'screenshots/activity-indicator.gif', - Appbar: 'screenshots/appbar.png', - 'Appbar.Action': 'screenshots/appbar-action-android.png', - 'Appbar.BackAction': 'screenshots/appbar-backaction-android.png', - 'Appbar.Content': 'screenshots/appbar-content.png', - 'Appbar.Header': { - small: 'screenshots/appbar-small.png', - medium: 'screenshots/appbar-medium.png', - large: 'screenshots/appbar-large.png', - 'center-aligned': 'screenshots/appbar-center-aligned.png', - }, 'Avatar.Icon': 'screenshots/avatar-icon.png', 'Avatar.Image': 'screenshots/avatar-image.png', 'Avatar.Text': 'screenshots/avatar-text.png', diff --git a/docs/src/data/themeColors.ts b/docs/src/data/themeColors.ts index 20f16962f7..d347b65889 100644 --- a/docs/src/data/themeColors.ts +++ b/docs/src/data/themeColors.ts @@ -8,29 +8,20 @@ export const themeColors = { default: { backgroundColor: 'theme.colors.surface', }, - elevated: { - backgroundColor: 'theme.colors.elevation.level2', + scrolled: { + backgroundColor: 'theme.colors.surfaceContainer', }, - }, - 'Appbar.Action': { - 'leading icon': { + 'leading action': { iconColor: 'theme.colors.onSurface', }, - 'not leading icon': { + 'trailing action': { iconColor: 'theme.colors.onSurfaceVariant', }, - }, - 'Appbar.Content': { - '-': { + headline: { textColor: 'theme.colors.onSurface', }, - }, - 'Appbar.Header': { - default: { - backgroundColor: 'theme.colors.surface', - }, - elevated: { - backgroundColor: 'theme.colors.elevation.level2', + subtitle: { + textColor: 'theme.colors.onSurfaceVariant', }, }, Banner: { diff --git a/example/src/ExampleList.tsx b/example/src/ExampleList.tsx index bd30b8feb6..8a2646d798 100644 --- a/example/src/ExampleList.tsx +++ b/example/src/ExampleList.tsx @@ -6,7 +6,6 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import ActivityIndicatorExample from './Examples/ActivityIndicatorExample'; import AppbarExample from './Examples/AppbarExample'; -import AppbarV3Example from './Examples/AppbarV3Example'; import AvatarExample from './Examples/AvatarExample'; import BadgeExample from './Examples/BadgeExample'; import BannerExample from './Examples/BannerExample'; @@ -52,7 +51,6 @@ import TouchableRippleExample from './Examples/TouchableRippleExample'; export const mainExamples = { ActivityIndicator: ActivityIndicatorExample, Appbar: AppbarExample, - AppbarV3: AppbarV3Example, Avatar: AvatarExample, Badge: BadgeExample, Banner: BannerExample, diff --git a/example/src/Examples/AppbarExample.tsx b/example/src/Examples/AppbarExample.tsx index 10f4b0bb92..17f20da4ee 100644 --- a/example/src/Examples/AppbarExample.tsx +++ b/example/src/Examples/AppbarExample.tsx @@ -1,122 +1,320 @@ import * as React from 'react'; -import { Platform, StyleSheet, View } from 'react-native'; +import { + Image, + Platform, + StyleSheet, + useWindowDimensions, + View, +} from 'react-native'; +import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { Appbar, - FAB, List, Palette, RadioButton, Snackbar, Switch, Text, - useTheme, +} from 'react-native-paper'; +import type { + AppbarFilledTrailingAction, + AppbarHeadlineAlignment, + AppbarLeadingButton, + AppbarStandardTrailingAction, + AppbarTrailingActions, + AppbarVariant, } from 'react-native-paper'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import ScreenWrapper from '../ScreenWrapper'; -type AppbarModes = 'small' | 'medium' | 'large' | 'center-aligned'; +type FilledTrailingActionVariant = AppbarFilledTrailingAction['variant']; +type FilledTrailingActionWidth = NonNullable< + AppbarFilledTrailingAction['width'] +>; const MORE_ICON = Platform.OS === 'ios' ? 'dots-horizontal' : 'dots-vertical'; -const MEDIUM_FAB_HEIGHT = 56; + +type SearchAppbarHeaderProps = { + trailingActions: AppbarStandardTrailingAction[]; + isScrolled: boolean; + leadingButton?: AppbarLeadingButton; + showCustomColor: boolean; +}; + +const SearchAppbarHeader = ({ + trailingActions, + isScrolled, + leadingButton, + showCustomColor, +}: SearchAppbarHeaderProps) => { + const [searchQuery, setSearchQuery] = React.useState(''); + + return ( + + ); +}; const AppbarExample = () => { const navigation = useNavigation('Appbar'); - const [showLeftIcon, setShowLeftIcon] = React.useState(true); + const [showLeadingButton, setShowLeadingButton] = React.useState(true); const [showSubtitle, setShowSubtitle] = React.useState(true); + const [showHeadlineImage, setShowHeadlineImage] = React.useState(false); const [showSearchIcon, setShowSearchIcon] = React.useState(true); const [showMoreIcon, setShowMoreIcon] = React.useState(true); const [showCustomColor, setShowCustomColor] = React.useState(false); - const [appbarMode, setAppbarMode] = React.useState('small'); + const [appbarConfiguration, setAppbarConfiguration] = + React.useState('small'); + const [isHeadlineCentered, setIsHeadlineCentered] = React.useState(false); const [showCalendarIcon, setShowCalendarIcon] = React.useState(false); - const [showElevated, setShowElevated] = React.useState(false); + const [showFilledTrailingAction, setShowFilledTrailingAction] = + React.useState(false); + const [filledTrailingActionVariant, setFilledTrailingActionVariant] = + React.useState('filled'); + const [filledTrailingActionWidth, setFilledTrailingActionWidth] = + React.useState('default'); + const [isScrolled, setIsScrolled] = React.useState(false); const [showSnackbar, setShowSnackbar] = React.useState(false); - const theme = useTheme(); - const { bottom, left, right } = useSafeAreaInsets(); - const height = 80; - - const isCenterAlignedMode = appbarMode === 'center-aligned'; + const { bottom } = useSafeAreaInsets(); + const { height: windowHeight } = useWindowDimensions(); + const headlineAlignment: AppbarHeadlineAlignment = isHeadlineCentered + ? 'center' + : 'leading'; React.useLayoutEffect(() => { + const standardTrailingActions: AppbarStandardTrailingAction[] = []; + + if (showCalendarIcon) { + standardTrailingActions.push({ + key: 'calendar', + icon: 'calendar', + 'aria-label': 'Calendar', + onPress: () => {}, + }); + } + + if (showSearchIcon && appbarConfiguration !== 'search') { + standardTrailingActions.push({ + key: 'search', + icon: 'magnify', + 'aria-label': 'Search', + onPress: () => {}, + }); + } + + if (showMoreIcon) { + standardTrailingActions.push({ + key: 'more', + icon: MORE_ICON, + 'aria-label': 'More options', + onPress: () => {}, + }); + } + + const trailingActions: AppbarTrailingActions = showFilledTrailingAction + ? [ + { + key: 'share', + icon: 'share-variant', + 'aria-label': 'Share', + variant: filledTrailingActionVariant, + width: filledTrailingActionWidth, + onPress: () => {}, + }, + ] + : standardTrailingActions; + + const leadingButton = showLeadingButton + ? { + type: 'back' as const, + onPress: () => navigation.goBack(), + } + : undefined; + const headlineImage = ( + + ); + const commonProps = { + headlineAlignment, + isScrolled, + leadingButton, + onHeadlinePress: () => setShowSnackbar(true), + style: showCustomColor ? styles.customColor : null, + trailingActions, + }; + navigation.setOptions({ - header: () => ( - - {showLeftIcon && ( - navigation.goBack()} /> - )} - setShowSnackbar(true)} + header: () => { + if (appbarConfiguration === 'search') { + return ( + + ); + } + + if (appbarConfiguration === 'small') { + return showHeadlineImage ? ( + + ) : ( + + ); + } + + return showHeadlineImage ? ( + + ) : ( + - {isCenterAlignedMode - ? false - : showCalendarIcon && ( - {}} /> - )} - {showSearchIcon && ( - {}} /> - )} - {showMoreIcon && ( - {}} /> - )} - - ), + ); + }, }); }, [ + appbarConfiguration, + filledTrailingActionVariant, + filledTrailingActionWidth, navigation, - showLeftIcon, - showSubtitle, - showSearchIcon, - showMoreIcon, - showCustomColor, - appbarMode, showCalendarIcon, - isCenterAlignedMode, - showElevated, + showCustomColor, + showFilledTrailingAction, + showLeadingButton, + showMoreIcon, + isScrolled, + showSearchIcon, + showSubtitle, + showHeadlineImage, + headlineAlignment, ]); - const renderFAB = () => { - return ( - {}} - style={[styles.fab, { top: (height - MEDIUM_FAB_HEIGHT) / 2 }]} - /> - ); - }; + const handleScroll = React.useCallback( + ({ nativeEvent }: NativeSyntheticEvent) => { + const nextIsScrolled = nativeEvent.contentOffset.y > 1; + + setIsScrolled((currentIsScrolled) => + currentIsScrolled === nextIsScrolled + ? currentIsScrolled + : nextIsScrolled + ); + }, + [] + ); const renderDefaultOptions = () => ( <> - Left icon - + Leading button + Subtitle - + + + + Headline image + + + + Center headline and subtitle + + + + Filled trailing action + Search icon - + More icon - + Calendar icon @@ -124,73 +322,110 @@ const AppbarExample = () => { Custom Color - - Elevated - - ); return ( <> + + + {renderDefaultOptions()} - + {showFilledTrailingAction ? ( + + Style + { + if (value === 'filled' || value === 'tonal') { + setFilledTrailingActionVariant(value); + } + }} + > + + Primary filled + + + + Tonal filled + + + + Width + { + if (value === 'default' || value === 'wide') { + setFilledTrailingActionWidth(value); + } + }} + > + + Default + + + + Wide + + + + + ) : null} + - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - setAppbarMode(value as AppbarModes) - } + value={appbarConfiguration} + onValueChange={(value: string) => { + if ( + value === 'small' || + value === 'search' || + value === 'medium-flexible' || + value === 'large-flexible' + ) { + setAppbarConfiguration(value); + } + }} > - Small (default) - + Search + - Medium - + Small (default) + - Large - + Medium flexible + - Center-aligned - + Large flexible + - - {}} /> - {}} /> - {}} /> - {}} /> - {renderFAB()} - setShowSnackbar(false)} duration={Snackbar.DURATION_SHORT} > - Heading pressed + Headline pressed ); @@ -211,17 +446,11 @@ const styles = StyleSheet.create({ paddingVertical: 8, paddingHorizontal: 16, }, - bottom: { - position: 'absolute', - left: 0, - right: 0, - bottom: 0, - }, - fab: { - position: 'absolute', - right: 16, - }, customColor: { backgroundColor: Palette.secondary80, }, + headlineImage: { + width: 32, + height: 32, + }, }); diff --git a/example/src/Examples/AppbarV3Example.tsx b/example/src/Examples/AppbarV3Example.tsx deleted file mode 100644 index e737fcae64..0000000000 --- a/example/src/Examples/AppbarV3Example.tsx +++ /dev/null @@ -1,456 +0,0 @@ -import * as React from 'react'; -import { - Image, - Platform, - StyleSheet, - useWindowDimensions, - View, -} from 'react-native'; -import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native'; - -import { useNavigation } from '@react-navigation/native'; -import { - AppbarV3, - List, - Palette, - RadioButton, - Snackbar, - Switch, - Text, -} from 'react-native-paper'; -import type { - AppbarV3FilledTrailingAction, - AppbarV3HeadlineAlignment, - AppbarV3LeadingButton, - AppbarV3StandardTrailingAction, - AppbarV3TrailingActions, - AppbarV3Variant, -} from 'react-native-paper'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -import ScreenWrapper from '../ScreenWrapper'; - -type FilledTrailingActionVariant = AppbarV3FilledTrailingAction['variant']; -type FilledTrailingActionWidth = NonNullable< - AppbarV3FilledTrailingAction['width'] ->; - -const MORE_ICON = Platform.OS === 'ios' ? 'dots-horizontal' : 'dots-vertical'; - -type SearchAppbarHeaderProps = { - trailingActions: AppbarV3StandardTrailingAction[]; - isScrolled: boolean; - leadingButton?: AppbarV3LeadingButton; - showCustomColor: boolean; -}; - -const SearchAppbarHeader = ({ - trailingActions, - isScrolled, - leadingButton, - showCustomColor, -}: SearchAppbarHeaderProps) => { - const [searchQuery, setSearchQuery] = React.useState(''); - - return ( - - ); -}; - -const AppbarV3Example = () => { - const navigation = useNavigation('AppbarV3'); - - const [showLeadingButton, setShowLeadingButton] = React.useState(true); - const [showSubtitle, setShowSubtitle] = React.useState(true); - const [showHeadlineImage, setShowHeadlineImage] = React.useState(false); - const [showSearchIcon, setShowSearchIcon] = React.useState(true); - const [showMoreIcon, setShowMoreIcon] = React.useState(true); - const [showCustomColor, setShowCustomColor] = React.useState(false); - const [appbarConfiguration, setAppbarConfiguration] = - React.useState('small'); - const [isHeadlineCentered, setIsHeadlineCentered] = React.useState(false); - const [showCalendarIcon, setShowCalendarIcon] = React.useState(false); - const [showFilledTrailingAction, setShowFilledTrailingAction] = - React.useState(false); - const [filledTrailingActionVariant, setFilledTrailingActionVariant] = - React.useState('filled'); - const [filledTrailingActionWidth, setFilledTrailingActionWidth] = - React.useState('default'); - const [isScrolled, setIsScrolled] = React.useState(false); - const [showSnackbar, setShowSnackbar] = React.useState(false); - - const { bottom } = useSafeAreaInsets(); - const { height: windowHeight } = useWindowDimensions(); - const headlineAlignment: AppbarV3HeadlineAlignment = isHeadlineCentered - ? 'center' - : 'leading'; - - React.useLayoutEffect(() => { - const standardTrailingActions: AppbarV3StandardTrailingAction[] = []; - - if (showCalendarIcon) { - standardTrailingActions.push({ - key: 'calendar', - icon: 'calendar', - 'aria-label': 'Calendar', - onPress: () => {}, - }); - } - - if (showSearchIcon && appbarConfiguration !== 'search') { - standardTrailingActions.push({ - key: 'search', - icon: 'magnify', - 'aria-label': 'Search', - onPress: () => {}, - }); - } - - if (showMoreIcon) { - standardTrailingActions.push({ - key: 'more', - icon: MORE_ICON, - 'aria-label': 'More options', - onPress: () => {}, - }); - } - - const trailingActions: AppbarV3TrailingActions = showFilledTrailingAction - ? [ - { - key: 'share', - icon: 'share-variant', - 'aria-label': 'Share', - variant: filledTrailingActionVariant, - width: filledTrailingActionWidth, - onPress: () => {}, - }, - ] - : standardTrailingActions; - - const leadingButton = showLeadingButton - ? { - type: 'back' as const, - onPress: () => navigation.goBack(), - } - : undefined; - const headlineImage = ( - - ); - const commonProps = { - headlineAlignment, - isScrolled, - leadingButton, - onHeadlinePress: () => setShowSnackbar(true), - style: showCustomColor ? styles.customColor : null, - trailingActions, - }; - - navigation.setOptions({ - header: () => { - if (appbarConfiguration === 'search') { - return ( - - ); - } - - if (appbarConfiguration === 'small') { - return showHeadlineImage ? ( - - ) : ( - - ); - } - - return showHeadlineImage ? ( - - ) : ( - - ); - }, - }); - }, [ - appbarConfiguration, - filledTrailingActionVariant, - filledTrailingActionWidth, - navigation, - showCalendarIcon, - showCustomColor, - showFilledTrailingAction, - showLeadingButton, - showMoreIcon, - isScrolled, - showSearchIcon, - showSubtitle, - showHeadlineImage, - headlineAlignment, - ]); - - const handleScroll = React.useCallback( - ({ nativeEvent }: NativeSyntheticEvent) => { - const nextIsScrolled = nativeEvent.contentOffset.y > 1; - - setIsScrolled((currentIsScrolled) => - currentIsScrolled === nextIsScrolled - ? currentIsScrolled - : nextIsScrolled - ); - }, - [] - ); - - const renderDefaultOptions = () => ( - <> - - Leading button - - - - Subtitle - - - - Headline image - - - - Center headline and subtitle - - - - Filled trailing action - - - - Search icon - - - - More icon - - - - Calendar icon - - - - Custom Color - - - - ); - - return ( - <> - - - - - - {renderDefaultOptions()} - - {showFilledTrailingAction ? ( - - Style - { - if (value === 'filled' || value === 'tonal') { - setFilledTrailingActionVariant(value); - } - }} - > - - Primary filled - - - - Tonal filled - - - - Width - { - if (value === 'default' || value === 'wide') { - setFilledTrailingActionWidth(value); - } - }} - > - - Default - - - - Wide - - - - - ) : null} - - { - if ( - value === 'small' || - value === 'search' || - value === 'medium-flexible' || - value === 'large-flexible' - ) { - setAppbarConfiguration(value); - } - }} - > - - Search - - - - Small (default) - - - - Medium flexible - - - - Large flexible - - - - - - setShowSnackbar(false)} - duration={Snackbar.DURATION_SHORT} - > - Headline pressed - - - ); -}; - -AppbarV3Example.title = 'Appbar V3'; - -export default AppbarV3Example; - -const styles = StyleSheet.create({ - contentContainer: { - paddingVertical: 8, - }, - row: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingVertical: 8, - paddingHorizontal: 16, - }, - customColor: { - backgroundColor: Palette.secondary80, - }, - headlineImage: { - width: 32, - height: 32, - }, -}); diff --git a/example/src/Examples/BottomNavigationExample.tsx b/example/src/Examples/BottomNavigationExample.tsx index b66bb474d6..ea5ead5ace 100644 --- a/example/src/Examples/BottomNavigationExample.tsx +++ b/example/src/Examples/BottomNavigationExample.tsx @@ -86,45 +86,53 @@ const BottomNavigationExample = () => { 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" - /> - - + navigation.goBack(), + }} + trailingActions={[ + { + key: 'more', + icon: MORE_ICON, + 'aria-label': 'More options', + onPress: () => setMenuVisible(true), + }, + ]} + /> + setMenuVisible(false)} + anchor={{ x: Dimensions.get('window').width - 24, y: 56 }} + > + { + setSceneAnimation(undefined); + setMenuVisible(false); + }} + title="Scene animation: none" + /> + { + setSceneAnimation('shifting'); + setMenuVisible(false); + }} + title="Scene animation: shifting" + /> + { + setSceneAnimation('opacity'); + setMenuVisible(false); + }} + title="Scene animation: opacity" + /> + { const [visible, setVisible] = React.useState({}); const [contextualMenuCoord, setContextualMenuCoor] = React.useState({ x: 0, y: 0 }); + const [appbarMenuCoord, setAppbarMenuCoord] = + React.useState({ x: 0, y: 0 }); const _toggleMenu = (name: string) => () => setVisible({ ...visible, [name]: !visible[name] }); @@ -42,6 +44,14 @@ const MenuExample = () => { setVisible({ menu3: true }); }; + const _openAppbarMenu = (event: GestureResponderEvent) => { + setAppbarMenuCoord({ + x: event.nativeEvent.pageX, + y: event.nativeEvent.pageY, + }); + setVisible({ ...visible, menu1: true }); + }; + React.useLayoutEffect(() => { navigation.setOptions({ headerShown: false, @@ -50,24 +60,35 @@ const MenuExample = () => { return ( - - navigation.goBack()} /> - - - } - > - {}} title="Undo" /> - {}} title="Redo" /> - - {}} title="Cut" disabled /> - {}} title="Copy" disabled /> - {}} title="Paste" /> - - + navigation.goBack(), + }} + trailingActions={[ + { + key: 'more', + icon: MORE_ICON, + 'aria-label': 'More options', + onPress: _openAppbarMenu, + }, + ]} + /> + + {}} title="Undo" /> + {}} title="Redo" /> + + {}} title="Cut" disabled /> + {}} title="Copy" disabled /> + {}} title="Paste" /> + { return ( - - navigation.goBack()} /> - - + navigation.goBack(), + }} + /> { React.useLayoutEffect(() => { navigation.setOptions({ header: () => ( - - - navigation.goBack()} /> - - - - {}} /> - - - {}} /> - - - {}} /> - - + navigation.goBack(), + }} + trailingActions={[ + { + key: 'print', + icon: 'printer', + 'aria-label': 'Print', + onPress: () => {}, + }, + { + key: 'search', + icon: 'magnify', + 'aria-label': 'Search', + onPress: () => {}, + }, + { + key: 'more', + icon: MORE_ICON, + 'aria-label': 'More options', + onPress: () => {}, + }, + ]} + /> ), }); }); diff --git a/example/src/RootNavigator.tsx b/example/src/RootNavigator.tsx index 95ea077275..872ef9b79e 100644 --- a/example/src/RootNavigator.tsx +++ b/example/src/RootNavigator.tsx @@ -25,18 +25,20 @@ function Header({ navigation, route, options, back }: NativeStackHeaderProps) { const drawerNavigation = useNavigation('Home'); return ( - - {back ? ( - navigation.goBack()} /> - ) : ( - drawerNavigation.openDrawer()} - /> - )} - - + navigation.goBack() } + : { + icon: 'menu', + 'aria-label': 'Open navigation menu', + onPress: () => drawerNavigation.openDrawer(), + } + } + /> ); } diff --git a/src/components/Appbar/Appbar.tsx b/src/components/Appbar/Appbar.tsx index c42304765b..46a9d00402 100644 --- a/src/components/Appbar/Appbar.tsx +++ b/src/components/Appbar/Appbar.tsx @@ -1,340 +1,451 @@ import * as React from 'react'; -import { Animated, StyleSheet, View } from 'react-native'; -import type { ColorValue, StyleProp, ViewProps, ViewStyle } from 'react-native'; +import { StyleSheet, View } from 'react-native'; +import type { ColorValue, ViewProps } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import AppbarButton from './AppbarButton'; import AppbarContent from './AppbarContent'; +import type { Props } from './types'; import { - getAppbarBackgroundColor, - modeAppbarHeight, - renderAppbarContent, - filterAppbarActions, + APPBAR_HEADLINE_IMAGE_HEIGHT, + APPBAR_ICON_BUTTON_SIZE, + APPBAR_SEARCH_MAX_WIDTH, + getAppbarBorders, + getAppbarHeight, + getTrailingActionsWidth, } from './utils'; -import type { AppbarModes, AppbarChildProps } from './utils'; import { useInternalTheme } from '../../core/theming'; -import type { Elevation, ThemeProp } from '../../types'; +import Searchbar from '../Searchbar'; import Surface from '../Surface'; -export type Props = Omit, 'style'> & { - /** - * Whether the background color is a dark color. A dark appbar will render light text and vice-versa. - */ - dark?: boolean; - /** - * Content of the `Appbar`. - */ - children: React.ReactNode; - /** - * @supported Available in v5.x with theme version 3 - * - * Mode of the Appbar. - * - `small` - Appbar with default height (64). - * - `medium` - Appbar with medium height (112). - * - `large` - Appbar with large height (152). - * - `center-aligned` - Appbar with default height and center-aligned title. - */ - mode?: 'small' | 'medium' | 'large' | 'center-aligned'; - /** - * @supported Available in v5.x with theme version 3 - * Whether Appbar background should have the elevation along with primary color pigment. - */ - elevated?: boolean; - /** - * Safe area insets for the Appbar. This can be used to avoid elements like the navigation bar on Android and bottom safe area on iOS. - */ - safeAreaInsets?: { - bottom?: number; - top?: number; - left?: number; - right?: number; - }; - /** - * @optional - */ - theme?: ThemeProp; - style?: Animated.WithAnimatedValue>; -}; +const EMPTY_TRAILING_ACTIONS = [] as const; /** - * A component to display action items in a bar. It can be placed at the top or bottom. - * The top bar usually contains the screen title, controls such as navigation buttons, menu button etc. - * The bottom bar usually provides access to a drawer and up to four actions. + * A Material Design app bar for displaying a page headline, navigation, and + * contextual actions. Appbar supports small, medium flexible, large flexible, + * and search variants. Its surface color automatically changes when content + * has scrolled, and it accounts for safe-area insets. + * + * ## Variants + * + * | `variant` | Purpose | + * | --- | --- | + * | `small` | A compact 64dp app bar with a one-line headline. | + * | `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. | + * | `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. | + * | `search` | An app bar containing a centered Paper `Searchbar`. | * - * By default Appbar 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 informations + * ## Main props + * + * | Prop | Description | + * | --- | --- | + * | `headline` | Required accessible headline for every non-search variant. | + * | `subtitle` | Optional supporting text for written headlines. | + * | `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. | + * | `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. | + * | `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. | + * | `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. | + * | `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`. | + * | `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. | + * | `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. | + * | `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. | + * | `statusBarHeight` | Overrides only the automatic top inset. | + * | `contentStyle` | Styles the headline and subtitle area. | + * | `style` | Styles the app bar and can override its background color. | + * + * ## Migrating from the compound API + * + * `Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and + * `Appbar.BackAction` have been removed. Render one `Appbar` and provide its + * headline, leading button, and trailing actions as props. The former + * `mode="medium"` and `mode="large"` values are now + * `variant="medium-flexible"` and `variant="large-flexible"`; centered + * content uses `headlineAlignment="center"`. * * ## Usage - * ### Top bar + * + * ### Small app bar * ```js * import * as React from 'react'; * import { Appbar } from 'react-native-paper'; * * const MyComponent = () => ( - * - * {}} /> - * - * {}} /> - * {}} /> - * + * {} }} + * trailingActions={[ + * { + * key: 'search', + * icon: 'magnify', + * 'aria-label': 'Search', + * onPress: () => {}, + * }, + * { + * key: 'more', + * icon: 'dots-vertical', + * 'aria-label': 'More options', + * onPress: () => {}, + * }, + * ]} + * /> * ); * * export default MyComponent; * ``` * - * ### Bottom bar + * ### Flexible app bar * ```js - * import * as React from 'react'; - * import { StyleSheet } from 'react-native'; - * import { Appbar, FAB, useTheme } from 'react-native-paper'; - * import { useSafeAreaInsets } from 'react-native-safe-area-context'; - * - * const BOTTOM_APPBAR_HEIGHT = 80; - * const MEDIUM_FAB_HEIGHT = 56; - * - * const MyComponent = () => { - * const { bottom } = useSafeAreaInsets(); - * const theme = useTheme(); - * - * return ( - * - * {}} /> - * {}} /> - * {}} /> - * {}} /> - * {}} - * style={[ - * styles.fab, - * { top: (BOTTOM_APPBAR_HEIGHT - MEDIUM_FAB_HEIGHT) / 2 }, - * ]} - * /> - * - * ); - * }; - * - * const styles = StyleSheet.create({ - * bottom: { - * backgroundColor: 'aquamarine', - * position: 'absolute', - * left: 0, - * right: 0, - * bottom: 0, - * }, - * fab: { - * position: 'absolute', - * right: 16, - * }, - * }); + * + * ``` * - * export default MyComponent; + * ### Search app bar + * ```js + * {} }} + * searchBar={{ + * placeholder: 'Search messages', + * value: query, + * onChangeText: setQuery, + * }} + * /> * ``` */ const Appbar = ({ - children, - dark, - style, - mode = 'small', - elevated, + contentStyle, + headline, + headlineAlignment = 'leading', + headlineImage, + headlinePressableProps, + headlineProps, + isScrolled = false, + leadingButton, + onHeadlinePress, safeAreaInsets, + searchBar, + statusBarHeight, + style, + subtitle, + subtitleProps, + testID = 'appbar', theme: themeOverrides, + trailingActions = EMPTY_TRAILING_ACTIONS, + variant, + ref, ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); - const flattenedStyle = StyleSheet.flatten(style); - const { - backgroundColor: customBackground, - elevation = elevated ? 2 : 0, - ...restStyle + const detectedInsets = useSafeAreaInsets(); + const { customBackground, restStyle, borderRadius } = React.useMemo(() => { + const flattenedStyle = StyleSheet.flatten(style); // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - } = (flattenedStyle || {}) as Exclude & { - elevation?: Elevation; - backgroundColor?: ColorValue; - }; + const resolvedStyle = (flattenedStyle || {}) as Exclude< + typeof flattenedStyle, + number + > & { + backgroundColor?: ColorValue; + }; + const { backgroundColor, ...remainingStyle } = resolvedStyle; - const backgroundColor = getAppbarBackgroundColor( - theme, - elevation, - customBackground, - elevated + return { + customBackground: backgroundColor, + restStyle: remainingStyle, + borderRadius: getAppbarBorders(remainingStyle), + }; + }, [style]); + const backgroundColor = + customBackground ?? + (isScrolled ? theme.colors.surfaceContainer : theme.colors.surface); + const hasSubtitle = typeof subtitle === 'string' && subtitle.length > 0; + const minHeight = getAppbarHeight(variant, hasSubtitle); + const topInset = statusBarHeight ?? safeAreaInsets?.top ?? detectedInsets.top; + const leftInset = safeAreaInsets?.left ?? detectedInsets.left; + const rightInset = safeAreaInsets?.right ?? detectedInsets.right; + const horizontalInset = Math.max(leftInset, rightInset); + const centered = headlineAlignment === 'center'; + const sideWidth = Math.max( + leadingButton ? APPBAR_ICON_BUTTON_SIZE : 0, + getTrailingActionsWidth(trailingActions) + ); + const sideStyle = React.useMemo(() => ({ width: sideWidth }), [sideWidth]); + const surfaceStyle = React.useMemo( + () => [ + { + backgroundColor, + paddingTop: topInset, + paddingHorizontal: horizontalInset, + }, + borderRadius, + ], + [backgroundColor, borderRadius, horizontalInset, topInset] + ); + const appbarStyle = React.useMemo( + () => [styles.appbar, { backgroundColor, minHeight }, restStyle], + [backgroundColor, minHeight, restStyle] ); + const resolvedSearchInputStyle = React.useMemo( + () => [ + searchBar?.inputStyle, + styles.searchInput, + { color: theme.colors.onSurface }, + ], + [searchBar?.inputStyle, theme.colors.onSurface] + ); + const searchBackgroundColor = isScrolled + ? theme.colors.surfaceContainerHighest + : theme.colors.surfaceContainer; + const resolvedSearchStyle = React.useMemo( + () => [ + styles.searchBar, + { backgroundColor: searchBackgroundColor }, + searchBar?.style, + ], + [searchBackgroundColor, searchBar?.style] + ); + const { + accessibilityLabel: _accessibilityLabel, + accessibilityRole: _accessibilityRole, + accessible: _accessible, + 'aria-label': _ariaLabel, + role: _role, + ...viewProps + } = rest as ViewProps; - const isMode = (modeToCompare: AppbarModes) => { - return mode === modeToCompare; - }; + const renderLeadingButton = () => + leadingButton ? ( + + ) : null; - const isDark = typeof dark === 'boolean' ? dark : false; + const renderTrailingActions = () => + trailingActions.map((action) => ( + + )); - const isCenterAlignedMode = isMode('center-aligned'); + const renderSearchAppbar = () => { + if (!searchBar) { + return null; + } - let shouldCenterContent = false; - let shouldAddLeftSpacing = false; - let shouldAddRightSpacing = false; - if (isCenterAlignedMode) { - let hasAppbarContent = false; - let leftItemsCount = 0; - let rightItemsCount = 0; + const { + inputStyle: _searchInputStyle, + style: _searchStyle, + testID: searchTestID = `${testID}-search`, + ...searchProps + } = searchBar; - React.Children.forEach(children, (child) => { - if (React.isValidElement(child)) { - const isLeading = child.props.isLeading === true; + return ( + + {renderLeadingButton()} + + + + + + {renderTrailingActions()} + + ); + }; - if (child.type === AppbarContent) { - hasAppbarContent = true; - } else if (isLeading || !hasAppbarContent) { - leftItemsCount++; - } else { - rightItemsCount++; + const contentProps = + variant !== 'search' + ? { + alignment: headlineAlignment, + contentStyle, + headline, + headlineImage, + headlinePressableProps, + headlineProps, + onHeadlinePress, + subtitle, + subtitleProps, + testID: `${testID}-content`, + theme, + variant, } - } - }); + : null; + + const renderFlexibleHeadlineImage = () => + headlineImage ? ( + + {headlineImage} + + ) : null; + + const renderSmallAppbar = () => { + if (!contentProps) { + return null; + } - shouldCenterContent = - hasAppbarContent && leftItemsCount < 2 && rightItemsCount < 3; - shouldAddLeftSpacing = shouldCenterContent && leftItemsCount === 0; - shouldAddRightSpacing = shouldCenterContent && rightItemsCount === 0; - } + if (centered || headlineImage) { + return ( + + {renderLeadingButton()} + + + {renderTrailingActions()} + + + ); + } + + return ( + + {renderLeadingButton()} + + {renderTrailingActions()} + + ); + }; - const spacingStyle = styles.v3Spacing; + const renderFlexibleAppbar = () => { + if (!contentProps) { + return null; + } - const insets = { - paddingBottom: safeAreaInsets?.bottom, - paddingTop: safeAreaInsets?.top, - paddingLeft: safeAreaInsets?.left, - paddingRight: safeAreaInsets?.right, + return ( + + {headlineImage ? ( + + + {renderLeadingButton()} + + {renderFlexibleHeadlineImage()} + + {renderTrailingActions()} + + + ) : ( + + {renderLeadingButton()} + + {renderTrailingActions()} + + + )} + + + ); }; return ( - {shouldAddLeftSpacing ? : null} - {(isMode('small') || isMode('center-aligned')) && ( - <> - {/* Render only the back action at first place */} - {renderAppbarContent({ - children, - isDark, - theme, - renderOnly: ['Appbar.BackAction'], - shouldCenterContent: isCenterAlignedMode || shouldCenterContent, - })} - {/* Render the rest of the content except the back action */} - {renderAppbarContent({ - // Filter appbar actions - first leading icons, then trailing icons - children: [ - ...filterAppbarActions(children, true), - ...filterAppbarActions(children), - ], - isDark, - theme, - renderExcept: ['Appbar.BackAction'], - shouldCenterContent: isCenterAlignedMode || shouldCenterContent, - })} - - )} - {(isMode('medium') || isMode('large')) && ( - - {/* Appbar top row with controls */} - - {/* Left side of row container, can contain AppbarBackAction or AppbarAction if it's leading icon */} - {renderAppbarContent({ - children, - isDark, - renderOnly: ['Appbar.BackAction'], - mode, - })} - {renderAppbarContent({ - children: filterAppbarActions(children, true), - isDark, - renderOnly: ['Appbar.Action'], - mode, - })} - {/* Right side of row container, can contain other AppbarAction if they are not leading icons */} - - {renderAppbarContent({ - children: filterAppbarActions(children), - isDark, - renderExcept: [ - 'Appbar', - 'Appbar.BackAction', - 'Appbar.Content', - 'Appbar.Header', - ], - mode, - })} - - - {renderAppbarContent({ - children, - isDark, - renderOnly: ['Appbar.Content'], - mode, - })} - - )} - {shouldAddRightSpacing ? : null} + + {variant === 'search' + ? renderSearchAppbar() + : variant === 'small' + ? renderSmallAppbar() + : renderFlexibleAppbar()} + ); }; const styles = StyleSheet.create({ appbar: { - flexDirection: 'row', - alignItems: 'center', paddingHorizontal: 4, }, - v3Spacing: { - width: 52, + smallRow: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', }, - controlsRow: { + searchRow: { flex: 1, flexDirection: 'row', alignItems: 'center', + }, + searchSlot: { + flex: 1, + marginHorizontal: 8, + alignItems: 'center', + }, + searchBar: { + width: '100%', + }, + searchWidthLimiter: { + width: '100%', + maxWidth: APPBAR_SEARCH_MAX_WIDTH, + }, + searchInput: { + textAlign: 'center', + }, + flexibleContainer: { + flex: 1, + }, + controlsRow: { + minHeight: 56, + flexDirection: 'row', + alignItems: 'flex-start', justifyContent: 'space-between', }, - rightActionControls: { + trailingActions: { flexDirection: 'row', - flex: 1, justifyContent: 'flex-end', }, - columnContainer: { - flexDirection: 'column', - flex: 1, - paddingTop: 8, + side: { + flexDirection: 'row', + }, + headlineLeading: { + marginStart: 12, }, - centerAlignedContainer: { - paddingTop: 0, + headlineWithLeading: { + marginStart: 4, + }, + flexibleHeadlineImage: { + flex: 1, + height: APPBAR_HEADLINE_IMAGE_HEIGHT, + maxWidth: '100%', + marginTop: 12, + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', }, }); -export default Appbar; +const MemoizedAppbar = React.memo(Appbar); + +export default MemoizedAppbar; // @component-docs ignore-next-line -export { Appbar }; +export { MemoizedAppbar as Appbar }; diff --git a/src/components/Appbar/AppbarAction.tsx b/src/components/Appbar/AppbarAction.tsx deleted file mode 100644 index 400271a988..0000000000 --- a/src/components/Appbar/AppbarAction.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import * as React from 'react'; -import type { - Animated, - ColorValue, - StyleProp, - View, - ViewStyle, -} from 'react-native'; - -import { useInternalTheme } from '../../core/theming'; -import type { ThemeProp } from '../../types'; -import type { IconSource } from '../Icon'; -import IconButton from '../IconButton/IconButton'; - -export type Props = React.ComponentPropsWithoutRef & { - /** - * Custom color for action icon. - */ - color?: ColorValue; - /** - * Name of the icon to show. - */ - icon: IconSource; - /** - * Optional icon size. - */ - size?: number; - /** - * Whether the button is disabled. A disabled button is greyed out and `onPress` is not called on touch. - */ - disabled?: boolean; - /** - * Accessibility label for the button. This is read by the screen reader when the user taps the button. - */ - 'aria-label'?: string; - /** - * Function to execute on press. - */ - onPress?: () => void; - /** - * @supported Available in v5.x with theme version 3 - * - * Whether it's the leading button. Note: If `Appbar.BackAction` is present, it will be rendered before any `isLeading` icons. - */ - isLeading?: boolean; - style?: Animated.WithAnimatedValue>; - ref?: React.Ref; - /** - * @optional - */ - theme?: ThemeProp; -}; - -/** - * A component used to display an action item in the appbar. - * - * ## Usage - * ```js - * import * as React from 'react'; - * import { Appbar } from 'react-native-paper'; - * import { Platform } from 'react-native'; - * - * const MORE_ICON = Platform.OS === 'ios' ? 'dots-horizontal' : 'dots-vertical'; - * - * const MyComponent = () => ( - * - * - * {}} /> - * {}} /> - * - * ); - * - * export default MyComponent; - * ``` - */ -const AppbarAction = ({ - size = 24, - color: iconColor, - icon, - disabled, - onPress, - 'aria-label': ariaLabel, - isLeading, - theme: themeOverrides, - ref, - ...rest -}: Props) => { - const theme = useInternalTheme(themeOverrides); - const { colors } = theme; - - const actionIconColor = iconColor - ? iconColor - : isLeading - ? colors.onSurface - : colors.onSurfaceVariant; - - return ( - - ); -}; - -AppbarAction.displayName = 'Appbar.Action'; - -export default AppbarAction; - -// @component-docs ignore-next-line -export { AppbarAction }; diff --git a/src/components/Appbar/AppbarBackAction.tsx b/src/components/Appbar/AppbarBackAction.tsx deleted file mode 100644 index 2835c27c91..0000000000 --- a/src/components/Appbar/AppbarBackAction.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import * as React from 'react'; -import type { - Animated, - ColorValue, - GestureResponderEvent, - StyleProp, - View, - ViewStyle, -} from 'react-native'; - -import type { $Omit } from './../../types'; -import AppbarAction from './AppbarAction'; -import AppbarBackIcon from './AppbarBackIcon'; - -export type Props = $Omit< - React.ComponentPropsWithoutRef, - 'icon' -> & { - /** - * Custom color for back icon. - */ - color?: ColorValue; - /** - * Optional icon size. - */ - size?: number; - /** - * Whether the button is disabled. A disabled button is greyed out and `onPress` is not called on touch. - */ - disabled?: boolean; - /** - * Accessibility label for the button. This is read by the screen reader when the user taps the button. - */ - 'aria-label'?: string; - /** - * Function to execute on press. - */ - onPress?: (e: GestureResponderEvent) => void; - style?: Animated.WithAnimatedValue>; - ref?: React.Ref; -}; - -/** - * A component used to display a back button in the appbar. - * - * ## Usage - * ```js - * import * as React from 'react'; - * import { Appbar } from 'react-native-paper'; - * - * const MyComponent = () => ( - * - * {}} /> - * - * ); - * - * export default MyComponent; - * ``` - */ -const AppbarBackAction = ({ - 'aria-label': ariaLabel = 'Back', - ref, - ...rest -}: Props) => ( - -); - -AppbarBackAction.displayName = 'Appbar.BackAction'; - -export default AppbarBackAction; - -// @component-docs ignore-next-line -export { AppbarBackAction }; diff --git a/src/components/AppbarV3/AppbarButton.tsx b/src/components/Appbar/AppbarButton.tsx similarity index 97% rename from src/components/AppbarV3/AppbarButton.tsx rename to src/components/Appbar/AppbarButton.tsx index b6e7d942aa..9fb694c330 100644 --- a/src/components/AppbarV3/AppbarButton.tsx +++ b/src/components/Appbar/AppbarButton.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import { StyleSheet } from 'react-native'; +import AppbarBackIcon from './AppbarBackIcon'; import type { AppbarLeadingButton, AppbarTrailingAction } from './types'; import type { Theme } from '../../types'; -import AppbarBackIcon from '../Appbar/AppbarBackIcon'; import IconButton from '../IconButton/IconButton'; type Props = { diff --git a/src/components/Appbar/AppbarContent.tsx b/src/components/Appbar/AppbarContent.tsx index 826477619d..96d8e0dee6 100644 --- a/src/components/Appbar/AppbarContent.tsx +++ b/src/components/Appbar/AppbarContent.tsx @@ -1,228 +1,216 @@ import * as React from 'react'; -import { StyleSheet, Pressable, View } from 'react-native'; -import type { - GestureResponderEvent, - StyleProp, - TextStyle, - ViewStyle, - ViewProps, -} from 'react-native'; +import { Pressable, StyleSheet, View } from 'react-native'; +import type { StyleProp, ViewStyle } from 'react-native'; -import { - modeTextVariant, - modeSubtitleTextVariant, - modeSubtitleTextSpacing, -} from './utils'; -import { useInternalTheme } from '../../core/theming'; -import type { TypescaleKey } from '../../theme/types'; -import type { $RemoveChildren, ThemeProp } from '../../types'; +import type { + AppbarHeadlineAlignment, + AppbarHeadlineVariant, + Props as AppbarProps, +} from './types'; +import { APPBAR_HEADLINE_IMAGE_HEIGHT } from './utils'; +import type { Theme, TypescaleKey } from '../../types'; import Text from '../Typography/Text'; -import type { TextRef } from '../Typography/Text'; -type TitleString = { - title: string; - titleStyle?: StyleProp; +type Props = Pick< + AppbarProps, + | 'contentStyle' + | 'headline' + | 'headlineImage' + | 'headlinePressableProps' + | 'headlineProps' + | 'onHeadlinePress' + | 'subtitle' + | 'subtitleProps' +> & { + alignment: AppbarHeadlineAlignment; + theme: Theme; + variant: AppbarHeadlineVariant; + style?: StyleProp; + testID: string; }; -type TitleElement = { title: React.ReactNode; titleStyle?: never }; +const headlineVariants: Record = { + small: 'titleLarge', + 'medium-flexible': 'headlineMedium', + 'large-flexible': 'displaySmall', +}; -export type Props = $RemoveChildren & { - // For `title` and `titleStyle` props their types are duplicated due to the generation of documentation. - // Appropriate type for them are either `TitleString` or `TitleElement`, depends on `title` type. - /** - * Text or component for the title. - */ - title: React.ReactNode; - /** - * Style for the title, if `title` is a string. - */ - titleStyle?: StyleProp; - /** - * Reference for the title. - */ - titleRef?: React.RefObject; - /** - * Text or component for the subtitle. - */ - subtitle?: React.ReactNode; - /** - * Style for the subtitle. - */ - subtitleStyle?: StyleProp; - /** - * Function to execute on press. - */ - onPress?: (e: GestureResponderEvent) => void; - /** - * If true, disable all interactions for this component. - */ - disabled?: boolean; - /** - * Custom color for the text. - */ - color?: string; - /** - * Specifies the largest possible scale a title font can reach. - */ - titleMaxFontSizeMultiplier?: number; - /** - * @internal - */ - mode?: 'small' | 'medium' | 'large' | 'center-aligned'; - style?: StyleProp; - /** - * @optional - */ - theme?: ThemeProp; - /** - * testID to be used on tests. - */ - testID?: string; -} & (TitleString | TitleElement); +const subtitleVariants: Record = { + small: 'labelMedium', + 'medium-flexible': 'labelLarge', + 'large-flexible': 'titleMedium', +}; + +const subtitleSpacing: Record = { + small: 0, + 'medium-flexible': 4, + 'large-flexible': 8, +}; -/** - * A component used to display a title and optional subtitle in an appbar. - * - * ## Usage - * ```js - * import * as React from 'react'; - * import { Appbar } from 'react-native-paper'; - * - * const MyComponent = () => ( - * - * - * - * ); - * - * export default MyComponent; - * ``` - */ const AppbarContent = ({ - color: titleColor, + alignment, + contentStyle, + headline, + headlineImage, + headlinePressableProps, + headlineProps, + onHeadlinePress, subtitle, - subtitleStyle, - onPress, - disabled, + subtitleProps, + testID, + theme, + variant, style, - titleRef, - titleStyle, - title, - titleMaxFontSizeMultiplier, - mode = 'small', - theme: themeOverrides, - testID = 'appbar-content', - ...rest }: Props) => { - const theme = useInternalTheme(themeOverrides); - const { colors, fonts } = theme; - - const titleTextColor = titleColor ? titleColor : colors.onSurface; - - const modeContainerStyles = { - small: styles.v3DefaultContainer, - medium: styles.v3MediumContainer, - large: styles.v3LargeContainer, - 'center-aligned': styles.v3DefaultContainer, - }; - - const variant = modeTextVariant[mode] as TypescaleKey; - const subtitleVariant = modeSubtitleTextVariant[mode] as TypescaleKey; - const subtitleTopPadding = modeSubtitleTextSpacing[mode]; - - const contentWrapperProps = { - pointerEvents: 'box-none', - style: [styles.container, modeContainerStyles[mode], style], - testID, - ...rest, - } satisfies ViewProps; + const headlineVariant = headlineVariants[variant]; + const subtitleVariant = subtitleVariants[variant]; + const centered = alignment === 'center'; + const hasHeadlineImage = variant === 'small' && Boolean(headlineImage); + const headlineStyle = React.useMemo( + () => [ + styles.text, + centered && styles.centeredText, + { color: theme.colors.onSurface }, + headlineProps?.style, + ], + [centered, headlineProps?.style, theme.colors.onSurface] + ); + const subtitleStyle = React.useMemo( + () => [ + styles.text, + centered && styles.centeredText, + { marginTop: subtitleSpacing[variant] }, + { color: theme.colors.onSurfaceVariant }, + subtitleProps?.style, + ], + [centered, subtitleProps?.style, theme.colors.onSurfaceVariant, variant] + ); + const wrapperStyle = React.useMemo( + () => [ + styles.container, + variant !== 'small' && styles.flexibleContainer, + centered && styles.centeredContainer, + style, + contentStyle, + ], + [centered, contentStyle, style, variant] + ); - const content = ( + const content = hasHeadlineImage ? ( + + {headlineImage} + + ) : ( <> - {typeof title === 'string' ? ( - <> - - {title} - - {subtitle ? ( - - {subtitle} - - ) : null} - - ) : ( - title - )} + + {headline} + + {subtitle ? ( + + {subtitle} + + ) : null} ); - if (onPress) { + const wrapperProps = { + testID, + style: wrapperStyle, + }; + + if (onHeadlinePress) { + const { + 'aria-label': ariaLabel, + accessibilityLabel, + disabled, + ...restHeadlinePressableProps + } = headlinePressableProps ?? {}; + return ( {content} ); } - return {content}; + return ( + + {content} + + ); }; -AppbarContent.displayName = 'Appbar.Content'; - const styles = StyleSheet.create({ container: { flex: 1, - paddingHorizontal: 12, + justifyContent: 'center', + minWidth: 0, }, - v3DefaultContainer: { - paddingHorizontal: 0, + flexibleContainer: { + flex: 0, + paddingHorizontal: 16, + paddingTop: 8, + paddingBottom: 12, }, - v3MediumContainer: { - paddingHorizontal: 0, - justifyContent: 'flex-end', - paddingBottom: 24, + centeredContainer: { + alignItems: 'center', }, - v3LargeContainer: { - paddingHorizontal: 0, - paddingTop: 36, - justifyContent: 'flex-end', - paddingBottom: 28, + text: { + alignSelf: 'stretch', + }, + centeredText: { + textAlign: 'center', + }, + headlineImage: { + height: APPBAR_HEADLINE_IMAGE_HEIGHT, + maxWidth: '100%', + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', }, }); -export default AppbarContent; - -// @component-docs ignore-next-line -export { AppbarContent }; +export default React.memo(AppbarContent); diff --git a/src/components/Appbar/AppbarHeader.tsx b/src/components/Appbar/AppbarHeader.tsx deleted file mode 100644 index 0252cc89ba..0000000000 --- a/src/components/Appbar/AppbarHeader.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import * as React from 'react'; -import { Animated, Platform, StyleSheet, View } from 'react-native'; -import type { ColorValue, StyleProp, ViewStyle } from 'react-native'; - -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -import { Appbar } from './Appbar'; -import { - getAppbarBackgroundColor, - modeAppbarHeight, - getAppbarBorders, -} from './utils'; -import { useInternalTheme } from '../../core/theming'; -import { shadow } from '../../theme/tokens/sys/elevation'; -import type { ThemeProp } from '../../types'; - -export type Props = Omit< - React.ComponentProps, - 'safeAreaInsets' -> & { - /** - * Whether the background color is a dark color. A dark header will render light text and vice-versa. - */ - dark?: boolean; - /** - * Extra padding to add at the top of header to account for translucent status bar. - * This is automatically handled on iOS >= 11 including iPhone X using `SafeAreaView`. - * If you are using Expo, we assume translucent status bar and set a height for status bar automatically. - * Pass `0` or a custom value to disable the default behaviour, and customize the height. - */ - statusBarHeight?: number; - /** - * Content of the header. - */ - children: React.ReactNode; - /** - * @supported Available in v5.x with theme version 3 - * - * Mode of the Appbar. - * - `small` - Appbar with default height (64). - * - `medium` - Appbar with medium height (112). - * - `large` - Appbar with large height (152). - * - `center-aligned` - Appbar with default height and center-aligned title. - */ - mode?: 'small' | 'medium' | 'large' | 'center-aligned'; - /** - * @supported Available in v5.x with theme version 3 - * Whether Appbar background should have the elevation along with primary color pigment. - */ - elevated?: boolean; - /** - * @optional - */ - theme?: ThemeProp; - style?: Animated.WithAnimatedValue>; -}; - -/** - * A component to use as a header at the top of the screen. - * It can contain the screen title, controls such as navigation buttons, menu button etc. - * - * ## Usage - * ```js - * import * as React from 'react'; - * import { Appbar } from 'react-native-paper'; - * - * const MyComponent = () => { - * const _goBack = () => console.log('Went back'); - * - * const _handleSearch = () => console.log('Searching'); - * - * const _handleMore = () => console.log('Shown more'); - * - * return ( - * - * - * - * - * - * - * ); - * }; - * - * export default MyComponent; - * ``` - */ -const AppbarHeader = ({ - // Don't use default props since we check it to know whether we should use SafeAreaView - statusBarHeight, - style, - dark, - mode = Platform.OS === 'ios' ? 'center-aligned' : 'small', - elevated = false, - theme: themeOverrides, - testID = 'appbar-header', - ...rest -}: Props) => { - const theme = useInternalTheme(themeOverrides); - - const flattenedStyle = StyleSheet.flatten(style); - const { - height = modeAppbarHeight[mode], - elevation = elevated ? 2 : 0, - backgroundColor: customBackground, - zIndex = elevated ? 1 : 0, - ...restStyle - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - } = (flattenedStyle || {}) as Exclude & { - height?: number; - elevation?: number; - backgroundColor?: ColorValue; - zIndex?: number; - }; - - const borderRadius = getAppbarBorders(restStyle); - - const backgroundColor = getAppbarBackgroundColor( - theme, - elevation, - customBackground, - elevated - ); - - const { top, left, right } = useSafeAreaInsets(); - - return ( - - - - ); -}; - -AppbarHeader.displayName = 'Appbar.Header'; - -const styles = StyleSheet.create({ - appbar: { - elevation: 0, - }, -}); - -export default AppbarHeader; - -// @component-docs ignore-next-line -export { AppbarHeader }; diff --git a/src/components/Appbar/index.ts b/src/components/Appbar/index.ts index 498c8761f2..d8ba839263 100644 --- a/src/components/Appbar/index.ts +++ b/src/components/Appbar/index.ts @@ -1,22 +1,16 @@ -import AppbarComponent from './Appbar'; -import AppbarAction from './AppbarAction'; -import AppbarBackAction from './AppbarBackAction'; -import AppbarContent from './AppbarContent'; -import AppbarHeader from './AppbarHeader'; - -const Appbar = Object.assign( - // @component ./Appbar.tsx - AppbarComponent, - { - // @component ./AppbarContent.tsx - Content: AppbarContent, - // @component ./AppbarAction.tsx - Action: AppbarAction, - // @component ./AppbarBackAction.tsx - BackAction: AppbarBackAction, - // @component ./AppbarHeader.tsx - Header: AppbarHeader, - } -); - -export default Appbar; +export { default } from './Appbar'; +export { Appbar } from './Appbar'; +export type { + AppbarFilledTrailingAction, + AppbarHeadlineAlignment, + AppbarHeadlinePressableProps, + AppbarHeadlineTextProps, + AppbarLeadingButton, + AppbarSearchbarProps, + AppbarStandardTrailingAction, + AppbarTextProps, + AppbarTrailingAction, + AppbarTrailingActions, + AppbarVariant, + Props, +} from './types'; diff --git a/src/components/AppbarV3/types.ts b/src/components/Appbar/types.ts similarity index 100% rename from src/components/AppbarV3/types.ts rename to src/components/Appbar/types.ts diff --git a/src/components/Appbar/utils.ts b/src/components/Appbar/utils.ts index 85c5eecc7a..f42801754c 100644 --- a/src/components/Appbar/utils.ts +++ b/src/components/Appbar/utils.ts @@ -1,58 +1,19 @@ -import React from 'react'; -import type { ColorValue, StyleProp, ViewStyle } from 'react-native'; -import { StyleSheet, Animated } from 'react-native'; +import type { Animated, ViewStyle } from 'react-native'; -import { white } from '../../theme/colors'; -import type { InternalTheme, ThemeProp } from '../../types'; +import type { AppbarTrailingAction, AppbarVariant } from './types'; -export type AppbarModes = 'small' | 'medium' | 'large' | 'center-aligned'; +export const APPBAR_ICON_BUTTON_SIZE = 48; +export const APPBAR_WIDE_ICON_BUTTON_SIZE = 64; +export const APPBAR_HEADLINE_IMAGE_HEIGHT = 32; +export const APPBAR_SEARCH_MAX_WIDTH = 720; -export type AppbarChildProps = { - isLeading?: boolean; - color: string; - style?: StyleProp; -}; - -const borderStyleProperties = [ +const borderStyleProperties: readonly (keyof ViewStyle)[] = [ 'borderRadius', 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius', -] satisfies readonly (keyof ViewStyle)[]; - -export const getAppbarBackgroundColor = ( - theme: InternalTheme, - _elevation: number, - customBackground?: ColorValue, - elevated?: boolean -) => { - const { colors } = theme; - if (customBackground) { - return customBackground; - } - - if (elevated) { - return colors.surfaceContainer; - } - - return colors.surface; -}; - -export const getAppbarColor = ({ - color, - isDark, -}: BaseProps & { color: string }) => { - if (typeof color !== 'undefined') { - return color; - } - - if (isDark) { - return white; - } - - return undefined; -}; +]; export const getAppbarBorders = ( style: @@ -73,123 +34,29 @@ export const getAppbarBorders = ( return borders; }; -type BaseProps = { - isDark: boolean; -}; - -type RenderAppbarContentProps = BaseProps & { - children: React.ReactNode; - shouldCenterContent?: boolean; - renderOnly?: (string | boolean)[]; - renderExcept?: string[]; - mode?: AppbarModes; - theme?: ThemeProp; -}; - -const MD3_DEFAULT_APPBAR_HEIGHT = 64; - -export const modeAppbarHeight = { - small: MD3_DEFAULT_APPBAR_HEIGHT, - medium: 112, - large: 152, - 'center-aligned': MD3_DEFAULT_APPBAR_HEIGHT, -}; - -export const modeTextVariant = { - small: 'titleLarge', - medium: 'headlineSmall', - large: 'headlineMedium', - 'center-aligned': 'titleLarge', -} as const; - -export const modeSubtitleTextVariant = { - small: 'labelSmall', - medium: 'labelMedium', - large: 'labelLarge', - 'center-aligned': 'labelSmall', -} as const; - -export const modeSubtitleTextSpacing = { - small: undefined, - medium: 4, - large: 8, - 'center-aligned': undefined, -} as const satisfies Record; - -export const filterAppbarActions = ( - children: React.ReactNode, - isLeading = false +export const getAppbarHeight = ( + variant: AppbarVariant, + hasSubtitle: boolean ) => { - return React.Children.toArray(children).filter((child) => { - if (!React.isValidElement(child)) return false; - return isLeading ? child.props.isLeading : !child.props.isLeading; - }); -}; - -export const renderAppbarContent = ({ - children, - isDark, - shouldCenterContent = false, - renderOnly, - renderExcept, - mode = 'small', - theme, -}: RenderAppbarContentProps) => { - return React.Children.toArray(children) - .filter((child) => child != null && typeof child !== 'boolean') - .filter((child) => - // @ts-expect-error: TypeScript complains about the type of type but it doesn't matter - renderExcept ? !renderExcept.includes(child.type.displayName) : child - ) - .filter((child) => - // @ts-expect-error: TypeScript complains about the type of type but it doesn't matter - renderOnly ? renderOnly.includes(child.type.displayName) : child - ) - .map((child, i) => { - if ( - !React.isValidElement(child) || - ![ - 'Appbar.Content', - 'Appbar.Action', - 'Appbar.BackAction', - 'Tooltip', - ].includes( - // @ts-expect-error: TypeScript complains about the type of type but it doesn't matter - child.type.displayName - ) - ) { - return child; - } + if (variant === 'search' || variant === 'small') { + return 64; + } - const props: { - color?: string; - style?: StyleProp; - mode?: AppbarModes; - theme?: ThemeProp; - } = { - theme, - color: getAppbarColor({ color: child.props.color, isDark }), - }; + if (variant === 'medium-flexible') { + return hasSubtitle ? 136 : 112; + } - // @ts-expect-error: TypeScript complains about the type of type but it doesn't matter - if (child.type.displayName === 'Appbar.Content') { - props.mode = mode; - props.style = [ - i === 0 && !shouldCenterContent && styles.v3Spacing, - shouldCenterContent && styles.centerAlignedContent, - child.props.style, - ]; - props.color; - } - return React.cloneElement(child, props); - }); + return hasSubtitle ? 152 : 120; }; -const styles = StyleSheet.create({ - centerAlignedContent: { - alignItems: 'center', - }, - v3Spacing: { - marginLeft: 12, - }, -}); +export const getTrailingActionsWidth = ( + actions: readonly AppbarTrailingAction[] +) => + actions.reduce( + (width, action) => + width + + (action.variant !== 'standard' && action.width === 'wide' + ? APPBAR_WIDE_ICON_BUTTON_SIZE + : APPBAR_ICON_BUTTON_SIZE), + 0 + ); diff --git a/src/components/AppbarV3/Appbar.tsx b/src/components/AppbarV3/Appbar.tsx deleted file mode 100644 index 4d756ca44f..0000000000 --- a/src/components/AppbarV3/Appbar.tsx +++ /dev/null @@ -1,353 +0,0 @@ -import * as React from 'react'; -import { StyleSheet, View } from 'react-native'; -import type { ColorValue, ViewProps } from 'react-native'; - -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -import AppbarButton from './AppbarButton'; -import AppbarContent from './AppbarContent'; -import type { Props } from './types'; -import { - APPBAR_HEADLINE_IMAGE_HEIGHT, - APPBAR_ICON_BUTTON_SIZE, - APPBAR_SEARCH_MAX_WIDTH, - getAppbarHeight, - getTrailingActionsWidth, -} from './utils'; -import { useInternalTheme } from '../../core/theming'; -import { getAppbarBorders } from '../Appbar/utils'; -import Searchbar from '../Searchbar'; -import Surface from '../Surface'; - -const EMPTY_TRAILING_ACTIONS = [] as const; - -const Appbar = ({ - contentStyle, - headline, - headlineAlignment = 'leading', - headlineImage, - headlinePressableProps, - headlineProps, - isScrolled = false, - leadingButton, - onHeadlinePress, - safeAreaInsets, - searchBar, - statusBarHeight, - style, - subtitle, - subtitleProps, - testID = 'appbar', - theme: themeOverrides, - trailingActions = EMPTY_TRAILING_ACTIONS, - variant, - ref, - ...rest -}: Props) => { - const theme = useInternalTheme(themeOverrides); - const detectedInsets = useSafeAreaInsets(); - const { customBackground, restStyle, borderRadius } = React.useMemo(() => { - const flattenedStyle = StyleSheet.flatten(style); - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - const resolvedStyle = (flattenedStyle || {}) as Exclude< - typeof flattenedStyle, - number - > & { - backgroundColor?: ColorValue; - }; - const { backgroundColor, ...remainingStyle } = resolvedStyle; - - return { - customBackground: backgroundColor, - restStyle: remainingStyle, - borderRadius: getAppbarBorders(remainingStyle), - }; - }, [style]); - const backgroundColor = - customBackground ?? - (isScrolled ? theme.colors.surfaceContainer : theme.colors.surface); - const hasSubtitle = typeof subtitle === 'string' && subtitle.length > 0; - const minHeight = getAppbarHeight(variant, hasSubtitle); - const topInset = statusBarHeight ?? safeAreaInsets?.top ?? detectedInsets.top; - const leftInset = safeAreaInsets?.left ?? detectedInsets.left; - const rightInset = safeAreaInsets?.right ?? detectedInsets.right; - const horizontalInset = Math.max(leftInset, rightInset); - const centered = headlineAlignment === 'center'; - const sideWidth = Math.max( - leadingButton ? APPBAR_ICON_BUTTON_SIZE : 0, - getTrailingActionsWidth(trailingActions) - ); - const sideStyle = React.useMemo(() => ({ width: sideWidth }), [sideWidth]); - const surfaceStyle = React.useMemo( - () => [ - { - backgroundColor, - paddingTop: topInset, - paddingHorizontal: horizontalInset, - }, - borderRadius, - ], - [backgroundColor, borderRadius, horizontalInset, topInset] - ); - const appbarStyle = React.useMemo( - () => [styles.appbar, { backgroundColor, minHeight }, restStyle], - [backgroundColor, minHeight, restStyle] - ); - const resolvedSearchInputStyle = React.useMemo( - () => [ - searchBar?.inputStyle, - styles.searchInput, - { color: theme.colors.onSurface }, - ], - [searchBar?.inputStyle, theme.colors.onSurface] - ); - const searchBackgroundColor = isScrolled - ? theme.colors.surfaceContainerHighest - : theme.colors.surfaceContainer; - const resolvedSearchStyle = React.useMemo( - () => [ - styles.searchBar, - { backgroundColor: searchBackgroundColor }, - searchBar?.style, - ], - [searchBackgroundColor, searchBar?.style] - ); - const { - accessibilityLabel: _accessibilityLabel, - accessibilityRole: _accessibilityRole, - accessible: _accessible, - 'aria-label': _ariaLabel, - role: _role, - ...viewProps - } = rest as ViewProps; - - const renderLeadingButton = () => - leadingButton ? ( - - ) : null; - - const renderTrailingActions = () => - trailingActions.map((action) => ( - - )); - - const renderSearchAppbar = () => { - if (!searchBar) { - return null; - } - - const { - inputStyle: _searchInputStyle, - style: _searchStyle, - testID: searchTestID = `${testID}-search`, - ...searchProps - } = searchBar; - - return ( - - {renderLeadingButton()} - - - - - - {renderTrailingActions()} - - ); - }; - - const contentProps = - variant !== 'search' - ? { - alignment: headlineAlignment, - contentStyle, - headline, - headlineImage, - headlinePressableProps, - headlineProps, - onHeadlinePress, - subtitle, - subtitleProps, - testID: `${testID}-content`, - theme, - variant, - } - : null; - - const renderFlexibleHeadlineImage = () => - headlineImage ? ( - - {headlineImage} - - ) : null; - - const renderSmallAppbar = () => { - if (!contentProps) { - return null; - } - - if (centered || headlineImage) { - return ( - - {renderLeadingButton()} - - - {renderTrailingActions()} - - - ); - } - - return ( - - {renderLeadingButton()} - - {renderTrailingActions()} - - ); - }; - - const renderFlexibleAppbar = () => { - if (!contentProps) { - return null; - } - - return ( - - {headlineImage ? ( - - - {renderLeadingButton()} - - {renderFlexibleHeadlineImage()} - - {renderTrailingActions()} - - - ) : ( - - {renderLeadingButton()} - - {renderTrailingActions()} - - - )} - - - ); - }; - - return ( - - - {variant === 'search' - ? renderSearchAppbar() - : variant === 'small' - ? renderSmallAppbar() - : renderFlexibleAppbar()} - - - ); -}; - -const styles = StyleSheet.create({ - appbar: { - paddingHorizontal: 4, - }, - smallRow: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - }, - searchRow: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - }, - searchSlot: { - flex: 1, - marginHorizontal: 8, - alignItems: 'center', - }, - searchBar: { - width: '100%', - }, - searchWidthLimiter: { - width: '100%', - maxWidth: APPBAR_SEARCH_MAX_WIDTH, - }, - searchInput: { - textAlign: 'center', - }, - flexibleContainer: { - flex: 1, - }, - controlsRow: { - minHeight: 56, - flexDirection: 'row', - alignItems: 'flex-start', - justifyContent: 'space-between', - }, - trailingActions: { - flexDirection: 'row', - justifyContent: 'flex-end', - }, - side: { - flexDirection: 'row', - }, - headlineLeading: { - marginStart: 12, - }, - headlineWithLeading: { - marginStart: 4, - }, - flexibleHeadlineImage: { - flex: 1, - height: APPBAR_HEADLINE_IMAGE_HEIGHT, - maxWidth: '100%', - marginTop: 12, - alignItems: 'center', - justifyContent: 'center', - overflow: 'hidden', - }, -}); - -const MemoizedAppbar = React.memo(Appbar); - -export default MemoizedAppbar; - -// @component-docs ignore-next-line -export { MemoizedAppbar as Appbar }; diff --git a/src/components/AppbarV3/AppbarContent.tsx b/src/components/AppbarV3/AppbarContent.tsx deleted file mode 100644 index ecc54513ae..0000000000 --- a/src/components/AppbarV3/AppbarContent.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import * as React from 'react'; -import { Pressable, StyleSheet, View } from 'react-native'; -import type { StyleProp, ViewStyle } from 'react-native'; - -import type { - AppbarHeadlineAlignment, - AppbarHeadlineVariant, - Props as AppbarProps, -} from './types'; -import { APPBAR_HEADLINE_IMAGE_HEIGHT } from './utils'; -import type { Theme, TypescaleKey } from '../../types'; -import Text from '../Typography/Text'; - -type Props = Pick< - AppbarProps, - | 'contentStyle' - | 'headline' - | 'headlineImage' - | 'headlinePressableProps' - | 'headlineProps' - | 'onHeadlinePress' - | 'subtitle' - | 'subtitleProps' -> & { - alignment: AppbarHeadlineAlignment; - theme: Theme; - variant: AppbarHeadlineVariant; - style?: StyleProp; - testID: string; -}; - -const headlineVariants = { - small: 'titleLarge', - 'medium-flexible': 'headlineMedium', - 'large-flexible': 'displaySmall', -} as const satisfies Record; - -const subtitleVariants = { - small: 'labelMedium', - 'medium-flexible': 'labelLarge', - 'large-flexible': 'titleMedium', -} as const satisfies Record; - -const subtitleSpacing = { - small: 0, - 'medium-flexible': 4, - 'large-flexible': 8, -} as const satisfies Record; - -const AppbarContent = ({ - alignment, - contentStyle, - headline, - headlineImage, - headlinePressableProps, - headlineProps, - onHeadlinePress, - subtitle, - subtitleProps, - testID, - theme, - variant, - style, -}: Props) => { - const headlineVariant = headlineVariants[variant]; - const subtitleVariant = subtitleVariants[variant]; - const centered = alignment === 'center'; - const hasHeadlineImage = variant === 'small' && Boolean(headlineImage); - const headlineStyle = React.useMemo( - () => [ - styles.text, - centered && styles.centeredText, - { color: theme.colors.onSurface }, - headlineProps?.style, - ], - [centered, headlineProps?.style, theme.colors.onSurface] - ); - const subtitleStyle = React.useMemo( - () => [ - styles.text, - centered && styles.centeredText, - { marginTop: subtitleSpacing[variant] }, - { color: theme.colors.onSurfaceVariant }, - subtitleProps?.style, - ], - [centered, subtitleProps?.style, theme.colors.onSurfaceVariant, variant] - ); - const wrapperStyle = React.useMemo( - () => [ - styles.container, - variant !== 'small' && styles.flexibleContainer, - centered && styles.centeredContainer, - style, - contentStyle, - ], - [centered, contentStyle, style, variant] - ); - - const content = hasHeadlineImage ? ( - - {headlineImage} - - ) : ( - <> - - {headline} - - {subtitle ? ( - - {subtitle} - - ) : null} - - ); - - const wrapperProps = { - testID, - style: wrapperStyle, - }; - - if (onHeadlinePress) { - const { - 'aria-label': ariaLabel, - accessibilityLabel, - disabled, - ...restHeadlinePressableProps - } = headlinePressableProps ?? {}; - - return ( - - {content} - - ); - } - - return ( - - {content} - - ); -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - minWidth: 0, - }, - flexibleContainer: { - flex: 0, - paddingHorizontal: 16, - paddingTop: 8, - paddingBottom: 12, - }, - centeredContainer: { - alignItems: 'center', - }, - text: { - alignSelf: 'stretch', - }, - centeredText: { - textAlign: 'center', - }, - headlineImage: { - height: APPBAR_HEADLINE_IMAGE_HEIGHT, - maxWidth: '100%', - alignItems: 'center', - justifyContent: 'center', - overflow: 'hidden', - }, -}); - -export default React.memo(AppbarContent); diff --git a/src/components/AppbarV3/index.ts b/src/components/AppbarV3/index.ts deleted file mode 100644 index d8ba839263..0000000000 --- a/src/components/AppbarV3/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -export { default } from './Appbar'; -export { Appbar } from './Appbar'; -export type { - AppbarFilledTrailingAction, - AppbarHeadlineAlignment, - AppbarHeadlinePressableProps, - AppbarHeadlineTextProps, - AppbarLeadingButton, - AppbarSearchbarProps, - AppbarStandardTrailingAction, - AppbarTextProps, - AppbarTrailingAction, - AppbarTrailingActions, - AppbarVariant, - Props, -} from './types'; diff --git a/src/components/AppbarV3/utils.ts b/src/components/AppbarV3/utils.ts deleted file mode 100644 index 4677ffc43c..0000000000 --- a/src/components/AppbarV3/utils.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { AppbarTrailingAction, AppbarVariant } from './types'; - -export const APPBAR_ICON_BUTTON_SIZE = 48; -export const APPBAR_WIDE_ICON_BUTTON_SIZE = 64; -export const APPBAR_HEADLINE_IMAGE_HEIGHT = 32; -export const APPBAR_SEARCH_MAX_WIDTH = 720; - -export const getAppbarHeight = ( - variant: AppbarVariant, - hasSubtitle: boolean -) => { - if (variant === 'search' || variant === 'small') { - return 64; - } - - if (variant === 'medium-flexible') { - return hasSubtitle ? 136 : 112; - } - - return hasSubtitle ? 152 : 120; -}; - -export const getTrailingActionsWidth = ( - actions: readonly AppbarTrailingAction[] -) => - actions.reduce( - (width, action) => - width + - (action.variant !== 'standard' && action.width === 'wide' - ? APPBAR_WIDE_ICON_BUTTON_SIZE - : APPBAR_ICON_BUTTON_SIZE), - 0 - ); diff --git a/src/components/__tests__/Appbar/Appbar.test.tsx b/src/components/__tests__/Appbar/Appbar.test.tsx index e18fc08671..117a479ea2 100644 --- a/src/components/__tests__/Appbar/Appbar.test.tsx +++ b/src/components/__tests__/Appbar/Appbar.test.tsx @@ -1,486 +1,725 @@ -import { Animated } from 'react-native'; +import * as React from 'react'; +import { Text, View } from 'react-native'; import { describe, expect, it, jest } from '@jest/globals'; -import { act } from '@testing-library/react-native'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { getTheme } from '../../../core/theming'; -import { render, screen } from '../../../test-utils'; -import { tokens } from '../../../theme/tokens'; +import { render, screen, userEvent } from '../../../test-utils'; import Appbar from '../../Appbar'; -import { - getAppbarBackgroundColor, - getAppbarBorders, - modeTextVariant, - renderAppbarContent as utilRenderAppbarContent, -} from '../../Appbar/utils'; -import Menu from '../../Menu/Menu'; -import Searchbar from '../../Searchbar'; -import Text from '../../Typography/Text'; - -// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -const renderAppbarContent = utilRenderAppbarContent as ( - props: Parameters[0] -) => { props: any }[]; - -describe('Appbar', () => { - it('does not pass any additional props to Searchbar', async () => { - const tree = ( +import type { AppbarVariant } from '../../Appbar'; + +const writtenHeadlineVariants: Exclude[] = [ + 'small', + 'medium-flexible', + 'large-flexible', +]; + +const decorativeHeadlineImage = ( + + + + Brand words + + +); + +describe('Appbar content', () => { + it.each([ + { + variant: 'small', + headlineVariant: 'titleLarge', + headlineLines: 1, + subtitleVariant: 'labelMedium', + subtitleSpacing: 0, + }, + { + variant: 'medium-flexible', + headlineVariant: 'headlineMedium', + headlineLines: 2, + subtitleVariant: 'labelLarge', + subtitleSpacing: 4, + }, + { + variant: 'large-flexible', + headlineVariant: 'displaySmall', + headlineLines: 2, + subtitleVariant: 'titleMedium', + subtitleSpacing: 8, + }, + ] as const)( + 'uses the $variant content treatment', + async ({ + variant, + headlineVariant, + headlineLines, + subtitleVariant, + subtitleSpacing, + }) => { await render( - - - - ) - ).toJSON(); + + ); - expect(tree).toMatchSnapshot(); - }); + expect(screen.getByTestId('appbar-content-headline-text')).toHaveStyle( + getTheme().fonts[headlineVariant] + ); + expect(screen.getByTestId('appbar-content-headline-text')).toHaveProp( + 'numberOfLines', + headlineLines + ); + expect(screen.getByTestId('appbar-content-subtitle-text')).toHaveStyle({ + ...getTheme().fonts[subtitleVariant], + color: getTheme().colors.onSurfaceVariant, + marginTop: subtitleSpacing, + }); + } + ); - it('passes additional props to AppbarBackAction, AppbarContent and AppbarAction', async () => { - const tree = ( - await render( - - {}} /> - - {}} /> - - ) - ).toJSON(); - - expect(tree).toMatchSnapshot(); - }); -}); + it('centers text and balances asymmetric controls for centered and image layouts', async () => { + const { rerender } = await render( + + ); -describe('renderAppbarContent', () => { - const children = [ - {}} key={0} />, - , - {}} key={2} />, - {}} key={3} />, - ]; - - it('should render all children types if renderOnly is not specified', () => { - const result = renderAppbarContent({ - children, - isDark: false, + expect(screen.getByTestId('appbar-content-headline-text')).toHaveStyle({ + textAlign: 'center', }); - - expect(result).toHaveLength(4); - }); - - it('should render all children types except specified in renderExcept', () => { - const result = renderAppbarContent({ - children: [ - ...children, - {}} />} - visible={false} - > - {null} - , - ], - isDark: false, - renderExcept: [ - 'Appbar', - 'Appbar.Header', - 'Appbar.BackAction', - 'Appbar.Content', - ], + expect(screen.getByTestId('appbar-content-subtitle-text')).toHaveStyle({ + textAlign: 'center', }); + expect( + screen.getByTestId('leading-action-container-outer-layer').parent + ).toHaveStyle({ width: 96 }); + expect( + screen.getByTestId('trailing-action-container-outer-layer').parent + ).toHaveStyle({ width: 96 }); + + await rerender( + } + leadingButton={{ + type: 'back', + testID: 'leading-action', + }} + trailingActions={[ + { + key: 'more', + icon: 'dots-vertical', + 'aria-label': 'More options', + testID: 'trailing-action', + }, + { + key: 'search', + icon: 'magnify', + 'aria-label': 'Search inbox', + }, + ]} + /> + ); - expect(result).toHaveLength(3); + expect( + screen.getByTestId('leading-action-container-outer-layer').parent + ).toHaveStyle({ width: 96 }); + expect( + screen.getByTestId('trailing-action-container-outer-layer').parent + ).toHaveStyle({ width: 96 }); + expect( + screen.getByTestId('brand-mark', { includeHiddenElements: true }) + ).toBeOnTheScreen(); }); - it('should render only children types specifed in renderOnly', () => { - const result = renderAppbarContent({ - children, - isDark: false, - renderOnly: ['Appbar.Action'], - }); - - expect(result).toHaveLength(2); - }); + it('adjusts leading headline spacing when a leading button is present', async () => { + const { rerender } = await render( + + ); - it('should render AppbarContent with correct mode', () => { - const result = renderAppbarContent({ - children, - isDark: false, - renderOnly: ['Appbar.Content'], - mode: 'large', + expect(screen.getByTestId('appbar-content')).toHaveStyle({ + marginStart: 12, }); - // eslint-disable-next-line no-restricted-syntax -- TODO: replace TestInstance props access with a user-visible assertion. - expect(result[0].props.mode).toBe('large'); - }); + await rerender( + + ); - it('should render centered AppbarContent', () => { - const result = renderAppbarContent({ - children, - isDark: false, - renderOnly: ['Appbar.Content'], - mode: 'center-aligned', - shouldCenterContent: true, + expect(screen.getByTestId('appbar-content')).toHaveStyle({ + marginStart: 4, }); + }); +}); - const centerAlignedContent = { - alignItems: 'center', - }; - - // eslint-disable-next-line no-restricted-syntax -- TODO: replace TestInstance props access with a user-visible assertion. - expect(result[0].props.style).toEqual( - expect.arrayContaining([expect.objectContaining(centerAlignedContent)]) +describe('Appbar surface', () => { + it('uses scroll container colors unless a custom background is supplied', async () => { + const customBackground = 'rebeccapurple'; + const { rerender } = await render( + ); - }); - it('should render AppbarContent with correct spacings', () => { - const renderResult = (withAppbarBackAction = false) => - renderAppbarContent({ - children, - isDark: false, - renderOnly: [ - 'Appbar.Content', - withAppbarBackAction && 'Appbar.BackAction', - ], - }); + expect(screen.getByTestId('appbar-root-layer')).toHaveStyle({ + backgroundColor: getTheme().colors.surface, + }); - const v3Spacing = { - marginLeft: 12, - }; + await rerender(); - // eslint-disable-next-line no-restricted-syntax -- TODO: replace TestInstance props access with a user-visible assertion. - expect(renderResult()[0].props.style).toEqual( - expect.arrayContaining([expect.objectContaining(v3Spacing)]) - ); - }); + expect(screen.getByTestId('appbar-root-layer')).toHaveStyle({ + backgroundColor: getTheme().colors.surfaceContainer, + }); - it('Is recognized as a heading when no onPress callback has been passed', async () => { - await render( - - - - - + await rerender( + ); - expect(screen.getByRole('heading')).toBeOnTheScreen(); + expect(screen.getByTestId('appbar-root-layer')).toHaveStyle({ + backgroundColor: customBackground, + }); }); - it('is recognized as a button when onPress callback has been passed', async () => { - await render( - - - {}} /> - - - ); - expect(screen.getByRole('button')).toBeEnabled(); - expect(screen.queryByRole('heading')).not.toBeOnTheScreen(); - }); - it('is recognized as a disabled button when onPress and disabled is passed', async () => { + it('applies border clipping and resolves safe-area overrides on the surface', async () => { await render( - - - {}} disabled /> - + + ); - expect(screen.getByRole('button')).toBeDisabled(); - expect(screen.queryByRole('heading')).not.toBeOnTheScreen(); + expect(screen.getByTestId('appbar-root-layer')).toHaveStyle({ + borderBottomLeftRadius: 16, + borderBottomRightRadius: 16, + paddingTop: 20, + paddingHorizontal: 12, + }); }); }); -describe('AppbarAction', () => { - it('should be rendered with default theme color', async () => { - await render( - - - +describe('Appbar actions', () => { + it('skips rendering unchanged action configurations', async () => { + const icon = jest.fn(() => ); + const { rerender } = await render( + ); - // eslint-disable-next-line no-restricted-syntax -- TODO: replace TestInstance props access with a user-visible assertion. - const appbarActionIcon = screen.getByTestId('cross-fade-icon-current').props - .children; - // eslint-disable-next-line no-restricted-syntax -- TODO: replace TestInstance props access with a user-visible assertion. - expect(appbarActionIcon.props.color).toBe( - getTheme().colors.onSurfaceVariant + const initialRenderCount = icon.mock.calls.length; + + await rerender( + ); - }); - it('should be rendered with specific theme color if is leading', async () => { - await render( - - - - ); - // eslint-disable-next-line no-restricted-syntax -- TODO: replace TestInstance props access with a user-visible assertion. - const appbarActionIcon = screen.getByTestId('cross-fade-icon-current').props - .children; - // eslint-disable-next-line no-restricted-syntax -- TODO: replace TestInstance props access with a user-visible assertion. - expect(appbarActionIcon.props.color).toBe(getTheme().colors.onSurface); + expect(screen.getByTestId('stable-action-icon')).toBeOnTheScreen(); + expect(icon).toHaveBeenCalledTimes(initialRenderCount); }); - it('should be rendered with custom color', async () => { + it('maps leading, trailing, and custom action colors', async () => { await render( - - - + ( + + ), + 'aria-label': 'Navigation', + }} + trailingActions={[ + { + key: 'default', + icon: ({ color }) => ( + + ), + 'aria-label': 'Default action', + }, + { + key: 'custom', + icon: ({ color }) => ( + + ), + 'aria-label': 'Custom action', + color: 'rebeccapurple', + }, + ]} + /> ); - // eslint-disable-next-line no-restricted-syntax -- TODO: replace TestInstance props access with a user-visible assertion. - const appbarActionIcon = screen.getByTestId('cross-fade-icon-current').props - .children; - // eslint-disable-next-line no-restricted-syntax -- TODO: replace TestInstance props access with a user-visible assertion. - expect(appbarActionIcon.props.color).toBe('purple'); - }); - it('should render AppbarBackAction with custom color', async () => { - await render( - - - - ); - // eslint-disable-next-line no-restricted-syntax -- TODO: replace TestInstance props access with a user-visible assertion. - const appbarBackActionIcon = screen.getByTestId('cross-fade-icon-current') - .props.children; - // eslint-disable-next-line no-restricted-syntax -- TODO: replace TestInstance props access with a user-visible assertion. - expect(appbarBackActionIcon.props.color).toBe('purple'); + expect(screen.getByTestId('leading-icon-color')).toHaveStyle({ + backgroundColor: getTheme().colors.onSurface, + }); + expect(screen.getByTestId('trailing-icon-color')).toHaveStyle({ + backgroundColor: getTheme().colors.onSurfaceVariant, + }); + expect(screen.getByTestId('custom-icon-color')).toHaveStyle({ + backgroundColor: 'rebeccapurple', + }); }); -}); -describe('AppbarContent', () => { - (['small', 'medium', 'large', 'center-aligned'] as const).forEach((mode) => - it(`should render text component with appropriate variant for ${mode} mode`, async () => { + it.each([ + { variant: 'filled', color: getTheme().colors.primary }, + { variant: 'tonal', color: getTheme().colors.secondaryContainer }, + ] as const)( + 'maps $variant actions to their selected container color', + async ({ variant, color }) => { await render( - - - + ); - expect(screen.getByTestId('appbar-content-title-text')).toHaveStyle( - getTheme().fonts[modeTextVariant[mode]] - ); - }) + expect(screen.getByTestId('expressive-action-container')).toHaveStyle({ + backgroundColor: color, + }); + expect( + screen.getByTestId('expressive-action-container-outer-layer') + ).toHaveStyle({ width: 56 }); + } ); +}); - it('should render component passed to title', async () => { - await render( - - - Title - - } - /> - +describe('Appbar search', () => { + it('uses the placeholder as the searchbox label unless an explicit label is supplied', async () => { + const { rerender } = await render( + ); - expect(screen.getByText('Title')).toBeOnTheScreen(); - }); + expect( + screen.getByRole('searchbox', { name: 'Search messages' }) + ).toBeOnTheScreen(); + expect(screen.getByTestId('appbar-search-container')).toHaveStyle({ + backgroundColor: getTheme().colors.surfaceContainer, + }); - it('should render subtitle with MD3 typography and color tokens', async () => { - await render( - - - + await rerender( + ); - expect(screen.getByTestId('appbar-content-subtitle-text')).toHaveStyle({ - ...getTheme().fonts.labelSmall, - color: getTheme().colors.onSurfaceVariant, - }); + expect( + screen.getByRole('searchbox', { name: 'Message search' }) + ).toBeOnTheScreen(); }); - it('should apply a custom subtitle style', async () => { - const customSubtitleColor = getTheme().colors.error; - - await render( - - { + const onChangeText = jest.fn(); + const SearchAppbar = () => { + const [value, setValue] = React.useState(''); + + return ( + { + setValue(nextValue); + onChangeText(nextValue); + }, + testID: 'message-search', + }} + trailingActions={[ + { + key: 'more', + icon: 'dots-vertical', + 'aria-label': 'More options', + }, + ]} /> - + ); + }; + + await render(); + + const searchbox = screen.getByRole('searchbox', { + name: 'Search messages', + }); + expect(searchbox).toHaveStyle({ + color: getTheme().colors.onSurface, + textAlign: 'center', + }); + expect(searchbox).toHaveProp( + 'placeholderTextColor', + getTheme().colors.onSurfaceVariant ); + expect(screen.getByTestId('message-search-container')).toHaveStyle({ + backgroundColor: getTheme().colors.surfaceContainerHighest, + }); + expect( + screen.getByRole('button', { name: 'Navigate back' }) + ).toBeOnTheScreen(); + expect( + screen.getByRole('button', { name: 'More options' }) + ).toBeOnTheScreen(); - expect(screen.getByTestId('appbar-content-subtitle-text')).toHaveStyle({ - color: customSubtitleColor, + await userEvent.type(searchbox, 'draft'); + expect(onChangeText).toHaveBeenLastCalledWith('draft'); + expect(searchbox).toHaveProp('value', 'draft'); + + expect( + screen.getByTestId('message-search-container-outer-layer') + ).toHaveStyle({ width: '100%' }); + expect(screen.getByTestId('appbar-search-width-limiter')).toHaveStyle({ + width: '100%', + maxWidth: 720, }); }); }); -describe('getAppbarColors', () => { - const elevation = 4; - const customBackground = 'aquamarine'; +describe('Appbar accessibility', () => { + it.each(writtenHeadlineVariants)( + 'exposes a written %s headline as a heading', + async (variant) => { + await render(); - it('should return custom color no matter what is the theme version', () => { - expect( - getAppbarBackgroundColor(getTheme(), elevation, customBackground) - ).toBe(customBackground); - }); + expect(screen.getByRole('heading', { name: 'Inbox' })).toBeOnTheScreen(); + } + ); - it('should return v3 light color if theme version is 3', () => { - expect(getAppbarBackgroundColor(getTheme(), elevation)).toBe( - tokens.md.ref.palette.neutral98 + it('hides a small headline image behind one named heading', async () => { + await render( + ); + + expect(screen.getAllByRole('heading')).toHaveLength(1); + expect(screen.getByRole('heading', { name: 'Inbox' })).toBeOnTheScreen(); + expect(screen.queryByRole('img')).not.toBeOnTheScreen(); + expect(screen.queryByText('Brand words')).not.toBeOnTheScreen(); }); - it('should return v3 dark color if theme version is 3', () => { - expect(getAppbarBackgroundColor(getTheme(true), elevation)).toBe( - tokens.md.ref.palette.neutral6 + it.each(['medium-flexible', 'large-flexible'] as const)( + 'does not expose duplicate content for a %s headline image', + async (variant) => { + await render( + + ); + + expect(screen.getAllByRole('heading', { name: 'Inbox' })).toHaveLength(1); + expect(screen.queryByRole('img')).not.toBeOnTheScreen(); + expect(screen.queryByLabelText('Brand artwork')).not.toBeOnTheScreen(); + } + ); + + it('exposes a pressable written headline as a named button and heading', async () => { + const onHeadlinePress = jest.fn(); + await render( + ); + + expect(screen.getByRole('button', { name: 'Inbox' })).toBeOnTheScreen(); + expect(screen.getByRole('heading', { name: 'Inbox' })).toBeOnTheScreen(); }); -}); -describe('animated value changes correctly', () => { - it('appbar animated value changes correctly', async () => { - const value = new Animated.Value(1); + it('does not invoke a disabled headline action', async () => { + const onHeadlinePress = jest.fn(); await render( - - - + ); - expect(screen.getByTestId('appbar-outer-layer')).toHaveStyle({ - transform: [{ scale: 1 }], - }); - Animated.timing(value, { - toValue: 1.5, - useNativeDriver: false, - duration: 200, - }).start(); - - await act(() => { - jest.advanceTimersByTime(200); + const headlineButton = screen.getByRole('button', { + name: 'Inbox', + disabled: true, }); + await userEvent.press(headlineButton); - expect(screen.getByTestId('appbar-outer-layer')).toHaveStyle({ - transform: [{ scale: 1.5 }], - }); + expect(onHeadlinePress).not.toHaveBeenCalled(); }); - it('action animated value changes correctly', async () => { - const value = new Animated.Value(1); + it('names a pressable small headline image from its fallback headline', async () => { await render( - - - + {}} + /> ); - expect( - screen.getByTestId('appbar-action-container-outer-layer') - ).toHaveStyle({ - transform: [{ scale: 1 }], - }); - Animated.timing(value, { - toValue: 1.5, - useNativeDriver: false, - duration: 200, - }).start(); + expect(screen.getByRole('button', { name: 'Inbox' })).toBeOnTheScreen(); + }); + + it('forwards custom headline button accessibility props', async () => { + const onAccessibilityAction = jest.fn(); + await render( + {}} + headlinePressableProps={{ + accessibilityActions: [{ name: 'activate', label: 'Open inbox' }], + accessibilityHint: 'Opens the inbox menu', + accessibilityLabel: 'Inbox options', + accessibilityState: { busy: true, expanded: true }, + accessibilityValue: { text: 'Unread messages available' }, + onAccessibilityAction, + }} + /> + ); - await act(() => { - jest.advanceTimersByTime(200); + const titleButton = screen.getByRole('button', { + name: 'Inbox options', + busy: true, + expanded: true, }); - expect( - screen.getByTestId('appbar-action-container-outer-layer') - ).toHaveStyle({ - transform: [{ scale: 1.5 }], + expect(titleButton).toHaveProp( + 'accessibilityActions', + expect.arrayContaining([ + expect.objectContaining({ name: 'activate', label: 'Open inbox' }), + ]) + ); + expect(titleButton).toHaveProp('accessibilityHint', 'Opens the inbox menu'); + expect(titleButton).toHaveAccessibilityValue({ + text: 'Unread messages available', }); + expect(titleButton).toHaveProp( + 'onAccessibilityAction', + onAccessibilityAction + ); }); - it('back action animated value changes correctly', async () => { - const value = new Animated.Value(1); + it('uses default and custom labels for back buttons', async () => { await render( - - + + - + ); - expect( - screen.getByTestId('appbar-back-action-container-outer-layer') - ).toHaveStyle({ - transform: [{ scale: 1 }], - }); - - Animated.timing(value, { - toValue: 1.5, - useNativeDriver: false, - duration: 200, - }).start(); - - await act(() => { - jest.advanceTimersByTime(200); - }); + expect(screen.getByRole('button', { name: 'Back' })).toBeOnTheScreen(); expect( - screen.getByTestId('appbar-back-action-container-outer-layer') - ).toHaveStyle({ - transform: [{ scale: 1.5 }], - }); + screen.getByRole('button', { name: 'Return to inbox' }) + ).toBeOnTheScreen(); }); - it('header animated value changes correctly', async () => { - const value = new Animated.Value(1); + it('preserves trailing action labels, hints, disabled state, and busy state', async () => { await render( - - - {null} - - + ); - expect(screen.getByTestId('appbar-header-outer-layer')).toHaveStyle({ - transform: [{ scale: 1 }], - }); - - Animated.timing(value, { - toValue: 1.5, - useNativeDriver: false, - duration: 200, - }).start(); - await act(() => { - jest.advanceTimersByTime(200); + const trailingAction = screen.getByRole('button', { + name: 'Save message', + busy: true, + disabled: true, }); - expect(screen.getByTestId('appbar-header-outer-layer')).toHaveStyle({ - transform: [{ scale: 1.5 }], - }); + expect(trailingAction).toHaveProp( + 'accessibilityHint', + 'Saves the current draft' + ); + expect(trailingAction).toBeDisabled(); + expect(trailingAction).toBeBusy(); }); - it('header bottom border radius applied correctly', async () => { - const style = { borderBottomLeftRadius: 16, borderBottomRightRadius: 16 }; + it.each(['small', 'medium-flexible'] as const)( + 'hides decorative descendants of a %s headline image', + async (variant) => { + await render( + variant === 'small' ? ( + + ) : ( + + ) + ); + + const imageContainer = screen.getByTestId( + 'appbar-content-headline-image', + { includeHiddenElements: true } + ); + + expect(imageContainer).toHaveProp('aria-hidden', true); + expect(imageContainer).toHaveProp( + 'importantForAccessibility', + 'no-hide-descendants' + ); + expect( + screen.queryByRole('heading', { name: 'Brand words' }) + ).not.toBeOnTheScreen(); + } + ); + it('exposes the leading button, headline, and trailing actions in source order', async () => { await render( - - - {null} - - + ); - expect(screen.getByTestId('appbar-header-root-layer')).toHaveStyle(style); - }); - describe('getAppbarBorders', () => { - const style = { borderRadius: 10, height: 60, top: 13 }; + expect(screen.getAllByRole(/button|heading/)).toEqual([ + screen.getByRole('button', { name: 'Navigate back' }), + screen.getByRole('heading', { name: 'Inbox' }), + screen.getByRole('button', { name: 'Search inbox' }), + screen.getByRole('button', { name: 'More options' }), + ]); + }); - it('should return only border radius styles', () => { - expect(getAppbarBorders(style)).toEqual({ borderRadius: 10 }); + it('does not remount a surviving trailing action when its configuration changes', async () => { + const { rerender } = await render( + + ); + const survivingTrailingAction = screen.getByRole('button', { + name: 'Keep action', }); - it('should return empty object if no borders are passed', () => { - const style = { height: 60, top: 13 }; - expect(getAppbarBorders(style)).toEqual({}); - }); + await rerender( + + ); + + expect(screen.getByRole('button', { name: 'Keep action' })).toBe( + survivingTrailingAction + ); }); }); diff --git a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap deleted file mode 100644 index 59c778e4be..0000000000 --- a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap +++ /dev/null @@ -1,881 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Appbar does not pass any additional props to Searchbar 1`] = ` - - - - - - - - - - magnify - - - - - - - - - - - - - close - - - - - - - - - - -`; - -exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and AppbarAction 1`] = ` - - - - - - - - - - - - - - - - - - - - Examples - - - - - - - - - - menu - - - - - - - - - -`; diff --git a/src/components/__tests__/AppbarV3/utils.test.ts b/src/components/__tests__/Appbar/utils.test.ts similarity index 98% rename from src/components/__tests__/AppbarV3/utils.test.ts rename to src/components/__tests__/Appbar/utils.test.ts index d37a585372..dff18b2684 100644 --- a/src/components/__tests__/AppbarV3/utils.test.ts +++ b/src/components/__tests__/Appbar/utils.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from '@jest/globals'; -import { getAppbarHeight, getTrailingActionsWidth } from '../../AppbarV3/utils'; +import { getAppbarHeight, getTrailingActionsWidth } from '../../Appbar/utils'; describe('getAppbarHeight', () => { it.each([ diff --git a/src/components/__tests__/AppbarV3/AppbarV3.test.tsx b/src/components/__tests__/AppbarV3/AppbarV3.test.tsx deleted file mode 100644 index fc0ceb397b..0000000000 --- a/src/components/__tests__/AppbarV3/AppbarV3.test.tsx +++ /dev/null @@ -1,725 +0,0 @@ -import * as React from 'react'; -import { Text, View } from 'react-native'; - -import { describe, expect, it, jest } from '@jest/globals'; -import { SafeAreaProvider } from 'react-native-safe-area-context'; - -import { getTheme } from '../../../core/theming'; -import { render, screen, userEvent } from '../../../test-utils'; -import AppbarV3 from '../../AppbarV3'; -import type { AppbarVariant } from '../../AppbarV3'; - -const writtenHeadlineVariants: Exclude[] = [ - 'small', - 'medium-flexible', - 'large-flexible', -]; - -const decorativeHeadlineImage = ( - - - - Brand words - - -); - -describe('AppbarV3 content', () => { - it.each([ - { - variant: 'small', - headlineVariant: 'titleLarge', - headlineLines: 1, - subtitleVariant: 'labelMedium', - subtitleSpacing: 0, - }, - { - variant: 'medium-flexible', - headlineVariant: 'headlineMedium', - headlineLines: 2, - subtitleVariant: 'labelLarge', - subtitleSpacing: 4, - }, - { - variant: 'large-flexible', - headlineVariant: 'displaySmall', - headlineLines: 2, - subtitleVariant: 'titleMedium', - subtitleSpacing: 8, - }, - ] as const)( - 'uses the $variant content treatment', - async ({ - variant, - headlineVariant, - headlineLines, - subtitleVariant, - subtitleSpacing, - }) => { - await render( - - ); - - expect(screen.getByTestId('appbar-content-headline-text')).toHaveStyle( - getTheme().fonts[headlineVariant] - ); - expect(screen.getByTestId('appbar-content-headline-text')).toHaveProp( - 'numberOfLines', - headlineLines - ); - expect(screen.getByTestId('appbar-content-subtitle-text')).toHaveStyle({ - ...getTheme().fonts[subtitleVariant], - color: getTheme().colors.onSurfaceVariant, - marginTop: subtitleSpacing, - }); - } - ); - - it('centers text and balances asymmetric controls for centered and image layouts', async () => { - const { rerender } = await render( - - ); - - expect(screen.getByTestId('appbar-content-headline-text')).toHaveStyle({ - textAlign: 'center', - }); - expect(screen.getByTestId('appbar-content-subtitle-text')).toHaveStyle({ - textAlign: 'center', - }); - expect( - screen.getByTestId('leading-action-container-outer-layer').parent - ).toHaveStyle({ width: 96 }); - expect( - screen.getByTestId('trailing-action-container-outer-layer').parent - ).toHaveStyle({ width: 96 }); - - await rerender( - } - leadingButton={{ - type: 'back', - testID: 'leading-action', - }} - trailingActions={[ - { - key: 'more', - icon: 'dots-vertical', - 'aria-label': 'More options', - testID: 'trailing-action', - }, - { - key: 'search', - icon: 'magnify', - 'aria-label': 'Search inbox', - }, - ]} - /> - ); - - expect( - screen.getByTestId('leading-action-container-outer-layer').parent - ).toHaveStyle({ width: 96 }); - expect( - screen.getByTestId('trailing-action-container-outer-layer').parent - ).toHaveStyle({ width: 96 }); - expect( - screen.getByTestId('brand-mark', { includeHiddenElements: true }) - ).toBeOnTheScreen(); - }); - - it('adjusts leading headline spacing when a leading button is present', async () => { - const { rerender } = await render( - - ); - - expect(screen.getByTestId('appbar-content')).toHaveStyle({ - marginStart: 12, - }); - - await rerender( - - ); - - expect(screen.getByTestId('appbar-content')).toHaveStyle({ - marginStart: 4, - }); - }); -}); - -describe('AppbarV3 surface', () => { - it('uses scroll container colors unless a custom background is supplied', async () => { - const customBackground = 'rebeccapurple'; - const { rerender } = await render( - - ); - - expect(screen.getByTestId('appbar-root-layer')).toHaveStyle({ - backgroundColor: getTheme().colors.surface, - }); - - await rerender(); - - expect(screen.getByTestId('appbar-root-layer')).toHaveStyle({ - backgroundColor: getTheme().colors.surfaceContainer, - }); - - await rerender( - - ); - - expect(screen.getByTestId('appbar-root-layer')).toHaveStyle({ - backgroundColor: customBackground, - }); - }); - - it('applies border clipping and resolves safe-area overrides on the surface', async () => { - await render( - - - - ); - - expect(screen.getByTestId('appbar-root-layer')).toHaveStyle({ - borderBottomLeftRadius: 16, - borderBottomRightRadius: 16, - paddingTop: 20, - paddingHorizontal: 12, - }); - }); -}); - -describe('AppbarV3 actions', () => { - it('skips rendering unchanged action configurations', async () => { - const icon = jest.fn(() => ); - const { rerender } = await render( - - ); - const initialRenderCount = icon.mock.calls.length; - - await rerender( - - ); - - expect(screen.getByTestId('stable-action-icon')).toBeOnTheScreen(); - expect(icon).toHaveBeenCalledTimes(initialRenderCount); - }); - - it('maps leading, trailing, and custom action colors', async () => { - await render( - ( - - ), - 'aria-label': 'Navigation', - }} - trailingActions={[ - { - key: 'default', - icon: ({ color }) => ( - - ), - 'aria-label': 'Default action', - }, - { - key: 'custom', - icon: ({ color }) => ( - - ), - 'aria-label': 'Custom action', - color: 'rebeccapurple', - }, - ]} - /> - ); - - expect(screen.getByTestId('leading-icon-color')).toHaveStyle({ - backgroundColor: getTheme().colors.onSurface, - }); - expect(screen.getByTestId('trailing-icon-color')).toHaveStyle({ - backgroundColor: getTheme().colors.onSurfaceVariant, - }); - expect(screen.getByTestId('custom-icon-color')).toHaveStyle({ - backgroundColor: 'rebeccapurple', - }); - }); - - it.each([ - { variant: 'filled', color: getTheme().colors.primary }, - { variant: 'tonal', color: getTheme().colors.secondaryContainer }, - ] as const)( - 'maps $variant actions to their selected container color', - async ({ variant, color }) => { - await render( - - ); - - expect(screen.getByTestId('expressive-action-container')).toHaveStyle({ - backgroundColor: color, - }); - expect( - screen.getByTestId('expressive-action-container-outer-layer') - ).toHaveStyle({ width: 56 }); - } - ); -}); - -describe('AppbarV3 search', () => { - it('uses the placeholder as the searchbox label unless an explicit label is supplied', async () => { - const { rerender } = await render( - - ); - - expect( - screen.getByRole('searchbox', { name: 'Search messages' }) - ).toBeOnTheScreen(); - expect(screen.getByTestId('appbar-search-container')).toHaveStyle({ - backgroundColor: getTheme().colors.surfaceContainer, - }); - - await rerender( - - ); - - expect( - screen.getByRole('searchbox', { name: 'Message search' }) - ).toBeOnTheScreen(); - }); - - it('configures the search field, forwards its behavior, and constrains its width', async () => { - const onChangeText = jest.fn(); - const SearchAppbar = () => { - const [value, setValue] = React.useState(''); - - return ( - { - setValue(nextValue); - onChangeText(nextValue); - }, - testID: 'message-search', - }} - trailingActions={[ - { - key: 'more', - icon: 'dots-vertical', - 'aria-label': 'More options', - }, - ]} - /> - ); - }; - - await render(); - - const searchbox = screen.getByRole('searchbox', { - name: 'Search messages', - }); - expect(searchbox).toHaveStyle({ - color: getTheme().colors.onSurface, - textAlign: 'center', - }); - expect(searchbox).toHaveProp( - 'placeholderTextColor', - getTheme().colors.onSurfaceVariant - ); - expect(screen.getByTestId('message-search-container')).toHaveStyle({ - backgroundColor: getTheme().colors.surfaceContainerHighest, - }); - expect( - screen.getByRole('button', { name: 'Navigate back' }) - ).toBeOnTheScreen(); - expect( - screen.getByRole('button', { name: 'More options' }) - ).toBeOnTheScreen(); - - await userEvent.type(searchbox, 'draft'); - expect(onChangeText).toHaveBeenLastCalledWith('draft'); - expect(searchbox).toHaveProp('value', 'draft'); - - expect( - screen.getByTestId('message-search-container-outer-layer') - ).toHaveStyle({ width: '100%' }); - expect(screen.getByTestId('appbar-search-width-limiter')).toHaveStyle({ - width: '100%', - maxWidth: 720, - }); - }); -}); - -describe('AppbarV3 accessibility', () => { - it.each(writtenHeadlineVariants)( - 'exposes a written %s headline as a heading', - async (variant) => { - await render(); - - expect(screen.getByRole('heading', { name: 'Inbox' })).toBeOnTheScreen(); - } - ); - - it('hides a small headline image behind one named heading', async () => { - await render( - - ); - - expect(screen.getAllByRole('heading')).toHaveLength(1); - expect(screen.getByRole('heading', { name: 'Inbox' })).toBeOnTheScreen(); - expect(screen.queryByRole('img')).not.toBeOnTheScreen(); - expect(screen.queryByText('Brand words')).not.toBeOnTheScreen(); - }); - - it.each(['medium-flexible', 'large-flexible'] as const)( - 'does not expose duplicate content for a %s headline image', - async (variant) => { - await render( - - ); - - expect(screen.getAllByRole('heading', { name: 'Inbox' })).toHaveLength(1); - expect(screen.queryByRole('img')).not.toBeOnTheScreen(); - expect(screen.queryByLabelText('Brand artwork')).not.toBeOnTheScreen(); - } - ); - - it('exposes a pressable written headline as a named button and heading', async () => { - const onHeadlinePress = jest.fn(); - await render( - - ); - - expect(screen.getByRole('button', { name: 'Inbox' })).toBeOnTheScreen(); - expect(screen.getByRole('heading', { name: 'Inbox' })).toBeOnTheScreen(); - }); - - it('does not invoke a disabled headline action', async () => { - const onHeadlinePress = jest.fn(); - await render( - - ); - - const headlineButton = screen.getByRole('button', { - name: 'Inbox', - disabled: true, - }); - await userEvent.press(headlineButton); - - expect(onHeadlinePress).not.toHaveBeenCalled(); - }); - - it('names a pressable small headline image from its fallback headline', async () => { - await render( - {}} - /> - ); - - expect(screen.getByRole('button', { name: 'Inbox' })).toBeOnTheScreen(); - }); - - it('forwards custom headline button accessibility props', async () => { - const onAccessibilityAction = jest.fn(); - await render( - {}} - headlinePressableProps={{ - accessibilityActions: [{ name: 'activate', label: 'Open inbox' }], - accessibilityHint: 'Opens the inbox menu', - accessibilityLabel: 'Inbox options', - accessibilityState: { busy: true, expanded: true }, - accessibilityValue: { text: 'Unread messages available' }, - onAccessibilityAction, - }} - /> - ); - - const titleButton = screen.getByRole('button', { - name: 'Inbox options', - busy: true, - expanded: true, - }); - - expect(titleButton).toHaveProp( - 'accessibilityActions', - expect.arrayContaining([ - expect.objectContaining({ name: 'activate', label: 'Open inbox' }), - ]) - ); - expect(titleButton).toHaveProp('accessibilityHint', 'Opens the inbox menu'); - expect(titleButton).toHaveAccessibilityValue({ - text: 'Unread messages available', - }); - expect(titleButton).toHaveProp( - 'onAccessibilityAction', - onAccessibilityAction - ); - }); - - it('uses default and custom labels for back buttons', async () => { - await render( - - - - - ); - - expect(screen.getByRole('button', { name: 'Back' })).toBeOnTheScreen(); - expect( - screen.getByRole('button', { name: 'Return to inbox' }) - ).toBeOnTheScreen(); - }); - - it('preserves trailing action labels, hints, disabled state, and busy state', async () => { - await render( - - ); - - const trailingAction = screen.getByRole('button', { - name: 'Save message', - busy: true, - disabled: true, - }); - - expect(trailingAction).toHaveProp( - 'accessibilityHint', - 'Saves the current draft' - ); - expect(trailingAction).toBeDisabled(); - expect(trailingAction).toBeBusy(); - }); - - it.each(['small', 'medium-flexible'] as const)( - 'hides decorative descendants of a %s headline image', - async (variant) => { - await render( - variant === 'small' ? ( - - ) : ( - - ) - ); - - const imageContainer = screen.getByTestId( - 'appbar-content-headline-image', - { includeHiddenElements: true } - ); - - expect(imageContainer).toHaveProp('aria-hidden', true); - expect(imageContainer).toHaveProp( - 'importantForAccessibility', - 'no-hide-descendants' - ); - expect( - screen.queryByRole('heading', { name: 'Brand words' }) - ).not.toBeOnTheScreen(); - } - ); - - it('exposes the leading button, headline, and trailing actions in source order', async () => { - await render( - - ); - - expect(screen.getAllByRole(/button|heading/)).toEqual([ - screen.getByRole('button', { name: 'Navigate back' }), - screen.getByRole('heading', { name: 'Inbox' }), - screen.getByRole('button', { name: 'Search inbox' }), - screen.getByRole('button', { name: 'More options' }), - ]); - }); - - it('does not remount a surviving trailing action when its configuration changes', async () => { - const { rerender } = await render( - - ); - const survivingTrailingAction = screen.getByRole('button', { - name: 'Keep action', - }); - - await rerender( - - ); - - expect(screen.getByRole('button', { name: 'Keep action' })).toBe( - survivingTrailingAction - ); - }); -}); diff --git a/src/index.tsx b/src/index.tsx index a7b8e58f80..7a45fc0c3b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -45,7 +45,6 @@ export { default as Snackbar } from './components/Snackbar'; export { default as Surface } from './components/Surface'; export { default as Switch } from './components/Switch/Switch'; export { default as Appbar } from './components/Appbar'; -export { default as AppbarV3 } from './components/AppbarV3'; export { default as TouchableRipple } from './components/TouchableRipple/TouchableRipple'; export { default as TextInput } from './components/TextInput'; export { default as ToggleButton } from './components/ToggleButton'; @@ -56,25 +55,20 @@ export { default as Text, customText } from './components/Typography/Text'; // Types export type { Props as ActivityIndicatorProps } from './components/ActivityIndicator'; -export type { Props as AppbarProps } from './components/Appbar/Appbar'; -export type { Props as AppbarActionProps } from './components/Appbar/AppbarAction'; -export type { Props as AppbarBackActionProps } from './components/Appbar/AppbarBackAction'; -export type { Props as AppbarContentProps } from './components/Appbar/AppbarContent'; -export type { Props as AppbarHeaderProps } from './components/Appbar/AppbarHeader'; export type { - Props as AppbarV3Props, - AppbarFilledTrailingAction as AppbarV3FilledTrailingAction, - AppbarHeadlineAlignment as AppbarV3HeadlineAlignment, - AppbarHeadlinePressableProps as AppbarV3HeadlinePressableProps, - AppbarHeadlineTextProps as AppbarV3HeadlineTextProps, - AppbarLeadingButton as AppbarV3LeadingButton, - AppbarSearchbarProps as AppbarV3SearchbarProps, - AppbarStandardTrailingAction as AppbarV3StandardTrailingAction, - AppbarTextProps as AppbarV3TextProps, - AppbarTrailingAction as AppbarV3TrailingAction, - AppbarTrailingActions as AppbarV3TrailingActions, - AppbarVariant as AppbarV3Variant, -} from './components/AppbarV3'; + Props as AppbarProps, + AppbarFilledTrailingAction, + AppbarHeadlineAlignment, + AppbarHeadlinePressableProps, + AppbarHeadlineTextProps, + AppbarLeadingButton, + AppbarSearchbarProps, + AppbarStandardTrailingAction, + AppbarTextProps, + AppbarTrailingAction, + AppbarTrailingActions, + AppbarVariant, +} from './components/Appbar'; export type { Props as AvatarIconProps } from './components/Avatar/AvatarIcon'; export type { Props as AvatarImageProps } from './components/Avatar/AvatarImage'; export type { Props as AvatarTextProps } from './components/Avatar/AvatarText'; From bb814f0c347540fe3331e2a13732a99249e270d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Wed, 26 Aug 2026 08:46:39 +0200 Subject: [PATCH 21/34] fix(appbar): drop default testID --- src/components/Appbar/Appbar.tsx | 2 +- .../__tests__/Appbar/Appbar.test.tsx | 44 +++++++++++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/components/Appbar/Appbar.tsx b/src/components/Appbar/Appbar.tsx index 46a9d00402..90f0d38c9d 100644 --- a/src/components/Appbar/Appbar.tsx +++ b/src/components/Appbar/Appbar.tsx @@ -135,7 +135,7 @@ const Appbar = ({ style, subtitle, subtitleProps, - testID = 'appbar', + testID, theme: themeOverrides, trailingActions = EMPTY_TRAILING_ACTIONS, variant, diff --git a/src/components/__tests__/Appbar/Appbar.test.tsx b/src/components/__tests__/Appbar/Appbar.test.tsx index 117a479ea2..952b556ae1 100644 --- a/src/components/__tests__/Appbar/Appbar.test.tsx +++ b/src/components/__tests__/Appbar/Appbar.test.tsx @@ -9,6 +9,8 @@ import { render, screen, userEvent } from '../../../test-utils'; import Appbar from '../../Appbar'; import type { AppbarVariant } from '../../Appbar'; +const testIDPrefix = 'appbar'; + const writtenHeadlineVariants: Exclude[] = [ 'small', 'medium-flexible', @@ -57,7 +59,12 @@ describe('Appbar content', () => { subtitleSpacing, }) => { await render( - + ); expect(screen.getByTestId('appbar-content-headline-text')).toHaveStyle( @@ -99,6 +106,7 @@ describe('Appbar content', () => { 'aria-label': 'Search inbox', }, ]} + testID={testIDPrefix} /> ); @@ -137,6 +145,7 @@ describe('Appbar content', () => { 'aria-label': 'Search inbox', }, ]} + testID={testIDPrefix} /> ); @@ -153,7 +162,7 @@ describe('Appbar content', () => { it('adjusts leading headline spacing when a leading button is present', async () => { const { rerender } = await render( - + ); expect(screen.getByTestId('appbar-content')).toHaveStyle({ @@ -165,6 +174,7 @@ describe('Appbar content', () => { variant="small" headline="Inbox" leadingButton={{ type: 'back' }} + testID={testIDPrefix} /> ); @@ -178,14 +188,21 @@ describe('Appbar surface', () => { it('uses scroll container colors unless a custom background is supplied', async () => { const customBackground = 'rebeccapurple'; const { rerender } = await render( - + ); expect(screen.getByTestId('appbar-root-layer')).toHaveStyle({ backgroundColor: getTheme().colors.surface, }); - await rerender(); + await rerender( + + ); expect(screen.getByTestId('appbar-root-layer')).toHaveStyle({ backgroundColor: getTheme().colors.surfaceContainer, @@ -197,6 +214,7 @@ describe('Appbar surface', () => { headline="Inbox" isScrolled style={{ backgroundColor: customBackground }} + testID={testIDPrefix} /> ); @@ -222,6 +240,7 @@ describe('Appbar surface', () => { borderBottomLeftRadius: 16, borderBottomRightRadius: 16, }} + testID={testIDPrefix} /> ); @@ -245,6 +264,7 @@ describe('Appbar actions', () => { trailingActions={[ { key: 'stable', icon, 'aria-label': 'Stable action' }, ]} + testID={testIDPrefix} /> ); const initialRenderCount = icon.mock.calls.length; @@ -256,6 +276,7 @@ describe('Appbar actions', () => { trailingActions={[ { key: 'stable', icon, 'aria-label': 'Stable action' }, ]} + testID={testIDPrefix} /> ); @@ -300,6 +321,7 @@ describe('Appbar actions', () => { color: 'rebeccapurple', }, ]} + testID={testIDPrefix} /> ); @@ -334,6 +356,7 @@ describe('Appbar actions', () => { testID: 'expressive-action', }, ]} + testID={testIDPrefix} /> ); @@ -353,6 +376,7 @@ describe('Appbar search', () => { ); @@ -371,6 +395,7 @@ describe('Appbar search', () => { value: '', 'aria-label': 'Message search', }} + testID={testIDPrefix} /> ); @@ -405,6 +430,7 @@ describe('Appbar search', () => { 'aria-label': 'More options', }, ]} + testID={testIDPrefix} /> ); }; @@ -479,6 +505,7 @@ describe('Appbar accessibility', () => { variant={variant} headline="Inbox" headlineImage={decorativeHeadlineImage} + testID={testIDPrefix} /> ); @@ -495,6 +522,7 @@ describe('Appbar accessibility', () => { variant="small" headline="Inbox" onHeadlinePress={onHeadlinePress} + testID={testIDPrefix} /> ); @@ -510,6 +538,7 @@ describe('Appbar accessibility', () => { headline="Inbox" onHeadlinePress={onHeadlinePress} headlinePressableProps={{ disabled: true }} + testID={testIDPrefix} /> ); @@ -529,6 +558,7 @@ describe('Appbar accessibility', () => { headline="Inbox" headlineImage={decorativeHeadlineImage} onHeadlinePress={() => {}} + testID={testIDPrefix} /> ); @@ -550,6 +580,7 @@ describe('Appbar accessibility', () => { accessibilityValue: { text: 'Unread messages available' }, onAccessibilityAction, }} + testID={testIDPrefix} /> ); @@ -640,12 +671,14 @@ describe('Appbar accessibility', () => { variant="small" headline="Inbox" headlineImage={decorativeHeadlineImage} + testID={testIDPrefix} /> ) : ( ) ); @@ -676,6 +709,7 @@ describe('Appbar accessibility', () => { { key: 'search', icon: 'magnify', 'aria-label': 'Search inbox' }, { key: 'more', icon: 'dots-vertical', 'aria-label': 'More options' }, ]} + testID={testIDPrefix} /> ); @@ -696,6 +730,7 @@ describe('Appbar accessibility', () => { { key: 'keep', icon: 'star', 'aria-label': 'Keep action' }, { key: 'remove', icon: 'delete', 'aria-label': 'Remove action' }, ]} + testID={testIDPrefix} /> ); const survivingTrailingAction = screen.getByRole('button', { @@ -715,6 +750,7 @@ describe('Appbar accessibility', () => { accessibilityHint: 'Updated hint', }, ]} + testID={testIDPrefix} /> ); From b0ce566aaa1f81a25675d6fd342e73f4e2ba0143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Wed, 26 Aug 2026 10:45:06 +0200 Subject: [PATCH 22/34] fix(appbar): prioritize searchBar style override --- src/components/Appbar/Appbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Appbar/Appbar.tsx b/src/components/Appbar/Appbar.tsx index 90f0d38c9d..24cec9b902 100644 --- a/src/components/Appbar/Appbar.tsx +++ b/src/components/Appbar/Appbar.tsx @@ -193,9 +193,9 @@ const Appbar = ({ ); const resolvedSearchInputStyle = React.useMemo( () => [ - searchBar?.inputStyle, styles.searchInput, { color: theme.colors.onSurface }, + searchBar?.inputStyle, ], [searchBar?.inputStyle, theme.colors.onSurface] ); From 637dc991610e9373391525b5d0c7bdfd6aed14ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Wed, 26 Aug 2026 11:06:46 +0200 Subject: [PATCH 23/34] fix(appbar): drop search input default centering --- src/components/Appbar/Appbar.tsx | 9 +-------- src/components/__tests__/Appbar/Appbar.test.tsx | 1 - 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/Appbar/Appbar.tsx b/src/components/Appbar/Appbar.tsx index 24cec9b902..906d5dc414 100644 --- a/src/components/Appbar/Appbar.tsx +++ b/src/components/Appbar/Appbar.tsx @@ -192,11 +192,7 @@ const Appbar = ({ [backgroundColor, minHeight, restStyle] ); const resolvedSearchInputStyle = React.useMemo( - () => [ - styles.searchInput, - { color: theme.colors.onSurface }, - searchBar?.inputStyle, - ], + () => [{ color: theme.colors.onSurface }, searchBar?.inputStyle], [searchBar?.inputStyle, theme.colors.onSurface] ); const searchBackgroundColor = isScrolled @@ -407,9 +403,6 @@ const styles = StyleSheet.create({ width: '100%', maxWidth: APPBAR_SEARCH_MAX_WIDTH, }, - searchInput: { - textAlign: 'center', - }, flexibleContainer: { flex: 1, }, diff --git a/src/components/__tests__/Appbar/Appbar.test.tsx b/src/components/__tests__/Appbar/Appbar.test.tsx index 952b556ae1..3164d774c1 100644 --- a/src/components/__tests__/Appbar/Appbar.test.tsx +++ b/src/components/__tests__/Appbar/Appbar.test.tsx @@ -442,7 +442,6 @@ describe('Appbar search', () => { }); expect(searchbox).toHaveStyle({ color: getTheme().colors.onSurface, - textAlign: 'center', }); expect(searchbox).toHaveProp( 'placeholderTextColor', From e7948e8b4633647cccaf0fae6c013e818dba0d94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Thu, 27 Aug 2026 09:19:37 +0200 Subject: [PATCH 24/34] fix(appbar): bring back accessibility related props --- src/components/Appbar/types.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/Appbar/types.ts b/src/components/Appbar/types.ts index 4696a7bb27..9369fbdc1b 100644 --- a/src/components/Appbar/types.ts +++ b/src/components/Appbar/types.ts @@ -149,13 +149,7 @@ type AppbarFlexibleImageHeadline = AppbarWrittenHeadline & type AppbarBaseProps = Omit< ViewProps, - | 'accessibilityLabel' - | 'accessibilityRole' - | 'accessible' - | 'aria-label' - | 'children' - | 'role' - | 'style' + 'accessibilityLabel' | 'accessibilityRole' | 'children' | 'style' > & { /** Optional leading button. */ leadingButton?: AppbarLeadingButton; From 825c8a98fad7c142303003141331ce85e4f98387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Thu, 27 Aug 2026 09:33:56 +0200 Subject: [PATCH 25/34] fix(appbar): drop unnecessary a11y prop --- src/components/Appbar/AppbarContent.tsx | 1 - src/components/__tests__/Appbar/Appbar.test.tsx | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/components/Appbar/AppbarContent.tsx b/src/components/Appbar/AppbarContent.tsx index 96d8e0dee6..b5f84d418f 100644 --- a/src/components/Appbar/AppbarContent.tsx +++ b/src/components/Appbar/AppbarContent.tsx @@ -100,7 +100,6 @@ const AppbarContent = ({ {headlineImage} diff --git a/src/components/__tests__/Appbar/Appbar.test.tsx b/src/components/__tests__/Appbar/Appbar.test.tsx index 3164d774c1..f302a640f9 100644 --- a/src/components/__tests__/Appbar/Appbar.test.tsx +++ b/src/components/__tests__/Appbar/Appbar.test.tsx @@ -688,10 +688,6 @@ describe('Appbar accessibility', () => { ); expect(imageContainer).toHaveProp('aria-hidden', true); - expect(imageContainer).toHaveProp( - 'importantForAccessibility', - 'no-hide-descendants' - ); expect( screen.queryByRole('heading', { name: 'Brand words' }) ).not.toBeOnTheScreen(); From d4ab10db5c67db3e57dfef9eef5d48a4aaa55419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Thu, 27 Aug 2026 11:03:05 +0200 Subject: [PATCH 26/34] fix(appbar): drop unnecessary margin from AppbarButton --- src/components/Appbar/AppbarButton.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/Appbar/AppbarButton.tsx b/src/components/Appbar/AppbarButton.tsx index 9fb694c330..42ed831fdb 100644 --- a/src/components/Appbar/AppbarButton.tsx +++ b/src/components/Appbar/AppbarButton.tsx @@ -51,7 +51,7 @@ const AppbarButton = ({ button, leading = false, theme }: Props) => { selected={mode ? true : undefined} size={24} animated - style={[styles.button, width === 'wide' && styles.wideButton, style]} + style={[width === 'wide' && styles.wideButton, style]} theme={theme} /> ); @@ -93,9 +93,6 @@ const MemoizedAppbarButton = React.memo( ); const styles = StyleSheet.create({ - button: { - margin: 4, - }, wideButton: { width: 56, }, From d6666cfab3989425d16d39f41f699a054a09d982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Thu, 27 Aug 2026 17:10:07 +0200 Subject: [PATCH 27/34] fix(appbar): drop low quality optimization from AppbarButton --- src/components/Appbar/AppbarButton.tsx | 38 +------------------ .../__tests__/Appbar/Appbar.test.tsx | 29 -------------- 2 files changed, 1 insertion(+), 66 deletions(-) diff --git a/src/components/Appbar/AppbarButton.tsx b/src/components/Appbar/AppbarButton.tsx index 42ed831fdb..3e2f905f2c 100644 --- a/src/components/Appbar/AppbarButton.tsx +++ b/src/components/Appbar/AppbarButton.tsx @@ -1,4 +1,3 @@ -import * as React from 'react'; import { StyleSheet } from 'react-native'; import AppbarBackIcon from './AppbarBackIcon'; @@ -57,45 +56,10 @@ const AppbarButton = ({ button, leading = false, theme }: Props) => { ); }; -const hasEqualButtonProps = ( - previous: Props['button'], - next: Props['button'] -) => { - if (previous === next) { - return true; - } - - const previousKeys = Object.keys(previous); - const nextKeys = Object.keys(next); - - if (previousKeys.length !== nextKeys.length) { - return false; - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - const previousRecord = previous as unknown as Record; - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - const nextRecord = next as unknown as Record; - - return previousKeys.every( - (key) => - Object.prototype.hasOwnProperty.call(nextRecord, key) && - Object.is(previousRecord[key], nextRecord[key]) - ); -}; - -const MemoizedAppbarButton = React.memo( - AppbarButton, - (previous, next) => - previous.leading === next.leading && - previous.theme === next.theme && - hasEqualButtonProps(previous.button, next.button) -); - const styles = StyleSheet.create({ wideButton: { width: 56, }, }); -export default MemoizedAppbarButton; +export default AppbarButton; diff --git a/src/components/__tests__/Appbar/Appbar.test.tsx b/src/components/__tests__/Appbar/Appbar.test.tsx index f302a640f9..523bbeed6b 100644 --- a/src/components/__tests__/Appbar/Appbar.test.tsx +++ b/src/components/__tests__/Appbar/Appbar.test.tsx @@ -255,35 +255,6 @@ describe('Appbar surface', () => { }); describe('Appbar actions', () => { - it('skips rendering unchanged action configurations', async () => { - const icon = jest.fn(() => ); - const { rerender } = await render( - - ); - const initialRenderCount = icon.mock.calls.length; - - await rerender( - - ); - - expect(screen.getByTestId('stable-action-icon')).toBeOnTheScreen(); - expect(icon).toHaveBeenCalledTimes(initialRenderCount); - }); - it('maps leading, trailing, and custom action colors', async () => { await render( Date: Fri, 28 Aug 2026 10:07:43 +0200 Subject: [PATCH 28/34] fix(appbar): handling tooltips and menu in buttons --- docs/6.x/docs/components/Appbar/Appbar.mdx | 25 ++++- docs/src/data/componentDocs6x.json | 4 +- example/src/Examples/MenuExample.tsx | 38 +++---- example/src/Examples/TooltipExample.tsx | 8 ++ src/components/Appbar/Appbar.tsx | 25 ++++- src/components/Appbar/AppbarButton.tsx | 5 +- src/components/Appbar/index.ts | 1 + src/components/Appbar/types.ts | 7 ++ .../__tests__/Appbar/Appbar.test.tsx | 99 ++++++++++++++++++- src/index.tsx | 1 + 10 files changed, 181 insertions(+), 32 deletions(-) diff --git a/docs/6.x/docs/components/Appbar/Appbar.mdx b/docs/6.x/docs/components/Appbar/Appbar.mdx index 0338f367ff..683d948640 100644 --- a/docs/6.x/docs/components/Appbar/Appbar.mdx +++ b/docs/6.x/docs/components/Appbar/Appbar.mdx @@ -31,8 +31,8 @@ has scrolled, and it accounts for safe-area insets. | `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. | | `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. | | `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. | -| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. | -| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`. | +| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. | +| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. | | `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. | | `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. | | `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. | @@ -80,6 +80,27 @@ const MyComponent = () => ( export default MyComponent; ``` +### Decorated action +```js +import { Appbar, Tooltip } from 'react-native-paper'; + + {}, + decorate: (button) => ( + {button} + ), + }, + ]} +/> +``` + ### Flexible app bar ```js (\n {} }}\n trailingActions={[\n {\n key: 'search',\n icon: 'magnify',\n 'aria-label': 'Search',\n onPress: () => {},\n },\n {\n key: 'more',\n icon: 'dots-vertical',\n 'aria-label': 'More options',\n onPress: () => {},\n },\n ]}\n />\n);\n\nexport default MyComponent;\n```\n\n### Flexible app bar\n```js\n\n```\n\n### Search app bar\n```js\n {} }}\n searchBar={{\n placeholder: 'Search messages',\n value: query,\n onChangeText: setQuery,\n }}\n/>\n```", + "description": "A Material Design app bar for displaying a page headline, navigation, and\ncontextual actions. Appbar supports small, medium flexible, large flexible,\nand search variants. Its surface color automatically changes when content\nhas scrolled, and it accounts for safe-area insets.\n\n## Variants\n\n| `variant` | Purpose |\n| --- | --- |\n| `small` | A compact 64dp app bar with a one-line headline. |\n| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. |\n| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. |\n| `search` | An app bar containing a centered Paper `Searchbar`. |\n\n## Main props\n\n| Prop | Description |\n| --- | --- |\n| `headline` | Required accessible headline for every non-search variant. |\n| `subtitle` | Optional supporting text for written headlines. |\n| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. |\n| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. |\n| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. |\n| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. |\n| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. |\n| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. |\n| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. |\n| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. |\n| `statusBarHeight` | Overrides only the automatic top inset. |\n| `contentStyle` | Styles the headline and subtitle area. |\n| `style` | Styles the app bar and can override its background color. |\n\n## Migrating from the compound API\n\n`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and\n`Appbar.BackAction` have been removed. Render one `Appbar` and provide its\nheadline, leading button, and trailing actions as props. The former\n`mode=\"medium\"` and `mode=\"large\"` values are now\n`variant=\"medium-flexible\"` and `variant=\"large-flexible\"`; centered\ncontent uses `headlineAlignment=\"center\"`.\n\n## Usage\n\n### Small app bar\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n {} }}\n trailingActions={[\n {\n key: 'search',\n icon: 'magnify',\n 'aria-label': 'Search',\n onPress: () => {},\n },\n {\n key: 'more',\n icon: 'dots-vertical',\n 'aria-label': 'More options',\n onPress: () => {},\n },\n ]}\n />\n);\n\nexport default MyComponent;\n```\n\n### Decorated action\n```js\nimport { Appbar, Tooltip } from 'react-native-paper';\n\n {},\n decorate: (button) => (\n {button}\n ),\n },\n ]}\n/>\n```\n\n### Flexible app bar\n```js\n\n```\n\n### Search app bar\n```js\n {} }}\n searchBar={{\n placeholder: 'Search messages',\n value: query,\n onChangeText: setQuery,\n }}\n/>\n```", "link": "appbar", "data": { - "description": "A Material Design app bar for displaying a page headline, navigation, and\ncontextual actions. Appbar supports small, medium flexible, large flexible,\nand search variants. Its surface color automatically changes when content\nhas scrolled, and it accounts for safe-area insets.\n\n## Variants\n\n| `variant` | Purpose |\n| --- | --- |\n| `small` | A compact 64dp app bar with a one-line headline. |\n| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. |\n| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. |\n| `search` | An app bar containing a centered Paper `Searchbar`. |\n\n## Main props\n\n| Prop | Description |\n| --- | --- |\n| `headline` | Required accessible headline for every non-search variant. |\n| `subtitle` | Optional supporting text for written headlines. |\n| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. |\n| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. |\n| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. |\n| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. |\n| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`. |\n| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. |\n| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. |\n| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. |\n| `statusBarHeight` | Overrides only the automatic top inset. |\n| `contentStyle` | Styles the headline and subtitle area. |\n| `style` | Styles the app bar and can override its background color. |\n\n## Migrating from the compound API\n\n`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and\n`Appbar.BackAction` have been removed. Render one `Appbar` and provide its\nheadline, leading button, and trailing actions as props. The former\n`mode=\"medium\"` and `mode=\"large\"` values are now\n`variant=\"medium-flexible\"` and `variant=\"large-flexible\"`; centered\ncontent uses `headlineAlignment=\"center\"`.\n\n## Usage\n\n### Small app bar\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n {} }}\n trailingActions={[\n {\n key: 'search',\n icon: 'magnify',\n 'aria-label': 'Search',\n onPress: () => {},\n },\n {\n key: 'more',\n icon: 'dots-vertical',\n 'aria-label': 'More options',\n onPress: () => {},\n },\n ]}\n />\n);\n\nexport default MyComponent;\n```\n\n### Flexible app bar\n```js\n\n```\n\n### Search app bar\n```js\n {} }}\n searchBar={{\n placeholder: 'Search messages',\n value: query,\n onChangeText: setQuery,\n }}\n/>\n```", + "description": "A Material Design app bar for displaying a page headline, navigation, and\ncontextual actions. Appbar supports small, medium flexible, large flexible,\nand search variants. Its surface color automatically changes when content\nhas scrolled, and it accounts for safe-area insets.\n\n## Variants\n\n| `variant` | Purpose |\n| --- | --- |\n| `small` | A compact 64dp app bar with a one-line headline. |\n| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. |\n| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. |\n| `search` | An app bar containing a centered Paper `Searchbar`. |\n\n## Main props\n\n| Prop | Description |\n| --- | --- |\n| `headline` | Required accessible headline for every non-search variant. |\n| `subtitle` | Optional supporting text for written headlines. |\n| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. |\n| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. |\n| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. |\n| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. |\n| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. |\n| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. |\n| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. |\n| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. |\n| `statusBarHeight` | Overrides only the automatic top inset. |\n| `contentStyle` | Styles the headline and subtitle area. |\n| `style` | Styles the app bar and can override its background color. |\n\n## Migrating from the compound API\n\n`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and\n`Appbar.BackAction` have been removed. Render one `Appbar` and provide its\nheadline, leading button, and trailing actions as props. The former\n`mode=\"medium\"` and `mode=\"large\"` values are now\n`variant=\"medium-flexible\"` and `variant=\"large-flexible\"`; centered\ncontent uses `headlineAlignment=\"center\"`.\n\n## Usage\n\n### Small app bar\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n {} }}\n trailingActions={[\n {\n key: 'search',\n icon: 'magnify',\n 'aria-label': 'Search',\n onPress: () => {},\n },\n {\n key: 'more',\n icon: 'dots-vertical',\n 'aria-label': 'More options',\n onPress: () => {},\n },\n ]}\n />\n);\n\nexport default MyComponent;\n```\n\n### Decorated action\n```js\nimport { Appbar, Tooltip } from 'react-native-paper';\n\n {},\n decorate: (button) => (\n {button}\n ),\n },\n ]}\n/>\n```\n\n### Flexible app bar\n```js\n\n```\n\n### Search app bar\n```js\n {} }}\n searchBar={{\n placeholder: 'Search messages',\n value: query,\n onChangeText: setQuery,\n }}\n/>\n```", "displayName": "Appbar", "methods": [], "statics": [], diff --git a/example/src/Examples/MenuExample.tsx b/example/src/Examples/MenuExample.tsx index 4401e55392..af584757b3 100644 --- a/example/src/Examples/MenuExample.tsx +++ b/example/src/Examples/MenuExample.tsx @@ -28,8 +28,6 @@ const MenuExample = () => { const [visible, setVisible] = React.useState({}); const [contextualMenuCoord, setContextualMenuCoor] = React.useState({ x: 0, y: 0 }); - const [appbarMenuCoord, setAppbarMenuCoord] = - React.useState({ x: 0, y: 0 }); const _toggleMenu = (name: string) => () => setVisible({ ...visible, [name]: !visible[name] }); @@ -44,14 +42,6 @@ const MenuExample = () => { setVisible({ menu3: true }); }; - const _openAppbarMenu = (event: GestureResponderEvent) => { - setAppbarMenuCoord({ - x: event.nativeEvent.pageX, - y: event.nativeEvent.pageY, - }); - setVisible({ ...visible, menu1: true }); - }; - React.useLayoutEffect(() => { navigation.setOptions({ headerShown: false, @@ -73,22 +63,24 @@ const MenuExample = () => { key: 'more', icon: MORE_ICON, 'aria-label': 'More options', - onPress: _openAppbarMenu, + onPress: _toggleMenu('menu1'), + decorate: (button) => ( + + {}} title="Undo" /> + {}} title="Redo" /> + + {}} title="Cut" disabled /> + {}} title="Copy" disabled /> + {}} title="Paste" /> + + ), }, ]} /> - - {}} title="Undo" /> - {}} title="Redo" /> - - {}} title="Cut" disabled /> - {}} title="Copy" disabled /> - {}} title="Paste" /> - { type: 'back', 'aria-label': 'Go back', onPress: () => navigation.goBack(), + decorate: (button) => {button}, }} trailingActions={[ { @@ -59,18 +60,25 @@ const TooltipExample = () => { icon: 'printer', 'aria-label': 'Print', onPress: () => {}, + decorate: (button) => ( + {button} + ), }, { key: 'search', icon: 'magnify', 'aria-label': 'Search', onPress: () => {}, + decorate: (button) => {button}, }, { key: 'more', icon: MORE_ICON, 'aria-label': 'More options', onPress: () => {}, + decorate: (button) => ( + {button} + ), }, ]} /> diff --git a/src/components/Appbar/Appbar.tsx b/src/components/Appbar/Appbar.tsx index 906d5dc414..ba6eddc629 100644 --- a/src/components/Appbar/Appbar.tsx +++ b/src/components/Appbar/Appbar.tsx @@ -45,8 +45,8 @@ const EMPTY_TRAILING_ACTIONS = [] as const; * | `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. | * | `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. | * | `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. | - * | `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. | - * | `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`. | + * | `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. | + * | `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. | * | `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. | * | `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. | * | `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. | @@ -95,6 +95,27 @@ const EMPTY_TRAILING_ACTIONS = [] as const; * export default MyComponent; * ``` * + * ### Decorated action + * ```js + * import { Appbar, Tooltip } from 'react-native-paper'; + * + * {}, + * decorate: (button) => ( + * {button} + * ), + * }, + * ]} + * /> + * ``` + * * ### Flexible app bar * ```js * { const { color, + decorate, key: _key, style, variant = 'standard', @@ -38,7 +39,7 @@ const AppbarButton = ({ button, leading = false, theme }: Props) => { ? theme.colors.onSurface : theme.colors.onSurfaceVariant); - return ( + const iconButton = ( { theme={theme} /> ); + + return decorate ? decorate(iconButton) : iconButton; }; const styles = StyleSheet.create({ diff --git a/src/components/Appbar/index.ts b/src/components/Appbar/index.ts index d8ba839263..c996944de4 100644 --- a/src/components/Appbar/index.ts +++ b/src/components/Appbar/index.ts @@ -1,6 +1,7 @@ export { default } from './Appbar'; export { Appbar } from './Appbar'; export type { + AppbarActionDecorator, AppbarFilledTrailingAction, AppbarHeadlineAlignment, AppbarHeadlinePressableProps, diff --git a/src/components/Appbar/types.ts b/src/components/Appbar/types.ts index 9369fbdc1b..dc3bf84348 100644 --- a/src/components/Appbar/types.ts +++ b/src/components/Appbar/types.ts @@ -59,6 +59,11 @@ export type AppbarHeadlinePressableProps = Pick< disabled?: boolean; }; +/** Decorates the resolved icon button without replacing its action configuration. */ +export type AppbarActionDecorator = ( + button: React.ReactElement +) => React.ReactElement; + type AppbarTrailingActionBase = Omit< IconButtonProps, 'icon' | 'iconColor' | 'mode' | 'selected' | 'size' | 'theme' | 'aria-label' @@ -71,6 +76,8 @@ type AppbarTrailingActionBase = Omit< 'aria-label': string; /** Custom trailing action icon color. */ color?: ColorValue; + /** Wraps the resolved icon button, for example with a Tooltip or Menu. */ + decorate?: AppbarActionDecorator; }; export type AppbarStandardTrailingAction = AppbarTrailingActionBase & { diff --git a/src/components/__tests__/Appbar/Appbar.test.tsx b/src/components/__tests__/Appbar/Appbar.test.tsx index 523bbeed6b..c031222785 100644 --- a/src/components/__tests__/Appbar/Appbar.test.tsx +++ b/src/components/__tests__/Appbar/Appbar.test.tsx @@ -1,13 +1,17 @@ import * as React from 'react'; -import { Text, View } from 'react-native'; +import { Dimensions, Text, View } from 'react-native'; +import type { GestureResponderEvent } from 'react-native'; import { describe, expect, it, jest } from '@jest/globals'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { getTheme } from '../../../core/theming'; -import { render, screen, userEvent } from '../../../test-utils'; +import { render, screen, userEvent, waitFor } from '../../../test-utils'; import Appbar from '../../Appbar'; import type { AppbarVariant } from '../../Appbar'; +import Menu from '../../Menu/Menu'; +import Portal from '../../Portal/Portal'; +import Tooltip from '../../Tooltip/Tooltip'; const testIDPrefix = 'appbar'; @@ -339,6 +343,97 @@ describe('Appbar actions', () => { ).toHaveStyle({ width: 56 }); } ); + + it('decorates an action with a working tooltip without losing its press handler', async () => { + const onPress = jest.fn<(event: GestureResponderEvent) => void>(); + + await render( + + ( + {button} + ), + }, + ]} + /> + + ); + + const action = screen.getByRole('button', { name: 'Print' }); + + await userEvent.press(action); + expect(onPress).toHaveBeenCalledTimes(1); + + await userEvent.longPress(action); + expect(await screen.findByText('Print shortcut')).toBeOnTheScreen(); + }); + + it('decorates an action with a menu anchored to the resolved button', async () => { + const dimensionsSpy = jest.spyOn(Dimensions, 'get').mockReturnValue({ + width: 400, + height: 800, + scale: 2, + fontScale: 2, + }); + const measureSpy = jest + .spyOn(View.prototype, 'measureInWindow') + .mockImplementation((callback) => callback(100, 100, 80, 32)); + const MenuAppbar = () => { + const [visible, setVisible] = React.useState(false); + + return ( + + setVisible(true), + decorate: (button) => ( + setVisible(false)} + anchor={button} + anchorPosition="bottom" + > + {}} /> + + ), + }, + ]} + /> + + ); + }; + + await render(); + + await userEvent.press(screen.getByRole('button', { name: 'More options' })); + + await waitFor(() => { + expect(screen.getByText('Undo')).toBeOnTheScreen(); + expect(screen.getByTestId('menu-view')).toHaveStyle({ + position: 'absolute', + left: 100, + top: 132, + }); + }); + expect(measureSpy).toHaveBeenCalled(); + + measureSpy.mockRestore(); + dimensionsSpy.mockRestore(); + }); }); describe('Appbar search', () => { diff --git a/src/index.tsx b/src/index.tsx index 7a45fc0c3b..00002929c4 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -57,6 +57,7 @@ export { default as Text, customText } from './components/Typography/Text'; export type { Props as ActivityIndicatorProps } from './components/ActivityIndicator'; export type { Props as AppbarProps, + AppbarActionDecorator, AppbarFilledTrailingAction, AppbarHeadlineAlignment, AppbarHeadlinePressableProps, From 870db24a0e0ddf32fbdbdb7bb5df1eb5e20719aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Fri, 28 Aug 2026 11:56:44 +0200 Subject: [PATCH 29/34] docs(appbar): improve docs for appbar --- .../docs/components/{Appbar => }/Appbar.mdx | 137 ++++++++++++++- docs/6.x/docs/components/Appbar/_meta.json | 3 - docs/6.x/docs/components/_meta.json | 8 +- docs/component-docs-plugin/generatePageMDX.ts | 10 +- docs/component-docs.config.ts | 24 ++- .../generate-current-component-docs.ts | 35 ++-- docs/scripts/generate-rspress-content.ts | 23 ++- docs/scripts/typescript-component-props.ts | 153 +++++++++++++++++ docs/src/data/componentDocs6x.json | 158 ++++++++++++++++-- docs/src/data/versionRouteFallbacks.json | 6 + src/components/Appbar/Appbar.tsx | 2 + src/components/Card/CardActions.tsx | 10 +- 12 files changed, 513 insertions(+), 56 deletions(-) rename docs/6.x/docs/components/{Appbar => }/Appbar.mdx (74%) delete mode 100644 docs/6.x/docs/components/Appbar/_meta.json create mode 100644 docs/scripts/typescript-component-props.ts diff --git a/docs/6.x/docs/components/Appbar/Appbar.mdx b/docs/6.x/docs/components/Appbar.mdx similarity index 74% rename from docs/6.x/docs/components/Appbar/Appbar.mdx rename to docs/6.x/docs/components/Appbar.mdx index 683d948640..e10581f196 100644 --- a/docs/6.x/docs/components/Appbar/Appbar.mdx +++ b/docs/6.x/docs/components/Appbar.mdx @@ -48,6 +48,11 @@ headline, leading button, and trailing actions as props. The former `mode="medium"` and `mode="large"` values are now `variant="medium-flexible"` and `variant="large-flexible"`; centered content uses `headlineAlignment="center"`. + + + + + ## Usage ### Small app bar @@ -126,18 +131,28 @@ import { Appbar, Tooltip } from 'react-native-paper'; ``` + + ## Props - + ### View props + +
+ +### theme + +
+ +
-### headlineAlignment +### leadingButton
- +
@@ -149,11 +164,43 @@ import { Appbar, Tooltip } from 'react-native-paper';
-### testID +### statusBarHeight + +
+ + + +
+ +### safeAreaInsets + +
+ + + +
+ +### style + +
+ + + +
+ +### ref
- + + +
+ +### headlineAlignment + +
+ +
@@ -163,6 +210,86 @@ import { Appbar, Tooltip } from 'react-native-paper'; +
+ +### contentStyle + +
+ + + +
+ +### searchBar + +
+ + + +
+ +### onHeadlinePress + +
+ + + +
+ +### headlinePressableProps + +
+ + + +
+ +### headline + +
+ + + +
+ +### subtitle + +
+ + + +
+ +### headlineProps + +
+ + + +
+ +### subtitleProps + +
+ + + +
+ +### variant (required) + +
+ + + +
+ +### headlineImage + +
+ + + diff --git a/docs/6.x/docs/components/Appbar/_meta.json b/docs/6.x/docs/components/Appbar/_meta.json deleted file mode 100644 index 8350bf635e..0000000000 --- a/docs/6.x/docs/components/Appbar/_meta.json +++ /dev/null @@ -1,3 +0,0 @@ -[ - "Appbar" -] diff --git a/docs/6.x/docs/components/_meta.json b/docs/6.x/docs/components/_meta.json index 6f793d41a3..c4e47d98fe 100644 --- a/docs/6.x/docs/components/_meta.json +++ b/docs/6.x/docs/components/_meta.json @@ -1,12 +1,6 @@ [ "ActivityIndicator", - { - "type": "dir", - "name": "Appbar", - "label": "Appbar", - "collapsible": true, - "collapsed": false - }, + "Appbar", { "type": "dir", "name": "Avatar", diff --git a/docs/component-docs-plugin/generatePageMDX.ts b/docs/component-docs-plugin/generatePageMDX.ts index 274f3622d8..2253501ea6 100644 --- a/docs/component-docs-plugin/generatePageMDX.ts +++ b/docs/component-docs-plugin/generatePageMDX.ts @@ -1,8 +1,16 @@ import componentDocsConfig from '../component-docs.config.ts'; -type ComponentProp = { +export type ComponentProp = { + defaultValue?: { + computed?: boolean; + value: string; + } | null; description?: string; required?: boolean; + tsType?: { + name: string; + raw?: string; + } | null; }; export type ComponentDoc = { diff --git a/docs/component-docs.config.ts b/docs/component-docs.config.ts index bb57ff6940..cb0bbc9db1 100644 --- a/docs/component-docs.config.ts +++ b/docs/component-docs.config.ts @@ -13,6 +13,13 @@ type ComponentDocsConfig = { docsRootDir: string; libsRootDir: string; pages: Pages; + typescriptProps: Record< + string, + { + sourcePath: string; + typeName: string; + } + >; customFields: { moreExamples: Record>; knownIssues: Record>; @@ -24,9 +31,7 @@ type ComponentDocsConfig = { const pages = { ActivityIndicator: 'ActivityIndicator', - Appbar: { - Appbar: 'Appbar/Appbar', - }, + Appbar: 'Appbar/Appbar', Avatar: { AvatarIcon: 'Avatar/AvatarIcon', AvatarImage: 'Avatar/AvatarImage', @@ -144,6 +149,19 @@ const componentDocsConfig: ComponentDocsConfig = { docsRootDir: path.join(__dirname, '6.x', 'docs', 'components'), libsRootDir: path.join(__dirname, '..', 'src', 'components'), pages, + typescriptProps: { + 'Appbar/Appbar': { + sourcePath: path.join( + __dirname, + '..', + 'src', + 'components', + 'Appbar', + 'types.ts' + ), + typeName: 'Props', + }, + }, customFields: { moreExamples: { Portal: { diff --git a/docs/scripts/generate-current-component-docs.ts b/docs/scripts/generate-current-component-docs.ts index c36d2e4ed1..c7cc44df85 100644 --- a/docs/scripts/generate-current-component-docs.ts +++ b/docs/scripts/generate-current-component-docs.ts @@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url'; import type { ComponentDoc } from '../component-docs-plugin/generatePageMDX.ts'; import generatePageMDX from '../component-docs-plugin/generatePageMDX.ts'; import componentDocsConfig from '../component-docs.config.ts'; +import { extractTypeScriptProps } from './typescript-component-props.ts'; const parseComponentDocs = componentDocsParser.default; @@ -78,27 +79,25 @@ const writeDocPage = ( const doc = parseComponentDocs(sourcePath, { root: componentDocsConfig.libsRootDir, }); + const componentLink = path + .relative(componentDocsConfig.libsRootDir, sourcePath) + .replace(/\.tsx$/, '') + .split(path.sep) + .join('/'); + const typescriptPropsConfig = + componentDocsConfig.typescriptProps[componentLink]; + + if (typescriptPropsConfig) { + doc.data.props = extractTypeScriptProps( + typescriptPropsConfig, + doc.data.props + ); + } ensureDir(path.dirname(targetPath)); - fs.writeFileSync( - targetPath, - generatePageMDX( - doc, - path - .relative(componentDocsConfig.libsRootDir, sourcePath) - .replace(/\.tsx$/, '') - .split(path.sep) - .join('/') - ) - ); + fs.writeFileSync(targetPath, generatePageMDX(doc, componentLink)); - docs[ - path - .relative(componentDocsConfig.libsRootDir, sourcePath) - .replace(/\.tsx$/, '') - .split(path.sep) - .join('/') - ] = doc; + docs[componentLink] = doc; }; const writeMetaFiles = () => { diff --git a/docs/scripts/generate-rspress-content.ts b/docs/scripts/generate-rspress-content.ts index 1c9accd93a..200cbc5e52 100644 --- a/docs/scripts/generate-rspress-content.ts +++ b/docs/scripts/generate-rspress-content.ts @@ -128,8 +128,29 @@ const getVersionComponentOrder = (version: string): MetaEntry[] => { return fromConfig; } + const stableComponentsDir = path.join( + getVersionDocsDir(version), + 'components' + ); + const stableOrder = fromConfig.map((entry) => { + const entryName = typeof entry === 'string' ? entry : entry.name; + const entryPath = path.join(stableComponentsDir, entryName); + + if (fs.existsSync(entryPath) && fs.statSync(entryPath).isDirectory()) { + return { + type: 'dir', + name: entryName, + label: entryName, + collapsible: true, + collapsed: false, + }; + } + + return entry; + }); + return [ - ...fromConfig, + ...stableOrder, { type: 'dir', name: 'HelperText', diff --git a/docs/scripts/typescript-component-props.ts b/docs/scripts/typescript-component-props.ts new file mode 100644 index 0000000000..d458ef4b34 --- /dev/null +++ b/docs/scripts/typescript-component-props.ts @@ -0,0 +1,153 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import ts from 'typescript'; + +import type { ComponentProp } from '../component-docs-plugin/generatePageMDX.ts'; + +type TypeScriptPropsConfig = { + sourcePath: string; + typeName: string; +}; + +const repoRootDir = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..', + '..' +); +const tsconfigPath = path.join(repoRootDir, 'tsconfig.source.json'); + +let program: ts.Program | undefined; + +const getProgram = () => { + if (program) { + return program; + } + + const config = ts.readConfigFile(tsconfigPath, ts.sys.readFile); + + if (config.error) { + throw new Error( + ts.flattenDiagnosticMessageText(config.error.messageText, '\n') + ); + } + + const parsedConfig = ts.parseJsonConfigFileContent( + config.config, + ts.sys, + path.dirname(tsconfigPath) + ); + + program = ts.createProgram(parsedConfig.fileNames, parsedConfig.options); + return program; +}; + +const getDeclarationTypeTexts = ( + symbol: ts.Symbol, + sourceFile: ts.SourceFile +) => { + const typeTexts = (symbol.declarations ?? []) + .filter( + (declaration): declaration is ts.PropertySignature => + declaration.getSourceFile() === sourceFile && + ts.isPropertySignature(declaration) && + Boolean(declaration.type) + ) + .map((declaration) => declaration.type?.getText(sourceFile) ?? '') + .filter((typeText) => typeText !== '' && typeText !== 'never'); + + return [...new Set(typeTexts)]; +}; + +const formatType = ( + checker: ts.TypeChecker, + symbol: ts.Symbol, + typeDeclaration: ts.TypeAliasDeclaration, + sourceFile: ts.SourceFile +) => { + const declarationTypeTexts = getDeclarationTypeTexts(symbol, sourceFile); + + if (declarationTypeTexts.length === 1) { + return declarationTypeTexts[0]; + } + + const propType = checker.getTypeOfSymbolAtLocation(symbol, typeDeclaration); + + return checker + .typeToString( + propType, + typeDeclaration, + ts.TypeFormatFlags.NoTruncation | + ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope + ) + .replace(/ \| undefined/g, ''); +}; + +const formatDescription = (checker: ts.TypeChecker, symbol: ts.Symbol) => { + const description = ts.displayPartsToString( + symbol.getDocumentationComment(checker) + ); + const tags = symbol + .getJsDocTags(checker) + .map((tag) => { + const text = ts.displayPartsToString(tag.text); + return `@${tag.name}${text ? ` ${text}` : ''}`; + }) + .filter((tag, index, allTags) => allTags.indexOf(tag) === index); + + return [description, ...tags].filter(Boolean).join('\n'); +}; + +export const extractTypeScriptProps = ( + { sourcePath, typeName }: TypeScriptPropsConfig, + parsedProps: Record +) => { + const currentProgram = getProgram(); + const checker = currentProgram.getTypeChecker(); + const resolvedSourcePath = fs.realpathSync(sourcePath); + const sourceFile = currentProgram + .getSourceFiles() + .find((file) => fs.realpathSync(file.fileName) === resolvedSourcePath); + + if (!sourceFile) { + throw new Error(`Could not find TypeScript props source: ${sourcePath}`); + } + + const typeDeclaration = sourceFile.statements.find( + (statement): statement is ts.TypeAliasDeclaration => + ts.isTypeAliasDeclaration(statement) && statement.name.text === typeName + ); + + if (!typeDeclaration) { + throw new Error(`Could not find type ${typeName} in ${sourcePath}`); + } + + const propsType = checker.getTypeAtLocation(typeDeclaration.name); + const props = checker + .getPropertiesOfType(propsType) + .filter((symbol) => + symbol.declarations?.some( + (declaration) => declaration.getSourceFile() === sourceFile + ) + ) + .map((symbol): [string, ComponentProp] => { + const parsedProp = parsedProps[symbol.name]; + const type = formatType(checker, symbol, typeDeclaration, sourceFile); + + return [ + symbol.name, + { + defaultValue: parsedProp?.defaultValue, + description: + formatDescription(checker, symbol) || parsedProp?.description || '', + required: (symbol.flags & ts.SymbolFlags.Optional) === 0, + tsType: { + name: type, + raw: type, + }, + }, + ]; + }); + + return Object.fromEntries(props); +}; diff --git a/docs/src/data/componentDocs6x.json b/docs/src/data/componentDocs6x.json index e9977beae4..af5d9b62c2 100644 --- a/docs/src/data/componentDocs6x.json +++ b/docs/src/data/componentDocs6x.json @@ -95,45 +95,177 @@ "Appbar/Appbar": { "filepath": "Appbar/Appbar.tsx", "title": "Appbar", - "description": "A Material Design app bar for displaying a page headline, navigation, and\ncontextual actions. Appbar supports small, medium flexible, large flexible,\nand search variants. Its surface color automatically changes when content\nhas scrolled, and it accounts for safe-area insets.\n\n## Variants\n\n| `variant` | Purpose |\n| --- | --- |\n| `small` | A compact 64dp app bar with a one-line headline. |\n| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. |\n| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. |\n| `search` | An app bar containing a centered Paper `Searchbar`. |\n\n## Main props\n\n| Prop | Description |\n| --- | --- |\n| `headline` | Required accessible headline for every non-search variant. |\n| `subtitle` | Optional supporting text for written headlines. |\n| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. |\n| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. |\n| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. |\n| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. |\n| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. |\n| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. |\n| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. |\n| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. |\n| `statusBarHeight` | Overrides only the automatic top inset. |\n| `contentStyle` | Styles the headline and subtitle area. |\n| `style` | Styles the app bar and can override its background color. |\n\n## Migrating from the compound API\n\n`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and\n`Appbar.BackAction` have been removed. Render one `Appbar` and provide its\nheadline, leading button, and trailing actions as props. The former\n`mode=\"medium\"` and `mode=\"large\"` values are now\n`variant=\"medium-flexible\"` and `variant=\"large-flexible\"`; centered\ncontent uses `headlineAlignment=\"center\"`.\n\n## Usage\n\n### Small app bar\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n {} }}\n trailingActions={[\n {\n key: 'search',\n icon: 'magnify',\n 'aria-label': 'Search',\n onPress: () => {},\n },\n {\n key: 'more',\n icon: 'dots-vertical',\n 'aria-label': 'More options',\n onPress: () => {},\n },\n ]}\n />\n);\n\nexport default MyComponent;\n```\n\n### Decorated action\n```js\nimport { Appbar, Tooltip } from 'react-native-paper';\n\n {},\n decorate: (button) => (\n {button}\n ),\n },\n ]}\n/>\n```\n\n### Flexible app bar\n```js\n\n```\n\n### Search app bar\n```js\n {} }}\n searchBar={{\n placeholder: 'Search messages',\n value: query,\n onChangeText: setQuery,\n }}\n/>\n```", + "description": "A Material Design app bar for displaying a page headline, navigation, and\ncontextual actions. Appbar supports small, medium flexible, large flexible,\nand search variants. Its surface color automatically changes when content\nhas scrolled, and it accounts for safe-area insets.\n\n## Variants\n\n| `variant` | Purpose |\n| --- | --- |\n| `small` | A compact 64dp app bar with a one-line headline. |\n| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. |\n| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. |\n| `search` | An app bar containing a centered Paper `Searchbar`. |\n\n## Main props\n\n| Prop | Description |\n| --- | --- |\n| `headline` | Required accessible headline for every non-search variant. |\n| `subtitle` | Optional supporting text for written headlines. |\n| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. |\n| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. |\n| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. |\n| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. |\n| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. |\n| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. |\n| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. |\n| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. |\n| `statusBarHeight` | Overrides only the automatic top inset. |\n| `contentStyle` | Styles the headline and subtitle area. |\n| `style` | Styles the app bar and can override its background color. |\n\n## Migrating from the compound API\n\n`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and\n`Appbar.BackAction` have been removed. Render one `Appbar` and provide its\nheadline, leading button, and trailing actions as props. The former\n`mode=\"medium\"` and `mode=\"large\"` values are now\n`variant=\"medium-flexible\"` and `variant=\"large-flexible\"`; centered\ncontent uses `headlineAlignment=\"center\"`.\n\n## Usage\n\n### Small app bar\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n {} }}\n trailingActions={[\n {\n key: 'search',\n icon: 'magnify',\n 'aria-label': 'Search',\n onPress: () => {},\n },\n {\n key: 'more',\n icon: 'dots-vertical',\n 'aria-label': 'More options',\n onPress: () => {},\n },\n ]}\n />\n);\n\nexport default MyComponent;\n```\n\n### Decorated action\n```js\nimport { Appbar, Tooltip } from 'react-native-paper';\n\n {},\n decorate: (button) => (\n {button}\n ),\n },\n ]}\n/>\n```\n\n### Flexible app bar\n```js\n\n```\n\n### Search app bar\n```js\n {} }}\n searchBar={{\n placeholder: 'Search messages',\n value: query,\n onChangeText: setQuery,\n }}\n/>\n```\n\n@extends View props https://reactnative.dev/docs/view#props", "link": "appbar", "data": { - "description": "A Material Design app bar for displaying a page headline, navigation, and\ncontextual actions. Appbar supports small, medium flexible, large flexible,\nand search variants. Its surface color automatically changes when content\nhas scrolled, and it accounts for safe-area insets.\n\n## Variants\n\n| `variant` | Purpose |\n| --- | --- |\n| `small` | A compact 64dp app bar with a one-line headline. |\n| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. |\n| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. |\n| `search` | An app bar containing a centered Paper `Searchbar`. |\n\n## Main props\n\n| Prop | Description |\n| --- | --- |\n| `headline` | Required accessible headline for every non-search variant. |\n| `subtitle` | Optional supporting text for written headlines. |\n| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. |\n| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. |\n| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. |\n| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. |\n| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. |\n| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. |\n| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. |\n| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. |\n| `statusBarHeight` | Overrides only the automatic top inset. |\n| `contentStyle` | Styles the headline and subtitle area. |\n| `style` | Styles the app bar and can override its background color. |\n\n## Migrating from the compound API\n\n`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and\n`Appbar.BackAction` have been removed. Render one `Appbar` and provide its\nheadline, leading button, and trailing actions as props. The former\n`mode=\"medium\"` and `mode=\"large\"` values are now\n`variant=\"medium-flexible\"` and `variant=\"large-flexible\"`; centered\ncontent uses `headlineAlignment=\"center\"`.\n\n## Usage\n\n### Small app bar\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n {} }}\n trailingActions={[\n {\n key: 'search',\n icon: 'magnify',\n 'aria-label': 'Search',\n onPress: () => {},\n },\n {\n key: 'more',\n icon: 'dots-vertical',\n 'aria-label': 'More options',\n onPress: () => {},\n },\n ]}\n />\n);\n\nexport default MyComponent;\n```\n\n### Decorated action\n```js\nimport { Appbar, Tooltip } from 'react-native-paper';\n\n {},\n decorate: (button) => (\n {button}\n ),\n },\n ]}\n/>\n```\n\n### Flexible app bar\n```js\n\n```\n\n### Search app bar\n```js\n {} }}\n searchBar={{\n placeholder: 'Search messages',\n value: query,\n onChangeText: setQuery,\n }}\n/>\n```", + "description": "A Material Design app bar for displaying a page headline, navigation, and\ncontextual actions. Appbar supports small, medium flexible, large flexible,\nand search variants. Its surface color automatically changes when content\nhas scrolled, and it accounts for safe-area insets.\n\n## Variants\n\n| `variant` | Purpose |\n| --- | --- |\n| `small` | A compact 64dp app bar with a one-line headline. |\n| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. |\n| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. |\n| `search` | An app bar containing a centered Paper `Searchbar`. |\n\n## Main props\n\n| Prop | Description |\n| --- | --- |\n| `headline` | Required accessible headline for every non-search variant. |\n| `subtitle` | Optional supporting text for written headlines. |\n| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. |\n| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. |\n| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. |\n| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. |\n| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. |\n| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. |\n| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. |\n| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. |\n| `statusBarHeight` | Overrides only the automatic top inset. |\n| `contentStyle` | Styles the headline and subtitle area. |\n| `style` | Styles the app bar and can override its background color. |\n\n## Migrating from the compound API\n\n`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and\n`Appbar.BackAction` have been removed. Render one `Appbar` and provide its\nheadline, leading button, and trailing actions as props. The former\n`mode=\"medium\"` and `mode=\"large\"` values are now\n`variant=\"medium-flexible\"` and `variant=\"large-flexible\"`; centered\ncontent uses `headlineAlignment=\"center\"`.\n\n## Usage\n\n### Small app bar\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n {} }}\n trailingActions={[\n {\n key: 'search',\n icon: 'magnify',\n 'aria-label': 'Search',\n onPress: () => {},\n },\n {\n key: 'more',\n icon: 'dots-vertical',\n 'aria-label': 'More options',\n onPress: () => {},\n },\n ]}\n />\n);\n\nexport default MyComponent;\n```\n\n### Decorated action\n```js\nimport { Appbar, Tooltip } from 'react-native-paper';\n\n {},\n decorate: (button) => (\n {button}\n ),\n },\n ]}\n/>\n```\n\n### Flexible app bar\n```js\n\n```\n\n### Search app bar\n```js\n {} }}\n searchBar={{\n placeholder: 'Search messages',\n value: query,\n onChangeText: setQuery,\n }}\n/>\n```\n\n@extends View props https://reactnative.dev/docs/view#props", "displayName": "Appbar", "methods": [], "statics": [], "props": { - "headlineAlignment": { - "defaultValue": { - "value": "'leading'", - "computed": false - }, + "theme": { + "description": "", "required": false, - "description": "" + "tsType": { + "name": "ThemeProp", + "raw": "ThemeProp" + } + }, + "leadingButton": { + "description": "Optional leading button.", + "required": false, + "tsType": { + "name": "AppbarLeadingButton", + "raw": "AppbarLeadingButton" + } }, "isScrolled": { "defaultValue": { "value": "false", "computed": false }, + "description": "Uses the on-scroll container color when true.", "required": false, - "description": "" + "tsType": { + "name": "boolean", + "raw": "boolean" + } }, - "testID": { + "statusBarHeight": { + "description": "Override for the automatic top safe-area inset.", + "required": false, + "tsType": { + "name": "number", + "raw": "number" + } + }, + "safeAreaInsets": { + "description": "Safe-area inset overrides. Unspecified values use the detected insets.", + "required": false, + "tsType": { + "name": "{\n top?: number;\n left?: number;\n right?: number;\n }", + "raw": "{\n top?: number;\n left?: number;\n right?: number;\n }" + } + }, + "style": { + "description": "", + "required": false, + "tsType": { + "name": "Animated.WithAnimatedValue>", + "raw": "Animated.WithAnimatedValue>" + } + }, + "ref": { + "description": "", + "required": false, + "tsType": { + "name": "React.Ref", + "raw": "React.Ref" + } + }, + "headlineAlignment": { "defaultValue": { - "value": "'appbar'", + "value": "'leading'", "computed": false }, + "description": "Headline and subtitle alignment.", "required": false, - "description": "" + "tsType": { + "name": "AppbarHeadlineAlignment", + "raw": "AppbarHeadlineAlignment" + } }, "trailingActions": { "defaultValue": { "value": "[]", "computed": false }, + "description": "Trailing actions.\nExterior trailing actions.", "required": false, - "description": "" + "tsType": { + "name": "AppbarTrailingActions | readonly AppbarStandardTrailingAction[]", + "raw": "AppbarTrailingActions | readonly AppbarStandardTrailingAction[]" + } + }, + "contentStyle": { + "description": "Style applied to the headline and subtitle area.", + "required": false, + "tsType": { + "name": "StyleProp", + "raw": "StyleProp" + } + }, + "searchBar": { + "description": "Props forwarded to the existing Paper Searchbar.", + "required": false, + "tsType": { + "name": "AppbarSearchbarProps", + "raw": "AppbarSearchbarProps" + } + }, + "onHeadlinePress": { + "description": "Called when the headline area is pressed.", + "required": false, + "tsType": { + "name": "(event: GestureResponderEvent) => void", + "raw": "(event: GestureResponderEvent) => void" + } + }, + "headlinePressableProps": { + "description": "Props applied to the interactive headline area.", + "required": false, + "tsType": { + "name": "AppbarHeadlinePressableProps", + "raw": "AppbarHeadlinePressableProps" + } + }, + "headline": { + "description": "Written headline displayed by the app bar.\nAccessible page headline. The image replaces this text visually.", + "required": false, + "tsType": { + "name": "string", + "raw": "string" + } + }, + "subtitle": { + "description": "Optional supporting text displayed below the headline.", + "required": false, + "tsType": { + "name": "string", + "raw": "string" + } + }, + "headlineProps": { + "description": "Props applied to the headline heading.", + "required": false, + "tsType": { + "name": "AppbarHeadlineTextProps", + "raw": "AppbarHeadlineTextProps" + } + }, + "subtitleProps": { + "description": "Props applied to the subtitle text.", + "required": false, + "tsType": { + "name": "AppbarTextProps", + "raw": "AppbarTextProps" + } + }, + "variant": { + "description": "", + "required": true, + "tsType": { + "name": "\"search\" | AppbarHeadlineVariant", + "raw": "\"search\" | AppbarHeadlineVariant" + } + }, + "headlineImage": { + "description": "Image or logo displayed in the app bar. It should fit within 32dp height.", + "required": false, + "tsType": { + "name": "React.ReactElement", + "raw": "React.ReactElement" + } } } }, diff --git a/docs/src/data/versionRouteFallbacks.json b/docs/src/data/versionRouteFallbacks.json index 326741b5b5..df2f5e3ac0 100644 --- a/docs/src/data/versionRouteFallbacks.json +++ b/docs/src/data/versionRouteFallbacks.json @@ -1,5 +1,10 @@ { "next": { + "/docs/components/Appbar/Appbar": "/6.x/docs/components/Appbar", + "/docs/components/Appbar/AppbarAction": "/6.x/docs/components/Appbar", + "/docs/components/Appbar/AppbarBackAction": "/6.x/docs/components/Appbar", + "/docs/components/Appbar/AppbarContent": "/6.x/docs/components/Appbar", + "/docs/components/Appbar/AppbarHeader": "/6.x/docs/components/Appbar", "/docs/components/Checkbox/CheckboxAndroid": "/6.x/docs/components/Checkbox/Checkbox", "/docs/components/Checkbox/CheckboxIOS": "/6.x/docs/components/Checkbox/Checkbox", "/docs/components/FAB/AnimatedFAB": "/6.x/docs/components/FAB/FAB", @@ -9,6 +14,7 @@ "/docs/guides/migration-guide-to-5.0": "/6.x/docs/guides/migration" }, "stable": { + "/6.x/docs/components/Appbar": "/docs/components/Appbar/Appbar", "/6.x/docs/components/FAB/FABExtended": "/docs/components/FAB/FAB", "/6.x/docs/components/FAB/FABMenu": "/docs/components/FAB/FAB", "/6.x/docs/guides/migration": "/docs/guides/migration-guide-to-5.0", diff --git a/src/components/Appbar/Appbar.tsx b/src/components/Appbar/Appbar.tsx index ba6eddc629..94964d842f 100644 --- a/src/components/Appbar/Appbar.tsx +++ b/src/components/Appbar/Appbar.tsx @@ -139,6 +139,8 @@ const EMPTY_TRAILING_ACTIONS = [] as const; * }} * /> * ``` + * + * @extends View props https://reactnative.dev/docs/view#props */ const Appbar = ({ contentStyle, diff --git a/src/components/Card/CardActions.tsx b/src/components/Card/CardActions.tsx index 529db00f52..3717069cb8 100644 --- a/src/components/Card/CardActions.tsx +++ b/src/components/Card/CardActions.tsx @@ -15,6 +15,10 @@ export type Props = ViewProps & { theme?: ThemeProp; }; +const defaultContainerStyle: ViewStyle = { + justifyContent: 'flex-end', +}; + /** * A component to show a list of actions inside a Card. * @@ -38,11 +42,7 @@ export type Props = ViewProps & { const CardActions = ({ theme, style, children, ...rest }: Props) => { useInternalTheme(theme); - const containerStyle = [ - styles.container, - { justifyContent: 'flex-end' } satisfies ViewStyle, - style, - ]; + const containerStyle = [styles.container, defaultContainerStyle, style]; return ( From 758a395eae0f16ecc4a2fca93c2a137016aaf8d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Fri, 28 Aug 2026 13:47:55 +0200 Subject: [PATCH 30/34] docs(appbar): further improvements (screenshots) --- docs/6.x/docs/components/Appbar.mdx | 72 +++++++++--------- .../screenshots/appbar-v6-large-flexible.png | Bin 0 -> 92306 bytes .../screenshots/appbar-v6-medium-flexible.png | Bin 0 -> 86237 bytes docs/public/screenshots/appbar-v6-search.png | Bin 0 -> 72559 bytes docs/public/screenshots/appbar-v6-small.png | Bin 0 -> 83054 bytes docs/src/data/componentDocs6x.json | 4 +- docs/src/data/screenshots.ts | 6 ++ src/components/Appbar/Appbar.tsx | 72 +++++++++--------- 8 files changed, 81 insertions(+), 73 deletions(-) create mode 100644 docs/public/screenshots/appbar-v6-large-flexible.png create mode 100644 docs/public/screenshots/appbar-v6-medium-flexible.png create mode 100644 docs/public/screenshots/appbar-v6-search.png create mode 100644 docs/public/screenshots/appbar-v6-small.png diff --git a/docs/6.x/docs/components/Appbar.mdx b/docs/6.x/docs/components/Appbar.mdx index e10581f196..f043c757b5 100644 --- a/docs/6.x/docs/components/Appbar.mdx +++ b/docs/6.x/docs/components/Appbar.mdx @@ -13,45 +13,11 @@ contextual actions. Appbar supports small, medium flexible, large flexible, and search variants. Its surface color automatically changes when content has scrolled, and it accounts for safe-area insets. -## Variants - -| `variant` | Purpose | -| --- | --- | -| `small` | A compact 64dp app bar with a one-line headline. | -| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. | -| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. | -| `search` | An app bar containing a centered Paper `Searchbar`. | - -## Main props - -| Prop | Description | -| --- | --- | -| `headline` | Required accessible headline for every non-search variant. | -| `subtitle` | Optional supporting text for written headlines. | -| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. | -| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. | -| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. | -| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. | -| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. | -| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. | -| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. | -| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. | -| `statusBarHeight` | Overrides only the automatic top inset. | -| `contentStyle` | Styles the headline and subtitle area. | -| `style` | Styles the app bar and can override its background color. | - -## Migrating from the compound API -`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and -`Appbar.BackAction` have been removed. Render one `Appbar` and provide its -headline, leading button, and trailing actions as props. The former -`mode="medium"` and `mode="large"` values are now -`variant="medium-flexible"` and `variant="large-flexible"`; centered -content uses `headlineAlignment="center"`. + - ## Usage @@ -130,6 +96,42 @@ import { Appbar, Tooltip } from 'react-native-paper'; /> ``` +## Variants + +| `variant` | Purpose | +| --- | --- | +| `small` | A compact 64dp app bar with a one-line headline. | +| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. | +| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. | +| `search` | An app bar containing a centered Paper `Searchbar`. | + +## Main props + +| Prop | Description | +| --- | --- | +| `headline` | Required accessible headline for every non-search variant. | +| `subtitle` | Optional supporting text for written headlines. | +| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. | +| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. | +| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. | +| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. | +| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. | +| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. | +| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. | +| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. | +| `statusBarHeight` | Overrides only the automatic top inset. | +| `contentStyle` | Styles the headline and subtitle area. | +| `style` | Styles the app bar and can override its background color. | + +## Migrating from the compound API + +`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and +`Appbar.BackAction` have been removed. Render one `Appbar` and provide its +headline, leading button, and trailing actions as props. The former +`mode="medium"` and `mode="large"` values are now +`variant="medium-flexible"` and `variant="large-flexible"`; centered +content uses `headlineAlignment="center"`. + diff --git a/docs/public/screenshots/appbar-v6-large-flexible.png b/docs/public/screenshots/appbar-v6-large-flexible.png new file mode 100644 index 0000000000000000000000000000000000000000..65b7a8c11135e0ea5c814bfa08596317d7752266 GIT binary patch literal 92306 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 D2YtUR literal 0 HcmV?d00001 diff --git a/docs/public/screenshots/appbar-v6-medium-flexible.png b/docs/public/screenshots/appbar-v6-medium-flexible.png new file mode 100644 index 0000000000000000000000000000000000000000..0a1c1db9e0b9d28156a2f2cd399610f3ab925d3c GIT binary patch literal 86237 zcmeFZbyQXB7d8qA2qN9F0ZD0TX_Rj129>T2(w&NebccYnwB)8Yh|=BNo9=GD#W_ch z{=PBpzxR%DkB+fed%bI|x95E3oX=cAFBGNFACNqNgM&ktkrr2hgF}1@2Zu_7f(%?a zAL=Us{=hq^NIi!u?j>6Xe!MWzk}*|KfMW#CQQ#2Zap3OV-2(iFdv5`U_}4ic93Ai% z4({G(_nmg1*Cwp4Hg1bj4Z%tr_ zf4T*9knOI7jh*!g+pl*6j|$#hp zomq!S^H5U|sb#7YH5^OBC*p!xJPFJG)2!_Y<4I_39hE z!9Q;P>siXCX72y301ly;kg{pvLRI?y|5hOC3wM9)f7%25tTyn4d%vml2=qVg0e&Ko z>D~LI`FBsxy93vYMcz67+5EewfexVm*Ma|L;J2mv-x9hL0KWfi;$Lgx|94WzF9hp< zg(4O@!X@Tbe7MNX3)9|DkC=RP8R}eiqa0RyCB%jhz+!<9iL<05GgJypSuCciQXDr& z#U3oPVeQ>mC)oQr<|?H^BMxRJaDR>I-=f%W7)@}0+34Wg7wlp*^RvwwWI=K=F7Z@1 z_b*Td2BmB`_%JnuG695Uluv?gDvF<;KB}>YMTHO(i{giXrl@|Z5(^f)n=3c%3fJ^P z-P3q70cZIK_{=n+B;0X>*p2yE3ti>eSv_ zu6&g6VXk~bM?+FCms-&|j+0A5MNkT0DG>ULnD>lEuugmqSXalvHRfERkE7tRM!uT% zWCfTBB;Xu1S~MxW(w!(Ik>_nammYe>hqI&l+tJZ+=mug$oo`1hS z*Olf|eEg_-qq88}dct1#e4%~ZWV(#qYV=_uuWc%`UWI5F86Q=yQcBd2nPL~ilCn>4 zs*shN`L#cbTpaU?(u#Z{ZTY^SQmWwKmsds>qZNr2?BLwTdSAZ68W%c;=UZVZ{NuxfwMD7efclDSO_E$>4w&%I+Xg)c-KuTMzJBhg8@q71!L zDGrdW)O^Md#Y~3RBE2`k6B)4~Zu_(~%TbozkYkA7&BC_5Nv?5n(XCZx41+3;A zh(y#B2;%u4!0;(C=KE0+944K{Zl?{X8PCG(=+VJ?4%@LonU4kA3Y0T*Vi?tRFi3nT zHE*h(`or&IlE?T3>|nZ!IRBa@EIWd6h;7L>@_h{Q zuPky3&5hTW%R@>caTY_3k)g>vo!(28VdE$!gHzxzav{r`t?5ctXJQe~5H>_6^+Fm= zy#a~Z*?xtCl^)VgF}l%g2k`d`8-}11g#+=+LN7DLFq+4Y++}^K#Us>ky%2P(uBZ)_ zu4U;O1)d$vewU^V&9I7ScKG;#95R>U(&g*18+`y{qTyNUPZv9wSWRo6e-zLy+Gem6 zZdx)c1k0U(N{<$5<3i9@I`*(zjIS@8nzz1H{fnF6QJ#jzJoUP8gyTkR3?ko@TOwH1 zH1N{qA%dt|*}=LKIsG&vf?2qIGeo8!w$XF9XZmzXH_vyVKA-ixwcFH&H|h$C<1%oA zTdf8gF^TL%3JTtyAy779zkZSbk=&0y&HcIXm3fp>z$9do@85YOf}Ql&TsYpBHZHQWf^OMnB|E02k`B<@8{L|V4XXy;(@Xc+D?^j+@d0m$ z(Y;^T)XxWr$99&c+_aC&_Sk$fD{;6$Q>4ej(Z7H7Jtl4gt;AHkhFsRhrHM^fs!*Lt zTQ7*iE1I2OJ*OE;4Hc39QC>912@B?>0X)vH!gF@llkK!L82EleQv>`-4E8b z`l+QtqPU^Av_p;Ov!Ay4^-@MWsD)%~rHHxgzOH7&mVo7$wpA5F+?q7koU_yx-gS-n zP z2bb@~r?N9;k1*fo;4{U0IAfmeXTLEL*R&dNoN?l959M_(PeEjwmz}`+*@F+t@F{5u zP_l!@?d4G3Hd47<4PL1U$=c1K9EI4nR9Cf8qOJzyBuD(le08?SeD#pDeD&vR9%uwRuaCLZQpuNEqmTJID!#RmEbf+4_-xP1Vf^LE)`ZEp zLrb>))JaE$?Y2kq^l*86Vy;!l{ip|ck0YulQiVKl((WIKj|upI$IDl8YP-h{C6kW~ z6mWjoKfaYsN+Lb`s9@KsPnU<&T&Q;SJU1i00i^s?nO*prvQs=Zw9tKF6)CedqsIF4 ztH#?A%z@BkuGo8*ryqL*(!)8;dSdeCJXv%h;Qc3B!W(b1NPLMoO&#;^k=2F5z)M1I zq{dy59eOzs&63%b)ezTH8JSyDyDafgFTF=tVQaa?0^V&Y5L(59;kr|WGPfa~4nTrt z$js@2C zNwA7}UL}xIHeYfZzT%XhqfsfqZF&w=g;R$6Z27W z`5mx$mzeXz)ehcP>8OzcO}AiXiU4Ah*V=xO#C?ahuJVNs9p83;8K9=suXQ|_?q3y3 zJ|OxG=xw<4@rUsE(oa@urTGIYi#;>ME~1d1`o|7lG~O=MeV>^miaZk?n%b>rfL>jc z*VGiNdpnc}`|Wf(|0{=-XnEux5mrJZ0%?GK|KS!`P40|7-%~=Sml)WJU-ecYP5? zCTv}ty2vM*?*DGNlc|IUe7{DXgv%PQ3}0UG*h9o}Ufd^NEzh$1m8Oz+!cBmD%0(>S zZlKvz9E(mz)8&0Pg99Quco3iB1Hgop+B4n4ZbRT#v!0eKA&QR2;0Zwe5sa@?D*61m zzkI@3)kC+n=I{Jm(#kal5Y+yD_7o&pp~R$nHRl|kNyC0Ccy{!w#gODgr9K{2FEv`xqqfX2?1c3sk3D>e$D$$M8DBz4B3dd-vb^?MKCT2eq3VgGW8vAY@yU;m zgz@}N8^N=TV$Uf_0DE72NjqIZndS4dFaUBPj!34w(b)x5@v|VCsj80CIUg&rvv zsetLBTWGd{(78r9h1b=yMI8N$`*i)+g&S0uKgZgx+*i?)Z@$q3M{3;C(9tO}l3KDu z|N5dX-%5dmms|68s6&LCZzkA&oM^k$c>T2lt&l#pNj&!WQ@cIiHB0)o(5BX)X!_;l zj%ZU~vH15raoowv?TM-ft9@+ehp_kzNh~oDcv(xsU2Y2;|JaJCBIF3ge{Om1Ow`hDmRb>;~9B@lYaJINs zn%Ag6Hv#YJ_tRBTEj=Hr*+z>7HXm{d13O?7|l-0dF3{-E1oUdg|40jji8q|-fCokDNlxkXn>-j zQi{Gf%}Abg@-$AjXX=HDD%&Mhga!iQ(?QwPrPkOc(h?}`l*^R|8!QNy-gs`dq0DaJ z`FVRniq3n=4T_i(&}2Ik8+4_5=VHvzhk?T9D%~QNQ(xq$pZX0<+6?QEdpK;`B--VVCq)Z<)1Vz*2z7aq}g@>SKx#$L1 zupxl)`m-BIxUH>k*6fSN4CmA(!%3)_GzxjrJT8WBQ|oZzy^fdN$WOQD=ewSQS!=77{+}Rt>U>blDwM<#(#3(&qaO+`iVFUuL+BWSngn9Z`{u zV~*XDIz*6JAIgm{H|s+#W0<1EazdESz;Vibi~J+m7vr|>viN3jH7>cQ`1Yw$t@T`^ zf^5v=L}_-b6M47m&@miC2zW-v(S)Gxs0DM5o+*jXO0o?0BZ;?8?I@fO_17Hpz=J9; z2IY*5NAl;oV})y7c3&~8PjXBq5$~U<#>ToG`W*8)=g22^o4n~FV!F0?nd!Jb zxDxeMhIcsI2x2|)x^M-eelu|p&Ns1^$Y~Y@el0u}qrabt?LO22(_FzOm*DVs`g$$Z)TT%}x6p{DC> zvB$DQtKgztjB*n2_=!&urwbJmNJ~pU8KXXRe16-G$8n=|CO7LNu9(6fYfZQlAm7@I z_EUW-`(5rpdA04t>hcbsKP2anu65p~lXyTFG?03KwCH4;Uz)+_Z7b#wG~VatT#8Zc z3nh2R=eNDbakO#-&{*^N%vS-DD@v!}+z^>v!USp)9_w*Rlg^k(jBPf~02*(m*8yMA z5D$o{wnFU|-~q?!Ue>_!8oubjTkoDgD;ny zN|^X5GyMHr>j9@dAL`96XL;wS)brK$>+wLX($9aoIx`eru9wlqDGb71Xcp_e@`i&< zQ-*OA06MC^q#$X7+)7m3_IlwYO-(g0+aEmR{x*A7t0V?asxa-<1)(xLy8K!ZE&uz# z9Jj+&B)oa8dSm4>=QtKk)``uR?~MT?LXNerUjH>HT=*aXJC-f^EQ$W7`WEGSKql-t zUZpKX)@kjML1~G^)wC#N$!AGQniHrUT01b+Zo4xgCE2j|Iw6c?YJ{WfrFEL2JK-z8 zhr}GxfLg{4=ii@kSq>NRR`Rqt7N;L{zYg6Gk{Hu-NWo)%@noM{>F1IHRz8a4{P!T- zGV}fChZ+P8DH3T`23*-Ht<#V8Q|r-Bw)iReHD{~i4T4;f@J`dU?$b`@#JkCE;}Q z*YLkLV2}E9CRL8&wJbT2YuZtLUh;WnA2(Z)qN%qc0ohK^5B_Gfz<8PdUomI-2G zrb^Ao9&B-#CNwVfPJJ)W(%7Bl?O0Kmit4dYF4l7+zTF@lEZ1~VHJ_=4c26`V^H|Uo z=~TucAU$Z0q?TIdHCiucP;HdXdAe1z04SvvW4HRB;uKi9lrQ{{&{pMcJ$9qAAeYSA zW#ipUDivQUtOpqT^S{;V=XMEn(5HFgjT(u2z$`@{6ao9W-(5vF1dWDFB$a5ozj$W` zqN$iBoWk6ce$G2>mucLSq`p&g3ZAI3$~8^2iqkF9EjFp${;z!#e=oj1Fch_GLzoJ*_I8E~+Q&OR8Wkc49+iJ9o*=n?~#-!1- z%6ee1tWbN%H10fUB1=ZKt8DRyob<)fC`FoVEVD_CQWj^Wm!`f`=$Ckx!TR2my77}{ zk4xzW@7o#7Ic#wnrVbN6)a>Yq&A$8-2vWep{+1qvwb! z=P$$T4rgH{h-S;iX1g8A!FG=~Ve`%RR=!N!-=TQiYDUndjH+XGVm$%NXDW$9C*?`N zZ6bX9;zcpExS;;9w=RlPO0nynha^9lpz903yfi{oAC#$c1}!gPio{0I%IfT7CT7dU z60onV%1b|GUF}W5Z2;Bi*PW=y9%jep$U?9Q=Gl$l1GDkOJ(N8OcB z7ig#fU@>!L=;z^+?I{yL70v!A_Aa_HY72?J47EzcGw^e4^|~0~?2>oAl->Sq&)i`V zW!A5+f7mB-d*l@AG{~N8&DtPGXViCeB;{tBJ|JYEx^q?X6ivx)PdY`#9#VM%Kh>GRFw`ze*Nxff8X|OUa)b_U9=1 zp#YQc-}L-9J^$^V|8@`7f5Of8Kivb5`2TI)!y&o$^ncW%T}l&Rh!|gnrDTM+;o>Y` zogYRC>GE_Wa^~VOX{bFQcucuInC+ksHGhIo`WR531bvKNzwA;#7y99$S>LQuXxCc5 zW7e*fKiQt!)lvfd_SxWV9hDL=C{G9WS|%;^*+JM#JSr^7zoW=ltrvMDf2e1>cTI+y zqaDK*a~)|uMRLj>AMsfj_4!@60zilWYph)nB}*1i`1xlyQ|CvCoMmrXBx}wVKi+gL z6aNkk#2cChGTWAy`h?>pKE}VzD#B+{i*~LUy{VxN<@Ge%ScDtJ-2U*uWSv4Iu%)=00gX6O} z{DkP6uoI%UHB#qWyS2-EY>% zF;o%{M$IO1QdbTjc z6N;x;cDARxn4VL_g!_N2@C9wE_VJSZf%bH^(>?}Ou!cWz|@R8oxtr81~Z6d)S#(;#UMua+Yno@eTHPorg~O;CT#}+ zcpZy2-xwLh0Mg*T7IpHYpD-Cf`=T^Rajd$mmnXZYe8wF(KeGFJc(rE@7R%qj5g%_1 z^Q^Ay%^pqHIB*QDbgAsxxPg)p+yzw64`5f$ijV@VCTDM^;Bs^nJ4Cy0*GRmb@l5nf zt&NY79$-zQ_(m)IHcisFen=k)bjeRItV*!;QQ2eimP|n6^i0Q!hlH$AhpT-aW{3QW zh>l@#QD^*n-z?laS)5SMlYaH{*#LL}V#wDK*sYQPPn?KXh|2AoYzN(y+tTTG%l zsCO)rnUBmwlNRcNH?Q>l`jlPgw9J&l$pR{OO3K#q(8@~eDU^oYr=x%>vgE#k-}VG6UV9REfl>kB5Zs6++z2sh3%^uLWaL0Lx2x-J3|t0TYaf2X7L9q`1>Lo+;1B8117we;cf~*1Y#0!-Z|>USl>MMvE9on zLHUS(mhfFQP|@{r>+3?9@p{?!`M?vyc=l02mhvP#-qaA4$+2qk<^{Iu^e;uakSG|F zMv@4FgyOb$3M*8e!?-QUoGrcjsI9Z|EkXOTQ{O)4>|?V%31FY{E2&@GHmF8mGYJPJ*9I{igvxQNW*;Y_0q zDFT^=H-Y)Td%!mtsWXO^g&(s`rUX@Zwa4)B_GCre(YlyOwb#I6cARl6lX_Vf6iuNUc|5emf%*?Wmp)Es~Wo!^!e5A&Iha*Q6LbKz%MuE_LI1{uY}|nE6d~sl}lP{HfF73Jbh#SK#e~ z?c+RRC7J6z#|7LV1Yg5 zptKM`=iQ&4cv*#BJxDb7g!LX7p`mAF2uMh_43Lsbx$!%{3sGOG&niD2sQfr%o=j|W6Xup=8ZOTqZF)pT^lMrvTM>(E%0524v{EY@VXF98 zJJ~^Ms}!MoIc?oL5;+LIJvwANIA3OCae*ELE55GOD}uEew`Pd~3_6DWYMDyfv3zolqlVidw z7thFWy;H65fRNEx8yufUp~d}`-J@42E;)utSCX7Zo%;!gk9bsNO z?$=I(jW$+oHjj_++xO0ll!Rf)AQ8SFAMvA;xGmcO-~|^h86KLA~tl+|=YT=l6^sa?2xQb6gvGo2qH1#(k*@ zuR@pmmC5*% z0{#&3JB0nBtzWU(+8eztZ3HqQ!G>xzQTy-Aofy>qf>=XDev!&yAOX7fm=tj$w>tFD zA_EpwRC;RwWm!*@b)-O!W9!`xq*s^|1IR&(T72({@yDxd>X@}Ew5n|8z$fYAL5qXc zukKNQ)flYwlHCjh$PxC)S4eypGv3KZ>s}5~Lax8f0vrMu|1W0VQKwbw{caHUra!U* z7kI>jR-*HB^FjJ?O-xu5-|&OJc0ASiRhIaK<(4-^qkSV8!c?#7Ey8 zi%zL??kJwA6PpYG!l><^?<2SGzeSuVH8Lx!jW5w+koZUt3y5&~u2m!ilq^Lc=o+5} zrEH7J?&`kWbDYKamz4v&y;@fk+B8flweTDXR&wl<>z=0tN*_q~(ydt(n)c%+?{$pcJAv(?m#Idl(lJS)# zLkd6fJ28G?UE6pJkPf&sUT#?-%5TWEh7fmO^WGkY7wT-2&i6m}Cwqon&W_-Xln~J3 z*FN`&>~vT27scA5wy57PUS`91zI$5R{J`RxaAz^JiX z@ULNhlZlL;l~pRp&R}u};Oq_wmx)|&;nv-So`u0Zp7@um2Ip6eN7;@YpF2}FMADtW zzb$b7kBt)QDBcGk9$8?i_Y4Thbh+*r`tm)!B9$q9EzuLJYTBJ-B5B>=qW#q_a+!U! zqA|--eV5AMcQ6HquwohFv^gB-)GD3>kSi`TxbXgDZ|pNzYHMzr;6LhZ7G7W8n;WeSJnN!uiu89Veirt|XvkiixhJgO?X2)kGI^Oz@B-u>+*iNBSoNC{&i(J;3kL$koX(HH1}fL%_&_+ zT@^cozY>xjRqKyW%D(*38O!0;sd;dgi8;keYgI|%_WO+Z`aYnc`4myg24y7W0dqa$ zs(!OObG;muuMQW??spHM2bdYkM`m}$ePqQfl(|Q8x6}Zl2opdC`dCUXl_LPeFqXQN z|6DXsgjC8l5#1`R>AeEj4f>tvt#jUG7f`8?es$;KxD<#D6rya33_K(I!%)N8Bj~4W zI{^t~0cv_yq8@Pm-v?w5##6lCh0R|Q(pLa6!FU+78l>^tDF1CQ`33;n{XW?rGUP9l z9KgefPp99cn$!Ni>;DSWJVn8pw`)rOm$UZMhM#@}EQ|lD`EStvTWSC8oBxE(f1>k0 zee<98{10;czXza<9Uo`@1Jfw{8ZJ*dI%A<71DR6UqJ9V}bs7o)E{60u0}j$(hBTIp zU-d&f`P74KW}T2_GxVR|0w|*y>0KYnjxU@jlLg3_v;b8O0I1l9xoUY)Dz$11K==%v zBTq61VGQNzwE;exhCK50kl|pq9AV^5{cZF6gG&k@E%MDyCVsggItAvg_9M0@Uaj(O zrfUxNK!)P?P=}Rn2I@2)#f_1ogIV zZWGAhY%|_#3GZC)6UM%VhA(6I4dwQY-=w~+y+0Acb}xy`k{Mvr-vyv}11Qslk&k~N z@Q3UuL)>9=g)S0i$}h&#P^ zBzfzTEaKz0N*j(sgZuQ&Vpew&r&Ox@iuSta2=~E7vTX+t42`mB`AHiVrK|*yx(X9f z)6GTeZROZpgq3YI`|G(lq<&_eXOtNT7q3jWs^h8MOusREup}vZ zOQ-01Dpb>`im<33Zw0mYXGx6Qdfb-o0e0%YI z&7Eq_cW>xFJNOFT3=fsrk1!JV--5xgkUAz`jK8gPth>)Ar1Ku@F43oXvC=E0M!iSI zq-tN#WNPGn>bMtCzdt$^hhn1xvDfz%6X38vGB+9t)rsP@CGYZ%Oyu_i>SEE=V_ceZ z9XjB_*O$n)P^Gg~n3?$4*Wl#82;?Sp?X3oKU|V<^ARPT|OZi0r1gX7@t-+e;qqWf< zrm+hcQVnIUVoKE8x430yyW|5h1cYn`R6d2;Mh#+7(S3=4hl*-mcoU}E>l8F$>SKcRRV<%WB3NX^ybaWAU#-)Uww~Z;;)P;9U|Dsccb1B zEl@4GT;s4xvmowI#_y=*s4@u{ja~ll&|p9`D~u7^o6IJbM!~8ibVKClBgOFGA3j`@ z%$>JNk=zK$*B;jC4nw{vo7;wTj22B9A7~l^G+XbLvm8YzYLRYkm|9(y<^27Za)_@* z=aX%r_#nT*EOw@(>TSm=@Px_+@vZIkO&bZ|zWTD<6|a=9UMF5=*0oYq+;1R)U?LmO z(^rz#L-x_bv2Ng@vnQubQa$IsKK0p~Z_bw#Ik)Dl+=Yf*5kC!CzZjG!9pH|$nGX=BX}@}$lDo|i{Sbc+=LdujH7dT``M8sKO4AA?Y`rd) z57`#)y*2npFNwAT`8RJrak;&7x4m#s64`cD+7IV6rJW1XMEM@nv>KgD1mB4NW>B{_ z2o6EJ+%(oK)l<(=n6U0Bj~yE_djnMRk009~A7J4XYCWie0zv=fd{)iz9X_K`WFtgrI*dTHsn1{ijlJx)D@vJ4cJ+ zP9A6_;|t{>h@*OkmTN&Op~m{guvLPNIwZX>2R2JSSuE5eR=VSLAk!#Vsj$3 zZ5e^zUuHFVqS`|B!2cGSbhIEfSoR}Zr9p?g%N}A}yPo9y(Si$dxVO+!=GBHV0mS1! zJ-G>&P7`i0=G2HDxZOOy)zp6#Xw33i1Bet&CzyNG!;qmxIyQ-EgYvvL;0^KRx=<+J z1!T27GSmKGS>`T!zuX?#Qr{R%$VR7KW-P-XBYpDmo)4Uq4d4HOVe5-+eqfS-a8qZPGbqLwmJoOUA^QK67w+YOQeQ& zC-1salsB?lPJc->#v>bZp!lniL)}bwr$aIKf;bj&`XMx~^{71PbycVR zVKxSCzvWaQhR3YENKgHKAn9{*u5zJ-G{8Y$-j#`o8QBP#v#5g|QX51B*+LUnY{zX# z%~c}^#vFC@IWPPG)`($w%;OhdKd*62I9p6Y<~lm(yvv!vGmI5-$hQrz0&3(TBh8NM zs`>-e*&T+|u)b0gT3^k%fN!(FpG;Y~`)cPh2nv6HB z**|toqgZK#ua6Gg#6fY{St+JEP6_cc3cPM^iiv4S|ksxk_Lw)BmcH}~B<7R3if7!YItvCeUi z^+HmpZokNAH`$}(c+3Dd&X&{hZEJHh!VtYYs@w)D(1lNH*hB|d8N{N=^ zSu}jxW5qV4d5_aU_%TsD?k59qhJ8CTKr~(W#F*A&7&CjJW7(8Qxiqgbtjy-E4&OZX|TW3f-H0;Ks|4x`pnMnXsPpYcGZhbuiC3k|2R_v<3a z_0XM65&q|S;FQV1-LR=1QS&DJ?`MIExS^I~;Ut*^Dme4fU2_Trs#Sw1FRer;1%MAj z2x|L^NfoUP?)!xq42TBYqzqDk_FemCTx6z3i$L_;r(m%sC7Cd<)(w%HCjeQhQXkzh zD?f~R0LongJ&#+8$Gw0g|K;z+(naWQ#%?~$dxlu&NcE_>xZU>V0(u*#ySql;hfaZQ zXV+4v(treHs?S9jcwHpTk-Np`u2eRj*Oq!HxlIrSlgZn)TqaydF5?XPJDm?Wdv2^;8?dKcZ+T%Fkumbu!jdX4`Q+w<+aRkxp6jer4mHDtT^ z(Q2e5NEvzK}l5t zg}#jE$hI?I2WU~UX}Y!$P_c!JV4WwZ*UbZ3Cx#>1&r8xLd|Lm|ZCM!#$r!I}V>F~U z8R31i+bGhJ|5U>50^J^ZbDBhrqN3*aL(#oaDwLS1PmnhRlk4`h13aC7&@L9hQgPEW z$eY;`7|yl&v2^A!I@+|OLhhqzSh`bVT5k%!PMpQc)jKrnE`Nr8%-8N70qT~s6?u20 zll*oLB`Ji5WLFU(r-v+GVZA(wK+^+vVu^s1Uc-X<_F)u}f!X3E^}8$wKJv6=EA|dfuRKI{eWnE07%`Cad{*V1V-8dqEn0 z`yUJzy1Ec+hFTKEg>Ub=gPu6O4MZ-|D7k5=hPqeP)CTK=_^sWinM&@`#m}r&dS1b> za$}_`sB%T0`B66Jomi+Pa5}R38?N+}H)LC0k=n|Z)Ae>q2i61$k4O!1Z;x87EPiC= zxD}bGuqY4TMWBwbYdVt@fR>g$vm87k9);w((y3!)fvl6wJ*BA{IWi+oI&`1pJno@2b zTW9+CK+}Y04ml<=i@URYIZVL_w0C|yE~FbF>EW&Jq89UBISd4!>)WY2}llzuW5D|ylA*~BssiL6*)wkDMA zGCiqT%-u*kM*OX45Pi(h%savLaNEUN>e(9P^&Kj=4f7e%OX|%hFA(%Hy4`k})5TA8 ztHtSTXcd1M?_|EKRZ^ua^7tW$XZnPI%q!rm^K75P`RMV~5+DBB+s{DiX8bmt+re_= zv(q_xfKpeXU#wx)4Bef*S@{wGnN57D8{eBX_vMw>l7dyV@swkIVZd6r`XH*3lOk5Z zaO)Y5=iX;O4GHD6cqUEOfevz~FOBMhhxrsbtx%5MeUj2iUNW}z>qB@AVReSN?n{sS`+v+<^l-#YRLRxi^y}J&@M?;cJ^;I(42hN_v(hzQ$1UNM zl57Qy-Qt?^O~*NQ(5ef?RE3uw)#Wwwq7P$woDe%?eI8nlzwpzixAwja9qSnos}hy; zL*94Fn0~2J0qsMnV7u^M>u4j7wJ=m*U-cg3^F2aC294wwkgN~Z>KCt(nLAm(zcVf7 zaR37w`>FCr0ygJQ6^ETn#b2*;TI<}>JTLNUwm!QwHjdiX8MwcxShEjp&ax`59lWX< z0H^WJxl8q?2$*SpV)p6%lop~Zk3rXUSpOJcn}v{a@~=HwOAJNTq98x`ytF|oLfqAk z9DRg7H~yw^j>==B-`0Gj%si4Rwa}N`$vcS9kjR1*YogXExjTWK^KUb-Mvf*Y zPhsg|?@hX^xREPyH}U-{YvY6rW+vq)$dO-J!6a2>p?+ixY=N;8jE|rer!@HZbOyvBih_2 z?bU#BTA{nsX6GgDEIU+w{}kz6U+>>;v+6ruV1iOwV~y)#o|$!c6uVKY?|b9TESFuy zBxBvN{@k>f16b!ooB;mxaAw`wd-sAYCLCsivf=!8`aX--gE!YZj#p1MRkfoYqR$>} zOi;@x9^_s$KTgOx6tSMJ=&&aQh3-$6E6G(^YqNA+o8&KHEuEm%9gV16DpNaOez$By z=0(QzY^w6$@9St+X5+_bshZ&vM*b3m3w$Dh7LQEZG zLrHn<8F{*1!(0w0a?>7Md_=_z3sWK5zj`uoisQUq&PF$df4*F|;1#Hp*}!o$S_;f| zSDo$6j=H14l`3le(IQ=2R>T-TD_yfXX#>;0ZW8hdg=Sr7GEJfCmSClK$`(C~q|N5ezsaw5^7%)WZtY^HSit=W z@5ckdYt8(O)z2o~Kcx{mA9yhe*re4-;BA|DxB z+<|jYp$b6pg9Z1d6bAcu4*7;*nE7l>q{m&Z^Y-wor5%+t-D~At;+JjTnR2)m&7wf? zZ!mdW*$AvO)u}$|-4g0D$9MDv_%x1+plcCnpSb`TXFqJvi1UC%?6n4W3(DdoQiM}6 zQQiF8x!_x ztyE7kf*P*_oC;s0tNH#7NhMbBQe7Sf=$pu1BYYM7qlOzvQaSQ4uE5s<3R^@Q!ga7D z3@0eHvf<^44a+?;lEPtBU$));NAsp@jI{i(dl@a7I3lc8q>^DKmAzr{`ljkm{ccX1 zqop%uq3pq8%Q{o`yo77#=K~2IYXBA>M`jng-t;ya^Yxc0J*dl*YYL=V&sxwVV#4z> zi+nzEN{EJ%#0ETUZ+V&hRA~aM7c>cd!|7I z@YO;@Pa^cB@oXl3mkOo_{LnXaerC z8{9Lxo=^baaL7Fe&^n$P@HL(IN;$xnYpEHNP;!XDY*@;%?=(nXt7k!p8$&nKw&Qje z1$h1L!NF-Dx^VJZk<~z)DRH!<)op-`Szih_m{8D$6=QM**N zhum?`;=LIu8+0+2>HT!#mlQ+ZwSAd)JO2!`JygiH3}h;_@q=BZPb;t0g24bH z@LlfGi54fNV5J3M+^?4@A`~(uE%zk1<#sa~Rqa2UW?avgqzxv7Fv`Y3oQsZasB4uE zO%*`&cJdm^aBQrNXH;Eq?-f_bAO zDqa_YD+f`NUACqfOk}6nWcn^cv>N%>?y_L8L=e`OatkcxhOCMkW$79O*w^L*G5W@6 zj|-z_H&g_D-RKKXDm$6BzB1>JK*BVf#2sdLggnHBilV&BNj7-L_|KDBHCL}4-oQB^ zXjQV09y(&g!e3ubIjcgqDFk}mj*+HP7(Vd(@w5t(X>>Rw2dgK#q=rS5>4GbkV16?z z2V=EPvm$ch&ZkO?cDg zLtT;we|e+z=$cT%P(D2YQF+Pj&PEMlRtmW6l-Xo^lR6w^9d|NT37j+Zo2V4gc?f!* zeMl1uU>db$aw47q$mdCiscWRn*L<&zR{hxrzcDKC?kp)POxjGNf#dyaOxlYv4piCf zMq>5F={@yKALdobx}#Uv3`yz`6>Ny>%6_!spM20tXXA|G!;si-qxC`H&ao+OTVA?} zEr48{r;|<>A>7urYnmM#Yi$degt(-!s24W&ecc8=haV1)h%2&I&>9L1{R;f}7S6cQe zn%9%n`*;Kc4MlC$2k<>==N!x3`l?5;rRtw_eqxzhZ^;<(PVfB~XvIJ56>RL~UR*~M z?b~=Y>6+GZbFeAs;Emz4s-@{#_J7!W@2@7eu5Hwf*tP<)K|qSAC`Bn!LlLmhREmHC z(vgkS&^sg%-GWk8ngjw$Z=pyDH9;v+5HJZf0fHce&>|%S5(3}N_Iby5p7$R(W1Mk* zW87rq&YEkkS*~l&x$bJZ1inW#`H;|^<$o&E&@s<_`ffS+?}jgM$b9g%lU^d639M4H zO2c&+prdzgJqmg3u*PhCMEs#fINWnZEmX#8%6*KU5`9pA7_-fLzQZM;YH5hu*YsrT zS8DyDCmE&y67rkbBz2`pR@~XPu(kHuBKUP!ybb<}<Z=e z!6}f>=if`DuC9gwbDVnnG8&c&Usor>9L_;JUb;mzDHl-T++j|g4zPO?r(MoWZ!^@= zyYU|&S|mwPpt~aUaToW>GGn9aJGB$RX#5ckx&DuY{^#+5v>_zw(4xg4osNCyY(;TC zVHnRKo0QCrAq}2H4#bei?%~W&|6<06IAr#R#0o+|^yLeTiD8A(DeAAqs>vu@d9I2& zEKC>DVvj4cydNStx&5>3`_!|y^{dXgOW6|a?;pr5N8bXj*p*Wi{T4J8X^7XX`Qb_P zEHDX`5&45#3$F4Nvq$v)yEvetmj>H)iP)_!fl7b@j`Mz1v}?rsOIf&Z6DiN!t9b`x z)Z$1Pc;apS=Mr{gcD&_NSKQ3yjD6a?N=D=e44(o@1RVzlG?w{%&83`S)rg zq{K}3$0<%mc~hV$!6Tc@&o34t2}{*DP5f<|-RREE-OF6uR1orVl3zf`iAkV0M-s|% zZ=g*cUQK<@&EU0LNmWsB(2T=L3CdrZOrw~ef6s@Rf_ghe01b4qc69`st^0<@x!Pb) z-6Ber3U=V!55)-|&mm4d!0hNosYM*Zx0$%DJ@zCwBF#27L+cKh;?(N1U{ZbKH&kC@ zW5+WGt*?wT(_zZZU&n(e2YCFP3KI{eDZdR)DEW7904fFF?97|ISPt#huz#rw0QYV5d6ArhKXLgvLJRv*DdC$3rdSTX z+#(CC#ju&J9#m{bav$9Ls?-6@(^KlZ;}KH!LwLA?H8h*{VP9cEj@#;v)T`v7g{t24 zE4HdJ*G2w-f*>w`iAt6ayZycLHv7Or#QbX^{@{Gjv6HH7aP!1M;;%em3DsbuhA{ja zG2x#=N*?dS-jGML>c%iKR8k3Q!bP@DP>x#CteY)vutFpNm^`7w_e=96^h599{52o# z3o(ZbOLJ~NNYk-V|qiko;-p|0l!EsiI&l%deq2Ro3cQop?2lI<4&((S2rJcWag78@xRF(Dok>3JWcT z3vY5#Gv21vDBjRNqJO$|)=w~>k%{iF3I!dk&?yc5Jh@5yg*bMjP{7=xZ(W=! zXTS?@%Lc+C_xpF_qMs}P^`ORj>)zO77^>&8d zqYf~Kc(qCVHvc=9SnEc3`KkI4+=C)ZL(E~M^7pn^{>D7@Y(B6fYT#yp#@%4aN5z;B zs(o-penhW7{$fHfPmY5Fr*F}k)HG^*N2{5syHCAd@J{o7o;BZ`X2H@x*3GBD6&0M0 zf1REddU4x@kOVSSWZp(?)*a?H8sz#;h=8=0TD9C)>@_Ok9}eEv?l@vn5@O&@Y#4PI zRbBcJ${dv|hR#@s=7kf% z0BdpXz1pulcwKB% z%x7l3Phm9VO{t7O!2}eB@H0^609G|Q*gCzt$L9q9>(h@?7tQ{f9{)}dL*>&caE46Jy!Dy|pcF(Ue7O~uz#}~o;raYLV!=RdlPQEA| z=0_yY8}ZG(8>p?Gy__N>-)I@nt2_guqk{IPJVPUAML;z5_RLosgCM7PqQoR#+*GrS zBufza>zyq$yLoS_D}pu%(`a5oDc=8;Z|`~0-aU=9n0}yU$AkG!PcKL+f2I&W|X?+#aM4T!9Eec6Z=@e{=_Dz|-PokoOB zLKv^t7wi42xGxnne{0(ah2o>S)2)jWl!s@Urk|a(R1dw|C*NvZ@|9g;LV&%MkpcEn z*pmcqJQouGD$p2p)A*OGq~;~)%H6AtD;Ci&pIo6J!k}29pJLiCO>gA5AL4`YSY(A< zt332uEw(7z58b@9`{P^F$`;8o@u&osne(kZHLc~voo@0gQECn*Ps=%II7w~H`TJwZ z&mleUwOkHXdivca2Zv}u`0>hJVmjVHBotG+P5*@Pq_e*FNp>BH4jvkpLu_MH@y(R5 z>RckY-IpNY{E-k9YN}Wdt~Pr&pB*;kJ6`lLGGg?|(JbCS-YP`zTzxJ(Oj<5Ix<4Wo zw%qbd0mfe1^uZ0lE>W}GbMG7zkMzLG=I=g?U;Nx&m}vrX813%4NBQUg@=tS*4n|G3 z9AyWd(Kb(}F7BCfVZV*og>tJ!Q!AYAA!383db~5> z4)x4<^rXyem9?8*&3204(>8!7tFM;^|MKJZ9M;)h<$;d1)X?Vc%qsQCPBtv0#zSw# zl49IdFylcqv(T06NZHM1isopBG{xEpB~RQ5Yt3q|^qAe>aN7OYvi*9>5Sf&>jDn3` z2tpAbC-954(BBq|zwh7t8>0vZH$Eb`>x{RT-Bmz8ms5U|Obv7@j&*&)rjM*>63v_Q zhDZC=#+#S{HniD^mW?PfA!JfrAsVpj!WPGH2&JgC=&e`5WHzIuRB>yH0Xfg!_}Hf@ z=S4j9uaLlDXlqgHb{(Tq3hxrzKMz8ngC_3h4KpI73r;36mJ-p3_1NEMgcmp?9I4*x zZ|}*zX(WY`s>a2F>}e14oap7&XAqg8ep~BbBvJ}fmYc0xcH^(MW`MpYmh%2unUR@G z@fLI5`_ZLaJjobS)~|!d;BP?e^pau+VQYO<43D?Pr{(am% zTMqK;FpXyIsPUtwGsVbIl-^opV%$5oSj{^zR=wafx8RzL9Q1-D6GgPzH&(*q8Y8rySe-l6~T7{ zV%9gDf~#$9o!nU~Q)yOF>~i{7NS-)k6IZ_!7j{qau3^cWqfCLy_E+z}9Mh?{i$A$- zO5L0BoV{8M(=T}hA!IoDHvbWaZC_)L^=M2r)0&H2IeTG~ifI?29cFhl3(!a%zJJ|k%MD8F zD$_zI#przG0*pc;XK=3s&Wf-tyHeX9T1QO2pVQ-dS`C@&qe8^Dr=NmRJPPovoJ|U* zP%Qqb(W!)%u9*bXBxeub7ZR;W!XadMm|9(uqdDCj$?1-!Csm_z)41znYQc)veNQD3 z<@DuVYcuxr;3)XhmwjK;*;e7eiEmG6%8sBI&#_f?y4Mo5V%gGf%>g( zz;un@_kD*6XYtW;4lF98fh(y)~`y+YCEedsL6=fP@d#^^Qz z8qAo)4{qtc_lEL*OSBaV3bMVno<1W@r$?oT|sP+v9&kYlhiMbZ|>*Y*o&8}Qfd;an1ZgPg< zxrlo8V&5x!l8C%yIZYXHpG%D)BaT`~)T$=VP--jSJ+9@(aiA;$V8Hr-L_^xKwBZ zE{Q{b#pKbTOWDZx#;9pM8-2{BwQA$Fu5f9wH}Z7&XrW*9N(J)duu_Vn6UZLMwsNnY z%)kI)yyb31i5cWE?GEqsOLnsJBhG>W9ZHU7=Sy9)dRNWRaKOgoOZH}Tf3G$alz10> zFG04=-!?SsoZ+R#j5~2uS|mja|C~o>%&U0BC6rtUvkwl4hrO98zQ$a8*LZwH$6iHn zIaG&gw^IaCjHYB_wqN89u<0REEDB(HM3=P^X6F^8@+O$p@*=P}*T`z@gG$ig^^L+o z*u15iz-_45;ON=gQ)jONaoN+3!oR-jxh0}kJuJ`l^R*Pg96}yyvJ;hC`&U8QaE;w) zOkR<`phG>m&6jV+K%3)Y`%zGS>}^iajLi4@_$hJn)pqFcYQIP9TkDW<1bJiikBSth zheF&^NQYAjJI`rQDSe*x_TmqmpvPbO@>W~T2E`IaP#1Eg6;R6`>0h5i&-s>wJb!YX zTkxgujVM_3V3im)IXG2{sHoi_s+@faAJ(C!e@&I+WV7YZX39Bn6LBK_u(rDxD3A9+ zGBh@_oi|BjlNP;^jhqe|Jo2c<#5td>Mp8p>pB8(=GtzY>)@YUfE`HY?5i(Lg$b-(D z&y8A*47km_o?ERPTQ4CXd7xW&F2COI^|x5#0=pN4t%qO1%dIdr@_5QpYhMUDN{ekK zvD;E3dUs!FnBLkBb2kri$K)+*d2HnRE@3yD#+mkOmwcuFcxOX`V7_Jl@uqtw$fp*mZ*lzNT|^Ey*ubcY@bJOWf{ZHYGR4<2n(E3H=kP7&hXqOjO1?N7d5 zK4x1z)(P^GDus1I&p@&7I}OEFk6&V&VmLGPm}r zXwU95Rtt?bg9ISXO|EA+Mio1W);Lv|#pPO8$_XLp`uAso@^*qN#310%L2=iA5<9sy z)WJ;rwb%D#(Ke$}jph|I`3VYC4I1+q?#IF>4AbrEYbAVk@|k$G9o}o>72$tgAL`J9 zw9W+ikQit-lE1;LaFt_C5B45+rlneAJLgrXu^AWEn3JP%GIA-|X`r{-p-lCb9yaT& zlmXw{g5+&5u}J8a8kVNOv{L{1-l@prC?88I$^JksmZC&q3f#KOuQ!XqsIoy}JVmH> zW!h;kf0#1z+Di2AA8U~a#V=2?O%Afk)CV2I!$Pn%JKwb){*|8ABBwp(dzax`FC{PF z;;?_{vOI}ixpT)^5nQs4y6ut;0yBkO6Wf|d5;_WlH& zGc%IYB2N2WBR)T%~T(z=6i`fX! z9e*gM%*+|?o(umc?4~7|G3J+&uhz*0no9F+`Vkw9MT7}xGY!ciW`+CO4ErUSpOxJ; z@C^aPYq^hKKFer!h%TiTqys2%Tk(n+%IMR*x)GLv^BZBrd zsx7^N-PfAU9U^7%4*GHBnF10td+TTn4}XDb8rj?alEU_)bxBc1q={`LwJ2`5jB4Yv zKV;FPr&%nf9FfwpFt`Bx92o=kmI9%Mg@H{EMgC<=eO*Jv&lW=?+1^ze3GY{U^E8rpxG72w8Ex%E5==crwTj>#S~FZQ<{%AUle>@(6T(*4g=)N_FdA zQ?=Zklo{B--3e8hjTe?RDO?Lj;Qo28bOJER&e121`zKb@I!6xdGoXoj9wz>y|M%IS$kDOG! zu~fIDp9ZnVVv*P6E$p0m2PJ{D+s5!1z{O>K8JrH$t zG*9ER|Mgu$F@UU(|EdH=D*l&L6L|nxudQj1{rSIz$_WTtqNDE0e|;R-1RX%wJy?eG z)&H#)yvG4)9~uOPgZ-DjbZj4}%m2TK|Ivm2kM9Ja`6>N1XE@<};k#>C!X#sY(D(;{ zg2CVcJ!gr)>RnxLB*rhEq5b_D;(47q$B}ZrUDng2x&#Ixj=E(!gkCmWB}C1H?lX0XTA^_v#= z8ca4^!a4R?<-FyXiC1mtwEyheLFVdDQ@7GGtpqh|;CK(a=?K^#6ONy7OFlbuS%N3`< zSKR<6*gr%}_bfrE^($?B>rPsh^+ZQdO#0;Oj1S-$DWWfTOFQobfCDk^fCO$98Uf&` z19QwU^xjoj%*6X8)1;HY6%Tb(7HgSA!Fh^1ZG9}8t+(Ghs=4ACLcHp@7O&DLTgQ)y z%Zd-0=kOda%3Ne29%ODneU3oaDxTvl&2%TNJ$6IGA_iOI9@HTTiobq1kn?7=#H*=j z7R`*jJrZU)R1?oXdd!Kr>rPm%8&q=yjfehcJI}`UAKplu zN%G8}TAmCgfgxC%X38k}*JFR=u01U2&=)m3u*Sp5U8ktY5HM(C5QqwxOEM*<4_P+* z=Z-OV<8*U}=5e0m9_31^g{GJo$E}KJuXC6eqq5C<3Kcd__&N7N=om{Caz*h0@&n*G zen?Z{Uyu^l>1OVRjHBQFPIS+WnmPjclqAjkVR-Hi1;|jBXgDFi_?iDWNtffs@rSTC z^B!%)UrjkIX`>wyxCc1Kv<(?A~YFnpnh5dNq zhY{wB^1$`0lYyia$hcmAGl*8>K&CLWO<_@zOBxlvv&z5E-*JxA4by`4{t4h(J8FiV zFVdL0vZBqHvQxR)^a1q(>s+>>ux%Ls^xh7wC)@AbbBhgrMfPU3$FP0rd}C)Q$#je; zGRzhlV8Qk&@XaJYm_m2029AKg*Is<4Mus6#UhLPfPsmlOJC%T%&sXca28xz1TNOkp1}U zHG1u&e4B?+pIx|f3}qxNVONVI!7Yu5*7)+eE_{Pg%CL?Dq$-D8QD4wy>i zgj?7ase_D@DE~ByA*ETzzi}^=rXPQOoVY0t=T!1Wt-LRZ!QlOAQ|_VE zuRba7Pu8r4zP(>qXiRA=NXC0jxotfi34NBY{P(A%%6g^W!DNB_HP6GM!;TZPcbMt> zE#>aZ-c{}L-F#b0`g|u}br^BxZ=kh9_x&;1t^4*tU?#;aGv$)^QZiqRpk{ED`pTtc z?q zk@Yk2p4~GiEK1(!t+udMgRSPu<^ihd;bdX(6C79crBnYi3n1%9Fu`;b1b$8*6|0F4 zI~5T{)m1RdbO_LInH|s~+~(2Fd2%JUYQXWqr~|aw4PRbS`BosB&H>p8i44MZXV$|n zrftk4+*dbaXUdj=eCCl&9zwol8_$3Hm;TC&l;y1t^}lYN%t$B}GX3@8?wP%2O72zg zdTfj#>J^-8zlFINJeO|W&hf-55?)?gmlVrKf*_AWb(o^+moZ}!1!-PDCeQE-VxV_X`Js-9!Zfb0%ty)A&P_>GJ*ha5) z+p4)8U$GP%*7%ghIf?UJ zm%C{!Cu;}9-lN3NA>IMY`Wr=pxv&fU8Wd6F#wJj<*fR04Z&Qmw(V_9UWiRzPu2s*V zEOR!j4>0*h5__VsL3#Oa1-khF(^jO;xNiG%=hhl3pppM}Z8ZShP($>SY68brykcCU zz6D?zCN0}<@3E$^*$cW$U&vwvkep^nGSST~9-T6;lvC3pPB1LB5|hY8v7?Bu;$pU?LuZUmgVjGnG= zx~|*3XDvRIwi9{z-{3aY!5M#4-O6DuL`GgcpBYpbeoLtHlYR&_#=`!FlfC`WLxNXD zl~Um$L;E%{*o-)V>Yg)f4b5`;maw&e66j3aTgeHsyZM5F_AVJY0&6yIGw?4Ya9M_A zE$%jbSRRRG1Dvdf!)bIlug2xLrVru2F#9H;iEvlVwL;wz`ly3cJH214@a&U!F1@$X z!*GMQ8oo8XvM+1ePC;K<1sGKHWmoYfgj`R*&xBFh0TCg5ts}q$xTf@HjfAaW#hm8c zQafWRbB25nGiq*a^WjB6Bo%NSJp(E7DKKk|Dg5bOQ}ZiYp>crhgl!^6&6m$7(^Hx4 z^Gxo(L|37?+wlXO1zE=0-6o+EoL${v6aGCfIYp{4j@Xm*rt#8qd~Zg9uv5kPsf6z? zx6WqGNPkqra#M2x4Cih-@w&~uB&;yiOas|sbak8e6Vi)GA`g1Ai+epc`nCMv%?sZ* zMvsZ(1zsoY1lafJVAXOWiDx|rwL{GUI<+VGvC}&ZPd!wtPbN&?RSjVw&|}84e$?%J z&l?lY)|NS`GZSZG(g>!ybs=@^0Tc$@G3#~-Gwd~9K-c+J0o)2mDVu3ME8X!1&}K-C zXlU0Pr0WNC9^gcrmx`4Kl3;sFUy)D8OkeCQ>%aG`;ipt}#&=lJpP=k1wKJ$QluznC z1t&MOyW+6~f%VD6)k*|EibrRsciReREAN}`q z@ov={y9jy9oPuM2`sREmAg0GN zpiO2=2A6t8WxJDpGGLpt!LDnZ+F|kQvv)?TVL=P`%)T6z|KZndi4;H6`EJCorUiKh z6weah?rWSjXkK=1;O(8$Y&S65Chz0WU_l}xBf&GERs?kZm>^5tocZyk@KHKHNFRas zh>pWyzL{-ly?6C5Ky&e)ep$XaER|1pi(S{?xXRk|*@u2giaf{rI`Peu*wiWo-@6#r zb8-4f-=U2bR&Ep?dQ5a|E2>&SSs-tAUYbMa$3&l>!ZShg&J>Tubr+gy(uiNf==xHh zet%vhy!M`Rhzj&DuPl=Y!b~bQnUkxwMO6{dnnAR_TpL!ie>GbG)Dy6}$Um`Qw@?J) zH3EWqjIJ-d)MUSxN@$(h*=p=h%-$snsul(os_Tjej{TqlwCJ+XL}?~(%*ZzBOlL>3 zTm1Vjct!Py)!&JwTMGq~7?y}{TbW!F@6~GaK8a%eUy|sJ(sm`Ami?Gx_!L-+Q_K6~ zv3GvM2Oaz|cSNRNo?HkSb@YzcTU*VU>4~V>WNZ*KANe5~@O6X6C&EuP?%j+N9kH^! zkdfj@0+D@EHUZeN|Qov)FKWB0SJ?Zz)Fs?h_jqd&V2=nK8VEakg1Ezc618y*0H_CQL)43xD#`CuU399PrrW@ z`*`O^Vbg_|#bR&rd#tXCK(`eK0!-xU6hH9;-LkZ zzLX8UA-x28fv1UTd-#BQ4Y|3v#hA|a*tM$I;hBR>Ec9@>sP@~ckfI`R{3VvXY>}7e z@n!%C=X6cRfEbUnByZfNgN8u`>&2R|+JrKQ^dn4ai;)N2hSH< zPa*>*^gHsQjtS(@99DkTO z4`8i?)%zZAQ9bff%1bfLC2MBps#na!BTs9)fu&7hvxu1!A*$`@rkfG_@-(pby;d+T zXXg8ijMdNsY0C#UQ$&y<3E6!{wMpXpV}^a>REen7Nr6M-9-VYMPyyuR(3IZ8eJ`!K zciIOeDi35D0$%xAFwyh_8M-bwTmq7YU((Cocf*b3~S`zoZB)8f`UVRB{pzKP9ngT-H*Q z0%P_$+3ojwu4w~_*|Cyp$j(U_==1i%9L%s|?^3<-(E0LntXgBvK+PiDHkTfuTj`Kh z#@QG`zuIII(l7P6!M+DMuO6O~-lv}@%)9XN$^Bveo7vCfZPkZ?ITSw)_@1jbmSdeK z7OV&R*Cu-?+PW>QmxHWfu<$Aiwb}$(0tPFn;auXx-=H^l2DeT|Z`m3wyPi5`Z-S%f z@9JDU^q?5o815Pm|HqtAVC(|;PWJ{VpQ0Rx%P=PC*zSxq_-3yM(-Pq{ubej8Enuh8 zuR59omiTJ#7|lRV4dYksUyc1%Ff+gEEWw;Fg^!X}4dA>aL& zUK!oA-7oof`lPm)HvE@==bCpEu6cU<1IoE?E9km3RND1q^oB5>Q07w*>T~AmN_DiNfn=x-igPQhJO}Q{ZuZ)aVX&S(7oOr zyOp9grjm;&;^T-R<)zfRxn-55a1!~3_Hi+6q;?Z8=`U6FDT|5aQA^IEMH$USB1zIbe^jAAr%^WgIR`4T9s+KTFDflvy#Nm znKH&RPV86S8nAlDg4Di+<$8Gj6RVKT4~uQ#m=!BK0aeU^f&n6;&Ce>BIM!z*r~#{h zycnIziC!%0*6#Ayk~Z_%BB-xZvCxNNJKOSi)?fiXNQ_=xWhoA1C1W6vFo5dgXY=a& zS4<^J8ZgU?!=*n)7i~TKmLTad90V5ifFxY!i<4M1mcGT^L>FV%SW8q-NjzbiEp zGj6aPc0JOSxHlm4S%R$omul+?3XL94Qn>-Hrv?8U<~DNOsBYeBv8vvgtLi%PNTvDZ z1I*W}nPnIQy;jEstCiA?ezZT@vg0KqGD~?@Jr%=Y=amDlhX1 z68wlymI86S?Nc_VUfG!NI#=!=t=O1_?E1!~9&J5QN`7j1R`J`i%`x?=cIe%pra;{S zgZ>*bzPVOT;Ng*LxVb=Fr98BHSnAVU+oJaX*-MqY$P(g?l$L>i4r#@SAjvm2zv8_Y zgOg?U8`C1X{7{Z8-?@up4m|mpeOT{P$C%9gyf`oA*SC!4Ebc&9A{_~pfz_TE8pj;)GoSIV_O3a8qIVW01ykM8*WS=wj zp!KUPrQ4hS%3p&WlP}2l#34HOv4Ad2f-oi-_2m>DTa}=fA1?Jx_hYKO{08vE*z5lk zG-yF~bm&8=5ZbH>dp@!r-MMf>MIPVB59%Saex%q5t^{?^5F6NT{{{B1fDYzxAlQ$KU&vjB(gwo6s-GI z4_UjIx@GeO2#k?}1loqL`0fpi=d9=F-HkzO70W1B^MoH}knA&{MMdF9?I#S#9Jj;y zAgYrQ6oh`=XO?YDZs`T{$Whjk(5=Qtf=S%g73?Ctpc>Sf+oKsWUT4`X-O)60yXuN| zbo+CB6SP#k?>SpLIdPFPy?B2ae1!R%|B}q=C`5iT=epYl9Ud(@piZ1v;|uqdIe5&I z)Gh1_?d|RB(`DpkM{K|i>#o9H{qapk30L~u1rbG);j<=?oy6hnIPr*jCvz5mCx%<~ z#XUpLCCmL&K+I}TeI}hi@10hn~i^*b9_GCg^WHTQ-NYpGvdjwi5 zY*t_4se>>hu!Mz^3N1}^1+NUBx?uq-mx%B85TQXP6QMKu z+^sr#ZWAGp{eV#3&-WO+)DYg{66~dBK3DjO#J(Vc#J`#3zdYctt8S9-CvDBvWz`HF) zWh`kg;a-uI!Wxj=r?8TK@;=-vAbPd+yzqNO-;C4UyVJbb6b-r1VBLK#A!(PSL1~Qb zs>nF=kYSxL&sY8EchmYI&!ObY8R4~<%n25rs+BkkTtf!7*=(;h zh&O+6@tyg~_v#id-goLP3(NA)o<^JSy@)IvDX>) zu+~V&;b`nSJFrilBn&CEeX{w;pCuMnullC-Cmz=5E8H4#)Xo7`NL#|hc*xTWATXkJ zLG?G?Mo9OW3^Vgq52kfjn6k;$FPb(hC}yqLj<%gjK$fjVG|XENrduIa)+24_ja2l9 zDbm@x+uzAsB6nnB15$8$`9Z6tbc+#ZO|;NJMBAnYFY6JPf7k08oTBg0P}}mytHe-c z_)e0QFbmUD&>*$`8#|QXdsnZjo?G2e=G3x~t{s~vi~W$2Fk$E0jvVH>5sbGkE`8hr zG=!uBCPrb%00595?fBot73SNfgPycU#I4}xvwiVlKdc_Lsq+7gfvT6EJhj38CRT2^ zADky%>{$$^jRn>$J2`R+^eAbsb)`~EU6VbpDrqK8)HT&6OL+n)ObDN1N5DhmD0{5h zEz_|a%;=fXN_-Q2@jLH+{zy*}Xfmy;bv2e>@E85UiE0c`cr?aqSQLIsH%DdTn{v_O z>AE%k@Q)>t%CV-mkAx|ekM6y4`yAF*e7v;#r3U@Rr&^5e1;~7ARM_M)j>#ozu2TC_ z=X5W>*saxCR$Tl>!2W-ekz1my;v_K57H!5ZVd-7_@x+a(7%rw z{p2gC9g2IEJTFd*qP5!HuC?xqXMhCR`bn9GuvfajDB`+Qy`Tp1&i*QjiQcakBV;wr z<+LRI8q%XR%De>z(~zYu^%z%7RA!%5j;J_x>EcKnBorI_!5 zj%+$mO9;;9FZg2xf4K?QIIX^Ynk~^yUazRi#h1~CyIIWFSZ~4G_%O{p{rB=-I}e_F zF{Z-Er6C`$bAR#S_?LkbdKlwrsLD8z5dk^g6Euja@hA_3k{tD_shxLl=l+I6YrTeDh)ztyFVaown zlx73ccNWloVztB|ZkxM6z#tKAzE`1ZE{Bql#~1K@V$gP{0xif(h%oXn11jxz5K=dG z&l;eGe|co@;N!|xBx}(r2)Mibqq9GXfp+p8JZ!HnniQGzlyEV7mu*P;K)+?aSu*qc$4m$4 z&V%uY(BaOg-`>p@sQ)-K-o*P-O3^KTv5`k-+j&Q^+hoi?P9Ld*kWr3qTPVWV#m@`f zzC##+XI|f**l@5;Nm$W%lGhI2128@$*opK6y?H_2s$c^nv{mzJsBDtScR2CqU)pPC zY5ZnkJi6;)WsZGPx!k@rQ!VUJR~2-pU4rDJe={x^pqVyyloonYIGtl-RH`9Xa}`t@ zccM=LG#6$q4_%ij$?(%c@BK)zj4fGi>fp?4+J2m{#X{`u{Ita1KnOdrX)s1b3&skU z*{f&7TVn$hxDwS=eWMi?@YJ7xG|MF%(?pc+zgK~S>2Iy%45Z47OA+|8Vv}ZLm6SxB z-b}-;Ai`yoaDRYB4+(-LcTf&6SSZ3{%VMCeK_{%-#9?s>OaAbIbH7Bcu>!6M!4B_q zMu>~0SZndL16&Y^qaK$Q_})AwU&B;Y-=urnPRk~qY#3s+@F@e7_is&@@ll}qg@|hN zMIOd2E_-}dn&1b%wR0W;`=m=GE@VU)KHOwOq0T$Cw2=2DE_L4~bd)~bFvApR3}o2( zOlA7DMJ$$;#H;;c6$#B(S;i;qlKHhK4Eb_tKqt@5=NAy`LezR8$-3MDO;!%GaD=iT zv;k0JlYUb-V%Y-co}UP)6V_gVF^=gjf>gXGt^vfQgXQK6b29*w7|DBJcz?B8DvU=d zxaF&mujR`Zg0`h+Zd9(iN5?5XHZ9?okkiAQw+xCGDPG7cf#=75x}Z-!ir^>?BL|r= zhi}F`5cHc24*BzSgPcmTPH=hm8Y}YDISs$q&}u8r(BXa4Rq zXx&cGBr+C0U&eJ(tZ^7!-Mc%}q}w8Nv%+%m=efTA2TL$X_ysQh8`Lz0GvVTPJwTcR zO|r!2L%WH|4=N<;^bHPbfHa%Ke|=x^v*J{S)eM^1{z1j?$C$nPUAPx+KJ9j%c*N1# zzEF=vZu(JO+@ua+m#>&TD&L5T!1Q=kn;qcF5ZOjUsLzI zAXDr~vQ(ZAxfq=o55m>K8USX`7p=jb89jz7N1IxHb8?P3VsV{Uo)D>jUW!9@c#2%m zznO_%r`JQxUC);bWA#IVV+GA*3YtxQ{0gNhC9M7H0M?hcG*;XsE5Yy%UKIwhhfksx zO!oJHa-a%+GozBcy)($<$(AFa$5EPGxE#hg8aukqX0`H z_0{8L%7Fg;%0XlXI4IeY+&t=7dm$c!QHg!506V{ z@5L{!OXa8+Vc<5ViB8))b;#F28nO^#N2d%(d+u-`##!} zRq~()G}-?-J>FJZus(MtlOQvqMUgkt8s+g1r5N7+$~y^a09`X`&{9gpazZ zJm5udu#gw526Ffc{@Wi%2=@nBbh_c}h6spUKkVX>Ly9!1w%chy;^5_dh9z}4!-qMC zB|7-o5jkuWres@dSVo(#b4>tNx9B+l==SWJQM9hsd!fr-4OZfIKWwXSnhzFa0?~8N zf-VB@yn|IcU>^l;e-Z!Q$i!jUIt~wpj=nOV%I_T6ZVlLH^EATKtXR7*XRjnr#ypb+ zpJn|{=ZYU6*o;h(m7>urnujZ_S@tXR3IQ`b%bK?esWXl32azs0{7OlPZ)~uH7f--4UtY`OSb`qR^XHVdL8qft?24fjT$pMWRiQl z@R1Tkz>_7X_Qds|O4dmJ?dGNv;s9=Q+^6wt4^ql*IMCIIyghm^KD(&cf-|=KW~ncU zS5+;PHGS;p%9uZ3${nUQsy35o(HLZ`sY-Sk2vO!2C7P-wxRm_!-OKmHViC**03|#N z9>=EkZiCKRMq|a99V|jvg01DSj^X395)Zt}ecN_*_ZJksdnDg>x0Z#Odqj>ba|hUD z!|0;*NqJ!ZlgVNzvOK%;__UQ}@O4{SbiF2qxfgS{u4i#mJY zos_R4o1%GbG|_U|$Nx`z*B%aK{>3HLD8Drm#kypoTxwlPYDi2mRKL~`mL=(akz7&{ z!x&qkap}&ETbV-Hnp8q!Mk(Fclv|PO?jkWRX&5v7-kD)$cbyEX-nWuE&3 z_0G*X*~RPn&~r}p-K9})kSz-Zx!&o`6RUSG+23nc|1ld)bhM*o#y+mFy6s7%oGo;J zI~bo*Fz;I;wL_n422SWcC*k6+?BrjIl`C`;EFS=4psSMs)vCO+X6{@9oq!X3TAAm8X@FX9B_4h$UU zI)|};jC*%G+hXb6eZSk<#Woa+g>DOX(0WcWN+67^UhSd>cf6B z&5Ih`D>;5+Wbe_hqa1ROO%hc5cMLE|UkeO@V7C zuDeyfzCO61WpC3WLr)&H%{`iSh#WnnY;!LHp)mzC6M`Afk9d<#f7f+qPe=SpeOls6 z%bLo*LK2D0Ztm8+k$q;MW;`p!f4`?u6yC{nVfta=#({3;t%EIUHBC zdJ@v7P-)lBX36WN*Qkx(iLi>t_+>IrtMaw0Y--2%Wu9zCW`~n6hE|^CH*MWYvJafD zPKj;E*tf@#-J+8);Cf2!Sc>lhrw=R6I+(p_OXuVsi>pKCZ*SS$=M63m>szWv8z8j> zWo*b+4ky~4yAry$uKMr_pWC)~E+m!s-alNktm2-Td7RUp`?ZJPcK7-(O*rG0|FVZ) zba<>0r;&sz&2URKMbw8Fg~bFJ-xI$l`Pc5EswJm!AD`N}h(@=S`7XhEGNdPKn^c+- z*R(M0?-~r1s1qnT&((Tk>HgHhWhr-ZoG`si`_Qs^2g})o9aVJ!^gt)oQM<^@BoaOG zR580$Z;i*N5{%|HyP(*R9d*|}u=QVmN*xX-OsFeb_HR4jP1h}pVniINa4-+63RvQ9 z!U>=b)3;&gDCxK#r5a4y5qF5!6BGexmo_< z{M?^<4r)F-M%Mp5?(uERr=mMuvwpuE zy}&0eXiwU(>kU?fMvu-0OjArLvAsBMo3q@{^po~-sTBwAb4tfN%F;(}RIpc9{WM3p zv#cRv62dNHEUUVuTp1a1j$|1}9C7rlIF4bhZq2qn+Gahibal#l@!SJ4Es~`fzezBS zLTN);ZmF4T-m1b}V$mLch*i7O_Jr!QtF+8>n~7$*+Reg2U!liL-Xg1$%Qi3i<)h&= zS-L(tt!u%^i=RBgBg#$E*amhx+z-Vr=3WR1tLA-(oKAkrU?l&m&auRj0!wOtrstu_eiHC>*g?xpz>W14 zoDb;H59*7AZjt+b<%Ex<>ri9mP;vSA!@ArwE#9r#%iWI86EKNZZVGfY&GwC=J_=E0 zzZHqn&&J+22FZ3fVC`1n2D^4{R!D64_o+6^;0C`&yJ(}n-K!04#27`gy5=Cm9Hn`4 zQE1gn{HL-&WPmJt>CFerkrYYO;ADK9vd-0V$GjKD%q<>a@n)~1=9GrUn0uy5hD#{d zT#-=}AO4HOZS>0JFeoNk(7K^r35!T%B4#-zbr44FNjY&*S zyr^N4@15B^PkL+x{Zt>>feGXM!=Ii~fHd9|VTzT7OGWaG1ba9gkO7;4<$(et03!e+ zfIx}l4UiK+P5?##;Q%}VgaZf%K%NDKamWcECjcXWoB%ulZb502Gj73Ecu@o8p8Tpa3XS_ z04M+o;L^aQflGr@21*$yWgriUYe#<%D2hJ!sXjjYDDGjGddQqJ9IWp0jlPEXOS@-3 zkZFt!SoC1lewB@C`608Z4su#Ya#v*ESv8l#FrT4Gdbue@`qhFfWo0X#ymuA84_ETm zL98+ww{DZO)INGFJA7+HVex;r6<-6g6`y*ro4h#J5*|<-@vW$3-(&s~d#1^%PowPY z=v(sF-vNuBjnY_{Wgzw4w&`F4e4D%$OTMiz!(TUzqEo(tAbESE(6#x>$tA=W=Sn@u zSNwi_Br7j4So*Drs2Oqz=r_tz-|ZF#ja%7wbFS2bT#jU6cF+K11Qq~UVjEz|lEUPd z=_j}giH`!GEOxdO@X6wc27ynKOMtrock!joqQPB&yZD0S0qz3a1-OgJVjqwZAR|z_ zm@FSbfk;wbgDRX@4q-v5Q(Rw#Ql}(dfV%*90m{fE#{^|W!m2=ilGJQL;}aKQKuDF; zY{6ZCyMUH}Nl_RyK1sZQqP;j?fW{|@7f`g9#EXBbyO0eIoG&Bu4Q+$%S{EqbLjfO} z4+TB}DsoVfgErd91QIB=BxU+LQtZ}I(u8u=&(b~9s8r)tVnC{YpSGANhz`(mP#Vi} z%q4YbVV2ZNnnvj$1*u5)o4ey%W7bwm_nUVj*XT?sUN6qOMF-``OXU@O62vNy=m6)9 zZ^gAV5v1;josY)ta14^4-%Y(Lt388dtR{_>9yY zW000})fx?{2M;KXWCfNMQKjyP{|*~ae(4X1M--X^WeB;(P^H%*sXOjK+T>@N*-Jfm zsxkH-3w8QHy3m+LsPeo|6B!Gv$JrGX#!MbBBrI%<%J1VHMWGAtvxY`Vq~Y3@1uZ;& zN<}Bb+M6G^MoaJuDa^3qf9@F@>o0vH_+AdVA*~jx z(uuw?3c^%8!X;jI5+zq(~X@e}ox@e&vX@h?g`vPlt#@3c|TU$~{Lc66T zB0~&`E<82C-FO4_n@x~kxYb9N!Cc-+6T zvo66+-IV%Ex_$i1hXnt&Ky3a<^9j9$_(;}OIV&=Mbbxy%u2k^pd6at2m3oe@w`i5> zP>4)EcYM5>+U$4E#a2Q<8XzRy07l0lZN#){q3t%Azp<*Ei8H(C?w$xNZXcNBd-khWXbu z8rpN2<>>p*r9dUDHfO!(KyHM!?9*lEvVim7y_< zv!(TQ1GE>;0>GuEvAqF}v!w;pPQY2{@n0*JpGh<-n>;8q=U;NebKkofK&I{J-!T%x5 z|7rTyT|iY~ycev$%_fZZK=%g@8k#7Y};RR>ijz} z$7v#Av{`IK00{8FfeIWk#2sf=u2>-i#Z_!1po7PX8 z@aX?@*z3{mEa+4Jm;9n=KHqNLS$OMCqJ;J@ zKaYP)d5=}p`^m2ATl9aa=8cAf`nQx%b-mFf7W2a}{-qikI>FI@SpP3&-Zr42hkR3i zg!M1gt|#Eezoh)%1pKz)|0dwt=l{0?{;ZMzLC`gl{l7zy=#!iO4P!Z&J{uzPyvOry z)epEg`8kjLO2M0B^xz+DA0kW4Qma!9+COGl!W^@@&V>4SV7J-YKkgXKR6vQ&E|@f& ze6aUdnLygd)%$Y)jlY|$KSMbO*~oP5Jr^;^KS&jMcXiVyLI(IzdLKwv4qfr?EacGA zSV^SZ_u;o&jhQYtiB~HG#l8tP6;%zQ>V5nRx>rawDonq)xE*M%_Odp!aheQpF)}s# zZp^tUkEXUmjA0d_BA4IY4~9RjM#zh>Q%{i+Qcrimy+rt}-PAz2jT=q6PUTIOnk54> zTQP^*7Y>s;tk%;?!l%q=~r*)%cFEJ?1 za?N@)rS;EGY{1J#p)=I)L;L!9=RjMBIZ5{g9fMrff8g{h%ZqHBuh*P9Zj6i{AghC? zJyES$dhp9E$%y9&>BFWuM}4pTkzQ7T)7$f&ICgUmym3{GRfSGX?|$icGedJ>!z76# z+3Ql1**DQ?ro(=FDtBVv<^_9)8bWWMFP^l5D$Bjg-6qJmxyUvEvtpK`P>k-FJz9WTVvV(Mik0VF>M|(zdEiy z|JmJ-Wc=#5_KJf{$6c(o0c9~asqNam0#0<8V6L%9#X4vge3#v-8R&7?;aolgp(;;W zLjO%4M4f#A{ibJa)+~LjG;-&p!0ST>KEgz3Tc;{Iz7986=Xb<|r zq^sS07Y71?$OMuy%k+{m%jRcB^9ZoaSys=NSJMMQtN2e3U592JQOs||LTVy>De4hb%2^~I>YEUAlf3yw zGp&6kB4I`%_l>~)aVrfO6QmG_-NjqQF-1C)_GGMQj-5ddz~3Zki7$lbe@_@qnBGEm zvS(jy%OI!cz$wT>_r&Z>e)STCUw6K)6j9eqP}Di5qIoE4JCBq3)!R>-R|ezU8}7))NHf~Y;6|2 zrYP5C5UMba`Sp!}EbiRxE-ELC^88l~H|{KazW;TbL1Lk={L%o?v^D^usMzuLc_Z?2 z9_>}(ht1t@6jn`8KL%bC+Y{6S6Y`7Gh{r!ZJ+!USbc{9bZ$j_cvnx!-tg-P@xU!^l@Gt6h)#TS<9(9bd~mMk~?>Sv?Nj}L48pYPJfJ35|?(uHUFGOJIgOsepEUL6f>W(yTL`pP-up#4D={+2gaut)NVoDDVhJfaHr zP;*Wxsoc(!EZ+0#!pW}9zsq>O3a&@+eRhrD&q}cLF)wQDDS$P`^DlmH@9WMEVvDyN z?xnQbR;(mr`oT&51UfY@-tTb?H|h*V)ybiIXGVc~%t%s}hPWrSdmk*x9>Xf2MhBo$ z$s19Si5Nj--Q0FHTsYQdJD|U-`8-E}a{p}q?U~W-v|=%k^(!l>5R%6!t>IkogRn<} zvB5mEQ>K?2N>3{@tViX<9a&Dq({_~d?_F5a1wTMFp^k>CqxEcZhb@?;>XA*^)8@qn z{5Y+6iS)OAqp~VP%I>ti&3E|D@)<=@rE+%BT16SN!!Wg%dj|g!wHIgd%!Y3J^ZvH# zBLH?Q>!+J5Cwb;3Hok%l*=a^e%LTf z7gN)7IW*?n=`zbo?e`jL*=d7<7i^7ozE+vZd)}zrq#Ng#uACuS$eD=C)Lz}|Eecw> zwOCY-w2s5)v6`PfM_mnY-l&@9kwz{2_!QH;U7x(4uzH^`Vy40@Lem0xjYSzCOq=$v zP{$&PuetjtYaQdq)Fa;iSq)4-N2FON@Sru)P3Jwm#<<3k##xl<_`8Lx!-R?cQBXOX-A42VT@+ej; ziGXi`72L)fA+yvO%=x^XP~fSZ$Tk%|-{x~!PEsDTq*^c>)w*#E#9Rk7M1EgSlfB4($W!h)w9J)lUZE-r*ha7F9I zPj0FwtZ=Z)+Mtd;fa=^Z%j!m#rQpeaqpD}!64V~+O~lSxGK>)|x3?qA9(uKN<8mn6 ziVb9GYT+^+YB`5D4>*vm@MHinO_&1HM{i826AklKBJPcSY`%adwV6eyWY2XH|I+?TR z4LjMlHeyAedl!Y(z}7ylrbSU{U5iSpw}r}6*HdLUV<8&$9Wf{!Xr8wy;&9D}>-xwg z3dFY)2({&SWrf$}m==VdWe6e~m^m*$?oFE@J;aTV$JO=XniFl}$_p@<$aI_~8!?zu z)p}n3p_ZjL6YLkeS10v6ZNTz2>LqP^j4_FhGaZgN58m-?j0=`MkeNNdsK!fJFV?EA z+_~>a5u2UtG8063`k52)vECxN5EXZqHep$KnddorB_%Bh5Lc!&A{R0%OYJKRB5ynP$~AuC5{DaeaY=k;^>q-_gVLcyf?Bt4^G7MH*@ zC_dbsb6)232hM)`SL&cT5%Z|VSWXiGqu}qa$X23s95rdZhoPB~GHV0*6j%zr;l7mO z+xn;EE%xTHG^4)o)@@j|m6hw!>`+erZCoEIVwx63|H?KV)e3K4N-BW@wJ{#ZH?u=C z6(t43kqpVz-0UQ4Y)4y8CG#+&kW0Hf_2bhNm>Z&zk@I!5r+!-#D;;Z+_%D6JnNoaM zLj^BmWNv9izAg{cz6Rd!PeSBWhIwH2b|yz6_?Nj~iy3=Zt17z$ks*%Yk;{f>L}_!fOhoUp!F&MsVQQ`f1?IA&u|g@qZY8fpn*ot&vC7rWixP}Yhq)Za;A zX&9yJx&czdD_FP6FGSss|6*p;g$QE?v?2kt%H?i%RE=yQ5QMIsw=6S^nHM*i@l4mK z7e$vRC39^E+V#+T@!V=yUhLhRlYMTE_2PFVfA6XF__+EzEZ&L@@+Y#REzlV^&NWP7 zu?`!Io2M8Q?+XvEue7PX&Wobxvsz{?#+j}dHz@nPAfPt8Y5Zs22 zRjrsp)B(Q5_!lcTj#wKUAG|w&Xy}nG&sCaR0G1a!o;;$rI4UPNetv;8AwM+hi{?^C z!7n{svsSunx{~z0f~xwCNp*jlI1i-1{9WQkZ}w7FSF$sc4SGmR8DmhbMMt$)=~5? zBd^w_`Q45z_D4fS;;QF8duNR#vek%QU8GnDmNeIH_L%Pt)L*Wr6UMtC8)2oRZzsJj zwl;EYGfIA~GjBc7IX2r&J+BMyNojvKT_eaj9H5(_Ff(nE27eASciu|u)03spAnfXf ziwQwlX;|Wi_o5@1knpNj3swwC;-N9!MmNjNb2EWSp`-f{mZo(jPH>;ZfJe2A1hw5e z@QnH~|M5}whDhvP+T?X%uIQ=bYO}fL`!RyQL_otBXJmuUPL^WwHWD1ucyn4ki1Y?91UouLL!o?@9N6qV`=hXW7>3(A%-!V6v zd8(P2d80q=ot!&>VMvtjTD+;ln$UEcWAe7C(oErPvDu@4Ql=|%66RO-kj&Nk+m}zq z%zM2-@ZhXlPagfE(veSpc912s8y4I!qM=?u`X3Ek{%)QLj3H&=1 z&9UaijWa$xX;|0%zfS@| z`cj00af&+dW%tz+4kyHHLG=(JN1z|h-yWiniP4p8ZJ}l@-Z55lCp*D4{DFXdj9I-Q zjDgvu#?VqwKSguAYRLI4HoLDH?rE;3=ix9qhy9FeLJMG?x4>yF62-Evf?jTf@#Z*< zUZZC8oX}Z5AY96i^??!C`aZ?tH1wImYD*CR)^rzF8y!6aQ;j@cyB4DyJjijQYA1TS z1%&JVDJ)CW?u#Jhrc+^nN zgD3y+2_Gl88?AK&7uDuesZj#a&lW7DgLlRA;ZXq#L7e}L5Ch!ZHtJ$!%3t8wk0o3 zL>F96r2Wl`Ao!sj&aa$Z&MO~1&(mjX;ETAm$^@&$4*?_!-W_mAtsyT@7XB@WLppRB zg>*`(U2l^%1erUS^wRl_>ok8Y`i6l|!NY~xZnOdyR?j<@pDss)a)$aLOlbYMf226*-sm4~Pp|F_Tk-2?jwQZ6KGc4esk$@u=$d_*DSPseGa>_e zi=_l*6MN;o(+BJu*b6b7<~%-Wnlx7SbvC-jt-TS_3N$zo!@15*@DE-Ap&WR9JX zc#R>qsdq#Z5hxJNVFb-G`~e z>k94yjtF)$7V7$gMd+Aqcnzl$6szk-p<_i}79GZjvsq2PdLhVI^8=jb{YiH;B@LgP z%Q~3_RtWAmg~8={iD*5wsx0Z&0tD?f`ePsDB)}Acx3U7IxF;Bl17t-DISB+0KFeG} zY=+Zb2lg2u6Ca5?@>n%rBn@)_k?qIh%svlU2ggNR+E^!vP<9DQPnFl_Q}1{}{(>ipo~q3q~P{bUw6{_;jp=hFI&L%)VE#r&Zy z^{>VDHeB==v4sOI0_SOYT|D$Dwk++4@)_Lf7S=k(b+-)cufFN7D-W%jUiB%iv}!KP z&BYb=069de-heA_(pGv1A{+9*M2l?HQaRoaFTZ`cB7Jxg0z+{4VMEpP#v?t181j2$ z0wicoh{y-)zLWeK_Af=h(_0^DUfq6yF0%JIy`O>K_a<$!d!E^7oowL?w}Yvc+vF7< zg=>fJ>+T(I*BTT~ehuBbB^CFKRXr?Iy?9>CioVzG>ZD8ajn89;bTtL>>lHI_0)zLK z$$n51*c+zpYi%cFuFzQ2Q-7eUrsR%+f92U3Ytj)d#Q)^i#_|_j|G=!X-+)xj+A{7R zs_Vr*;Pi9^^(Q&E-8351#L#+1H@Pa!%`1qk2zX&(BEce(AMvZIqtt^1Js@e zsj)l`c|arw2S{9*^Xbmetip^jQOW%;{WT( zH(KC~O<2qJ%`g7{t5=_UfY5Yzx$?#S_2dNeru8=y>487A?br7F7!cr@FPQsZPwpfG zFm__m{op@5^;&GHr$8|FYN!DFUr!c^fV^lj^M832(6y*PAi&p~apV6$`u{VVs5@}v zxmfK!AH<<>dc*@>w3RjeFH{&@W^C?)BL{fam{ja{f0t|366d6e%fQLgTj*Tr3O%t#jp-N7njvhi?LrI+SDD}Wlsn&97qr)HSWl*4 z?zslC!HOyAo%1{vQ!VT40ErHN1i;mU!*6O?D*8WOQVf-d1!2`ydOiKUs(qZX5^hNi z7)-YH1(5R;um^7^`%=}(17r7Gr%7TD+te~OfUxg_oT#`Mz61B8-@(w$uj(ze>lMp} z5p`8`Q$NnRAsX}eMmslY!k!K3&r`w%5&;@zOrpRh?h|}^6#9o#g;Dx+@_ZkD>s7J+ zRxYzNQNm8WV?d@;^*>$SFW_n>DSmt>8o;KS zcim(@lo*ggcCV(o)FIFloV)VB_fvmT)3G6J0rJtjC&Q87J86{w$aR;K#^Sgb=EmH+0EP=5+KS0b~piJX4Jcc_%+q>mWj}uxz=-)_}OGY#F?^&bk}7ZyWoZ!J|K)+mQKW2JoiN_Nb%)=ep(^K7aNI(J0aKtYBFq z!8`PT5CKFqTp9^v(oghP`{L!T{Vx8fN5g}z^LCV-_ODhcfl@ZQOo@g*9f4%=(??xz zZUlt?atpxPpOy75&p&{N(>lQPu()(FKWv+#fQGv{y1s6C}g7BLqaF~r)e#gy-c2wGSKHT(B~J}M|CBZ6wlDUui<-% z-iyE8jpS*Hkup^}pb3oz&|k@|LBPyOz7#>)iF=LzXctMFVx&Tq5420E0$gI|`HKi! z^w%A0_h#Ro>Wc3EqDMYGBtxYP%=`@1o>OIe*EA$q$h=4t{m?vPA9s2BTC}UEuPFTMjG>@303hkaJq78DSfn$x z9kI-ri~0WeuYOP0Pu%tUYF%Z37qL|==+lbomnLet4@7M~?`BNYd(jyz z)7CFXLf(7v(^Y2h$DVhli2lsxJ%$t`O{&rGJ8$a&Wh;>A^sLR;CLq(UdB7om&qvrh zbgEGf4bO1%Fw4-Ffc1z2mcP734;JV@0QnaP@R0vngrYQ}qFq&e1^3X}m2>X`vk{k< zm7?qO+L7itSY|iC`giOhnnI3gACzyTa}UD|z`QS=pzJ=Lno=~Z-yn&OnWyA!G?!PP zhDUNe~Rm>K{fTzR*E6sPypy@>aNnG64PPBvqxEJS@chT`=+5<0N^kc=nHzZ|F@hGca~SzRdG z{uxgM(_W^)$mkx%I1}Jxapuuwv8Vf|_e^;Ijz6zQ6vd#VD5qAr_6SJv58~)Sv&Pnn z68iZYbOQfWZM1VFpq;Vp0s?SgMN4ie7Y>Y9Ih@3^e`ZFs!ASmkuo&u#n}AX43C#0b+}8jk*=Klb((EkWz*X=Qm4?+%dn z?qYGN1tOs8qcm8GvnD6>!>50A$A=-HHo?w`>+@Tx`+WZYtlRU!z_`r=Li-#|L=PD1 z%XsITHKK)Hfne>l8;c94!}pvjTn=Cy7f4so0J%vv%!0?nY&n~C0Szd_|DG4T*P zr(ZDXJ42uM00_L*_)uDVibZoR;6Ud!&)Ix|CMOV9-Wsg}0<<~@^U)MBKYQfOuMdAaLQ0BvNMbyIZ(cyf_l-36RK0*~)lDO5fzF5KLO>F72v#sKO&Ou@G zrm|`WNlSI@;oFpaL%5E2)^h@BkiX|TPb;MqA379}Ubn?3oB5>WuA=2&ya+sqns>gS zaw6{T%3GceyuJkPY6$lIM1H$0my<6NeQwBRJtpymy+i>A4mW%%2=UK=LC*}Ts_=Ws&Cv{FVTSCo53KGEcTvat z6Va~riL-8(q7dNyexBa?+kc$kW`?+EA@5j#p9;B;iG+|9;tJ{KqRwLnkwONKK^gp<_Kv(8inV#r!#1B-P=-zhEF8Zuzv(W*r zU5UEoo=BTcrcq;Ua}XQF*5x$sF$tVRsQZ|2mmf~Np#?i6E;{rwSFth9#41DUPFF!o zu6@91#f!s(QE+sk6M?qVOtQ)y)0t2aa{My^;~wIDRDmXVezrDCax_csX`@cPBRF_m zowt{%TxKShPULT|Q}OW*$$enLbs3=2yHjFzcJQQk6w+gCY7;PUE9^)%o4}*X`~v!fS0QlfEv1jQ+}F&`VlF{SOZ5X z_29_6uR&fHhyC7`Rk@Lo7)+2J`7?2SpkQNlkFnUk4*4Dzx~>CYUu|t$)0z(sHP=4b zQ?KXLN56>0+icPB(A)?WBPv11n~k+p>RQYPWlSfZfDG!(ZQKvO6m_<49mdfyJeD`1 z?-zpHzdQ|X-5qd4V!7sM1%Q|QIV?w;n_W;l)xp?2d(SJ?rVM^A2srueu0}4SHb2^q zb5F*^I=&(If=pHwj*`4yN-?d9+ZfG`*YmvKHfOK=s2Ck3{*jP3cqc{SuPDH04=?Cy zmBMav&dG%5<&73(!>C+r7qEx=Sol1pRyK*wD5EZ%uPxu0t&Mmfav9%1k+-5&-TMU~ z^DT-k`3lzQ%Gn{SQWUcqO|tPA{JXZ47?L4eS3sfJY9MPMuCAy8G6>z{>XbIF}~ ztXerFY=7djI=j!B=g``xI$XN;sGab00e5x06*+VxEj&94SioG$9jehf5H$^h4=-we zDkWe9Zu^6kM!zAO^118%d(wzDYm|-=b2rM9^l?UH?38$^DtoL)m86-C_yRbxj@mCBAqevhAb-zrQP-FJ;%`W+=Gm|;i?R~2#fy2Hxb_Il$vs#FuW>VrGw6DgQu6oVVezVM4YgwPvOKE!(q610&noxxgrc_7c_<~) z=2?GwVUzo%cV~uV#3IPJV=aH;KAx0wL>!xW)K|9L$>;Gg$O5>zqm(XhuqGIk8 zIPfV0S<%_fw-fY$WJc_p<}-XG%%RJZLX z)g7`@ET>8k(aIG<)kYkQcJ0l%AT5{}?Fn(A$yuhNFf`S*a z{8eY`I{khMzq(v2$X>&yh#{yN*GkRMG;O*(GvD{CjPMbDLO$i-0((^+K^33Xn5gp> zJ%=v5O3x_k8bABWw#=Zt&1sby?Vzu&GS9|CL5kfw>VnQYGFl?R1(`+L-wPvl2sD$= zqN96q_8`>b0u^%O%@*8|_CPKvz;GPZd}wc}EaajkWOhdv>x}EtXH#iq*nW&bS^^cF z|FAMK+3P5lQ@^;Xyc2O;xb35d<3X^kZ{Fd~lP#CHxh>A1Aztr!;aT5(fRborM?zJI{tqKwHBmaz`D{V|#TaN#ksKWqYn}X>~!Uat?EE zgOYp7KVGmXQH{}__HUk1aXTTj*EkW--jNA$R9$>Uvpe`WG1+24=a}R~Z=k7YH}}9J zrWjh$oVyJr=k;uGo2m?z|dbE~QFZ7$G594zOGla!wiYK-0 zWYggUK!8BA;Mt`ju1O~@fVFvSu5Jtl(2)@+>M5#QSnf4``p=ysHrh2oeaeY4Y!j1X(XiQxq9hx%YdVNadGU=p~C%F&u&YHn?R zPI&hl(Q13Ba1WDh=SH?5%&{xcX<`#2Gorv9onXgnFqqt!izrym)Am_nN*+qV1N)mQ z&5zbk)Q)m0wuL5s4~6;=^*d?@l3qpUwnD`h76%S4etf=dZ(~b&zi)tQ6Bw~jJx<(bXpV6;{o1d~N^UMa`S+zULni zDHqPlhkTK394TyN)o=vgttlz0KPsDLkOZnjfla)0>VPcidm6>_Xw|cG;>*K|9 zjYgngsmGywyRS@KH-VbL?$g_5b7GRHp#edpMTHeveA;D*QdoaI@&GwGc#0UltrseE z6tkZgxmB-VJa4|PKs0twaYe~V+#`Bf3NOZavT%i^c0H{D716|*RD$X=;rSWGxWSo$ z$P~nXU|8W5ap!HBXwjfvc<;BWY-w+_uJAJgqS>0&o?cw2%G8=s_lE*JSe|wK=$P*A z0WFg0;pJqSE9DqS9OueRw(cy;Px%-}hjyxNL}y!rE)G&Xbx)A7On>Ul`^=ia6^$_kdsk2O~!S2l7J#}9e>)?bOpxDH}hBg&C>@}KP*B~X_@ zRm}QwF%cKmod8+9+naeP<|>&F6TBT&1vPt{`Tb%zlna<@6@=PO?f6*7RL6EHqKmq( z2UIc!r{2NJessn|8ET4iS6U7o-H26Y2e6|8B;#}>IXUR*6zI&nnvUwWw$bb;9`rXX z6IPMBUTJGRzW{4HT^tTv##y=Sh@S}$c~*C<#;<8{ZB_C$r_Yk)LdQ5>uR>ZQSi83Y z#=|8+?ecCu#jNQ?fXmJy?>rl!@Ie^S!N9Sw$46qcVXLm9`fWp?+_~I6aGxB5ggI>4 zXe;2O@siZ@p`S>T1Q)gL3AI#4+v~8(D5+YV-!DAQTi0%dB+COO67P%6++~+Z7Q^f1 zBMG4;;nulyT>QqwMYRspu!s)RLN#NFo!P*B_e~UJkcQ-$F6FYkhr*F@C`eq9n7V_~ z((^?7%>YLQb26vcv-zguOVj}Fr{lajkca)&M1O4>Y$l{5?R32noj#E(xLP*D;pNxj z5B|Pf>M&97x);t7b?NJPMOlxaR$3YKX1DM0GMDJQB3@pH_Uh*J`ud`6uKsu)Nvoa$ zW`?ByW*ioFM_rYFw9@*@qB@Y=e(!c(}`G~*yc^>JR)ri2(hcl}`G!So1jb^Ms| zTY%w@RcO!MRVy_b;VPBAXzZV<44OSmU`mq{@E`RrtPcD%k($3B}Q8c zjRX6=g-=gMyUtAToNns%GRD{;J&uiQH(bX3CQ`F5H`UTeA3|;AMhaq$wDf08nNfY^ zgLPI8aup=rqrw>(Ed~;>JaP5eO4e>ABS&0GB)+A$j42V=EtM9<1@^~X9 z92yc1)ip1YB^`!`?u#3oId>gWWqOFAvg6+sPiR5sa<&3ts&5OM$2QxT%jcg6V%;Nt z_0x+$1>PgC13DlhHnnw+YA7=1QmEKImxS9KPo)$(HIE}d_`)|0Gt?UKq;iGlGK@tk zQI$>YrrkdPgy2H=Adll(C3mLZlGRbBm5sCTtZMGATaQ`26{=X1?TKV2o*g0_8&}tU zWBnwck@@3`GQwyh4Y452B2z-w3rQR(Ib$oVc&1s#gBnMNT@Izv1%s9cbLx7V=TpZ> zv()$1-ph_azY?pc*VeJ(?tCvN+=w%<=~Vx#AeKBL(=-v6vr;@qB zRIIP(!vRMai;COSwFi~8^D?Pic*m{l9hEGhVut8YcZ7S zkWBL4eF8yJS)<`XRDNOjgg-?H=^ONkh5blla7m)?VPLenJC7v+64tOc!S z-se|sI@2A$+(s&8o{xLHPF6k^Tu^p9yfWJ=f*Kk`_)0?X(<=_uv|=x-j3b;VZ(?B) zFAsa}AVgf{w$!Lrq;0$;Tqbw0Q|(1X-gQy+Kc}!taIj`&9j~stsFBo3B@Q}ts#P3n zhq=D=xq}qWS}4gnX8Bstd?q43BjQxSe0aXp!l&kVrQ+Qyd*-Rvj*WW@D`0M=_H{}) zX1KeBj%0{Im2qd#RV<3ydC0NfO=RCK1WNb-|)tbFwh^%wq z=IxhMzlAeJ^QjtHRn)fm?Ml-v5`)<-SX7rHE8WL~^DLd?N*3g)Zzro~i9Dy$>qHbW|n)G>8- zROh-?3Q5Mey9&L6bJK_pQqB~^eiXSOx|#A156WEDUkTFPPl+N$A*+)|7uzUAl@xZY zvJ=%*7_+L5e7!q={KVdvwT0-Z>H}P)SY&0BAkA{Ze=!w@+G3#jXPZnfPUXWqX*La= z2H(zxA+%b7QZ;=EBd!`Tg38UIZiiGBc5=Zs7)Z3WEI&L`J$rV96{@ z>Nx47lQJ1ZxZAfi4S%GVa1Hb|cGVSoLp;~0)*uPTQbj0`hm`|2D_!n} z{Y*rseOPVvbfXOM9oL0coJ7WYy!Y!2^+oFw4q5p9OsSsBP$fpp#O-Biyq9z-L|C-0 zzkUlK^>RU~jyyj#IgDx}8SCwXR&L`{3lBf@E;hDzcN*06^_ILv9qg&7s z+>APr+Bq=sf-+53%r4)@=~ik#UkZePeSIYJCk+3250(B)KMeQz<6O$E>4>XRYWFY~ z5XzLjRo|f>7Z3W5qr&McoW+q%uJXRkD5?rI&f;hd(?(@uP*}lQ4d*gi#(Ypny`h(L z3&1xR;>S6}S{Zk;4I+ez#9Hx@DBcROkWbt9g`tcDLVFzGLNWTPk`8boQEQt3ec=kxTGyaGSlqNiq929p9KG(YxqAwvLCA-K4h9 z=QE2$0*Hjrm3twRJr?f{GvE6(KlRFtA=Ovh<;S!a_jBc&uux+SjcTpGU z3|FZ0P(7`C8P>^@F^mSj7Mbq3~B32`})R6d`_~lpS3(aNWj8|-f=216w9ymsT zGIGX6W0s4(u%7saS09GxQd`_`G60R4Qjak)%pdH9dj}lXx*wB#X8cNwwu>|u7$)3* z@Y6|$!I65$#1Gvd9ODyY7}yf9$HpJ2D_OS-zIk8w_WjOLHuTwi)5KAx_bY!>`H-U^rwtjL|IUR=a>aA zXi1&AgdKD7n#0hxkfKt#hmI;i1Q&B!wR8`rpyL|_z>R&e5Xsm@ws? zg`IuY7RMoaWDO?R83Ljt<_!4=3ZPrqcphx`Gh%0g)M3x5P?0Qwty_c*+(fV8w`YED z1jNjC-fDu3;kj%WFC2>J;(0rjF_;h1bWvu4Z`ja*#pMn;r3ni{a91u}x-fQEJ3N3K`$9Y}{NtM#HUqRn8aoHo_|=5BoDki&saCVms?y}$7Q;WUA^`G*a7K*&$N-4 zD^||A{*FOrgvTuC25+vhkCV6D@hMw1xq{+F?|5F<#l9Z?ank2c-Q}HCVK?Ks%!7GP zi&@q0LMQ)4aCq~?nxdpNhZDM4z7#Wr-aW%(2z zMCiS&q%p9V+OQR79dh!+dFknh9QFo@7p;cBn0r)ic8-!TPP%QLHF!}5X`>S)>bq&( z?XDO9VnlxGb)!KnuMXOL4N}p^ovR(Yl|tn8?@hT&Ob+B6hrD|#MHZIC#4*E#}8*m z1TxR|T}=@y45uXhA5(77Ug2gI*_wy-dWZ+fh84S$SGNjgc{^!3bnAb0Dt^0zf{+%A z=tPdTHfL3h@zGy`m&s%Hlmx%e{y16|R5!;)ek}k+bT-snex!gDb9Hgfmy%R`rxdgk zPh}@OXwNyUx!4@9ZWxOEPMIINZHCkj>fxHyK>Jcy@{(f@=gT=~+?W%mSE-Mp6~wA5 zf{rIQGprO<9S=U)vri7)tGd_hcV4c&0#VFHOFe$7W#s&R1G;p$ANpkR3cAZeaeQIx zE^(?A+M(0QcAC%KS>&jo*e?n|!X2v;WpZ=3@o*{hS6FQ-UXo=GZnba7V9$k?yY0kd@98TzLah=^sWSg%L@!}{CnCjbhwYWPUQ?G?GL5;*SKpE zL~=!rNYr(V2`b73i3M{qL3nf}yPM|4r{xY0w=#4%*~8j!*U{9N@ei_Fmz&B)9fVD~ zxP}YkiTP@FSK||RS$*Yny$IJ4KVzxvD~cowC2XJR7`rd^^9E;~9}HlJnV%{NBdNT^ zY5ff1r=x8tmNcT9A4cuTD3f=boukyJ)EMU-Q1$E1TRrvKm2fMtykTRvo)g0Sa&7A! zLMz($`HIw~$6MIlNhg)sp8JlC0EMAf@)DL@67~k@Q?D?!`=cv zyO8pkG}fQhJQlCyXl5>`P8Op5-CeTReyd8Y>9sSnC{3h}N&^8jtQ}T7iaG<56kvG` z=&o?b1>U`!D7x}2`2)g~H4gbIBcTMhc^A2o$CIkBoHXY)zp2eBURuH<7=a@c1=Pg~ zYihlO^Zwc;wV#e7aO6vAoUJ#$O+61DCCB;UPn&%E-QL5eI4law5%Y{)^VJXvXwwSm zTFa~#MwXvVKX!VozP$qC(gjX^>|w=ARjf0+#q4ysNxm%Gp}TS1#Jk=!sanj7H+|6^ zAMKS8P$FxT&JySp-(#b5jg4686uHy@Q)e+gvE&Q?GJ)t`DEF_@(w&H4GQ9w3q zBb;PxfTUprS>|cZ+IVhjcUU<|E^7c(NOWV2+gj*!JCsxik;07E9%1p?07qUNpeU6D z(IZx88)RWz<{XVg5rP5FdVRt2#0N)3Ha0(#My`D5ef5HZ&N|*vYx}48-W~LC?4x~J zEatH`aU(O+49CQTJePQa8hhWI`~t{jk!RcHOGVS4LJpr4oj$o7y{hr2`jkF3@#u0# zdf$i&jd92t>ER_uF9hD!C`TdcCpU!`cSd*O@TYAgj-kWV(yZk&3vOctAdil@478%G zw4LE}iGjL_+k)h?Rm768B?9MsHP#vG(V=hkxfcm;%40eo&V9~as();NEzT#K*K@f< zIO%I4_s2cq)I;A_6nbBDEhL^c%nth2%cMe05Vkh6QJaDcA(o+x@`>6xg4w>5aJdB2 zrmE)hI7R8Xw-+dX(1DKij}LPBy$(A_E?l3dco{#AYwm3ti(YvS?4Et^@?#CapEhvg zO!0zCO^toCyn!C_OjV8U`*o_}0e+g(0y>JeC_LZAFF%(gksvKZc$3bqU6%KOED6O| z`^PynA1^l}>>~%LP+}tQApJj*xl@EsX;&KYWSj{Vwq|r*iiLDN^qPH8qHL@#WM`ra@z?sv!OT_A)vY8Sw!@m z!$Q|0|6uZJ`-M)>X4&1#Ql_?wlfB}utA}1mmn->)rOo*tn-R0NsO00&@^iy8=|%g_?-jVFC&nl zhKpm^U=TTXPtFIQczfU6R+-2V(&eF))aWT6vAK*5#zFoqrelM3Evl2vPv2qc7wRyJ z>E!eN+uRE#W8FJsh}Ha7MMnCX)E$t)2a+-ngKV|(7Xd%5Itrc4>Ps6)3#LsW{O}DlPT8E8zQv7DpP!(NgffSC)F4T?;=a zqbQbt@(Mac*RR%X^;0>`?FcVd^3&AXrFurvXYUD2Om{_xODbsL%a*?{j1S@;qk_`<%1Rj`hCpUQ53pH7wS< z!&v3kjBG;r&k*&G)|iU*E7pHt1w8y9-s)Msy<(5?4z zaoF$3d`SyOW8X)b5*|9e`?Cr1lRVv%7eb^b1Vy?QrAG(@KZ}R$bGm_3jN{rqaZYP?Lw~8n9A3&Xg`T%c0yYUbY{j7EVhaA`AU2EPydS7w10~ z({!7|#JMm^h{iN0=8(sWp?qu2{KY31n*2O6b9jb2+84ghpOf=Gr)hpyK?mX6Ztj(& z9)FrSVki5Tg$e`0N1T8QPk`so5F}Q^&0Apa~W_QdokZEx+b;kFfS_UKX}fOnx^nu zNtKza#3bk1(dFs#Tz7^jdsRu(hmuow6)4>f_bPXKmpS$q#YpS(qjO3oc!gTA(VSfA znL*wZOcmDq`+q7m$=MQS2J^d8GhcTPx`Yrb%E8RdQmIMxG@?VFJ+wZsQPiIeV9GH*2VJ z_O^kP%+|^5;)LZVyiT3*^uDzC(3Ksu2cCY z2S*KAmW@%$Q&ijsqTd6U0(KLFaPfYIY}uF36J5uQTCI#sOS22kNVc1U-6!q~BW-Lk z-fBx@CeE>s?>kvOplx8{na-w__|$k#t1n8ws!!L6L)ZS=7L=fe;IyaitLac zSFbl=XS1136l61ayq|rJ)8n2^smS5GjPqSI?T;KL+|G=t)VSgypE44TU+iEqF3qfc zs=?}cb%Dh%cn)NGV;0k=%Iv&bFk8pA-{##>cz(CEuLpi9Z_cm(gnqo;&vVYA6g2u7 zj-?-R%Hz`g3uzrgG%O^2%gR4rI!L3t4A?W_Yt?FQp3R81~q=b!Y4wEGo>sw z$AWY0ii$rJW{rw#H(8W?#A-C)eJL(Z4QAf|TD<4$^O|L!gOX-ff=|uJ#!A!%e2Vv8 zJ!;d=WuBB5U~b~G@=0c`cA{y2^1H1Zk@?hD@$TPff0m8m>~R`hSz2wknR6>veXWi~ zsTg!7tNv`TOPpV5Ht?qGjF=dbw7+o~Y@srmf17;ADqf6c&|R~gCuwHTv12uhpk{8z zJuZ|J<7wPGOGOhsZq}(^ktx19eZ1sYb$gp~E@!}xcjAswsTv;>p7xJ%O&j2pgifEi zRB;GbF;$sNDAH>Cq&=+W)V7-F%-I=`JN~3XWmFHXY}(#IQ0mVaMBH9`PilROquP%DvJMu8JA+}OqJfmCW z@?deC=%rP^CR&E+>4l>rm0jxl6!o)rhlJG|HJ8R5xUBG zp6$m8%hn6li{i7{0h7-#TTxHG$02w3H8UA@dEY4K{9I8s+&6P)2od|;|53D4eNTF+ zh_v_1gZx6ei8_ugirHt)e3vX|Gvd=)5O*;WxA3i* zxmfAMNL|V^D(h@pSP}G#E1BTCborp&&U0Oz5$?@2zGD31SV>n!D?0_}ae>NjZp$Ht z<_iPoz$?Cj+tb$wPCThXb0K^%WrBF0e>UI{nU%(Cut>k zTx#$wa=EluJogF2CZ!-Q-VvKRy8N<(_w#6jJL{BZprlBy#^J${U=0_}fnd>WPYYRK0ZRUPMAJUbv5#wwo$=DM%jgi_QM{Af(2VMu#3%ODfM@-W7rA}j zJBmL>_DO|<;Zl2=UC_1UjJv7=BIqr`wzVRQ-cR_UKP;FOCkRXVYnQql9xqU&pS zP2y#{gaEI|Ue_-RR&Qzhtx@MA44d2>giLkiGs@*?35FDpZwZuknBBW1_W5AKEu$oS zUCq1e5zRDi9u2M{Jv44UvD!7IKzwa-s6I8$Bb-sJrM6h6R(@;V5IV@4#Z=|Q*DH*3 z3!RGsxnpa}oSwSbJZGbxYXp0Qf862pxypDj(rd^z^_h%MS$Iyw=c6i?dI8EDu8Ljn z?L_WnW*Jd;eZUy=v0pHu$|{~I1ABtDrRG#Fhp^#_be+J)#O&`0DN+w3-aYi2dgo#P zW!Ap0M%XVzrsU!9p2oH(*}3+bIf9cy{#o&EeKLfc^1yT^;*U?c3OKU{vrTdw7zxje zSA49mYLm$?8)~^IAZ{qoo+wEZ(4unKW4YM)-7!-$&~4Eg><9av5zThNL{uzAXqIpafY9O?{B`dzyba+ae5kT{=g-~ zUXf)$iQxrIv!xeTsE=3C4-nL!clu73emZw}kV3B&M|Cbb?a_;_Vs59q`wi5v)B)P8nY@9XM5au3-H4T)R)n7-;W7b2bRhD5UmdK}RvyTI2k@m{*$@?_wU)9zIc zB`*o_VT%a$36DaJLneXJqdQZ1mQ9M->bxVkmJHrW=`S*fyqhr(VXp5Nptu^Yf%9e+ zeC3gUD5DIS&U1uNO@&hHP`5Q<+uqvg%aJxIUL1=JK5W;Plf&!Eg%bC8pYFtwIq>ewk)!k;_?jG3Vl*k0Xr#KXHUQA@2RXk zqVZwnNBLo}b!Av|=J@!sQvS!ZiS5ql8p@ku+nCMmq<(BrNG(?_4AtGgr?+ht7!; za#J6~PuHT-g^?2pg7yo~+Oo24c0bxud{&N@$>dPua<3DU#9lR>GkiBWiM#RpnNjWz zbh3wJsl>QjlK6&J`M!LAs_nhqw4z&#uBTgXiTB((D+6?TNuIYT>1U3R$-=;5in7G!}+HO3ZI^NE)W7?f+CVdi1 zVGt%5grC*(4@@&-syb6ohnn1}o6&6D$FGy_{NX2-_oVpFw=LUse{OFmHl-0EOMml# zs(dZ;b^A_}`|Z;KLj3`v)v4ukh$01J{FP1ui4dA`F+90 z{R4mT)xOUPp`12HDYhRPR= zv@p*U&1+aKPrsnyHmzjP>%y^wBxAon%Q?5ngluyRDUZJBW}_duep=}C-48>{GeQ$Gr#6J1rz**6+8$@V z1ny^7%woJb&>c`TPuA(HR0VqX`l?!M)h|*JzIVI3{ho9$vZ&f=1|^vT%#WB#HLST zj=75K)SD-}a?(vP&qwR7LUmY3=nhf)>{6=(lW4|`{ql2EpLgQ~cpqnOxn%!Ei$)@z zZ@f_98fVp#xD1EE^czyi| zBTa(LKeu&PQF&416Sv1|pq^jQr&FlD6Lx%R{2AWz)N5*C-c^2}c&yvma({jWMa}2_ zf#I2+6NuM1yk>Mu#7%>5)!|9fm~`R>acmH|P2kx}bb9q_@hp36;t*lI@+ zLNwsW@v}50@qPzZjrw{_xSFT)qYC%bd(NL0p8ntoh=&q5T(iP7tW<@>_+!|}T z@#P2oq!Wx)jp4&|lHQL}_gN4&h9xkWsLeY}6uaI#f7omZX=ewV_epXcGRy#yUFcKM zyC{;ZSe*PvUK8SF`^F6^fjZ~`PzSLRWm4wZ_-Cg@MC_cD`LL0*6gx@XS{5A|gpKlS zI4z&l2lObqCShmxC?~m^0~>IvO>L2HbID>)dEwrP24X5!yL*Bi3@t$_H1<6e_SIj= zH*_c&D399>BahODP#I-k;t=|t-|^)xiuiWZwasdyBC?vdh3Y#r#ZsCi5>S((l+0)a;zA=YWdiqW#gm1uQLG9uYH(j0lucAuzh7?kR zk5+QeR-idu*CS$tsf7z_UKI?Tn%{F5o98}uAM2~($rCH=6ge@c<3qX*K`zE+#d#_O zj2z((x4QZ??MH0AU&S`ddsDQ5Ualc7{0&a?Tnv!bUUy~h{4KV?~=W3$naxfrw6 zkyE!oLj@fkb@fI%?N!2#@&-}AA%7}92#fLjO~yyB^;`iTW@){lnmctX2$%|sW9Wsm^KwKbOG^5aq$AD<4vE<>CZHK~%SXQ-WWS^xX zO*=qNZ9W(|OU5H`0)gFW5P-X7W}e*Fh&!$!gs>zDBk9uQ5gZ>T4DVl&WkvPOw;N7I z_R`x7q&>79DA~)psc-3C4Z=El-UiQ>4H9K-#`BT=^fp~7Pg|>!H%pqs4=0%$bM%7* zV?LR+mu-%J-3;a7$5xkVSd%u3llP%K4riJh0|hA2bN`n@6=Yf;8VLc3D${zG0CIXD z5d2M|8aYhgqHC-sVjoOF>ds`y8w@SSvapswFMNJ`A~}#)MpFu(d43YN@Kk0)Yd^}xevnShzX zkugE8ie5`M?wEKF%f_4X0u3;}U?45B@4O_hHn{F!7gfLRG2}$|+bLcq9S#S|_1oSx zMsP$3jw#t?CI*KB{VYAh&DU*wN%zuYk95B3I9lxE>y1>!lBRYQ*_tE?X3L^TK|Qie z^+?d$74>$pnFt^n`ExjKgZ9dvja5lQQkgUCA(H9XM_JYahUZq*w zr@|3D+2#0Ys~j7n^@UzJ91tiNOs=#3rxWk&5v_-g_@wpjsn>=@aQ zOYY44STtvzEV>X0gm*`Q0PmFJ5|7x%UlC9^VJ8dR@pnJn2?uNUHWIt)+2u%SLCjU^ zT`VXGn?`yT9u|_?Go4N=lLqMb8%{8Fs_!)i8ghD}qrItWvi$$Z^4C#BYn_tC&=d{; ziK479X!DWlfnDT+$XgD%xuwTjo}RCM(cu1@_La%TqQ>3cZVQ`;*J)9Jjv~>Ds0`c)#`>e#=RX~>xQso_w1?D{ z$LU+d*uvOEg>g5CHV-5xp0^CZFX44H$;~Ti0!8Vd>(8W9MMYeYFEG6r(|sOPmXmg} zCiv>@R&7F|`nG>H?dULM7;9e_Ytd^8Q|&gx<;c=F@Dz3F&1E|LN!6#~^*M^D>)*e= zrQ;~~jwFLmp(Yx;jt4(-;@-Xo_pQ%jIf56wX1Yj?mqApJv8PQJ6H02+#9T`c%qH?5 z0q+QL&o|Nfxu7pjWSgi)Yavh)aY3OpzAbtCA4a{k`gh@w?W9V3d@Y!zi8JJyzN56x z6APj{hgHSXP9A#T_dva?w~OOXCN4?IqcnmMN%@MWg}bStmJLDvh{+{~Rcsf{ zYMg>827A)$Zrkgb>>0_xu{$P?!F!a;c|MZn+XG5Tn4%6Z{?_cW2R(smPL61WJ4`K4 zinr6^9`IT!&{Gn}>f09)ln0cnN?#-;PRf|?sqOVC+PSd?f}`uFf@m%1<--4VLW(5% z04+@!{q3Hl2rP<-Uf?7_?)tQXSvM#6+db(zSkx8CRgSL@{EePYTvJB#tsDRPg^p-Z zbRAv$w-Ztw(ca3e_>m7al{<3JIe-!*U@Z-Rd14qvP_GxWot@o)pEBMdau3T6(HNUt`lSa19ot%;m z#ExVL4ZlB*C32nCFq zVAKSoCKxrrs0nIAHbNvAD!@aJdx-TgaSeVGqW&B4fFLdR3IgoArue_Slj!jED7sQq_F^@fKb44 z{jVevEW5$78!Ws1nhs&DiIhQrP(UbP*$tN6NNXd60zv_!CKxrrs0l_*q|FBq3J3)y z3>9Fg07C`Hk0C#X{220M$d4f@grpFX!i}w|kRL;S4EZtS$B-XGehm3Bt3si`7oP z`QqsI;VRSXGjjNF;Rz)9eV!7s!fpE1(w_w!D3qP*UhZV(TxzP7Y9HiY3!Gzr@cv*e z9h#1TPE7vpn~izvpO9D@fl0P@7WzGF3NQZ6OAtc0nfyUCAQ}iHoHs}jGB|)}Ks5eK z7tNle<#V&Wu17bA4GI44rPT(JWqYTn^YqyoU^G z1xg{c)4~Ilwg_)Fl4j;jkqOgl8A1Z2Z{rY}q8R^7w(tofLX{!{$n2G}Y z+rZ_rQWv-D7n3dFLc5&R`6@#EKO%g7#g!_AvtNsMl?Vd!0e3T(elrB1 zWi{X=t!8@vB?EYtC337L|CfETK~)aOp;)VO`cHeou@n+nBjx?>0BHDu>d22%N=JU( zdZfa(h|P5m@pwPlrNModhCF!XbaD-6jsCCqZkrJ=KRM!~DKQkg{sjM^W4|_H4}Jvq zqZ2z3{`E&k@P{4C@y*uU#3$Fs_zxF<=str@9H0|Th#C?E?H@jbb~)^AN&BZY_HAUq zNz0vI|2+MlUyiKdejh^`4gVY%YRHZl&eX9#_~ZO;FrR3@`ZG#+5*JkUbRbIn=pWI; zQ7TIIMf4X;9xdEWv492Q{ZzQOS8|`v?UjnhW zY?|{hqvYh+(VPa%pXaq@M|Pi-V*Kr51hO7{=6chZEq|U*4yrnK(}!zw%iz_bL1&9D m7u@>ic_r{Z|BnG*MWi!39@JOuSpv51WGW{$6m#Uy2mc2Xlo+G{ literal 0 HcmV?d00001 diff --git a/docs/public/screenshots/appbar-v6-small.png b/docs/public/screenshots/appbar-v6-small.png new file mode 100644 index 0000000000000000000000000000000000000000..bc47f6512d7ee6783fd88a544f3804514045b44c GIT binary patch literal 83054 zcmeFZXH-+$_C5@#GzC;_bOdSAl-@y6iZtocRXU+b?*UOzkRlyIuTlciYY2$admupQ z(rf4;fxx>x_vroId&c;GdOuzrBW(6uti9G;Yt8x0XU?6E&(#$v$Z5$52nZ;Ym7Zu3 z5L^@^ARxO(N(|h=A_mHUAB3)2ijN6O`{~z!U!GgOP_|N2Be)M-lM)aSUMINls|oO* zAk>!N;$PPU1nj_10)h*fg#VUi68)B56wAEu`Yx5B16v1%q-<%VI`sUMB(p02Y!=gv2k;Amf+|2^z`KO6y$SqvF3j$ zE-udhK!9IBfEQ@N>+0?3_R5Rb(UtXIo&4_SiKVN#i>c5-)|X0Tbl^CE;CJr`h)9_O|KsD{SGkajG9Hu+gcq;M z5&Xx;*Nddi&aD5{o;gtN+vTfsO2;alWd9Hm5K?acM;CwfsN_XRL{A1L6aS}(xusS5 zKgaW{PcnYFK-PKwyBz-%T@ffeqD+DaGg>egA@^% z0;(1+zqi;=y?0cSq=T`zAQ>*VT+gy}%rSQ5!|KJ6q z@4?5H#T8N!fW;gp?OY~($LhR3!X@s#muoSQZo*d=NUk$KUe5R1k~)`OIy>Fh@U~l| zu9zFe?)C7WM+*4pPX`OsXHN-+^WG3OJ2IsF5tSqjLz+uPFw8ZhksadG;b~|(k<>Pp{dMMO|E!2 z>shfGO`up|Y}=2x`_=zM#NZSOS*H5Np1k{$r(;ja(tfJ$(XHlPGjBBB6Dli_?ow8l z9VcWvrDIra#~VQ>{P`1&pwF#5tY_u2+9JX-@;2vFVhfEkq zC<{9#WReY-3fqaIlV!J=pr8|qA=A)^b6M&pF%canM}v3Ls~lENNRC2zCztkm!igv2 zmj1oL0xs6rV@>btxPbRfD;0PRb|>s)jTG2@Ka$xs?Y2KQs5#XZw|Yz4aW2M(^WV<^ z48zovm|;4nCS_V-d-~)HR9err8ia@a`zR!Xkc%%W?O@Un zEGg-8MH!+GH?GYRp}2+`4ndal-_E;LDy_qa#k126qWUOAYg7uK&QF;A2#e2ko@a46 z9=waN9VQlk^YdM`63EB~8m=K@%9r;n{-}iU2;CM+rE*T_-!>CT@o(rsblGN~3T)eh zMB{m%X`eZffMfO5T$5^N15|3z<9sB2O~~;N781TkCG?UWy|@YMlxZ6TB&zpvEB7*kW(8U`(Sbm&(bM61q=IOC1S%JYX=8CL`YToJB~PB9ioxmg*_l<`lNF zsov{zua9lM&Eyj>*RZgdP}oe4wP@um4Oipl#DZx(eBgpslh?<%CRWABPSCjlb1*TL z5}BKg5C#z?8>^ZukXLfM|N9&XN%uSJc1e*e;?UrCSAHK+W*$NcA-7ex&<&B5G|%)@ z8JhafG6yG%bon3mbGrt*L=Yu7U}`tF&RG+bAsQ=vz8~WR`if4Dw)lCCs=`*c-K*jr zzuzxtqbU0Gc6ODU6r2F{NHILi*WDx@dBMp;%~jlW`BRR^ zd-ESURwah8VUD`pov(^dz?;gf^V1DS(W$pr;v^+!9C?v8+rDTR>zElh$Nw;MpIaa5 zOyPKOI{N1Cl?b|b^LVX*q2geHO;0<@$9+TPa9y~+{+-8F36~M=Y&ETe*c^`{nMYv2*Z%{M%pP)r;MswKaNa^Xqxhq zpD<68+n@){6{2F%%@Zt6@Wb?xQWFlll_CdLX=O;xl~vhJ!42F=JDt;!ZNlDXntjQs zgAOLJ(rf^xz#dsHzY=H5{(Go|7gLD_G?MQvaVLCEhc)!HJi_NW;thuz2Vng-&69Hc zzgNTYdV}(2w3;qkCG3fv+`8TY!7$y5#BB<*Gz6X4pXYsI!?2=F83!y zGN6^#I{2nsHaGZ{e|Vbu(tMPOdbC)^EG)+2^z7psmdrG^H7|t2WKHb)sNDy>3*k?P zQ*Hp0*m8e#9=-a;7J41umSx-4&MyrKJ@x%rN&Q3-G~|X*tL=2Ctxil_X|m|sm;Jp> z2nfvxw=&esC?_OWQ$_s4Xe6rN>h>T#!&e7j_@mb}B7R@|tV4V$glz?5DlB`HCj^!Y z>Tr>>qizK=HLWW#H8GSu@~~ZA*?VtM*oa9^agKVD2x=-zHOE=`Z?TysC zl3;B+9P4+!UDj6JZY~COyM3WX&41yz*=g1yNOMpd={i$(eu1k)aIbI(>HPJg*xTW9 zqQ5=mz4Deb{0TlCUz!%gw1;<0YQJQ3rVkN){xF_f|Fa&ik%|d*=3u{U_$}UBHtu7V zI0kX2^d@AK^?SgWtMnv5Ek=vt_Yw+~D{K*C=}$}HZH1OU4FkJWm(N6{*d2)t<*lZU zA7(R*$zZ=L4HTVHY62n5hvy|Bf#j(V{7t+tFY(1|Lx#l%-Xvn*;)nNUe9x!j`cjgV z{JyOHpg}BE-t2*nx|i&!Gc>+y;M(~20(}207eOnGM`jMv(s|AdJ)#x1vp+1p)_=MD zxw_^BHk@|WeJcW7A~OG4!sGP{*UN(w0>5N;2o%w@Czw_Xo%Cd0z^<$;~(2 zczDAhqfi<0#m0=uRvD!qQgBYeyAnBxzEW7n6fk~YK9r&yIZDt|SFTaOHF$kk-GJ;* zn96*g=mL%+ZZ?XsT0vuXbMCsJDyY$846@A3n`;F{U88ILuHB5UC00Ku0B}M+Rpa{J z840@Q=a2A`08yz85&JodC4>GV%4%3mmd%;&O^$dUcYRkTW-yvE|8W;S@D(( zfF-Kc?H{Q*ES5`dTkZs3(C!{&ep;xNr@f36?laiAWLREceAY zdZS^UC6Q@TmuwxPXE6Bl=Vj@BwX^%n18OwcD(5Zl8q$mm7XPgIN1jXuvzc=_bR2Ds z51&LsXVTC@uKvKu_4Sc;WTAD$SwR}uj(p_C(wD-m^0R$+q>_&WOE*9J*Eg#>cZY`5 z&mIIt#1^P`7it%j5QI`vLBzGKx>9%{i~VUvE-T%Pl>xIdN5cWCB#5nqrTOEyx>Qa7I{;S^v3w0 z%$y9?tFT0ff1pb6xx;#iuow{=v@1)tVM{}HOUdAHScR_|cQBaJa=0;?NK8Rjk6b5p zlj3^+Fi4J`^y=J);P95ax!@q&Vda^#yxv;#lO1*t}OZ|1`mN%t~6HZ!Wsu0}U&508yqwZM1 zgge2{?t_q}pS|-v?VxYJfS;!bJF`^wDKZ8$Y!LwYmL!z>G=O=OSfRm*$MjW7MpSNQQ_`lC&!|L;HP3@Czj+Yuqc#buTK>6M6xzj8Wd?E*k_ZnNxv~d&JdWbdt*NI+2Aw}GYcND9*W6h!d~I`(8U)&J@@+j zXO+FE2MvpmYYRt{=m+xgBA;s8K4Eu_H7nv)Ik|tqr_GaZm-(wge!!+a z9TWsbusjWWA^**%?2CNI_EbIM({im!zznu>bYe9Fe6|J)6NWNW64NCh%dgRpe*Xv@ z5V88J^a?JQ=)S-4Xi&AK3G4m{}5Jvd_0J+mqtn%Tml;`H=27u2@ST-dqwDWO*i|O;`89 zmfWP-?-C-}?xx@-NSw?4sqK*a^zeLJsO1KgQ3>TUQTy?ihxO&IW9k!5xam#^=6az{ zdCY+T|IAd#CwJ~Bsf$0=0Jqd+axIc0>ymn^*sBP|ZE-R@tLsOs`8A%2^AD*q{wQl| z)FYhppR>HCGZ)nC@AsC@Ck4g-J+t+~BGPD5rCDV^4{pW%x~F`1Xj zACFvm>>cs}_vqdnCBR+IJza=i(4YI>MeU+i+0I3ByncG#c{nzkB!G7MIhZ9c6YjEf z5YZrev`U=~GYU;v?)9Gt>-0sUr`GtmwDUh%>s8UR{&8F%M1uz4FVH?p67=is5p*Ra z&SN)YI4c1kwd`v$A(T;0{G@7)st-J@N)t~6MDMKkm+~=>0pE$??#2Glk-I>Rs(NxV zI9TN}+8s?qzkw_}6z|Xe5r*vzHDasO%IhA^UJ~ZAyy9<@^s)Q4TCz3K@9qCVF(^kd z3MXAMdx2-a@!I8|dU6+djtqO=__x>Nc>uOQUx^VGgvkdHeWBmZ` zaHFKj54*3@MjY{&_kPhvwc~{0EuN&?qyFE0=k4UeN`SN6>7!*Raa(M+gNdt^LVDB; ze4^W*D!xEHH+X8~Sg$5v*?kXoGR1uoFi~KDOj>eWNZ1fC@jUoaKAhvrpT_pbeUma@ ztqF`2+dp220vw>!dz%*dMB9SwU!f$&Nh%(x`6fi>G#ZFv_r)bcT zL^ER4sN)a3bj1t4klAc~+Mf2wk1G@AtFY=z5kN~1)oi&}cx>Iy-iYJW9QM`huA+2D zmZ1r-e$&Kj6ALqS$L6sv&Z_@b!o9daVzJYRCpjG1J#J}U%JPhUoTij)%N?J4R~Xgo z?VO)zCsr)z$%UGXW;n+xG*%-rf-VpnREv<`;Jowo!zn-~1)c@xg5g1WZiXONdf5Qu zDo+cB$p)|8>n^RAKA&5U*J)CVyRO@}n491Iy%VoG5#daFW-oXp@EH8ua$eZ;a-EM5?u2|}cDJ|@FsGa8X+0N~}{5+U-FF!7+|Iz_fnI1s-Ozl_= zI=CevQPt~82==iLM;)3)I%{7piZ)?-gnmCNT8Zc;x1Ni%F+*V~a1cv-*2m!8!e)YW zA~vfHB>^y8DK^ER)=_%Q@~~ILhu)FP6j$9DeK*+HbDE;Gqp)KpU(IO9{U;afblBK- zsL4`_K}#p<>45G)@(qTd+Y06q5fRL-*bd3@9J0=~H`z(@! z%nUhOty-p_!|jNR=f9LRIbD`?8JM1a3r*PRIix={Rvw}jJD-0Q_34A@ZrrE;I|$Nu z?9M-K<}DT#mnJZQj~`Jo$yki0Y>m~zldNxz8Il>=CG;9BHh7Hn0EV`Na&CXIi(7Qo zt++R7H^!nTrO&-1J%uuqiYeg&obkb<`2EEcP)g6KzG?j9#G5YD3W9W^cCQbUYdq1W zh@n&4LQ`nSFOVZ-H$ILQDL0K4R?*XD&3{o4umlRTn$7k$sV{}M31W5K>YBv|Hxfj z{Av8MErj24twUvTL(Qxw2!FEli)-CyJ(8A!3cK1i`)QRwerh{hSEy5{HR^e3Ih!2- z-*#0u{!>T4R;&DaCROAMMzs>ufald8*MxZymN%9ZL|7&c_E!VhhkNzO$KgU*+8juJZk}Lcf?cWay zdO-#t-*};uNB@v~`6Z#{y~O%w9)EWyR|2FhzduXC|LOiW{U|pANS;I_y!|izUzKCl z0|4J^w!VPhXzI_jnC$>bwt`CQ?@oSejrl4!5Y3gxSjqlVA`D10bMjvPa}nZ!Mfl(H z{BL>wcY6MJdj4nJh)DnEd;Wj-JwcbV^#m@lUVRj_lmU$fpJ6#RuMbF~rRrC+KP_{_ zE_S6E@zvievjAf4O_yF_L(j}5!OzSc`M^s^b6iTO=#V=|f$B3C(;dEV$jZ%qKh@i)IMep|2&UTxaG@hgu zot*^Owuigqk)1}faPPE3!Yvn9A710uDbszwx0LI+g>uow@Ur6ZhDBVR1Q6D758- z$#1n&5=iVdoo5Nomb1{T7|WH7>YF*ma3Rd1CnY^%(lLh*tK&q{_7{E<{EaA>-vHR- zUVrsZr5xt3)@W0j-SLItVo#%^1sCsdtrwupsgub*3T zEbjP7CfRKtyACqG`iAem%eTz}xG-*LuAGjD+b~6?-|$230f0JM-@Qn`K|}|~8E!We zKu+r%jI>gnd?dS0w$a=a5RrW>jqH4KokZW*?dH3<76ljbC+j%fk{3e;bzOzVg)tzR z--?Y%P{f!G#1qmf+q^%D&#h^r}M1_IvX`);E1G3gjBWnnk zvW7t#@gyP%U zw}~7KYhx(rL^?^Bc$E#ZwyGU+EyI_&_En#lWe)gbp6$N=@pgNa3ENSW{N}r#4Eh*r zjkW*rY@*gFW5WXl?+$FG>UJGa&CIdUzbo)JsFMRq!JL`yl>{C&nANIpeT_6ze2sP7 zxAK0|&SOyTQ*AyOOhVOVgE0FxuF1;K(<#RcC6ZYG9xiD8QHCIfg+G(kS?X++bZrJ& ze0ntbsLOMWO)FC_s1yC+`UP?wPBYW{x+Pbg8EW6-x6!N{vggL>5UHm%XFI{>H@7kt z-qQ%?VPG(Npx@uSUMGpQDZg-kw(p+6R z$-2CS?^XASXPO5s6Af7JU7I4^yy2wcek0yc7}VW+J72l6W~_lRosv*h`>{Ul@A z-0+-;(lbIl%k*x)WG)_BTbB;6!Lr}I%1h`_^tj;T&~V<=liQt14|A^22t5b#=5s#x z%X-b`QZ(3qBmQ4CY0QO}8fN?xjopTxQU+@z@YX6UI`c4J2qGo5pDmX+O+R^d@mV(M z#o>ZL;w+K_e>tJ?R~k$yGJ26%nui_DeEYu-l`#OAV>WzTYea(VCn}VCmirv)StP!% z9x=#@?{&#B8wVNJyL^->nLzX=TJ>OzYEYHQiUdLDWX3LCY{RE1AbLi?XchS3562?? z1pWYW37REFC7&Q5!P}YrBba^mc(T` z{Es#byzLHbk{>!08QVy7d@8sBJ>fu&p~uV3k^WY-JU-Z^QsL@=<6T8p+4MsLX~m;W z+w~+J2xEQ3hz>J2pzAYh=s@$^KAVEh$r^`5eo@CbCh!#d3DssI!b;~$Z?b^OjLKeE zx<1xrDc3%kdtsZA+`}=8&k2=V-gMY68953T;sf|4_eGNKK@c>@)_;G?Q;#DbSO108 z9*@V&#_$^H4S{s3Y%~j>H>buu$WTLBYugP2Ht~KaCDYNwK}?f3o9FhFj5iztCo@Zd zUqkI4mYOs|;n#fn_(d?vpIP2RK}OZjEM4s<{A$8SJ`snB`0qmxX<%&^52zR=Y%~fZ54Cf0} zUGdxn)dPHK5)H(m+9Fne({Z-}3;ZZK!?vA zGChShVto}%DJB=ElIw{*3BW!D(zG>AM>z0PrlZHo)u7QBNz^R6Fi1z^@U{;czk2&% ziCeF;$JO+qSvq{zbkWlg{=~CxfwI0R02bJ>b2?%RlpScCCM>{SqG$Yvv-VA943KV< zB2de5d3k#5Q19w+VXWWYnm|V|8Z+zk8C%wDbx1-e2}gGFe^x>{4>PIT&W#TDhBBInhh4`LYHzs*K(TZ#wMG{T&{45>ooF8sWMM z1UiJwUFg)lo7NF4OHK`$I@WDBxZPge3Al44;{?-}SEGG_cka?ku6-Fq=DbO6TN%Ul ztkWSsD*sj=nGV zge_5xSt7d?PwfIYGcNGD-7rZBfMTxt8|9g;9#H7lDZ}q=BJGEGt0}!bHM|G2In6zw>ZhLIf0R7yK6Pt#C(6tppE**jtr&8J@lL!Uuj5IC zt|{PY7qdu6lrg3lagRbwkM7~Ra{2403_Y*oukm9vO8Untnf$$~NyMtaaSoHU1rl># zf_Qv~Yj?7cTEqUb;x1LXC*7uza4f)vbsm#ZFF|W(<4M$3Qf&*pmz|ha?wf^P@KouD zV({$oSX{Fn`ts2HX(#~|$C?tahLo_@t)`V^ID@-+`J3IPE zWc!{r?InqeIe99Ug78oPK?ll%ZQ6LYXIGOL0$) z_5;}NASn=BHkERe=K;45{iWe(DWd+6`7YP(R4$@~mW+HjMePms${<6=(bWO_kI> zs9Eb2k=n~~ZjEHr8Sz2Eq3;-`cKGxm#O_-|+@oXPwG)u2;}*9_39rt<=wP~Zhr?Q= z)X=~RfAi_Eq~xOQVba;pD>M&|Tfe@gn~@xZUz6V6;)QR{qSF|V2e$hdkHeJez3-ix zgu!&LZgzM7%YS(Rw9?MEmjdFKs*piO-R2H=+Q_cjHO0HhMjJ)5(Eh?x%=iY!hJ%(aT+>0tYLnm8V%4^qZ(H~8H6|ZnvxHYmZBgHGka>}YB*&7B9vB)8S(mF5 zQKYe=f2>@%b%8Ce!r(6skr_{3C;KXQc7Lh&sxxVcLF3Hl0-8j#vvY>5`qCY^Dztj{ zM0TPt2p~79A@U0qnNX zFTc07O@&cNYFvPc8jtl0i5D)qMpU`3-{jUe%;<9rx`Ox3s%El+iAgV>>B$JJ{(FHy#Z;IIuTjJj^D0MYWh0M_Y8>+t_%7i zuMDXxY$=1@H#yAsn-)Sq`emUy*2g3TGTVc$tR8d?@>=UcJBTe4cwW&RU;#w3PWs%-3gj>r|`FS+Jap z5MFOQR>T{TWjq~2oWCjj%?TC}vWP;&N>+-uB(zgqU}?j^d7LPk`0w3vKRBnin-_mEgqzDe3+z)9ag? z?nkNON2BuTW2pDq9J1CaZ8{V_QK811-2WW~WtLA<}D!gAhk& znS;qOUo;+LB3XuDCv`L<-kS7Xt~P-ksXJ-lLinEZhTSYqWw>h!N~ylVXWVJ3(}LZY ztadeNw(QXx^F#MOb!uLd2B~d8w4m+K$Sihf-|paCYvgEk!TE?s(F!L|q`*PetDP3Y zQpH?tD9My;R~bg~TKls449&c5S^V5$@4~5FepI3UzO&dXKsHo71I@YloQCzYZvD zF&{2y$hzN{KHP3J?r|$QhF_s*Ul%#b#pu~d(2u6jwK4iUn1fTCG$$$k6>T%?08zkS zhUVJwj8q+FZBk>oNnqeG)yECiQ7saL>BcRcuqBdEQ|PSP(9E;f7ImWfy?gX9K&`Zl zn=I6@W(Nxt78ZY-u8!j|_+}?yKKtMWM+U8**LhpiCd44^D9`Fn>-uPsA;Z?h<*Bc` z+`f7ZNlq_IF z?Pq6rC%bzxWZ?tW*#QbOehU#Y_{8JsaZnI(L>1Xb>9q&Vg=!9yVvzhx+$x}@!&T&{ zyD$t)lvlsMB6$n+^=AUa(PfdO#|BZ92YPNhvA5JJ;?6U+{5gmS97zF?Ae0xYqSE{F zgJ-KZkdT&n0fW~#6~w@oGEskX|47AsClC5mnb1JF$7F+DHjhpCD3L~TvyAn+TSumf z1&8wsJM*l2hymH2Zrx&rG)F%m!rj_m0As>z0!5!^Ew!+MV&k%JUzbWFU+5P9PfJaB~t`y_z|Yv zfdLBRB&NI-2c_-D>xPD|$ekQIZy2>VKB!$*!iAUx-z-!{9XwKey{Ep>oA`#pW=%M1 zCAXsvanW7&Ar+BEw8`XATkNgR;X48BL-r| z#MeKkxhyx=R*N<-_&TM2M4TAq>KA*OozZJbppPEZwJ*_(h!y3 z7(_>8fXiEHeJ0QFy#e$FswDNJMLHsC^F=8`>!(o(*0Yh`7q5Q!^|m5u@;V)UYqF-X z`O>FBKF;>1nFte1-C{nUN-$j%CHO^^TEN#TD`+Lk6JRl*N8ej(+>J`j{5HmlIl^!8 z9~sz#$1w(XT9bJU1SFqH0@a?Z1Ck4qPVoKo>iqDV+_WClH0a0O@w^MGgM8-ggOCQV z#CTE!PQ*QP4K)8s@NI~6gul;Q9YM3WDct(kGT+h_ZP)YE4f7f_{Ijqghh@3K$(3bKA{~#Has)TRtWvSJ%h7DJYK>U-lO_80iGz?j72GU`+I8R%C!EwKg zX8)&(1!Qb#67Gg>oG%!%QH&PVPGxvux5gDmUyqSxK}e{NuY@};PMVv2-*8ezv3FOU z_RchTE)bNVLZ?cS3C15LV%CvWkv8$DTz~F~3-o4S7+3X=at5L|+?lhmTX1}-O~mZ; zSPvn5^4bL@FnLmng~Mc3#ao6#3j2@-Go(BLVFB>&YJu{&IPKIOfSS!VbE#h7+tLn- z6C*-dzoRVlDPDs~4_^3)p;#wdu z_val)zXdxacJ@F_5F>Uonbjgqu`*3kg1h40Kg+KH6^4q+auD6oGX1eDL-pO(R5(lF z=YjH)K5n4Mxa5Nip=lV9MX@f_mhf`QO)UO7b6#UU5I*^&Mu{ zBcWn+*(&?~i?I9N7>G1Zb5h6{w&Dazqoa^jx2?N}H4>CpB0cNQ=+soO_K@a_9_ShK z(dB6eQw1CCf^YBnVo~}0rH_|Ix!eJuptX`v5zkRKZwtoDod;bM*K+#_&W>1%)1D<= z4}SDTnTc_#S?U&_4yPLHEg(#w?6`Zn*At*<`h~_P)%R$-Iz)IR)Y88()5&#uW*vHd z8vZ=I8i|NJuZp_r+i<$aa>TP7OuEWRgU8J{6}1Fx+br+Q$FL1oDVrDDc}2fwX%Er?htP`|9F)0@zEsl|Qe z0~+pr=><#7)s^el zqGZ`T-M2ZbRka1L6U`9O_70{zc1qTyhMhQL+ee}U&fD0m-qq^qsb#**DfCY4I)DQ@ z9d_mz52iDWlR3V&sC2>ZiYTnul>?qudr9I7RDJ8#y`d52k>$gvU zH-5U;t3f5Sgc;!7_ZwI1K8wjn^VQ~3>F4fTI-n@=Xu>Rh8H$WhjH2Vf9qB2We)I?% z#ZzzXD>-0+xbT*LCNjWRN*SVldLuKr+nU=rr68%Ot2G8bXSv}qMlvb1xpa003o&_q zMRf3*D$3LsI^~9Uk4aGvhf(R4tt94mg+y{zOdV+c6!}zw!R?Ac)kfXC^iRGBfKPWr zH|E#r9{CPy2GNQ*SPiJ}H^I%Fxy7Ku22*l$h z!2?%GmJYade>R(o4m{*oM|UvNFapKcmDS}786G8=SSB6F-J~l$ zsbf}!=_7`>C-7H3u5#%}E<(=NK?1)0Q#$ap{kWUm=!K4HxXuR2LtwX$)!6bf@tWM< zK!Ufmfx6vvA+t4%dK|KEVcL9Xo_{?3( z$%VQd<4Cqa7YDeN8=7{#IJf#G zP$pf}f@BJaTPc9e;eK|zs^2#AE7yT)DR(FGa(nMD$8SzlcD=WH$LcWbGBOf9Dp0jn zAwqJHXliyGGTQ1SpEy){>L4G0yLmkA=L9zY>;(69v_5Ii7F9!R(Z;Yw^wCdus}Fbs zr~i%ng-fDUVq1jZ%`XL{j=A4NQq`)|YGgjWi{We9A@a5xxs#z=EiVauGw(zylMxTI zI+LQtlEU?OBfH>ln>5D{`a}g@?pzhr>V4HwGt|8^Hk97{>Rz6@+===fsh5FO=)s}g z1dm-Kr(Y?*Cupovf$N*|CpdZ*gLIEbTPo*O`HIMyR~oZ^C(wmfgOg~oye^1GDNV5( z>f*F{vT2e8+bXHRkPIV*%@sxgor5!VelEqx%}9b!c%Uy&Fs1eDQ_+VoV~zfWsms5z zJ;erLMo?qvhVZ`2Up*WqmGBMj#-QQ#PA-YpdR){=4v3=nHi)kP6;naP=x8nKIYWfF zva2O1VQO3ZW0CE1i>FoG_vdz3)+8W(HgBu#@}W_U_p+oG(no=b5GMQBJ{PtUD~NIp1L>hu@z68S+5Ma6+Rg(>Xr|U0^TBAwYiFV_&P7_rhJidZrk~L#LySGxrBdY$n#HC z*m@px`$kz}RDvImnbRIafTm<*stEoP&C8JZh$j+>7>#?8vWAw%FN+vJu2}|^W=9i0 z?mOiFygBz3u$}5JVX-2GU(9MtJriba!F~LUG$JEtkwTMb)wJ_<$haaCcv!ejv*HQa z+Hhfxu`b+HDW}|qqSb5NW@IKAIzu&aDr7bS_jYv3%XtHi=h2j^u4SLR>)d})4{8dM z_%!>o0lc+8w@l|mx^@z4LA?9(WB-FiAlUiblb9t zb5XF*##A-t+pkKyZ2hluApviEJiZ}fBt5sUs#sLNxC|6K%5^7jvCcZoY*ud6s-b`2 zLU~5!X{xCo*NzlDy=*(fN+cT8`$06zBRff4n?1w9U7=W_)^T+BvxWk&nNvXv`ex!HHXyro ziq#W2r67uq_BYX4gu)IRP8w%K^r*6rN3)A*j&-FBm_$)hgfe$#t1LK85-G&*=UBhi zE(8u8MngPjM-B2-eOu>GcME!Q>uFeI^h;ACEiLYa$_-^_MMpmX4sb=LU+ZVN&2FFgyT*n%k z^g)w#Rw5Y|+v!R*_L-Jnb|eXdZjFi39nEtE$G5q75<-`Y+HN=I6(~e`NY&LrZ*!uJ zXJvyTc^MPxDfV$NZ^X*m*cE4A^E0v1wM+X~aaO|hK*Jk~G&Oz_<%27u4sY)(**40?<(@foaF8Y`L?X+l#pOf!?ey(bsHwEK7TJRBH{bCG3-dE0DDOdsB;Ccfr_V!-F@E&?MZG)#9 zF=AmODNlg!GbsJ2BZhbK@+e_&o`^ltj!uYFsIKIvi19i<-ryW;MjwcjIjl1II7frj zk`q^&S#V!tP&cSB8g^0AdXvJ=G~MW6D%lE;G6s$Hwkl6M^OS^l`#)ZU%DbzWIV$1Z z-MRUb_fAKj*9pY4-l7+Gx%b-dOhJ;&o#Zc6SadeCS$`>0BSXd^#U=o4NSRD}FFcSE zdJ3g!{gz(JY4Er^ksI_2pNW8)YuvdN#Zd+QQIj>2dtFM5K`N?h{jaDfZf%haG#b>a z>zC<`8s=|Ae*?Z3XTF2yg|ob3UXq2KCbVC~UIm4NN4SZdWH^R9=rn{?8p+;lhcjkr5) z8jL!_`4t-epm{guP!)CkI#CzT4q`yk#O(Ws2iEj9S3!|3cjK*U2jA0A!;3zF778YE z5Ey3Fr&dbPu?i@wXu%DwrB83~PTrGQCyA%@kPB>EVj%|h376Ijb~Qf*K4Ei%*z4MJ z+k9yzv!67QWlV$`n0Q8^CX-iceLkC4C-!}mm;jz`@cZ$jQu>cL+tIrTC* z12XN6jD~h0;|OL%v;T>4kBUS7b!o>ik6Z@7#A1~ZZ=>go?||kBXPo(t zDMuV!b7n&@LQaZo)1{EGxI*Oll4_gq^JuwG??&Yto=jn9M*#=kEoC?<9G5Y+H4%wT73UX`_Y*&9 z9JJdw8oEtSN78u=Q~=rfwy9NYRt^xAu*G!bb*D7PkBRoq-}AppEAw;6Swz`DR3(AO z#bgqsy(~=~ehbbvSrcluwHL|+j-xVW++0t3srA)u{(V1TmU@~5gMH<@7o`lWGD2R@ zpF{~WZZn~&!^SrB2d>oGJ9!Aa`dL|FxuJ({OBBh+l@s%PQxi4zOv~}(U9hc;8u2hJ zAw&jI$=su&WMFq}3k&i6^s%ZLPMN7+Wie!~YosW(^Ht4fvfTe4j zIY)lWiFueK@twqf7HbAAB6zG3GY={a5U!15Z1{e|-aP`qL}TsiPT&xD<=S&jp- zn`*>+h2l9tcr&k(*;(sI)6U+`I&9L>%lM6NqF-UxH`I=vBAei~Su@;bv!!Wma_|G0gB|CwyY{f!!*m&Y zRD&$;I2NdWs3N|;frKt})>>P*($t7ZA5LdEy`G%)3CCAyOEd9fqE;^cJj7XwI_dn% z47w%-fH$s=01TXUWI*J2Z1Y{TsPk8P3AfH>&bg+WU;Pdq4xQ1;2={SsZzEZCTiXLA zKQ6py>xJ;w9t`)m8Y`+g8|i}_RRwbFc)hk{9mD%LMf*{%rQ8h`>oz8!F{r1(&-A>E zyqxyPSO&=emy+jDJ6$pnw@l(t_b1H0!>`;jXrAZ~M{BYXy4_P(YBI;%9Pm99 zIP9p@NyA!oKPs6YW-;K=3!=^=@tIxe9Q&kc-X0c^?0zC|H!Ft`wM4nq!_B}MH5mT> zR5pNvEA@8=HRsb%wDZ1nJ{rXPZmQ(`$Euq9) zNZaCoyXZGgPS~}Tj4g=RGWhfjNZJm==s+NO+BDcfe|CE?{ zYh&X<4UnI=yCfpjQ-!OPiMCsb>53*d@kn-;-dw>=wgwBl91MO0*|p^rz)z)^OY9yd z9_(ww`E%LVYH;G``Grfjco+oL*qD5ODtlvK7!&<1efU7Hg5q@}5j)R=Y4Y~qcI07v4q258h*U_*|#Zq}@? zszQNwL&?3iM?ao7jD4ic+_)Ef`AAj6&a116gv7cP9mGm3LRl~~@N-r~&(O|?J*nff zv~Hi6X+o~YSDzHUhVs_u<~@{);#Z=Q!hS4kPZkwn1?e2+U(7K=Dw+?^VJ^CzIdt|G zR~Y+@6GyfT4DR~fR)*LEA-OxvJbY-V8Zi7S7ooiW@Y+%N&t7IgNN|p*_-q zXl3Fh(6lBc)gD!-dfF&C!46YkNf_u9$1lBZ%5%^8a4A7dJ6;ynbZv6?3gY$UD(@WU zP%DCu0gC}E$JUuP;C~CL~d&aVunm)=gzA zFjek-<7EQwxvm+VXc2Zh%-nsWjLfC@BL;PEKtv?e*(>O3PrZ{ewRdLOv0<{(snvUY z;tKjq5fXGn_TrsIKMc>_V`cSwPP)KOxmV!I;pw{{Z0&k@`P7KnJ$eK+Z?9({1Sq+s zSIs1zq*}DQY6b6^JeFU(CwjFWkyK&%p5Cjr6=4nW+aFDsu?C2yHdMzT#qqe^3JIxd zAA>iqkr6AV($CVvwT~YIsu|rm6SUg{E)?cXsN8aRi~d48uG@xzXuqA{l@HyBu65SZ zrwsd+7L&40qR*EgMtrk3LqOCi!`bD%C;aY<0FI|*pq=z8tiH-KB!oNv=$nclXffyG zw=-rCS6^Sa(c@3Xx5hp5!i((;Tlywf)OfS5+V5U-lltU1 zRFb3eL-?MI^Zo^^bG_Q@3%Zsbbn?CRZtX~@(LLB*W~q@Y4~4*KCr>sym(0il=b)s1 zew_&J>SoFgNFncVfkdxa74?#}EFKIm=mqi0owb?cfV3d~40_a+OJ^ zu5%`%*In@O!Q*>Qo0dLukPhE&hTwilfht#tseR(@B5^p6hte48K47S-Wmo?EL|?6c zppy5h{MoGeuyOtw6Z$4!sULy!!ri4ERn7Nom=@Pk{P&)4yD#!$G}606+C#R}&)HtF zN~D3iE({P1%Qo=BM(!6&ua=P&Skm|+7`R@>s3EC@E zNd4~m-DxE~6(WP>If2!X@lCQ5PwLOy-nEMJy2Zy+R$m7#v`Kz(?LD2-AL^ZNddL;R z|3vU&7HlTk`b+AnbXfamF-?k8PtJoe`MDp^rKXkV_ma*Gwa*fc;V(A-ZEH_rDl+?5r_PT)4w(`U%TN z5aAM!`1DxPN1UAIfrOgfR|nmdxo@klDJf}-wpG2d^J27ASnD}$&nC)h#mS7Kl_#5> zNvVr1Blkpyd~f<+;CJ>E`R*QhSJis|@v6G`p77(Uo4%$x7s+G2qjUKaFBQt}RR)E8 z&^>nf%3xad0 zFBpwn$hUp`qbTE*gn7Mkx_U`u?Yt+7_+zlvxPbqV(erd+(HG|oF7Hm2E25`>^;6|) zPWFMu2a#Vkq_QW0sHo7npXPtGvnc_2q0=w?VSdpw-VTu!PeFi+sca3VBCv4Xqo{kB zoLf)z$T{|E$of*7g*I=%z(?JBZ0dq9ckZ^ZO1DpKO0j48A~3vU!Rhw3w|8gJbZ4e0 zzraUM62TO`Vh)ZfpBivku`YRVG__a;dL|`2ZhqEqrRu$|(#_}Pn_~dUK1LRF#`43h zuH)sL$qr(1e8g*_;*I;!3BE8g;&N0CCiRMY#_f>)wXOc{f`!_;x^tn#+k`T+MY#ja zF=w(Q{f3>Ri!3Tm7T7q-Pb2-Y$d2y$p~`sgyVg;gtEWG@QT(?nWxAgC`uSSk3}l|p zP%&DmQ9PO5Q8SWn55sPPh?WLz=G``Q&UPmYcVE z<8y6aeW`C=RtMw_@c?=f)kJMtqtKN!qGJ0EV#Oo1Ng}2{G4~WzY%Q~(lH zBeKx*Qtub?;28Y@#)@;+br9!Wwns;9ual(_*;x2Ai$L(4b!gSGJCSIShDNaQuj<_c z>cZ(ev~AP5wPV!C2fDJx-n3x+#pznjxQ-{_>cK(TIjh9{JRyPMN z-`##f{8w2rmm#6>(F-XVN4Ye9?8p@*vGDLg_8xkKDwB4}%E-yC@`Uh7fVOz@!qdYM zXS?c6d2!4^*H?brc)o6+y;on@iACV+!K+WyDH<~)Yne;8OppF9Xa z#PK&Hr&a~t_Aex_k6*9Ky_S1XhLi5Q>F@(auOBwz}oO%-T0hKnMi7we(cc*RQGEfDB<$d~-g8WZ)_MVeB zY6GRDoy~bL+5OxTT4~)099!gk!Vk3+as&D615A6@_=;d754OmpyY1bT?8t~%8`A@9 z6X&THEFqRdSq{NCATQw;mb4AYOGIEI#6v=)b{1NM1)Jb zju8Mm{s`%*p66HOTO;r|Uh46`Fc*2C0NG& zi(xhj=!GP>8qzh$XEl4%*rZ8|aAO2$D*`KT1kQP)*gNcgLseM9(M}7yz1v{dS;S3?vJ)Q&e z4|XX8ugyqgDEWVMYdQB%0go`y7kdwv&iM%SD2^}9SJ387qNuqp8OJvj3eHBwy!n=; zpquom2s90*yi!OGQopsYd%rCDdUyExr46 zq%PPbJNIr!s!T^zk<7(ZIhQj{Q5+=zPq%)2uXf5#1%VTHg}JyR-U^+ReUc^id0CCl z2$U?b50P*Ru48W;d(6MNRD^gQ%m~?~eJMp9_l| zjBq5H$-KP{oqAItce5wxw4o|brf`&fk3~yV#evG}9T~e7PO7R{Ei7ViGM|^* zeV(6qxG7vie3i!X;?wbe7wzlpi~ve$u0ZWEI%PS!ciZ|w6Zyq*U*NLu10E`#N zeWGM6QT68Tp3~Mv1weV&$lRlE^*0OoW!g`jB@}qix3PH-XeMh5yD|G-;D3NiIx0)v z?>#d`rHn`-My@{**?HCIS8oGhOZKe|yzFKWMi1swJNkhAZR2g@%?T->TSAPeCr~fs zGVucFa2CVCo+f3d_2sEb&6?5+>23p~%pmttmnZ6oTzvP9gS|LK#n`W@e!w5AZX4sV z%Sl&=sWR`LZ0cUk07yh!SYWkwuM*B8hJEd#-<9zHvwwcB{_`D|67E(HMxbv$JOZ$E zjYoo~Ezf=&tAp>ZDRRSJ9fUjG+f`qiBqeAux)l|ra^d)fbAdsYT6k}bx9Fzh?9H>z zE_|jH!SNFpZhr$(Np{xF%KtTfRl{RGt5btJ!wH<=uRseYOPSSo&a|z~NdUI8OnRQ8 zx>MDRTyoB0H3#>+d&(e1+TrPe7U-sB)UE~QxHsbqJf#ew)c&_@ zlY@YKRg9Jn0q{40yC;3OLT1K!tF69?Kp^D}v+8qE_gn4wQ`Gn->?vIUqKQ^MZPpIz>e0Zc(;Lk~kxO+(W^V3k{ddKeL z$6&tQZIYZpmPT8~_r;bjj_g?%jsnO##hX|815i`{HQj`rm-l$Lr>bZt&}bowii8Q3 zLqr>RAX|3!OmN(r1GKPD=S#-Y6yT?9BE3J{ta_HN5T>QI4>X1F;6O#*{k<5#m}))j zJNYlKKVX-50%!uAvCEI2jht55ksdG-baU!!&vv!P#OiEsQLe97f`B(<(dwCuixFn> zE}2h$u|2PhzG2;d;NNEwXuD*>mM-sgb9>{v4%K4^Gj_|7hldOS@){2mTb6iuuYh8x ziB(A0>rh%t(win0m!47WiVwFZ`qMmK3|D!#EIIa|i`YEh!d$+j{MSq$um?U6051+- z@f2w+zIQ4L0`y1Ryhmqp>D$_rcnMT9J}x~%*x&}%oHf$*zveV*!RGkH4n0!mt@-7^ zv@L0#C*l#XH=ce>N?`fPOH2!v46LeF?sjs#uzz?3YX+6UXP~D8%i;QQq57o)&rzK>7X}&21 zrU&sJUiZjE`z4JZk0{ z*rM`&uknDL+uY!(T6)0#uCZ@T)FV^*W}fZikNWm4UvGPp+m8TSS?L)rF4(Y4OU7`r z`0c^d*FCE9uVHy-}krPLYuF+3{nSsb5L@-vX@ zWzc>5I%Tmlpg+^8~L#dWCr4M>WKahcv^x!v5WungH@wWYMI8UV}2&tS|u@1A;Qr^1pCR0xb zr4rV99f;=2Uhmf6k@>!evzdMkdwl53izIlV`IXm~lXjVBOy(U7q+m$^G6PY%-+TO` zV>jR>8hMZ+Vz+yOhp9qm;hJ+PeU=vuhAw-7Cne>eH}|z zUgrSSbWzpLxB7=jZIY#q%R{H{C5ijITf33*j5~^F`d(Kx2OW0d?&l@C^sJ*ZDYNcJ z?4HP4diQ}@FEkeDJYDigS;_6ySjL5cA{eQ3^?V&Z7j_rW^f~EYH>Y8eh?KE!A8r-7 zI?vy|sGCo7n|o1uSus<7Wg7N|sc5^p|7fv?n0T7pHI&2A!93j*n(fzEzBGPa1kV53 z*U+~o%Ol59D+q4^qyO~Gg*THk)Q|qu>C^XQ-G)!uXg~h)BL&9%>1S&=wd(DI=Pv0+ zRw;RzZxkgk+NCOJRAA<(Csm#jmEb|V^1Td2@6yju8a`Gw-TB-Om2f4)tU?2)c+&fa zDvd`>Keqn1ivN7z*VBT^fjU}=FkzKytjO&HZ*#7GZA^;+=-*#Y_BLySoPnPA;sNEz zz7VhKw2|sH`op8a-i79}Nv-kxIW;TaczQ;4Q_fzaq^GS{{220JokTojHT+TG)Jb6H zOPMlOcx#0nJiYlvevBHFw%BtmTkGR!1*XHnPNDbKDVl08XEIveBg#f{SI>|`_%{i~ zU!9Jc>j0ZoiPR9tvb5=y!+m`!qAlvs2_-@WR&XBQ&kP#D4rZzW#b$R`pC?8k4AP4V z!)~q;ePHNrr%@M50&F5)jt@E_lDhiZt9lk?JM*N`sP_Jj(MWJ@OmPHu`PEWC{4?vf-rL0+7S4oI}C8y!^>T`=b%x4Pg z)(6|_!YN63eF(X%J6E;FBh5R&2x7plP_H}~Oeo})iT4O~z@57~S}IE=UbCisopI6+ zAAhreIOq}TMsBx`try~9V&2JCpc>&aUAfXXo!jGla3*RDmGX5lv(j~j7|J=8hNE?c z8rLi+p5Zcdcdu)i!U~Wn?g*mEu}BJJ!j7&Qi%Ew2?y@DlxI;-pR=G=*9&vH6o!LYS zVmDgbK8;)(e4lS<##T@$6k*apjssaAc~+De`+*C1kIN1w=9N8%8>L_ggjX*bEEhs^ z-Kp_`H&7W%@!c#72tsY5WItgekq0SPuRCAgBiSrp@3{-toI*<`Kw1;g0xeH)VEFA- zjC&1V0dgcbTdBh7XvxFI0^V#UTd_n(2Md|YSxC!s>A`LBatqgoyITo@#tI6yeGN?b4>DI5wrCZMOdIpLnF zRj{YDO;2fEZnnPlREh}|=~41|Ce@Afw8~7tGc$B9FT1Q$v8Kk_Rr09P%Vs8%t?&{` z$4ysm-)7$gSh@6QiP6>a6$rU^!LRg*=4}x!1?nouFI(-?*~^uFgN;)}_mH&ijD^4` zO`^NkvLr-7v8qs#rv}-)Qro)Uy-ToC%Nl{ix=j-a&#p-vp-x``3-F*mZdIZ~#dGAE z2ltn7T$vx{b~!W*?;9+w)3-HF2wZIT&2%JS=X0{}Rch)LN4F=9e1IcXywD6TP> zrpxB*6tES2*-AcN1)Nl_=~Il;I)!~pEbyfaVT(cpJA(JOxF{^hVL!cr)HPFlqTpGc z{*;Ma5|MY!vYTN4+RwOOdZafu(?8gF>Pge9I-&6gA_>aQTj3+Fy}C)}HX-%st>jMu zS3@#Z#Z}Sx6}O^wEPNg1iAve9h(<@5is$TIr~5ZMeAz$ffYvRZ18do@iOJeyG52<| z0o&K+)&2W+uVUL?y;XBH=4X%7qHLleQn4(g4Rv57DrhS!&A-bU>8GS)eZ7Gj`nhFk zkzLXP?j4RZ4#h5>W%YG2Y`)aK$`mSaF0{1pj+8N>7}|BO=)Oq*!p5hhb&w#gldIF& zdccG39#Mk=+ZV%Y+Re(`djta^G^jPdb-)U5yFl1Hrlx|Q%#P>$JbNFXTnv)CbBHy( zj5$GGM{%8iN?Zqz-iN=Nf63YuhNNOpy2|0QM4IdMrtlQ=~oe=mfYi1KidGczD=w zHfbI{pHXxOOrOKhRhEOH2B&>?2`r}z1}?oOBGp@meyoSJu7t0o70U&$OT(lF5rvXm zN~Ic5mKUZ>C~j>b71aD`wBK8PKEiU?#3t~)#dM9IN1V^p8oTD2%GR9>FeR&gg8MN{SS%) zk?Z1C#`z+n(ZL84h<8Rz@+Ud#n$ZWa0GM0TV>1*2ys+UO+@?NMbdEE;i)Bisp*@_g zofW!)N#F-wmhiBq0VvWdB2RTSA^SPPJuU>34Pmvz+uc@z2WEy>I$`zKNIvwSqm4R7 z4cx3a2O37dnkMoYiqT89%<%V9!3Pl`m{}CJ;g4~oW8!xg^dxh_rs@jD!Y97lVVxRk zUQ3e1Qtg6gMm)mW+A&%?dFkujiHYDg+p}2HDXLG{Xd`Q4j`m&6rw=(4%FS05a_Fv= zjk_%^>Dt@6b0_a{o7k_HUkuv=t+zohnqCj!$m;>qVHPEaop82KzqF)3-7+#4q-RiS z9XdU;7fCPRAMqoL!K96I{*cW+D!)VN2%JBWp!GkOJ+>L#$0c z71@aj{;QcJh>d+^H%_cSJP60C`i)rLWi3mU(UO~A$t1-m06cU&%Y)gWX0(*;s5)qK z;MC;@`9rE0=3*8OWyj9x@i$NjAtF_h64w;}xF2f3edog2;TcNw>#Y*Odz#QLsmw&+ z#=w#3bqtcWHUpeBL<~3g+2C3f;&EIQFhg3f+pW)Y&XA2Jkl59Zy&{yO&Ai_|f+!KP~&GD+s7APcZ ziB)^Sw$KY?Kg|>_1{%wKEkL##1ICt1_~UUV5qYB%6J$}5B@BEGzfNrTP5&OVUbq2aZC6?o!I&oF1mvX)d`y@Rrd~b9BYlUmfPn_CX6Q{6GP{D zcp%?eUOYT}k2{-reBMjIMjPuWbuOH~9+>&@ejyh=fM7n~$)*0mSavj8oGl7zJbz+o zDQZErx`Wr;SF}@prM)}cS~ZKx6f)UpJioLIa~u3*1RWShKnn871T&Yy?-`<7rfItm zigSD%4;q@0W{sUQ0-fqjU8uau=`!~9DJ___DB5Ns&2`)!(JE3c@NuvfWh+B5D; zhSw1aCAvN{=JPrS91@~f{i%Mv)=`W3fhx_OTLc6;zOOHQsp($t^EXo#hM420Lg=jE zVHHWL9kknYXxN3YP)6L6t(bYg{{4XAt2beF@l$LVQxV%z9AN39-Ha)`u!`jrzTO?h zj(6Bwm!c}N)&j8DTx$?@+D%!`RGb$QkKW>}cpQ`6 z5(55IgM@s6zEaAv*6G-XP9_h~ie()#5e1O}~$leV?%8=!(_wZV(PF|C%XGDiL)Qqc2aOgu9^TP$7o(wc$2WrbcX+7YYf(A zjy|6FS)RQgVX|-Kon-K(fE_*7ypZ?xb#5;F1WgU499^aFb-HEAhmu!Gv&hz4549IJ zqv3|1wXmS^vq~Sv-}|dwaFq;dT+v2U_npK;Z9gv!iEO^wiNz>6^CeP=YWq=Z%3O@}kP@-7X~uFf+=D&5 z^Dv3$Shp=pLZBOIF>ki({0JvEM@L_$5`D|v*$5IQkY4{|QZkK=d7-zzB)VG+qPg6<}O)gTunK*OW9yVlt6#{?z!R` zdQpup%$ynlxsIc`cvyTB0PV5S(m(NbG{@avnNP`+O_XXW4@q+S^K|g#t}>0Xvz$ zc#y}1+1PM2_AKq&RBAEc%1Eb2_5^@fc~KC%?-6$Up2l+@0zC6WRNX1SDx&O|dTGc3 zZ?Ec>H!)2z9OH@gIExCa##J{yicf*dc~>BY!f#tgiOXp!peodcz1JwEuJL=t@*(N^ zpjl`pPCR-5^sQi?En$NilARo)3Ru|E5}HQb!CCY6+twf4I_wo)y{0^?TNK%;wy(S_ z3)rQBTZT|fdXH?vDE90f_JT`Rq029`J;(j!TwK`O&7vipo4;dd$kz@y`sMo8rA;8# z2np?F+jrbIC7dSW7&Ld)J!Eq=SLNRE;b_}#I>!($z(TIR>V5o2vkdWmzQb@faff*C z=%>O>)6zLdbYV}0)}^;-hs4jvV%NHXTM856@06su*}RTQxAqUKJJ>$X_pKTlakI z^0#hnSL5MCS`19&V!FFEc;QhD`jd7AhH!p3dI*KomD=UFVJfHWB!$g|?b!|>8*VhP zCA^MYOe$9c-DpmEAMS-P*;MJg)uIzo-}3E6vNjGea7TD>iF1ZlZEB4e^1tJNbN|MB zTL3PP2Q{MloVrn-@(_U&XtFxkx!Ij_Im!l{D(tV3Y<13;T;kfR#kbnny6Oj|D)8>N zG=M&=l~?Cub?P=M-xve%x|bMz&=Xq4W3Apo-0Li4XiZS(x7J(F$~skZYnNIE$@|t z&PtRGu2PQY;0lU(7ZacBDF#_c0B;>x3`$%^_u}RUJ|z!4&GuMM=j5Cxx8))w?Bijx zl%knwL_I#^Z1t4@YLU|7x5q4H?gB7q(^Dtzta_ibCStjgE2Y(T{KN^`qs(6s9#SqLVEv z?{{;Tj5K_3ymNr{QproAhh$Z`c|bO(q$}#EC3fH2+so|bsv)R)%0Q84wPy51SETc$ zK?^i`Rgo9DHCg|JLsi9O|od4}Dfov^X z;$u_R7h!ciXr*F0oI-AcN1`o?v2>xG&0hF1o$MIBBJ$W=Jss_m%|zy9{U}v&r-;{K zJ;@BZCSrSMZa_%EEN&oH_+-DFX$lZp80Sn~Uf@CM$8pSI6k{r^0aaU2@(m#_i>q~$ z3@f9d7F@Q{R0Q@Pq-&*jKY&&Z(znpoDC3s*$vA|~MJ-9^Um>h26B8;_vB>-;GnFk) zW{5ew9 zPzne+n6T)wh(#CHI(DZdOZe7{*jWm8CQ>fFe#V(9FcJmwx{Ek}hpxTYH}`R=GpXFh zeqemxxGzv6MU;fOTx+F~$1omf2ZD zDQ}m0PZ6P^Re}<4MI4>CXDZmkp^hG9sOTQpRMC66?(vrU5k$=W*)MK2ONd&xMX!L8 z@HT?=%70356bV=aUK+&hcqJ@>cb8 zv^0(+aP9*y^o7J*%}VZkxITRu1SMhLifaTlp`karqQ>Ost_`k*TL*{ zZs~_0#zSw`bKM@beQY@bPaB!Y!TYHjJ?{Nz)k<1U*DvP0pi+2zu6^%mUNbt{E$Q(r zZ*ey=?CW#$XfKTW@^CUn)wP607b48xQ~{%vixsbOoH5=C`mqM`nJZ>@Ba;&D`_}Uw zyD9K-?bs*Kkm>YSUKhQsHl-28Wv+3XA2P-CT=#GK7Tij(`RX=3H^HPdy*zakN4vb@s(V?MgtQ9ZEl*L8@--uK zz*{`*bbvj-wY=I;-x7Y_zcy;2Wb#$p?PQN@1C#E`_y?xNO-JN>xPPh<)Gk1cEb}$U z%wiG?nJK(VC!33QQP<<7l;xQdcy>dft+50zS-1yebL9tXGS#oC7zN+82Z#p?(w1I` zsl4uB9SI%ZN7h!Q#l&oWZc;T`ryMPiQpi>Ie;0VyoXzu?TmOsL$HN}?JXTV|z+i6o z;X3Nm8cb%Qg?m6;Qtj3uyo|gNy#GTzPQfi7OS>=Y+~VTWpF)%Ax3Kg5ZnNAi?=oa{ zkUcnXMVB)cHpIRJA`kV{#lC$D!UzNk8hl&}*|FhRb99q?Ov9To3h-hk21nZBIa`?5}VYpAAE zQBJ5h_h3z$MuXJwhYbz{GAtl%mcQ&%zKKT2Yw?hsGhbXn?W^+AZOJRK3rR&^a*Y=U0A5 zm}hM~OO98?!daZ-Koq-FYxQmjsXZ56-{F#IAX-A5n66)Xx7@Wj(1)K{ClBNA)Q~vuG+N`c?hfPxs2#Qn za3ibwLu1fT8Ws8Gv_mh`=T$}KYb{g9IKywqRpi00^1vcWS{7Grv#5B>=~g-dP`~^` zAByI`rOuNeuHD0|0#eM9T4@E2Z#$YL%QV8Hv!=WNv}U!D6H*?cu{=CadUro6o+pYaxBTCQ%% zSlPP1lRPVEBn}=<%pJaqsaB-6lxN+63aU>HK79@LJTek~It-M*Lh8*J_i6zx)hyKF z+oJ-9hOTEW)6mafiGPq=;aITMq^aiok_YiT2B*GC@YfC86 zm9XpDM;1~KGIi3dGbuKTIXCtVg`(+8{$=&(a5`$H@(#sdXm>!-`Xmd7KwX(`Jd{2{ zis0<dF;PS^QA?_8fd~Zy^llg9iA5_)io-><|5GADB zTGOeZSlZuFqUto_9~pWorm`=HNE<0t7G%#vc>eZ8!V{geBl> zkhg0jsGIJCn_lw&^a9A0gc5A$)Y`#TIyC@V1@VzKtKLQ;4!Cs($flsao{S4ORG&k?Fd{w7^rG!41@!3Zs(} zwR2I03sMTjTiXFriRkib*zqjIAMfpQBp1SI-*3e~Uy^0X zff}Y3s=E)tJiz5zGpXCw_wy?Gw6T>mzhKbnJZjh)qD!gns5{BWq%t$A?m*isRBQS+i{k!LeIQ13o*&Z~91clb=vOC+VB-XdQYL=?ADdN4f zn5$a0*p9B6C(*HpTAUp6&#Refbub|G9>qQlt~2|w zg{@+fCXPkM-;d!k6Zsi-u^%$o;vT&m;;+PKt1#rn5Yd-O~!+<-3v7;f#dgOBbfobn^JM(-ggNB?&@Yx z*fx%bJ-=p>cC0h9u*e^3UH~?jQ^Ki~2>PBgzbheREucnR$^5b;+;5OVvkQNdeS1kg zE-rfMEDbs>!4WsUW6muEng96cqKEmQgt^9r&VIBU)$Y0Zh_@{wmaTfEe;&QDoWhf+ z%u#Er`v^kqiqfWpk3q36j^n$DQA&teaW8n8$8yxxE-*caj^A*3w7=_T#nd5wQILJPwehN}Pmr5)346!VsH(r!Ab|c~S0*9JeZ|^~9wN+< z*-Ew!&-Tj=+kCm%tW&`{48g>p_l>(M4Xy% z|3&gNT;y{JN)V@uNaolB07z07KHxf)gw%IRE^qDAE^b*lAR-$Hd#Kbd73Ps4E1u`E zd_)L(qqvio9y#CLk2i5_TaT;mb%8X$YA}V>aaRKP&6lo^bjw8xN&A8zRF5 zhHs~nzsz74DGf7kpprr(1?F#MtY9TT2a0lthd#{V7P?uG`g6%KK};^57SqAWPaLtOu4$L-{^Ys$8YFE)5hGhDg;ehE zpFv!XW63iKl-9~Bs8~1420~ureKk%FD0hXym8T(b^_93JR1(N*!T>wBl{V~+H*oC> z7}MlSj5L4M3ZgGZg|<6c35wJ1Cu(Zq6Q>-#b}s<%cR=~r?xW=_UyfFYvq+`~hEnZf zS&9RT$Qh30Hsdu6hT5BEY%*RHP`@&hq7NC=m^n|-(+}OU({_M`=}ik_P^kjk=O0ta zZeBg|Z2}@9k*qvYg(t#EV>)prJa#1ih9j4B$^&Uult{UFTenBsn24Y+Ey!1ky`Qc9XHb`kOFgw25Kx!JXqW~!p9?!-(etqkv2g$?yE~@$xl>_Whftex=etsPS z_q4AjeuImuzH=i6^>FGh6ve6$@UX4bnHB3nq=Kt_tmPRAM-gab?-|>JM)M07(;vK| z(h^B6hOpJ5^1Fx<8-B~Vf$P?@@=aX4tPP)wU;73pz3XmcT^Er&}BNr0n= zkmcw+OgVbU4`Z2&oIOJcMq2tWjJQl^(0dQ{e_oG!kX8=Sjuh{|9t>ov6k}zaYzecd z`HJbKIZX&iB(45O-@}s&d@%i{Sp^1uR4O&Vy7YFtnD!@4YK5piEP z&c#_el6i}JS31EE->4y65%Tjd4E=oWeQp7tp33o5VJ-(zL=9S&htrQjgr{u;m4N4a z`mLj-5ly^}TIrYOn&JnZ+62cZ%D;#8*;cNyEI^Pk`Qvd;0@v4BMR|BN;r5r;96aa z-nAN)B_79jPbovYFVO&`DrubNv;N^hgtU=?$NQRsC}%%_94xG5zTUI`<6^k71IPPu%;IzD1%(z)&}s-b>J~QQ3&VzJE45q@i|@-rqXyI;{1N z{ok;LkejTH(X(-nRgWV?K0ewxemyj9a038lW;=OXza{0bX!`(3%I~}QeAVmmuH;A} z=lZOtO@A>cTQ0m?E81eP-#?n=*n3BC9l3k!6As^7>n%q(#p-Nb$a91$;D>iB&%Rk(r_!=gEb6t^Grx)wpDEr ztxM}dGm}xR5MIWwH4!o#`D9VrewcWPQvk^Laf0ZfZW}ov`W3>XJ#cmnf$P{xx9s4R zwf?lA28i26*fd#iT*h;`1u0-jt7$XeL6Nh3uY~C|@L4N4GeTe9vIy+fbPITWt;Qx0 z>(`&+qn%$RWE>if`u56%N9^)=U^w2d{+l`6A?`i&sm~a~)^|QhR8x7l-^(7CZ!f z3@>DN3za}DsMh+K9{x#EIL)5?rt}3&Ffkq&;0tp}JxXarhE$|f@_Ay+TFKi&M$2~} z0`Qr}j{Z#O{5*lzt^=^D==!5Tc>pCcxy_uL9y}N*_VjQmlF+Gy@Kagn+CP*CL@N!r zV^MFV?U={&`Yx04YQ_%kA z4?2yVfaH)PJ9I(HtjsN@U^~p(&WN*dRi0#HlP}tXeumPyza!#q1mL=uW`i7zsx>^-$`ko#BLA6D*3|}y zGq-=t9r*oQzdkrIGbdcUD)?s#zb@YfE}seV`-xNkn%egDh>J{2+WhYlem&x!XFUU4 zepy@npGW`ax?1Y}`y%l-wg0~S^8Wqot5%kOP4NG6-f9g7qQw&b4EmqRJUJ;ms+0Zg zC$jQW-oO5TLsWS5{WIgFKU4mBU5AIu^R2;L^k0|XAK>!5^(Ft>?*;$$;q4wa`JPjL z8h>5B1zi3x(f2DW^XHj*0Hnd`h2rslmheCF{Qz8Mi2J|Rm?7@}lT*pq-3(>IP$t_F zVCZg!srz>3{%N4l?+qKh$42X&W zQT^LZVnmgUsFD#?{*nO0R5DB@!&Lr~03(~jNS^&GWyi=>GE&2Tuq8GwhN)zjN`|Rq zm`VoEghA{1H}!}?>tV2{{*{zukk%L+!GCky7*Qo7s$@i!za+pgl?+qKFqOX~z@XDJ z==6Um3&M=3k`Yz@KZq*d-jpQ$(+l8V8g(0;VJH@cVqqwjpAv{*RG~4d(EdgJ`Cq5# zZ-YKRAhmzTjvb%Q(~G1^9_8*9o>llatibho=gx$<9XRV~hA0?##ZSw?fE5_90s~fHzzV+&0z(uGQ81z= zM%2WJnix^jKY-G&)4+(Dep?2HC>Ww(1dxmX^0%A75CuaN%#36#BN@v`#xjzzzY#@- zC>Ww(gbIvMfe|V&{9}fH%9;{(h=L&sMgaMb)6De$BT+OA*4Q1a*?WFplbk2tn~*1YC*rGj zG~U}Qq2~7BwkKybds^euv>)dmw`h;}TtNHEeI>z^S#tufT=<fyE5v;BWeZoj9X-yoHJQRA;6ZZR`!i!Mt1w~i4$&%-5e|042V9T$IQo3E;k zzMsH~{v{!Xkbg@rf+6JJ`-`!Y|1|GqsEa?ElA$gb>VjeT{=;2jco%;T1H-%cZ95q* z+#lP?@RR@8PKLT*s0&8K_aEKB2&;c13JmPx4;P$)UHpM<7y!{9Y>c5U80vxnb^gb8 zGVtWzhk*gw|FNBn%*Y?x$;gcSv7HQc!B7{BMCvcyz{v6a$^SBPe1C9CMvm_fPRYpe z{lO_2lz`u94Tid4sEhv(Gl(K6)Jd#s``@o(Rm|Bz^WdEO-?TMD zfVTG4DekW!P63>=%UIn%vBA&5Y|aRKURQ|!`^B`60ovO8nk#=v==!dm38!eJhZ%go+x%LFj%V;o-FLa7Z{nGE6rg4M*bY$#aAuiU$st zSs$cGY6@1|7NE|q5h?}Uc8$C=A(^6!IU0VMn@zY%_X1n$}hWln^djY5IV=D@R5H|y%ryaJq%QDxvH1dTiv zI|e*j-cBL#vu-|MBAq8kLwdiDL{D#D4gw~)Qp>J?8TeVlJ<4G#z9CyWTH8M@4{gAQ z^x#9DOa9zyOQ{Vt5PDEh5D~l~^Xm#%;FeA+N?$y1t0^3fM&HoxdaN|1D>v!asWF6%dnftQW%xT2}2QFPhn!T$l%>VKI4 literal 0 HcmV?d00001 diff --git a/docs/src/data/componentDocs6x.json b/docs/src/data/componentDocs6x.json index af5d9b62c2..7d93774bab 100644 --- a/docs/src/data/componentDocs6x.json +++ b/docs/src/data/componentDocs6x.json @@ -95,10 +95,10 @@ "Appbar/Appbar": { "filepath": "Appbar/Appbar.tsx", "title": "Appbar", - "description": "A Material Design app bar for displaying a page headline, navigation, and\ncontextual actions. Appbar supports small, medium flexible, large flexible,\nand search variants. Its surface color automatically changes when content\nhas scrolled, and it accounts for safe-area insets.\n\n## Variants\n\n| `variant` | Purpose |\n| --- | --- |\n| `small` | A compact 64dp app bar with a one-line headline. |\n| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. |\n| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. |\n| `search` | An app bar containing a centered Paper `Searchbar`. |\n\n## Main props\n\n| Prop | Description |\n| --- | --- |\n| `headline` | Required accessible headline for every non-search variant. |\n| `subtitle` | Optional supporting text for written headlines. |\n| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. |\n| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. |\n| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. |\n| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. |\n| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. |\n| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. |\n| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. |\n| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. |\n| `statusBarHeight` | Overrides only the automatic top inset. |\n| `contentStyle` | Styles the headline and subtitle area. |\n| `style` | Styles the app bar and can override its background color. |\n\n## Migrating from the compound API\n\n`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and\n`Appbar.BackAction` have been removed. Render one `Appbar` and provide its\nheadline, leading button, and trailing actions as props. The former\n`mode=\"medium\"` and `mode=\"large\"` values are now\n`variant=\"medium-flexible\"` and `variant=\"large-flexible\"`; centered\ncontent uses `headlineAlignment=\"center\"`.\n\n## Usage\n\n### Small app bar\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n {} }}\n trailingActions={[\n {\n key: 'search',\n icon: 'magnify',\n 'aria-label': 'Search',\n onPress: () => {},\n },\n {\n key: 'more',\n icon: 'dots-vertical',\n 'aria-label': 'More options',\n onPress: () => {},\n },\n ]}\n />\n);\n\nexport default MyComponent;\n```\n\n### Decorated action\n```js\nimport { Appbar, Tooltip } from 'react-native-paper';\n\n {},\n decorate: (button) => (\n {button}\n ),\n },\n ]}\n/>\n```\n\n### Flexible app bar\n```js\n\n```\n\n### Search app bar\n```js\n {} }}\n searchBar={{\n placeholder: 'Search messages',\n value: query,\n onChangeText: setQuery,\n }}\n/>\n```\n\n@extends View props https://reactnative.dev/docs/view#props", + "description": "A Material Design app bar for displaying a page headline, navigation, and\ncontextual actions. Appbar supports small, medium flexible, large flexible,\nand search variants. Its surface color automatically changes when content\nhas scrolled, and it accounts for safe-area insets.\n\n## Usage\n\n### Small app bar\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n {} }}\n trailingActions={[\n {\n key: 'search',\n icon: 'magnify',\n 'aria-label': 'Search',\n onPress: () => {},\n },\n {\n key: 'more',\n icon: 'dots-vertical',\n 'aria-label': 'More options',\n onPress: () => {},\n },\n ]}\n />\n);\n\nexport default MyComponent;\n```\n\n### Decorated action\n```js\nimport { Appbar, Tooltip } from 'react-native-paper';\n\n {},\n decorate: (button) => (\n {button}\n ),\n },\n ]}\n/>\n```\n\n### Flexible app bar\n```js\n\n```\n\n### Search app bar\n```js\n {} }}\n searchBar={{\n placeholder: 'Search messages',\n value: query,\n onChangeText: setQuery,\n }}\n/>\n```\n\n## Variants\n\n| `variant` | Purpose |\n| --- | --- |\n| `small` | A compact 64dp app bar with a one-line headline. |\n| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. |\n| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. |\n| `search` | An app bar containing a centered Paper `Searchbar`. |\n\n## Main props\n\n| Prop | Description |\n| --- | --- |\n| `headline` | Required accessible headline for every non-search variant. |\n| `subtitle` | Optional supporting text for written headlines. |\n| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. |\n| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. |\n| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. |\n| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. |\n| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. |\n| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. |\n| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. |\n| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. |\n| `statusBarHeight` | Overrides only the automatic top inset. |\n| `contentStyle` | Styles the headline and subtitle area. |\n| `style` | Styles the app bar and can override its background color. |\n\n## Migrating from the compound API\n\n`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and\n`Appbar.BackAction` have been removed. Render one `Appbar` and provide its\nheadline, leading button, and trailing actions as props. The former\n`mode=\"medium\"` and `mode=\"large\"` values are now\n`variant=\"medium-flexible\"` and `variant=\"large-flexible\"`; centered\ncontent uses `headlineAlignment=\"center\"`.\n\n@extends View props https://reactnative.dev/docs/view#props", "link": "appbar", "data": { - "description": "A Material Design app bar for displaying a page headline, navigation, and\ncontextual actions. Appbar supports small, medium flexible, large flexible,\nand search variants. Its surface color automatically changes when content\nhas scrolled, and it accounts for safe-area insets.\n\n## Variants\n\n| `variant` | Purpose |\n| --- | --- |\n| `small` | A compact 64dp app bar with a one-line headline. |\n| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. |\n| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. |\n| `search` | An app bar containing a centered Paper `Searchbar`. |\n\n## Main props\n\n| Prop | Description |\n| --- | --- |\n| `headline` | Required accessible headline for every non-search variant. |\n| `subtitle` | Optional supporting text for written headlines. |\n| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. |\n| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. |\n| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. |\n| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. |\n| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. |\n| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. |\n| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. |\n| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. |\n| `statusBarHeight` | Overrides only the automatic top inset. |\n| `contentStyle` | Styles the headline and subtitle area. |\n| `style` | Styles the app bar and can override its background color. |\n\n## Migrating from the compound API\n\n`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and\n`Appbar.BackAction` have been removed. Render one `Appbar` and provide its\nheadline, leading button, and trailing actions as props. The former\n`mode=\"medium\"` and `mode=\"large\"` values are now\n`variant=\"medium-flexible\"` and `variant=\"large-flexible\"`; centered\ncontent uses `headlineAlignment=\"center\"`.\n\n## Usage\n\n### Small app bar\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n {} }}\n trailingActions={[\n {\n key: 'search',\n icon: 'magnify',\n 'aria-label': 'Search',\n onPress: () => {},\n },\n {\n key: 'more',\n icon: 'dots-vertical',\n 'aria-label': 'More options',\n onPress: () => {},\n },\n ]}\n />\n);\n\nexport default MyComponent;\n```\n\n### Decorated action\n```js\nimport { Appbar, Tooltip } from 'react-native-paper';\n\n {},\n decorate: (button) => (\n {button}\n ),\n },\n ]}\n/>\n```\n\n### Flexible app bar\n```js\n\n```\n\n### Search app bar\n```js\n {} }}\n searchBar={{\n placeholder: 'Search messages',\n value: query,\n onChangeText: setQuery,\n }}\n/>\n```\n\n@extends View props https://reactnative.dev/docs/view#props", + "description": "A Material Design app bar for displaying a page headline, navigation, and\ncontextual actions. Appbar supports small, medium flexible, large flexible,\nand search variants. Its surface color automatically changes when content\nhas scrolled, and it accounts for safe-area insets.\n\n## Usage\n\n### Small app bar\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n {} }}\n trailingActions={[\n {\n key: 'search',\n icon: 'magnify',\n 'aria-label': 'Search',\n onPress: () => {},\n },\n {\n key: 'more',\n icon: 'dots-vertical',\n 'aria-label': 'More options',\n onPress: () => {},\n },\n ]}\n />\n);\n\nexport default MyComponent;\n```\n\n### Decorated action\n```js\nimport { Appbar, Tooltip } from 'react-native-paper';\n\n {},\n decorate: (button) => (\n {button}\n ),\n },\n ]}\n/>\n```\n\n### Flexible app bar\n```js\n\n```\n\n### Search app bar\n```js\n {} }}\n searchBar={{\n placeholder: 'Search messages',\n value: query,\n onChangeText: setQuery,\n }}\n/>\n```\n\n## Variants\n\n| `variant` | Purpose |\n| --- | --- |\n| `small` | A compact 64dp app bar with a one-line headline. |\n| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. |\n| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. |\n| `search` | An app bar containing a centered Paper `Searchbar`. |\n\n## Main props\n\n| Prop | Description |\n| --- | --- |\n| `headline` | Required accessible headline for every non-search variant. |\n| `subtitle` | Optional supporting text for written headlines. |\n| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. |\n| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. |\n| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. |\n| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. |\n| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. |\n| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. |\n| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. |\n| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. |\n| `statusBarHeight` | Overrides only the automatic top inset. |\n| `contentStyle` | Styles the headline and subtitle area. |\n| `style` | Styles the app bar and can override its background color. |\n\n## Migrating from the compound API\n\n`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and\n`Appbar.BackAction` have been removed. Render one `Appbar` and provide its\nheadline, leading button, and trailing actions as props. The former\n`mode=\"medium\"` and `mode=\"large\"` values are now\n`variant=\"medium-flexible\"` and `variant=\"large-flexible\"`; centered\ncontent uses `headlineAlignment=\"center\"`.\n\n@extends View props https://reactnative.dev/docs/view#props", "displayName": "Appbar", "methods": [], "statics": [], diff --git a/docs/src/data/screenshots.ts b/docs/src/data/screenshots.ts index a30ca3bff6..efcec64c88 100644 --- a/docs/src/data/screenshots.ts +++ b/docs/src/data/screenshots.ts @@ -3,6 +3,12 @@ export const screenshots = { 'Avatar.Icon': 'screenshots/avatar-icon.png', 'Avatar.Image': 'screenshots/avatar-image.png', 'Avatar.Text': 'screenshots/avatar-text.png', + Appbar: { + search: 'screenshots/appbar-v6-search.png', + small: 'screenshots/appbar-v6-small.png', + 'medium flexible': 'screenshots/appbar-v6-medium-flexible.png', + 'large flexible': 'screenshots/appbar-v6-large-flexible.png', + }, Badge: { 'with text': 'screenshots/badge-1.png', 'without text': 'screenshots/badge-2.png', diff --git a/src/components/Appbar/Appbar.tsx b/src/components/Appbar/Appbar.tsx index 94964d842f..8750f0dff5 100644 --- a/src/components/Appbar/Appbar.tsx +++ b/src/components/Appbar/Appbar.tsx @@ -27,42 +27,6 @@ const EMPTY_TRAILING_ACTIONS = [] as const; * and search variants. Its surface color automatically changes when content * has scrolled, and it accounts for safe-area insets. * - * ## Variants - * - * | `variant` | Purpose | - * | --- | --- | - * | `small` | A compact 64dp app bar with a one-line headline. | - * | `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. | - * | `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. | - * | `search` | An app bar containing a centered Paper `Searchbar`. | - * - * ## Main props - * - * | Prop | Description | - * | --- | --- | - * | `headline` | Required accessible headline for every non-search variant. | - * | `subtitle` | Optional supporting text for written headlines. | - * | `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. | - * | `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. | - * | `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. | - * | `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. | - * | `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. | - * | `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. | - * | `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. | - * | `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. | - * | `statusBarHeight` | Overrides only the automatic top inset. | - * | `contentStyle` | Styles the headline and subtitle area. | - * | `style` | Styles the app bar and can override its background color. | - * - * ## Migrating from the compound API - * - * `Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and - * `Appbar.BackAction` have been removed. Render one `Appbar` and provide its - * headline, leading button, and trailing actions as props. The former - * `mode="medium"` and `mode="large"` values are now - * `variant="medium-flexible"` and `variant="large-flexible"`; centered - * content uses `headlineAlignment="center"`. - * * ## Usage * * ### Small app bar @@ -140,6 +104,42 @@ const EMPTY_TRAILING_ACTIONS = [] as const; * /> * ``` * + * ## Variants + * + * | `variant` | Purpose | + * | --- | --- | + * | `small` | A compact 64dp app bar with a one-line headline. | + * | `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. | + * | `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. | + * | `search` | An app bar containing a centered Paper `Searchbar`. | + * + * ## Main props + * + * | Prop | Description | + * | --- | --- | + * | `headline` | Required accessible headline for every non-search variant. | + * | `subtitle` | Optional supporting text for written headlines. | + * | `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. | + * | `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. | + * | `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. | + * | `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. | + * | `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. | + * | `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. | + * | `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. | + * | `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. | + * | `statusBarHeight` | Overrides only the automatic top inset. | + * | `contentStyle` | Styles the headline and subtitle area. | + * | `style` | Styles the app bar and can override its background color. | + * + * ## Migrating from the compound API + * + * `Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and + * `Appbar.BackAction` have been removed. Render one `Appbar` and provide its + * headline, leading button, and trailing actions as props. The former + * `mode="medium"` and `mode="large"` values are now + * `variant="medium-flexible"` and `variant="large-flexible"`; centered + * content uses `headlineAlignment="center"`. + * * @extends View props https://reactnative.dev/docs/view#props */ const Appbar = ({ From 9127f1dc9d328135e8916297941805fc84c32c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Fri, 28 Aug 2026 13:54:38 +0200 Subject: [PATCH 31/34] docs(appbar): further clarifications --- docs/6.x/docs/components/Appbar.mdx | 6 ++++++ docs/src/data/componentDocs6x.json | 4 ++-- src/components/Appbar/Appbar.tsx | 6 ++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/6.x/docs/components/Appbar.mdx b/docs/6.x/docs/components/Appbar.mdx index f043c757b5..24ea396482 100644 --- a/docs/6.x/docs/components/Appbar.mdx +++ b/docs/6.x/docs/components/Appbar.mdx @@ -132,6 +132,12 @@ headline, leading button, and trailing actions as props. The former `variant="medium-flexible"` and `variant="large-flexible"`; centered content uses `headlineAlignment="center"`. +## Bottom toolbar support + +Material Design 3 drops the bottom bar support contained previously in the Appbar scope +and moves it to Toolbars, hence you can't use the component to construct a bottom bar +anymore - for these cases please use the Toolbar component. + diff --git a/docs/src/data/componentDocs6x.json b/docs/src/data/componentDocs6x.json index 7d93774bab..59469cb48b 100644 --- a/docs/src/data/componentDocs6x.json +++ b/docs/src/data/componentDocs6x.json @@ -95,10 +95,10 @@ "Appbar/Appbar": { "filepath": "Appbar/Appbar.tsx", "title": "Appbar", - "description": "A Material Design app bar for displaying a page headline, navigation, and\ncontextual actions. Appbar supports small, medium flexible, large flexible,\nand search variants. Its surface color automatically changes when content\nhas scrolled, and it accounts for safe-area insets.\n\n## Usage\n\n### Small app bar\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n {} }}\n trailingActions={[\n {\n key: 'search',\n icon: 'magnify',\n 'aria-label': 'Search',\n onPress: () => {},\n },\n {\n key: 'more',\n icon: 'dots-vertical',\n 'aria-label': 'More options',\n onPress: () => {},\n },\n ]}\n />\n);\n\nexport default MyComponent;\n```\n\n### Decorated action\n```js\nimport { Appbar, Tooltip } from 'react-native-paper';\n\n {},\n decorate: (button) => (\n {button}\n ),\n },\n ]}\n/>\n```\n\n### Flexible app bar\n```js\n\n```\n\n### Search app bar\n```js\n {} }}\n searchBar={{\n placeholder: 'Search messages',\n value: query,\n onChangeText: setQuery,\n }}\n/>\n```\n\n## Variants\n\n| `variant` | Purpose |\n| --- | --- |\n| `small` | A compact 64dp app bar with a one-line headline. |\n| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. |\n| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. |\n| `search` | An app bar containing a centered Paper `Searchbar`. |\n\n## Main props\n\n| Prop | Description |\n| --- | --- |\n| `headline` | Required accessible headline for every non-search variant. |\n| `subtitle` | Optional supporting text for written headlines. |\n| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. |\n| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. |\n| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. |\n| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. |\n| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. |\n| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. |\n| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. |\n| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. |\n| `statusBarHeight` | Overrides only the automatic top inset. |\n| `contentStyle` | Styles the headline and subtitle area. |\n| `style` | Styles the app bar and can override its background color. |\n\n## Migrating from the compound API\n\n`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and\n`Appbar.BackAction` have been removed. Render one `Appbar` and provide its\nheadline, leading button, and trailing actions as props. The former\n`mode=\"medium\"` and `mode=\"large\"` values are now\n`variant=\"medium-flexible\"` and `variant=\"large-flexible\"`; centered\ncontent uses `headlineAlignment=\"center\"`.\n\n@extends View props https://reactnative.dev/docs/view#props", + "description": "A Material Design app bar for displaying a page headline, navigation, and\ncontextual actions. Appbar supports small, medium flexible, large flexible,\nand search variants. Its surface color automatically changes when content\nhas scrolled, and it accounts for safe-area insets.\n\n## Usage\n\n### Small app bar\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n {} }}\n trailingActions={[\n {\n key: 'search',\n icon: 'magnify',\n 'aria-label': 'Search',\n onPress: () => {},\n },\n {\n key: 'more',\n icon: 'dots-vertical',\n 'aria-label': 'More options',\n onPress: () => {},\n },\n ]}\n />\n);\n\nexport default MyComponent;\n```\n\n### Decorated action\n```js\nimport { Appbar, Tooltip } from 'react-native-paper';\n\n {},\n decorate: (button) => (\n {button}\n ),\n },\n ]}\n/>\n```\n\n### Flexible app bar\n```js\n\n```\n\n### Search app bar\n```js\n {} }}\n searchBar={{\n placeholder: 'Search messages',\n value: query,\n onChangeText: setQuery,\n }}\n/>\n```\n\n## Variants\n\n| `variant` | Purpose |\n| --- | --- |\n| `small` | A compact 64dp app bar with a one-line headline. |\n| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. |\n| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. |\n| `search` | An app bar containing a centered Paper `Searchbar`. |\n\n## Main props\n\n| Prop | Description |\n| --- | --- |\n| `headline` | Required accessible headline for every non-search variant. |\n| `subtitle` | Optional supporting text for written headlines. |\n| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. |\n| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. |\n| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. |\n| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. |\n| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. |\n| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. |\n| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. |\n| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. |\n| `statusBarHeight` | Overrides only the automatic top inset. |\n| `contentStyle` | Styles the headline and subtitle area. |\n| `style` | Styles the app bar and can override its background color. |\n\n## Migrating from the compound API\n\n`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and\n`Appbar.BackAction` have been removed. Render one `Appbar` and provide its\nheadline, leading button, and trailing actions as props. The former\n`mode=\"medium\"` and `mode=\"large\"` values are now\n`variant=\"medium-flexible\"` and `variant=\"large-flexible\"`; centered\ncontent uses `headlineAlignment=\"center\"`.\n\n## Bottom toolbar support\n\nMaterial Design 3 drops the bottom bar support contained previously in the Appbar scope\nand moves it to Toolbars, hence you can't use the component to construct a bottom bar\nanymore - for these cases please use the Toolbar component.\n\n@extends View props https://reactnative.dev/docs/view#props", "link": "appbar", "data": { - "description": "A Material Design app bar for displaying a page headline, navigation, and\ncontextual actions. Appbar supports small, medium flexible, large flexible,\nand search variants. Its surface color automatically changes when content\nhas scrolled, and it accounts for safe-area insets.\n\n## Usage\n\n### Small app bar\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n {} }}\n trailingActions={[\n {\n key: 'search',\n icon: 'magnify',\n 'aria-label': 'Search',\n onPress: () => {},\n },\n {\n key: 'more',\n icon: 'dots-vertical',\n 'aria-label': 'More options',\n onPress: () => {},\n },\n ]}\n />\n);\n\nexport default MyComponent;\n```\n\n### Decorated action\n```js\nimport { Appbar, Tooltip } from 'react-native-paper';\n\n {},\n decorate: (button) => (\n {button}\n ),\n },\n ]}\n/>\n```\n\n### Flexible app bar\n```js\n\n```\n\n### Search app bar\n```js\n {} }}\n searchBar={{\n placeholder: 'Search messages',\n value: query,\n onChangeText: setQuery,\n }}\n/>\n```\n\n## Variants\n\n| `variant` | Purpose |\n| --- | --- |\n| `small` | A compact 64dp app bar with a one-line headline. |\n| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. |\n| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. |\n| `search` | An app bar containing a centered Paper `Searchbar`. |\n\n## Main props\n\n| Prop | Description |\n| --- | --- |\n| `headline` | Required accessible headline for every non-search variant. |\n| `subtitle` | Optional supporting text for written headlines. |\n| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. |\n| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. |\n| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. |\n| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. |\n| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. |\n| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. |\n| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. |\n| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. |\n| `statusBarHeight` | Overrides only the automatic top inset. |\n| `contentStyle` | Styles the headline and subtitle area. |\n| `style` | Styles the app bar and can override its background color. |\n\n## Migrating from the compound API\n\n`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and\n`Appbar.BackAction` have been removed. Render one `Appbar` and provide its\nheadline, leading button, and trailing actions as props. The former\n`mode=\"medium\"` and `mode=\"large\"` values are now\n`variant=\"medium-flexible\"` and `variant=\"large-flexible\"`; centered\ncontent uses `headlineAlignment=\"center\"`.\n\n@extends View props https://reactnative.dev/docs/view#props", + "description": "A Material Design app bar for displaying a page headline, navigation, and\ncontextual actions. Appbar supports small, medium flexible, large flexible,\nand search variants. Its surface color automatically changes when content\nhas scrolled, and it accounts for safe-area insets.\n\n## Usage\n\n### Small app bar\n```js\nimport * as React from 'react';\nimport { Appbar } from 'react-native-paper';\n\nconst MyComponent = () => (\n {} }}\n trailingActions={[\n {\n key: 'search',\n icon: 'magnify',\n 'aria-label': 'Search',\n onPress: () => {},\n },\n {\n key: 'more',\n icon: 'dots-vertical',\n 'aria-label': 'More options',\n onPress: () => {},\n },\n ]}\n />\n);\n\nexport default MyComponent;\n```\n\n### Decorated action\n```js\nimport { Appbar, Tooltip } from 'react-native-paper';\n\n {},\n decorate: (button) => (\n {button}\n ),\n },\n ]}\n/>\n```\n\n### Flexible app bar\n```js\n\n```\n\n### Search app bar\n```js\n {} }}\n searchBar={{\n placeholder: 'Search messages',\n value: query,\n onChangeText: setQuery,\n }}\n/>\n```\n\n## Variants\n\n| `variant` | Purpose |\n| --- | --- |\n| `small` | A compact 64dp app bar with a one-line headline. |\n| `medium-flexible` | A flexible app bar with a two-line headline and optional subtitle or image. |\n| `large-flexible` | A prominent flexible app bar with a two-line headline and optional subtitle or image. |\n| `search` | An app bar containing a centered Paper `Searchbar`. |\n\n## Main props\n\n| Prop | Description |\n| --- | --- |\n| `headline` | Required accessible headline for every non-search variant. |\n| `subtitle` | Optional supporting text for written headlines. |\n| `headlineAlignment` | Aligns headline content to `leading` (default) or `center`. |\n| `headlineImage` | Replaces the visible small headline, or appears above a flexible headline. Images should fit within 32dp height. |\n| `onHeadlinePress` | Makes the headline area interactive; use `headlinePressableProps` for its accessibility state. |\n| `leadingButton` | A back button (`{ type: 'back' }`) or standard icon-button configuration. Use `decorate` to wrap it with components such as `Tooltip` or `Menu`. |\n| `trailingActions` | Standard icon-button configurations, or exactly one `filled`/`tonal` action with optional `wide` width. Every action requires a stable `key` and `aria-label`, and can use `decorate` to wrap its resolved button. |\n| `searchBar` | Required for the `search` variant. Accepts Paper `Searchbar` props except `mode`, `elevation`, `showDivider`, and `theme`. |\n| `isScrolled` | Uses `surfaceContainer` instead of `surface` when content has scrolled. |\n| `safeAreaInsets` | Overrides detected top, left, or right safe-area insets. |\n| `statusBarHeight` | Overrides only the automatic top inset. |\n| `contentStyle` | Styles the headline and subtitle area. |\n| `style` | Styles the app bar and can override its background color. |\n\n## Migrating from the compound API\n\n`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and\n`Appbar.BackAction` have been removed. Render one `Appbar` and provide its\nheadline, leading button, and trailing actions as props. The former\n`mode=\"medium\"` and `mode=\"large\"` values are now\n`variant=\"medium-flexible\"` and `variant=\"large-flexible\"`; centered\ncontent uses `headlineAlignment=\"center\"`.\n\n## Bottom toolbar support\n\nMaterial Design 3 drops the bottom bar support contained previously in the Appbar scope\nand moves it to Toolbars, hence you can't use the component to construct a bottom bar\nanymore - for these cases please use the Toolbar component.\n\n@extends View props https://reactnative.dev/docs/view#props", "displayName": "Appbar", "methods": [], "statics": [], diff --git a/src/components/Appbar/Appbar.tsx b/src/components/Appbar/Appbar.tsx index 8750f0dff5..b18e9ed1bb 100644 --- a/src/components/Appbar/Appbar.tsx +++ b/src/components/Appbar/Appbar.tsx @@ -140,6 +140,12 @@ const EMPTY_TRAILING_ACTIONS = [] as const; * `variant="medium-flexible"` and `variant="large-flexible"`; centered * content uses `headlineAlignment="center"`. * + * ## Bottom toolbar support + * + * Material Design 3 drops the bottom bar support contained previously in the Appbar scope + * and moves it to Toolbars, hence you can't use the component to construct a bottom bar + * anymore - for these cases please use the Toolbar component. + * * @extends View props https://reactnative.dev/docs/view#props */ const Appbar = ({ From bdcb3028c0a3c465d5d99e62991b9c4d8aa8ad22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Fri, 28 Aug 2026 13:59:55 +0200 Subject: [PATCH 32/34] docs(appbar): migration to v6.x info --- docs/6.x/docs/guides/migration.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/6.x/docs/guides/migration.md b/docs/6.x/docs/guides/migration.md index 1fcd32bd25..fd1fe6c720 100644 --- a/docs/6.x/docs/guides/migration.md +++ b/docs/6.x/docs/guides/migration.md @@ -6,6 +6,18 @@ TBC ## Components +### Appbar + +The Paper 6.x `Appbar` is a big refatctor, which drops the previously used compound component approach. + +#### Migrating from the compound API + +`Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and `Appbar.BackAction` have been removed. Render one `Appbar` and provide its headline, leading button, and trailing actions as props. The former `mode="medium"` and `mode="large"` values are now `variant="medium-flexible"` and `variant="large-flexible"`; centered content uses `headlineAlignment="center"`. + +#### Bottom toolbar support + +Material Design 3 drops the bottom bar support contained previously in the `Appbar` scope and moves it to `Toolbars`, hence you can't use the component to construct a bottom bar anymore - for these cases please use the `Toolbar` component. + ### TextInput The Paper 6.x `TextInput` is a complete rewrite with a new API. Import the component the same way, but note that the props and behavior have changed significantly. From 205af279f5352bb0e35ecd9b06132f01be578452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Fri, 28 Aug 2026 14:06:02 +0200 Subject: [PATCH 33/34] docs(appbar): migration example --- docs/6.x/docs/guides/migration.md | 37 +++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/6.x/docs/guides/migration.md b/docs/6.x/docs/guides/migration.md index fd1fe6c720..0e690d68a0 100644 --- a/docs/6.x/docs/guides/migration.md +++ b/docs/6.x/docs/guides/migration.md @@ -14,6 +14,43 @@ The Paper 6.x `Appbar` is a big refatctor, which drops the previously used compo `Appbar.Header`, `Appbar.Content`, `Appbar.Action`, and `Appbar.BackAction` have been removed. Render one `Appbar` and provide its headline, leading button, and trailing actions as props. The former `mode="medium"` and `mode="large"` values are now `variant="medium-flexible"` and `variant="large-flexible"`; centered content uses `headlineAlignment="center"`. +```tsx +// Before (v5) + +const MyComponent = () => ( + + {}} /> + + {}} /> + {}} /> + +); + +// After (v6) + +const MyComponent = () => ( + {} }} + trailingActions={[ + { + key: 'search', + icon: 'magnify', + 'aria-label': 'Search', + onPress: () => {}, + }, + { + key: 'more', + icon: 'dots-vertical', + 'aria-label': 'More options', + onPress: () => {}, + }, + ]} + /> +); +``` + #### Bottom toolbar support Material Design 3 drops the bottom bar support contained previously in the `Appbar` scope and moves it to `Toolbars`, hence you can't use the component to construct a bottom bar anymore - for these cases please use the `Toolbar` component. From 2447364f8ff30a9bf077ccc7db73e81c26633199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kata?= Date: Fri, 28 Aug 2026 14:15:08 +0200 Subject: [PATCH 34/34] docs(appbar): even more docs improvements --- docs/6.x/docs/components/Appbar.mdx | 8 ++++++++ docs/src/data/componentDocs6x.json | 16 ++++++++++++---- src/components/Appbar/types.ts | 11 ++++++++++- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/docs/6.x/docs/components/Appbar.mdx b/docs/6.x/docs/components/Appbar.mdx index 24ea396482..0767059467 100644 --- a/docs/6.x/docs/components/Appbar.mdx +++ b/docs/6.x/docs/components/Appbar.mdx @@ -204,6 +204,14 @@ anymore - for these cases please use the Toolbar component.
+### testID + +
+ + + +
+ ### headlineAlignment
diff --git a/docs/src/data/componentDocs6x.json b/docs/src/data/componentDocs6x.json index 59469cb48b..fb17f3a4de 100644 --- a/docs/src/data/componentDocs6x.json +++ b/docs/src/data/componentDocs6x.json @@ -104,7 +104,7 @@ "statics": [], "props": { "theme": { - "description": "", + "description": "Theme override for the app bar.", "required": false, "tsType": { "name": "ThemeProp", @@ -148,7 +148,7 @@ } }, "style": { - "description": "", + "description": "Style applied to the app bar container.", "required": false, "tsType": { "name": "Animated.WithAnimatedValue>", @@ -156,13 +156,21 @@ } }, "ref": { - "description": "", + "description": "Reference for the app bar container.", "required": false, "tsType": { "name": "React.Ref", "raw": "React.Ref" } }, + "testID": { + "description": "TestID used for testing purposes.", + "required": false, + "tsType": { + "name": "string", + "raw": "string" + } + }, "headlineAlignment": { "defaultValue": { "value": "'leading'", @@ -252,7 +260,7 @@ } }, "variant": { - "description": "", + "description": "Visual and layout variant of the app bar.", "required": true, "tsType": { "name": "\"search\" | AppbarHeadlineVariant", diff --git a/src/components/Appbar/types.ts b/src/components/Appbar/types.ts index dc3bf84348..6f9ea22abe 100644 --- a/src/components/Appbar/types.ts +++ b/src/components/Appbar/types.ts @@ -136,11 +136,13 @@ type AppbarHeadlineImage = { }; type AppbarTextHeadline = AppbarWrittenHeadline & { + /** Visual and layout variant of the app bar. */ variant: AppbarHeadlineVariant; headlineImage?: never; }; type AppbarSmallImageHeadline = AppbarHeadlineImage & { + /** Visual and layout variant of the app bar. */ variant: 'small'; /** Accessible page headline. The image replaces this text visually. */ headline: string; @@ -151,12 +153,13 @@ type AppbarSmallImageHeadline = AppbarHeadlineImage & { type AppbarFlexibleImageHeadline = AppbarWrittenHeadline & AppbarHeadlineImage & { + /** Visual and layout variant of the app bar. */ variant: Exclude; }; type AppbarBaseProps = Omit< ViewProps, - 'accessibilityLabel' | 'accessibilityRole' | 'children' | 'style' + 'accessibilityLabel' | 'accessibilityRole' | 'children' | 'style' | 'testID' > & { /** Optional leading button. */ leadingButton?: AppbarLeadingButton; @@ -170,9 +173,14 @@ type AppbarBaseProps = Omit< left?: number; right?: number; }; + /** Style applied to the app bar container. */ style?: Animated.WithAnimatedValue>; + /** Reference for the app bar container. */ ref?: React.Ref; + /** Theme override for the app bar. */ theme?: ThemeProp; + /** TestID used for testing purposes. */ + testID?: string; }; type AppbarHeadlineProps = { @@ -205,6 +213,7 @@ export type AppbarSearchbarProps = Omit< }; type AppbarSearchProps = { + /** Visual and layout variant of the app bar. */ variant: 'search'; /** Props forwarded to the existing Paper Searchbar. */ searchBar: AppbarSearchbarProps;