(frontend) add EmojiPicker in DocumentTitle

We can now add emojis to the document title using
the EmojiPicker component.
This commit is contained in:
Olivier Laurendeau
2025-09-15 18:55:35 +02:00
committed by Anthony LC
parent b1d033edc9
commit 08f3ceaf3f
9 changed files with 205 additions and 58 deletions

View File

@@ -65,16 +65,36 @@ test.describe('Doc Header', () => {
page,
browserName,
}) => {
await createDoc(page, 'doc-update', browserName, 1);
await createDoc(page, 'doc-update-emoji', browserName, 1);
const emojiPicker = page.locator('.--docs--doc-title').getByRole('button');
// Top parent should not have emoji picker
await expect(emojiPicker).toBeHidden();
const { name: docChild } = await createRootSubPage(
page,
browserName,
'doc-update-emoji-child',
);
await verifyDocName(page, docChild);
await expect(emojiPicker).toBeVisible();
await emojiPicker.click({
delay: 100,
});
await page.getByRole('button', { name: '😀' }).first().click();
await expect(emojiPicker).toHaveText('😀');
const docTitle = page.getByRole('textbox', { name: 'Document title' });
await expect(docTitle).toBeVisible();
await docTitle.fill('👍 Hello Emoji World');
await docTitle.fill('Hello Emoji World');
await docTitle.blur();
await verifyDocName(page, '👍 Hello Emoji World');
await verifyDocName(page, 'Hello Emoji World');
// Check the tree
const row = await getTreeRow(page, 'Hello Emoji World');
await expect(row.getByText('👍')).toBeVisible();
await expect(row.getByText('😀')).toBeVisible();
});
test('it deletes the doc', async ({ page, browserName }) => {

View File

@@ -340,9 +340,11 @@ test.describe('Doc Tree', () => {
// Verify the emoji is updated in the tree and in the document title
await expect(row.getByText('😀')).toBeVisible();
await expect(
page.getByRole('textbox', { name: 'Document title' }),
).toContainText('😀');
const titleEmojiPicker = page
.locator('.--docs--doc-title')
.getByRole('button');
await expect(titleEmojiPicker).toHaveText('😀');
// Now remove the emoji using the new action
await row.hover();
@@ -350,9 +352,7 @@ test.describe('Doc Tree', () => {
await page.getByRole('menuitem', { name: 'Remove emoji' }).click();
await expect(row.getByText('😀')).toBeHidden();
await expect(
page.getByRole('textbox', { name: 'Document title' }),
).not.toContainText('😀');
await expect(titleEmojiPicker).not.toHaveText('😀');
});
});

View File

@@ -19,7 +19,7 @@ export const EmojiPicker = ({
const { i18n } = useTranslation();
return (
<Box>
<Box $position="absolute" $zIndex={1000} $margin="2rem 0 0 0">
<Picker
data={emojiData}
locale={i18n.resolvedLanguage}

View File

@@ -7,11 +7,15 @@ import { Box, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import {
Doc,
DocIcon,
getEmojiAndTitle,
useDocStore,
useDocTitleUpdate,
useDocUtils,
useIsCollaborativeEditable,
useTrans,
} from '@/docs/doc-management';
import SimpleFileIcon from '@/features/docs/doc-management/assets/simple-document.svg';
import { useResponsiveStore } from '@/stores';
interface DocTitleProps {
@@ -46,22 +50,77 @@ export const DocTitleText = () => {
);
};
const DocTitleEmojiPicker = ({ doc }: DocTitleProps) => {
const { t } = useTranslation();
const { colorsTokens } = useCunninghamTheme();
const { emoji } = getEmojiAndTitle(doc.title ?? '');
return (
<Tooltip content={t('Document emoji')} aria-hidden={true} placement="top">
<Box
$css={css`
padding: 4px;
padding-top: 3px;
cursor: pointer;
&:hover {
background-color: ${colorsTokens['greyscale-100']};
border-radius: 4px;
}
transition: background-color 0.2s ease-in-out;
`}
>
<DocIcon
withEmojiPicker={doc.abilities.partial_update}
docId={doc.id}
title={doc.title}
emoji={emoji}
$size="25px"
defaultIcon={
<SimpleFileIcon
width="25px"
height="25px"
aria-hidden="true"
aria-label={t('Simple document icon')}
color={colorsTokens['primary-500']}
/>
}
/>
</Box>
</Tooltip>
);
};
const DocTitleInput = ({ doc }: DocTitleProps) => {
const { isDesktop } = useResponsiveStore();
const { t } = useTranslation();
const { colorsTokens } = useCunninghamTheme();
const [titleDisplay, setTitleDisplay] = useState(doc.title);
const { spacingsTokens } = useCunninghamTheme();
const { isTopRoot } = useDocUtils(doc);
const { untitledDocument } = useTrans();
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(doc.title ?? '');
const [titleDisplay, setTitleDisplay] = useState(
isTopRoot ? doc.title : titleWithoutEmoji,
);
const { updateDocTitle } = useDocTitleUpdate();
const handleTitleSubmit = useCallback(
(inputText: string) => {
const sanitizedTitle = updateDocTitle(doc, inputText.trim());
setTitleDisplay(sanitizedTitle);
if (isTopRoot) {
const sanitizedTitle = updateDocTitle(doc, inputText);
setTitleDisplay(sanitizedTitle);
} else {
const sanitizedTitle = updateDocTitle(
doc,
emoji ? `${emoji} ${inputText}` : inputText,
);
const { titleWithoutEmoji: sanitizedTitleWithoutEmoji } =
getEmojiAndTitle(sanitizedTitle);
setTitleDisplay(sanitizedTitleWithoutEmoji);
}
},
[doc, updateDocTitle],
[updateDocTitle, doc, emoji, isTopRoot],
);
const handleKeyDown = (e: React.KeyboardEvent) => {
@@ -72,43 +131,62 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
};
useEffect(() => {
setTitleDisplay(doc.title);
}, [doc]);
setTitleDisplay(isTopRoot ? doc.title : titleWithoutEmoji);
}, [doc.title, isTopRoot, titleWithoutEmoji]);
return (
<Tooltip content={t('Rename')} aria-hidden={true} placement="top">
<Box
as="span"
role="textbox"
className="--docs--doc-title-input"
contentEditable
defaultValue={titleDisplay || undefined}
onKeyDownCapture={handleKeyDown}
suppressContentEditableWarning={true}
aria-label={`${t('Document title')}`}
aria-multiline={false}
onBlurCapture={(event) =>
handleTitleSubmit(event.target.textContent || '')
}
$color={colorsTokens['greyscale-1000']}
$minHeight="40px"
$padding={{ right: 'big' }}
$css={css`
&[contenteditable='true']:empty:not(:focus):before {
content: '${untitledDocument}';
color: grey;
pointer-events: none;
font-style: italic;
<Box
className="--docs--doc-title"
$direction="row"
$align="center"
$gap={spacingsTokens['xs']}
$minHeight="40px"
>
{isTopRoot && (
<SimpleFileIcon
width="25px"
height="25px"
aria-hidden="true"
aria-label={t('Simple document icon')}
color={colorsTokens['primary-500']}
style={{ flexShrink: '0' }}
/>
)}
{!isTopRoot && <DocTitleEmojiPicker doc={doc} />}
<Tooltip content={t('Rename')} aria-hidden={true} placement="top">
<Box
as="span"
role="textbox"
className="--docs--doc-title-input"
contentEditable
defaultValue={titleDisplay || undefined}
onKeyDownCapture={handleKeyDown}
suppressContentEditableWarning={true}
aria-label={`${t('Document title')}`}
aria-multiline={false}
onBlurCapture={(event) =>
handleTitleSubmit(event.target.textContent || '')
}
font-size: ${isDesktop
? css`var(--c--theme--font--sizes--h2)`
: css`var(--c--theme--font--sizes--sm)`};
font-weight: 700;
outline: none;
`}
>
{titleDisplay}
</Box>
</Tooltip>
$color={colorsTokens['greyscale-1000']}
$padding={{ right: 'big' }}
$css={css`
&[contenteditable='true']:empty:not(:focus):before {
content: '${untitledDocument}';
color: grey;
pointer-events: none;
font-style: italic;
}
font-size: ${isDesktop
? css`var(--c--theme--font--sizes--h2)`
: css`var(--c--theme--font--sizes--sm)`};
font-weight: 700;
outline: none;
`}
>
{titleDisplay}
</Box>
</Tooltip>
</Box>
);
};

View File

@@ -20,9 +20,11 @@ import {
KEY_DOC,
KEY_LIST_DOC,
ModalRemoveDoc,
getEmojiAndTitle,
useCopyDocLink,
useCreateFavoriteDoc,
useDeleteFavoriteDoc,
useDocTitleUpdate,
useDocUtils,
useDuplicateDoc,
} from '@/docs/doc-management';
@@ -49,7 +51,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const treeContext = useTreeContext<Doc>();
const queryClient = useQueryClient();
const router = useRouter();
const { isChild } = useDocUtils(doc);
const { isChild, isTopRoot } = useDocUtils(doc);
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
@@ -83,6 +85,10 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
});
}, [selectHistoryModal.isOpen, queryClient]);
// Emoji Management
const { emoji } = getEmojiAndTitle(doc.title ?? '');
const { updateDocEmoji } = useDocTitleUpdate();
const options: DropdownMenuOption[] = [
...(isSmallMobile
? [
@@ -118,6 +124,17 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
},
testId: `docs-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`,
},
...(emoji && doc.abilities.partial_update && !isTopRoot
? [
{
label: t('Remove emoji'),
icon: 'emoji_emotions',
callback: () => {
updateDocEmoji(doc.id, doc.title ?? '', '');
},
},
]
: []),
{
label: t('Version history'),
icon: 'history',

View File

@@ -1,6 +1,4 @@
<svg
width="33"
height="33"
viewBox="0 0 33 33"
fill="none"
xmlns="http://www.w3.org/2000/svg"

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -73,6 +73,8 @@ export const SimpleDocItem = ({
/>
) : (
<SimpleFileIcon
width="32px"
height="32px"
aria-hidden="true"
data-testid="doc-simple-icon"
color={colorsTokens['primary-500']}

View File

@@ -70,6 +70,7 @@
"Document access mode": "Doare moned ar restr",
"Document accessible to any connected person": "Restr a c'hall bezañ tizhet gant ne vern piv a vefe kevreet",
"Document duplicated successfully!": "Restr eilet gant berzh!",
"Document emoji": "Emoju ar restr",
"Document owner": "Perc'henn ar restr",
"Document role text": "Testenn rol ar restr",
"Document sections": "Kevrennoù ar restr",
@@ -175,6 +176,7 @@
"Reader": "Lenner",
"Reading": "Lenn hepken",
"Remove access": "Dilemel ar moned",
"Remove emoji": "Dilemel ar emoju",
"Rename": "Adenvel",
"Rephrase": "Adformulenniñ",
"Request access": "Goulenn mont e-barzh",
@@ -296,6 +298,7 @@
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Pages: Ihr neuer Begleiter für eine effiziente, intuitive und sichere Zusammenarbeit bei Dokumenten.",
"Document accessible to any connected person": "Dokument für jeden angemeldeten Benutzer zugänglich",
"Document duplicated successfully!": "Dokument erfolgreich dupliziert!",
"Document emoji": "Dokument-Emoji",
"Document owner": "Besitzer des Dokuments",
"Docx": "Docx",
"Download": "Herunterladen",
@@ -378,6 +381,7 @@
"Reader": "Leser",
"Reading": "Lesen",
"Remove access": "Zugriff entziehen",
"Remove emoji": "Emoji entfernen",
"Rename": "Umbenennen",
"Rephrase": "Umformulieren",
"Request access": "Zugriff anfragen",
@@ -496,6 +500,7 @@
"Docs transforms your documents into knowledge bases thanks to subpages, powerful search and the ability to pin your important documents.": "Docs transforma sus documentos en bases de conocimiento gracias a las subpáginas, una potente herramienta de búsqueda y la capacidad de marcar como favorito sus documentos más importantes.",
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs: su nuevo compañero para colaborar en documentos de forma eficiente, intuitiva y segura.",
"Document accessible to any connected person": "Documento accesible a cualquier persona conectada",
"Document emoji": "Emoji del documento",
"Document owner": "Propietario del documento",
"Docx": "Docx",
"Download": "Descargar",
@@ -565,6 +570,7 @@
"Quick search input": "Entrada de búsqueda rápida",
"Reader": "Lector",
"Reading": "Lectura",
"Remove emoji": "Eliminar emoji",
"Rename": "Cambiar el nombre",
"Rephrase": "Reformular",
"Request access": "Solicitar acceso",
@@ -694,6 +700,7 @@
"Document accessible to any connected person": "Document accessible à toute personne connectée",
"Document deleted": "Document supprimé",
"Document duplicated successfully!": "Document dupliqué avec succès !",
"Document emoji": "Emoji du document",
"Document owner": "Propriétaire du document",
"Document role text": "Texte du rôle du document",
"Document sections": "Sections du document",
@@ -806,6 +813,7 @@
"Reader": "Lecteur",
"Reading": "Lecture seule",
"Remove access": "Supprimer l'accès",
"Remove emoji": "Supprimer l'emoji",
"Rename": "Renommer",
"Rephrase": "Reformuler",
"Request access": "Demander l'accès",
@@ -930,6 +938,7 @@
"Docs transforms your documents into knowledge bases thanks to subpages, powerful search and the ability to pin your important documents.": "Docs trasforma i tuoi documenti in piattaforme di conoscenza grazie alle sotto-pagine, alla ricerca potente e alla capacità di fissare i tuoi documenti importanti.",
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs: Il tuo nuovo compagno di collaborare sui documenti in modo efficiente, intuitivo e sicuro.",
"Document accessible to any connected person": "Documento accessibile a qualsiasi persona collegata",
"Document emoji": "Emoji del documento",
"Document owner": "Proprietario del documento",
"Docx": "Docx",
"Download": "Scarica",
@@ -990,6 +999,7 @@
"Public document": "Documento pubblico",
"Reader": "Lettore",
"Reading": "Leggendo",
"Remove emoji": "Rimuovi emoji",
"Rename": "Rinomina",
"Rephrase": "Riformula",
"Restore": "Ripristina",
@@ -1103,6 +1113,7 @@
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs: Je nieuwe metgezel om efficiënt, intuïtief en veilig samen te werken aan documenten.",
"Document access mode": "Document toegangsmodus",
"Document accessible to any connected person": "Document is toegankelijk voor ieder verbonden persoon",
"Document emoji": "Document emoji",
"Document duplicated successfully!": "Document met succes gedupliceerd!",
"Document owner": "Document eigenaar",
"Document role text": "Document roltekst",
@@ -1214,6 +1225,7 @@
"Reader": "Lezer",
"Reading": "Lezen",
"Remove access": "Toegang verwijderen",
"Remove emoji": "Emoji verwijderen",
"Rename": "Hernoemen",
"Rephrase": "Herschrijf",
"Request access": "Toegang aanvragen",
@@ -1287,7 +1299,11 @@
"pdf": "PDF"
}
},
"pt": { "translation": {} },
"pt": {
"translation": {
"Remove emoji": "Remover emoji"
}
},
"ru": {
"translation": {
"\"{{email}}\" is already invited to the document.": "\"{{email}}\" уже имеет приглашение для этого документа.",
@@ -1368,6 +1384,7 @@
"Document accessible to any connected person": "Документ доступен всем, кто присоединится",
"Document deleted": "Документ удалён",
"Document duplicated successfully!": "Документ успешно дублирован!",
"Document emoji": "Эмодзи документа",
"Document owner": "Владелец документа",
"Document role text": "Текст роли документа",
"Document sections": "Разделы документа",
@@ -1480,6 +1497,7 @@
"Reader": "Читатель",
"Reading": "Чтение",
"Remove access": "Отменить доступ",
"Remove emoji": "Убрать эмодзи",
"Rename": "Переименовать",
"Rephrase": "Переформулировать",
"Request access": "Запрос доступа",
@@ -1565,6 +1583,7 @@
"sl": {
"translation": {
"Load more": "Naloži več",
"Remove emoji": "Odstrani emoji",
"Untitled document": "Dokument brez naslova"
}
},
@@ -1598,7 +1617,8 @@
"This file is flagged as unsafe.": "Denna fil är flaggad som osäker.",
"Too many requests. Please wait 60 seconds.": "För många förfrågningar. Vänligen vänta 60 sekunder.",
"Use as prompt": "Använd som prompt",
"Warning": "Varning"
"Warning": "Varning",
"Remove emoji": "Ta bort emoji"
}
},
"tr": {
@@ -1625,6 +1645,7 @@
"Docs": "Docs",
"Docs Logo": "Docs logosu",
"Document accessible to any connected person": "Bağlanan herhangi bir kişi tarafından erişilebilen belge",
"Document emoji": "Belge emojisi",
"Docx": "Docx",
"Download": "İndir",
"Download anyway": "Yine de indir",
@@ -1668,7 +1689,8 @@
"Version history": "Sürüm geçmişi",
"Warning": "Uyarı",
"Write": "Yaz",
"Your {{format}} was downloaded succesfully": "{{format}} indirildi"
"Your {{format}} was downloaded succesfully": "{{format}} indirildi",
"Remove emoji": "Emoji kaldır"
}
},
"uk": {
@@ -1751,6 +1773,7 @@
"Document accessible to any connected person": "Документ, доступний для будь-якої особи, що приєдналася",
"Document deleted": "Документ видалено",
"Document duplicated successfully!": "Документ успішно продубльовано!",
"Document emoji": "Емодзі документа",
"Document owner": "Власник документа",
"Document role text": "Текст ролі документа",
"Document sections": "Розділи документу",
@@ -1863,6 +1886,7 @@
"Reader": "Читач",
"Reading": "Читання",
"Remove access": "Вилучити доступ",
"Remove emoji": "Видалити емодзі",
"Rename": "Перейменувати",
"Rephrase": "Перефразувати",
"Request access": "Запит доступу",
@@ -2000,6 +2024,7 @@
"Docs transforms your documents into knowledge bases thanks to subpages, powerful search and the ability to pin your important documents.": "Docs 通过子页面、强大的搜索功能以及固定重要文档的能力,将您的文档转化为知识库。",
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs 为您提供高效、直观且安全的文档协作解决方案。",
"Document accessible to any connected person": "任何来访的人都可以访问文档",
"Document emoji": "文档表情符号",
"Document owner": "文档所有者",
"Document title": "文档标题",
"Docx": "Doc",
@@ -2076,6 +2101,7 @@
"Quick search input": "快速搜索",
"Reader": "阅读者",
"Reading": "阅读中",
"Remove emoji": "移除表情符号",
"Rename": "重命名",
"Rephrase": "改写",
"Reset": "重置",