From a9d60d288e3474107dfe65aded1e95578e0b317a Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 1 Feb 2021 19:29:54 -0800 Subject: [PATCH] feat: Automatically scroll to active item in sidebar (#1858) --- .../Sidebar/components/DocumentLink.js | 3 - app/components/Sidebar/components/NavLink.js | 105 ++++++++++++++++++ app/components/Sidebar/components/Section.js | 2 +- .../Sidebar/components/SidebarLink.js | 22 ++-- package.json | 3 +- yarn.lock | 2 +- 6 files changed, 117 insertions(+), 20 deletions(-) create mode 100644 app/components/Sidebar/components/NavLink.js diff --git a/app/components/Sidebar/components/DocumentLink.js b/app/components/Sidebar/components/DocumentLink.js index 70546706c..f0bf5e474 100644 --- a/app/components/Sidebar/components/DocumentLink.js +++ b/app/components/Sidebar/components/DocumentLink.js @@ -21,7 +21,6 @@ type Props = {| canUpdate: boolean, collection?: Collection, activeDocument: ?Document, - activeDocumentRef?: (?HTMLElement) => void, prefetchDocument: (documentId: string) => Promise, depth: number, index: number, @@ -33,7 +32,6 @@ function DocumentLink({ canUpdate, collection, activeDocument, - activeDocumentRef, prefetchDocument, depth, index, @@ -213,7 +211,6 @@ function DocumentLink({
+ typeof to === "function" ? to(currentLocation) : to; + +const normalizeToLocation = (to, currentLocation) => { + return typeof to === "string" + ? createLocation(to, null, null, currentLocation) + : to; +}; + +const joinClassnames = (...classnames) => { + return classnames.filter((i) => i).join(" "); +}; + +type Props = {| + activeClassName?: String, + activeStyle?: Object, + className?: string, + exact?: boolean, + isActive?: any, + location?: Location, + strict?: boolean, + style?: Object, + to: string, +|}; + +/** + * A wrapper that knows if it's "active" or not. + */ +const NavLink = ({ + "aria-current": ariaCurrent = "page", + activeClassName = "active", + activeStyle, + className: classNameProp, + exact, + isActive: isActiveProp, + location: locationProp, + strict, + style: styleProp, + to, + ...rest +}: Props) => { + const linkRef = React.useRef(); + const context = React.useContext(RouterContext); + const currentLocation = locationProp || context.location; + const toLocation = normalizeToLocation( + resolveToLocation(to, currentLocation), + currentLocation + ); + const { pathname: path } = toLocation; + // Regex taken from: https://github.com/pillarjs/path-to-regexp/blob/master/index.js#L202 + const escapedPath = path && path.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1"); + + const match = escapedPath + ? matchPath(currentLocation.pathname, { + path: escapedPath, + exact, + strict, + }) + : null; + const isActive = !!(isActiveProp + ? isActiveProp(match, currentLocation) + : match); + + const className = isActive + ? joinClassnames(classNameProp, activeClassName) + : classNameProp; + const style = isActive ? { ...styleProp, ...activeStyle } : styleProp; + + React.useEffect(() => { + if (isActive && linkRef.current) { + scrollIntoView(linkRef.current, { + scrollMode: "if-needed", + behavior: "instant", + }); + } + }, [linkRef, isActive]); + + const props = { + "aria-current": (isActive && ariaCurrent) || null, + className, + style, + to: toLocation, + ...rest, + }; + + return ; +}; + +export default NavLink; diff --git a/app/components/Sidebar/components/Section.js b/app/components/Sidebar/components/Section.js index ab2be4325..b18d275a8 100644 --- a/app/components/Sidebar/components/Section.js +++ b/app/components/Sidebar/components/Section.js @@ -5,7 +5,7 @@ import Flex from "components/Flex"; const Section = styled(Flex)` position: relative; flex-direction: column; - margin: 24px 8px; + margin: 20px 8px; min-width: ${(props) => props.theme.sidebarMinWidth}px; flex-shrink: 0; `; diff --git a/app/components/Sidebar/components/SidebarLink.js b/app/components/Sidebar/components/SidebarLink.js index 9fb5d4b86..29ca5c9d7 100644 --- a/app/components/Sidebar/components/SidebarLink.js +++ b/app/components/Sidebar/components/SidebarLink.js @@ -1,14 +1,10 @@ // @flow import * as React from "react"; -import { - withRouter, - NavLink, - type RouterHistory, - type Match, -} from "react-router-dom"; +import { withRouter, type RouterHistory, type Match } from "react-router-dom"; import styled, { withTheme } from "styled-components"; import breakpoint from "styled-components-breakpoint"; import EventBoundary from "components/EventBoundary"; +import NavLink from "./NavLink"; import { type Theme } from "types"; type Props = { @@ -47,7 +43,6 @@ function SidebarLink({ theme, exact, href, - innerRef, depth, history, match, @@ -66,14 +61,14 @@ function SidebarLink({ ...style, }; - const activeFontWeightOnly = { + const activeDropStyle = { fontWeight: 600, }; return ( - {icon && {icon}} {menu && {menu}} - + ); } @@ -121,7 +115,7 @@ const Actions = styled(EventBoundary)` } `; -const StyledNavLink = styled(NavLink)` +const Link = styled(NavLink)` display: flex; position: relative; text-overflow: ellipsis; @@ -138,7 +132,7 @@ const StyledNavLink = styled(NavLink)` svg { ${(props) => (props.$isActiveDrop ? `fill: ${props.theme.white};` : "")} - transition: fill 50ms + transition: fill 50ms; } &:hover { diff --git a/package.json b/package.json index 758769b54..954703a52 100644 --- a/package.json +++ b/package.json @@ -162,6 +162,7 @@ "slate": "0.45.0", "slate-md-serializer": "5.5.4", "slug": "^1.0.0", + "smooth-scroll-into-view-if-needed": "^1.1.29", "socket.io": "^2.3.0", "socket.io-redis": "^5.4.0", "socketio-auth": "^0.1.1", @@ -211,4 +212,4 @@ "js-yaml": "^3.13.1" }, "version": "0.52.0" -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index ae96b8516..ae9af2e29 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11028,7 +11028,7 @@ slugify@^1.4.0: resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.4.6.tgz#ef288d920a47fb01c2be56b3487b6722f5e34ace" integrity sha512-ZdJIgv9gdrYwhXqxsH9pv7nXxjUEyQ6nqhngRxoAAOlmMGA28FDq5O4/5US4G2/Nod7d1ovNcgURQJ7kHq50KQ== -smooth-scroll-into-view-if-needed@^1.1.27: +smooth-scroll-into-view-if-needed@^1.1.27, smooth-scroll-into-view-if-needed@^1.1.29: version "1.1.29" resolved "https://registry.yarnpkg.com/smooth-scroll-into-view-if-needed/-/smooth-scroll-into-view-if-needed-1.1.29.tgz#4f532d9f0353dbca122e546fb062e7b5e0643734" integrity sha512-UxvIEbmMEqwbw0aZI4SOAtwwkMaLYVION20bDQmazVp3sNb1+WIA5koukqoJizRuAAUANRmcBcrTnodcB7maqw==