Skip to content

Commit c18bdfe

Browse files
committed
🐛(frontend) open the emoji picker when adding a document emoji
Previously, picking an emoji required two clicks; now the picker opens on click and updates only on selection.
1 parent 6e5dee3 commit c18bdfe

4 files changed

Lines changed: 74 additions & 68 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ and this project adheres to
8787

8888
### Fixed
8989

90+
- 🐛(frontend) open the emoji picker when adding a document emoji
9091
- 🐛(documentation) fix minio port and keycloak realm in the k8s guide #2751
9192
- 🐛(collaboration) bound the calls yhub makes to the backend with a timeout,
9293
`YHUB_BACKEND_REQUEST_TIMEOUT_MS`, 5s by default #2753

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

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -218,16 +218,10 @@ test.describe('Doc Header', () => {
218218
// Emoji picker should be hidden initially
219219
await expect(emojiPicker).toBeHidden();
220220

221-
// Add emoji
221+
// Add emoji opens the picker; the document stays unchanged until a choice
222222
await expect(removeEmoji).toBeHidden();
223223
await addEmoji.click();
224-
// The 1 April the emoji is a fish
225-
await expect(emojiPicker).toHaveText(/📄|🐟/);
226-
227-
// Change emoji
228-
await emojiPicker.click({
229-
delay: 100,
230-
});
224+
await expect(emojiPicker).toBeHidden();
231225
await page.getByRole('button', { name: '😀' }).first().click();
232226
await expect(emojiPicker).toHaveText('😀');
233227

Lines changed: 31 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,71 @@
1-
import { fireEvent, render, screen } from '@testing-library/react';
2-
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
1+
import { render, screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { beforeEach, describe, expect, test, vi } from 'vitest';
34

45
import { AppWrapper } from '@/tests/utils';
56

67
const mockUpdateDocEmoji = vi.fn();
7-
const mockUpdateDocTitle = vi.fn((_doc: unknown, title: string) => title);
88

99
vi.mock('@/docs/doc-management', async () => {
1010
const actual = await vi.importActual('@/docs/doc-management');
1111
return {
1212
...actual,
1313
useDocTitleUpdate: () => ({
1414
updateDocEmoji: mockUpdateDocEmoji,
15-
updateDocTitle: mockUpdateDocTitle,
15+
updateDocTitle: (_doc: unknown, title: string) => title,
1616
}),
1717
};
1818
});
1919

20+
vi.mock('@/components/Emoji/EmojiPicker', () => ({
21+
EmojiPicker: ({
22+
onEmojiSelect,
23+
}: {
24+
onEmojiSelect: (emoji: { native: string }) => void;
25+
}) => (
26+
<button type="button" onClick={() => onEmojiSelect({ native: '😀' })}>
27+
😀
28+
</button>
29+
),
30+
}));
31+
2032
import { DocHeader } from '../components/DocHeader';
2133

2234
const doc = {
2335
id: 'doc-1',
2436
title: 'My document',
25-
is_favorite: false,
26-
nb_accesses_direct: 1,
27-
abilities: {
28-
versions_list: true,
29-
destroy: true,
30-
partial_update: true,
31-
duplicate: true,
32-
accesses_view: true,
33-
},
37+
abilities: { partial_update: true },
3438
} as any;
3539

36-
describe('DocHeader - Add emoji (April Fools easter egg)', () => {
40+
describe('DocHeader - Add emoji', () => {
3741
beforeEach(() => {
38-
vi.useFakeTimers();
3942
mockUpdateDocEmoji.mockClear();
40-
mockUpdateDocTitle.mockClear();
41-
});
42-
43-
afterEach(() => {
44-
vi.useRealTimers();
4543
});
4644

47-
[
48-
{ emoji: '🐟', date: '2026-04-01' },
49-
{ emoji: '📄', date: '2026-03-30' },
50-
{ emoji: '📄', date: '2026-04-02' },
51-
].forEach(({ emoji, date }) => {
52-
test(`uses ${emoji} emoji on ${date}`, () => {
53-
vi.setSystemTime(new Date(date));
45+
test('opens the picker without setting a default emoji', async () => {
46+
const user = userEvent.setup();
47+
render(<DocHeader doc={doc} />, { wrapper: AppWrapper });
5448

55-
render(<DocHeader doc={doc} />, { wrapper: AppWrapper });
49+
await user.click(screen.getByRole('button', { name: 'Add emoji' }));
5650

57-
fireEvent.click(screen.getByRole('button', { name: 'Add emoji' }));
58-
59-
expect(mockUpdateDocEmoji).toHaveBeenCalledWith(
60-
'doc-1',
61-
'My document',
62-
emoji,
63-
);
64-
});
51+
expect(screen.getByRole('button', { name: '😀' })).toBeVisible();
52+
expect(mockUpdateDocEmoji).not.toHaveBeenCalled();
6553
});
6654

67-
test('preserves a title changed immediately before adding an emoji', () => {
68-
vi.setSystemTime(new Date('2026-03-30'));
69-
70-
render(<DocHeader doc={{ ...doc, title: '' }} />, {
71-
wrapper: AppWrapper,
72-
});
55+
test('sets the chosen emoji, keeping a title edited just before', async () => {
56+
const user = userEvent.setup();
57+
render(<DocHeader doc={{ ...doc, title: '' }} />, { wrapper: AppWrapper });
7358

7459
const titleInput = screen.getByRole('textbox', { name: 'Document title' });
60+
await user.click(titleInput);
7561
titleInput.textContent = 'My new document';
76-
fireEvent.blur(titleInput);
77-
fireEvent.click(screen.getByRole('button', { name: 'Add emoji' }));
62+
await user.click(screen.getByRole('button', { name: 'Add emoji' }));
63+
await user.click(screen.getByRole('button', { name: '😀' }));
7864

7965
expect(mockUpdateDocEmoji).toHaveBeenCalledWith(
8066
'doc-1',
8167
'My new document',
82-
'📄',
68+
'😀',
8369
);
8470
});
8571
});

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

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import { Button } from '@gouvfr-lasuite/ui-components';
2-
import { useEffect, useRef } from 'react';
2+
import { MouseEvent, useEffect, useRef, useState } from 'react';
33
import { useTranslation } from 'react-i18next';
44
import { css } from 'styled-components';
55

66
import RemoveEmojiSVG from '@/assets/icons/ui-kit/face-remove.svg';
77
import AddEmojiSVG from '@/assets/icons/ui-kit/face.svg';
8-
import { Box, HorizontalSeparator } from '@/components';
8+
import { Box, EmojiPicker, HorizontalSeparator } from '@/components';
9+
import { getEmojidata } from '@/components/Emoji/initEmojiCallout';
910
import {
1011
Doc,
1112
getEmojiAndTitle,
1213
useDocTitleUpdate,
1314
useDocUtils,
1415
} from '@/docs/doc-management';
1516
import { useIsOffline } from '@/features/service-worker/hooks/useOffline';
17+
import { useFocusStore } from '@/stores';
1618

1719
import { AlertOffline } from './AlertOffline';
1820
import { AlertRestore } from './AlertRestore';
@@ -34,6 +36,31 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
3436
const { isTopRoot } = useDocUtils(doc);
3537
const displayEmojiButton = doc.abilities.partial_update && !isTopRoot;
3638
const latestTitleRef = useRef(doc.title ?? '');
39+
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false);
40+
const { addLastFocus, restoreFocus } = useFocusStore();
41+
42+
const closeEmojiPicker = () => {
43+
setIsEmojiPickerOpen(false);
44+
restoreFocus();
45+
};
46+
47+
const onEmojiSelect = ({ native }: { native: string }) => {
48+
updateDocEmoji(doc.id, latestTitleRef.current, native);
49+
closeEmojiPicker();
50+
};
51+
52+
const handleEmojiButtonClick = (
53+
event: MouseEvent<HTMLButtonElement> | MouseEvent<HTMLAnchorElement>,
54+
) => {
55+
if (emoji) {
56+
updateDocEmoji(doc.id, latestTitleRef.current, '');
57+
return;
58+
}
59+
60+
event.stopPropagation();
61+
addLastFocus(event.currentTarget);
62+
setIsEmojiPickerOpen(true);
63+
};
3764

3865
useEffect(() => {
3966
latestTitleRef.current = doc.title ?? '';
@@ -71,23 +98,13 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
7198
{isOffline && <AlertOffline />}
7299
</Box>
73100
<Box $gap="sm">
74-
<Box>
101+
<Box $position="relative">
75102
{displayEmojiButton && (
76103
<Button
77104
className="--docs--doc-header-emoji-button"
78105
size="nano"
79-
onClick={() => {
80-
const today = new Date();
81-
const isAprilFools =
82-
today.getMonth() === 3 && today.getDate() === 1;
83-
emoji
84-
? updateDocEmoji(doc.id, latestTitleRef.current, '')
85-
: updateDocEmoji(
86-
doc.id,
87-
latestTitleRef.current,
88-
isAprilFools ? '🐟' : '📄',
89-
);
90-
}}
106+
onClick={handleEmojiButtonClick}
107+
aria-expanded={emoji ? undefined : isEmojiPickerOpen}
91108
aria-label={emoji ? t('Remove emoji') : t('Add emoji')}
92109
color="neutral"
93110
variant="tertiary"
@@ -103,6 +120,14 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
103120
{emoji ? t('Remove emoji') : t('Add emoji')}
104121
</Button>
105122
)}
123+
{isEmojiPickerOpen && (
124+
<EmojiPicker
125+
emojiData={getEmojidata()}
126+
onClickOutside={closeEmojiPicker}
127+
onEmojiSelect={onEmojiSelect}
128+
withOverlay={true}
129+
/>
130+
)}
106131
</Box>
107132
<DocTitle
108133
doc={doc}

0 commit comments

Comments
 (0)