(frontend) improve accessibility of search modal for screen readers

added clearer sr-only translations and aria-hidden for non-essential content

Signed-off-by: Cyril <c.gromoff@gmail.com>
This commit is contained in:
Cyril
2025-08-07 11:54:32 +02:00
parent 0cf8b9da1a
commit 81f3997628
13 changed files with 70 additions and 29 deletions

View File

@@ -13,6 +13,7 @@ and this project adheres to
- ⚡️(frontend) improve accessibility: - ⚡️(frontend) improve accessibility:
- #1248 - #1248
- #1235 - #1235
- #1275
- #1255 - #1255
- #1262 - #1262
- #1244 - #1244

View File

@@ -93,7 +93,7 @@ test.describe('Doc Editor', () => {
await page.getByRole('button', { name: 'Share' }).click(); await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByLabel('Visibility', { exact: true }); const selectVisibility = page.getByTestId('doc-visibility');
// When the visibility is changed, the ws should close the connection (backend signal) // When the visibility is changed, the ws should close the connection (backend signal)
const wsClosePromise = webSocket.waitForEvent('close'); const wsClosePromise = webSocket.waitForEvent('close');
@@ -561,7 +561,7 @@ test.describe('Doc Editor', () => {
await page.getByRole('button', { name: 'Share' }).click(); await page.getByRole('button', { name: 'Share' }).click();
await page.getByLabel('Visibility', { exact: true }).click(); await page.getByTestId('doc-visibility').click();
await page await page
.getByRole('menuitem', { .getByRole('menuitem', {
@@ -573,7 +573,7 @@ test.describe('Doc Editor', () => {
page.getByText('The document visibility has been updated.'), page.getByText('The document visibility has been updated.'),
).toBeVisible(); ).toBeVisible();
await page.getByLabel('Visibility mode').click(); await page.getByTestId('doc-access-mode').click();
await page.getByRole('menuitem', { name: 'Editing' }).click(); await page.getByRole('menuitem', { name: 'Editing' }).click();
// Close the modal // Close the modal
@@ -655,7 +655,7 @@ test.describe('Doc Editor', () => {
await page.getByRole('button', { name: 'Share' }).click(); await page.getByRole('button', { name: 'Share' }).click();
await page.getByLabel('Visibility mode').click(); await page.getByTestId('doc-access-mode').click();
await page.getByRole('menuitem', { name: 'Reading' }).click(); await page.getByRole('menuitem', { name: 'Reading' }).click();
// Close the modal // Close the modal

View File

@@ -30,7 +30,7 @@ test.describe('Doc Header', () => {
await page.getByRole('button', { name: 'Share' }).click(); await page.getByRole('button', { name: 'Share' }).click();
await page.getByLabel('Visibility', { exact: true }).click(); await page.getByTestId('doc-visibility').click();
await page await page
.getByRole('menuitem', { .getByRole('menuitem', {

View File

@@ -259,6 +259,10 @@ test.describe('Doc Tree: Inheritance', () => {
test.use({ storageState: { cookies: [], origins: [] } }); test.use({ storageState: { cookies: [], origins: [] } });
test('A child inherit from the parent', async ({ page, browserName }) => { test('A child inherit from the parent', async ({ page, browserName }) => {
// test.slow() to extend timeout since this scenario chains Keycloak login + redirects,
// doc creation/navigation and async doc-tree loading (/documents/:id/tree), which can exceed 30s (especially in CI).
test.slow();
await page.goto('/'); await page.goto('/');
await keyCloakSignIn(page, browserName); await keyCloakSignIn(page, browserName);
@@ -271,7 +275,7 @@ test.describe('Doc Tree: Inheritance', () => {
await verifyDocName(page, docParent); await verifyDocName(page, docParent);
await page.getByRole('button', { name: 'Share' }).click(); await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByLabel('Visibility', { exact: true }); const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click(); await selectVisibility.click();
await page await page
@@ -307,6 +311,7 @@ test.describe('Doc Tree: Inheritance', () => {
await expect(page.locator('h2').getByText(docChild)).toBeVisible(); await expect(page.locator('h2').getByText(docChild)).toBeVisible();
const docTree = page.getByTestId('doc-tree'); const docTree = page.getByTestId('doc-tree');
await expect(docTree).toBeVisible({ timeout: 10000 });
await expect(docTree.getByText(docParent)).toBeVisible(); await expect(docTree.getByText(docParent)).toBeVisible();
}); });
}); });

View File

@@ -41,7 +41,7 @@ test.describe('Doc Visibility', () => {
await page.getByRole('button', { name: 'Share' }).click(); await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByLabel('Visibility', { exact: true }); const selectVisibility = page.getByTestId('doc-visibility');
await expect(selectVisibility.getByText('Private')).toBeVisible(); await expect(selectVisibility.getByText('Private')).toBeVisible();
@@ -51,13 +51,13 @@ test.describe('Doc Visibility', () => {
await selectVisibility.click(); await selectVisibility.click();
await page.getByLabel('Connected').click(); await page.getByLabel('Connected').click();
await expect(page.getByLabel('Visibility mode')).toBeVisible(); await expect(page.getByTestId('doc-access-mode')).toBeVisible();
await selectVisibility.click(); await selectVisibility.click();
await page.getByLabel('Public', { exact: true }).click(); await page.getByLabel('Public', { exact: true }).click();
await expect(page.getByLabel('Visibility mode')).toBeVisible(); await expect(page.getByTestId('doc-access-mode')).toBeVisible();
}); });
}); });
@@ -205,7 +205,7 @@ test.describe('Doc Visibility: Public', () => {
await verifyDocName(page, docTitle); await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click(); await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByLabel('Visibility', { exact: true }); const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click(); await selectVisibility.click();
await page await page
@@ -218,8 +218,8 @@ test.describe('Doc Visibility: Public', () => {
page.getByText('The document visibility has been updated.'), page.getByText('The document visibility has been updated.'),
).toBeVisible(); ).toBeVisible();
await expect(page.getByLabel('Visibility mode')).toBeVisible(); await expect(page.getByTestId('doc-access-mode')).toBeVisible();
await page.getByLabel('Visibility mode').click(); await page.getByTestId('doc-access-mode').click();
await page await page
.getByRole('menuitem', { .getByRole('menuitem', {
name: 'Reading', name: 'Reading',
@@ -289,7 +289,7 @@ test.describe('Doc Visibility: Public', () => {
await verifyDocName(page, docTitle); await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click(); await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByLabel('Visibility', { exact: true }); const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click(); await selectVisibility.click();
await page await page
@@ -302,7 +302,7 @@ test.describe('Doc Visibility: Public', () => {
page.getByText('The document visibility has been updated.'), page.getByText('The document visibility has been updated.'),
).toBeVisible(); ).toBeVisible();
await page.getByLabel('Visibility mode').click(); await page.getByTestId('doc-access-mode').click();
await page.getByLabel('Editing').click(); await page.getByLabel('Editing').click();
await expect( await expect(
@@ -358,7 +358,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await verifyDocName(page, docTitle); await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click(); await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByLabel('Visibility', { exact: true }); const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click(); await selectVisibility.click();
await page await page
.getByRole('menuitem', { .getByRole('menuitem', {
@@ -410,7 +410,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await verifyDocName(page, docTitle); await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click(); await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByLabel('Visibility', { exact: true }); const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click(); await selectVisibility.click();
await page await page
.getByRole('menuitem', { .getByRole('menuitem', {
@@ -495,6 +495,7 @@ test.describe('Doc Visibility: Authenticated', () => {
page, page,
browserName, browserName,
}) => { }) => {
test.slow();
await page.goto('/'); await page.goto('/');
await keyCloakSignIn(page, browserName); await keyCloakSignIn(page, browserName);
@@ -508,7 +509,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await verifyDocName(page, docTitle); await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click(); await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByLabel('Visibility', { exact: true }); const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click(); await selectVisibility.click();
await page await page
.getByRole('menuitem', { .getByRole('menuitem', {
@@ -521,7 +522,7 @@ test.describe('Doc Visibility: Authenticated', () => {
).toBeVisible(); ).toBeVisible();
const urlDoc = page.url(); const urlDoc = page.url();
await page.getByLabel('Visibility mode').click(); await page.getByTestId('doc-access-mode').click();
await page.getByLabel('Editing').click(); await page.getByLabel('Editing').click();
await expect( await expect(
@@ -539,13 +540,17 @@ test.describe('Doc Visibility: Authenticated', () => {
const otherBrowser = BROWSERS.find((b) => b !== browserName); const otherBrowser = BROWSERS.find((b) => b !== browserName);
await keyCloakSignIn(page, otherBrowser!); await keyCloakSignIn(page, otherBrowser!);
await expect(page.getByTestId('header-logo-link')).toBeVisible(); await expect(page.getByTestId('header-logo-link')).toBeVisible({
timeout: 10000,
});
await page.goto(urlDoc); await page.goto(urlDoc);
await verifyDocName(page, docTitle); await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click(); await page.getByRole('button', { name: 'Share' }).click();
await page.getByRole('button', { name: 'Copy link' }).click(); await page.getByRole('button', { name: 'Copy link' }).click();
await expect(page.getByText('Link Copied !')).toBeVisible(); await expect(page.getByText('Link Copied !')).toBeVisible({
timeout: 10000,
});
}); });
}); });

View File

@@ -45,7 +45,7 @@ export const updateShareLink = async (
linkReach: LinkReach, linkReach: LinkReach,
linkRole?: LinkRole | null, linkRole?: LinkRole | null,
) => { ) => {
await page.getByRole('button', { name: 'Visibility', exact: true }).click(); await page.getByTestId('doc-visibility').click();
await page.getByRole('menuitem', { name: linkReach }).click(); await page.getByRole('menuitem', { name: linkReach }).click();
const visibilityUpdatedText = page const visibilityUpdatedText = page
@@ -55,9 +55,7 @@ export const updateShareLink = async (
await expect(visibilityUpdatedText).toBeVisible(); await expect(visibilityUpdatedText).toBeVisible();
if (linkRole) { if (linkRole) {
await page await page.getByTestId('doc-access-mode').click();
.getByRole('button', { name: 'Visibility mode', exact: true })
.click();
await page.getByRole('menuitem', { name: linkRole }).click(); await page.getByRole('menuitem', { name: linkRole }).click();
await expect(visibilityUpdatedText).toBeVisible(); await expect(visibilityUpdatedText).toBeVisible();
} }

View File

@@ -48,6 +48,7 @@ export interface DropButtonProps {
isOpen?: boolean; isOpen?: boolean;
onOpenChange?: (isOpen: boolean) => void; onOpenChange?: (isOpen: boolean) => void;
label?: string; label?: string;
testId?: string;
} }
export const DropButton = ({ export const DropButton = ({
@@ -57,6 +58,7 @@ export const DropButton = ({
onOpenChange, onOpenChange,
children, children,
label, label,
testId,
}: PropsWithChildren<DropButtonProps>) => { }: PropsWithChildren<DropButtonProps>) => {
const { themeTokens } = useCunninghamTheme(); const { themeTokens } = useCunninghamTheme();
const font = themeTokens['font']?.['families']['base']; const font = themeTokens['font']?.['families']['base'];
@@ -79,6 +81,7 @@ export const DropButton = ({
ref={triggerRef} ref={triggerRef}
onPress={() => onOpenChangeHandler(true)} onPress={() => onOpenChangeHandler(true)}
aria-label={label} aria-label={label}
data-testid={testId}
$css={css` $css={css`
font-family: ${font}; font-family: ${font};
${buttonCss}; ${buttonCss};

View File

@@ -38,6 +38,7 @@ export type DropdownMenuProps = {
topMessage?: string; topMessage?: string;
selectedValues?: string[]; selectedValues?: string[];
afterOpenChange?: (isOpen: boolean) => void; afterOpenChange?: (isOpen: boolean) => void;
testId?: string;
}; };
export const DropdownMenu = ({ export const DropdownMenu = ({
@@ -52,6 +53,7 @@ export const DropdownMenu = ({
topMessage, topMessage,
afterOpenChange, afterOpenChange,
selectedValues, selectedValues,
testId,
}: PropsWithChildren<DropdownMenuProps>) => { }: PropsWithChildren<DropdownMenuProps>) => {
const { spacingsTokens, colorsTokens } = useCunninghamTheme(); const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const [isOpen, setIsOpen] = useState(opened ?? false); const [isOpen, setIsOpen] = useState(opened ?? false);
@@ -100,6 +102,7 @@ export const DropdownMenu = ({
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
label={label} label={label}
buttonCss={buttonCss} buttonCss={buttonCss}
testId={testId}
button={ button={
showArrow ? ( showArrow ? (
<Box <Box

View File

@@ -46,7 +46,9 @@ export const QuickSearchInput = ({
$gap={spacingsTokens['2xs']} $gap={spacingsTokens['2xs']}
$padding={{ horizontal: 'base', vertical: 'sm' }} $padding={{ horizontal: 'base', vertical: 'sm' }}
> >
{!loading && <Icon iconName="search" $variation="600" />} {!loading && (
<Icon iconName="search" $variation="600" aria-hidden="true" />
)}
{loading && ( {loading && (
<div> <div>
<Loader size="small" /> <Loader size="small" />

View File

@@ -119,7 +119,11 @@ export const DocVersionEditor = ({
causes={error.cause} causes={error.cause}
icon={ icon={
error.status === 502 ? ( error.status === 502 ? (
<Text className="material-icons" $theme="danger"> <Text
className="material-icons"
$theme="danger"
aria-hidden={true}
>
wifi_off wifi_off
</Text> </Text>
) : undefined ) : undefined

View File

@@ -39,7 +39,11 @@ export const DocShareModalFooter = ({
fullWidth={false} fullWidth={false}
onClick={copyDocLink} onClick={copyDocLink}
color="tertiary" color="tertiary"
icon={<span className="material-icons">add_link</span>} icon={
<span className="material-icons" aria-hidden={true}>
add_link
</span>
}
> >
{t('Copy link')} {t('Copy link')}
</Button> </Button>

View File

@@ -129,7 +129,8 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
$gap={canManage ? spacingsTokens['3xs'] : spacingsTokens['base']} $gap={canManage ? spacingsTokens['3xs'] : spacingsTokens['base']}
> >
<DropdownMenu <DropdownMenu
label={t('Visibility')} testId="doc-visibility"
label={t('Document visibility')}
arrowCss={css` arrowCss={css`
color: ${colorsTokens['primary-800']} !important; color: ${colorsTokens['primary-800']} !important;
`} `}
@@ -170,6 +171,7 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
<Box $direction="row" $align="center" $gap={spacingsTokens['3xs']}> <Box $direction="row" $align="center" $gap={spacingsTokens['3xs']}>
{docLinkReach !== LinkReach.RESTRICTED && ( {docLinkReach !== LinkReach.RESTRICTED && (
<DropdownMenu <DropdownMenu
testId="doc-access-mode"
disabled={!canManage} disabled={!canManage}
showArrow={true} showArrow={true}
options={linkRoleOptions} options={linkRoleOptions}
@@ -180,7 +182,7 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
) )
: undefined : undefined
} }
label={t('Visibility mode')} label={t('Document access mode')}
> >
<Text $weight="initial" $variation="600"> <Text $weight="initial" $variation="600">
{linkModeTranslations[docLinkRole]} {linkModeTranslations[docLinkRole]}

View File

@@ -152,6 +152,8 @@
"Version restored successfully": "Stumm assavet gant berzh", "Version restored successfully": "Stumm assavet gant berzh",
"Visibility": "Gwelusted", "Visibility": "Gwelusted",
"Visibility mode": "Mod gwelusted", "Visibility mode": "Mod gwelusted",
"Document visibility": "Gwelusted an teul",
"Document access mode": "Mod aotreet an teul",
"Warning": "Diwallit", "Warning": "Diwallit",
"Why you can't edit the document?": "Perak ne c'hellit ket aozañ ar restr?", "Why you can't edit the document?": "Perak ne c'hellit ket aozañ ar restr?",
"Write": "Skrivañ" "Write": "Skrivañ"
@@ -354,6 +356,8 @@
"Version restored successfully": "Version erfolgreich wiederhergestellt", "Version restored successfully": "Version erfolgreich wiederhergestellt",
"Visibility": "Sichtbarkeit", "Visibility": "Sichtbarkeit",
"Visibility mode": "Sichtbarkeitseinstellungen", "Visibility mode": "Sichtbarkeitseinstellungen",
"Document visibility": "Dokumentensichtbarkeit",
"Document access mode": "Dokumentenzugriffsmodus",
"Warning": "Warnung", "Warning": "Warnung",
"Why you can't edit the document?": "Warum können Sie dieses Dokument nicht bearbeiten?", "Why you can't edit the document?": "Warum können Sie dieses Dokument nicht bearbeiten?",
"Write": "Schreiben", "Write": "Schreiben",
@@ -561,6 +565,8 @@
"Version restored successfully": "Versión restaurada con éxito", "Version restored successfully": "Versión restaurada con éxito",
"Visibility": "Visibilidad", "Visibility": "Visibilidad",
"Visibility mode": "Modo de visibilidad", "Visibility mode": "Modo de visibilidad",
"Document visibility": "Visibilidad del documento",
"Document access mode": "Modo de acceso al documento",
"Warning": "Aviso", "Warning": "Aviso",
"Write": "Escribe", "Write": "Escribe",
"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.": "Eres el único propietario de este grupo, haz que otro miembro sea el propietario del grupo para poder cambiar tu propio rol o ser eliminado del documento.", "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.": "Eres el único propietario de este grupo, haz que otro miembro sea el propietario del grupo para poder cambiar tu propio rol o ser eliminado del documento.",
@@ -792,6 +798,8 @@
"Version restored successfully": "Version restaurée avec succès", "Version restored successfully": "Version restaurée avec succès",
"Visibility": "Visibilité", "Visibility": "Visibilité",
"Visibility mode": "Mode de visibilité", "Visibility mode": "Mode de visibilité",
"Document visibility": "Visibilité du document",
"Document access mode": "Mode d'accès au document",
"Warning": "Attention", "Warning": "Attention",
"Why you can't edit the document?": "Pourquoi vous ne pouvez pas modifier le document ?", "Why you can't edit the document?": "Pourquoi vous ne pouvez pas modifier le document ?",
"Write": "Écrire", "Write": "Écrire",
@@ -951,6 +959,8 @@
"Version restored successfully": "Revisione ripristinata correttamente", "Version restored successfully": "Revisione ripristinata correttamente",
"Visibility": "Visibilità", "Visibility": "Visibilità",
"Visibility mode": "Modalità visibilità", "Visibility mode": "Modalità visibilità",
"Document visibility": "Visibilità del documento",
"Document access mode": "Modalità di accesso al documento",
"Warning": "Attenzione", "Warning": "Attenzione",
"Write": "Scrivi", "Write": "Scrivi",
"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.": "Sei l'unico proprietario di questo gruppo, devi nominare proprietario un altro membro del gruppo prima di poter cambiare il proprio ruolo o di essere rimosso dal documento.", "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.": "Sei l'unico proprietario di questo gruppo, devi nominare proprietario un altro membro del gruppo prima di poter cambiare il proprio ruolo o di essere rimosso dal documento.",
@@ -1128,6 +1138,8 @@
"Version restored successfully": "Versie teruggezet", "Version restored successfully": "Versie teruggezet",
"Visibility": "Zichtbaarheid", "Visibility": "Zichtbaarheid",
"Visibility mode": "Zichtbaarheid modus", "Visibility mode": "Zichtbaarheid modus",
"Document visibility": "Document zichtbaarheid",
"Document access mode": "Document toegangsmodus",
"Warning": "Waarschuwing", "Warning": "Waarschuwing",
"Write": "Schrijf", "Write": "Schrijf",
"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.": "U bent de enige eigenaar van deze groep, maak een ander lid de groepseigenaar voordat u uw eigen rol kunt wijzigen of kan worden verwijderd van het document.", "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.": "U bent de enige eigenaar van deze groep, maak een ander lid de groepseigenaar voordat u uw eigen rol kunt wijzigen of kan worden verwijderd van het document.",
@@ -1415,6 +1427,8 @@
"Version restored successfully": "已成功还原版本", "Version restored successfully": "已成功还原版本",
"Visibility": "可见性", "Visibility": "可见性",
"Visibility mode": "可见模式", "Visibility mode": "可见模式",
"Document visibility": "文档可见性",
"Document access mode": "文档访问模式",
"Warning": "警告", "Warning": "警告",
"Write": "写入", "Write": "写入",
"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.": "您是当前群组唯一所有者,需先指定另一管理员,才能更改自身角色或退出文档。", "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.": "您是当前群组唯一所有者,需先指定另一管理员,才能更改自身角色或退出文档。",