diff --git a/.changeset/slow-masks-agree.md b/.changeset/slow-masks-agree.md new file mode 100644 index 000000000..e2887fee4 --- /dev/null +++ b/.changeset/slow-masks-agree.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Clicking an active TOC item toggles its descendants diff --git a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx index 3f5332893..f9476acbb 100644 --- a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx @@ -25,6 +25,26 @@ export function ToggleableLinkItem( const currentPagePath = useCurrentPagePath(); const isActive = pathnames.some((pathname) => pathname === currentPagePath); + const defaultIsOpen = + isActive || pathnames.some((pathname) => currentPagePath.startsWith(`${pathname}/`)); + const [isOpen, setIsOpen] = React.useState(defaultIsOpen); + const hasBeenToggled = useRef(false); + + // Update the visibility of the children if one of the descendants becomes active. + React.useEffect(() => { + if (defaultIsOpen && !hasBeenToggled.current) { + setIsOpen(defaultIsOpen); + } + }, [defaultIsOpen]); + + const handleToggle = (newState: boolean | ((prev: boolean) => boolean)) => { + hasBeenToggled.current = true; + if (typeof newState === 'function') { + setIsOpen(newState); + } else { + setIsOpen(newState); + } + }; if (!descendants) { return ( @@ -35,15 +55,15 @@ export function ToggleableLinkItem( } return ( - currentPagePath.startsWith(`${pathname}/`)) - } - > + {({ descendants, toggler }) => ( <> - + handleToggle(!isOpen)} + > {children} {toggler} @@ -57,11 +77,20 @@ export function ToggleableLinkItem( function LinkItem( props: Pick & { isActive: boolean; + onActiveClick?: () => void; } ) { - const { isActive, href, insights, children } = props; + const { isActive, href, insights, children, onActiveClick } = props; const anchorRef = useRef(null); useScrollToActiveTOCItem({ anchorRef, isActive }); + + const handleClick = (event: React.MouseEvent) => { + if (isActive && onActiveClick) { + event.preventDefault(); + onActiveClick(); + } + }; + return ( {children} @@ -79,24 +109,17 @@ function LinkItem( } function DescendantsRenderer(props: { - defaultIsOpen: boolean; descendants: React.ReactNode; + isOpen: boolean; + setIsOpen: React.Dispatch>; children: (renderProps: { descendants: React.ReactNode; toggler: React.ReactNode; }) => React.ReactNode; }) { - const { defaultIsOpen, children, descendants } = props; - const [isOpen, setIsOpen] = React.useState(defaultIsOpen); + const { descendants, isOpen, setIsOpen } = props; - // Update the visibility of the children if one of the descendants becomes active. - React.useEffect(() => { - if (defaultIsOpen) { - setIsOpen(defaultIsOpen); - } - }, [defaultIsOpen]); - - return children({ + return props.children({ toggler: (