fix: Bag 'o fixes

Remove menu hover styles on mobile
Fixed duplicate hover+active behavior on editor menus
Fixed editor menus visibly scroll to the top when reopened
Fixed some minor editor spacing issues
Renamed shred routeHelpers -> urlHelpers
This commit is contained in:
Tom Moor
2022-01-25 23:43:11 -08:00
parent 13b8ed58fd
commit 175857753e
21 changed files with 103 additions and 64 deletions

View File

@@ -17,7 +17,7 @@ import {
changelogUrl, changelogUrl,
mailToUrl, mailToUrl,
githubIssuesUrl, githubIssuesUrl,
} from "@shared/utils/routeHelpers"; } from "@shared/utils/urlHelpers";
import stores from "~/stores"; import stores from "~/stores";
import KeyboardShortcuts from "~/scenes/KeyboardShortcuts"; import KeyboardShortcuts from "~/scenes/KeyboardShortcuts";
import { createAction } from "~/actions"; import { createAction } from "~/actions";

View File

@@ -3,6 +3,7 @@ import * as React from "react";
import { MenuItem as BaseMenuItem } from "reakit/Menu"; import { MenuItem as BaseMenuItem } from "reakit/Menu";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
import { hover } from "~/styles";
import MenuIconWrapper from "../MenuIconWrapper"; import MenuIconWrapper from "../MenuIconWrapper";
type Props = { type Props = {
@@ -123,7 +124,7 @@ export const MenuAnchorCSS = css<{ level?: number; disabled?: boolean }>`
? "pointer-events: none;" ? "pointer-events: none;"
: ` : `
&:hover, &:${hover},
&:focus, &:focus,
&.focus-visible { &.focus-visible {
color: ${props.theme.white}; color: ${props.theme.white};

View File

@@ -19,6 +19,7 @@ import useCurrentTeam from "~/hooks/useCurrentTeam";
import useCurrentUser from "~/hooks/useCurrentUser"; import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores"; import useStores from "~/hooks/useStores";
import DocumentMenu from "~/menus/DocumentMenu"; import DocumentMenu from "~/menus/DocumentMenu";
import { hover } from "~/styles";
import { newDocumentPath } from "~/utils/routeHelpers"; import { newDocumentPath } from "~/utils/routeHelpers";
type Props = { type Props = {
@@ -200,7 +201,7 @@ const DocumentLink = styled(Link)<{
opacity: ${(props) => (props.$isStarred ? "1 !important" : 0)}; opacity: ${(props) => (props.$isStarred ? "1 !important" : 0)};
} }
&:hover, &:${hover},
&:active, &:active,
&:focus, &:focus,
&:focus-within { &:focus-within {

View File

@@ -4,7 +4,7 @@ import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { withTranslation, Trans, WithTranslation } from "react-i18next"; import { withTranslation, Trans, WithTranslation } from "react-i18next";
import styled from "styled-components"; import styled from "styled-components";
import { githubIssuesUrl } from "@shared/utils/routeHelpers"; import { githubIssuesUrl } from "@shared/utils/urlHelpers";
import Button from "~/components/Button"; import Button from "~/components/Button";
import CenteredContent from "~/components/CenteredContent"; import CenteredContent from "~/components/CenteredContent";
import HelpText from "~/components/HelpText"; import HelpText from "~/components/HelpText";

View File

@@ -3,6 +3,7 @@ import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import styled, { useTheme } from "styled-components"; import styled, { useTheme } from "styled-components";
import Document from "~/models/Document"; import Document from "~/models/Document";
import { hover } from "~/styles";
import NudeButton from "./NudeButton"; import NudeButton from "./NudeButton";
type Props = { type Props = {
@@ -56,7 +57,7 @@ export const AnimatedStar = styled(StarredIcon)`
flex-shrink: 0; flex-shrink: 0;
transition: all 100ms ease-in-out; transition: all 100ms ease-in-out;
&:hover { &: ${hover} {
transform: scale(1.1); transform: scale(1.1);
} }
&:active { &:active {

View File

@@ -10,13 +10,9 @@ type BlockMenuProps = Omit<
> & > &
Required<Pick<Props, "onLinkToolbarOpen" | "embeds">>; Required<Pick<Props, "onLinkToolbarOpen" | "embeds">>;
class BlockMenu extends React.Component<BlockMenuProps> { function BlockMenu(props: BlockMenuProps) {
get items() { const clearSearch = () => {
return getMenuItems(this.props.dictionary); const { state, dispatch } = props.view;
}
clearSearch = () => {
const { state, dispatch } = this.props.view;
const parent = findParentNode((node) => !!node)(state.selection); const parent = findParentNode((node) => !!node)(state.selection);
if (parent) { if (parent) {
@@ -24,27 +20,25 @@ class BlockMenu extends React.Component<BlockMenuProps> {
} }
}; };
render() { return (
return ( <CommandMenu
<CommandMenu {...props}
{...this.props} filterable={true}
filterable={true} onClearSearch={clearSearch}
onClearSearch={this.clearSearch} renderMenuItem={(item, _index, options) => {
renderMenuItem={(item, _index, options) => { return (
return ( <BlockMenuItem
<BlockMenuItem onClick={options.onClick}
onClick={options.onClick} selected={options.selected}
selected={options.selected} icon={item.icon}
icon={item.icon} title={item.title}
title={item.title} shortcut={item.shortcut}
shortcut={item.shortcut} />
/> );
); }}
}} items={getMenuItems(props.dictionary)}
items={this.items} />
/> );
);
}
} }
export default BlockMenu; export default BlockMenu;

View File

@@ -29,7 +29,7 @@ function BlockMenuItem({
if (selected && node) { if (selected && node) {
scrollIntoView(node, { scrollIntoView(node, {
scrollMode: "if-needed", scrollMode: "if-needed",
block: "center", block: "nearest",
boundary: (parent) => { boundary: (parent) => {
// All the parent elements of your target are checked until they // All the parent elements of your target are checked until they
// reach the #block-menu-container. Prevents body and other parent // 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<{ const MenuItem = styled.button<{
selected: boolean; selected: boolean;
}>` }>`
@@ -90,7 +96,6 @@ const MenuItem = styled.button<{
padding: 0 16px; padding: 0 16px;
outline: none; outline: none;
&:hover,
&:active { &:active {
color: ${(props) => props.theme.blockToolbarTextSelected}; color: ${(props) => props.theme.blockToolbarTextSelected};
background: ${(props) => background: ${(props) =>
@@ -98,13 +103,11 @@ const MenuItem = styled.button<{
? props.theme.blockToolbarSelectedBackground || ? props.theme.blockToolbarSelectedBackground ||
props.theme.blockToolbarTrigger props.theme.blockToolbarTrigger
: props.theme.blockToolbarHoverBackground}; : 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; export default BlockMenuItem;

View File

@@ -84,6 +84,11 @@ class CommandMenu<T = MenuItem> extends React.Component<Props<T>, State> {
componentDidUpdate(prevProps: Props) { componentDidUpdate(prevProps: Props) {
if (!prevProps.isActive && this.props.isActive) { 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); const position = this.calculatePosition(this.props);
this.setState({ this.setState({
@@ -485,16 +490,25 @@ class CommandMenu<T = MenuItem> extends React.Component<Props<T>, State> {
</ListItem> </ListItem>
); );
} }
const selected = index === this.state.selectedIndex && isActive;
if (!item.title) { if (!item.title) {
return null; return null;
} }
const handlePointer = () => {
if (this.state.selectedIndex !== index) {
this.setState({ selectedIndex: index });
}
};
return ( return (
<ListItem key={index}> <ListItem
key={index}
onPointerMove={handlePointer}
onPointerDown={handlePointer}
>
{this.props.renderMenuItem(item as any, index, { {this.props.renderMenuItem(item as any, index, {
selected, selected: index === this.state.selectedIndex,
onClick: () => this.insertItem(item), onClick: () => this.insertItem(item),
})} })}
</ListItem> </ListItem>

View File

@@ -540,7 +540,8 @@ const EditorStyles = styled.div<{
ul.checkbox_list { ul.checkbox_list {
list-style: none; list-style: none;
padding: 0; 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, ul li,

View File

@@ -18,10 +18,7 @@ import {
} from "outline-icons"; } from "outline-icons";
import { MenuItem } from "@shared/editor/types"; import { MenuItem } from "@shared/editor/types";
import { Dictionary } from "~/hooks/useDictionary"; import { Dictionary } from "~/hooks/useDictionary";
import { metaDisplay } from "~/utils/keyboard";
const SSR = typeof window === "undefined";
const isMac = !SSR && window.navigator.platform === "MacIntel";
const mod = isMac ? "⌘" : "ctrl";
export default function blockMenuItems(dictionary: Dictionary): MenuItem[] { export default function blockMenuItems(dictionary: Dictionary): MenuItem[] {
return [ return [
@@ -84,7 +81,7 @@ export default function blockMenuItems(dictionary: Dictionary): MenuItem[] {
name: "blockquote", name: "blockquote",
title: dictionary.quote, title: dictionary.quote,
icon: BlockQuoteIcon, icon: BlockQuoteIcon,
shortcut: `${mod} ]`, shortcut: `${metaDisplay} ]`,
}, },
{ {
name: "code_block", name: "code_block",
@@ -97,7 +94,7 @@ export default function blockMenuItems(dictionary: Dictionary): MenuItem[] {
name: "hr", name: "hr",
title: dictionary.hr, title: dictionary.hr,
icon: HorizontalRuleIcon, icon: HorizontalRuleIcon,
shortcut: `${mod} _`, shortcut: `${metaDisplay} _`,
keywords: "horizontal rule break line", keywords: "horizontal rule break line",
}, },
{ {
@@ -117,7 +114,7 @@ export default function blockMenuItems(dictionary: Dictionary): MenuItem[] {
name: "link", name: "link",
title: dictionary.link, title: dictionary.link,
icon: LinkIcon, icon: LinkIcon,
shortcut: `${mod} k`, shortcut: `${metaDisplay} k`,
keywords: "link url uri href", keywords: "link url uri href",
}, },
{ {

View File

@@ -6,6 +6,7 @@ import styled from "styled-components";
import Document from "~/models/Document"; import Document from "~/models/Document";
import DocumentMeta from "~/components/DocumentMeta"; import DocumentMeta from "~/components/DocumentMeta";
import Flex from "~/components/Flex"; import Flex from "~/components/Flex";
import { hover } from "~/styles";
import { NavigationNode } from "~/types"; import { NavigationNode } from "~/types";
type Props = { type Props = {
@@ -25,7 +26,7 @@ const DocumentLink = styled(Link)`
overflow: hidden; overflow: hidden;
position: relative; position: relative;
&:hover, &:${hover},
&:active, &:active,
&:focus { &:focus {
background: ${(props) => props.theme.listItemHoverBackground}; background: ${(props) => props.theme.listItemHoverBackground};

View File

@@ -8,6 +8,7 @@ import Fade from "~/components/Fade";
import NudeButton from "~/components/NudeButton"; import NudeButton from "~/components/NudeButton";
import Tooltip from "~/components/Tooltip"; import Tooltip from "~/components/Tooltip";
import useStores from "~/hooks/useStores"; import useStores from "~/hooks/useStores";
import { hover } from "~/styles";
import { searchUrl } from "~/utils/routeHelpers"; import { searchUrl } from "~/utils/routeHelpers";
function RecentSearches() { function RecentSearches() {
@@ -90,7 +91,7 @@ const RecentSearch = styled(Link)`
padding: 1px 4px; padding: 1px 4px;
border-radius: 4px; border-radius: 4px;
&:hover { &: ${hover} {
color: ${(props) => props.theme.text}; color: ${(props) => props.theme.text};
background: ${(props) => props.theme.secondaryBackground}; background: ${(props) => props.theme.secondaryBackground};

View File

@@ -1,6 +1,6 @@
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { slackAuth } from "@shared/utils/routeHelpers"; import { slackAuth } from "@shared/utils/urlHelpers";
import Button from "~/components/Button"; import Button from "~/components/Button";
import env from "~/env"; import env from "~/env";

8
app/styles/index.ts Normal file
View File

@@ -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";

17
app/utils/browser.ts Normal file
View File

@@ -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";
}

View File

@@ -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( export function isModKey(
event: KeyboardEvent | MouseEvent | React.KeyboardEvent event: KeyboardEvent | MouseEvent | React.KeyboardEvent
) { ) {
return isMac ? event.metaKey : event.ctrlKey; return isMac() ? event.metaKey : event.ctrlKey;
} }

View File

@@ -1,7 +1,7 @@
import { Table, TBody, TR, TD } from "oy-vey"; import { Table, TBody, TR, TD } from "oy-vey";
import * as React from "react"; import * as React from "react";
import theme from "@shared/theme"; import theme from "@shared/theme";
import { twitterUrl } from "@shared/utils/routeHelpers"; import { twitterUrl } from "@shared/utils/urlHelpers";
type Props = { type Props = {
unsubscribeUrl?: string; unsubscribeUrl?: string;

View File

@@ -27,7 +27,7 @@ import {
} from "sequelize-typescript"; } from "sequelize-typescript";
import isUUID from "validator/lib/isUUID"; import isUUID from "validator/lib/isUUID";
import { sortNavigationNodes } from "@shared/utils/collections"; 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 slugify from "@server/utils/slugify";
import { NavigationNode, CollectionSort } from "~/types"; import { NavigationNode, CollectionSort } from "~/types";
import CollectionGroup from "./CollectionGroup"; import CollectionGroup from "./CollectionGroup";

View File

@@ -32,8 +32,8 @@ import { MAX_TITLE_LENGTH } from "@shared/constants";
import { DateFilter } from "@shared/types"; import { DateFilter } from "@shared/types";
import getTasks from "@shared/utils/getTasks"; import getTasks from "@shared/utils/getTasks";
import parseTitle from "@shared/utils/parseTitle"; import parseTitle from "@shared/utils/parseTitle";
import { SLUG_URL_REGEX } from "@shared/utils/routeHelpers";
import unescape from "@shared/utils/unescape"; import unescape from "@shared/utils/unescape";
import { SLUG_URL_REGEX } from "@shared/utils/urlHelpers";
import slugify from "@server/utils/slugify"; import slugify from "@server/utils/slugify";
import Backlink from "./Backlink"; import Backlink from "./Backlink";
import Collection from "./Collection"; import Collection from "./Collection";

View File

@@ -1,5 +1,5 @@
import Router from "koa-router"; import Router from "koa-router";
import { signin } from "@shared/utils/routeHelpers"; import { signin } from "@shared/utils/urlHelpers";
import { requireDirectory } from "@server/utils/fs"; import { requireDirectory } from "@server/utils/fs";
interface AuthenicationProvider { interface AuthenicationProvider {

View File

@@ -32,7 +32,7 @@ export function githubIssuesUrl(): string {
} }
export function twitterUrl(): string { export function twitterUrl(): string {
return "https://twitter.com/outlinewiki"; return "https://twitter.com/getoutline";
} }
export function mailToUrl(): string { export function mailToUrl(): string {