Move sidebar toggle into the sidebar itself instead of overlaying document content (#5749)

This commit is contained in:
Tom Moor
2023-08-29 21:45:03 -04:00
committed by GitHub
parent 864ddbd438
commit b7055ef853
11 changed files with 184 additions and 364 deletions

View File

@@ -1,19 +1,18 @@
import { observer } from "mobx-react";
import { SubscribeIcon } from "outline-icons";
import * as React from "react";
import styled, { useTheme } from "styled-components";
import styled from "styled-components";
import { s } from "@shared/styles";
import useStores from "~/hooks/useStores";
import Relative from "../Sidebar/components/Relative";
const NotificationIcon = () => {
const { notifications } = useStores();
const theme = useTheme();
const count = notifications.approximateUnreadCount;
return (
<Relative style={{ height: 24 }}>
<SubscribeIcon color={theme.textTertiary} />
<SubscribeIcon />
{count > 0 && <Badge />}
</Relative>
);

View File

@@ -1,5 +1,11 @@
import { observer } from "mobx-react";
import { EditIcon, SearchIcon, ShapesIcon, HomeIcon } from "outline-icons";
import {
EditIcon,
SearchIcon,
ShapesIcon,
HomeIcon,
SidebarIcon,
} from "outline-icons";
import * as React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
@@ -14,7 +20,7 @@ import useCurrentUser from "~/hooks/useCurrentUser";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import OrganizationMenu from "~/menus/OrganizationMenu";
import Desktop from "~/utils/Desktop";
import { metaDisplay } from "~/utils/keyboard";
import {
homePath,
draftsPath,
@@ -22,23 +28,23 @@ import {
searchPath,
} from "~/utils/routeHelpers";
import TeamLogo from "../TeamLogo";
import Tooltip from "../Tooltip";
import Sidebar from "./Sidebar";
import ArchiveLink from "./components/ArchiveLink";
import Collections from "./components/Collections";
import DragPlaceholder from "./components/DragPlaceholder";
import FullWidthButton, {
FullWidthButtonProps,
} from "./components/FullWidthButton";
import HistoryNavigation from "./components/HistoryNavigation";
import Section from "./components/Section";
import SidebarAction from "./components/SidebarAction";
import SidebarButton, { SidebarButtonProps } from "./components/SidebarButton";
import SidebarLink from "./components/SidebarLink";
import Starred from "./components/Starred";
import ToggleButton from "./components/ToggleButton";
import TrashLink from "./components/TrashLink";
function AppSidebar() {
const { t } = useTranslation();
const { documents } = useStores();
const { documents, ui } = useStores();
const team = useCurrentTeam();
const user = useCurrentUser();
const can = usePolicy(team);
@@ -59,6 +65,15 @@ function AppSidebar() {
[dndArea]
);
const handleToggleSidebar = React.useCallback(
(ev) => {
ev.preventDefault();
ev.stopPropagation();
ui.toggleCollapsedSidebar();
},
[ui]
);
return (
<Sidebar ref={handleSidebarRef}>
<HistoryNavigation />
@@ -67,23 +82,31 @@ function AppSidebar() {
<DragPlaceholder />
<OrganizationMenu>
{(props: FullWidthButtonProps) => (
<FullWidthButton
{(props: SidebarButtonProps) => (
<SidebarButton
{...props}
title={team.name}
image={
<TeamLogo
model={team}
size={Desktop.hasInsetTitlebar() ? 24 : 32}
size={24}
alt={t("Logo")}
style={{ marginLeft: 4 }}
/>
}
style={
// Move the logo over to align with smaller size
Desktop.hasInsetTitlebar() ? { paddingLeft: 8 } : undefined
}
showDisclosure
/>
>
<Tooltip
tooltip={t("Toggle sidebar")}
shortcut={`${metaDisplay}+.`}
delay={500}
>
<ToggleButton
position="bottom"
image={<SidebarIcon />}
onClick={handleToggleSidebar}
/>
</Tooltip>
</SidebarButton>
)}
</OrganizationMenu>
<Scrollable flex shadow>

View File

@@ -11,10 +11,10 @@ import useSettingsConfig from "~/hooks/useSettingsConfig";
import Desktop from "~/utils/Desktop";
import isCloudHosted from "~/utils/isCloudHosted";
import Sidebar from "./Sidebar";
import FullWidthButton from "./components/FullWidthButton";
import Header from "./components/Header";
import HistoryNavigation from "./components/HistoryNavigation";
import Section from "./components/Section";
import SidebarButton from "./components/SidebarButton";
import SidebarLink from "./components/SidebarLink";
import Version from "./components/Version";
@@ -31,7 +31,7 @@ function SettingsSidebar() {
return (
<Sidebar>
<HistoryNavigation />
<FullWidthButton
<SidebarButton
title={t("Return to App")}
image={<StyledBackIcon />}
onClick={returnToApp}

View File

@@ -11,9 +11,9 @@ import { homePath, sharedDocumentPath } from "~/utils/routeHelpers";
import { useTeamContext } from "../TeamContext";
import TeamLogo from "../TeamLogo";
import Sidebar from "./Sidebar";
import FullWidthButton from "./components/FullWidthButton";
import Section from "./components/Section";
import DocumentLink from "./components/SharedDocumentLink";
import SidebarButton from "./components/SidebarButton";
type Props = {
rootNode: NavigationNode;
@@ -28,7 +28,7 @@ function SharedSidebar({ rootNode, shareId }: Props) {
return (
<Sidebar>
{team && (
<FullWidthButton
<SidebarButton
title={team.name}
image={<TeamLogo model={team} size={32} alt={t("Logo")} />}
onClick={() =>

View File

@@ -1,9 +1,8 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Portal } from "react-portal";
import { useLocation } from "react-router-dom";
import styled, { useTheme } from "styled-components";
import styled, { css, useTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { depths, s } from "@shared/styles";
import Flex from "~/components/Flex";
@@ -17,25 +16,23 @@ import Desktop from "~/utils/Desktop";
import Avatar from "../Avatar";
import NotificationIcon from "../Notifications/NotificationIcon";
import NotificationsPopover from "../Notifications/NotificationsPopover";
import FullWidthButton, {
FullWidthButtonProps,
} from "./components/FullWidthButton";
import ResizeBorder from "./components/ResizeBorder";
import Toggle, { ToggleButton, Positioner } from "./components/Toggle";
import SidebarButton, { SidebarButtonProps } from "./components/SidebarButton";
import ToggleButton from "./components/ToggleButton";
const ANIMATION_MS = 250;
type Props = {
children: React.ReactNode;
className?: string;
};
const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
{ children }: Props,
{ children, className }: Props,
ref: React.RefObject<HTMLDivElement>
) {
const [isCollapsing, setCollapsing] = React.useState(false);
const theme = useTheme();
const { t } = useTranslation();
const { ui, auth } = useStores();
const location = useLocation();
const previousLocation = usePrevious(location);
@@ -48,6 +45,7 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
const setWidth = ui.setSidebarWidth;
const [offset, setOffset] = React.useState(0);
const [isHovering, setHovering] = React.useState(false);
const [isAnimating, setAnimating] = React.useState(false);
const [isResizing, setResizing] = React.useState(false);
const isSmallerThanMinimum = width < minWidth;
@@ -101,6 +99,22 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
[width]
);
const handlePointerMove = React.useCallback(() => {
setHovering(true);
}, []);
const handlePointerLeave = React.useCallback(
(ev) => {
setHovering(
ev.pageX < width &&
ev.pageX > 0 &&
ev.pageY < window.innerHeight &&
ev.pageY > 0
);
},
[width]
);
React.useEffect(() => {
if (isAnimating) {
setTimeout(() => setAnimating(false), ANIMATION_MS);
@@ -149,23 +163,19 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
[width]
);
const toggleStyle = React.useMemo(
() => ({
right: "auto",
marginLeft: `${collapsed ? theme.sidebarCollapsedWidth : width}px`,
}),
[width, theme.sidebarCollapsedWidth, collapsed]
);
return (
<>
<Container
ref={ref}
style={style}
$isHovering={isHovering}
$isAnimating={isAnimating}
$isSmallerThanMinimum={isSmallerThanMinimum}
$mobileSidebarVisible={ui.mobileSidebarVisible}
$collapsed={collapsed}
className={className}
onPointerMove={handlePointerMove}
onPointerLeave={handlePointerLeave}
column
>
{ui.mobileSidebarVisible && (
@@ -177,31 +187,32 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
{user && (
<AccountMenu>
{(props: FullWidthButtonProps) => (
<FullWidthButton
{(props: SidebarButtonProps) => (
<SidebarButton
{...props}
showMoreMenu
title={user.name}
position="bottom"
image={
<StyledAvatar
<Avatar
alt={user.name}
model={user}
size={24}
showBorder={false}
style={{ marginLeft: 4 }}
/>
}
>
<NotificationsPopover>
{(rest: FullWidthButtonProps) => (
<FullWidthButton
{(rest: SidebarButtonProps) => (
<SidebarButton
{...rest}
position="bottom"
image={<NotificationIcon />}
/>
)}
</NotificationsPopover>
</FullWidthButton>
</SidebarButton>
)}
</AccountMenu>
)}
@@ -209,28 +220,11 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
onMouseDown={handleMouseDown}
onDoubleClick={ui.sidebarIsClosed ? undefined : handleReset}
/>
{ui.sidebarIsClosed && (
<Toggle
onClick={ui.toggleCollapsedSidebar}
direction={"right"}
aria-label={t("Expand")}
/>
)}
</Container>
<Toggle
style={toggleStyle}
onClick={ui.toggleCollapsedSidebar}
direction={ui.sidebarIsClosed ? "right" : "left"}
aria-label={ui.sidebarIsClosed ? t("Expand") : t("Collapse")}
/>
</>
);
});
const StyledAvatar = styled(Avatar)`
margin-left: 4px;
`;
const Backdrop = styled.a`
animation: ${fadeIn} 250ms ease-in-out;
position: fixed;
@@ -247,16 +241,33 @@ type ContainerProps = {
$mobileSidebarVisible: boolean;
$isAnimating: boolean;
$isSmallerThanMinimum: boolean;
$isHovering: boolean;
$collapsed: boolean;
};
const hoverStyles = (props: ContainerProps) => `
transform: none;
box-shadow: ${
props.$collapsed
? "rgba(0, 0, 0, 0.2) 1px 0 4px"
: props.$isSmallerThanMinimum
? "rgba(0, 0, 0, 0.1) inset -1px 0 2px"
: "none"
};
${ToggleButton} {
opacity: 1;
}
`;
const Container = styled(Flex)<ContainerProps>`
position: fixed;
top: 0;
bottom: 0;
width: 100%;
background: ${s("sidebarBackground")};
transition: box-shadow 100ms ease-in-out, transform 100ms ease-out,
transition: box-shadow 100ms ease-in-out, opacity 100ms ease-in-out,
transform 100ms ease-out,
${s("backgroundTransition")}
${(props: ContainerProps) =>
props.$isAnimating ? `,width ${ANIMATION_MS}ms ease-out` : ""};
@@ -268,15 +279,15 @@ const Container = styled(Flex)<ContainerProps>`
min-width: 280px;
${fadeOnDesktopBackgrounded()}
${Positioner} {
display: none;
}
@media print {
display: none;
transform: none;
}
& > div {
opacity: ${(props) => (props.$collapsed && !props.$isHovering ? "0" : "1")};
}
${breakpoint("tablet")`
margin: 0;
min-width: 0;
@@ -285,28 +296,14 @@ const Container = styled(Flex)<ContainerProps>`
? `calc(-100% + ${Desktop.hasInsetTitlebar() ? 8 : 16}px)`
: 0});
&:hover,
${(props: ContainerProps) => props.$isHovering && css(hoverStyles)}
&:focus-within {
transform: none;
box-shadow: ${(props: ContainerProps) =>
props.$collapsed
? "rgba(0, 0, 0, 0.2) 1px 0 4px"
: props.$isSmallerThanMinimum
? "rgba(0, 0, 0, 0.1) inset -1px 0 2px"
: "none"};
${hoverStyles}
${Positioner} {
display: block;
}
${ToggleButton} {
& > div {
opacity: 1;
}
}
&:not(:hover):not(:focus-within) > div {
opacity: ${(props: ContainerProps) => (props.$collapsed ? "0" : "1")};
transition: opacity 100ms ease-in-out;
}
}
`};
`;

View File

@@ -1,64 +1,62 @@
import { ExpandedIcon, MoreIcon } from "outline-icons";
import { MoreIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import { s } from "@shared/styles";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
import { draggableOnDesktop, undraggableOnDesktop } from "~/styles";
import Desktop from "~/utils/Desktop";
export type FullWidthButtonProps = React.ComponentProps<typeof Button> & {
export type SidebarButtonProps = React.ComponentProps<typeof Button> & {
position: "top" | "bottom";
title: React.ReactNode;
image: React.ReactNode;
minHeight?: number;
rounded?: boolean;
showDisclosure?: boolean;
showMoreMenu?: boolean;
onClick: React.MouseEventHandler<HTMLButtonElement>;
children?: React.ReactNode;
};
const FullWidthButton = React.forwardRef<
HTMLButtonElement,
FullWidthButtonProps
>(function _FullWidthButton(
{
position = "top",
showDisclosure,
showMoreMenu,
image,
title,
minHeight = 0,
children,
...rest
}: FullWidthButtonProps,
ref
) {
return (
<Container
justify="space-between"
align="center"
shrink={false}
$position={position}
>
<Button
{...rest}
minHeight={minHeight}
as="button"
ref={ref}
role="button"
const SidebarButton = React.forwardRef<HTMLButtonElement, SidebarButtonProps>(
function _SidebarButton(
{
position = "top",
showMoreMenu,
image,
title,
minHeight = 0,
children,
...rest
}: SidebarButtonProps,
ref
) {
return (
<Container
justify="space-between"
align="center"
shrink={false}
$position={position}
>
<Title gap={8} align="center">
{image}
{title}
</Title>
{showDisclosure && <ExpandedIcon />}
{showMoreMenu && <MoreIcon />}
</Button>
{children}
</Container>
);
});
<Button
{...rest}
$minHeight={minHeight}
$position={position}
as="button"
ref={ref}
role="button"
>
<Title gap={8} align="center">
{image}
{title && <Text as="span">{title}</Text>}
</Title>
{showMoreMenu && <MoreIcon />}
</Button>
{children}
</Container>
);
}
);
const Container = styled(Flex)<{ $position: "top" | "bottom" }>`
padding-top: ${(props) =>
@@ -67,7 +65,6 @@ const Container = styled(Flex)<{ $position: "top" | "bottom" }>`
`;
const Title = styled(Flex)`
color: ${s("text")};
flex-shrink: 1;
flex-grow: 1;
text-overflow: ellipsis;
@@ -75,19 +72,22 @@ const Title = styled(Flex)`
overflow: hidden;
`;
const Button = styled(Flex)<{ minHeight: number }>`
const Button = styled(Flex)<{
$minHeight: number;
$position: "top" | "bottom";
}>`
flex: 1;
color: ${s("textTertiary")};
align-items: center;
padding: 8px 4px;
padding: 4px;
font-size: 15px;
font-weight: 500;
border-radius: 4px;
margin: 8px 0;
border: 0;
margin: ${(props) => (props.$position === "top" ? 16 : 8)}px 0;
background: none;
flex-shrink: 0;
min-height: ${(props) => props.minHeight}px;
min-height: ${(props) => props.$minHeight}px;
-webkit-appearance: none;
text-decoration: none;
@@ -114,4 +114,4 @@ const Button = styled(Flex)<{ minHeight: number }>`
}
`;
export default FullWidthButton;
export default SidebarButton;

View File

@@ -1,106 +0,0 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled, { css } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { s } from "@shared/styles";
import Arrow from "~/components/Arrow";
import useEventListener from "~/hooks/useEventListener";
type Props = {
direction: "left" | "right";
style?: React.CSSProperties;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
};
const Toggle = React.forwardRef<HTMLButtonElement, Props>(function Toggle_(
{ direction = "left", onClick, style }: Props,
ref
) {
const { t } = useTranslation();
const [hovering, setHovering] = React.useState(false);
const positionRef = React.useRef<HTMLDivElement>(null);
// Not using CSS hover here so that we can disable pointer events on this
// div and allow click through to the editor elements behind.
useEventListener("mousemove", (event: MouseEvent) => {
if (!positionRef.current) {
return;
}
const bound = positionRef.current.getBoundingClientRect();
const withinBounds =
event.clientX >= bound.left && event.clientX <= bound.right;
if (withinBounds !== hovering) {
setHovering(withinBounds);
}
});
return (
<Positioner style={style} ref={positionRef} $hovering={hovering}>
<ToggleButton
ref={ref}
$direction={direction}
onClick={onClick}
aria-label={t("Toggle sidebar")}
>
<Arrow />
</ToggleButton>
</Positioner>
);
});
export const ToggleButton = styled.button<{ $direction?: "left" | "right" }>`
opacity: 0;
background: none;
transition: opacity 100ms ease-in-out;
transform: translateY(-50%)
scaleX(${(props) => (props.$direction === "left" ? 1 : -1)});
position: fixed;
top: 50vh;
padding: 8px;
border: 0;
pointer-events: none;
color: ${s("divider")};
&:active {
color: ${s("sidebarText")};
}
${breakpoint("tablet")`
pointer-events: all;
cursor: var(--pointer);
`}
@media (hover: none) {
opacity: 1;
}
`;
export const Positioner = styled.div<{ $hovering: boolean }>`
display: none;
z-index: 2;
position: absolute;
top: 0;
bottom: 0;
right: -30px;
width: 30px;
pointer-events: none;
&:focus-within ${ToggleButton} {
opacity: 1;
}
${(props) =>
props.$hovering &&
css`
${ToggleButton} {
opacity: 1;
}
`}
${breakpoint("tablet")`
display: block;
`}
`;
export default Toggle;

View File

@@ -0,0 +1,15 @@
import styled from "styled-components";
import { hover } from "~/styles";
import SidebarButton from "./SidebarButton";
const ToggleButton = styled(SidebarButton)`
opacity: 0;
transition: opacity 100ms ease-in-out;
&:${hover},
&:active {
opacity: 1;
}
`;
export default ToggleButton;

View File

@@ -4,7 +4,8 @@ import Avatar from "./Avatar";
const TeamLogo = styled(Avatar)`
border-radius: 4px;
border: 1px solid ${s("divider")};
box-shadow: inset 0 0 0 1px ${s("divider")};
border: 0;
`;
export default TeamLogo;