fix(mobile): keep in-note search on one row

The search field, match count, and previous, next, and close controls sit on a single bar.
This commit is contained in:
tianma-if committed 2026-09-26 18:26:11 +08:00
1 parent 76bfb958bf
commit aaf3dacfb6
4 files changed
+101 -46

No files matched your search

+1
View File
@@ -32,6 +32,7 @@ export const Camera = createIcon("camera");
export const Check = createIcon("check");
export const CheckSquare = createIcon("check-square");
export const ChevronDown = createIcon("chevron-down");
export const ChevronUp = createIcon("chevron-up");
export const ChevronLeft = createIcon("chevron-left");
export const ChevronRight = createIcon("chevron-right");
export const Code = createIcon("code");
+58 -41
View File
@@ -11,7 +11,7 @@ import { Image as RNImage, KeyboardAvoidingView, Platform, ScrollView, StyleShee
import { Modal } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { SvgXml } from "react-native-svg";
import { ActivityIndicator, ChevronDown, ChevronLeft, ChevronRight, Copy, Download, History, MoreHorizontal, Pencil, RotateCcw, Search, Share2, Sparkles, Tag, Trash2, X } from "../components/icons";
import { ActivityIndicator, ChevronDown, ChevronLeft, ChevronUp, Copy, Download, History, MoreHorizontal, Pencil, RotateCcw, Search, Share2, Sparkles, Tag, Trash2, X } from "../components/icons";
import { Alert, Pressable, Text, TextInput } from "../components/LocalizedText";
import LocalTiptapEditor, { type LocalTiptapEditorRef } from "../components/LocalTiptapEditor";
import { MobileAiAssistantModal } from "../components/MobileAiAssistantModal";
@@ -362,13 +362,6 @@ const DetailActionSheetItem = ({ danger = false, disabled = false, icon, label,
</Pressable>
);
const DetailActionButton = ({ children, disabled = false, label, onPress }: { children: ReactNode; disabled?: boolean; label: string; onPress: () => void }) => (
<Pressable disabled={disabled} onPress={onPress} style={[styles.actionButton, disabled && styles.buttonDisabled]}>
{children}
<Text style={styles.actionButtonText}>{label}</Text>
</Pressable>
);
const HighlightedMetadataText = ({
activeIndex,
matchOffset,
@@ -733,6 +726,13 @@ export const MemoDetailModal = ({
}
setActiveMatchIndex((current) => getNextMobileNoteSearchIndex(current, direction, searchMatchCount));
};
const closeNoteSearch = () => {
setSearchOpen(false);
setSearchQuery("");
setBodySearchMatchCount(0);
setActiveMatchIndex(0);
safeDomCall(() => viewerRef.current?.search("", -1));
};
const closeActionsAndRun = (action: () => void) => {
setActionsOpen(false);
@@ -1183,41 +1183,58 @@ export const MemoDetailModal = ({
</Text>
{searchOpen ? (
<View style={styles.noteSearchPanel}>
<View style={styles.searchBox}>
<Search color="#64748b" size={18} />
<TextInput
accessibilityLabel="在当前笔记内搜索"
autoCapitalize="none"
autoCorrect={false}
onChangeText={(value) => {
setSearchQuery(value);
setBodySearchMatchCount(0);
setActiveMatchIndex(0);
}}
placeholder="在当前笔记内搜索"
placeholderTextColor="#94a3b8"
style={styles.searchInput}
value={searchQuery}
/>
<Text style={[styles.noteSearchCount, searchQuery.trim() && searchMatchCount === 0 && styles.noteSearchCountEmpty]}>{searchMatchLabel}</Text>
</View>
<View style={styles.richEditorSearchActions}>
<DetailActionButton disabled={searchMatchCount === 0} label="上一个搜索结果" onPress={() => moveSearchMatch(-1)}>
<ChevronLeft color={searchMatchCount === 0 ? "#cbd5e1" : "#0f172a"} size={16} />
</DetailActionButton>
<DetailActionButton disabled={searchMatchCount === 0} label="下一个搜索结果" onPress={() => moveSearchMatch(1)}>
<ChevronRight color={searchMatchCount === 0 ? "#cbd5e1" : "#0f172a"} size={16} />
</DetailActionButton>
<DetailActionButton label="关闭搜索" onPress={() => {
setSearchOpen(false);
setSearchQuery("");
<Search color="#64748b" size={16} />
<TextInput
accessibilityLabel="在当前笔记内搜索"
autoCapitalize="none"
autoCorrect={false}
onChangeText={(value) => {
setSearchQuery(value);
setBodySearchMatchCount(0);
setActiveMatchIndex(0);
safeDomCall(() => viewerRef.current?.search("", -1));
}}>
<X color="#0f172a" size={16} />
</DetailActionButton>
</View>
}}
onSubmitEditing={() => moveSearchMatch(1)}
placeholder="搜索"
placeholderTextColor="#94a3b8"
returnKeyType="search"
style={styles.noteSearchInput}
value={searchQuery}
/>
<Text
accessibilityLiveRegion="polite"
style={[styles.noteSearchCount, searchQuery.trim() && searchMatchCount === 0 && styles.noteSearchCountEmpty]}
>
{searchMatchLabel}
</Text>
<Pressable
accessibilityLabel="上一个搜索结果"
accessibilityRole="button"
disabled={searchMatchCount === 0}
hitSlop={4}
onPress={() => moveSearchMatch(-1)}
style={[styles.noteSearchIconButton, searchMatchCount === 0 && styles.buttonDisabled]}
>
<ChevronUp color={searchMatchCount === 0 ? "#cbd5e1" : "#0f172a"} size={18} />
</Pressable>
<Pressable
accessibilityLabel="下一个搜索结果"
accessibilityRole="button"
disabled={searchMatchCount === 0}
hitSlop={4}
onPress={() => moveSearchMatch(1)}
style={[styles.noteSearchIconButton, searchMatchCount === 0 && styles.buttonDisabled]}
>
<ChevronDown color={searchMatchCount === 0 ? "#cbd5e1" : "#0f172a"} size={18} />
</Pressable>
<Pressable
accessibilityLabel="关闭搜索"
accessibilityRole="button"
hitSlop={4}
onPress={closeNoteSearch}
style={styles.noteSearchIconButton}
>
<X color="#0f172a" size={16} />
</Pressable>
</View>
) : null}
<View style={styles.detailDivider} />
+28 -5
View File
@@ -1949,18 +1949,41 @@ const baseWorkspaceStyles = StyleSheet.create({
marginTop: 20,
},
noteSearchPanel: {
backgroundColor: "#f8fafc",
alignItems: "center",
backgroundColor: "#ffffff",
borderColor: "#e2e8f0",
borderRadius: 8,
borderWidth: 1,
gap: 10,
marginTop: 14,
padding: 10,
flexDirection: "row",
gap: 2,
marginTop: 12,
minHeight: 40,
paddingLeft: 10,
paddingRight: 2,
},
noteSearchInput: {
color: "#0f172a",
flex: 1,
fontSize: 15,
minHeight: 36,
minWidth: 0,
paddingVertical: 0,
},
noteSearchIconButton: {
alignItems: "center",
flexShrink: 0,
height: 36,
justifyContent: "center",
width: 36,
},
noteSearchCount: {
color: "#64748b",
flexShrink: 0,
fontSize: 12,
fontWeight: "800",
fontVariant: ["tabular-nums"],
fontWeight: "700",
minWidth: 44,
textAlign: "right",
},
noteSearchCountEmpty: {
color: "#b91c1c",
+14
View File
@@ -128,6 +128,20 @@ describe("mobile app scope", () => {
expect(localTiptapEditorSource).toContain("scrollEditorPositionIntoView(editor, match.from");
});
test("keeps in-note search on one icon row", () => {
expect(memoDetailSource).toContain('accessibilityLabel="上一个搜索结果"');
expect(memoDetailSource).toContain('accessibilityLabel="下一个搜索结果"');
expect(memoDetailSource).toContain('accessibilityLabel="关闭搜索"');
expect(memoDetailSource).not.toContain("label=\"上一个搜索结果\"");
expect(memoDetailSource).not.toContain("label=\"下一个搜索结果\"");
expect(memoDetailSource).not.toContain("label=\"关闭搜索\"");
const stylesSource = readFileSync(
new URL("../apps/mobile/src/screens/workspace-styles.ts", import.meta.url),
"utf8",
);
expect(stylesSource).toMatch(/noteSearchPanel: \{[\s\S]*?flexDirection: "row"/);
});
test("keeps the Android editor caret visible while the keyboard viewport changes", () => {
expect(workspaceEditorsSource).toContain("KeyboardAvoidingView");
expect(workspaceEditorsSource).toContain('enabled={Platform.OS === "android"}');