diff --git a/CHANGELOG.md b/CHANGELOG.md
index af92ae62c1..9234862d41 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,9 @@ and this project adheres to
- 🐛(docs) run migration 0027 without superuser role
- 🐛(backend) prevent admins/owners from overwriting other users comments
+### Changed
+
+- ♿️(frontend) improve keyboard tab order in document list #2325
## [v5.1.0] - 2026-05-11
@@ -37,7 +40,7 @@ and this project adheres to
- 💬(frontend) add missing link in onboarding description #2233
- 🐛(frontend) sanitize pasted and dropped content in document title #2210
- 🐛(frontend) Emoji menu doesn't display above comment box #2229
-- 🐛(frontend) Block menu doesn't stay open on 1st line #2229
+- 🐛(frontend) Block menu doesn't stay open on 1st line #2229
- 🐛(frontend) The "+" on the first line of a new doc doesn't work #2229
- 🐛(backend) manage race condition between GET and PATCH content #2271
- 🐛(backend) replace document creation table locks with retry strategy #2274
@@ -46,8 +49,6 @@ and this project adheres to
- 🔒️(frontend) sanitize color during collaboration #2270
-
-
## [v5.0.0] - 2026-05-05
### Added
@@ -144,7 +145,6 @@ and this project adheres to
- 🐛(y-provider) destroy Y.Doc instances after each convert request #2129
- 🐛(backend) remove deleted sub documents in favorite_list endpoint #2083
-
## [v4.8.3] - 2026-03-23
### Changed
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts
index 136488a5d3..5eb2d0347b 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts
@@ -239,9 +239,7 @@ test.describe('Doc grid move', () => {
.click();
await expect(docsGrid.getByText(titleDoc1)).toBeHidden();
- await docsGrid
- .getByRole('link', { name: `Open document ${titleDoc2}` })
- .click();
+ await docsGrid.getByRole('link', { name: new RegExp(titleDoc2) }).click();
await verifyDocName(page, titleDoc2);
@@ -383,9 +381,7 @@ test.describe('Doc grid move', () => {
await page.keyboard.press('Enter');
await expect(docsGrid.getByText(titleDoc1)).toBeHidden();
- await docsGrid
- .getByRole('link', { name: `Open document ${titleDoc2}` })
- .click();
+ await docsGrid.getByRole('link', { name: new RegExp(titleDoc2) }).click();
await verifyDocName(page, titleDoc2);
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-trashbin.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-trashbin.spec.ts
index 9e60c1bc09..f4b19fecea 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/doc-trashbin.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-trashbin.spec.ts
@@ -84,7 +84,7 @@ test.describe('Doc Trashbin', () => {
await page.getByRole('link', { name: 'All docs' }).click();
const row2Restored = await getGridRow(page, title2);
await expect(row2Restored.getByText(title2)).toBeVisible();
- await row2Restored.getByRole('link', { name: /Open document/ }).click();
+ await row2Restored.getByRole('link', { name: new RegExp(title2) }).click();
await verifyDocName(page, title2);
await page.getByRole('button', { name: 'Back to homepage' }).click();
diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx
index 8c1ab9ba8e..1be367ae1d 100644
--- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx
+++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx
@@ -17,6 +17,42 @@ import { DocsGridActions } from './DocsGridActions';
import { DocsGridItemSharedButton } from './DocsGridItemSharedButton';
import { DocsGridTrashbinActions } from './DocsGridTrashbinActions';
+const useDateToDisplay = (doc: Doc, isInTrashbin: boolean) => {
+ const { data: config } = useConfig();
+ const { t } = useTranslation();
+ const { relativeDate, calculateDaysLeft } = useDate();
+
+ let dateToDisplay = relativeDate(doc.updated_at);
+
+ if (isInTrashbin && config?.TRASHBIN_CUTOFF_DAYS && doc.deleted_at) {
+ const daysLeft = calculateDaysLeft(
+ doc.deleted_at,
+ config.TRASHBIN_CUTOFF_DAYS,
+ );
+
+ dateToDisplay = `${daysLeft} ${t('days', { count: daysLeft })}`;
+ }
+
+ return dateToDisplay;
+};
+
+const useDocItemAriaLabel = (doc: Doc, isInTrashbin: boolean) => {
+ const { t } = useTranslation();
+ const { untitledDocument } = useTrans();
+ const dateToDisplay = useDateToDisplay(doc, isInTrashbin);
+ const title = doc.title || untitledDocument;
+ const participantCount = Math.max((doc.nb_accesses_direct ?? 1) - 1, 0);
+
+ return t(
+ '{{title}}, updated {{date}}, shared with {{count}} participant(s)',
+ {
+ title,
+ date: dateToDisplay,
+ count: participantCount,
+ },
+ );
+};
+
type DocsGridItemProps = {
doc: Doc;
dragMode?: boolean;
@@ -26,13 +62,11 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
const searchParams = useSearchParams();
const target = searchParams.get('target');
const isInTrashbin = target === 'trashbin';
- const { untitledDocument } = useTrans();
- const { t } = useTranslation();
const { isDesktop } = useResponsiveStore();
const { flexLeft, flexRight } = useResponsiveDocGrid();
const { spacingsTokens } = useCunninghamTheme();
- const dateToDisplay = useDateToDisplay(doc, isInTrashbin);
+ const docItemAriaLabel = useDocItemAriaLabel(doc, isInTrashbin);
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
@@ -60,9 +94,6 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
}
`}
className="--docs--doc-grid-item"
- aria-label={t('Open document: {{title}}', {
- title: doc.title || untitledDocument,
- })}
>
{
`}
href={`/docs/${doc.id}`}
onKeyDown={handleKeyDown}
+ aria-label={docItemAriaLabel}
>
@@ -91,20 +123,13 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
$justify={isDesktop ? 'space-between' : 'flex-end'}
$gap="32px"
>
-
+
-
+
{isDesktop && (
@@ -202,25 +227,6 @@ const IconPublic = ({ isPublic }: { isPublic: boolean }) => {
);
};
-const useDateToDisplay = (doc: Doc, isInTrashbin: boolean) => {
- const { data: config } = useConfig();
- const { t } = useTranslation();
- const { relativeDate, calculateDaysLeft } = useDate();
-
- let dateToDisplay = relativeDate(doc.updated_at);
-
- if (isInTrashbin && config?.TRASHBIN_CUTOFF_DAYS && doc.deleted_at) {
- const daysLeft = calculateDaysLeft(
- doc.deleted_at,
- config.TRASHBIN_CUTOFF_DAYS,
- );
-
- dateToDisplay = `${daysLeft} ${t('days', { count: daysLeft })}`;
- }
-
- return dateToDisplay;
-};
-
export const DocsGridItemDate = ({
doc,
isDesktop,
diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItemSharedButton.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItemSharedButton.tsx
index 72e9b1ac94..b9146835c2 100644
--- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItemSharedButton.tsx
+++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItemSharedButton.tsx
@@ -44,6 +44,7 @@ export const DocsGridItemSharedButton = ({ doc, disabled }: Props) => {
className="--docs--doc-grid-item-shared-button"
aria-label={t('Open the sharing settings for the document')}
data-testid={`docs-grid-item-shared-button-${doc.id}`}
+ tabIndex={-1}
style={{
padding: `0 var(--c--globals--spacings--xxxs) 0 var(--c--globals--spacings--xxxs)`,
}}
diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/Draggable.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/Draggable.tsx
index bd72a1248c..fbe5bc2cfe 100644
--- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/Draggable.tsx
+++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/Draggable.tsx
@@ -19,6 +19,7 @@ export const Draggable = (props: PropsWithChildren>) => {
ref={setNodeRef}
{...listeners}
{...attributes}
+ tabIndex={-1}
data-testid={`draggable-doc-${props.id}`}
className="--docs--grid-draggable"
role="none"
diff --git a/src/frontend/apps/impress/src/i18n/translations.json b/src/frontend/apps/impress/src/i18n/translations.json
index b1b27e053b..ba3d7dd37e 100644
--- a/src/frontend/apps/impress/src/i18n/translations.json
+++ b/src/frontend/apps/impress/src/i18n/translations.json
@@ -566,7 +566,8 @@
"{{count}} result(s) available_one": "{{count}} Ergebnis verfügbar",
"{{count}} result(s) available_other": "{{count}} Ergebnisse verfügbar",
"{{name}} added to invite list. Add more members or press Tab to select role and invite.": "{{name}} wurde zur Einladungsliste hinzugefügt. Füge mehr Mitglieder hinzu oder drücke Tab, um Rollen auszuwählen und einzuladen.",
- "{{name}} removed from invite list": "{{name}} aus der Einladungsliste entfernt"
+ "{{name}} removed from invite list": "{{name}} aus der Einladungsliste entfernt",
+ "{{title}}, updated {{date}}, shared with {{count}} participant(s)": "{{title}}, aktualisiert {{date}}, geteilt mit {{count}} Teilnehmer(n)"
}
},
"el": {
@@ -1561,7 +1562,8 @@
"{{count}} result(s) available_other": "{{count}} résultat(s) disponible(s)",
"{{name}} added to invite list. Add more members or press Tab to select role and invite.": "{{name}} a été ajouté à la liste d'invitation. Ajoutez plus de membres ou appuyez sur Tab pour sélectionner le rôle et inviter.",
"{{name}} removed from invite list": "{{name}} a été retiré de la liste d'invitation",
- "{{title}}, updated {{date}}": "{{title}}, mise à jour {{date}}"
+ "{{title}}, updated {{date}}": "{{title}}, mise à jour {{date}}",
+ "{{title}}, updated {{date}}, shared with {{count}} participant(s)": "{{title}}, mis à jour {{date}}, partagé avec {{count}} participant(s)"
}
},
"it": {
@@ -2434,7 +2436,8 @@
"{{count}} result(s) available_other": "Результатов: {{count}}",
"{{name}} added to invite list. Add more members or press Tab to select role and invite.": "{{name}} добавлен(а) в список приглашённых. Добавьте других участников или нажмите Tab, чтобы выбрать роль и пригласить.",
"{{name}} removed from invite list": "{{name}} удален(а) из списка приглашений",
- "{{title}}, updated {{date}}": "{{title}}, обновлено {{date}}"
+ "{{title}}, updated {{date}}": "{{title}}, обновлено {{date}}",
+ "{{title}}, updated {{date}}, shared with {{count}} participant(s)": "{{title}}, обновлено {{date}}, доступ у {{count}} участник(ов)"
}
},
"sl": {
@@ -2933,7 +2936,8 @@
"{{count}} result(s) available_other": "Результатів: {{count}}",
"{{name}} added to invite list. Add more members or press Tab to select role and invite.": "{{name}} додано до списку запрошень. Додайте більше учасників або натисніть Tab, щоб вибрати роль та надіслати запрошення.",
"{{name}} removed from invite list": "{{name}} видалено зі списку запрошень",
- "{{title}}, updated {{date}}": "{{title}}, оновлено {{date}}"
+ "{{title}}, updated {{date}}": "{{title}}, оновлено {{date}}",
+ "{{title}}, updated {{date}}, shared with {{count}} participant(s)": "{{title}}, оновлено {{date}}, доступ у {{count}} учасник(ів)"
}
},
"zh": {