diff --git a/app/components/Sidebar/components/Bubble.js b/app/components/Bubble.js
similarity index 92%
rename from app/components/Sidebar/components/Bubble.js
rename to app/components/Bubble.js
index e70087e3..2bfd3dc2 100644
--- a/app/components/Sidebar/components/Bubble.js
+++ b/app/components/Bubble.js
@@ -3,11 +3,15 @@ import * as React from "react";
import styled from "styled-components";
import { bounceIn } from "shared/styles/animations";
-type Props = {
+type Props = {|
count: number,
-};
+|};
const Bubble = ({ count }: Props) => {
+ if (!count) {
+ return null;
+ }
+
return {count};
};
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/Main.js b/app/components/Sidebar/Main.js
index fdbd2e35..d6696725 100644
--- a/app/components/Sidebar/Main.js
+++ b/app/components/Sidebar/Main.js
@@ -16,11 +16,11 @@ import { useTranslation } from "react-i18next";
import styled from "styled-components";
import CollectionNew from "scenes/CollectionNew";
import Invite from "scenes/Invite";
+import Bubble from "components/Bubble";
import Flex from "components/Flex";
import Modal from "components/Modal";
import Scrollable from "components/Scrollable";
import Sidebar from "./Sidebar";
-import Bubble from "./components/Bubble";
import Collections from "./components/Collections";
import HeaderBlock from "./components/HeaderBlock";
import Section from "./components/Section";
@@ -118,9 +118,7 @@ function MainSidebar() {
label={
{t("Drafts")}
- {documents.totalDrafts > 0 && (
-
- )}
+
}
active={
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/components/Switch.js b/app/components/Switch.js
index aaa08f18..172c54e9 100644
--- a/app/components/Switch.js
+++ b/app/components/Switch.js
@@ -13,17 +13,23 @@ type Props = {|
id?: string,
|};
-function Switch({ width = 38, height = 20, label, ...props }: Props) {
+function Switch({ width = 38, height = 20, label, disabled, ...props }: Props) {
const component = (
-
+
);
if (label) {
return (
-