From 1a5281bb87c04e249ba1343f2dc5a4cc03e284fd Mon Sep 17 00:00:00 2001 From: Cyril Date: Tue, 18 Nov 2025 15:35:07 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20improve=20screen=20reader?= =?UTF-8?q?=20support=20in=20DocShare=20modal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit adds relevant aria-labels to enhance accessibility for assistive technologies Signed-off-by: Cyril --- CHANGELOG.md | 1 + .../__tests__/app-impress/doc-header.spec.ts | 4 +- .../app-impress/doc-member-create.spec.ts | 12 +++--- .../app-impress/doc-member-list.spec.ts | 12 ++---- .../__tests__/app-impress/doc-tree.spec.ts | 2 +- .../e2e/__tests__/app-impress/utils-share.ts | 6 +-- .../components/quick-search/QuickSearch.tsx | 2 +- .../doc-share/components/DocRoleDropdown.tsx | 8 +++- .../components/DocShareAccessRequest.tsx | 3 ++ .../components/DocShareAddMemberList.tsx | 6 ++- .../components/DocShareInvitation.tsx | 3 ++ .../doc-share/components/DocShareMember.tsx | 3 ++ .../doc-share/components/DocShareModal.tsx | 41 +++++++++++++++++++ 13 files changed, 80 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ab285789d..e2321cab91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to - ♿(frontend) improve ARIA in doc grid and editor for a11y #1519 - ♿(frontend) improve accessibility and styling of summary table #1528 - ♿(frontend) add focus trap and enter key support to remove doc modal #1531 + - ♿(frontend) improve screen reader support in DocShare modal #1628 - 🐛(docx) fix image overflow by limiting width to 600px during export #1525 - 🐛(frontend) preserve @ character when esc is pressed after typing it #1512 - 🐛(frontend) make summary button fixed to remain visible during scroll #1581 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 99635176fd..d6a3c03632 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 @@ -208,7 +208,7 @@ test.describe('Doc Header', () => { await expect( invitationCard.getByText('test.test@invitation.test').first(), ).toBeVisible(); - const invitationRole = invitationCard.getByLabel('doc-role-dropdown'); + const invitationRole = invitationCard.getByTestId('doc-role-dropdown'); await expect(invitationRole).toBeVisible(); await invitationRole.click(); @@ -217,7 +217,7 @@ test.describe('Doc Header', () => { await expect(invitationCard).toBeHidden(); const memberCard = shareModal.getByLabel('List members card'); - const roles = memberCard.getByLabel('doc-role-dropdown'); + const roles = memberCard.getByTestId('doc-role-dropdown'); await expect(memberCard).toBeVisible(); await expect( memberCard.getByText('test.test@accesses.test').first(), 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 11fdb734f7..773aeb6b20 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 @@ -74,7 +74,7 @@ test.describe('Document create member', () => { await expect(list.getByText(email)).toBeVisible(); // Check roles are displayed - await list.getByLabel('doc-role-dropdown').click(); + await list.getByTestId('doc-role-dropdown').click(); await expect(page.getByRole('menuitem', { name: 'Reader' })).toBeVisible(); await expect(page.getByRole('menuitem', { name: 'Editor' })).toBeVisible(); await expect(page.getByRole('menuitem', { name: 'Owner' })).toBeVisible(); @@ -128,7 +128,7 @@ test.describe('Document create member', () => { // Choose a role const container = page.getByTestId('doc-share-add-member-list'); - await container.getByLabel('doc-role-dropdown').click(); + await container.getByTestId('doc-role-dropdown').click(); await page.getByRole('menuitem', { name: 'Owner' }).click(); const responsePromiseCreateInvitation = page.waitForResponse( @@ -146,7 +146,7 @@ test.describe('Document create member', () => { await page.getByTestId(`search-user-row-${email}`).click(); // Choose a role - await container.getByLabel('doc-role-dropdown').click(); + await container.getByTestId('doc-role-dropdown').click(); await page.getByRole('menuitem', { name: 'Owner' }).click(); const responsePromiseCreateInvitationFail = page.waitForResponse( @@ -183,7 +183,7 @@ test.describe('Document create member', () => { // Choose a role const container = page.getByTestId('doc-share-add-member-list'); - await container.getByLabel('doc-role-dropdown').click(); + await container.getByTestId('doc-role-dropdown').click(); await page.getByRole('menuitem', { name: 'Administrator' }).click(); const responsePromiseCreateInvitation = page.waitForResponse( @@ -210,7 +210,7 @@ test.describe('Document create member', () => { response.request().method() === 'PATCH', ); - await userInvitation.getByLabel('doc-role-dropdown').click(); + await userInvitation.getByTestId('doc-role-dropdown').click(); await page.getByRole('menuitem', { name: 'Reader' }).click(); const responsePatchInvitation = await responsePromisePatchInvitation; @@ -272,7 +272,7 @@ test.describe('Document create member', () => { const container = page.getByTestId( `doc-share-access-request-row-${emailRequest}`, ); - await container.getByLabel('doc-role-dropdown').click(); + await container.getByTestId('doc-role-dropdown').click(); await page.getByRole('menuitem', { name: 'Administrator' }).click(); await container.getByRole('button', { name: 'Approve' }).click(); 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 22d56e58bb..e87fc7dfde 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 @@ -152,7 +152,7 @@ test.describe('Document list members', () => { const currentUser = list.getByTestId( `doc-share-member-row-user.test@${browserName}.test`, ); - const currentUserRole = currentUser.getByLabel('doc-role-dropdown'); + const currentUserRole = currentUser.getByTestId('doc-role-dropdown'); await expect(currentUser).toBeVisible(); await expect(currentUserRole).toBeVisible(); await currentUserRole.click(); @@ -169,7 +169,7 @@ test.describe('Document list members', () => { }); const newUserEmail = await addNewMember(page, 0, 'Owner'); const newUser = list.getByTestId(`doc-share-member-row-${newUserEmail}`); - const newUserRoles = newUser.getByLabel('doc-role-dropdown'); + const newUserRoles = newUser.getByTestId('doc-role-dropdown'); await expect(newUser).toBeVisible(); @@ -214,9 +214,7 @@ test.describe('Document list members', () => { const emailMyself = `user.test@${browserName}.test`; const mySelf = list.getByTestId(`doc-share-member-row-${emailMyself}`); - const mySelfRole = mySelf.getByRole('button', { - name: 'doc-role-dropdown', - }); + const mySelfRole = mySelf.getByTestId('doc-role-dropdown'); const userOwnerEmail = await addNewMember(page, 0, 'Owner'); const userOwner = list.getByTestId( @@ -231,9 +229,7 @@ test.describe('Document list members', () => { const userReader = list.getByTestId( `doc-share-member-row-${userReaderEmail}`, ); - const userReaderRole = userReader.getByRole('button', { - name: 'doc-role-dropdown', - }); + const userReaderRole = userReader.getByTestId('doc-role-dropdown'); await expect(mySelf).toBeVisible(); await expect(userOwner).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 0e3bf49cc5..d43a174923 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 @@ -226,7 +226,7 @@ test.describe('Doc Tree', () => { const currentUser = list.getByTestId( `doc-share-member-row-user.test@${browserName}.test`, ); - const currentUserRole = currentUser.getByLabel('doc-role-dropdown'); + const currentUserRole = currentUser.getByTestId('doc-role-dropdown'); await currentUserRole.click(); await page.getByRole('menuitem', { name: 'Administrator' }).click(); await list.click(); 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 23dfc0aaf6..8ba96e4b80 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-share.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-share.ts @@ -38,9 +38,9 @@ export const addNewMember = async ( await page.getByRole('option', { name: users[index].email }).click(); // Choose a role - await page.getByLabel('doc-role-dropdown').click(); + await page.getByTestId('doc-role-dropdown').click(); await page.getByRole('menuitem', { name: role }).click(); - await page.getByRole('button', { name: 'Invite' }).click(); + await page.getByTestId('doc-share-invite-button').click(); return users[index].email; }; @@ -74,7 +74,7 @@ export const updateRoleUser = async ( const list = page.getByTestId('doc-share-quick-search'); const currentUser = list.getByTestId(`doc-share-member-row-${email}`); - const currentUserRole = currentUser.getByLabel('doc-role-dropdown'); + const currentUserRole = currentUser.getByTestId('doc-role-dropdown'); await currentUserRole.click(); await page.getByRole('menuitem', { name: role }).click(); await list.click(); diff --git a/src/frontend/apps/impress/src/components/quick-search/QuickSearch.tsx b/src/frontend/apps/impress/src/components/quick-search/QuickSearch.tsx index 89a5417290..16cbb826a1 100644 --- a/src/frontend/apps/impress/src/components/quick-search/QuickSearch.tsx +++ b/src/frontend/apps/impress/src/components/quick-search/QuickSearch.tsx @@ -69,7 +69,7 @@ export const QuickSearch = ({ label={label} shouldFilter={false} ref={ref} - tabIndex={0} + tabIndex={-1} value={selectedValue} onValueChange={handleValueChange} > diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocRoleDropdown.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocRoleDropdown.tsx index 1796e5192d..0b28b589fc 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocRoleDropdown.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocRoleDropdown.tsx @@ -18,6 +18,7 @@ type DocRoleDropdownProps = { onSelectRole: (role: Role) => void; rolesAllowed?: Role[]; isLastOwner?: boolean; + ariaLabel?: string; }; export const DocRoleDropdown = ({ @@ -29,6 +30,7 @@ export const DocRoleDropdown = ({ rolesAllowed, access, isLastOwner = false, + ariaLabel, }: DocRoleDropdownProps) => { const { t } = useTranslation(); const { transRole, translatedRoles } = useTrans(); @@ -113,11 +115,15 @@ export const DocRoleDropdown = ({ return ( { onSelectRole={setRole} canUpdate={doc.abilities.accesses_manage} rolesAllowed={accessRequest.abilities.set_role_to} + ariaLabel={t('Change role for {{name}}', { + name: accessRequest.user.full_name || accessRequest.user.email, + })} /> diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitation.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitation.tsx index 31c367700d..42afae4800 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitation.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitation.tsx @@ -112,6 +112,9 @@ export const DocShareInvitationItem = ({ canUpdate={canUpdate} doc={doc} access={invitation} + ariaLabel={t('Change role for {{email}}', { + email: invitation.email, + })} /> {canUpdate && ( diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMember.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMember.tsx index 7e6758954d..d4dc8d02d1 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMember.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMember.tsx @@ -77,6 +77,9 @@ export const DocShareMemberItem = ({ rolesAllowed={access.abilities.set_role_to} access={access} doc={doc} + ariaLabel={t('Change role for {{name}}', { + name: access.user.full_name || access.user.email, + })} /> } diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx index 6ba81f05f7..4e586c45e5 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx @@ -76,6 +76,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => { const [selectedUsers, setSelectedUsers] = useState([]); const [userQuery, setUserQuery] = useState(''); const [inputValue, setInputValue] = useState(''); + const [liveAnnouncement, setLiveAnnouncement] = useState(''); const [listHeight, setListHeight] = useState('400px'); const canShare = doc.abilities.accesses_manage && isRootDoc; @@ -88,6 +89,19 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => { setSelectedUsers((prev) => [...prev, user]); setUserQuery(''); setInputValue(''); + + // Announce to screen readers + const userName = user.full_name || user.email; + setLiveAnnouncement( + t( + '{{name}} added to invite list. Add more members or press Tab to select role and invite.', + { + name: userName, + }, + ), + ); + // Clear announcement after it's been read + setTimeout(() => setLiveAnnouncement(''), 100); }; const { data: membersQuery } = useDocAccesses({ @@ -114,6 +128,16 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => { } const newArray = [...prevState]; newArray.splice(index, 1); + + // Announce to screen readers + const userName = row.full_name || row.email; + setLiveAnnouncement( + t('{{name}} removed from invite list', { + name: userName, + }), + ); + setTimeout(() => setLiveAnnouncement(''), 100); + return newArray; }); }; @@ -175,12 +199,29 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => { } hideCloseButton > + {/* Screen reader announcements */} +
+ {liveAnnouncement} +