From 1f126cf62941cd9f32cd291b96712a7fdcb7c9fe Mon Sep 17 00:00:00 2001
From: fch-aa <21101725+fch-aa@users.noreply.github.com>
Date: Fri, 14 Aug 2026 13:37:35 +0200
Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(frontend)=20preserve=20title=20whe?=
=?UTF-8?q?n=20adding=20an=20emoji?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The emoji action could reuse a stale document title while a rename was being
submitted, replacing the new title with the emoji alone.
Keep the latest submitted title in the header and cover the interaction
with a regression test.
Signed-off-by: fch-aa <21101725+fch-aa@users.noreply.github.com>
---
CHANGELOG.md | 4 +++
.../__tests__/DocHeaderEmoji.spec.tsx | 26 ++++++++++++++++++-
.../docs/doc-header/components/DocHeader.tsx | 17 +++++++++---
.../docs/doc-header/components/DocTitle.tsx | 12 ++++++---
4 files changed, 51 insertions(+), 8 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 357ad8bb1..50d2ddbb8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ and this project adheres to
## [Unreleased]
+### Fixed
+
+- 🐛(frontend) preserve page titles when adding an emoji #2586
+
## [v5.6.1] - 2026-09-04
### Added
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 caa82a8da..636ff918e 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 7a2ac462b..d7790a76a 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/ui-components';
+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 36b1cd063..68923f65b 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 = () => {
@@ -103,7 +104,7 @@ const DocTitleEmojiPicker = ({ doc }: DocTitleProps) => {
);
};
-const DocTitleInput = ({ doc }: DocTitleProps) => {
+const DocTitleInput = ({ doc, onTitleUpdate }: DocTitleProps) => {
const { isSmallMobile } = useResponsiveStore();
const { t } = useTranslation();
const { isTopRoot } = useDocUtils(doc);
@@ -121,6 +122,8 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
if (isTopRoot) {
const sanitizedTitle = updateDocTitle(doc, inputText);
setTitleDisplay(sanitizedTitle);
+ onTitleUpdate?.(sanitizedTitle);
+
return sanitizedTitle;
} else {
const { emoji: pastedEmoji } = getEmojiAndTitle(inputText);
@@ -136,9 +139,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) => {