From 2ffc0ae81c44dd9516baf278c0257a2afeafe66f Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 21 Apr 2021 18:15:07 -0700 Subject: [PATCH] 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 --- app/components/Guide.js | 112 ++++ app/components/Input.js | 4 + app/components/InputSearch.js | 11 +- app/components/Layout.js | 6 +- app/menus/AccountMenu.js | 6 +- app/scenes/Collection.js | 1 + .../components/KeyboardShortcutsButton.js | 71 ++- app/scenes/Drafts.js | 1 + app/scenes/Home.js | 1 + app/scenes/KeyboardShortcuts.js | 531 +++++++++++++----- app/scenes/Starred.js | 1 + shared/i18n/locales/en_US/translation.json | 28 +- 12 files changed, 568 insertions(+), 205 deletions(-) create mode 100644 app/components/Guide.js diff --git a/app/components/Guide.js b/app/components/Guide.js new file mode 100644 index 00000000..fea2a7e2 --- /dev/null +++ b/app/components/Guide.js @@ -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 ( + + {(props) => ( + + + {(props) => ( + + + {title &&
{title}
} + {children} +
+
+ )} +
+
+ )} +
+ ); +}; + +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); diff --git a/app/components/Input.js b/app/components/Input.js index f279af69..135c879f 100644 --- a/app/components/Input.js +++ b/app/components/Input.js @@ -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; `}; diff --git a/app/components/InputSearch.js b/app/components/InputSearch.js index c5db89b7..8cc696b5 100644 --- a/app/components/InputSearch.js +++ b/app/components/InputSearch.js @@ -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 { }; 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 { ref={(ref) => (this.input = ref)} type="search" placeholder={placeholder} - onInput={this.handleSearchInput} + onInput={redirectDisabled ? undefined : this.handleSearchInput} + onChange={onChange} icon={ { } 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 { } const InputMaxWidth = styled(Input)` - max-width: 30vw; + max-width: ${(props) => props.maxWidth}; `; export default withTranslation()( diff --git a/app/components/Layout.js b/app/components/Layout.js index 5bda5222..69200995 100644 --- a/app/components/Layout.js +++ b/app/components/Layout.js @@ -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 { /> - - + ); } diff --git a/app/menus/AccountMenu.js b/app/menus/AccountMenu.js index 9085fd6a..2768191b 100644 --- a/app/menus/AccountMenu.js +++ b/app/menus/AccountMenu.js @@ -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 ( <> - setKeyboardShortcutsOpen(false)} title={t("Keyboard shortcuts")} > - + {props.children} diff --git a/app/scenes/Collection.js b/app/scenes/Collection.js index 321bd2dc..d5c354d3 100644 --- a/app/scenes/Collection.js +++ b/app/scenes/Collection.js @@ -126,6 +126,7 @@ function CollectionScene() { label={`${t("Search in collection")}…`} labelHidden collectionId={collectionId} + maxWidth="30vw" /> {can.update && ( diff --git a/app/scenes/Document/components/KeyboardShortcutsButton.js b/app/scenes/Document/components/KeyboardShortcutsButton.js index a55af958..266f2dd7 100644 --- a/app/scenes/Document/components/KeyboardShortcutsButton.js +++ b/app/scenes/Document/components/KeyboardShortcutsButton.js @@ -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 { - @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 ( - <> - - - - - - - - ); - } + return ( + <> + + + + + + + + ); } const Button = styled(NudeButton)` diff --git a/app/scenes/Drafts.js b/app/scenes/Drafts.js index f5e6acbf..81bbd41d 100644 --- a/app/scenes/Drafts.js +++ b/app/scenes/Drafts.js @@ -86,6 +86,7 @@ class Drafts extends React.Component { diff --git a/app/scenes/Home.js b/app/scenes/Home.js index fee9c978..08290506 100644 --- a/app/scenes/Home.js +++ b/app/scenes/Home.js @@ -32,6 +32,7 @@ function Home() { diff --git a/app/scenes/KeyboardShortcuts.js b/app/scenes/KeyboardShortcuts.js index a4e56c31..2ce59361 100644 --- a/app/scenes/KeyboardShortcuts.js +++ b/app/scenes/KeyboardShortcuts.js @@ -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: n, + label: t("New document"), + }, + { shortcut: e, label: t("Edit current document") }, + { shortcut: m, label: t("Move current document") }, + { + shortcut: ( + <> + / or t + + ), + label: t("Jump to search"), + }, + { shortcut: d, label: t("Jump to home") }, + { + shortcut: ( + <> + Ctrl + {metaDisplay} + h + + ), + label: t("Table of contents"), + }, + { + shortcut: ( + <> + {metaDisplay} + . + + ), + label: t("Toggle navigation"), + }, + { + shortcut: ( + <> + {metaDisplay} + f + + ), + label: t("Focus search input"), + }, + { shortcut: ?, label: t("Open this guide") }, + { + shortcut: ( + <> + {metaDisplay} + Enter + + ), + label: t("Save document and exit"), + }, + { + shortcut: ( + <> + {metaDisplay} + + p + + ), + label: t("Publish document and exit"), + }, + { + shortcut: ( + <> + {metaDisplay} + s + + ), + label: t("Save document"), + }, + { + shortcut: ( + <> + {metaDisplay} + Esc + + ), + label: t("Cancel editing"), + }, + ], + }, + { + title: t("Formatting"), + items: [ + { + shortcut: ( + <> + Ctrl + + 0 + + ), + label: t("Paragraph"), + }, + { + shortcut: ( + <> + Ctrl + + 1 + + ), + label: t("Large header"), + }, + { + shortcut: ( + <> + Ctrl + + 2 + + ), + label: t("Medium header"), + }, + { + shortcut: ( + <> + Ctrl + + 3 + + ), + label: t("Small header"), + }, + { + shortcut: ( + <> + Ctrl + + \ + + ), + label: t("Code block"), + }, + { + shortcut: ( + <> + {metaDisplay} + b + + ), + label: t("Bold"), + }, + { + shortcut: ( + <> + {metaDisplay} + i + + ), + label: t("Italic"), + }, + { + shortcut: ( + <> + {metaDisplay} + u + + ), + label: t("Underline"), + }, + { + shortcut: ( + <> + {metaDisplay} + d + + ), + label: t("Strikethrough"), + }, + { + shortcut: ( + <> + {metaDisplay} + k + + ), + label: t("Link"), + }, + { + shortcut: ( + <> + {metaDisplay} + z + + ), + label: t("Undo"), + }, + { + shortcut: ( + <> + {metaDisplay} + + z + + ), + label: t("Redo"), + }, + ], + }, + { + title: t("Lists"), + items: [ + { + shortcut: ( + <> + Ctrl + + 7 + + ), + label: t("Todo list"), + }, + { + shortcut: ( + <> + Ctrl + + 8 + + ), + label: t("Bulleted list"), + }, + { + shortcut: ( + <> + Ctrl + + 9 + + ), + label: t("Ordered list"), + }, + { + shortcut: Tab, + label: t("Indent list item"), + }, + { + shortcut: ( + <> + + Tab + + ), + label: t("Outdent list item"), + }, + { + shortcut: ( + <> + Alt + + + ), + label: t("Move list item up"), + }, + { + shortcut: ( + <> + Alt + + + ), + label: t("Move list item down"), + }, + ], + }, + { + title: "Markdown", + items: [ + { + shortcut: ( + <> + # Space + + ), + label: t("Large header"), + }, + { + shortcut: ( + <> + ## Space + + ), + label: t("Medium header"), + }, + { + shortcut: ( + <> + ### Space + + ), + label: t("Small header"), + }, + { + shortcut: ( + <> + 1. Space + + ), + label: t("Numbered list"), + }, + { + shortcut: ( + <> + - Space + + ), + label: t("Bulleted list"), + }, + { + shortcut: ( + <> + [ ] Space + + ), + label: t("Todo list"), + }, + { + shortcut: ( + <> + > Space + + ), + label: t("Blockquote"), + }, + { + shortcut: ---, + label: t("Horizontal divider"), + }, + { + shortcut: {"```"}, + label: t("Code block"), + }, + { + shortcut: {":::"}, + 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 ( - - {t( - "Outline is designed to be fast and easy to use. All of your usual keyboard shortcuts work here, and there’s Markdown too." - )} - + + {categories.map((category, x) => { + const filtered = searchTerm + ? category.items.filter((item) => + item.label.toLowerCase().includes(searchTerm) + ) + : category.items; -

{t("Navigation")}

- - - n - - - - e - - - - m - - - - / or t - - - - d - - - - {metaDisplay} + Ctrl + h - - - - {metaDisplay} + . - - - - ? - - - + if (!filtered.length) { + return null; + } -

{t("Editor")}

- - - {metaDisplay} + Enter - - - - {metaDisplay} + Shift + p - - - - {metaDisplay} + s - - - - {metaDisplay} + Esc - - - - {metaDisplay} + b - - - - {metaDisplay} + i - - - - {metaDisplay} + u - - - - {metaDisplay} + d - - - - {metaDisplay} + k - - - - {metaDisplay} + z - - - - {metaDisplay} + Shift + z - - - - -

{t("Markdown")}

- - - # Space - - - - ## Space - - - - ### Space - - - - - 1. Space - - - - - Space - - - - [ ] Space - - - - > Space - - - - --- - - - - {"```"} - - - - {":::"} - - - - _italic_ - - **bold** - - ~~strikethrough~~ - - {"`code`"} - - ==highlight== - - + return ( + +
{category.title}
+ + {filtered.map((item) => ( + + + {item.shortcut} + + + + ))} + +
+ ); + })}
); } +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; diff --git a/app/scenes/Starred.js b/app/scenes/Starred.js index dd5145db..85bd58d9 100644 --- a/app/scenes/Starred.js +++ b/app/scenes/Starred.js @@ -35,6 +35,7 @@ function Starred(props: Props) { diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index a4f77c24..b02dbb31 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -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 there’s Markdown too.": "Outline is designed to be fast and easy to use. All of your usual keyboard shortcuts work here, and there’s 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",