diff --git a/CHANGELOG.md b/CHANGELOG.md
index e4e42d1b5..3a41dc02a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to
### Added
- ✨(frontend) Add "Copy link to block" feature #2547
+- ✨(frontend) add word count to doc header toolbox #2549
### Changed
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts
index a95ff4974..a2545999d 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts
@@ -69,6 +69,8 @@ test.describe('Doc Header', () => {
}) => {
await createDoc(page, 'doc-update', browserName, 1);
+ await writeInEditor({ page, text: 'Hello Content' });
+
const card = page.getByLabel(
'It is the card information about the document.',
);
@@ -93,6 +95,7 @@ test.describe('Doc Header', () => {
await expect(
page.getByRole('menuitem', { name: 'Download' }),
).toBeVisible();
+ await expect(page.getByText('Word count: 2 words').first()).toBeVisible();
});
test('it updates the title doc and check the broadcast', async ({
diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts b/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts
index e3e5e92b9..804f29e98 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts
+++ b/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts
@@ -1,5 +1,15 @@
+import { DocsBlockNoteEditor } from './types';
+
const HEX_COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
+export const getWordCount = (editor?: DocsBlockNoteEditor): number => {
+ const doc = editor?._tiptapEditor?.state.doc;
+ const text = doc ? doc.textBetween(0, doc.content.size, '\n') : '';
+ const trimmed = text.trim();
+
+ return trimmed ? trimmed.split(/\s+/).length : 0;
+};
+
export const sanitizeColor = (color: string): string => {
return HEX_COLOR_REGEX.test(color) ? color : randomColor();
};
diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/DocToolBox.tsx
index 65d14c3bb..635be7202 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-management/components/DocToolBox.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/DocToolBox.tsx
@@ -5,11 +5,16 @@ import {
useTreeContext,
} from '@gouvfr-lasuite/ui-components';
import { Present } from '@gouvfr-lasuite/ui-components/icons';
+import { announce } from '@react-aria/live-announcer';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router';
-import { useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
+import { Text } from '@/components/Text';
+import { useEditorStore } from '@/docs/doc-editor/stores/useEditorStore';
+import { getWordCount } from '@/docs/doc-editor/utils';
+import { printDocumentWithStyles } from '@/docs/doc-export/utils_print';
import { usePresenterStore } from '@/docs/doc-presenter/stores';
import { useAuth } from '@/features/auth';
import ContentCopyIcon from '@/icons/copy.svg';
@@ -25,7 +30,6 @@ import StarIcon from '@/icons/star.svg';
import DeleteIcon from '@/icons/trash.svg';
import { useFocusStore, useResponsiveStore } from '@/stores';
-import { printDocumentWithStyles } from '../../doc-export/utils_print';
import {
KEY_DOC,
KEY_LIST_DOC,
@@ -96,15 +100,34 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const [isModalHistoryOpen, setIsModalHistoryOpen] = useState(false);
const [isModalLeaveOpen, setIsModalLeaveOpen] = useState(false);
+ const { editor } = useEditorStore();
+ const wordCountLabel = useMemo(() => {
+ if (openDropdown) {
+ return t('Word count: {{count}} words', {
+ count: getWordCount(editor),
+ description:
+ 'In the document options menu, showing the number of words in the document.',
+ });
+ }
+ }, [editor, openDropdown, t]);
+
+ useEffect(() => {
+ if (wordCountLabel) {
+ announce(wordCountLabel, 'polite');
+ }
+ }, [wordCountLabel]);
+
const { restoreFocus, addLastFocus } = useFocusStore();
const { isMobile } = useResponsiveStore();
const copyDocLink = useCopyDocLink(doc.id);
+
const openPresenter = usePresenterStore((state) => state.open);
const { mutate: duplicateDoc } = useDuplicateDoc({
onSuccess: (data) => {
void router.push(`/docs/${data.id}`);
},
});
+
const removeFavoriteDoc = useDeleteFavoriteDoc({
listInvalidQueries: [KEY_LIST_DOC, KEY_DOC, KEY_LIST_FAVORITE_DOC],
});
@@ -232,6 +255,9 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
},
isHidden: !doc.abilities.destroy,
},
+ {
+ type: 'separator',
+ },
];
return (
@@ -241,6 +267,9 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
isOpen={openDropdown}
shouldCloseOnInteractOutside={() => true}
onOpenChange={setOpenDropdown}
+ bottomMessage={
+ wordCountLabel && {wordCountLabel}
+ }
>