diff --git a/app/actions/definitions/navigation.tsx b/app/actions/definitions/navigation.tsx index 825b2a0d0..d566737d4 100644 --- a/app/actions/definitions/navigation.tsx +++ b/app/actions/definitions/navigation.tsx @@ -17,7 +17,7 @@ import { changelogUrl, mailToUrl, githubIssuesUrl, -} from "@shared/utils/routeHelpers"; +} from "@shared/utils/urlHelpers"; import stores from "~/stores"; import KeyboardShortcuts from "~/scenes/KeyboardShortcuts"; import { createAction } from "~/actions"; diff --git a/app/components/ContextMenu/MenuItem.tsx b/app/components/ContextMenu/MenuItem.tsx index bc707a5d3..372141c8b 100644 --- a/app/components/ContextMenu/MenuItem.tsx +++ b/app/components/ContextMenu/MenuItem.tsx @@ -3,6 +3,7 @@ import * as React from "react"; import { MenuItem as BaseMenuItem } from "reakit/Menu"; import styled, { css } from "styled-components"; import breakpoint from "styled-components-breakpoint"; +import { hover } from "~/styles"; import MenuIconWrapper from "../MenuIconWrapper"; type Props = { @@ -123,7 +124,7 @@ export const MenuAnchorCSS = css<{ level?: number; disabled?: boolean }>` ? "pointer-events: none;" : ` - &:hover, + &:${hover}, &:focus, &.focus-visible { color: ${props.theme.white}; diff --git a/app/components/DocumentListItem.tsx b/app/components/DocumentListItem.tsx index a9a879eb5..19f4bc496 100644 --- a/app/components/DocumentListItem.tsx +++ b/app/components/DocumentListItem.tsx @@ -19,6 +19,7 @@ import useCurrentTeam from "~/hooks/useCurrentTeam"; import useCurrentUser from "~/hooks/useCurrentUser"; import useStores from "~/hooks/useStores"; import DocumentMenu from "~/menus/DocumentMenu"; +import { hover } from "~/styles"; import { newDocumentPath } from "~/utils/routeHelpers"; type Props = { @@ -200,7 +201,7 @@ const DocumentLink = styled(Link)<{ opacity: ${(props) => (props.$isStarred ? "1 !important" : 0)}; } - &:hover, + &:${hover}, &:active, &:focus, &:focus-within { diff --git a/app/components/ErrorBoundary.tsx b/app/components/ErrorBoundary.tsx index ec13ebdb1..5fb542720 100644 --- a/app/components/ErrorBoundary.tsx +++ b/app/components/ErrorBoundary.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import { withTranslation, Trans, WithTranslation } from "react-i18next"; import styled from "styled-components"; -import { githubIssuesUrl } from "@shared/utils/routeHelpers"; +import { githubIssuesUrl } from "@shared/utils/urlHelpers"; import Button from "~/components/Button"; import CenteredContent from "~/components/CenteredContent"; import HelpText from "~/components/HelpText"; diff --git a/app/components/Star.tsx b/app/components/Star.tsx index d2e9c9c9a..c91a6ec25 100644 --- a/app/components/Star.tsx +++ b/app/components/Star.tsx @@ -3,6 +3,7 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; import styled, { useTheme } from "styled-components"; import Document from "~/models/Document"; +import { hover } from "~/styles"; import NudeButton from "./NudeButton"; type Props = { @@ -56,7 +57,7 @@ export const AnimatedStar = styled(StarredIcon)` flex-shrink: 0; transition: all 100ms ease-in-out; - &:hover { + &: ${hover} { transform: scale(1.1); } &:active { diff --git a/app/editor/components/BlockMenu.tsx b/app/editor/components/BlockMenu.tsx index c4ec0b20b..d7052249a 100644 --- a/app/editor/components/BlockMenu.tsx +++ b/app/editor/components/BlockMenu.tsx @@ -10,13 +10,9 @@ type BlockMenuProps = Omit< > & Required>; -class BlockMenu extends React.Component { - get items() { - return getMenuItems(this.props.dictionary); - } - - clearSearch = () => { - const { state, dispatch } = this.props.view; +function BlockMenu(props: BlockMenuProps) { + const clearSearch = () => { + const { state, dispatch } = props.view; const parent = findParentNode((node) => !!node)(state.selection); if (parent) { @@ -24,27 +20,25 @@ class BlockMenu extends React.Component { } }; - render() { - return ( - { - return ( - - ); - }} - items={this.items} - /> - ); - } + return ( + { + return ( + + ); + }} + items={getMenuItems(props.dictionary)} + /> + ); } export default BlockMenu; diff --git a/app/editor/components/BlockMenuItem.tsx b/app/editor/components/BlockMenuItem.tsx index 951bd6aaf..4f5268843 100644 --- a/app/editor/components/BlockMenuItem.tsx +++ b/app/editor/components/BlockMenuItem.tsx @@ -29,7 +29,7 @@ function BlockMenuItem({ if (selected && node) { scrollIntoView(node, { scrollMode: "if-needed", - block: "center", + block: "nearest", boundary: (parent) => { // All the parent elements of your target are checked until they // reach the #block-menu-container. Prevents body and other parent @@ -64,6 +64,12 @@ function BlockMenuItem({ ); } +const Shortcut = styled.span` + color: ${(props) => props.theme.textTertiary}; + flex-grow: 1; + text-align: right; +`; + const MenuItem = styled.button<{ selected: boolean; }>` @@ -90,7 +96,6 @@ const MenuItem = styled.button<{ padding: 0 16px; outline: none; - &:hover, &:active { color: ${(props) => props.theme.blockToolbarTextSelected}; background: ${(props) => @@ -98,13 +103,11 @@ const MenuItem = styled.button<{ ? props.theme.blockToolbarSelectedBackground || props.theme.blockToolbarTrigger : props.theme.blockToolbarHoverBackground}; + + ${Shortcut} { + color: ${(props) => props.theme.textSecondary}; + } } `; -const Shortcut = styled.span` - color: ${(props) => props.theme.textSecondary}; - flex-grow: 1; - text-align: right; -`; - export default BlockMenuItem; diff --git a/app/editor/components/CommandMenu.tsx b/app/editor/components/CommandMenu.tsx index 49bc5d7ee..62d387d9c 100644 --- a/app/editor/components/CommandMenu.tsx +++ b/app/editor/components/CommandMenu.tsx @@ -84,6 +84,11 @@ class CommandMenu extends React.Component, State> { componentDidUpdate(prevProps: Props) { if (!prevProps.isActive && this.props.isActive) { + // reset scroll position to top when opening menu as the contents are + // hidden, not unrendered + if (this.menuRef.current) { + this.menuRef.current.scroll({ top: 0 }); + } const position = this.calculatePosition(this.props); this.setState({ @@ -485,16 +490,25 @@ class CommandMenu extends React.Component, State> { ); } - const selected = index === this.state.selectedIndex && isActive; if (!item.title) { return null; } + const handlePointer = () => { + if (this.state.selectedIndex !== index) { + this.setState({ selectedIndex: index }); + } + }; + return ( - + {this.props.renderMenuItem(item as any, index, { - selected, + selected: index === this.state.selectedIndex, onClick: () => this.insertItem(item), })} diff --git a/app/editor/components/Styles.ts b/app/editor/components/Styles.ts index 89b2104d8..4f4f37558 100644 --- a/app/editor/components/Styles.ts +++ b/app/editor/components/Styles.ts @@ -540,7 +540,8 @@ const EditorStyles = styled.div<{ ul.checkbox_list { list-style: none; padding: 0; - margin: ${(props) => (props.rtl ? "0 -24px 0 0" : "0 0 0 -24px")}; + margin-left: ${(props) => (props.rtl ? "0" : "-24px")}; + margin-right: ${(props) => (props.rtl ? "-24px" : "0")}; } ul li, diff --git a/app/editor/menus/block.ts b/app/editor/menus/block.ts index e38d3dc55..466919501 100644 --- a/app/editor/menus/block.ts +++ b/app/editor/menus/block.ts @@ -18,10 +18,7 @@ import { } from "outline-icons"; import { MenuItem } from "@shared/editor/types"; import { Dictionary } from "~/hooks/useDictionary"; - -const SSR = typeof window === "undefined"; -const isMac = !SSR && window.navigator.platform === "MacIntel"; -const mod = isMac ? "⌘" : "ctrl"; +import { metaDisplay } from "~/utils/keyboard"; export default function blockMenuItems(dictionary: Dictionary): MenuItem[] { return [ @@ -84,7 +81,7 @@ export default function blockMenuItems(dictionary: Dictionary): MenuItem[] { name: "blockquote", title: dictionary.quote, icon: BlockQuoteIcon, - shortcut: `${mod} ]`, + shortcut: `${metaDisplay} ]`, }, { name: "code_block", @@ -97,7 +94,7 @@ export default function blockMenuItems(dictionary: Dictionary): MenuItem[] { name: "hr", title: dictionary.hr, icon: HorizontalRuleIcon, - shortcut: `${mod} _`, + shortcut: `${metaDisplay} _`, keywords: "horizontal rule break line", }, { @@ -117,7 +114,7 @@ export default function blockMenuItems(dictionary: Dictionary): MenuItem[] { name: "link", title: dictionary.link, icon: LinkIcon, - shortcut: `${mod} k`, + shortcut: `${metaDisplay} k`, keywords: "link url uri href", }, { diff --git a/app/scenes/Document/components/ReferenceListItem.tsx b/app/scenes/Document/components/ReferenceListItem.tsx index 5f9305ebf..39d390307 100644 --- a/app/scenes/Document/components/ReferenceListItem.tsx +++ b/app/scenes/Document/components/ReferenceListItem.tsx @@ -6,6 +6,7 @@ import styled from "styled-components"; import Document from "~/models/Document"; import DocumentMeta from "~/components/DocumentMeta"; import Flex from "~/components/Flex"; +import { hover } from "~/styles"; import { NavigationNode } from "~/types"; type Props = { @@ -25,7 +26,7 @@ const DocumentLink = styled(Link)` overflow: hidden; position: relative; - &:hover, + &:${hover}, &:active, &:focus { background: ${(props) => props.theme.listItemHoverBackground}; diff --git a/app/scenes/Search/components/RecentSearches.tsx b/app/scenes/Search/components/RecentSearches.tsx index b064e51ac..d34937629 100644 --- a/app/scenes/Search/components/RecentSearches.tsx +++ b/app/scenes/Search/components/RecentSearches.tsx @@ -8,6 +8,7 @@ import Fade from "~/components/Fade"; import NudeButton from "~/components/NudeButton"; import Tooltip from "~/components/Tooltip"; import useStores from "~/hooks/useStores"; +import { hover } from "~/styles"; import { searchUrl } from "~/utils/routeHelpers"; function RecentSearches() { @@ -90,7 +91,7 @@ const RecentSearch = styled(Link)` padding: 1px 4px; border-radius: 4px; - &:hover { + &: ${hover} { color: ${(props) => props.theme.text}; background: ${(props) => props.theme.secondaryBackground}; diff --git a/app/scenes/Settings/components/SlackButton.tsx b/app/scenes/Settings/components/SlackButton.tsx index f93668f82..0076bd6b4 100644 --- a/app/scenes/Settings/components/SlackButton.tsx +++ b/app/scenes/Settings/components/SlackButton.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; -import { slackAuth } from "@shared/utils/routeHelpers"; +import { slackAuth } from "@shared/utils/urlHelpers"; import Button from "~/components/Button"; import env from "~/env"; diff --git a/app/styles/index.ts b/app/styles/index.ts new file mode 100644 index 000000000..d872d5244 --- /dev/null +++ b/app/styles/index.ts @@ -0,0 +1,8 @@ +import { isTouchDevice } from "~/utils/browser"; + +/** + * Returns "hover" on a non-touch device and "active" on a touch device. To + * avoid "sticky" hover on mobile. Use `&:${hover} {...}` instead of + * using `&:hover {...}`. + */ +export const hover = isTouchDevice() ? "active" : "hover"; diff --git a/app/utils/browser.ts b/app/utils/browser.ts new file mode 100644 index 000000000..a01dceaa7 --- /dev/null +++ b/app/utils/browser.ts @@ -0,0 +1,17 @@ +/** + * Returns true if the client is a touch device. + */ +export function isTouchDevice(): boolean { + if (typeof window === "undefined") { + return false; + } + return window.matchMedia?.("(hover: none) and (pointer: coarse)")?.matches; +} + +/** + * Returns true if the client is running on a Mac. + */ +export function isMac(): boolean { + const SSR = typeof window === "undefined"; + return !SSR && window.navigator.platform === "MacIntel"; +} diff --git a/app/utils/keyboard.ts b/app/utils/keyboard.ts index c4e6a880a..ee34f79a7 100644 --- a/app/utils/keyboard.ts +++ b/app/utils/keyboard.ts @@ -1,11 +1,11 @@ -const isMac = window.navigator.platform === "MacIntel"; +import { isMac } from "~/utils/browser"; -export const metaDisplay = isMac ? "⌘" : "Ctrl"; +export const metaDisplay = isMac() ? "⌘" : "Ctrl"; -export const meta = isMac ? "cmd" : "ctrl"; +export const meta = isMac() ? "cmd" : "ctrl"; export function isModKey( event: KeyboardEvent | MouseEvent | React.KeyboardEvent ) { - return isMac ? event.metaKey : event.ctrlKey; + return isMac() ? event.metaKey : event.ctrlKey; } diff --git a/server/emails/components/Footer.tsx b/server/emails/components/Footer.tsx index d13ef94d8..44eb46395 100644 --- a/server/emails/components/Footer.tsx +++ b/server/emails/components/Footer.tsx @@ -1,7 +1,7 @@ import { Table, TBody, TR, TD } from "oy-vey"; import * as React from "react"; import theme from "@shared/theme"; -import { twitterUrl } from "@shared/utils/routeHelpers"; +import { twitterUrl } from "@shared/utils/urlHelpers"; type Props = { unsubscribeUrl?: string; diff --git a/server/models/Collection.ts b/server/models/Collection.ts index fe479a346..efe9fab78 100644 --- a/server/models/Collection.ts +++ b/server/models/Collection.ts @@ -27,7 +27,7 @@ import { } from "sequelize-typescript"; import isUUID from "validator/lib/isUUID"; import { sortNavigationNodes } from "@shared/utils/collections"; -import { SLUG_URL_REGEX } from "@shared/utils/routeHelpers"; +import { SLUG_URL_REGEX } from "@shared/utils/urlHelpers"; import slugify from "@server/utils/slugify"; import { NavigationNode, CollectionSort } from "~/types"; import CollectionGroup from "./CollectionGroup"; diff --git a/server/models/Document.ts b/server/models/Document.ts index 683f278b1..6a551f0bd 100644 --- a/server/models/Document.ts +++ b/server/models/Document.ts @@ -32,8 +32,8 @@ import { MAX_TITLE_LENGTH } from "@shared/constants"; import { DateFilter } from "@shared/types"; import getTasks from "@shared/utils/getTasks"; import parseTitle from "@shared/utils/parseTitle"; -import { SLUG_URL_REGEX } from "@shared/utils/routeHelpers"; import unescape from "@shared/utils/unescape"; +import { SLUG_URL_REGEX } from "@shared/utils/urlHelpers"; import slugify from "@server/utils/slugify"; import Backlink from "./Backlink"; import Collection from "./Collection"; diff --git a/server/routes/auth/providers/index.ts b/server/routes/auth/providers/index.ts index d58752130..831437d39 100644 --- a/server/routes/auth/providers/index.ts +++ b/server/routes/auth/providers/index.ts @@ -1,5 +1,5 @@ import Router from "koa-router"; -import { signin } from "@shared/utils/routeHelpers"; +import { signin } from "@shared/utils/urlHelpers"; import { requireDirectory } from "@server/utils/fs"; interface AuthenicationProvider { diff --git a/shared/utils/routeHelpers.ts b/shared/utils/urlHelpers.ts similarity index 97% rename from shared/utils/routeHelpers.ts rename to shared/utils/urlHelpers.ts index 190bd5cb7..d931a4841 100644 --- a/shared/utils/routeHelpers.ts +++ b/shared/utils/urlHelpers.ts @@ -32,7 +32,7 @@ export function githubIssuesUrl(): string { } export function twitterUrl(): string { - return "https://twitter.com/outlinewiki"; + return "https://twitter.com/getoutline"; } export function mailToUrl(): string {