feat: Unified icon picker (#7038)

This commit is contained in:
Hemachandar
2024-06-23 19:01:18 +05:30
committed by GitHub
parent 56d90e6bc3
commit 6fd3a0fa8a
83 changed files with 2302 additions and 852 deletions

View File

@@ -18,29 +18,32 @@ import {
import { DocumentValidation } from "@shared/validations";
import ContentEditable, { RefHandle } from "~/components/ContentEditable";
import { useDocumentContext } from "~/components/DocumentContext";
import { Emoji, EmojiButton } from "~/components/EmojiPicker/components";
import Flex from "~/components/Flex";
import Icon from "~/components/Icon";
import { PopoverButton } from "~/components/IconPicker/components/PopoverButton";
import useBoolean from "~/hooks/useBoolean";
import usePolicy from "~/hooks/usePolicy";
import { isModKey } from "~/utils/keyboard";
const EmojiPicker = React.lazy(() => import("~/components/EmojiPicker"));
const IconPicker = React.lazy(() => import("~/components/IconPicker"));
type Props = {
/** ID of the associated document */
documentId: string;
/** Title to display */
title: string;
/** Emoji to display */
emoji?: string | null;
/** Icon to display */
icon?: string | null;
/** Icon color */
color: string;
/** Placeholder to display when the document has no title */
placeholder?: string;
/** Should the title be editable, policies will also be considered separately */
readOnly?: boolean;
/** Callback called on any edits to text */
onChangeTitle?: (text: string) => void;
/** Callback called when the user selects an emoji */
onChangeEmoji?: (emoji: string | null) => void;
/** Callback called when the user selects an icon */
onChangeIcon?: (icon: string | null, color: string | null) => void;
/** Callback called when the user expects to move to the "next" input */
onGoToNextInput?: (insertParagraph?: boolean) => void;
/** Callback called when the user expects to save (CMD+S) */
@@ -56,10 +59,11 @@ const DocumentTitle = React.forwardRef(function _DocumentTitle(
{
documentId,
title,
emoji,
icon,
color,
readOnly,
onChangeTitle,
onChangeEmoji,
onChangeIcon,
onSave,
onGoToNextInput,
onBlur,
@@ -68,7 +72,7 @@ const DocumentTitle = React.forwardRef(function _DocumentTitle(
externalRef: React.RefObject<RefHandle>
) {
const ref = React.useRef<RefHandle>(null);
const [emojiPickerIsOpen, handleOpen, handleClose] = useBoolean();
const [iconPickerIsOpen, handleOpen, handleClose] = useBoolean();
const { editor } = useDocumentContext();
const can = usePolicy(documentId);
@@ -212,19 +216,26 @@ const DocumentTitle = React.forwardRef(function _DocumentTitle(
[editor]
);
const handleEmojiChange = React.useCallback(
async (value: string | null) => {
// Restore focus on title
restoreFocus();
if (emoji !== value) {
onChangeEmoji?.(value);
const handleIconChange = React.useCallback(
(chosenIcon: string | null, iconColor: string | null) => {
if (icon !== chosenIcon || color !== iconColor) {
onChangeIcon?.(chosenIcon, iconColor);
}
},
[emoji, onChangeEmoji, restoreFocus]
[icon, color, onChangeIcon]
);
React.useEffect(() => {
if (!iconPickerIsOpen) {
restoreFocus();
}
}, [iconPickerIsOpen, restoreFocus]);
const dir = ref.current?.getComputedDirection();
const emojiIcon = <Emoji size={32}>{emoji}</Emoji>;
const fallbackIcon = icon ? (
<Icon value={icon} color={color} size={40} />
) : null;
return (
<Title
@@ -235,8 +246,8 @@ const DocumentTitle = React.forwardRef(function _DocumentTitle(
onBlur={handleBlur}
placeholder={placeholder}
value={title}
$emojiPickerIsOpen={emojiPickerIsOpen}
$containsEmoji={!!emoji}
$iconPickerIsOpen={iconPickerIsOpen}
$containsIcon={!!icon}
autoFocus={!title}
maxLength={DocumentValidation.maxTitleLength}
readOnly={readOnly}
@@ -244,47 +255,33 @@ const DocumentTitle = React.forwardRef(function _DocumentTitle(
ref={mergeRefs([ref, externalRef])}
>
{can.update && !readOnly ? (
<EmojiWrapper align="center" justify="center" dir={dir}>
<React.Suspense fallback={emojiIcon}>
<StyledEmojiPicker
value={emoji}
onChange={handleEmojiChange}
<IconWrapper align="center" justify="center" dir={dir}>
<React.Suspense fallback={fallbackIcon}>
<StyledIconPicker
icon={icon ?? null}
color={color}
size={40}
popoverPosition="bottom-start"
allowDelete={true}
borderOnHover={true}
onChange={handleIconChange}
onOpen={handleOpen}
onClose={handleClose}
onClickOutside={restoreFocus}
autoFocus
/>
</React.Suspense>
</EmojiWrapper>
) : emoji ? (
<EmojiWrapper align="center" justify="center" dir={dir}>
{emojiIcon}
</EmojiWrapper>
</IconWrapper>
) : icon ? (
<IconWrapper align="center" justify="center" dir={dir}>
{fallbackIcon}
</IconWrapper>
) : null}
</Title>
);
});
const StyledEmojiPicker = styled(EmojiPicker)`
${extraArea(8)}
`;
const EmojiWrapper = styled(Flex)<{ dir?: string }>`
position: absolute;
top: 8px;
height: 32px;
width: 32px;
// Always move above TOC
z-index: 1;
${(props: { dir?: string }) =>
props.dir === "rtl" ? "right: -40px" : "left: -40px"};
`;
type TitleProps = {
$containsEmoji: boolean;
$emojiPickerIsOpen: boolean;
$containsIcon: boolean;
$iconPickerIsOpen: boolean;
};
const Title = styled(ContentEditable)<TitleProps>`
@@ -293,7 +290,7 @@ const Title = styled(ContentEditable)<TitleProps>`
margin-top: 6vh;
margin-bottom: 0.5em;
margin-left: ${(props) =>
props.$containsEmoji || props.$emojiPickerIsOpen ? "40px" : "0px"};
props.$containsIcon || props.$iconPickerIsOpen ? "40px" : "0px"};
font-size: ${fontSize};
font-weight: 600;
border: 0;
@@ -314,14 +311,14 @@ const Title = styled(ContentEditable)<TitleProps>`
&:focus {
margin-left: 40px;
${EmojiButton} {
${PopoverButton} {
opacity: 1 !important;
}
}
${EmojiButton} {
${PopoverButton} {
opacity: ${(props: TitleProps) =>
props.$containsEmoji ? "1 !important" : 0};
props.$containsIcon ? "1 !important" : 0};
}
${breakpoint("tablet")`
@@ -333,7 +330,7 @@ const Title = styled(ContentEditable)<TitleProps>`
}
&:hover {
${EmojiButton} {
${PopoverButton} {
opacity: 0.5;
&:hover {
@@ -349,4 +346,21 @@ const Title = styled(ContentEditable)<TitleProps>`
}
`;
const StyledIconPicker = styled(IconPicker)`
${extraArea(8)}
`;
const IconWrapper = styled(Flex)<{ dir?: string }>`
position: absolute;
top: 3px;
height: 40px;
width: 40px;
// Always move above TOC
z-index: 1;
${(props: { dir?: string }) =>
props.dir === "rtl" ? "right: -48px" : "left: -48px"};
`;
export default observer(DocumentTitle);