feat: New keyboard shortcuts guide (#2051)

* feat: Add search

* feat: New design for keyboard shortcuts guide
feat: Include quick search
fix: Add missing shortcuts

* tweaks

* fix: Two other spots that should trigger guide-style instead of modal

* sink,lift -> indent,outdent

* fix: Animation should slide out as well as in
This commit is contained in:
Tom Moor 2021-04-21 18:15:07 -07:00 committed by GitHub
parent 50fdd73610
commit 2ffc0ae81c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 568 additions and 205 deletions

112
app/components/Guide.js Normal file
View File

@ -0,0 +1,112 @@
// @flow
import { observer } from "mobx-react";
import * as React from "react";
import { Dialog, DialogBackdrop, useDialogState } from "reakit/Dialog";
import styled from "styled-components";
import Scrollable from "components/Scrollable";
import usePrevious from "hooks/usePrevious";
type Props = {|
children?: React.Node,
isOpen: boolean,
title?: string,
onRequestClose: () => void,
|};
const Guide = ({
children,
isOpen,
title = "Untitled",
onRequestClose,
...rest
}: Props) => {
const dialog = useDialogState({ animated: 250 });
const wasOpen = usePrevious(isOpen);
React.useEffect(() => {
if (!wasOpen && isOpen) {
dialog.show();
}
if (wasOpen && !isOpen) {
dialog.hide();
}
}, [dialog, wasOpen, isOpen]);
return (
<DialogBackdrop {...dialog}>
{(props) => (
<Backdrop {...props}>
<Dialog
{...dialog}
aria-label={title}
preventBodyScrollhideOnEsc
hide={onRequestClose}
>
{(props) => (
<Scene {...props} {...rest}>
<Content>
{title && <Header>{title}</Header>}
{children}
</Content>
</Scene>
)}
</Dialog>
</Backdrop>
)}
</DialogBackdrop>
);
};
const Header = styled.h1`
font-size: 18px;
margin-top: 0;
margin-bottom: 1em;
`;
const Backdrop = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: ${(props) => props.theme.backdrop} !important;
z-index: ${(props) => props.theme.depths.modalOverlay};
transition: opacity 200ms ease-in-out;
opacity: 0;
&[data-enter] {
opacity: 1;
}
`;
const Scene = styled.div`
position: absolute;
top: 0;
right: 0;
bottom: 0;
margin: 12px;
z-index: ${(props) => props.theme.depths.modal};
display: flex;
justify-content: center;
align-items: flex-start;
width: 350px;
background: ${(props) => props.theme.background};
transition: ${(props) => props.theme.backgroundTransition};
border-radius: 8px;
outline: none;
opacity: 0;
transform: translateX(16px);
transition: transform 250ms ease, opacity 250ms ease;
&[data-enter] {
opacity: 1;
transform: translateX(0px);
}
`;
const Content = styled(Scrollable)`
width: 100%;
padding: 16px;
`;
export default observer(Guide);

View File

@ -35,6 +35,10 @@ const RealInput = styled.input`
color: ${(props) => props.theme.placeholder};
}
&::-webkit-search-cancel-button {
-webkit-appearance: none;
}
${breakpoint("mobile", "tablet")`
font-size: 16px;
`};

View File

@ -20,6 +20,9 @@ type Props = {
label?: string,
labelHidden?: boolean,
collectionId?: string,
redirectDisabled?: boolean,
maxWidth?: string,
onChange: (event: SyntheticInputEvent<>) => mixed,
t: TFunction,
};
@ -56,7 +59,7 @@ class InputSearch extends React.Component<Props> {
};
render() {
const { t } = this.props;
const { t, redirectDisabled, onChange } = this.props;
const { theme, placeholder = `${t("Search")}` } = this.props;
return (
@ -64,7 +67,8 @@ class InputSearch extends React.Component<Props> {
ref={(ref) => (this.input = ref)}
type="search"
placeholder={placeholder}
onInput={this.handleSearchInput}
onInput={redirectDisabled ? undefined : this.handleSearchInput}
onChange={onChange}
icon={
<SearchIcon
color={this.focused ? theme.inputBorderFocused : theme.inputBorder}
@ -72,6 +76,7 @@ class InputSearch extends React.Component<Props> {
}
label={this.props.label}
labelHidden={this.props.labelHidden}
maxWidth={this.props.maxWidth}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
margin={0}
@ -81,7 +86,7 @@ class InputSearch extends React.Component<Props> {
}
const InputMaxWidth = styled(Input)`
max-width: 30vw;
max-width: ${(props) => props.maxWidth};
`;
export default withTranslation()<InputSearch>(

View File

@ -24,8 +24,8 @@ import KeyboardShortcuts from "scenes/KeyboardShortcuts";
import Button from "components/Button";
import DocumentHistory from "components/DocumentHistory";
import Flex from "components/Flex";
import Guide from "components/Guide";
import { LoadingIndicatorBar } from "components/LoadingIndicator";
import Modal from "components/Modal";
import Sidebar from "components/Sidebar";
import SettingsSidebar from "components/Sidebar/Settings";
import SkipNavContent from "components/SkipNavContent";
@ -161,13 +161,13 @@ class Layout extends React.Component<Props> {
/>
</Switch>
</Container>
<Modal
<Guide
isOpen={this.keyboardShortcutsOpen}
onRequestClose={this.handleCloseKeyboardShortcuts}
title={t("Keyboard shortcuts")}
>
<KeyboardShortcuts />
</Modal>
</Guide>
</Container>
);
}

View File

@ -18,7 +18,7 @@ import ContextMenu from "components/ContextMenu";
import MenuItem, { MenuAnchor } from "components/ContextMenu/MenuItem";
import Separator from "components/ContextMenu/Separator";
import Flex from "components/Flex";
import Modal from "components/Modal";
import Guide from "components/Guide";
import usePrevious from "hooks/usePrevious";
import useStores from "hooks/useStores";
@ -90,13 +90,13 @@ function AccountMenu(props: Props) {
return (
<>
<Modal
<Guide
isOpen={keyboardShortcutsOpen}
onRequestClose={() => setKeyboardShortcutsOpen(false)}
title={t("Keyboard shortcuts")}
>
<KeyboardShortcuts />
</Modal>
</Guide>
<MenuButton {...menu}>{props.children}</MenuButton>
<ContextMenu {...menu} aria-label={t("Account")}>
<MenuItem {...menu} as={Link} to={settings()}>

View File

@ -126,6 +126,7 @@ function CollectionScene() {
label={`${t("Search in collection")}`}
labelHidden
collectionId={collectionId}
maxWidth="30vw"
/>
</Action>
{can.update && (

View File

@ -1,52 +1,49 @@
// @flow
import { observable } from "mobx";
import { observer } from "mobx-react";
import { KeyboardIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import KeyboardShortcuts from "scenes/KeyboardShortcuts";
import Modal from "components/Modal";
import Guide from "components/Guide";
import NudeButton from "components/NudeButton";
import Tooltip from "components/Tooltip";
type Props = {};
function KeyboardShortcutsButton() {
const { t } = useTranslation();
const [keyboardShortcutsOpen, setKeyboardShortcutsOpen] = React.useState(
false
);
@observer
class KeyboardShortcutsButton extends React.Component<Props> {
@observable keyboardShortcutsOpen: boolean = false;
const handleCloseKeyboardShortcuts = React.useCallback(() => {
setKeyboardShortcutsOpen(false);
}, []);
handleOpenKeyboardShortcuts = () => {
this.keyboardShortcutsOpen = true;
};
const handleOpenKeyboardShortcuts = React.useCallback(() => {
setKeyboardShortcutsOpen(true);
}, []);
handleCloseKeyboardShortcuts = () => {
this.keyboardShortcutsOpen = false;
};
render() {
return (
<>
<Modal
isOpen={this.keyboardShortcutsOpen}
onRequestClose={this.handleCloseKeyboardShortcuts}
title="Keyboard shortcuts"
>
<KeyboardShortcuts />
</Modal>
<Tooltip
tooltip="Keyboard shortcuts"
shortcut="?"
placement="left"
delay={500}
>
<Button onClick={this.handleOpenKeyboardShortcuts}>
<KeyboardIcon />
</Button>
</Tooltip>
</>
);
}
return (
<>
<Guide
isOpen={keyboardShortcutsOpen}
onRequestClose={handleCloseKeyboardShortcuts}
title={t("Keyboard shortcuts")}
>
<KeyboardShortcuts />
</Guide>
<Tooltip
tooltip={t("Keyboard shortcuts")}
shortcut="?"
placement="left"
delay={500}
>
<Button onClick={handleOpenKeyboardShortcuts}>
<KeyboardIcon />
</Button>
</Tooltip>
</>
);
}
const Button = styled(NudeButton)`

View File

@ -86,6 +86,7 @@ class Drafts extends React.Component<Props> {
<InputSearch
source="drafts"
label={t("Search documents")}
maxWidth="30vw"
labelHidden
/>
</Action>

View File

@ -32,6 +32,7 @@ function Home() {
<InputSearch
source="dashboard"
label={t("Search documents")}
maxWidth="30vw"
labelHidden
/>
</Action>

View File

@ -3,165 +3,395 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Flex from "components/Flex";
import HelpText from "components/HelpText";
import Input from "components/InputSearch";
import Key from "components/Key";
import { metaDisplay } from "utils/keyboard";
function KeyboardShortcuts() {
const { t } = useTranslation();
const categories = React.useMemo(
() => [
{
title: t("Navigation"),
items: [
{
shortcut: <Key>n</Key>,
label: t("New document"),
},
{ shortcut: <Key>e</Key>, label: t("Edit current document") },
{ shortcut: <Key>m</Key>, label: t("Move current document") },
{
shortcut: (
<>
<Key>/</Key> or <Key>t</Key>
</>
),
label: t("Jump to search"),
},
{ shortcut: <Key>d</Key>, label: t("Jump to home") },
{
shortcut: (
<>
<Key>Ctrl</Key> + <Key>{metaDisplay}</Key> + <Key>h</Key>
</>
),
label: t("Table of contents"),
},
{
shortcut: (
<>
<Key>{metaDisplay}</Key> + <Key>.</Key>
</>
),
label: t("Toggle navigation"),
},
{
shortcut: (
<>
<Key>{metaDisplay}</Key> + <Key>f</Key>
</>
),
label: t("Focus search input"),
},
{ shortcut: <Key>?</Key>, label: t("Open this guide") },
{
shortcut: (
<>
<Key>{metaDisplay}</Key> + <Key>Enter</Key>
</>
),
label: t("Save document and exit"),
},
{
shortcut: (
<>
<Key>{metaDisplay}</Key> + <Key></Key> + <Key>p</Key>
</>
),
label: t("Publish document and exit"),
},
{
shortcut: (
<>
<Key>{metaDisplay}</Key> + <Key>s</Key>
</>
),
label: t("Save document"),
},
{
shortcut: (
<>
<Key>{metaDisplay}</Key> + <Key>Esc</Key>
</>
),
label: t("Cancel editing"),
},
],
},
{
title: t("Formatting"),
items: [
{
shortcut: (
<>
<Key>Ctrl</Key> + <Key></Key> + <Key>0</Key>
</>
),
label: t("Paragraph"),
},
{
shortcut: (
<>
<Key>Ctrl</Key> + <Key></Key> + <Key>1</Key>
</>
),
label: t("Large header"),
},
{
shortcut: (
<>
<Key>Ctrl</Key> + <Key></Key> + <Key>2</Key>
</>
),
label: t("Medium header"),
},
{
shortcut: (
<>
<Key>Ctrl</Key> + <Key></Key> + <Key>3</Key>
</>
),
label: t("Small header"),
},
{
shortcut: (
<>
<Key>Ctrl</Key> + <Key></Key> + <Key>\</Key>
</>
),
label: t("Code block"),
},
{
shortcut: (
<>
<Key>{metaDisplay}</Key> + <Key>b</Key>
</>
),
label: t("Bold"),
},
{
shortcut: (
<>
<Key>{metaDisplay}</Key> + <Key>i</Key>
</>
),
label: t("Italic"),
},
{
shortcut: (
<>
<Key>{metaDisplay}</Key> + <Key>u</Key>
</>
),
label: t("Underline"),
},
{
shortcut: (
<>
<Key>{metaDisplay}</Key> + <Key>d</Key>
</>
),
label: t("Strikethrough"),
},
{
shortcut: (
<>
<Key>{metaDisplay}</Key> + <Key>k</Key>
</>
),
label: t("Link"),
},
{
shortcut: (
<>
<Key>{metaDisplay}</Key> + <Key>z</Key>
</>
),
label: t("Undo"),
},
{
shortcut: (
<>
<Key>{metaDisplay}</Key> + <Key></Key> + <Key>z</Key>
</>
),
label: t("Redo"),
},
],
},
{
title: t("Lists"),
items: [
{
shortcut: (
<>
<Key>Ctrl</Key> + <Key></Key> + <Key>7</Key>
</>
),
label: t("Todo list"),
},
{
shortcut: (
<>
<Key>Ctrl</Key> + <Key></Key> + <Key>8</Key>
</>
),
label: t("Bulleted list"),
},
{
shortcut: (
<>
<Key>Ctrl</Key> + <Key></Key> + <Key>9</Key>
</>
),
label: t("Ordered list"),
},
{
shortcut: <Key>Tab</Key>,
label: t("Indent list item"),
},
{
shortcut: (
<>
<Key></Key> + <Key>Tab</Key>
</>
),
label: t("Outdent list item"),
},
{
shortcut: (
<>
<Key>Alt</Key> + <Key></Key>
</>
),
label: t("Move list item up"),
},
{
shortcut: (
<>
<Key>Alt</Key> + <Key></Key>
</>
),
label: t("Move list item down"),
},
],
},
{
title: "Markdown",
items: [
{
shortcut: (
<>
<Key>#</Key> <Key>Space</Key>
</>
),
label: t("Large header"),
},
{
shortcut: (
<>
<Key>##</Key> <Key>Space</Key>
</>
),
label: t("Medium header"),
},
{
shortcut: (
<>
<Key>###</Key> <Key>Space</Key>
</>
),
label: t("Small header"),
},
{
shortcut: (
<>
<Key>1.</Key> <Key>Space</Key>
</>
),
label: t("Numbered list"),
},
{
shortcut: (
<>
<Key>-</Key> <Key>Space</Key>
</>
),
label: t("Bulleted list"),
},
{
shortcut: (
<>
<Key>[ ]</Key> <Key>Space</Key>
</>
),
label: t("Todo list"),
},
{
shortcut: (
<>
<Key>&gt;</Key> <Key>Space</Key>
</>
),
label: t("Blockquote"),
},
{
shortcut: <Key>---</Key>,
label: t("Horizontal divider"),
},
{
shortcut: <Key>{"```"}</Key>,
label: t("Code block"),
},
{
shortcut: <Key>{":::"}</Key>,
label: t("Info notice"),
},
{
shortcut: "_italic_",
label: t("Italic"),
},
{
shortcut: "**bold**",
label: t("Bold"),
},
{
shortcut: "~~strikethrough~~",
label: t("Strikethrough"),
},
{
shortcut: "`code`",
label: t("Inline code"),
},
{
shortcut: "==highlight==",
label: t("Highlight"),
},
],
},
],
[t]
);
const [searchTerm, setSearchTerm] = React.useState("");
const handleChange = React.useCallback((event) => {
setSearchTerm(event.target.value.toLowerCase());
}, []);
return (
<Flex column>
<HelpText>
{t(
"Outline is designed to be fast and easy to use. All of your usual keyboard shortcuts work here, and theres Markdown too."
)}
</HelpText>
<Input type="search" onChange={handleChange} redirectDisabled />
{categories.map((category, x) => {
const filtered = searchTerm
? category.items.filter((item) =>
item.label.toLowerCase().includes(searchTerm)
)
: category.items;
<h2>{t("Navigation")}</h2>
<List>
<Keys>
<Key>n</Key>
</Keys>
<Label>{t("New document in current collection")}</Label>
<Keys>
<Key>e</Key>
</Keys>
<Label>{t("Edit current document")}</Label>
<Keys>
<Key>m</Key>
</Keys>
<Label>{t("Move current document")}</Label>
<Keys>
<Key>/</Key> or <Key>t</Key>
</Keys>
<Label>{t("Jump to search")}</Label>
<Keys>
<Key>d</Key>
</Keys>
<Label>{t("Jump to dashboard")}</Label>
<Keys>
<Key>{metaDisplay}</Key> + <Key>Ctrl</Key> + <Key>h</Key>
</Keys>
<Label>{t("Table of contents")}</Label>
<Keys>
<Key>{metaDisplay}</Key> + <Key>.</Key>
</Keys>
<Label>{t("Toggle sidebar")}</Label>
<Keys>
<Key>?</Key>
</Keys>
<Label>{t("Open this guide")}</Label>
</List>
if (!filtered.length) {
return null;
}
<h2>{t("Editor")}</h2>
<List>
<Keys>
<Key>{metaDisplay}</Key> + <Key>Enter</Key>
</Keys>
<Label>{t("Save and exit document edit mode")}</Label>
<Keys>
<Key>{metaDisplay}</Key> + <Key>Shift</Key> + <Key>p</Key>
</Keys>
<Label>{t("Publish and exit document edit mode")}</Label>
<Keys>
<Key>{metaDisplay}</Key> + <Key>s</Key>
</Keys>
<Label>{t("Save document and continue editing")}</Label>
<Keys>
<Key>{metaDisplay}</Key> + <Key>Esc</Key>
</Keys>
<Label>{t("Cancel editing")}</Label>
<Keys>
<Key>{metaDisplay}</Key> + <Key>b</Key>
</Keys>
<Label>{t("Bold")}</Label>
<Keys>
<Key>{metaDisplay}</Key> + <Key>i</Key>
</Keys>
<Label>{t("Italic")}</Label>
<Keys>
<Key>{metaDisplay}</Key> + <Key>u</Key>
</Keys>
<Label>{t("Underline")}</Label>
<Keys>
<Key>{metaDisplay}</Key> + <Key>d</Key>
</Keys>
<Label>{t("Strikethrough")}</Label>
<Keys>
<Key>{metaDisplay}</Key> + <Key>k</Key>
</Keys>
<Label>{t("Link")}</Label>
<Keys>
<Key>{metaDisplay}</Key> + <Key>z</Key>
</Keys>
<Label>{t("Undo")}</Label>
<Keys>
<Key>{metaDisplay}</Key> + <Key>Shift</Key> + <Key>z</Key>
</Keys>
<Label>{t("Redo")}</Label>
</List>
<h2>{t("Markdown")}</h2>
<List>
<Keys>
<Key>#</Key> <Key>Space</Key>
</Keys>
<Label>{t("Large header")}</Label>
<Keys>
<Key>##</Key> <Key>Space</Key>
</Keys>
<Label>{t("Medium header")}</Label>
<Keys>
<Key>###</Key> <Key>Space</Key>
</Keys>
<Label>{t("Small header")}</Label>
<Keys>
<Key>1.</Key> <Key>Space</Key>
</Keys>
<Label>{t("Numbered list")}</Label>
<Keys>
<Key>-</Key> <Key>Space</Key>
</Keys>
<Label>{t("Bulleted list")}</Label>
<Keys>
<Key>[ ]</Key> <Key>Space</Key>
</Keys>
<Label>{t("Todo list")}</Label>
<Keys>
<Key>&gt;</Key> <Key>Space</Key>
</Keys>
<Label>{t("Blockquote")}</Label>
<Keys>
<Key>---</Key>
</Keys>
<Label>{t("Horizontal divider")}</Label>
<Keys>
<Key>{"```"}</Key>
</Keys>
<Label>{t("Code block")}</Label>
<Keys>
<Key>{":::"}</Key>
</Keys>
<Label>{t("Info notice")}</Label>
<Keys>_italic_</Keys>
<Label>{t("Italic")}</Label>
<Keys>**bold**</Keys>
<Label>{t("Bold")}</Label>
<Keys>~~strikethrough~~</Keys>
<Label>{t("Strikethrough")}</Label>
<Keys>{"`code`"}</Keys>
<Label>{t("Inline code")}</Label>
<Keys>==highlight==</Keys>
<Label>{t("Highlight")}</Label>
</List>
return (
<React.Fragment key={x}>
<Header>{category.title}</Header>
<List>
{filtered.map((item) => (
<React.Fragment key={item.label}>
<Keys>
<span>{item.shortcut}</span>
</Keys>
<Label>{item.label}</Label>
</React.Fragment>
))}
</List>
</React.Fragment>
);
})}
</Flex>
);
}
const Header = styled.h2`
font-size: 15px;
font-weight: 500;
margin-top: 2em;
`;
const List = styled.dl`
font-size: 14px;
width: 100%;
overflow: hidden;
padding: 0;
@ -169,19 +399,26 @@ const List = styled.dl`
`;
const Keys = styled.dt`
float: left;
width: 25%;
float: right;
width: 45%;
height: 30px;
margin: 0;
text-align: right;
font-size: 12px;
color: ${(props) => props.theme.textSecondary};
display: flex;
align-items: center;
justify-content: flex-end;
`;
const Label = styled.dd`
float: left;
width: 75%;
width: 55%;
height: 30px;
margin: 0;
display: flex;
align-items: center;
color: ${(props) => props.theme.textSecondary};
`;
export default KeyboardShortcuts;

View File

@ -35,6 +35,7 @@ function Starred(props: Props) {
<InputSearch
source="starred"
label={t("Search documents")}
maxWidth="30vw"
labelHidden
/>
</Action>

View File

@ -301,28 +301,32 @@
"This group has no members.": "This group has no members.",
"Recently viewed": "Recently viewed",
"Created by me": "Created by me",
"Outline is designed to be fast and easy to use. All of your usual keyboard shortcuts work here, and theres Markdown too.": "Outline is designed to be fast and easy to use. All of your usual keyboard shortcuts work here, and theres Markdown too.",
"Navigation": "Navigation",
"New document in current collection": "New document in current collection",
"Edit current document": "Edit current document",
"Move current document": "Move current document",
"Jump to search": "Jump to search",
"Jump to dashboard": "Jump to dashboard",
"Jump to home": "Jump to home",
"Table of contents": "Table of contents",
"Toggle sidebar": "Toggle sidebar",
"Toggle navigation": "Toggle navigation",
"Focus search input": "Focus search input",
"Open this guide": "Open this guide",
"Editor": "Editor",
"Save and exit document edit mode": "Save and exit document edit mode",
"Publish and exit document edit mode": "Publish and exit document edit mode",
"Save document and continue editing": "Save document and continue editing",
"Save document and exit": "Save document and exit",
"Publish document and exit": "Publish document and exit",
"Save document": "Save document",
"Cancel editing": "Cancel editing",
"Underline": "Underline",
"Undo": "Undo",
"Redo": "Redo",
"Markdown": "Markdown",
"Formatting": "Formatting",
"Paragraph": "Paragraph",
"Large header": "Large header",
"Medium header": "Medium header",
"Small header": "Small header",
"Underline": "Underline",
"Undo": "Undo",
"Redo": "Redo",
"Lists": "Lists",
"Indent list item": "Indent list item",
"Outdent list item": "Outdent list item",
"Move list item up": "Move list item up",
"Move list item down": "Move list item down",
"Numbered list": "Numbered list",
"Blockquote": "Blockquote",
"Horizontal divider": "Horizontal divider",