From cc90c8de1c412c0eff68491f7b4935243f11b940 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 7 Feb 2021 21:51:56 -0800 Subject: [PATCH] feat: Sidebar Improvements (#1862) * wip * refactor behaviorg * stash * simplify --- app/components/Layout.js | 5 +- app/components/Sidebar/Sidebar.js | 220 ++++++++++-------- .../Sidebar/components/CollapseToggle.js | 59 ----- .../Sidebar/components/Collections.js | 2 - .../Sidebar/components/ResizeBorder.js | 15 -- .../Sidebar/components/ResizeHandle.js | 39 ---- app/components/Sidebar/components/Toggle.js | 75 ++++++ app/stores/UiStore.js | 6 +- package.json | 2 +- shared/i18n/locales/en_US/translation.json | 5 +- 10 files changed, 207 insertions(+), 221 deletions(-) delete mode 100644 app/components/Sidebar/components/CollapseToggle.js delete mode 100644 app/components/Sidebar/components/ResizeHandle.js create mode 100644 app/components/Sidebar/components/Toggle.js diff --git a/app/components/Layout.js b/app/components/Layout.js index 4c96b26a..74b90bea 100644 --- a/app/components/Layout.js +++ b/app/components/Layout.js @@ -76,7 +76,6 @@ class Layout extends React.Component { @keydown("shift+/") handleOpenKeyboardShortcuts() { - if (this.props.ui.editMode) return; this.keyboardShortcutsOpen = true; } @@ -86,7 +85,6 @@ class Layout extends React.Component { @keydown(["t", "/", `${meta}+k`]) goToSearch(ev: SyntheticEvent<>) { - if (this.props.ui.editMode) return; ev.preventDefault(); ev.stopPropagation(); this.redirectTo = searchUrl(); @@ -94,7 +92,6 @@ class Layout extends React.Component { @keydown("d") goToDashboard() { - if (this.props.ui.editMode) return; this.redirectTo = homeUrl(); } @@ -102,7 +99,7 @@ class Layout extends React.Component { const { auth, t, ui } = this.props; const { user, team } = auth; const showSidebar = auth.authenticated && user && team; - const sidebarCollapsed = ui.editMode || ui.sidebarCollapsed; + const sidebarCollapsed = ui.isEditing || ui.sidebarCollapsed; if (auth.isSuspended) return ; if (this.redirectTo) return ; diff --git a/app/components/Sidebar/Sidebar.js b/app/components/Sidebar/Sidebar.js index 79e4d3d0..296be79b 100644 --- a/app/components/Sidebar/Sidebar.js +++ b/app/components/Sidebar/Sidebar.js @@ -3,29 +3,37 @@ import { observer } from "mobx-react"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { Portal } from "react-portal"; -import { withRouter } from "react-router-dom"; -import type { Location } from "react-router-dom"; +import { useLocation } from "react-router-dom"; import styled, { useTheme } from "styled-components"; import breakpoint from "styled-components-breakpoint"; import Fade from "components/Fade"; import Flex from "components/Flex"; -import CollapseToggle, { - Button as CollapseButton, -} from "./components/CollapseToggle"; import ResizeBorder from "./components/ResizeBorder"; -import ResizeHandle from "./components/ResizeHandle"; +import Toggle, { ToggleButton, Positioner } from "./components/Toggle"; import usePrevious from "hooks/usePrevious"; import useStores from "hooks/useStores"; let firstRender = true; -let BOUNCE_ANIMATION_MS = 250; +let ANIMATION_MS = 250; type Props = { children: React.Node, - location: Location, }; -const useResize = ({ width, minWidth, maxWidth, setWidth }) => { +function Sidebar({ children }: Props) { + const [isCollapsing, setCollapsing] = React.useState(false); + const theme = useTheme(); + const { t } = useTranslation(); + const { ui } = useStores(); + const location = useLocation(); + const previousLocation = usePrevious(location); + + const width = ui.sidebarWidth; + const collapsed = ui.isEditing || ui.sidebarCollapsed; + const maxWidth = theme.sidebarMaxWidth; + const minWidth = theme.sidebarMinWidth + 16; // padding + const setWidth = ui.setSidebarWidth; + const [offset, setOffset] = React.useState(0); const [isAnimating, setAnimating] = React.useState(false); const [isResizing, setResizing] = React.useState(false); @@ -38,24 +46,45 @@ const useResize = ({ width, minWidth, maxWidth, setWidth }) => { // this is simple because the sidebar is always against the left edge const width = Math.min(event.pageX - offset, maxWidth); - setWidth(width); + const isSmallerThanCollapsePoint = width < minWidth / 2; + + if (isSmallerThanCollapsePoint) { + setWidth(theme.sidebarCollapsedWidth); + } else { + setWidth(width); + } }, - [offset, maxWidth, setWidth] + [theme, offset, minWidth, maxWidth, setWidth] ); - const handleStopDrag = React.useCallback(() => { - setResizing(false); + const handleStopDrag = React.useCallback( + (event: MouseEvent) => { + setResizing(false); - if (isSmallerThanMinimum) { - setWidth(minWidth); - setAnimating(true); - } else { - setWidth(width); - } - }, [isSmallerThanMinimum, minWidth, width, setWidth]); + if (document.activeElement) { + document.activeElement.blur(); + } - const handleStartDrag = React.useCallback( - (event) => { + if (isSmallerThanMinimum) { + const isSmallerThanCollapsePoint = width < minWidth / 2; + + if (isSmallerThanCollapsePoint) { + setAnimating(false); + setCollapsing(true); + ui.collapseSidebar(); + } else { + setWidth(minWidth); + setAnimating(true); + } + } else { + setWidth(width); + } + }, + [ui, isSmallerThanMinimum, minWidth, width, setWidth] + ); + + const handleMouseDown = React.useCallback( + (event: MouseEvent) => { setOffset(event.pageX - width); setResizing(true); setAnimating(false); @@ -65,10 +94,19 @@ const useResize = ({ width, minWidth, maxWidth, setWidth }) => { React.useEffect(() => { if (isAnimating) { - setTimeout(() => setAnimating(false), BOUNCE_ANIMATION_MS); + setTimeout(() => setAnimating(false), ANIMATION_MS); } }, [isAnimating]); + React.useEffect(() => { + if (isCollapsing) { + setTimeout(() => { + setWidth(minWidth); + setCollapsing(false); + }, ANIMATION_MS); + } + }, [setWidth, minWidth, isCollapsing]); + React.useEffect(() => { if (isResizing) { document.addEventListener("mousemove", handleDrag); @@ -81,32 +119,6 @@ const useResize = ({ width, minWidth, maxWidth, setWidth }) => { }; }, [isResizing, handleDrag, handleStopDrag]); - return { isAnimating, isSmallerThanMinimum, isResizing, handleStartDrag }; -}; - -function Sidebar({ location, children }: Props) { - const theme = useTheme(); - const { t } = useTranslation(); - const { ui } = useStores(); - const previousLocation = usePrevious(location); - - const width = ui.sidebarWidth; - const maxWidth = theme.sidebarMaxWidth; - const minWidth = theme.sidebarMinWidth + 16; // padding - const collapsed = ui.editMode || ui.sidebarCollapsed; - - const { - isAnimating, - isSmallerThanMinimum, - isResizing, - handleStartDrag, - } = useResize({ - width, - minWidth, - maxWidth, - setWidth: ui.setSidebarWidth, - }); - const handleReset = React.useCallback(() => { ui.setSidebarWidth(theme.sidebarWidth); }, [ui, theme.sidebarWidth]); @@ -124,49 +136,60 @@ function Sidebar({ location, children }: Props) { const style = React.useMemo( () => ({ width: `${width}px`, - left: - collapsed && !ui.mobileSidebarVisible - ? `${-width + theme.sidebarCollapsedWidth}px` - : 0, }), - [width, collapsed, theme.sidebarCollapsedWidth, ui.mobileSidebarVisible] + [width] + ); + + const toggleStyle = React.useMemo( + () => ({ + right: "auto", + marginLeft: `${collapsed ? theme.sidebarCollapsedWidth : width}px`, + }), + [width, theme.sidebarCollapsedWidth, collapsed] ); const content = ( - - {!isResizing && ( - + + {ui.mobileSidebarVisible && ( + + + + + + )} + {children} + + {ui.sidebarCollapsed && !ui.isEditing && ( + + )} + + {!ui.isEditing && ( + )} - {ui.mobileSidebarVisible && ( - - - - - - )} - - {children} - {!ui.sidebarCollapsed && ( - - - - )} - + ); // Fade in the sidebar on first render after page load @@ -195,29 +218,36 @@ const Container = styled(Flex)` bottom: 0; width: 100%; background: ${(props) => props.theme.sidebarBackground}; - transition: box-shadow, 100ms, ease-in-out, margin-left 100ms ease-out, - left 100ms ease-out, + transition: box-shadow 100ms ease-in-out, transform 100ms ease-out, ${(props) => props.theme.backgroundTransition} ${(props) => - props.$isAnimating ? `,width ${BOUNCE_ANIMATION_MS}ms ease-out` : ""}; - margin-left: ${(props) => (props.$mobileSidebarVisible ? 0 : "-100%")}; + props.$isAnimating ? `,width ${ANIMATION_MS}ms ease-out` : ""}; + transform: translateX( + ${(props) => (props.$mobileSidebarVisible ? 0 : "-100%")} + ); z-index: ${(props) => props.theme.depths.sidebar}; max-width: 70%; min-width: 280px; + ${Positioner} { + display: none; + } + @media print { display: none; - left: 0; + transform: none; } ${breakpoint("tablet")` margin: 0; z-index: 3; min-width: 0; + transform: translateX(${(props) => + props.$collapsed ? "calc(-100% + 16px)" : 0}); &:hover, &:focus-within { - left: 0 !important; + transform: none; box-shadow: ${(props) => props.$collapsed ? "rgba(0, 0, 0, 0.2) 1px 0 4px" @@ -225,11 +255,11 @@ const Container = styled(Flex)` ? "rgba(0, 0, 0, 0.1) inset -1px 0 2px" : "none"}; - & ${CollapseButton} { - opacity: .75; + ${Positioner} { + display: block; } - & ${CollapseButton}:hover { + ${ToggleButton} { opacity: 1; } } @@ -241,4 +271,4 @@ const Container = styled(Flex)` `}; `; -export default withRouter(observer(Sidebar)); +export default observer(Sidebar); diff --git a/app/components/Sidebar/components/CollapseToggle.js b/app/components/Sidebar/components/CollapseToggle.js deleted file mode 100644 index d7dd88a8..00000000 --- a/app/components/Sidebar/components/CollapseToggle.js +++ /dev/null @@ -1,59 +0,0 @@ -// @flow -import { NextIcon, BackIcon } from "outline-icons"; -import * as React from "react"; -import { useTranslation } from "react-i18next"; -import styled from "styled-components"; -import Tooltip from "components/Tooltip"; -import { meta } from "utils/keyboard"; - -type Props = {| - collapsed: boolean, - onClick?: (event: SyntheticEvent<>) => void, -|}; - -function CollapseToggle({ collapsed, ...rest }: Props) { - const { t } = useTranslation(); - - return ( - - - - ); -} - -export const Button = styled.button` - display: block; - position: absolute; - top: 28px; - right: 8px; - border: 0; - width: 24px; - height: 24px; - z-index: 1; - font-weight: 600; - color: ${(props) => props.theme.sidebarText}; - background: transparent; - transition: opacity 100ms ease-in-out; - border-radius: 4px; - opacity: 0; - cursor: pointer; - padding: 0; - - &:hover { - color: ${(props) => props.theme.white}; - background: ${(props) => props.theme.primary}; - } -`; - -export default CollapseToggle; diff --git a/app/components/Sidebar/components/Collections.js b/app/components/Sidebar/components/Collections.js index 386407b1..eefe5672 100644 --- a/app/components/Sidebar/components/Collections.js +++ b/app/components/Sidebar/components/Collections.js @@ -42,8 +42,6 @@ class Collections extends React.Component { @keydown("n") goToNewDocument() { - if (this.props.ui.editMode) return; - const { activeCollectionId } = this.props.ui; if (!activeCollectionId) return; diff --git a/app/components/Sidebar/components/ResizeBorder.js b/app/components/Sidebar/components/ResizeBorder.js index 979907c5..528c519a 100644 --- a/app/components/Sidebar/components/ResizeBorder.js +++ b/app/components/Sidebar/components/ResizeBorder.js @@ -1,6 +1,5 @@ // @flow import styled from "styled-components"; -import ResizeHandle from "./ResizeHandle"; const ResizeBorder = styled.div` position: absolute; @@ -9,20 +8,6 @@ const ResizeBorder = styled.div` right: -6px; width: 12px; cursor: ew-resize; - - ${(props) => - props.$isResizing && - ` - ${ResizeHandle} { - opacity: 1; - } - `} - - &:hover { - ${ResizeHandle} { - opacity: 1; - } - } `; export default ResizeBorder; diff --git a/app/components/Sidebar/components/ResizeHandle.js b/app/components/Sidebar/components/ResizeHandle.js deleted file mode 100644 index c85c9749..00000000 --- a/app/components/Sidebar/components/ResizeHandle.js +++ /dev/null @@ -1,39 +0,0 @@ -// @flow -import styled from "styled-components"; -import breakpoint from "styled-components-breakpoint"; - -const ResizeHandle = styled.button` - opacity: 0; - transition: opacity 100ms ease-in-out; - transform: translateY(-50%); - position: absolute; - top: 50%; - height: 40px; - right: -10px; - width: 8px; - padding: 0; - border: 0; - background: ${(props) => props.theme.sidebarBackground}; - border-radius: 8px; - pointer-events: none; - - &:after { - content: ""; - position: absolute; - top: -24px; - bottom: -24px; - left: -12px; - right: -12px; - } - - &:active { - background: ${(props) => props.theme.sidebarText}; - } - - ${breakpoint("tablet")` - pointer-events: all; - cursor: ew-resize; - `} -`; - -export default ResizeHandle; diff --git a/app/components/Sidebar/components/Toggle.js b/app/components/Sidebar/components/Toggle.js new file mode 100644 index 00000000..4533c520 --- /dev/null +++ b/app/components/Sidebar/components/Toggle.js @@ -0,0 +1,75 @@ +// @flow +import * as React from "react"; +import styled from "styled-components"; +import breakpoint from "styled-components-breakpoint"; + +type Props = { + direction: "left" | "right", + style?: Object, + onClick?: () => any, +}; + +const Toggle = React.forwardRef( + ({ direction = "left", onClick, style }: Props, ref) => { + return ( + + + + + + + + + ); + } +); + +export const ToggleButton = styled.button` + opacity: 0; + background: none; + transition: opacity 100ms ease-in-out; + transform: translateY(-50%) + scaleX(${(props) => (props.$direction === "left" ? 1 : -1)}); + position: absolute; + top: 50vh; + padding: 8px; + border: 0; + pointer-events: none; + color: ${(props) => props.theme.divider}; + + &:active { + color: ${(props) => props.theme.sidebarText}; + } + + ${breakpoint("tablet")` + pointer-events: all; + cursor: pointer; + `} +`; + +export const Positioner = styled.div` + z-index: 2; + position: absolute; + top: 0; + bottom: 0; + right: -30px; + width: 30px; + + &:hover ${ToggleButton}, &:focus-within ${ToggleButton} { + opacity: 1; + } +`; + +export default Toggle; diff --git a/app/stores/UiStore.js b/app/stores/UiStore.js index f095feeb..e1d2ef59 100644 --- a/app/stores/UiStore.js +++ b/app/stores/UiStore.js @@ -21,7 +21,7 @@ class UiStore { @observable activeDocumentId: ?string; @observable activeCollectionId: ?string; @observable progressBarVisible: boolean = false; - @observable editMode: boolean = false; + @observable isEditing: boolean = false; @observable tocVisible: boolean = false; @observable mobileSidebarVisible: boolean = false; @observable sidebarWidth: number; @@ -151,12 +151,12 @@ class UiStore { @action enableEditMode = () => { - this.editMode = true; + this.isEditing = true; }; @action disableEditMode = () => { - this.editMode = false; + this.isEditing = false; }; @action diff --git a/package.json b/package.json index 40195c2e..b0b62137 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "react-waypoint": "^9.0.2", "react-window": "^1.8.6", "reakit": "^1.3.4", - "rich-markdown-editor": "^11.1.6", + "rich-markdown-editor": "^11.2.0-0", "semver": "^7.3.2", "sequelize": "^6.3.4", "sequelize-cli": "^6.2.0", diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index 20aaf406..3d538638 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -86,8 +86,6 @@ "Change Language": "Change Language", "Dismiss": "Dismiss", "Keyboard shortcuts": "Keyboard shortcuts", - "Expand": "Expand", - "Collapse": "Collapse", "New collection": "New collection", "Collections": "Collections", "Untitled": "Untitled", @@ -110,7 +108,8 @@ "Export Data": "Export Data", "Integrations": "Integrations", "Installation": "Installation", - "Resize sidebar": "Resize sidebar", + "Expand": "Expand", + "Collapse": "Collapse", "Unstar": "Unstar", "Star": "Star", "Appearance": "Appearance",