diff --git a/CHANGELOG.md b/CHANGELOG.md
index 53dd6ff375..09108a1f08 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ and this project adheres to
### Fixed
+- 🐛(frontend) preserve page titles when adding an emoji
- 🐛(frontend) refresh pins after document deletion and restoration
- 🐛(frontend) redirect homepage to login when homepage feat is disabled #2521
- 🐛(backend) ignore CSPs for API docs in development
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocHeaderEmoji.spec.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocHeaderEmoji.spec.tsx
index caa82a8da0..636ff918ed 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocHeaderEmoji.spec.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocHeaderEmoji.spec.tsx
@@ -4,12 +4,16 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { AppWrapper } from '@/tests/utils';
const mockUpdateDocEmoji = vi.fn();
+const mockUpdateDocTitle = vi.fn((_doc: unknown, title: string) => title);
vi.mock('@/docs/doc-management', async () => {
const actual = await vi.importActual('@/docs/doc-management');
return {
...actual,
- useDocTitleUpdate: () => ({ updateDocEmoji: mockUpdateDocEmoji }),
+ useDocTitleUpdate: () => ({
+ updateDocEmoji: mockUpdateDocEmoji,
+ updateDocTitle: mockUpdateDocTitle,
+ }),
};
});
@@ -33,6 +37,7 @@ describe('DocHeader - Add emoji (April Fools easter egg)', () => {
beforeEach(() => {
vi.useFakeTimers();
mockUpdateDocEmoji.mockClear();
+ mockUpdateDocTitle.mockClear();
});
afterEach(() => {
@@ -58,4 +63,23 @@ describe('DocHeader - Add emoji (April Fools easter egg)', () => {
);
});
});
+
+ test('preserves a title changed immediately before adding an emoji', () => {
+ vi.setSystemTime(new Date('2026-03-30'));
+
+ render(, {
+ wrapper: AppWrapper,
+ });
+
+ const titleInput = screen.getByRole('textbox', { name: 'Document title' });
+ titleInput.textContent = 'My new document';
+ fireEvent.blur(titleInput);
+ fireEvent.click(screen.getByRole('button', { name: 'Add icon' }));
+
+ expect(mockUpdateDocEmoji).toHaveBeenCalledWith(
+ 'doc-1',
+ 'My new document',
+ '📄',
+ );
+ });
});
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx
index 6e24e17fbd..364ef76a28 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx
@@ -1,4 +1,5 @@
import { Button } from '@gouvfr-lasuite/cunningham-react';
+import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
@@ -31,6 +32,11 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
const { updateDocEmoji } = useDocTitleUpdate();
const { isTopRoot } = useDocUtils(doc);
const displayEmojiButton = doc.abilities.partial_update && !isTopRoot;
+ const latestTitleRef = useRef(doc.title ?? '');
+
+ useEffect(() => {
+ latestTitleRef.current = doc.title ?? '';
+ }, [doc.title]);
return (
<>
@@ -74,10 +80,10 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
const isAprilFools =
today.getMonth() === 3 && today.getDate() === 1;
emoji
- ? updateDocEmoji(doc.id, doc.title ?? '', '')
+ ? updateDocEmoji(doc.id, latestTitleRef.current, '')
: updateDocEmoji(
doc.id,
- doc.title ?? '',
+ latestTitleRef.current,
isAprilFools ? '🐟' : '📄',
);
}}
@@ -97,7 +103,12 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
)}
-
+ {
+ latestTitleRef.current = title;
+ }}
+ />
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx
index 23a06c1075..342fb8aad4 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx
@@ -21,9 +21,10 @@ export const CLASS_DOC_TITLE = '--docs--doc-title';
interface DocTitleProps {
doc: Doc;
+ onTitleUpdate?: (title: string) => void;
}
-export const DocTitle = ({ doc }: DocTitleProps) => {
+export const DocTitle = ({ doc, onTitleUpdate }: DocTitleProps) => {
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
const readOnly = !doc.abilities.partial_update || !isEditable || isLoading;
@@ -31,7 +32,7 @@ export const DocTitle = ({ doc }: DocTitleProps) => {
return ;
}
- return ;
+ return ;
};
export const DocTitleText = () => {
@@ -98,7 +99,7 @@ const DocTitleEmojiPicker = ({ doc }: DocTitleProps) => {
);
};
-const DocTitleInput = ({ doc }: DocTitleProps) => {
+const DocTitleInput = ({ doc, onTitleUpdate }: DocTitleProps) => {
const { isSmallMobile } = useResponsiveStore();
const { t } = useTranslation();
const { isTopRoot } = useDocUtils(doc);
@@ -116,6 +117,8 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
if (isTopRoot) {
const sanitizedTitle = updateDocTitle(doc, inputText);
setTitleDisplay(sanitizedTitle);
+ onTitleUpdate?.(sanitizedTitle);
+
return sanitizedTitle;
} else {
const { emoji: pastedEmoji } = getEmojiAndTitle(inputText);
@@ -131,9 +134,10 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
getEmojiAndTitle(sanitizedTitle);
setTitleDisplay(sanitizedTitleWithoutEmoji);
+ onTitleUpdate?.(sanitizedTitle);
}
},
- [updateDocTitle, doc, emoji, isTopRoot],
+ [updateDocTitle, doc, emoji, isTopRoot, onTitleUpdate],
);
const handleKeyDown = (e: React.KeyboardEvent) => {