From 40ca73e684c636cd144bbc30cc9c06f24d3f79aa Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Thu, 17 Dec 2020 22:26:04 -0800 Subject: [PATCH] feat: Collapsible sidebar (#1721) * wip * styling, add keyboard shortcut * tweak styling --- app/components/Layout.js | 16 ++++- app/components/Sidebar/Sidebar.js | 34 ++++++++++- .../Sidebar/components/CollapseToggle.js | 59 +++++++++++++++++++ .../Sidebar/components/HeaderBlock.js | 2 +- app/stores/UiStore.js | 18 ++++++ shared/i18n/locales/en_US/translation.json | 2 + shared/styles/theme.js | 1 + 7 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 app/components/Sidebar/components/CollapseToggle.js diff --git a/app/components/Layout.js b/app/components/Layout.js index 62668c929..6e8dab13f 100644 --- a/app/components/Layout.js +++ b/app/components/Layout.js @@ -65,6 +65,11 @@ class Layout extends React.Component { window.document.body.style.background = props.theme.background; } + @keydown("meta+.") + handleToggleSidebar() { + this.props.ui.toggleCollapsedSidebar(); + } + @keydown("shift+/") handleOpenKeyboardShortcuts() { if (this.props.ui.editMode) return; @@ -119,7 +124,11 @@ class Layout extends React.Component { )} - + {this.props.children} @@ -159,7 +168,10 @@ const Content = styled(Flex)` } ${breakpoint("tablet")` - margin-left: ${(props) => (props.editMode ? 0 : props.theme.sidebarWidth)}; + margin-left: ${(props) => + props.sidebarCollapsed + ? props.theme.sidebarCollapsedWidth + : props.theme.sidebarWidth}; `}; `; diff --git a/app/components/Sidebar/Sidebar.js b/app/components/Sidebar/Sidebar.js index 7f785a1f2..d4ceaccc1 100644 --- a/app/components/Sidebar/Sidebar.js +++ b/app/components/Sidebar/Sidebar.js @@ -8,6 +8,7 @@ import styled from "styled-components"; import breakpoint from "styled-components-breakpoint"; import Fade from "components/Fade"; import Flex from "components/Flex"; +import CollapseToggle, { Button } from "./components/CollapseToggle"; import usePrevious from "hooks/usePrevious"; import useStores from "hooks/useStores"; @@ -30,10 +31,14 @@ function Sidebar({ location, children }: Props) { const content = ( + props.theme.sidebarBackground}; - transition: left 100ms ease-out, + transition: box-shadow, 100ms, ease-in-out, left 100ms ease-out, ${(props) => props.theme.backgroundTransition}; margin-left: ${(props) => (props.mobileSidebarVisible ? 0 : "-100%")}; z-index: ${(props) => props.theme.depths.sidebar}; @@ -90,10 +95,33 @@ const Container = styled(Flex)` } ${breakpoint("tablet")` - left: ${(props) => (props.editMode ? `-${props.theme.sidebarWidth}` : 0)}; + left: ${(props) => + props.collapsed + ? `calc(-${props.theme.sidebarWidth} + ${props.theme.sidebarCollapsedWidth})` + : 0}; width: ${(props) => props.theme.sidebarWidth}; margin: 0; z-index: 3; + + &:hover, + &:focus-within { + left: 0; + box-shadow: ${(props) => + props.collapsed ? "rgba(0, 0, 0, 0.2) 1px 0 4px" : "none"}; + + & ${Button} { + opacity: .75; + } + + & ${Button}:hover { + opacity: 1; + } + } + + &:not(:hover):not(:focus-within) > div { + opacity: ${(props) => (props.collapsed ? "0" : "1")}; + transition: opacity 100ms ease-in-out; + } `}; `; diff --git a/app/components/Sidebar/components/CollapseToggle.js b/app/components/Sidebar/components/CollapseToggle.js new file mode 100644 index 000000000..8de701be6 --- /dev/null +++ b/app/components/Sidebar/components/CollapseToggle.js @@ -0,0 +1,59 @@ +// @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?: () => 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: ${(props) => props.theme.sidebarItemBackground}; + 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/HeaderBlock.js b/app/components/Sidebar/components/HeaderBlock.js index 202adc03e..22ba64df8 100644 --- a/app/components/Sidebar/components/HeaderBlock.js +++ b/app/components/Sidebar/components/HeaderBlock.js @@ -61,7 +61,7 @@ const Header = styled.button` display: flex; align-items: center; flex-shrink: 0; - padding: 16px 24px; + padding: 20px 24px; position: relative; background: none; line-height: inherit; diff --git a/app/stores/UiStore.js b/app/stores/UiStore.js index b5d7d8c00..7f664bceb 100644 --- a/app/stores/UiStore.js +++ b/app/stores/UiStore.js @@ -23,6 +23,7 @@ class UiStore { @observable editMode: boolean = false; @observable tocVisible: boolean = false; @observable mobileSidebarVisible: boolean = false; + @observable sidebarCollapsed: boolean = false; @observable toasts: Map = new Map(); constructor() { @@ -51,6 +52,7 @@ class UiStore { // persisted keys this.languagePromptDismissed = data.languagePromptDismissed; + this.sidebarCollapsed = data.sidebarCollapsed; this.tocVisible = data.tocVisible; this.theme = data.theme || "system"; @@ -107,6 +109,21 @@ class UiStore { this.activeCollectionId = undefined; }; + @action + collapseSidebar = () => { + this.sidebarCollapsed = true; + }; + + @action + expandSidebar = () => { + this.sidebarCollapsed = false; + }; + + @action + toggleCollapsedSidebar = () => { + this.sidebarCollapsed = !this.sidebarCollapsed; + }; + @action showTableOfContents = () => { this.tocVisible = true; @@ -190,6 +207,7 @@ class UiStore { get asJson(): string { return JSON.stringify({ tocVisible: this.tocVisible, + sidebarCollapsed: this.sidebarCollapsed, languagePromptDismissed: this.languagePromptDismissed, theme: this.theme, }); diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index 1cc7284b6..654939983 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -82,6 +82,8 @@ "Change Language": "Change Language", "Dismiss": "Dismiss", "Keyboard shortcuts": "Keyboard shortcuts", + "Expand": "Expand", + "Collapse": "Collapse", "New collection": "New collection", "Collections": "Collections", "Untitled": "Untitled", diff --git a/shared/styles/theme.js b/shared/styles/theme.js index ba987cae2..3d5789943 100644 --- a/shared/styles/theme.js +++ b/shared/styles/theme.js @@ -48,6 +48,7 @@ const spacing = { vpadding: "1.5vw", hpadding: "1.875vw", sidebarWidth: "280px", + sidebarCollapsedWidth: "16px", sidebarMinWidth: "250px", sidebarMaxWidth: "350px", };