(frontend) improve screen reader support in DocShare modal

adds relevant aria-labels to enhance accessibility for assistive technologies

Signed-off-by: Cyril <c.gromoff@gmail.com>
This commit is contained in:
Cyril
2025-11-18 15:35:07 +01:00
parent f7baf238e3
commit 5f9968d81e
13 changed files with 69 additions and 23 deletions

View File

@@ -15,6 +15,7 @@ and this project adheres to
- ♿(frontend) improve accessibility:
- ♿(frontend) improve share modal button accessibility #1626
- ♿(frontend) improve screen reader support in DocShare modal #1628
- 🐛(frontend) fix toolbar not activated when reader #1640
- 🐛(frontend) preserve left panel width on window resize #1588
@@ -52,7 +53,6 @@ and this project adheres to
- 🔥(backend) remove api managing templates
## [3.9.0] - 2025-11-10
### Added

View File

@@ -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(),

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -69,7 +69,7 @@ export const QuickSearch = ({
label={label}
shouldFilter={false}
ref={ref}
tabIndex={0}
tabIndex={-1}
value={selectedValue}
onValueChange={handleValueChange}
>

View File

@@ -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 (
<DropdownMenu
topMessage={topMessage}
label="doc-role-dropdown"
label={t('{{action}}, current role: {{role}}', {
action: ariaLabel,
role: transRole(currentRole),
})}
showArrow={true}
arrowCss={css`
color: var(--c--theme--colors--primary-800) !important;
`}
testId="doc-role-dropdown"
options={[
...roles,
{

View File

@@ -78,6 +78,9 @@ const DocShareAccessRequestItem = ({ doc, accessRequest }: Props) => {
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,
})}
/>
<Button
color="tertiary"

View File

@@ -153,6 +153,7 @@ export const DocShareAddMemberList = ({
onClick={() => void onInvite()}
disabled={isLoading}
aria-label={inviteLabel}
data-testid="doc-share-invite-button"
>
{t('Invite')}
</Button>

View File

@@ -112,6 +112,9 @@ export const DocShareInvitationItem = ({
canUpdate={canUpdate}
doc={doc}
access={invitation}
ariaLabel={t('Change role for {{email}}', {
email: invitation.email,
})}
/>
{canUpdate && (

View File

@@ -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,
})}
/>
</Box>
}

View File

@@ -76,6 +76,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
const [selectedUsers, setSelectedUsers] = useState<User[]>([]);
const [userQuery, setUserQuery] = useState('');
const [inputValue, setInputValue] = useState('');
const [liveAnnouncement, setLiveAnnouncement] = useState('');
const [listHeight, setListHeight] = useState<string>('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,22 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
<ButtonCloseModal
aria-label={t('Close the share modal')}
onClick={onClose}
tabIndex={-1}
/>
</Box>
}
hideCloseButton
>
<ShareModalStyle />
{/* Screen reader announcements */}
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{liveAnnouncement}
</div>
<Box
$height="auto"
$maxHeight={canViewAccesses ? modalContentHeight : 'none'}