feat: Add "new doc" button on collections in sidebar (#3174)

* feat: Add new icon button on collections in sidebar, move sort into menu

* Remove unused menu, add warning when dragging in a-z collection

* fix: Add hover background to sidebar actions, add tooltip to new doc button

* Retain 'active' state on buttons when related context menu is open

* fix: Two more spots that deserve active background
This commit is contained in:
Tom Moor
2022-02-26 11:48:32 -08:00
committed by GitHub
parent 31c84d5479
commit 4c138ed585
11 changed files with 143 additions and 119 deletions

View File

@@ -41,7 +41,8 @@ const RealButton = styled.button<{
border: 0;
}
&:hover:not(:disabled) {
&:hover:not(:disabled),
&[aria-expanded="true"] {
background: ${(props) => darken(0.05, props.theme.buttonBackground)};
}
@@ -76,7 +77,8 @@ const RealButton = styled.button<{
}
&:hover:not(:disabled) {
&:hover:not(:disabled),
&[aria-expanded="true"] {
background: ${
props.borderOnHover
? props.theme.buttonNeutralBackground
@@ -103,7 +105,8 @@ const RealButton = styled.button<{
background: ${props.theme.danger};
color: ${props.theme.white};
&:hover:not(:disabled) {
&:hover:not(:disabled),
&[aria-expanded="true"] {
background: ${darken(0.05, props.theme.danger)};
}

View File

@@ -12,6 +12,7 @@ import DocumentMeta from "~/components/DocumentMeta";
import EventBoundary from "~/components/EventBoundary";
import Flex from "~/components/Flex";
import Highlight from "~/components/Highlight";
import NudeButton from "~/components/NudeButton";
import StarButton, { AnimatedStar } from "~/components/Star";
import Tooltip from "~/components/Tooltip";
import useBoolean from "~/hooks/useBoolean";
@@ -171,6 +172,13 @@ const Actions = styled(EventBoundary)`
flex-shrink: 0;
flex-grow: 0;
${NudeButton} {
&:hover,
&[aria-expanded="true"] {
background: ${(props) => props.theme.sidebarControlHoverBackground};
}
}
${breakpoint("tablet")`
display: flex;
`};

View File

@@ -1,22 +1,26 @@
import fractionalIndex from "fractional-index";
import { observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { useDrop, useDrag, DropTargetMonitor } from "react-dnd";
import { useTranslation } from "react-i18next";
import { useLocation, useHistory } from "react-router-dom";
import { useLocation, useHistory, Link } from "react-router-dom";
import styled from "styled-components";
import { sortNavigationNodes } from "@shared/utils/collections";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
import DocumentReparent from "~/scenes/DocumentReparent";
import CollectionIcon from "~/components/CollectionIcon";
import Fade from "~/components/Fade";
import Modal from "~/components/Modal";
import NudeButton from "~/components/NudeButton";
import Tooltip from "~/components/Tooltip";
import useBoolean from "~/hooks/useBoolean";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import CollectionMenu from "~/menus/CollectionMenu";
import CollectionSortMenu from "~/menus/CollectionSortMenu";
import { NavigationNode } from "~/types";
import { newDocumentPath } from "~/utils/routeHelpers";
import DocumentLink from "./DocumentLink";
import DropCursor from "./DropCursor";
import DropToImport from "./DropToImport";
@@ -254,20 +258,25 @@ function CollectionLink({
menu={
!isEditing &&
!isDraggingAnyCollection && (
<>
{can.update && displayDocumentLinks && (
<CollectionSortMenu
collection={collection}
onOpen={handleMenuOpen}
onClose={handleMenuClose}
/>
<Fade>
{can.update && (
<Tooltip tooltip={t("New doc")} delay={500}>
<NudeButton
type={undefined}
aria-label={t("New document")}
as={Link}
to={newDocumentPath(collection.id)}
>
<PlusIcon />
</NudeButton>
</Tooltip>
)}
<CollectionMenu
collection={collection}
onOpen={handleMenuOpen}
onClose={handleMenuClose}
/>
</>
</Fade>
)
}
/>

View File

@@ -11,8 +11,10 @@ import Collection from "~/models/Collection";
import Document from "~/models/Document";
import Fade from "~/components/Fade";
import NudeButton from "~/components/NudeButton";
import Tooltip from "~/components/Tooltip";
import useBoolean from "~/hooks/useBoolean";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import DocumentMenu from "~/menus/DocumentMenu";
import { NavigationNode } from "~/types";
import { newDocumentPath } from "~/utils/routeHelpers";
@@ -47,6 +49,7 @@ function DocumentLink(
}: Props,
ref: React.RefObject<HTMLAnchorElement>
) {
const { showToast } = useToasts();
const { documents, policies } = useStores();
const { t } = useTranslation();
const isActiveDocument = activeDocument && activeDocument.id === node.id;
@@ -225,6 +228,19 @@ function DocumentLink(
const [{ isOverReorder, isDraggingAnyDocument }, dropToReorder] = useDrop({
accept: "document",
drop: (item: DragObject) => {
if (!manualSort) {
showToast(
t(
"You can't reorder documents in an alphabetically sorted collection"
),
{
type: "info",
timeout: 5000,
}
);
return;
}
if (!collection) {
return;
}
@@ -327,16 +343,18 @@ function DocumentLink(
!isDraggingAnyDocument ? (
<Fade>
{can.createChildDocument && (
<NudeButton
type={undefined}
aria-label={t("New nested document")}
as={Link}
to={newDocumentPath(document.collectionId, {
parentDocumentId: document.id,
})}
>
<PlusIcon />
</NudeButton>
<Tooltip tooltip={t("New doc")} delay={500}>
<NudeButton
type={undefined}
aria-label={t("New nested document")}
as={Link}
to={newDocumentPath(document.collectionId, {
parentDocumentId: document.id,
})}
>
<PlusIcon />
</NudeButton>
</Tooltip>
)}
<DocumentMenu
document={document}
@@ -350,8 +368,12 @@ function DocumentLink(
</DropToImport>
</div>
</Draggable>
{manualSort && isDraggingAnyDocument && (
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
{isDraggingAnyDocument && (
<DropCursor
disabled={!manualSort}
isActiveDrop={isOverReorder}
innerRef={dropToReorder}
/>
)}
</Relative>
{openedOnce && (

View File

@@ -1,20 +1,30 @@
import * as React from "react";
import styled from "styled-components";
function DropCursor({
isActiveDrop,
innerRef,
position,
}: {
type Props = {
disabled?: boolean;
isActiveDrop: boolean;
innerRef: React.Ref<HTMLDivElement>;
position?: "top";
}) {
return <Cursor isOver={isActiveDrop} ref={innerRef} position={position} />;
};
function DropCursor({ isActiveDrop, innerRef, position, disabled }: Props) {
return (
<Cursor
isOver={isActiveDrop}
disabled={disabled}
ref={innerRef}
position={position}
/>
);
}
// transparent hover zone with a thin visible band vertically centered
const Cursor = styled.div<{ isOver?: boolean; position?: "top" }>`
const Cursor = styled.div<{
isOver?: boolean;
disabled?: boolean;
position?: "top";
}>`
opacity: ${(props) => (props.isOver ? 1 : 0)};
transition: opacity 150ms;
position: absolute;
@@ -26,7 +36,10 @@ const Cursor = styled.div<{ isOver?: boolean; position?: "top" }>`
${(props) => (props.position === "top" ? "top: -7px;" : "bottom: -7px;")}
::after {
background: ${(props) => props.theme.slateDark};
background: ${(props) =>
props.disabled
? props.theme.sidebarActiveBackground
: props.theme.slateDark};
position: absolute;
top: 6px;
content: "";

View File

@@ -71,7 +71,8 @@ const Wrapper = styled(Flex)<{ minHeight: number }>`
cursor: pointer;
&:active,
&:hover {
&:hover,
&[aria-expanded="true"] {
color: ${(props) => props.theme.sidebarText};
transition: background 100ms ease-in-out;
background: ${(props) => props.theme.sidebarActiveBackground};

View File

@@ -190,13 +190,12 @@ const Link = styled(NavLink)<{ $isActiveDrop?: boolean; $isDraft?: boolean }>`
& + ${Actions} {
${NudeButton} {
background: ${(props) => props.theme.sidebarBackground};
}
}
background: transparent;
&[aria-current="page"] + ${Actions} {
${NudeButton} {
background: ${(props) => props.theme.sidebarActiveBackground};
&:hover,
&[aria-expanded="true"] {
background: ${(props) => props.theme.sidebarControlHoverBackground};
}
}
}

View File

@@ -6,6 +6,8 @@ import {
ImportIcon,
ExportIcon,
PadlockIcon,
AlphabeticalSortIcon,
ManualSortIcon,
} from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
@@ -124,6 +126,20 @@ function CollectionMenu({
[history, showToast, collection.id, documents]
);
const handleChangeSort = React.useCallback(
(field: string) => {
menu.hide();
return collection.save({
sort: {
field,
direction: "asc",
},
});
},
[collection, menu]
);
const alphabeticalSort = collection.sort.field === "title";
const can = usePolicy(collection.id);
const canUserInTeam = usePolicy(team.id);
const items: MenuItem[] = React.useMemo(
@@ -145,6 +161,30 @@ function CollectionMenu({
{
type: "separator",
},
{
type: "submenu",
title: t("Sort in sidebar"),
visible: can.update,
icon: alphabeticalSort ? (
<AlphabeticalSortIcon color="currentColor" />
) : (
<ManualSortIcon color="currentColor" />
),
items: [
{
type: "button",
title: t("Alphabetical sort"),
onClick: () => handleChangeSort("title"),
selected: alphabeticalSort,
},
{
type: "button",
title: t("Manual sort"),
onClick: () => handleChangeSort("index"),
selected: !alphabeticalSort,
},
],
},
{
type: "button",
title: `${t("Edit")}`,
@@ -182,6 +222,8 @@ function CollectionMenu({
t,
can.update,
can.delete,
alphabeticalSort,
handleChangeSort,
handleNewDocument,
handleImportDocument,
collection,

View File

@@ -1,73 +0,0 @@
import { observer } from "mobx-react";
import { AlphabeticalSortIcon, ManualSortIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useMenuState, MenuButton } from "reakit/Menu";
import Collection from "~/models/Collection";
import ContextMenu from "~/components/ContextMenu";
import Template from "~/components/ContextMenu/Template";
import NudeButton from "~/components/NudeButton";
type Props = {
collection: Collection;
onOpen?: () => void;
onClose?: () => void;
};
function CollectionSortMenu({ collection, onOpen, onClose }: Props) {
const { t } = useTranslation();
const menu = useMenuState({
modal: true,
});
const handleChangeSort = React.useCallback(
(field: string) => {
menu.hide();
return collection.save({
sort: {
field,
direction: "asc",
},
});
},
[collection, menu]
);
const alphabeticalSort = collection.sort.field === "title";
return (
<>
<MenuButton {...menu}>
{(props) => (
<NudeButton aria-label={t("Show sort menu")} {...props}>
{alphabeticalSort ? <AlphabeticalSortIcon /> : <ManualSortIcon />}
</NudeButton>
)}
</MenuButton>
<ContextMenu
{...menu}
onOpen={onOpen}
onClose={onClose}
aria-label={t("Sort in sidebar")}
>
<Template
{...menu}
items={[
{
type: "button",
title: t("Alphabetical sort"),
onClick: () => handleChangeSort("title"),
selected: alphabeticalSort,
},
{
type: "button",
title: t("Manual sort"),
onClick: () => handleChangeSort("index"),
selected: !alphabeticalSort,
},
]}
/>
</ContextMenu>
</>
);
}
export default observer(CollectionSortMenu);

View File

@@ -139,6 +139,7 @@
"Document archived": "Document archived",
"Move document": "Move document",
"Collections": "Collections",
"You can't reorder documents in an alphabetically sorted collection": "You can't reorder documents in an alphabetically sorted collection",
"Untitled": "Untitled",
"New nested document": "New nested document",
"Document not supported try Markdown, Plain text, HTML, or Word": "Document not supported try Markdown, Plain text, HTML, or Word",
@@ -231,16 +232,15 @@
"Path to document": "Path to document",
"Group member options": "Group member options",
"Remove": "Remove",
"Sort in sidebar": "Sort in sidebar",
"Alphabetical sort": "Alphabetical sort",
"Manual sort": "Manual sort",
"Edit": "Edit",
"Permissions": "Permissions",
"Delete": "Delete",
"Collection permissions": "Collection permissions",
"Delete collection": "Delete collection",
"Export collection": "Export collection",
"Show sort menu": "Show sort menu",
"Sort in sidebar": "Sort in sidebar",
"Alphabetical sort": "Alphabetical sort",
"Manual sort": "Manual sort",
"Document restored": "Document restored",
"Document unpublished": "Document unpublished",
"Document options": "Document options",

View File

@@ -135,7 +135,7 @@ export const light = {
placeholder: "#a2b2c3",
sidebarBackground: colors.warmGrey,
sidebarActiveBackground: "#d7e0ea",
sidebarControlHoverBackground: "rgba(0,0,0,0.1)",
sidebarControlHoverBackground: "rgb(138 164 193 / 20%)",
sidebarDraftBorder: darken("0.25", colors.warmGrey),
sidebarText: "rgb(78, 92, 110)",
backdrop: "rgba(0, 0, 0, 0.2)",