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": {