1import { useState, type ReactNode } from "react";
2import { ActivityIndicator, View } from "react-native";
3
4import type { AppSymbolName } from "./AppSymbol";
5import { AppText as Text } from "./AppText";
6import { cn } from "../lib/cn";
7import { MaterialIconButton } from "./MaterialIconButton";
8import { AndroidAnchoredMenu } from "./AndroidAnchoredMenu";
9import { splitAndroidHeaderActions } from "./headerMenu.android";
10import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole";
11import { useMaterialToolbarLayout } from "./useMaterialToolbarLayout";
12
13export interface AndroidHeaderAction {
14 readonly accessibilityLabel: string;
15 readonly icon: AppSymbolName;
16 readonly onPress: () => void;
17 readonly disabled?: boolean;
18 readonly loading?: boolean;
19 readonly selected?: boolean;
20 readonly destructive?: boolean;
21 readonly overflowOnly?: boolean;
22}
23
24export function AndroidHeaderIconButton(props: {
25 readonly accessibilityLabel: string;
26 readonly icon: AppSymbolName;
27 readonly onPress?: () => void;
28 readonly disabled?: boolean;
29 readonly selected?: boolean;
30}) {
31 return <MaterialIconButton {...props} tintColorClassName="accent-header-foreground" />;
32}
33
34export function AndroidScreenHeader(props: {
35 readonly title: string;
36 readonly titleContent?: ReactNode;
37 readonly subtitle?: string | null;
38 readonly actions?: ReadonlyArray<AndroidHeaderAction>;
39 readonly leading?: ReactNode;
40 readonly trailing?: ReactNode;
41 readonly onBack?: () => void;
42 readonly embedded?: boolean;
43 readonly hideBottomBorder?: boolean;
44}) {
45 const titleTypography = useScaledTextRole("title");
46 const subtitleTypography = useScaledTextRole("label");
47 const { height: materialToolbarHeight, ...headerPadding } = useMaterialToolbarLayout(
48 props.embedded,
49 );
50 const [headerWidth, setHeaderWidth] = useState(0);
51 const { visibleActions, overflowActions } = splitAndroidHeaderActions(
52 props.actions ?? [],
53 headerWidth >= 600,
54 );
55
56 return (
57 <View
58 onLayout={(event) => setHeaderWidth(event.nativeEvent.layout.width)}
59 className="border-b border-header-border bg-header px-2"
60 style={{
61 ...headerPadding,
62 borderBottomWidth: props.hideBottomBorder ? 0 : undefined,
63 }}
64 >
65 <View style={{ minHeight: materialToolbarHeight }} className="flex-row items-center gap-1">
66 {props.onBack ? (
67 <MaterialIconButton
68 accessibilityLabel="Navigate up"
69 icon="arrow.left"
70 tintColorClassName="accent-header-foreground"
71 onPress={props.onBack}
72 />
73 ) : null}
74
75 {props.leading}
76
77 <View className={cn("min-w-0 flex-1", !props.onBack && "pl-1")}>
78 {props.titleContent ?? (
79 <Text numberOfLines={1} style={titleTypography} className="text-header-foreground">
80 {props.title}
81 </Text>
82 )}
83 {props.subtitle ? (
84 <Text
85 numberOfLines={1}
86 style={subtitleTypography}
87 className="mt-px text-[13px] font-t3-medium text-foreground-muted"
88 >
89 {props.subtitle}
90 </Text>
91 ) : null}
92 </View>
93
94 {visibleActions.map((action) =>
95 action.loading ? (
96 <View
97 key={action.accessibilityLabel}
98 accessibilityLabel={action.accessibilityLabel}
99 className="size-12 items-center justify-center"
100 >
101 <ActivityIndicator colorClassName="accent-header-foreground" />
102 </View>
103 ) : (
104 <AndroidHeaderIconButton
105 key={action.accessibilityLabel}
106 accessibilityLabel={action.accessibilityLabel}
107 disabled={action.disabled}
108 selected={action.selected}
109 icon={action.icon}
110 onPress={action.onPress}
111 />
112 ),
113 )}
114 {overflowActions.length > 0 ? (
115 <AndroidAnchoredMenu
116 actions={overflowActions.map((action, index) => ({
117 id: String(index),
118 title: action.accessibilityLabel,
119 attributes: {
120 disabled: Boolean(action.disabled),
121 destructive: action.destructive,
122 state: action.selected ? "on" : undefined,
123 },
124 }))}
125 onPressAction={({ nativeEvent }) =>
126 overflowActions[Number(nativeEvent.event)]?.onPress()
127 }
128 >
129 {(open) => (
130 <MaterialIconButton
131 accessibilityLabel="More actions"
132 icon="ellipsis"
133 tintColorClassName="accent-header-foreground"
134 onPress={open}
135 />
136 )}
137 </AndroidAnchoredMenu>
138 ) : null}
139 {props.trailing}
140 </View>
141 </View>
142 );
143}
144
145export function AndroidSheetHeader(
146 props: Omit<Parameters<typeof AndroidScreenHeader>[0], "embedded">,
147) {
148 return <AndroidScreenHeader {...props} embedded />;
149}