From c886cbb41d3ab6a0530701916a818a8169eebc03 Mon Sep 17 00:00:00 2001 From: Cyril Date: Tue, 17 Mar 2026 09:48:59 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BF=EF=B8=8F(frontend)=20fix=20language?= =?UTF-8?q?=20dropdown=20ARIA=20for=20screen=20readers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add missing attributes for language picker. --- CHANGELOG.md | 1 + .../e2e/__tests__/app-impress/doc-ai.spec.ts | 51 +++++++++++++------ .../app-impress/doc-comments.spec.ts | 7 ++- .../__tests__/app-impress/doc-editor.spec.ts | 5 +- .../app-impress/doc-grid-move.spec.ts | 11 ++-- .../__tests__/app-impress/doc-grid.spec.ts | 13 ++--- .../__tests__/app-impress/doc-header.spec.ts | 51 +++++++++++-------- .../app-impress/doc-inherited-share.spec.ts | 10 ++-- .../app-impress/doc-member-create.spec.ts | 31 ++++++----- .../app-impress/doc-member-list.spec.ts | 18 ++++--- .../__tests__/app-impress/doc-search.spec.ts | 12 +++-- .../__tests__/app-impress/doc-tree.spec.ts | 18 +++---- .../__tests__/app-impress/doc-version.spec.ts | 11 ++-- .../app-impress/doc-visibility.spec.ts | 29 ++++++----- .../e2e/__tests__/app-impress/footer.spec.ts | 6 +-- .../e2e/__tests__/app-impress/help.spec.ts | 7 ++- .../__tests__/app-impress/language.spec.ts | 2 +- .../e2e/__tests__/app-impress/utils-common.ts | 16 ++---- .../e2e/__tests__/app-impress/utils-share.ts | 9 ++-- .../components/dropdown-menu/DropdownMenu.tsx | 36 +++++++------ .../__tests__/DropdownMenu.spec.tsx | 12 ++--- .../language/components/LanguagePicker.tsx | 29 ++++++++--- 22 files changed, 220 insertions(+), 165 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6453730a0..063a52fc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to - 💄(frontend) improve comments highlights #1961 - ♿️(frontend) improve BoxButton a11y and native button semantics #2103 +- ♿️(frontend) improve language picker accessibility #2069 ## [v4.8.3] - 2026-03-23 diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-ai.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-ai.spec.ts index 170dbf7ed..9b4b76f19 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-ai.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-ai.spec.ts @@ -3,7 +3,6 @@ import { expect, test } from '@playwright/test'; import { createDoc, - getMenuItem, mockedDocument, overrideConfig, verifyDocName, @@ -179,18 +178,32 @@ test.describe('Doc AI feature', () => { await page.getByRole('button', { name: 'AI', exact: true }).click(); - await expect(getMenuItem(page, 'Use as prompt')).toBeVisible(); - await expect(getMenuItem(page, 'Rephrase')).toBeVisible(); - await expect(getMenuItem(page, 'Summarize')).toBeVisible(); - await expect(getMenuItem(page, 'Correct')).toBeVisible(); - await expect(getMenuItem(page, 'Language')).toBeVisible(); + await expect( + page.getByRole('menuitem', { name: 'Use as prompt' }), + ).toBeVisible(); + await expect( + page.getByRole('menuitem', { name: 'Rephrase' }), + ).toBeVisible(); + await expect( + page.getByRole('menuitem', { name: 'Summarize' }), + ).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Correct' })).toBeVisible(); + await expect( + page.getByRole('menuitem', { name: 'Language' }), + ).toBeVisible(); - await getMenuItem(page, 'Language').hover(); - await expect(getMenuItem(page, 'English', { exact: true })).toBeVisible(); - await expect(getMenuItem(page, 'French', { exact: true })).toBeVisible(); - await expect(getMenuItem(page, 'German', { exact: true })).toBeVisible(); + await page.getByRole('menuitem', { name: 'Language' }).hover(); + await expect( + page.getByRole('menuitem', { name: 'English', exact: true }), + ).toBeVisible(); + await expect( + page.getByRole('menuitem', { name: 'French', exact: true }), + ).toBeVisible(); + await expect( + page.getByRole('menuitem', { name: 'German', exact: true }), + ).toBeVisible(); - await getMenuItem(page, 'German', { exact: true }).click(); + await page.getByRole('menuitem', { name: 'German', exact: true }).click(); await expect(editor.getByText('Hallo Welt')).toBeVisible(); }); @@ -256,15 +269,23 @@ test.describe('Doc AI feature', () => { await page.getByRole('button', { name: 'AI', exact: true }).click(); if (ai_transform) { - await expect(getMenuItem(page, 'Use as prompt')).toBeVisible(); + await expect( + page.getByRole('menuitem', { name: 'Use as prompt' }), + ).toBeVisible(); } else { - await expect(getMenuItem(page, 'Use as prompt')).toBeHidden(); + await expect( + page.getByRole('menuitem', { name: 'Use as prompt' }), + ).toBeHidden(); } if (ai_translate) { - await expect(getMenuItem(page, 'Language')).toBeVisible(); + await expect( + page.getByRole('menuitem', { name: 'Language' }), + ).toBeVisible(); } else { - await expect(getMenuItem(page, 'Language')).toBeHidden(); + await expect( + page.getByRole('menuitem', { name: 'Language' }), + ).toBeHidden(); } }); }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts index e8028bddf..6641803be 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts @@ -3,7 +3,6 @@ import { expect, test } from '@playwright/test'; import { closeHeaderMenu, createDoc, - getMenuItem, getOtherBrowserName, verifyDocName, } from './utils-common'; @@ -152,7 +151,7 @@ test.describe('Doc Comments', () => { // Edit Comment await thread.getByText('This is a comment').first().hover(); await thread.locator('[data-test="moreactions"]').first().click(); - await getMenuItem(thread, 'Edit comment').click(); + await thread.getByRole('menuitem', { name: 'Edit comment' }).click(); const commentEditor = thread.getByText('This is a comment').first(); await commentEditor.fill('This is an edited comment'); const saveBtn = thread.locator('button[data-test="save"]').first(); @@ -177,7 +176,7 @@ test.describe('Doc Comments', () => { // Delete second comment await thread.getByText('This is a second comment').first().hover(); await thread.locator('[data-test="moreactions"]').first().click(); - await getMenuItem(thread, 'Delete comment').click(); + await thread.getByRole('menuitem', { name: 'Delete comment' }).click(); await expect( thread.getByText('This is a second comment').first(), ).toBeHidden(); @@ -210,7 +209,7 @@ test.describe('Doc Comments', () => { await thread.getByText('This is a new comment').first().hover(); await thread.locator('[data-test="moreactions"]').first().click(); - await getMenuItem(thread, 'Delete comment').click(); + await thread.getByRole('menuitem', { name: 'Delete comment' }).click(); await expect(editor.getByText('Hello')).not.toHaveClass('bn-thread-mark'); await expect(editor.getByText('Hello')).toHaveCSS( diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts index fc6aee7d8..94268a323 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts @@ -5,7 +5,6 @@ import cs from 'convert-stream'; import { createDoc, - getMenuItem, goToGridDoc, overrideConfig, verifyDocName, @@ -148,7 +147,7 @@ test.describe('Doc Editor', () => { const wsClosePromise = webSocket.waitForEvent('close'); await selectVisibility.click(); - await getMenuItem(page, 'Connected').click(); + await page.getByRole('menuitemradio', { name: 'Connected' }).click(); // Assert that the doc reconnects to the ws const wsClose = await wsClosePromise; @@ -605,7 +604,7 @@ test.describe('Doc Editor', () => { await page.getByRole('button', { name: 'Share' }).click(); await page.getByTestId('doc-access-mode').click(); - await getMenuItem(page, 'Reading').click(); + await page.getByRole('menuitemradio', { name: 'Reading' }).click(); // Close the modal await page.getByRole('button', { name: 'close' }).first().click(); 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 63d7f91bb..8a3c970a7 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 @@ -3,7 +3,6 @@ import { expect, test } from '@playwright/test'; import { createDoc, getGridRow, - getMenuItem, getOtherBrowserName, mockedListDocs, toggleHeaderMenu, @@ -207,7 +206,7 @@ test.describe('Doc grid move', () => { const row = await getGridRow(page, titleDoc1); await row.getByText(`more_horiz`).click(); - await getMenuItem(page, 'Move into a doc').click(); + await page.getByRole('menuitem', { name: 'Move into a doc' }).click(); await expect( page.getByRole('dialog').getByRole('heading', { name: 'Move' }), @@ -295,7 +294,7 @@ test.describe('Doc grid move', () => { const row = await getGridRow(page, titleDoc1); await row.getByText(`more_horiz`).click(); - await getMenuItem(page, 'Move into a doc').click(); + await page.getByRole('menuitem', { name: 'Move into a doc' }).click(); await expect( page.getByRole('dialog').getByRole('heading', { name: 'Move' }), @@ -342,7 +341,9 @@ test.describe('Doc grid move', () => { `doc-share-access-request-row-${emailRequest}`, ); await container.getByTestId('doc-role-dropdown').click(); - await getMenuItem(otherPage, 'Administrator').click(); + await otherPage + .getByRole('menuitemradio', { name: 'Administrator' }) + .click(); await container.getByRole('button', { name: 'Approve' }).click(); await expect(otherPage.getByText('Access Requests')).toBeHidden(); @@ -353,7 +354,7 @@ test.describe('Doc grid move', () => { await page.reload(); await row.getByText(`more_horiz`).click(); - await getMenuItem(page, 'Move into a doc').click(); + await page.getByRole('menuitem', { name: 'Move into a doc' }).click(); await expect( page.getByRole('dialog').getByRole('heading', { name: 'Move' }), diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts index eb13f07f0..95a4258ed 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts @@ -1,11 +1,6 @@ import { expect, test } from '@playwright/test'; -import { - createDoc, - getGridRow, - getMenuItem, - verifyDocName, -} from './utils-common'; +import { createDoc, getGridRow, verifyDocName } from './utils-common'; import { addNewMember, connectOtherUserToDoc } from './utils-share'; type SmallDoc = { @@ -104,7 +99,7 @@ test.describe('Document grid item options', () => { const row = await getGridRow(page, docTitle); await row.getByText(`more_horiz`).click(); - await getMenuItem(page, 'Share').click(); + await page.getByRole('menuitem', { name: 'Share' }).click(); await expect( page.getByRole('dialog').getByText('Share the document'), @@ -120,7 +115,7 @@ test.describe('Document grid item options', () => { // Pin await row.getByText(`more_horiz`).click(); - await getMenuItem(page, 'Pin').click(); + await page.getByRole('menuitem', { name: 'Pin' }).click(); // Check is pinned await expect(row.getByTestId('doc-pinned-icon')).toBeVisible(); @@ -147,7 +142,7 @@ test.describe('Document grid item options', () => { const row = await getGridRow(page, docTitle); await row.getByText(`more_horiz`).click(); - await getMenuItem(page, 'Delete').click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); await expect( page.getByRole('heading', { name: 'Delete a doc' }), 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 3c165050c..a1b98768d 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 @@ -3,7 +3,6 @@ import { expect, test } from '@playwright/test'; import { createDoc, getGridRow, - getMenuItem, goToGridDoc, mockedDocument, verifyDocName, @@ -79,7 +78,7 @@ test.describe('Doc Header', () => { await page.getByTestId('doc-visibility').click(); - await getMenuItem(page, 'Public').click(); + await page.getByRole('menuitemradio', { name: 'Public' }).click(); await page.getByRole('button', { name: 'close' }).first().click(); @@ -153,8 +152,10 @@ test.describe('Doc Header', () => { const emojiPicker = page.locator('.--docs--doc-title').getByRole('button'); const optionMenu = page.getByLabel('Open the document options'); - const addEmojiMenuItem = getMenuItem(page, 'Add emoji'); - const removeEmojiMenuItem = getMenuItem(page, 'Remove emoji'); + const addEmojiMenuItem = page.getByRole('menuitem', { name: 'Add emoji' }); + const removeEmojiMenuItem = page.getByRole('menuitem', { + name: 'Remove emoji', + }); // Top parent should not have emoji picker await expect(emojiPicker).toBeHidden(); @@ -208,7 +209,7 @@ test.describe('Doc Header', () => { const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1); await page.getByLabel('Open the document options').click(); - await getMenuItem(page, 'Delete document').click(); + await page.getByRole('menuitem', { name: 'Delete document' }).click(); await expect( page.getByRole('heading', { name: 'Delete a doc' }), @@ -270,7 +271,9 @@ test.describe('Doc Header', () => { await page.getByLabel('Open the document options').click(); - await expect(getMenuItem(page, 'Delete document')).toBeDisabled(); + await expect( + page.getByRole('menuitem', { name: 'Delete document' }), + ).toBeDisabled(); // Click somewhere else to close the options await page.locator('body').click({ position: { x: 0, y: 0 } }); @@ -293,7 +296,7 @@ test.describe('Doc Header', () => { await invitationRole.click(); - await getMenuItem(page, 'Remove access').click(); + await page.getByRole('menuitemradio', { name: 'Remove access' }).click(); await expect(invitationCard).toBeHidden(); const memberCard = shareModal.getByLabel('List members card'); @@ -305,7 +308,9 @@ test.describe('Doc Header', () => { await expect(roles).toBeVisible(); await roles.click(); - await expect(getMenuItem(page, 'Remove access')).toBeEnabled(); + await expect( + page.getByRole('menuitemradio', { name: 'Remove access' }), + ).toBeEnabled(); }); test('it checks the options available if editor', async ({ page }) => { @@ -345,7 +350,9 @@ test.describe('Doc Header', () => { ).toBeVisible(); await page.getByLabel('Open the document options').click(); - await expect(getMenuItem(page, 'Delete document')).toBeDisabled(); + await expect( + page.getByRole('menuitem', { name: 'Delete document' }), + ).toBeDisabled(); // Click somewhere else to close the options await page.locator('body').click({ position: { x: 0, y: 0 } }); @@ -415,7 +422,9 @@ test.describe('Doc Header', () => { ).toBeVisible(); await page.getByLabel('Open the document options').click(); - await expect(getMenuItem(page, 'Delete document')).toBeDisabled(); + await expect( + page.getByRole('menuitem', { name: 'Delete document' }), + ).toBeDisabled(); // Click somewhere else to close the options await page.locator('body').click({ position: { x: 0, y: 0 } }); @@ -473,7 +482,7 @@ test.describe('Doc Header', () => { // Copy content to clipboard await page.getByLabel('Open the document options').click(); - await getMenuItem(page, 'Copy as Markdown').click(); + await page.getByRole('menuitem', { name: 'Copy as Markdown' }).click(); await expect( page.getByText('Copied as Markdown to clipboard'), ).toBeVisible(); @@ -537,7 +546,7 @@ test.describe('Doc Header', () => { .click(); // Pin - await getMenuItem(page, 'Pin').click(); + await page.getByRole('menuitem', { name: 'Pin' }).click(); await page .getByRole('button', { name: 'Open the document options' }) .click(); @@ -558,11 +567,11 @@ test.describe('Doc Header', () => { .click(); // Unpin - await getMenuItem(page, 'Unpin').click(); + await page.getByRole('menuitem', { name: 'Unpin' }).click(); await page .getByRole('button', { name: 'Open the document options' }) .click(); - await expect(getMenuItem(page, 'Pin')).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Pin' })).toBeVisible(); await page.goto('/'); @@ -580,7 +589,7 @@ test.describe('Doc Header', () => { await page.getByLabel('Open the document options').click(); - await getMenuItem(page, 'Duplicate').click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); await expect( page.getByText('Document duplicated successfully!'), ).toBeVisible(); @@ -595,7 +604,7 @@ test.describe('Doc Header', () => { await expect(row.getByText(duplicateTitle)).toBeVisible(); await row.getByText(`more_horiz`).click(); - await getMenuItem(page, 'Duplicate').click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); const duplicateDuplicateTitle = 'Copy of ' + duplicateTitle; await page.getByText(duplicateDuplicateTitle).click(); await expect(page.getByText('Hello Duplicated World')).toBeVisible(); @@ -628,7 +637,7 @@ test.describe('Doc Header', () => { const currentUrl = page.url(); - await getMenuItem(page, 'Duplicate').click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); await expect(page).not.toHaveURL(new RegExp(currentUrl)); @@ -667,8 +676,10 @@ test.describe('Documents Header mobile', () => { await expect(page.getByRole('button', { name: 'Copy link' })).toBeHidden(); await page.getByLabel('Open the document options').click(); - await expect(getMenuItem(page, 'Copy link')).toBeVisible(); - await getMenuItem(page, 'Share').click(); + await expect( + page.getByRole('menuitem', { name: 'Copy link' }), + ).toBeVisible(); + await page.getByRole('menuitem', { name: 'Share' }).click(); await expect(page.getByRole('button', { name: 'Copy link' })).toBeVisible(); }); @@ -691,7 +702,7 @@ test.describe('Documents Header mobile', () => { await goToGridDoc(page); await page.getByLabel('Open the document options').click(); - await getMenuItem(page, 'Share').click(); + await page.getByRole('menuitem', { name: 'Share' }).click(); const shareModal = page.getByRole('dialog', { name: 'Share the document', diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-inherited-share.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-inherited-share.spec.ts index 452cead2f..d3a713272 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-inherited-share.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-inherited-share.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; -import { createDoc, getMenuItem, verifyDocName } from './utils-common'; +import { createDoc, verifyDocName } from './utils-common'; import { updateShareLink } from './utils-share'; import { createRootSubPage } from './utils-sub-pages'; @@ -53,17 +53,19 @@ test.describe('Inherited share accesses', () => { await expect(docVisibilityCard.getByText('Reading')).toBeVisible(); await docVisibilityCard.getByText('Reading').click(); - await getMenuItem(page, 'Editing').click(); + await page.getByRole('menuitemradio', { name: 'Editing' }).click(); await expect(docVisibilityCard.getByText('Reading')).toBeHidden(); await expect(docVisibilityCard.getByText('Editing')).toBeVisible(); // Verify inherited link await docVisibilityCard.getByText('Connected').click(); - await expect(getMenuItem(page, 'Private')).toBeDisabled(); + await expect( + page.getByRole('menuitemradio', { name: 'Private' }), + ).toBeDisabled(); // Update child link - await getMenuItem(page, 'Public').click(); + await page.getByRole('menuitemradio', { name: 'Public' }).click(); await expect(docVisibilityCard.getByText('Connected')).toBeHidden(); await expect( diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts index 8f2418b4d..20f9d93d7 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts @@ -3,7 +3,6 @@ import { expect, test } from '@playwright/test'; import { BROWSERS, createDoc, - getMenuItem, keyCloakSignIn, randomName, verifyDocName, @@ -111,13 +110,21 @@ test.describe('Document create member', () => { // Check roles are displayed await list.getByTestId('doc-role-dropdown').click(); - await expect(getMenuItem(page, 'Reader')).toBeVisible(); - await expect(getMenuItem(page, 'Editor')).toBeVisible(); - await expect(getMenuItem(page, 'Owner')).toBeVisible(); - await expect(getMenuItem(page, 'Administrator')).toBeVisible(); + await expect( + page.getByRole('menuitemradio', { name: 'Reader' }), + ).toBeVisible(); + await expect( + page.getByRole('menuitemradio', { name: 'Editor' }), + ).toBeVisible(); + await expect( + page.getByRole('menuitemradio', { name: 'Owner' }), + ).toBeVisible(); + await expect( + page.getByRole('menuitemradio', { name: 'Administrator' }), + ).toBeVisible(); // Validate - await getMenuItem(page, 'Administrator').click(); + await page.getByRole('menuitemradio', { name: 'Administrator' }).click(); await page.getByTestId('doc-share-invite-button').click(); // Check invitation added @@ -163,7 +170,7 @@ test.describe('Document create member', () => { // Choose a role const container = page.getByTestId('doc-share-add-member-list'); await container.getByTestId('doc-role-dropdown').click(); - await getMenuItem(page, 'Owner').click(); + await page.getByRole('menuitemradio', { name: 'Owner' }).click(); const responsePromiseCreateInvitation = page.waitForResponse( (response) => @@ -181,7 +188,7 @@ test.describe('Document create member', () => { // Choose a role await container.getByTestId('doc-role-dropdown').click(); - await getMenuItem(page, 'Owner').click(); + await page.getByRole('menuitemradio', { name: 'Owner' }).click(); const responsePromiseCreateInvitationFail = page.waitForResponse( (response) => @@ -218,7 +225,7 @@ test.describe('Document create member', () => { // Choose a role const container = page.getByTestId('doc-share-add-member-list'); await container.getByTestId('doc-role-dropdown').click(); - await getMenuItem(page, 'Administrator').click(); + await page.getByRole('menuitemradio', { name: 'Administrator' }).click(); const responsePromiseCreateInvitation = page.waitForResponse( (response) => @@ -245,13 +252,13 @@ test.describe('Document create member', () => { ); await userInvitation.getByTestId('doc-role-dropdown').click(); - await getMenuItem(page, 'Reader').click(); + await page.getByRole('menuitemradio', { name: 'Reader' }).click(); const responsePatchInvitation = await responsePromisePatchInvitation; expect(responsePatchInvitation.ok()).toBeTruthy(); await userInvitation.getByTestId('doc-role-dropdown').click(); - await getMenuItem(page, 'Remove access').click(); + await page.getByRole('menuitemradio', { name: 'Remove access' }).click(); await expect(userInvitation).toBeHidden(); }); @@ -303,7 +310,7 @@ test.describe('Document create member', () => { `doc-share-access-request-row-${emailRequest}`, ); await container.getByTestId('doc-role-dropdown').click(); - await getMenuItem(page, 'Administrator').click(); + await page.getByRole('menuitemradio', { name: 'Administrator' }).click(); await container.getByRole('button', { name: 'Approve' }).click(); await expect(page.getByText('Access Requests')).toBeHidden(); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts index 62298109d..726d4fa2e 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; -import { createDoc, getMenuItem, verifyDocName } from './utils-common'; +import { createDoc, verifyDocName } from './utils-common'; import { addNewMember } from './utils-share'; test.beforeEach(async ({ page }) => { @@ -160,7 +160,9 @@ test.describe('Document list members', () => { `You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.`, ); await expect(soloOwner).toBeVisible(); - await expect(getMenuItem(page, 'Administrator')).toBeDisabled(); + await expect( + page.getByRole('menuitemradio', { name: 'Administrator' }), + ).toBeDisabled(); await list.click({ force: true, // Force click to close the dropdown @@ -183,18 +185,20 @@ test.describe('Document list members', () => { }); await currentUserRole.click(); - await getMenuItem(page, 'Administrator').click(); + await page.getByRole('menuitemradio', { name: 'Administrator' }).click(); await list.click(); await expect(currentUserRole).toBeVisible(); await newUserRoles.click(); - await expect(getMenuItem(page, 'Owner')).toBeDisabled(); + await expect( + page.getByRole('menuitemradio', { name: 'Owner' }), + ).toBeDisabled(); await list.click({ force: true, // Force click to close the dropdown }); await currentUserRole.click(); - await getMenuItem(page, 'Reader').click(); + await page.getByRole('menuitemradio', { name: 'Reader' }).click(); await list.click({ force: true, // Force click to close the dropdown }); @@ -234,11 +238,11 @@ test.describe('Document list members', () => { await expect(userReader).toBeVisible(); await userReaderRole.click(); - await getMenuItem(page, 'Remove access').click(); + await page.getByRole('menuitemradio', { name: 'Remove access' }).click(); await expect(userReader).toBeHidden(); await mySelfRole.click(); - await getMenuItem(page, 'Remove access').click(); + await page.getByRole('menuitemradio', { name: 'Remove access' }).click(); await expect( page.getByText('Insufficient access rights to view the document.'), ).toBeVisible(); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-search.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-search.spec.ts index 36cebff75..7497d6737 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-search.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-search.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; -import { createDoc, getMenuItem, verifyDocName } from './utils-common'; +import { createDoc, verifyDocName } from './utils-common'; import { createRootSubPage } from './utils-sub-pages'; test.beforeEach(async ({ page }) => { @@ -136,9 +136,13 @@ test.describe('Document search', () => { await filters.click(); await filters.getByRole('button', { name: 'Current doc' }).click(); - await expect(getMenuItem(page, 'All docs')).toBeVisible(); - await expect(getMenuItem(page, 'Current doc')).toBeVisible(); - await getMenuItem(page, 'All docs').click(); + await expect( + page.getByRole('menuitemcheckbox', { name: 'All docs' }), + ).toBeVisible(); + await expect( + page.getByRole('menuitemcheckbox', { name: 'Current doc' }), + ).toBeVisible(); + await page.getByRole('menuitemcheckbox', { name: 'All docs' }).click(); await expect(page.getByRole('button', { name: 'Reset' })).toBeVisible(); }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts index 20c24bce7..af64a9c52 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts @@ -3,7 +3,6 @@ import { expect, test } from '@playwright/test'; import { createDoc, expectLoginPage, - getMenuItem, keyCloakSignIn, updateDocTitle, verifyDocName, @@ -158,7 +157,7 @@ test.describe('Doc Tree', () => { ); const currentUserRole = currentUser.getByTestId('doc-role-dropdown'); await currentUserRole.click(); - await getMenuItem(page, 'Administrator').click(); + await page.getByRole('menuitemradio', { name: 'Administrator' }).click(); await list.click(); await page.getByRole('button', { name: 'Ok' }).click(); @@ -188,10 +187,9 @@ test.describe('Doc Tree', () => { const menu = child.getByText(`more_horiz`); await menu.click(); - await expect(getMenuItem(page, 'Move to my docs')).toHaveAttribute( - 'aria-disabled', - 'true', - ); + await expect( + page.getByRole('menuitem', { name: 'Move to my docs' }), + ).toHaveAttribute('aria-disabled', 'true'); }); test('keyboard navigation with Enter key opens documents', async ({ @@ -335,7 +333,9 @@ test.describe('Doc Tree', () => { await row.hover(); const menu = row.getByText(`more_horiz`); await menu.click(); - await expect(getMenuItem(page, 'Remove emoji')).toBeHidden(); + await expect( + page.getByRole('menuitem', { name: 'Remove emoji' }), + ).toBeHidden(); // Close the menu await page.keyboard.press('Escape'); @@ -355,7 +355,7 @@ test.describe('Doc Tree', () => { // Now remove the emoji using the new action await row.hover(); await menu.click(); - await getMenuItem(page, 'Remove emoji').click(); + await page.getByRole('menuitem', { name: 'Remove emoji' }).click(); await expect(row.getByText('😀')).toBeHidden(); await expect(titleEmojiPicker).toBeHidden(); @@ -385,7 +385,7 @@ test.describe('Doc Tree: Inheritance', () => { const selectVisibility = page.getByTestId('doc-visibility'); await selectVisibility.click(); - await getMenuItem(page, 'Public').click(); + await page.getByRole('menuitemradio', { name: 'Public' }).click(); await expect( page.getByText('The document visibility has been updated.'), diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts index 87441eec3..fe754a3e6 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts @@ -2,7 +2,6 @@ import { expect, test } from '@playwright/test'; import { createDoc, - getMenuItem, goToGridDoc, mockedDocument, verifyDocName, @@ -21,7 +20,7 @@ test.describe('Doc Version', () => { // Initially, there is no version await page.getByLabel('Open the document options').click(); - await getMenuItem(page, 'Version history').click(); + await page.getByRole('menuitem', { name: 'Version history' }).click(); await expect(page.getByText('History', { exact: true })).toBeVisible(); const modal = page.getByRole('dialog', { name: 'Version history' }); @@ -75,7 +74,7 @@ test.describe('Doc Version', () => { ).toBeVisible(); await page.getByLabel('Open the document options').click(); - await getMenuItem(page, 'Version history').click(); + await page.getByRole('menuitem', { name: 'Version history' }).click(); await expect(panel).toBeVisible(); await expect(page.getByText('History', { exact: true })).toBeVisible(); @@ -125,7 +124,9 @@ test.describe('Doc Version', () => { await verifyDocName(page, 'Mocked document'); await page.getByLabel('Open the document options').click(); - await expect(getMenuItem(page, 'Version history')).toBeDisabled(); + await expect( + page.getByRole('menuitem', { name: 'Version history' }), + ).toBeDisabled(); }); test('it restores the doc version', async ({ page, browserName }) => { @@ -152,7 +153,7 @@ test.describe('Doc Version', () => { await expect(page.getByText('World')).toBeVisible(); await page.getByLabel('Open the document options').click(); - await getMenuItem(page, 'Version history').click(); + await page.getByRole('menuitem', { name: 'Version history' }).click(); const modal = page.getByRole('dialog', { name: 'Version history' }); const panel = modal.getByLabel('Version list'); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts index 4da4fbbd1..ab528ee4b 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts @@ -4,7 +4,6 @@ import { BROWSERS, createDoc, expectLoginPage, - getMenuItem, keyCloakSignIn, verifyDocName, } from './utils-common'; @@ -47,17 +46,21 @@ test.describe('Doc Visibility', () => { await expect(selectVisibility.getByText('Private')).toBeVisible(); - await expect(getMenuItem(page, 'Read only')).toBeHidden(); - await expect(getMenuItem(page, 'Can read and edit')).toBeHidden(); + await expect( + page.getByRole('menuitemradio', { name: 'Read only' }), + ).toBeHidden(); + await expect( + page.getByRole('menuitemradio', { name: 'Can read and edit' }), + ).toBeHidden(); await selectVisibility.click(); - await getMenuItem(page, 'Connected').click(); + await page.getByRole('menuitemradio', { name: 'Connected' }).click(); await expect(page.getByTestId('doc-access-mode')).toBeVisible(); await selectVisibility.click(); - await getMenuItem(page, 'Public').click(); + await page.getByRole('menuitemradio', { name: 'Public' }).click(); await expect(page.getByTestId('doc-access-mode')).toBeVisible(); }); @@ -202,7 +205,7 @@ test.describe('Doc Visibility: Public', () => { const selectVisibility = page.getByTestId('doc-visibility'); await selectVisibility.click(); - await getMenuItem(page, 'Public').click(); + await page.getByRole('menuitemradio', { name: 'Public' }).click(); await expect( page.getByText('The document visibility has been updated.'), @@ -210,7 +213,7 @@ test.describe('Doc Visibility: Public', () => { await expect(page.getByTestId('doc-access-mode')).toBeVisible(); await page.getByTestId('doc-access-mode').click(); - await getMenuItem(page, 'Reading').click(); + await page.getByRole('menuitemradio', { name: 'Reading' }).click(); await expect( page.getByText('The document visibility has been updated.').first(), @@ -296,14 +299,14 @@ test.describe('Doc Visibility: Public', () => { const selectVisibility = page.getByTestId('doc-visibility'); await selectVisibility.click(); - await getMenuItem(page, 'Public').click(); + await page.getByRole('menuitemradio', { name: 'Public' }).click(); await expect( page.getByText('The document visibility has been updated.'), ).toBeVisible(); await page.getByTestId('doc-access-mode').click(); - await getMenuItem(page, 'Editing').click(); + await page.getByRole('menuitemradio', { name: 'Editing' }).click(); await expect( page.getByText('The document visibility has been updated.').first(), @@ -387,7 +390,7 @@ test.describe('Doc Visibility: Authenticated', () => { await page.getByRole('button', { name: 'Share' }).click(); const selectVisibility = page.getByTestId('doc-visibility'); await selectVisibility.click(); - await getMenuItem(page, 'Connected').click(); + await page.getByRole('menuitemradio', { name: 'Connected' }).click(); await expect( page.getByText('The document visibility has been updated.'), @@ -435,7 +438,7 @@ test.describe('Doc Visibility: Authenticated', () => { await page.getByRole('button', { name: 'Share' }).click(); const selectVisibility = page.getByTestId('doc-visibility'); await selectVisibility.click(); - await getMenuItem(page, 'Connected').click(); + await page.getByRole('menuitemradio', { name: 'Connected' }).click(); await expect( page.getByText('The document visibility has been updated.'), @@ -533,7 +536,7 @@ test.describe('Doc Visibility: Authenticated', () => { await page.getByRole('button', { name: 'Share' }).click(); const selectVisibility = page.getByTestId('doc-visibility'); await selectVisibility.click(); - await getMenuItem(page, 'Connected').click(); + await page.getByRole('menuitemradio', { name: 'Connected' }).click(); await expect( page.getByText('The document visibility has been updated.'), @@ -541,7 +544,7 @@ test.describe('Doc Visibility: Authenticated', () => { const urlDoc = page.url(); await page.getByTestId('doc-access-mode').click(); - await getMenuItem(page, 'Editing').click(); + await page.getByRole('menuitemradio', { name: 'Editing' }).click(); await expect( page.getByText('The document visibility has been updated.').first(), diff --git a/src/frontend/apps/e2e/__tests__/app-impress/footer.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/footer.spec.ts index 7fb446f7e..b599e43ad 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/footer.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/footer.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; -import { getMenuItem, overrideConfig } from './utils-common'; +import { overrideConfig } from './utils-common'; test.describe('Footer', () => { test.use({ storageState: { cookies: [], origins: [] } }); @@ -47,7 +47,7 @@ test.describe('Footer', () => { // Check the translation const header = page.locator('header').first(); await header.getByRole('button').getByText('English').click(); - await getMenuItem(page, 'Français').click(); + await page.getByRole('menuitemradio', { name: 'Français' }).click(); await expect( page.locator('footer').getByText('Mentions légales'), @@ -131,7 +131,7 @@ test.describe('Footer', () => { // Check the translation const header = page.locator('header').first(); await header.getByRole('button').getByText('English').click(); - await getMenuItem(page, 'Français').click(); + await page.getByRole('menuitemradio', { name: 'Français' }).click(); await expect( page diff --git a/src/frontend/apps/e2e/__tests__/app-impress/help.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/help.spec.ts index f1922757c..428adf0aa 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/help.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/help.spec.ts @@ -2,7 +2,6 @@ import { expect, test } from '@playwright/test'; import { TestLanguage, - getMenuItem, overrideConfig, waitForLanguageSwitch, } from './utils-common'; @@ -45,7 +44,7 @@ test.describe('Help feature', () => { await page.getByRole('button', { name: 'Open help menu' }).click(); - await getMenuItem(page, 'Onboarding').click(); + await page.getByRole('menuitem', { name: 'Onboarding' }).click(); const modal = page.getByTestId('onboarding-modal'); await expect(modal).toBeVisible(); @@ -88,7 +87,7 @@ test.describe('Help feature', () => { test('closes modal with Skip button', async ({ page }) => { await page.getByRole('button', { name: 'Open help menu' }).click(); - await getMenuItem(page, 'Onboarding').click(); + await page.getByRole('menuitem', { name: 'Onboarding' }).click(); const modal = page.getByTestId('onboarding-modal'); await expect(modal).toBeVisible(); @@ -109,7 +108,7 @@ test.describe('Help feature', () => { await page.getByRole('button', { name: "Ouvrir le menu d'aide" }).click(); - await getMenuItem(page, 'Premiers pas').click(); + await page.getByRole('menuitem', { name: 'Premiers pas' }).click(); const modal = page.getByLabel('Apprenez les principes fondamentaux'); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts index f6e80f5b6..1a064ee3b 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts @@ -75,7 +75,7 @@ test.describe('Language', () => { await expect(page.locator('[role="menu"]')).toBeVisible(); - const menuItems = page.locator('[role="menuitem"], [role="menuitemradio"]'); + const menuItems = page.locator('[role="menuitemradio"]'); await expect(menuItems.first()).toBeVisible(); await menuItems.first().click(); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts index 22f603a45..ad4ae595d 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts @@ -8,16 +8,6 @@ import theme_customization from '../../../../../backend/impress/configuration/th export type BrowserName = 'chromium' | 'firefox' | 'webkit'; export const BROWSERS: BrowserName[] = ['chromium', 'webkit', 'firefox']; -/** Returns a locator for a menu item (handles both menuitem and menuitemradio roles) */ -export const getMenuItem = ( - context: Page | Locator, - name: string, - options?: { exact?: boolean }, -): Locator => - context - .getByRole('menuitem', { name, exact: options?.exact }) - .or(context.getByRole('menuitemradio', { name, exact: options?.exact })); - export const CONFIG = { AI_BOT: { name: 'Docs AI', @@ -392,12 +382,12 @@ export async function waitForLanguageSwitch( await languagePicker.click(); - await getMenuItem(page, lang.label).click(); + await page.getByRole('menuitemradio', { name: lang.label }).click(); } export const clickInEditorMenu = async (page: Page, textButton: string) => { await page.getByRole('button', { name: 'Open the document options' }).click(); - await getMenuItem(page, textButton).click(); + await page.getByRole('menuitem', { name: textButton }).click(); }; export const clickInGridMenu = async ( @@ -408,7 +398,7 @@ export const clickInGridMenu = async ( await row .getByRole('button', { name: /Open the menu of actions for the document/ }) .click(); - await getMenuItem(page, textButton).click(); + await page.getByRole('menuitem', { name: textButton }).click(); }; export const writeReport = async ( diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-share.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-share.ts index 5f9a4cd47..dc89907ea 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-share.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-share.ts @@ -2,7 +2,6 @@ import { Page, chromium, expect } from '@playwright/test'; import { BrowserName, - getMenuItem, getOtherBrowserName, keyCloakSignIn, verifyDocName, @@ -40,7 +39,7 @@ export const addNewMember = async ( // Choose a role await page.getByTestId('doc-role-dropdown').click(); - await getMenuItem(page, role).click(); + await page.getByRole('menuitemradio', { name: role }).click(); await page.getByTestId('doc-share-invite-button').click(); return users[index].email; @@ -52,7 +51,7 @@ export const updateShareLink = async ( linkRole?: LinkRole | null, ) => { await page.getByTestId('doc-visibility').click(); - await getMenuItem(page, linkReach).click(); + await page.getByRole('menuitemradio', { name: linkReach }).click(); const visibilityUpdatedText = page .getByText('The document visibility has been updated') @@ -62,7 +61,7 @@ export const updateShareLink = async ( if (linkRole) { await page.getByTestId('doc-access-mode').click(); - await getMenuItem(page, linkRole).click(); + await page.getByRole('menuitemradio', { name: linkRole }).click(); await expect(visibilityUpdatedText).toBeVisible(); } }; @@ -77,7 +76,7 @@ export const updateRoleUser = async ( const currentUser = list.getByTestId(`doc-share-member-row-${email}`); const currentUserRole = currentUser.getByTestId('doc-role-dropdown'); await currentUserRole.click(); - await getMenuItem(page, role).click(); + await page.getByRole('menuitemradio', { name: role }).click(); await list.click(); }; diff --git a/src/frontend/apps/impress/src/components/dropdown-menu/DropdownMenu.tsx b/src/frontend/apps/impress/src/components/dropdown-menu/DropdownMenu.tsx index 71f70eb0b..73adb5cde 100644 --- a/src/frontend/apps/impress/src/components/dropdown-menu/DropdownMenu.tsx +++ b/src/frontend/apps/impress/src/components/dropdown-menu/DropdownMenu.tsx @@ -26,6 +26,7 @@ import { useDropdownKeyboardNav } from './hook/useDropdownKeyboardNav'; export type DropdownMenuOption = { icon?: ReactNode; label: string; + lang?: string; testId?: string; value?: string; callback?: () => void | Promise; @@ -70,6 +71,9 @@ export const DropdownMenu = ({ const [focusedIndex, setFocusedIndex] = useState(-1); const blockButtonRef = useRef(null); const menuItemRefs = useRef<(HTMLButtonElement | null)[]>([]); + const isSingleSelectable = options.some( + (option) => option.isSelected !== undefined, + ); const onOpenChange = useCallback( (isOpen: boolean) => { @@ -110,10 +114,6 @@ export const DropdownMenu = ({ [onOpenChange], ); - const hasSelectable = - selectedValues !== undefined || - options.some((option) => option.isSelected !== undefined); - if (disabled) { return children; } @@ -176,20 +176,25 @@ export const DropdownMenu = ({ } const isDisabled = option.disabled !== undefined && option.disabled; const isFocused = index === focusedIndex; - const ariaChecked = hasSelectable - ? option.isSelected || - selectedValues?.includes(option.value ?? '') || - false - : undefined; + const isSelected = + option.isSelected === true || + (selectedValues?.includes(option.value ?? '') ?? false); + const itemRole = + selectedValues !== undefined + ? 'menuitemcheckbox' + : isSingleSelectable + ? 'menuitemradio' + : 'menuitem'; + const optionKey = option.value ?? option.testId ?? `option-${index}`; return ( - + { menuItemRefs.current[index] = el; }} - role={hasSelectable ? 'menuitemradio' : 'menuitem'} - aria-checked={ariaChecked} + role={itemRole} + aria-checked={itemRole === 'menuitem' ? undefined : isSelected} data-testid={option.testId} $direction="row" disabled={isDisabled} @@ -200,7 +205,6 @@ export const DropdownMenu = ({ triggerOption(option); }} onKeyDown={keyboardAction(() => triggerOption(option))} - key={option.label} $align="center" $justify="space-between" $background="var(--c--contextuals--background--surface--primary)" @@ -271,16 +275,16 @@ export const DropdownMenu = ({ )} - {option.label} + {option.label} - {(option.isSelected || - selectedValues?.includes(option.value ?? '')) && ( + {isSelected && ( ', () => { expect(radios[2]).toHaveAttribute('aria-checked', 'false'); }); - test('renders menuitemradio role with aria-checked when selectedValues is provided', async () => { + test('renders menuitemcheckbox role with aria-checked when selectedValues is provided', async () => { const optionsWithValues: DropdownMenuOption[] = [ { label: 'English', value: 'en', callback: vi.fn() }, { label: 'Français', value: 'fr', callback: vi.fn() }, @@ -77,12 +77,12 @@ describe('', () => { { wrapper: AppWrapper }, ); - const radios = screen.getAllByRole('menuitemradio'); - expect(radios).toHaveLength(3); + const checkboxes = screen.getAllByRole('menuitemcheckbox'); + expect(checkboxes).toHaveLength(3); - expect(radios[0]).toHaveAttribute('aria-checked', 'false'); - expect(radios[1]).toHaveAttribute('aria-checked', 'true'); - expect(radios[2]).toHaveAttribute('aria-checked', 'false'); + expect(checkboxes[0]).toHaveAttribute('aria-checked', 'false'); + expect(checkboxes[1]).toHaveAttribute('aria-checked', 'true'); + expect(checkboxes[2]).toHaveAttribute('aria-checked', 'false'); }); test('trigger button has aria-haspopup and aria-expanded', async () => { diff --git a/src/frontend/apps/impress/src/features/language/components/LanguagePicker.tsx b/src/frontend/apps/impress/src/features/language/components/LanguagePicker.tsx index 5a7f19ee4..59655ca59 100644 --- a/src/frontend/apps/impress/src/features/language/components/LanguagePicker.tsx +++ b/src/frontend/apps/impress/src/features/language/components/LanguagePicker.tsx @@ -1,3 +1,4 @@ +import { announce } from '@react-aria/live-announcer'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; @@ -17,23 +18,35 @@ export const LanguagePicker = () => { const { changeLanguageSynchronized } = useSynchronizedLanguage(); const language = i18n.language; + const toLangTag = (locale: string) => locale.replace('_', '-'); + // Compute options for dropdown const optionsPicker = useMemo(() => { const backendOptions = conf?.LANGUAGES ?? [[language, language]]; return backendOptions.map(([backendLocale, backendLabel]) => { return { label: backendLabel, + lang: toLangTag(backendLocale), + value: backendLocale, isSelected: getMatchingLocales([backendLocale], [language]).length > 0, - callback: () => changeLanguageSynchronized(backendLocale, user), + callback: async () => { + await changeLanguageSynchronized(backendLocale, user); + announce( + t('Language changed to {{language}}', { + language: backendLabel, + defaultValue: `Language changed to ${backendLabel}`, + }), + 'polite', + ); + }, }; }); - }, [changeLanguageSynchronized, conf?.LANGUAGES, language, user]); + }, [changeLanguageSynchronized, conf?.LANGUAGES, language, t, user]); // Extract current language label for display - const currentLanguageLabel = - conf?.LANGUAGES.find( - ([code]) => getMatchingLocales([code], [language]).length > 0, - )?.[1] || language; + const [currentLanguageCode, currentLanguageLabel] = conf?.LANGUAGES.find( + ([code]) => getMatchingLocales([code], [language]).length > 0, + ) ?? [language, language]; return ( { $align="center" > - {currentLanguageLabel} + + {currentLanguageLabel} + );