From 99a6f1b4fa24899b6844fc46fd57f0d5d114165b Mon Sep 17 00:00:00 2001 From: codedogQBY <1369175442@qq.com> Date: Sat, 13 Jun 2026 05:58:06 +0800 Subject: [PATCH] fix(mobile): navigate grouped toc items --- .../src/screens/reader/TOCTreeItem.tsx | 21 +++-- packages/core/src/reader/index.ts | 3 + packages/core/src/reader/toc.test.ts | 78 +++++++++++++++++++ packages/core/src/reader/toc.ts | 13 ++++ 4 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 packages/core/src/reader/toc.test.ts create mode 100644 packages/core/src/reader/toc.ts diff --git a/packages/app-expo/src/screens/reader/TOCTreeItem.tsx b/packages/app-expo/src/screens/reader/TOCTreeItem.tsx index 806776c3..1e5d4fd3 100644 --- a/packages/app-expo/src/screens/reader/TOCTreeItem.tsx +++ b/packages/app-expo/src/screens/reader/TOCTreeItem.tsx @@ -6,6 +6,7 @@ import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; import { ChevronDownIcon, ChevronRightIcon } from "@/components/ui/Icon"; import { type ThemeColors, fontSize, fontWeight, radius, useColors } from "@/styles/theme"; +import { getFirstTocHref } from "@readany/core/reader"; import type { TOCItem } from "@readany/core/types"; export function TOCTreeItem({ @@ -21,7 +22,9 @@ export function TOCTreeItem({ }) { const colors = useColors(); const tocS = makeTocStyles(colors); - const hasChildren = item.subitems && item.subitems.length > 0; + const children = item.subitems ?? []; + const hasChildren = children.length > 0; + const targetHref = getFirstTocHref(item); const isCurrent = item.title === currentChapter; const hasCurrentChild = (items: TOCItem[]): boolean => { for (const child of items) { @@ -30,20 +33,28 @@ export function TOCTreeItem({ } return false; }; - const shouldExpand = hasChildren && hasCurrentChild(item.subitems!); + const shouldExpand = hasChildren && hasCurrentChild(children); const [expanded, setExpanded] = useState(shouldExpand); + const handlePress = () => { + if (targetHref) { + onSelect(targetHref); + return; + } + + if (hasChildren) setExpanded((value) => !value); + }; return ( item.href && onSelect(item.href)} + onPress={handlePress} activeOpacity={0.7} > {hasChildren ? ( setExpanded(!expanded)} + onPress={() => setExpanded((value) => !value)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} > {expanded ? ( @@ -61,7 +72,7 @@ export function TOCTreeItem({ {expanded && hasChildren && ( - {item.subitems!.map((child) => ( + {children.map((child) => ( { + it("returns the current item href when present", () => { + expect( + getFirstTocHref({ + id: "chapter-1", + title: "Chapter 1", + level: 0, + href: "chapter-1.xhtml", + subitems: [ + { + id: "chapter-1-1", + title: "Chapter 1.1", + level: 1, + href: "chapter-1-1.xhtml", + }, + ], + }), + ).toBe("chapter-1.xhtml"); + }); + + it("falls back to the first descendant href for grouping nodes", () => { + expect( + getFirstTocHref({ + id: "volume-1", + title: "Volume 1", + level: 0, + subitems: [ + { + id: "part-1", + title: "Part 1", + level: 1, + subitems: [ + { + id: "chapter-1", + title: "Chapter 1", + level: 2, + href: "text/chapter-1.xhtml#start", + }, + ], + }, + ], + }), + ).toBe("text/chapter-1.xhtml#start"); + }); + + it("ignores blank href values", () => { + expect( + getFirstTocHref({ + id: "volume-1", + title: "Volume 1", + level: 0, + href: " ", + subitems: [ + { + id: "chapter-1", + title: "Chapter 1", + level: 1, + href: "chapter-1.xhtml", + }, + ], + }), + ).toBe("chapter-1.xhtml"); + }); + + it("returns null when no item in the branch can be opened", () => { + expect( + getFirstTocHref({ + id: "volume-1", + title: "Volume 1", + level: 0, + subitems: [{ id: "part-1", title: "Part 1", level: 1 }], + }), + ).toBeNull(); + }); +}); diff --git a/packages/core/src/reader/toc.ts b/packages/core/src/reader/toc.ts new file mode 100644 index 00000000..c45baf6c --- /dev/null +++ b/packages/core/src/reader/toc.ts @@ -0,0 +1,13 @@ +import type { TOCItem } from "../types"; + +export function getFirstTocHref(item: TOCItem | null | undefined): string | null { + const href = item?.href?.trim(); + if (href) return href; + + for (const child of item?.subitems ?? []) { + const childHref = getFirstTocHref(child); + if (childHref) return childHref; + } + + return null; +}