feat: Display right sidebar as drawer on mobile (#10175)

* wip

* Stack metadata on mobile

* fix: Allow viewing history
This commit is contained in:
Tom Moor
2025-09-14 23:09:33 +02:00
committed by GitHub
parent bf68a1d2bf
commit 7303970118
8 changed files with 86 additions and 65 deletions

View File

@@ -13,7 +13,6 @@ import ErrorSuspended from "~/scenes/Errors/ErrorSuspended";
import Layout from "~/components/Layout";
import RegisterKeyDown from "~/components/RegisterKeyDown";
import Sidebar from "~/components/Sidebar";
import SidebarRight from "~/components/Sidebar/Right";
import SettingsSidebar from "~/components/Sidebar/Settings";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import { usePostLoginPath } from "~/hooks/useLastVisitedPath";
@@ -109,12 +108,10 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
>
{(showHistory || showComments) && (
<Route path={`/doc/${slug}`}>
<SidebarRight>
<React.Suspense fallback={null}>
{showHistory && <DocumentHistory />}
{showComments && <DocumentComments />}
</React.Suspense>
</SidebarRight>
<React.Suspense fallback={null}>
{showHistory && <DocumentHistory />}
{showComments && <DocumentComments />}
</React.Suspense>
</Route>
)}
</AnimatePresence>

View File

@@ -155,14 +155,16 @@ const DocumentMeta: React.FC<Props> = ({
}
return (
<Viewed>
&nbsp;<Modified highlight>{t("Never viewed")}</Modified>
<Separator />
<Modified highlight>{t("Never viewed")}</Modified>
</Viewed>
);
}
return (
<Viewed>
&nbsp;{t("Viewed")} <Time dateTime={lastViewedAt} addSuffix shorten />
<Separator />
{t("Viewed")} <Time dateTime={lastViewedAt} addSuffix shorten />
</Viewed>
);
};
@@ -186,16 +188,17 @@ const DocumentMeta: React.FC<Props> = ({
)}
{showParentDocuments && nestedDocumentsCount > 0 && (
<span>
&nbsp; {nestedDocumentsCount}{" "}
<Separator />
{nestedDocumentsCount}{" "}
{t("nested document", {
count: nestedDocumentsCount,
})}
</span>
)}
&nbsp;{timeSinceNow()}
{timeSinceNow()}
{canShowProgressBar && (
<>
&nbsp;&nbsp;
<Separator />
<DocumentTasks document={document} />
</>
)}
@@ -204,6 +207,14 @@ const DocumentMeta: React.FC<Props> = ({
);
};
export const Separator = styled.span`
padding: 0 0.4em;
&::after {
content: "•";
}
`;
const Strong = styled.strong`
font-weight: 550;
`;

View File

@@ -7,7 +7,6 @@ import { depths, s } from "@shared/styles";
import ErrorBoundary from "~/components/ErrorBoundary";
import Flex from "~/components/Flex";
import ResizeBorder from "~/components/Sidebar/components/ResizeBorder";
import useMobile from "~/hooks/useMobile";
import useStores from "~/hooks/useStores";
import { sidebarAppearDuration } from "~/styles/animations";
@@ -20,7 +19,6 @@ function Right({ children, border, className }: Props) {
const theme = useTheme();
const { ui } = useStores();
const [isResizing, setResizing] = React.useState(false);
const isMobile = useMobile();
const maxWidth = theme.sidebarMaxWidth;
const minWidth = theme.sidebarMinWidth + 16; // padding
@@ -100,13 +98,11 @@ function Right({ children, border, className }: Props) {
<Sidebar {...animationProps} $border={border} className={className}>
<Position style={style} column>
<ErrorBoundary>{children}</ErrorBoundary>
{!isMobile && (
<ResizeBorder
onMouseDown={handleMouseDown}
onDoubleClick={handleReset}
dir="right"
/>
)}
<ResizeBorder
onMouseDown={handleMouseDown}
onDoubleClick={handleReset}
dir="right"
/>
</Position>
</Sidebar>
);

View File

@@ -18,6 +18,8 @@ Drawer.displayName = "Drawer";
/** Drawer's trigger. */
const DrawerTrigger = DrawerPrimitive.Trigger;
const DrawerHandle = DrawerPrimitive.Handle;
/** Drawer's content - renders the overlay and the actual content. */
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
@@ -56,11 +58,9 @@ const DrawerTitle = React.forwardRef<
const { hidden, children, ...rest } = props;
const title = (
<TitleWrapper justify="center">
<Text size="medium" weight="bold">
{children}
</Text>
</TitleWrapper>
<Text size="medium" weight="bold" as={TitleWrapper} justify="center">
{children}
</Text>
);
return (
@@ -100,4 +100,4 @@ const TitleWrapper = styled(Flex)`
padding: 8px 0;
`;
export { Drawer, DrawerTrigger, DrawerContent, DrawerTitle };
export { Drawer, DrawerTrigger, DrawerHandle, DrawerContent, DrawerTitle };

View File

@@ -23,6 +23,7 @@ import CommentForm from "./CommentForm";
import CommentSortMenu from "./CommentSortMenu";
import CommentThread from "./CommentThread";
import Sidebar from "./SidebarLayout";
import useMobile from "~/hooks/useMobile";
import { ArrowDownIcon } from "~/components/Icons/ArrowIcon";
function Comments() {
@@ -34,6 +35,8 @@ function Comments() {
const document = documents.get(match.params.documentSlug);
const focusedComment = useFocusedComment();
const can = usePolicy(document);
const isMobile = useMobile();
const query = useQuery();
const [viewingResolved, setViewingResolved] = useState(
query.get("resolved") !== null || focusedComment?.isResolved || false
@@ -130,8 +133,10 @@ function Comments() {
return (
<Sidebar
title={
<Flex align="center" justify="space-between" auto>
<span>{t("Comments")}</span>
<Flex align="center" justify="space-between" gap={8} auto>
<div style={isMobile ? { padding: "0 8px" } : undefined}>
{t("Comments")}
</div>
<CommentSortMenu
viewingResolved={viewingResolved}
onChange={(val) => {
@@ -184,7 +189,7 @@ function Comments() {
</Wrapper>
</Scrollable>
<AnimatePresence initial={false}>
{!focusedComment && can.comment && !viewingResolved && (
{(!focusedComment || isMobile) && can.comment && !viewingResolved && (
<NewCommentForm
draft={draft}
onSaveDraft={onSaveDraft}

View File

@@ -9,12 +9,13 @@ import { TeamPreference } from "@shared/types";
import Document from "~/models/Document";
import Revision from "~/models/Revision";
import { openDocumentInsights } from "~/actions/definitions/documents";
import DocumentMeta from "~/components/DocumentMeta";
import DocumentMeta, { Separator } from "~/components/DocumentMeta";
import Fade from "~/components/Fade";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import breakpoint from "styled-components-breakpoint";
import { documentPath } from "~/utils/routeHelpers";
import NudeButton from "~/components/NudeButton";
@@ -46,7 +47,7 @@ function TitleDocumentMeta({ to, document, revision, ...rest }: Props) {
<Meta document={document} revision={revision} to={to} replace {...rest}>
{commentingEnabled && can.comment && (
<>
&nbsp;&nbsp;
<Separator />
<CommentLink
to={{
pathname: documentPath(document),
@@ -66,7 +67,7 @@ function TitleDocumentMeta({ to, document, revision, ...rest }: Props) {
!document.isDraft &&
!document.isTemplate ? (
<Wrapper>
&nbsp;&nbsp;
<Separator />
<InsightsButton action={openDocumentInsights}>
{t("Viewed by")}{" "}
{onlyYou
@@ -108,6 +109,16 @@ export const Meta = styled(DocumentMeta)<{ rtl?: boolean }>`
user-select: none;
z-index: 1;
${breakpoint("mobile", "tablet")`
flex-direction: column;
align-items: flex-start;
line-height: 1.6;
${Separator} {
display: none;
}
`}
a {
color: inherit;
cursor: var(--pointer);

View File

@@ -15,6 +15,7 @@ import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import useStores from "~/hooks/useStores";
import { documentPath } from "~/utils/routeHelpers";
import Sidebar from "./SidebarLayout";
import useMobile from "~/hooks/useMobile";
const DocumentEvents = [
"documents.publish",
@@ -37,6 +38,7 @@ function History() {
const document = documents.get(match.params.documentSlug);
const [revisionsOffset, setRevisionsOffset] = React.useState(0);
const [eventsOffset, setEventsOffset] = React.useState(0);
const isMobile = useMobile();
const fetchHistory = React.useCallback(async () => {
if (!document) {
@@ -125,6 +127,10 @@ function History() {
}, [revisions, document, revisionEvents, nonRevisionEvents]);
const onCloseHistory = React.useCallback(() => {
if (isMobile) {
// Allow closing the history drawer on mobile to view revision content
return;
}
if (document) {
history.push({
pathname: documentPath(document),

View File

@@ -3,15 +3,19 @@ import { BackIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { depths, s, ellipsis } from "@shared/styles";
import { s, ellipsis } from "@shared/styles";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import { Portal } from "~/components/Portal";
import Scrollable from "~/components/Scrollable";
import Tooltip from "~/components/Tooltip";
import useMobile from "~/hooks/useMobile";
import { draggableOnDesktop } from "~/styles";
import { fadeIn } from "~/styles/animations";
import RightSidebar from "~/components/Sidebar/Right";
import {
Drawer,
DrawerContent,
DrawerTitle,
} from "~/components/primitives/Drawer";
type Props = Omit<React.HTMLAttributes<HTMLDivElement>, "title"> & {
/* The title of the sidebar */
@@ -19,7 +23,7 @@ type Props = Omit<React.HTMLAttributes<HTMLDivElement>, "title"> & {
/* The content of the sidebar */
children: React.ReactNode;
/* Called when the sidebar is closed */
onClose: React.MouseEventHandler;
onClose: () => void;
/* Whether the sidebar should be scrollable */
scrollable?: boolean;
};
@@ -28,8 +32,23 @@ function SidebarLayout({ title, onClose, children, scrollable = true }: Props) {
const { t } = useTranslation();
const isMobile = useMobile();
return (
<>
const content = scrollable ? (
<Scrollable hiddenScrollbars topShadow>
{children}
</Scrollable>
) : (
children
);
return isMobile ? (
<Drawer onClose={onClose} defaultOpen>
<DrawerContent>
<DrawerTitle>{title}</DrawerTitle>
{content}
</DrawerContent>
</Drawer>
) : (
<RightSidebar>
<Header>
<Title>{title}</Title>
<Tooltip content={t("Close")} shortcut="Esc">
@@ -41,35 +60,11 @@ function SidebarLayout({ title, onClose, children, scrollable = true }: Props) {
/>
</Tooltip>
</Header>
{scrollable ? (
<Scrollable hiddenScrollbars topShadow>
{children}
</Scrollable>
) : (
children
)}
{isMobile && (
<Portal>
<Backdrop onClick={onClose} />
</Portal>
)}
</>
{content}
</RightSidebar>
);
}
const Backdrop = styled.a`
animation: ${fadeIn} 250ms ease-in-out;
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
cursor: default;
z-index: ${depths.mobileSidebar - 1};
background: ${s("backdrop")};
`;
const ForwardIcon = styled(BackIcon)`
transform: rotate(180deg);
flex-shrink: 0;