Skip to content

Commit dd22197

Browse files
committed
revert(appbar): drop Appbar from this PR
callstack#4934 proposes an `actions` prop API for Appbar and callstack#5075 implements it, so the children refactor here is superseded. Appbar sources, tests and snapshots go back to their state on main.
1 parent f7b86a0 commit dd22197

7 files changed

Lines changed: 348 additions & 124 deletions

File tree

‎src/components/Appbar/Appbar.tsx‎

Lines changed: 115 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ import { Animated, StyleSheet, View } from 'react-native';
33
import type { ColorValue, StyleProp, ViewProps, ViewStyle } from 'react-native';
44

55
import AppbarContent from './AppbarContent';
6-
import { AppbarContext } from './AppbarContext';
7-
import { getAppbarBackgroundColor, modeAppbarHeight } from './utils';
6+
import {
7+
getAppbarBackgroundColor,
8+
modeAppbarHeight,
9+
renderAppbarContent,
10+
filterAppbarActions,
11+
} from './utils';
812
import type { AppbarModes, AppbarChildProps } from './utils';
913
import { useInternalTheme } from '../../core/theming';
1014
import type { Elevation, ThemeProp } from '../../types';
@@ -171,60 +175,45 @@ const Appbar = ({
171175

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

178+
const isCenterAlignedMode = isMode('center-aligned');
179+
180+
let shouldCenterContent = false;
181+
let shouldAddLeftSpacing = false;
182+
let shouldAddRightSpacing = false;
183+
if (isCenterAlignedMode) {
184+
let hasAppbarContent = false;
185+
let leftItemsCount = 0;
186+
let rightItemsCount = 0;
187+
188+
React.Children.forEach(children, (child) => {
189+
if (React.isValidElement<AppbarChildProps>(child)) {
190+
const isLeading = child.props.isLeading === true;
191+
192+
if (child.type === AppbarContent) {
193+
hasAppbarContent = true;
194+
} else if (isLeading || !hasAppbarContent) {
195+
leftItemsCount++;
196+
} else {
197+
rightItemsCount++;
198+
}
199+
}
200+
});
201+
202+
shouldCenterContent =
203+
hasAppbarContent && leftItemsCount < 2 && rightItemsCount < 3;
204+
shouldAddLeftSpacing = shouldCenterContent && leftItemsCount === 0;
205+
shouldAddRightSpacing = shouldCenterContent && rightItemsCount === 0;
206+
}
207+
208+
const spacingStyle = styles.v3Spacing;
209+
174210
const insets = {
175211
paddingBottom: safeAreaInsets?.bottom,
176212
paddingTop: safeAreaInsets?.top,
177213
paddingLeft: safeAreaInsets?.left,
178214
paddingRight: safeAreaInsets?.right,
179215
};
180216

181-
const appbarContextValue = React.useMemo(
182-
() => ({ isDark, mode }),
183-
[isDark, mode]
184-
);
185-
186-
let content: React.ReactNode = children;
187-
188-
if (isMode('medium') || isMode('large')) {
189-
// Medium/large top app bars use a two-row layout: a controls row with the
190-
// leading and trailing actions above a full-width title row. React Native
191-
// flexbox has no `order`, so the title has to be separated from the actions
192-
// structurally. We partition the children by element identity and the
193-
// `isLeading` prop for layout only — nothing is injected into them; shared
194-
// values flow through `AppbarContext`.
195-
const items = React.Children.toArray(children).filter(
196-
(child): child is React.ReactElement<AppbarChildProps> =>
197-
React.isValidElement(child)
198-
);
199-
const isAppbarContent = (child: React.ReactElement<AppbarChildProps>) => {
200-
const { type } = child;
201-
// React.memo(AppbarContent) wraps the component in an object whose
202-
// `.type` holds the original component — unwrap it so memoized
203-
// Content still lands in the title row.
204-
const innerType =
205-
typeof type === 'object' && type !== null && 'type' in type
206-
? (type as { type: unknown }).type
207-
: type;
208-
return innerType === AppbarContent;
209-
};
210-
const titleItems = items.filter(isAppbarContent);
211-
const actionItems = items.filter((child) => !isAppbarContent(child));
212-
const leadingActions = actionItems.filter((child) => child.props.isLeading);
213-
const trailingActions = actionItems.filter(
214-
(child) => !child.props.isLeading
215-
);
216-
217-
content = (
218-
<View style={styles.columnContainer}>
219-
<View style={styles.controlsRow}>
220-
{leadingActions}
221-
<View style={styles.rightActionControls}>{trailingActions}</View>
222-
</View>
223-
{titleItems}
224-
</View>
225-
);
226-
}
227-
228217
return (
229218
<Surface
230219
style={[
@@ -240,9 +229,77 @@ const Appbar = ({
240229
container
241230
{...rest}
242231
>
243-
<AppbarContext.Provider value={appbarContextValue}>
244-
{content}
245-
</AppbarContext.Provider>
232+
{shouldAddLeftSpacing ? <View style={spacingStyle} /> : null}
233+
{(isMode('small') || isMode('center-aligned')) && (
234+
<>
235+
{/* Render only the back action at first place */}
236+
{renderAppbarContent({
237+
children,
238+
isDark,
239+
theme,
240+
renderOnly: ['Appbar.BackAction'],
241+
shouldCenterContent: isCenterAlignedMode || shouldCenterContent,
242+
})}
243+
{/* Render the rest of the content except the back action */}
244+
{renderAppbarContent({
245+
// Filter appbar actions - first leading icons, then trailing icons
246+
children: [
247+
...filterAppbarActions(children, true),
248+
...filterAppbarActions(children),
249+
],
250+
isDark,
251+
theme,
252+
renderExcept: ['Appbar.BackAction'],
253+
shouldCenterContent: isCenterAlignedMode || shouldCenterContent,
254+
})}
255+
</>
256+
)}
257+
{(isMode('medium') || isMode('large')) && (
258+
<View
259+
style={[
260+
styles.columnContainer,
261+
isMode('center-aligned') && styles.centerAlignedContainer,
262+
]}
263+
>
264+
{/* Appbar top row with controls */}
265+
<View style={styles.controlsRow}>
266+
{/* Left side of row container, can contain AppbarBackAction or AppbarAction if it's leading icon */}
267+
{renderAppbarContent({
268+
children,
269+
isDark,
270+
renderOnly: ['Appbar.BackAction'],
271+
mode,
272+
})}
273+
{renderAppbarContent({
274+
children: filterAppbarActions(children, true),
275+
isDark,
276+
renderOnly: ['Appbar.Action'],
277+
mode,
278+
})}
279+
{/* Right side of row container, can contain other AppbarAction if they are not leading icons */}
280+
<View style={styles.rightActionControls}>
281+
{renderAppbarContent({
282+
children: filterAppbarActions(children),
283+
isDark,
284+
renderExcept: [
285+
'Appbar',
286+
'Appbar.BackAction',
287+
'Appbar.Content',
288+
'Appbar.Header',
289+
],
290+
mode,
291+
})}
292+
</View>
293+
</View>
294+
{renderAppbarContent({
295+
children,
296+
isDark,
297+
renderOnly: ['Appbar.Content'],
298+
mode,
299+
})}
300+
</View>
301+
)}
302+
{shouldAddRightSpacing ? <View style={spacingStyle} /> : null}
246303
</Surface>
247304
);
248305
};
@@ -253,6 +310,9 @@ const styles = StyleSheet.create({
253310
alignItems: 'center',
254311
paddingHorizontal: 4,
255312
},
313+
v3Spacing: {
314+
width: 52,
315+
},
256316
controlsRow: {
257317
flex: 1,
258318
flexDirection: 'row',
@@ -269,6 +329,9 @@ const styles = StyleSheet.create({
269329
flex: 1,
270330
paddingTop: 8,
271331
},
332+
centerAlignedContainer: {
333+
paddingTop: 0,
334+
},
272335
});
273336

274337
export default Appbar;

‎src/components/Appbar/AppbarAction.tsx‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ import type {
77
ViewStyle,
88
} from 'react-native';
99

10-
import { useAppbarContext } from './AppbarContext';
1110
import { useInternalTheme } from '../../core/theming';
12-
import { white } from '../../theme/colors';
1311
import type { ThemeProp } from '../../types';
1412
import type { IconSource } from '../Icon';
1513
import IconButton from '../IconButton/IconButton';
@@ -42,7 +40,7 @@ export type Props = React.ComponentPropsWithoutRef<typeof IconButton> & {
4240
/**
4341
* @supported Available in v5.x with theme version 3
4442
*
45-
* Whether it's the leading button. Sets the icon color, and in `medium` and `large` modes places the action on the leading side of the controls row. In `small` and `center-aligned` modes children render in the order they are written.
43+
* Whether it's the leading button. Note: If `Appbar.BackAction` is present, it will be rendered before any `isLeading` icons.
4644
*/
4745
isLeading?: boolean;
4846
style?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
@@ -89,15 +87,12 @@ const AppbarAction = ({
8987
}: Props) => {
9088
const theme = useInternalTheme(themeOverrides);
9189
const { colors } = theme;
92-
const { isDark = false } = useAppbarContext() ?? {};
9390

9491
const actionIconColor = iconColor
9592
? iconColor
96-
: isDark
97-
? white
98-
: isLeading
99-
? colors.onSurface
100-
: colors.onSurfaceVariant;
93+
: isLeading
94+
? colors.onSurface
95+
: colors.onSurfaceVariant;
10196

10297
return (
10398
<IconButton

‎src/components/Appbar/AppbarContent.tsx‎

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ import type {
88
ViewProps,
99
} from 'react-native';
1010

11-
import { useAppbarContext } from './AppbarContext';
1211
import { modeTextVariant } from './utils';
1312
import { useInternalTheme } from '../../core/theming';
14-
import { white } from '../../theme/colors';
1513
import type { $RemoveChildren, ThemeProp } from '../../types';
1614
import Text from '../Typography/Text';
1715
import type { TextRef } from '../Typography/Text';
@@ -95,27 +93,21 @@ const AppbarContent = ({
9593
titleStyle,
9694
title,
9795
titleMaxFontSizeMultiplier,
98-
mode: modeOverride,
96+
mode = 'small',
9997
theme: themeOverrides,
10098
testID = 'appbar-content',
10199
...rest
102100
}: Props) => {
103101
const theme = useInternalTheme(themeOverrides);
104102
const { colors, fonts } = theme;
105-
const { isDark = false, mode: contextMode } = useAppbarContext() ?? {};
106-
const mode = modeOverride ?? contextMode ?? 'small';
107103

108-
const titleTextColor = titleColor
109-
? titleColor
110-
: isDark
111-
? white
112-
: colors.onSurface;
104+
const titleTextColor = titleColor ? titleColor : colors.onSurface;
113105

114106
const modeContainerStyles = {
115107
small: styles.v3DefaultContainer,
116108
medium: styles.v3MediumContainer,
117109
large: styles.v3LargeContainer,
118-
'center-aligned': styles.v3CenterAlignedContainer,
110+
'center-aligned': styles.v3DefaultContainer,
119111
};
120112

121113
const variant = modeTextVariant[mode];
@@ -180,24 +172,17 @@ const styles = StyleSheet.create({
180172
},
181173
v3DefaultContainer: {
182174
paddingHorizontal: 0,
183-
marginLeft: 12,
184-
},
185-
v3CenterAlignedContainer: {
186-
paddingHorizontal: 0,
187-
alignItems: 'center',
188175
},
189176
v3MediumContainer: {
190177
paddingHorizontal: 0,
191178
justifyContent: 'flex-end',
192179
paddingBottom: 24,
193-
marginLeft: 12,
194180
},
195181
v3LargeContainer: {
196182
paddingHorizontal: 0,
197183
paddingTop: 36,
198184
justifyContent: 'flex-end',
199185
paddingBottom: 28,
200-
marginLeft: 12,
201186
},
202187
});
203188

‎src/components/Appbar/AppbarContext.tsx‎

Lines changed: 0 additions & 28 deletions
This file was deleted.

0 commit comments

Comments
 (0)