From 0d6651b0da19e7ea9be1969399298f4f0591d707 Mon Sep 17 00:00:00 2001 From: Apoorv Mishra Date: Fri, 3 Feb 2023 09:41:24 +0530 Subject: [PATCH] Scroll children into view upon expansion (#4812) * feat: smoothly scroll children into view * fix: disable smooth scroll and throttling --- app/components/DocumentExplorer.tsx | 32 +++++++++++++++++- app/components/DocumentExplorerNode.tsx | 45 ++++++++++--------------- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/app/components/DocumentExplorer.tsx b/app/components/DocumentExplorer.tsx index c2df49ad4..82b0ab1cb 100644 --- a/app/components/DocumentExplorer.tsx +++ b/app/components/DocumentExplorer.tsx @@ -1,11 +1,12 @@ import FuzzySearch from "fuzzy-search"; -import { includes, difference, concat, filter } from "lodash"; +import { includes, difference, concat, filter, map, fill } from "lodash"; import { observer } from "mobx-react"; import { StarredIcon, DocumentIcon } from "outline-icons"; import * as React from "react"; import { useTranslation } from "react-i18next"; import AutoSizer from "react-virtualized-auto-sizer"; import { FixedSizeList as List } from "react-window"; +import scrollIntoView from "smooth-scroll-into-view-if-needed"; import styled, { useTheme } from "styled-components"; import breakpoint from "styled-components-breakpoint"; import { NavigationNode } from "@shared/types"; @@ -50,6 +51,9 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) { const [nodes, setNodes] = React.useState([]); const [activeNode, setActiveNode] = React.useState(0); const [expandedNodes, setExpandedNodes] = React.useState([]); + const [itemRefs, setItemRefs] = React.useState< + React.RefObject[] + >([]); const inputSearchRef = React.useRef( null @@ -86,10 +90,31 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) { setNodes(results); }, [searchTerm, items, searchIndex]); + React.useEffect(() => { + setItemRefs((itemRefs) => + map( + fill(Array(items.length), 0), + (_, i) => itemRefs[i] || React.createRef() + ) + ); + }, [items.length]); + React.useEffect(() => { onSelect(selectedNode); }, [selectedNode, onSelect]); + const scrollNodeIntoView = React.useCallback( + (node: number) => { + if (itemRefs[node] && itemRefs[node].current) { + scrollIntoView(itemRefs[node].current as HTMLSpanElement, { + behavior: "auto", + block: "center", + }); + } + }, + [itemRefs] + ); + const handleSearch = (ev: React.ChangeEvent) => { setSearchTerm(ev.target.value); }; @@ -241,6 +266,7 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) { title={title} depth={node.depth as number} hasChildren={hasChildren(index)} + ref={itemRefs[index]} /> ); }; @@ -262,6 +288,7 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) { case "ArrowDown": { ev.preventDefault(); setActiveNode(next()); + scrollNodeIntoView(next()); break; } case "ArrowUp": { @@ -270,6 +297,7 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) { focusSearchInput(); } else { setActiveNode(prev()); + scrollNodeIntoView(prev()); } break; } @@ -282,6 +310,8 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) { case "ArrowRight": { if (!searchTerm) { toggleCollapse(activeNode); + // let the nodes re-render first and then scroll + setImmediate(() => scrollNodeIntoView(activeNode)); } break; } diff --git a/app/components/DocumentExplorerNode.tsx b/app/components/DocumentExplorerNode.tsx index e3eb6bfd7..1431661b9 100644 --- a/app/components/DocumentExplorerNode.tsx +++ b/app/components/DocumentExplorerNode.tsx @@ -1,7 +1,6 @@ import { observer } from "mobx-react"; import * as React from "react"; import { useTranslation } from "react-i18next"; -import scrollIntoView from "smooth-scroll-into-view-if-needed"; import styled from "styled-components"; import breakpoint from "styled-components-breakpoint"; import Flex from "~/components/Flex"; @@ -23,38 +22,28 @@ type Props = { onClick: (ev: React.MouseEvent) => void; }; -function DocumentExplorerNode({ - selected, - active, - style, - expanded, - icon, - title, - depth, - hasChildren, - onDisclosureClick, - onPointerMove, - onClick, -}: Props) { +function DocumentExplorerNode( + { + selected, + active, + style, + expanded, + icon, + title, + depth, + hasChildren, + onDisclosureClick, + onPointerMove, + onClick, + }: Props, + ref: React.RefObject +) { const { t } = useTranslation(); const OFFSET = 12; const ICON_SIZE = 24; const width = depth ? depth * ICON_SIZE + OFFSET : ICON_SIZE; - const ref = React.useCallback( - (node: HTMLSpanElement | null) => { - if (active && node) { - scrollIntoView(node, { - scrollMode: "if-needed", - behavior: "auto", - block: "nearest", - }); - } - }, - [active] - ); - return (