fix: Inset icon in collection headers, minor ContentEditable refactor (#3168)

This commit is contained in:
Tom Moor
2022-02-25 20:38:46 -08:00
committed by GitHub
parent 7bb12b3f6d
commit ccacb65d9e
5 changed files with 79 additions and 54 deletions

View File

@@ -81,6 +81,17 @@ const ContentEditable = React.forwardRef(
} }
}, [value, ref]); }, [value, ref]);
// Ensure only plain text can be pasted into title when pasting from another
// rich text editor
const handlePaste = React.useCallback(
(event: React.ClipboardEvent<HTMLSpanElement>) => {
event.preventDefault();
const text = event.clipboardData.getData("text/plain");
window.document.execCommand("insertText", false, text);
},
[]
);
return ( return (
<div className={className} dir={dir} onClick={onClick}> <div className={className} dir={dir} onClick={onClick}>
<Content <Content
@@ -89,6 +100,7 @@ const ContentEditable = React.forwardRef(
onInput={wrappedEvent(onInput)} onInput={wrappedEvent(onInput)}
onBlur={wrappedEvent(onBlur)} onBlur={wrappedEvent(onBlur)}
onKeyDown={wrappedEvent(onKeyDown)} onKeyDown={wrappedEvent(onKeyDown)}
onPaste={handlePaste}
data-placeholder={placeholder} data-placeholder={placeholder}
suppressContentEditableWarning suppressContentEditableWarning
role="textbox" role="textbox"
@@ -103,6 +115,14 @@ const ContentEditable = React.forwardRef(
); );
const Content = styled.span` const Content = styled.span`
background: ${(props) => props.theme.background};
transition: ${(props) => props.theme.backgroundTransition};
color: ${(props) => props.theme.text};
-webkit-text-fill-color: ${(props) => props.theme.text};
outline: none;
resize: none;
cursor: text;
&:empty { &:empty {
display: inline-block; display: inline-block;
} }

View File

@@ -5,14 +5,6 @@ const Heading = styled.h1<{ centered?: boolean }>`
align-items: center; align-items: center;
user-select: none; user-select: none;
${(props) => (props.centered ? "text-align: center;" : "")} ${(props) => (props.centered ? "text-align: center;" : "")}
svg {
margin-top: 4px;
margin-left: -6px;
margin-right: 2px;
align-self: flex-start;
flex-shrink: 0;
}
`; `;
export default Heading; export default Heading;

View File

@@ -0,0 +1,33 @@
import * as React from "react";
type Options = {
fontSize?: string;
lineHeight?: string;
};
/**
* Measures the width of an emoji character
*/
export default function useEmojiWidth(
emoji: string | undefined,
{ fontSize = "2.25em", lineHeight = "1.25" }: Options
) {
return React.useMemo(() => {
const element = window.document.createElement("span");
if (!emoji) {
return 0;
}
element.innerText = `${emoji}\u00A0`;
element.style.visibility = "hidden";
element.style.position = "absolute";
element.style.left = "-9999px";
element.style.lineHeight = lineHeight;
element.style.fontSize = fontSize;
element.style.width = "max-content";
window.document.body?.appendChild(element);
const width = window.getComputedStyle(element).width;
window.document.body?.removeChild(element);
return parseInt(width, 10);
}, [emoji, fontSize, lineHeight]);
}

View File

@@ -9,6 +9,8 @@ import {
useHistory, useHistory,
useRouteMatch, useRouteMatch,
} from "react-router-dom"; } from "react-router-dom";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import Collection from "~/models/Collection"; import Collection from "~/models/Collection";
import Search from "~/scenes/Search"; import Search from "~/scenes/Search";
import Badge from "~/components/Badge"; import Badge from "~/components/Badge";
@@ -107,8 +109,7 @@ function CollectionScene() {
title={ title={
<> <>
<CollectionIcon collection={collection} expanded /> <CollectionIcon collection={collection} expanded />
&nbsp; &nbsp;{collection.name}
{collection.name}
</> </>
} }
actions={<Actions collection={collection} />} actions={<Actions collection={collection} />}
@@ -123,9 +124,9 @@ function CollectionScene() {
<Empty collection={collection} /> <Empty collection={collection} />
) : ( ) : (
<> <>
<Heading> <HeadingWithIcon>
<CollectionIcon collection={collection} size={40} expanded />{" "} <HeadingIcon collection={collection} size={40} expanded />
{collection.name}{" "} {collection.name}
{!collection.permission && ( {!collection.permission && (
<Tooltip <Tooltip
tooltip={t( tooltip={t(
@@ -136,7 +137,7 @@ function CollectionScene() {
<Badge>{t("Private")}</Badge> <Badge>{t("Private")}</Badge>
</Tooltip> </Tooltip>
)} )}
</Heading> </HeadingWithIcon>
<CollectionDescription collection={collection} /> <CollectionDescription collection={collection} />
<PinnedDocuments <PinnedDocuments
@@ -243,4 +244,18 @@ function CollectionScene() {
); );
} }
const HeadingWithIcon = styled(Heading)`
display: flex;
align-items: center;
${breakpoint("tablet")`
margin-left: -40px;
`};
`;
const HeadingIcon = styled(CollectionIcon)`
align-self: flex-start;
flex-shrink: 0;
`;
export default observer(CollectionScene); export default observer(CollectionScene);

View File

@@ -7,6 +7,7 @@ import { light } from "@shared/theme";
import Document from "~/models/Document"; import Document from "~/models/Document";
import ContentEditable from "~/components/ContentEditable"; import ContentEditable from "~/components/ContentEditable";
import Star, { AnimatedStar } from "~/components/Star"; import Star, { AnimatedStar } from "~/components/Star";
import useEmojiWidth from "~/hooks/useEmojiWidth";
import usePolicy from "~/hooks/usePolicy"; import usePolicy from "~/hooks/usePolicy";
import { isModKey } from "~/utils/keyboard"; import { isModKey } from "~/utils/keyboard";
@@ -51,17 +52,6 @@ const EditableTitle = React.forwardRef(
ref.current?.focus(); ref.current?.focus();
}, [ref]); }, [ref]);
// Ensure only plain text can be pasted into title when pasting from another
// rich text editor
const handlePaste = React.useCallback(
(event: React.ClipboardEvent<HTMLSpanElement>) => {
event.preventDefault();
const text = event.clipboardData.getData("text/plain");
window.document.execCommand("insertText", false, text);
},
[]
);
const handleKeyDown = React.useCallback( const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent) => { (event: React.KeyboardEvent) => {
if (event.key === "Enter") { if (event.key === "Enter") {
@@ -102,34 +92,16 @@ const EditableTitle = React.forwardRef(
[onGoToNextInput, onSave] [onGoToNextInput, onSave]
); );
/** const emojiWidth = useEmojiWidth(document.emoji, {
* Measures the width of the document's emoji in the title fontSize,
*/ lineHeight,
const emojiWidth = React.useMemo(() => { });
const element = window.document.createElement("span");
if (!document.emoji) {
return 0;
}
element.innerText = `${document.emoji}\u00A0`;
element.style.visibility = "hidden";
element.style.position = "absolute";
element.style.left = "-9999px";
element.style.lineHeight = lineHeight;
element.style.fontSize = fontSize;
element.style.width = "max-content";
window.document.body?.appendChild(element);
const width = window.getComputedStyle(element).width;
window.document.body?.removeChild(element);
return parseInt(width, 10);
}, [document.emoji]);
return ( return (
<Title <Title
onClick={handleClick} onClick={handleClick}
onChange={onChange} onChange={onChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onPaste={handlePaste}
placeholder={placeholder} placeholder={placeholder}
value={normalizedTitle} value={normalizedTitle}
$emojiWidth={emojiWidth} $emojiWidth={emojiWidth}
@@ -170,17 +142,10 @@ const Title = styled(ContentEditable)<TitleProps>`
line-height: ${lineHeight}; line-height: ${lineHeight};
margin-top: 1em; margin-top: 1em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
background: ${(props) => props.theme.background};
transition: ${(props) => props.theme.backgroundTransition};
color: ${(props) => props.theme.text};
-webkit-text-fill-color: ${(props) => props.theme.text};
font-size: ${fontSize}; font-size: ${fontSize};
font-weight: 500; font-weight: 500;
outline: none;
border: 0; border: 0;
padding: 0; padding: 0;
resize: none;
cursor: text;
> span { > span {
outline: none; outline: none;