feat: Automatically scroll to active item in sidebar (#1858)
This commit is contained in:
@@ -21,7 +21,6 @@ type Props = {|
|
||||
canUpdate: boolean,
|
||||
collection?: Collection,
|
||||
activeDocument: ?Document,
|
||||
activeDocumentRef?: (?HTMLElement) => void,
|
||||
prefetchDocument: (documentId: string) => Promise<void>,
|
||||
depth: number,
|
||||
index: number,
|
||||
@@ -33,7 +32,6 @@ function DocumentLink({
|
||||
canUpdate,
|
||||
collection,
|
||||
activeDocument,
|
||||
activeDocumentRef,
|
||||
prefetchDocument,
|
||||
depth,
|
||||
index,
|
||||
@@ -213,7 +211,6 @@ function DocumentLink({
|
||||
<div ref={dropToReparent}>
|
||||
<DropToImport documentId={node.id} activeClassName="activeDropZone">
|
||||
<SidebarLink
|
||||
innerRef={isActiveDocument ? activeDocumentRef : undefined}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
to={{
|
||||
pathname: node.url,
|
||||
|
||||
105
app/components/Sidebar/components/NavLink.js
Normal file
105
app/components/Sidebar/components/NavLink.js
Normal file
@@ -0,0 +1,105 @@
|
||||
// @flow
|
||||
// ref: https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/NavLink.js
|
||||
|
||||
// This file is pulled almost 100% from react-router with the addition of one
|
||||
// thing, automatic scroll to the active link. It's worth the copy paste because
|
||||
// it avoids recalculating the link match again.
|
||||
import { createLocation } from "history";
|
||||
import * as React from "react";
|
||||
import {
|
||||
__RouterContext as RouterContext,
|
||||
matchPath,
|
||||
type Location,
|
||||
} from "react-router";
|
||||
import { Link } from "react-router-dom";
|
||||
import scrollIntoView from "smooth-scroll-into-view-if-needed";
|
||||
|
||||
const resolveToLocation = (to, currentLocation) =>
|
||||
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 <Link> 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 <Link ref={linkRef} {...props} />;
|
||||
};
|
||||
|
||||
export default NavLink;
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
@@ -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 (
|
||||
<StyledNavLink
|
||||
<Link
|
||||
$isActiveDrop={isActiveDrop}
|
||||
activeStyle={isActiveDrop ? activeFontWeightOnly : activeStyle}
|
||||
activeStyle={isActiveDrop ? activeDropStyle : activeStyle}
|
||||
style={active ? activeStyle : style}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
@@ -81,13 +76,12 @@ function SidebarLink({
|
||||
to={to}
|
||||
as={to ? undefined : href ? "a" : "div"}
|
||||
href={href}
|
||||
ref={innerRef}
|
||||
className={className}
|
||||
>
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
<Label>{label}</Label>
|
||||
{menu && <Actions showActions={showActions}>{menu}</Actions>}
|
||||
</StyledNavLink>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user