1import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
2import { BackHandler, Keyboard, type TextInputInstance, View } from "react-native";
3
4import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader";
5import { AndroidAnchoredMenu } from "../../components/AndroidAnchoredMenu";
6import { MaterialSearchField } from "../../components/MaterialSearchField";
7import { useMaterialToolbarLayout } from "../../components/useMaterialToolbarLayout";
8
9/** Keep Files search in the same header row on compact and expanded layouts. */
10export function MaterialFilesHeader(props: {
11 readonly projectName: string;
12 readonly searchQuery: string;
13 readonly onSearchQueryChange: (query: string) => void;
14 readonly onRefresh: () => void;
15 readonly onBack?: () => void;
16 readonly leading?: ReactNode;
17}) {
18 const { paddingTop, paddingBottom } = useMaterialToolbarLayout();
19 const searchRef = useRef<TextInputInstance>(null);
20 const [searchOpen, setSearchOpen] = useState(false);
21 const searching = searchOpen || props.searchQuery.length > 0;
22 const { onSearchQueryChange } = props;
23 const closeSearch = useCallback(() => {
24 onSearchQueryChange("");
25 setSearchOpen(false);
26 Keyboard.dismiss();
27 }, [onSearchQueryChange]);
28
29 useEffect(() => {
30 if (!searching) return;
31 const subscription = BackHandler.addEventListener("hardwareBackPress", () => {
32 closeSearch();
33 return true;
34 });
35 return () => subscription.remove();
36 }, [closeSearch, searching]);
37
38 return (
39 <View>
40 {/* Keep the title/subtitle's natural height, including larger text, while searching. */}
41 <View
42 pointerEvents={searching ? "none" : "auto"}
43 accessibilityElementsHidden={searching}
44 importantForAccessibility={searching ? "no-hide-descendants" : "auto"}
45 style={{ opacity: searching ? 0 : 1 }}
46 >
47 <AndroidScreenHeader
48 title="Files"
49 subtitle={props.projectName}
50 hideBottomBorder
51 onBack={props.onBack}
52 leading={props.leading}
53 actions={[
54 {
55 accessibilityLabel: "Search files",
56 icon: "magnifyingglass",
57 onPress: () => setSearchOpen(true),
58 },
59 ]}
60 trailing={
61 <AndroidAnchoredMenu
62 title="File options"
63 actions={[{ id: "refresh", title: "Refresh files" }]}
64 onPressAction={({ nativeEvent }) => {
65 if (nativeEvent.event === "refresh") props.onRefresh();
66 }}
67 >
68 {(open) => (
69 <AndroidHeaderIconButton
70 accessibilityLabel="File options"
71 icon="ellipsis"
72 onPress={open}
73 />
74 )}
75 </AndroidAnchoredMenu>
76 }
77 />
78 </View>
79 {searching ? (
80 <View className="absolute inset-0 bg-header px-2" style={{ paddingTop, paddingBottom }}>
81 <View className="flex-1 flex-row items-center gap-1">
82 <AndroidHeaderIconButton
83 accessibilityLabel="Close file search"
84 icon="arrow.left"
85 onPress={closeSearch}
86 />
87 <MaterialSearchField
88 inputRef={searchRef}
89 accessibilityLabel="Search files"
90 clearAccessibilityLabel="Clear file search"
91 placeholder="Search files"
92 value={props.searchQuery}
93 onChangeText={onSearchQueryChange}
94 />
95 </View>
96 </View>
97 ) : null}
98 </View>
99 );
100}