Skip to content

Commit b667200

Browse files
olaurendeauAntoLC
authored andcommitted
✨(frontend) add an EmojiPicker in the document tree
This allows users to easily add emojis easily to their documents from the tree, enhancing the overall user experience.
1 parent 294922f commit b667200

File tree

12 files changed

+506
-59
lines changed

12 files changed

+506
-59
lines changed

src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import {
99
verifyDocName,
1010
} from './utils-common';
1111
import { addNewMember } from './utils-share';
12-
import { clickOnAddRootSubPage, createRootSubPage } from './utils-sub-pages';
12+
import {
13+
clickOnAddRootSubPage,
14+
createRootSubPage,
15+
getTreeRow,
16+
} from './utils-sub-pages';
1317

1418
test.describe('Doc Tree', () => {
1519
test.beforeEach(async ({ page }) => {
@@ -298,6 +302,36 @@ test.describe('Doc Tree', () => {
298302
// Now test keyboard navigation on sub-document
299303
await expect(docTree.getByText(docChild)).toBeVisible();
300304
});
305+
306+
test('it updates the child icon from the tree', async ({
307+
page,
308+
browserName,
309+
}) => {
310+
const [docParent] = await createDoc(
311+
page,
312+
'doc-child-emoji',
313+
browserName,
314+
1,
315+
);
316+
await verifyDocName(page, docParent);
317+
318+
const { name: docChild } = await createRootSubPage(
319+
page,
320+
browserName,
321+
'doc-child-emoji-child',
322+
);
323+
324+
// Update the emoji from the tree
325+
const row = await getTreeRow(page, docChild);
326+
await row.locator('.--docs--doc-icon').click();
327+
await page.getByRole('button', { name: '😀' }).first().click();
328+
329+
// Verify the emoji is updated in the tree and in the document title
330+
await expect(row.getByText('😀')).toBeVisible();
331+
await expect(
332+
page.getByRole('textbox', { name: 'Document title' }),
333+
).toContainText('😀');
334+
});
301335
});
302336

303337
test.describe('Doc Tree: Inheritance', () => {

src/frontend/apps/impress/src/features/docs/doc-editor/components/EmojiPicker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const EmojiPicker = ({
1919
const { i18n } = useTranslation();
2020

2121
return (
22-
<Box $position="absolute" $zIndex={1000} $margin="2rem 0 0 0">
22+
<Box>
2323
<Picker
2424
data={emojiData}
2525
locale={i18n.resolvedLanguage}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './AccessibleImageBlock';
22
export * from './CalloutBlock';
3+
export { default as emojidata } from './initEmojiCallout';
34
export * from './PdfBlock';
45
export * from './UploadLoaderBlock';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './DocEditor';
2+
export * from './EmojiPicker';
23
export * from './custom-blocks/';

src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx

Lines changed: 6 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
21
import { Tooltip } from '@openfun/cunningham-react';
32
import React, { useCallback, useEffect, useState } from 'react';
43
import { useTranslation } from 'react-i18next';
@@ -8,14 +7,12 @@ import { Box, Text } from '@/components';
87
import { useCunninghamTheme } from '@/cunningham';
98
import {
109
Doc,
11-
KEY_DOC,
12-
KEY_LIST_DOC,
1310
useDocStore,
11+
useDocTitleUpdate,
1412
useIsCollaborativeEditable,
1513
useTrans,
16-
useUpdateDoc,
1714
} from '@/docs/doc-management';
18-
import { useBroadcastStore, useResponsiveStore } from '@/stores';
15+
import { useResponsiveStore } from '@/stores';
1916

2017
interface DocTitleProps {
2118
doc: Doc;
@@ -54,47 +51,17 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
5451
const { t } = useTranslation();
5552
const { colorsTokens } = useCunninghamTheme();
5653
const [titleDisplay, setTitleDisplay] = useState(doc.title);
57-
const treeContext = useTreeContext<Doc>();
5854

5955
const { untitledDocument } = useTrans();
6056

61-
const { broadcast } = useBroadcastStore();
62-
63-
const { mutate: updateDoc } = useUpdateDoc({
64-
listInvalideQueries: [KEY_DOC, KEY_LIST_DOC],
65-
onSuccess(updatedDoc) {
66-
// Broadcast to every user connected to the document
67-
broadcast(`${KEY_DOC}-${updatedDoc.id}`);
68-
69-
if (!treeContext) {
70-
return;
71-
}
72-
73-
if (treeContext.root?.id === updatedDoc.id) {
74-
treeContext?.setRoot(updatedDoc);
75-
} else {
76-
treeContext?.treeData.updateNode(updatedDoc.id, updatedDoc);
77-
}
78-
},
79-
});
57+
const { updateDocTitle } = useDocTitleUpdate();
8058

8159
const handleTitleSubmit = useCallback(
8260
(inputText: string) => {
83-
let sanitizedTitle = inputText.trim();
84-
sanitizedTitle = sanitizedTitle.replace(/(\r\n|\n|\r)/gm, '');
85-
86-
// When blank we set to untitled
87-
if (!sanitizedTitle) {
88-
setTitleDisplay('');
89-
}
90-
91-
// If mutation we update
92-
if (sanitizedTitle !== doc.title) {
93-
setTitleDisplay(sanitizedTitle);
94-
updateDoc({ id: doc.id, title: sanitizedTitle });
95-
}
61+
const sanitizedTitle = updateDocTitle(doc, inputText.trim());
62+
setTitleDisplay(sanitizedTitle);
9663
},
97-
[doc.id, doc.title, updateDoc],
64+
[doc, updateDocTitle],
9865
);
9966

10067
const handleKeyDown = (e: React.KeyboardEvent) => {
Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
1-
import { Text, TextType } from '@/components';
1+
import { MouseEvent, useRef, useState } from 'react';
2+
import { createPortal } from 'react-dom';
3+
4+
import { BoxButton, Icon, TextType } from '@/components';
5+
import { EmojiPicker, emojidata } from '@/docs/doc-editor/';
6+
7+
import { useDocTitleUpdate } from '../hooks/useDocTitleUpdate';
28

39
type DocIconProps = TextType & {
410
emoji?: string | null;
511
defaultIcon: React.ReactNode;
12+
docId?: string;
13+
title?: string;
14+
onEmojiUpdate?: (emoji: string) => void;
15+
withEmojiPicker?: boolean;
616
};
717

818
export const DocIcon = ({
@@ -11,22 +21,101 @@ export const DocIcon = ({
1121
$size = 'sm',
1222
$variation = '1000',
1323
$weight = '400',
24+
docId,
25+
title,
26+
onEmojiUpdate,
27+
withEmojiPicker = false,
1428
...textProps
1529
}: DocIconProps) => {
16-
if (!emoji) {
17-
return <>{defaultIcon}</>;
30+
const { updateDocEmoji } = useDocTitleUpdate();
31+
32+
const iconRef = useRef<HTMLDivElement>(null);
33+
34+
const [openEmojiPicker, setOpenEmojiPicker] = useState<boolean>(false);
35+
const [pickerPosition, setPickerPosition] = useState<{
36+
top: number;
37+
left: number;
38+
}>({ top: 0, left: 0 });
39+
40+
if (!withEmojiPicker && !emoji) {
41+
return defaultIcon;
1842
}
1943

44+
const toggleEmojiPicker = (e: MouseEvent) => {
45+
if (withEmojiPicker) {
46+
e.stopPropagation();
47+
e.preventDefault();
48+
49+
if (!openEmojiPicker && iconRef.current) {
50+
const rect = iconRef.current.getBoundingClientRect();
51+
setPickerPosition({
52+
top: rect.bottom + window.scrollY + 8,
53+
left: rect.left + window.scrollX,
54+
});
55+
}
56+
57+
setOpenEmojiPicker(!openEmojiPicker);
58+
}
59+
};
60+
61+
const handleEmojiSelect = ({ native }: { native: string }) => {
62+
setOpenEmojiPicker(false);
63+
64+
// Update document emoji if docId is provided
65+
if (docId && title !== undefined) {
66+
updateDocEmoji(docId, title ?? '', native);
67+
}
68+
69+
// Call the optional callback
70+
onEmojiUpdate?.(native);
71+
};
72+
73+
const handleClickOutside = () => {
74+
setOpenEmojiPicker(false);
75+
};
76+
2077
return (
21-
<Text
22-
{...textProps}
23-
$size={$size}
24-
$variation={$variation}
25-
$weight={$weight}
26-
aria-hidden="true"
27-
data-testid="doc-emoji-icon"
28-
>
29-
{emoji}
30-
</Text>
78+
<>
79+
<BoxButton
80+
ref={iconRef}
81+
onClick={toggleEmojiPicker}
82+
$position="relative"
83+
className="--docs--doc-icon"
84+
>
85+
{!emoji ? (
86+
defaultIcon
87+
) : (
88+
<Icon
89+
{...textProps}
90+
iconName={emoji}
91+
$size={$size}
92+
$variation={$variation}
93+
$weight={$weight}
94+
aria-hidden="true"
95+
data-testid="doc-emoji-icon"
96+
>
97+
{emoji}
98+
</Icon>
99+
)}
100+
</BoxButton>
101+
{openEmojiPicker &&
102+
createPortal(
103+
<div
104+
style={{
105+
position: 'absolute',
106+
top: pickerPosition.top,
107+
left: pickerPosition.left,
108+
zIndex: 1000,
109+
}}
110+
>
111+
<EmojiPicker
112+
emojiData={emojidata}
113+
onEmojiSelect={handleEmojiSelect}
114+
onClickOutside={handleClickOutside}
115+
/>
116+
</div>,
117+
document.body,
118+
)}
119+
</>
31120
);
32121
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './DocIcon';
12
export * from './DocPage403';
23
export * from './ModalRemoveDoc';
34
export * from './SimpleDocItem';

0 commit comments

Comments
 (0)