Skip to content
Source
Sign inSign up

SheetHeader.tsx

198 lines6.4 KB

1import { Pressable, View } from "react-native";
2​
3import { NativeStackScreenOptions } from "../../native/StackHeader";
4import { AppActivityIndicator } from "./AppActivityIndicator";
5import { AppText } from "./AppText";
6import { SymbolView } from "./AppSymbol";
7import { IconButton } from "./IconButton";
8import { Toolbar } from "./Toolbar";
9import { createNativeHeaderMenu } from "./nativeHeaderMenu.ios";
10import { useControlTheme } from "./useControlTheme";
11import type { SheetCloseButtonProps, SheetHeaderProps } from "./SheetHeader.types";
12​
13export function SheetCloseButton(props: SheetCloseButtonProps) {
14 return (
15 <IconButton
16 icon="xmark"
17 accessibilityLabel={props.accessibilityLabel ?? (props.onCancel ? "Cancel" : "Close")}
18 disabled={props.disabled}
19 onPress={props.onCancel ?? props.onClose}
20 />
21 );
23​
24export function SheetHeader(props: SheetHeaderProps) {
25 return (
26 <>
27 <SheetDismissGuard dismissDisabled={props.dismissDisabled} />
28 {props.presentation !== "inline" ? (
29 <NativeSheetHeader {...props} />
30 ) : (
31 <InlineSheetHeader {...props} />
32 )}
33 </>
34 );
36​
37/** A sheet that can't be closed with its button can't be swiped away either. */
38function SheetDismissGuard(props: { readonly dismissDisabled: boolean | undefined }) {
39 return props.dismissDisabled === undefined ? null : (
40 <NativeStackScreenOptions options={{ gestureEnabled: !props.dismissDisabled }} />
41 );
43​
44function NativeSheetHeader(props: SheetHeaderProps) {
45 const { colors } = useControlTheme();
46 const onDismiss = props.onCancel ?? props.onClose;
47 const dismissLabel = props.accessibilityLabel ?? (props.onCancel ? "Cancel" : "Close");
48 const confirmation = props.confirm;
49​
50 // Toolbar children are parsed as native items, so keep the actual Button
51 // elements here instead of hiding them behind another component.
52 return (
53 <>
54 <NativeStackScreenOptions
55 options={{
56 title: props.title,
57 headerBackVisible: false,
58 unstable_headerSubtitle: props.subtitle,
59 }}
60 />
61 <Toolbar placement="left">
62 <Toolbar.Button
63 icon="xmark"
64 accessibilityLabel={dismissLabel}
65 onPress={onDismiss}
66 disabled={props.dismissDisabled}
67 />
68 </Toolbar>
69 {confirmation || props.trailing || props.actions?.length || props.menus?.length ? (
70 <Toolbar placement="right">
71 {props.actions?.map((action) => (
72 <Toolbar.Button
73 key={action.accessibilityLabel}
74 icon={typeof action.icon === "string" ? action.icon : action.icon.ios}
75 accessibilityLabel={action.accessibilityLabel}
76 disabled={action.disabled || action.loading}
77 tintColor={action.tintColor}
78 onPress={action.onPress}
79 />
80 ))}
81 {props.menus?.map(createNativeHeaderMenu)}
82 {props.trailing ? (
83 <Toolbar.Custom>
84 <View>{props.trailing}</View>
85 </Toolbar.Custom>
86 ) : null}
87 {confirmation?.loading ? (
88 <Toolbar.Custom>
89 <View
90 className="size-11 items-center justify-center"
91 accessible
92 accessibilityLabel={confirmation.label}
93 accessibilityState={{ busy: true }}
94 >
95 <AppActivityIndicator colorClassName="accent-prominent-action" />
96 </View>
97 </Toolbar.Custom>
98 ) : confirmation ? (
99 <Toolbar.Button
100 icon="checkmark"
101 variant="prominent"
102 visibilityPriority="high"
103 tintColor={colors["--color-prominent-action"]}
104 accessibilityLabel={confirmation.label}
105 disabled={confirmation.disabled}
106 onPress={confirmation.onPress}
107 />
108 ) : null}
109 </Toolbar>
110 ) : null}
111 </>
112 );
114​
115function InlineConfirmation({
116 confirmation,
117}: {
118 readonly confirmation: NonNullable<SheetHeaderProps["confirm"]>;
119}) {
120 const confirmDisabled = confirmation.disabled || confirmation.loading;
121 return (
122 <Pressable
123 accessibilityRole="button"
124 accessibilityLabel={confirmation.label}
125 accessibilityState={{
126 disabled: Boolean(confirmDisabled),
127 busy: Boolean(confirmation.loading),
128 }}
129 disabled={confirmDisabled}
130 onPress={confirmation.onPress}
131 className={
132 confirmDisabled
133 ? "size-11 items-center justify-center rounded-full bg-subtle-strong"
134 : "size-11 items-center justify-center rounded-full bg-prominent-action"
135 }
136 >
137 {confirmation.loading ? (
138 <AppActivityIndicator colorClassName="accent-icon-subtle" />
139 ) : (
140 <SymbolView
141 name="checkmark"
142 size={20}
143 tintColorClassName={
144 confirmDisabled ? "accent-icon-subtle" : "accent-prominent-action-foreground"
145 }
146 />
147 )}
148 </Pressable>
149 );
151​
152function InlineSheetHeader(props: SheetHeaderProps) {
153 const close = (
154 <SheetCloseButton
155 {...(props.onCancel ? { onCancel: props.onCancel } : { onClose: props.onClose })}
156 accessibilityLabel={props.accessibilityLabel}
157 disabled={props.dismissDisabled}
158 />
159 );
160 const confirm = props.confirm ? <InlineConfirmation confirmation={props.confirm} /> : null;
161 const trailing =
162 props.trailing || confirm || props.actions?.length ? (
163 <View className="flex-row items-center gap-2">
164 {props.actions?.map((action) => (
165 <IconButton
166 key={action.accessibilityLabel}
167 icon={action.icon}
168 accessibilityLabel={action.accessibilityLabel}
169 disabled={action.disabled || action.loading}
170 onPress={action.onPress}
171 />
172 ))}
173 {props.trailing}
174 {confirm}
175 </View>
176 ) : undefined;
177​
178 return (
179 <View className="flex-row items-center gap-3 px-4 pt-3 pb-3">
180 {close}
181 <View className="min-w-0 flex-1 gap-1">
182 <AppText numberOfLines={1} className="text-base font-t3-bold text-foreground">
183 {props.title}
184 </AppText>
185 {props.subtitle ? (
186 <AppText
187 numberOfLines={1}
188 ellipsizeMode="middle"
189 className="text-xs text-foreground-muted"
190 >
191 {props.subtitle}
192 </AppText>
193 ) : null}
194 </View>
195 {trailing}
196 </View>
197 );