Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions example/src/Examples/CardExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,12 @@ const CardExample = () => {
<Card style={styles.card} mode={selectedMode}>
<Card.Cover source={require('../../assets/images/forest.jpg')} />
<Card.Actions>
<Button onPress={() => {}}>Share</Button>
<Button onPress={() => {}}>Explore</Button>
<Button mode="outlined" onPress={() => {}}>
Share
</Button>
<Button mode="contained" onPress={() => {}}>
Explore
</Button>
</Card.Actions>
</Card>
<Card style={styles.card} mode={selectedMode}>
Expand Down Expand Up @@ -104,10 +108,10 @@ const CardExample = () => {
/>
<Card.Title title="Custom Button styles" />
<Card.Actions>
<Button style={styles.button} onPress={() => {}}>
<Button mode="outlined" style={styles.button} onPress={() => {}}>
Share
</Button>
<Button style={styles.button} onPress={() => {}}>
<Button mode="contained" style={styles.button} onPress={() => {}}>
Explore
</Button>
</Card.Actions>
Expand Down
16 changes: 12 additions & 4 deletions example/src/Examples/TeamDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,12 @@ const News = () => {
</Text>
</Card.Content>
<Card.Actions>
<Button onPress={() => {}}>Share</Button>
<Button onPress={() => {}}>Read more</Button>
<Button mode="outlined" onPress={() => {}}>
Share
</Button>
<Button mode="contained" onPress={() => {}}>
Read more
</Button>
</Card.Actions>
</Card>
<Card style={styles.card} mode="contained">
Expand All @@ -110,8 +114,12 @@ const News = () => {
</Text>
</Card.Content>
<Card.Actions>
<Button onPress={() => {}}>Share</Button>
<Button onPress={() => {}}>Read more</Button>
<Button mode="outlined" onPress={() => {}}>
Share
</Button>
<Button mode="contained" onPress={() => {}}>
Read more
</Button>
</Card.Actions>
</Card>
</View>
Expand Down
155 changes: 40 additions & 115 deletions src/components/Appbar/Appbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@ import { Animated, StyleSheet, View } from 'react-native';
import type { ColorValue, StyleProp, ViewProps, ViewStyle } from 'react-native';

import AppbarContent from './AppbarContent';
import {
getAppbarBackgroundColor,
modeAppbarHeight,
renderAppbarContent,
filterAppbarActions,
} from './utils';
import { AppbarContext } from './AppbarContext';
import { getAppbarBackgroundColor, modeAppbarHeight } from './utils';
import type { AppbarModes, AppbarChildProps } from './utils';
import { useInternalTheme } from '../../core/theming';
import type { Elevation, ThemeProp } from '../../types';
Expand Down Expand Up @@ -174,45 +170,48 @@ const Appbar = ({

const isDark = typeof dark === 'boolean' ? dark : false;

const isCenterAlignedMode = isMode('center-aligned');

let shouldCenterContent = false;
let shouldAddLeftSpacing = false;
let shouldAddRightSpacing = false;
if (isCenterAlignedMode) {
let hasAppbarContent = false;
let leftItemsCount = 0;
let rightItemsCount = 0;

React.Children.forEach(children, (child) => {
if (React.isValidElement<AppbarChildProps>(child)) {
const isLeading = child.props.isLeading === true;

if (child.type === AppbarContent) {
hasAppbarContent = true;
} else if (isLeading || !hasAppbarContent) {
leftItemsCount++;
} else {
rightItemsCount++;
}
}
});

shouldCenterContent =
hasAppbarContent && leftItemsCount < 2 && rightItemsCount < 3;
shouldAddLeftSpacing = shouldCenterContent && leftItemsCount === 0;
shouldAddRightSpacing = shouldCenterContent && rightItemsCount === 0;
}

const spacingStyle = styles.v3Spacing;

const insets = {
paddingBottom: safeAreaInsets?.bottom,
paddingTop: safeAreaInsets?.top,
paddingLeft: safeAreaInsets?.left,
paddingRight: safeAreaInsets?.right,
};

const appbarContextValue = React.useMemo(
() => ({ isDark, mode }),
[isDark, mode]
);

let content: React.ReactNode = children;

if (isMode('medium') || isMode('large')) {
// Medium/large top app bars use a two-row layout: a controls row with the
// leading and trailing actions above a full-width title row. React Native
// flexbox has no `order`, so the title has to be separated from the actions
// structurally. We partition the children by element identity and the
// `isLeading` prop for layout only — nothing is injected into them; shared
// values flow through `AppbarContext`.
const items = React.Children.toArray(children).filter(
React.isValidElement
) as React.ReactElement<AppbarChildProps>[];
const titleItems = items.filter((child) => child.type === AppbarContent);
const actionItems = items.filter((child) => child.type !== AppbarContent);
const leadingActions = actionItems.filter((child) => child.props.isLeading);
const trailingActions = actionItems.filter(
(child) => !child.props.isLeading
);
Comment on lines +194 to +202

content = (
<View style={styles.columnContainer}>
<View style={styles.controlsRow}>
{leadingActions}
<View style={styles.rightActionControls}>{trailingActions}</View>
</View>
{titleItems}
</View>
);
}

return (
<Surface
style={[
Expand All @@ -228,77 +227,9 @@ const Appbar = ({
container
{...rest}
>
{shouldAddLeftSpacing ? <View style={spacingStyle} /> : 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')) && (
<View
style={[
styles.columnContainer,
isMode('center-aligned') && styles.centerAlignedContainer,
]}
>
{/* Appbar top row with controls */}
<View style={styles.controlsRow}>
{/* 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 */}
<View style={styles.rightActionControls}>
{renderAppbarContent({
children: filterAppbarActions(children),
isDark,
renderExcept: [
'Appbar',
'Appbar.BackAction',
'Appbar.Content',
'Appbar.Header',
],
mode,
})}
</View>
</View>
{renderAppbarContent({
children,
isDark,
renderOnly: ['Appbar.Content'],
mode,
})}
</View>
)}
{shouldAddRightSpacing ? <View style={spacingStyle} /> : null}
<AppbarContext.Provider value={appbarContextValue}>
{content}
</AppbarContext.Provider>
</Surface>
);
};
Expand All @@ -309,9 +240,6 @@ const styles = StyleSheet.create({
alignItems: 'center',
paddingHorizontal: 4,
},
v3Spacing: {
width: 52,
},
controlsRow: {
flex: 1,
flexDirection: 'row',
Expand All @@ -328,9 +256,6 @@ const styles = StyleSheet.create({
flex: 1,
paddingTop: 8,
},
centerAlignedContainer: {
paddingTop: 0,
},
});

export default Appbar;
Expand Down
11 changes: 8 additions & 3 deletions src/components/Appbar/AppbarAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import type {
ViewStyle,
} from 'react-native';

import { useAppbarContext } from './AppbarContext';
import { useInternalTheme } from '../../core/theming';
import { white } from '../../theme/colors';
import type { Theme, ThemeProp } from '../../types';
import type { IconSource } from '../Icon';
import IconButton from '../IconButton/IconButton';
Expand Down Expand Up @@ -87,12 +89,15 @@ const AppbarAction = ({
}: Props) => {
const theme = useInternalTheme(themeOverrides);
const { colors } = theme as Theme;
const { isDark = false } = useAppbarContext() ?? {};

const actionIconColor = iconColor
? iconColor
: isLeading
? colors.onSurface
: colors.onSurfaceVariant;
: isDark
? white
: isLeading
? colors.onSurface
: colors.onSurfaceVariant;

return (
<IconButton
Expand Down
21 changes: 18 additions & 3 deletions src/components/Appbar/AppbarContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import type {
ViewProps,
} from 'react-native';

import { useAppbarContext } from './AppbarContext';
import { modeTextVariant } from './utils';
import { useInternalTheme } from '../../core/theming';
import { white } from '../../theme/colors';
import type {
$RemoveChildren,
Theme,
Expand Down Expand Up @@ -98,21 +100,27 @@ const AppbarContent = ({
titleStyle,
title,
titleMaxFontSizeMultiplier,
mode = 'small',
mode: modeOverride,
theme: themeOverrides,
testID = 'appbar-content',
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);
const { colors, fonts } = theme as Theme;
const { isDark = false, mode: contextMode } = useAppbarContext() ?? {};
const mode = modeOverride ?? contextMode ?? 'small';

const titleTextColor = titleColor ? titleColor : colors.onSurface;
const titleTextColor = titleColor
? titleColor
: isDark
? white
: colors.onSurface;

const modeContainerStyles = {
small: styles.v3DefaultContainer,
medium: styles.v3MediumContainer,
large: styles.v3LargeContainer,
'center-aligned': styles.v3DefaultContainer,
'center-aligned': styles.v3CenterAlignedContainer,
};

const variant = modeTextVariant[mode] as TypescaleKey;
Expand Down Expand Up @@ -177,17 +185,24 @@ const styles = StyleSheet.create({
},
v3DefaultContainer: {
paddingHorizontal: 0,
marginLeft: 12,
},
v3CenterAlignedContainer: {
paddingHorizontal: 0,
alignItems: 'center',
},
v3MediumContainer: {
paddingHorizontal: 0,
justifyContent: 'flex-end',
paddingBottom: 24,
marginLeft: 12,
},
v3LargeContainer: {
paddingHorizontal: 0,
paddingTop: 36,
justifyContent: 'flex-end',
paddingBottom: 28,
marginLeft: 12,
},
});

Expand Down
28 changes: 28 additions & 0 deletions src/components/Appbar/AppbarContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as React from 'react';

import type { AppbarModes } from './utils';

export type AppbarContextType = {
/**
* Whether the Appbar background is dark, so children can derive a
* contrasting default foreground color without it being injected.
*/
isDark: boolean;
/**
* The Appbar mode, consumed by `Appbar.Content` to pick the title text
* variant and container layout.
*/
mode: AppbarModes;
};

/**
* Shared Appbar values provided to `Appbar.Action`, `Appbar.BackAction` and
* `Appbar.Content` via context instead of being injected with `cloneElement`.
* This keeps composition intact: children can be wrapped, reordered or
* conditionally rendered without losing the values they need.
*/
export const AppbarContext = React.createContext<AppbarContextType | null>(
null
);

export const useAppbarContext = () => React.useContext(AppbarContext);
Loading