fix: Emoji in title positioning (#2927)

* wip

* fix measure on first render

* wip

* refactor

* tsc

* remove fragment

* refactor (again)

* cleanup
This commit is contained in:
Tom Moor
2022-01-16 17:02:33 -08:00
committed by GitHub
parent 5abc73fabc
commit d0e7f2de65
8 changed files with 40 additions and 30 deletions

View File

@@ -96,6 +96,7 @@ export default class Document extends BaseModel {
} }
} }
@computed
get emoji() { get emoji() {
const { emoji } = parseTitle(this.title); const { emoji } = parseTitle(this.title);
return emoji; return emoji;

View File

@@ -96,8 +96,8 @@ const Sticky = styled.div`
box-shadow: 1px 0 0 ${(props) => props.theme.divider}; box-shadow: 1px 0 0 ${(props) => props.theme.divider};
margin-top: 40px; margin-top: 40px;
margin-right: 32px; margin-right: 52px;
width: 224px; width: 204px;
min-height: 40px; min-height: 40px;
overflow-y: auto; overflow-y: auto;
`; `;

View File

@@ -5,7 +5,6 @@ import styled from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
import { MAX_TITLE_LENGTH } from "@shared/constants"; import { MAX_TITLE_LENGTH } from "@shared/constants";
import { light } from "@shared/theme"; import { light } from "@shared/theme";
import parseTitle from "@shared/utils/parseTitle";
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";
@@ -27,6 +26,9 @@ type Props = {
onSave?: (options: { publish?: boolean; done?: boolean }) => void; onSave?: (options: { publish?: boolean; done?: boolean }) => void;
}; };
const lineHeight = "1.25";
const fontSize = "2.25em";
const EditableTitle = React.forwardRef( const EditableTitle = React.forwardRef(
( (
{ {
@@ -43,8 +45,6 @@ const EditableTitle = React.forwardRef(
const { policies } = useStores(); const { policies } = useStores();
const { t } = useTranslation(); const { t } = useTranslation();
const can = policies.abilities(document.id); const can = policies.abilities(document.id);
const { emoji } = parseTitle(value);
const startsWithEmojiAndSpace = !!(emoji && value.startsWith(`${emoji} `));
const normalizedTitle = const normalizedTitle =
!value && readOnly ? document.titleWithDefault : value; !value && readOnly ? document.titleWithDefault : value;
@@ -88,6 +88,28 @@ const EditableTitle = React.forwardRef(
[onGoToNextInput, onSave] [onGoToNextInput, onSave]
); );
/**
* Measures the width of the document's emoji in the title
*/
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
onChange={onChange} onChange={onChange}
@@ -98,7 +120,7 @@ const EditableTitle = React.forwardRef(
: t("Start with a title…") : t("Start with a title…")
} }
value={normalizedTitle} value={normalizedTitle}
$startsWithEmojiAndSpace={startsWithEmojiAndSpace} $emojiWidth={emojiWidth}
$isStarred={document.isStarred} $isStarred={document.isStarred}
autoFocus={!value} autoFocus={!value}
maxLength={MAX_TITLE_LENGTH} maxLength={MAX_TITLE_LENGTH}
@@ -121,19 +143,19 @@ const StarButton = styled(Star)`
`; `;
type TitleProps = { type TitleProps = {
$startsWithEmojiAndSpace: boolean;
$isStarred: boolean; $isStarred: boolean;
$emojiWidth: number;
}; };
const Title = styled(ContentEditable)<TitleProps>` const Title = styled(ContentEditable)<TitleProps>`
line-height: 1.25; line-height: ${lineHeight};
margin-top: 1em; margin-top: 1em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
background: ${(props) => props.theme.background}; background: ${(props) => props.theme.background};
transition: ${(props) => props.theme.backgroundTransition}; transition: ${(props) => props.theme.backgroundTransition};
color: ${(props) => props.theme.text}; color: ${(props) => props.theme.text};
-webkit-text-fill-color: ${(props) => props.theme.text}; -webkit-text-fill-color: ${(props) => props.theme.text};
font-size: 2.25em; font-size: ${fontSize};
font-weight: 500; font-weight: 500;
outline: none; outline: none;
border: 0; border: 0;
@@ -150,8 +172,7 @@ const Title = styled(ContentEditable)<TitleProps>`
} }
${breakpoint("tablet")` ${breakpoint("tablet")`
margin-left: ${(props: TitleProps) => margin-left: ${(props: TitleProps) => -props.$emojiWidth}px;
props.$startsWithEmojiAndSpace ? "-1.2em" : 0};
`}; `};
${AnimatedStar} { ${AnimatedStar} {

View File

@@ -76,7 +76,7 @@
"date-fns": "^2.25.0", "date-fns": "^2.25.0",
"dd-trace": "^0.32.2", "dd-trace": "^0.32.2",
"dotenv": "^4.0.0", "dotenv": "^4.0.0",
"emoji-regex": "^6.5.1", "emoji-regex": "^10.0.0",
"es6-error": "^4.1.1", "es6-error": "^4.1.1",
"exports-loader": "^0.6.4", "exports-loader": "^0.6.4",
"fetch-retry": "^4.1.1", "fetch-retry": "^4.1.1",
@@ -140,11 +140,9 @@
"react-dropzone": "^11.3.2", "react-dropzone": "^11.3.2",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-i18next": "^11.13.0", "react-i18next": "^11.13.0",
"react-is": "^17.0.2",
"react-portal": "^4.2.0", "react-portal": "^4.2.0",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-table": "^7.7.0", "react-table": "^7.7.0",
"react-virtual": "^2.8.2",
"react-virtualized-auto-sizer": "^1.0.5", "react-virtualized-auto-sizer": "^1.0.5",
"react-waypoint": "^10.1.0", "react-waypoint": "^10.1.0",
"react-window": "^1.8.6", "react-window": "^1.8.6",

View File

@@ -44,7 +44,6 @@ export default async function present(
urlId: document.urlId, urlId: document.urlId,
title: document.title, title: document.title,
text, text,
emoji: document.emoji,
tasks: document.tasks, tasks: document.tasks,
createdAt: document.createdAt, createdAt: document.createdAt,
createdBy: undefined, createdBy: undefined,

View File

@@ -1,4 +0,0 @@
declare module "emoji-regex" {
const RegExpFactory: () => RegExp;
export = RegExpFactory;
}

View File

@@ -14,7 +14,7 @@ export default function parseTitle(text = "") {
// find and extract first emoji // find and extract first emoji
const matches = regex.exec(title); const matches = regex.exec(title);
const firstEmoji = matches ? matches[0] : null; const firstEmoji = matches ? matches[0] : null;
const startsWithEmoji = firstEmoji && title.startsWith(firstEmoji); const startsWithEmoji = firstEmoji && title.startsWith(`${firstEmoji} `);
const emoji = startsWithEmoji ? firstEmoji : undefined; const emoji = startsWithEmoji ? firstEmoji : undefined;
return { return {

View File

@@ -5714,9 +5714,9 @@ cssstyle@^2.3.0:
cssom "~0.3.6" cssom "~0.3.6"
csstype@^3.0.2, csstype@^3.0.4: csstype@^3.0.2, csstype@^3.0.4:
version "3.0.9" version "3.0.10"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.9.tgz#6410af31b26bd0520933d02cbc64fce9ce3fbf0b" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5"
integrity sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw== integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==
cyclist@^1.0.1: cyclist@^1.0.1:
version "1.0.1" version "1.0.1"
@@ -6262,16 +6262,11 @@ emittery@^0.8.1:
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860"
integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==
emoji-regex@*: emoji-regex@*, emoji-regex@^10.0.0:
version "10.0.0" version "10.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.0.0.tgz#96559e19f82231b436403e059571241d627c42b8" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.0.0.tgz#96559e19f82231b436403e059571241d627c42b8"
integrity sha512-KmJa8l6uHi1HrBI34udwlzZY1jOEuID/ft4d8BSSEdRyap7PwBEt910453PJa5MuGvxkLqlt4Uvhu7tttFHViw== integrity sha512-KmJa8l6uHi1HrBI34udwlzZY1jOEuID/ft4d8BSSEdRyap7PwBEt910453PJa5MuGvxkLqlt4Uvhu7tttFHViw==
emoji-regex@^6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2"
integrity sha512-PAHp6TxrCy7MGMFidro8uikr+zlJJKJ/Q6mm2ExZ7HwkyR9lSVFfE3kt36qcwa24BQL7y0G9axycGjK1A/0uNQ==
emoji-regex@^7.0.1: emoji-regex@^7.0.1:
version "7.0.3" version "7.0.3"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
@@ -12186,7 +12181,7 @@ react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-i
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-is@^17.0.1, react-is@^17.0.2: react-is@^17.0.1:
version "17.0.2" version "17.0.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==