⚡️(frontend) improve accessibility of selected document's sub-menu
adds focus style to make the sub-menu accessible unify dropdownmenu Signed-off-by: Cyril <[email protected]>
This commit is contained in:
+2
-1
@@ -25,8 +25,9 @@ and this project adheres to
|
||||
- #1244
|
||||
- #1270
|
||||
- #1282
|
||||
- #1261
|
||||
- ♻️(backend) fallback to email identifier when no name #1298
|
||||
- 🐛(backend) allow ASCII characters in user sub field #1295
|
||||
- 🐛(backend) allow ASCII characters in user sub field #1295
|
||||
- ⚡️(frontend) improve fallback width calculation #1333
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -63,5 +63,6 @@ export const clickOnAddRootSubPage = async (page: Page) => {
|
||||
const rootItem = page.getByTestId('doc-tree-root-item');
|
||||
await expect(rootItem).toBeVisible();
|
||||
await rootItem.hover();
|
||||
await rootItem.getByRole('button', { name: 'add_box' }).click();
|
||||
|
||||
await rootItem.getByTestId('add-child-doc').click();
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { HorizontalSeparator } from '@gouvfr-lasuite/ui-kit';
|
||||
import {
|
||||
Fragment,
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
@@ -15,7 +16,7 @@ import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useDropdownKeyboardNav } from './hook/useDropdownKeyboardNav';
|
||||
|
||||
export type DropdownMenuOption = {
|
||||
icon?: string;
|
||||
icon?: string | ReactNode;
|
||||
label: string;
|
||||
testId?: string;
|
||||
value?: string;
|
||||
@@ -81,14 +82,28 @@ export const DropdownMenu = ({
|
||||
|
||||
// Focus selected menu item when menu opens
|
||||
useEffect(() => {
|
||||
if (isOpen && menuItemRefs.current.length > 0) {
|
||||
const selectedIndex = options.findIndex((option) => option.isSelected);
|
||||
if (selectedIndex !== -1) {
|
||||
setFocusedIndex(selectedIndex);
|
||||
setTimeout(() => {
|
||||
menuItemRefs.current[selectedIndex]?.focus();
|
||||
}, 0);
|
||||
}
|
||||
if (!isOpen || menuItemRefs.current.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIndex = options.findIndex((option) => option.isSelected);
|
||||
if (selectedIndex !== -1) {
|
||||
setFocusedIndex(selectedIndex);
|
||||
setTimeout(() => {
|
||||
menuItemRefs.current[selectedIndex]?.focus();
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: focus first enabled/visible option
|
||||
const firstEnabledIndex = options.findIndex(
|
||||
(opt) => opt.show !== false && !opt.disabled,
|
||||
);
|
||||
if (firstEnabledIndex !== -1) {
|
||||
setFocusedIndex(firstEnabledIndex);
|
||||
setTimeout(() => {
|
||||
menuItemRefs.current[firstEnabledIndex]?.focus();
|
||||
}, 0);
|
||||
}
|
||||
}, [isOpen, options]);
|
||||
|
||||
@@ -156,7 +171,6 @@ export const DropdownMenu = ({
|
||||
return;
|
||||
}
|
||||
const isDisabled = option.disabled !== undefined && option.disabled;
|
||||
const isFocused = index === focusedIndex;
|
||||
|
||||
return (
|
||||
<Fragment key={option.label}>
|
||||
@@ -207,17 +221,8 @@ export const DropdownMenu = ({
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--c--theme--colors--primary-500);
|
||||
outline-offset: -2px;
|
||||
background-color: var(--c--theme--colors--greyscale-050);
|
||||
}
|
||||
|
||||
${isFocused &&
|
||||
css`
|
||||
outline: 2px solid var(--c--theme--colors--primary-500);
|
||||
outline-offset: -2px;
|
||||
background-color: var(--c--theme--colors--greyscale-050);
|
||||
`}
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
@@ -225,14 +230,17 @@ export const DropdownMenu = ({
|
||||
$align="center"
|
||||
$gap={spacingsTokens['base']}
|
||||
>
|
||||
{option.icon && (
|
||||
<Icon
|
||||
$size="20px"
|
||||
$theme="greyscale"
|
||||
$variation={isDisabled ? '400' : '1000'}
|
||||
iconName={option.icon}
|
||||
/>
|
||||
)}
|
||||
{option.icon &&
|
||||
(typeof option.icon === 'string' ? (
|
||||
<Icon
|
||||
$size="20px"
|
||||
$theme="greyscale"
|
||||
$variation={isDisabled ? '400' : '1000'}
|
||||
iconName={option.icon}
|
||||
/>
|
||||
) : (
|
||||
option.icon
|
||||
))}
|
||||
<Text $variation={isDisabled ? '400' : '1000'}>
|
||||
{option.label}
|
||||
</Text>
|
||||
|
||||
@@ -95,13 +95,19 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
|
||||
: 'var(--c--theme--colors--greyscale-000)'};
|
||||
}
|
||||
|
||||
&:focus-within .light-doc-item-actions {
|
||||
display: flex;
|
||||
background: var(--c--theme--colors--greyscale-100);
|
||||
}
|
||||
|
||||
.c__tree-view--node.isSelected {
|
||||
.light-doc-item-actions {
|
||||
background: var(--c--theme--colors--greyscale-100);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
background-color: var(--c--theme--colors--greyscale-100);
|
||||
border-radius: 4px;
|
||||
|
||||
|
||||
@@ -191,7 +191,8 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
.doc-tree-root-item-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
+32
@@ -150,6 +150,25 @@ export const DocTreeItemActions = ({
|
||||
$align="center"
|
||||
className="--docs--doc-tree-item-actions"
|
||||
$gap="4px"
|
||||
$css={css`
|
||||
&:focus-within {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
button:focus-visible,
|
||||
[role='button']:focus-visible {
|
||||
outline: 2px solid var(--c--theme--colors--primary-500);
|
||||
outline-offset: 2px;
|
||||
background-color: var(--c--theme--colors--greyscale-050);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.icon-button:focus-visible {
|
||||
outline: 2px solid var(--c--theme--colors--primary-500);
|
||||
outline-offset: 2px;
|
||||
background-color: var(--c--theme--colors--greyscale-050);
|
||||
border-radius: 4px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<DropdownMenu
|
||||
options={options}
|
||||
@@ -166,10 +185,22 @@ export const DocTreeItemActions = ({
|
||||
variant="filled"
|
||||
$theme="primary"
|
||||
$variation="600"
|
||||
className="icon-button"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={t('More options')}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onOpenChange?.(!isOpen);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
{doc.abilities.children_create && (
|
||||
<BoxButton
|
||||
data-testid="add-child-doc"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@@ -179,6 +210,7 @@ export const DocTreeItemActions = ({
|
||||
});
|
||||
}}
|
||||
color="primary"
|
||||
aria-label={t('Add child document')}
|
||||
>
|
||||
<Icon
|
||||
variant="filled"
|
||||
|
||||
@@ -274,6 +274,8 @@
|
||||
"Move": "Verschieben",
|
||||
"Move document": "Dokument verschieben",
|
||||
"Move to my docs": "In \"Meine Dokumente\" verschieben",
|
||||
"More options": "Weitere Optionen",
|
||||
"Add child document": "Unterdokument hinzufügen",
|
||||
"My docs": "Meine Dokumente",
|
||||
"Name": "Name",
|
||||
"New doc": "Neues Dokument",
|
||||
@@ -387,7 +389,8 @@
|
||||
"Shared with {{count}} users_many": "Shared with {{count}} users",
|
||||
"Shared with {{count}} users_one": "Shared with {{count}} user",
|
||||
"Shared with {{count}} users_other": "Shared with {{count}} users",
|
||||
"Updated": "Updated"
|
||||
"Updated": "Updated",
|
||||
"Add child document": "Add child document"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
@@ -575,6 +578,8 @@
|
||||
"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.",
|
||||
"Your current document will revert to this version.": "Tu documento actual se revertirá a esta versión.",
|
||||
"Your {{format}} was downloaded succesfully": "Su {{format}} se ha descargado correctamente",
|
||||
"More options": "Más opciones",
|
||||
"Add child document": "Añadir documento hijo",
|
||||
"home-content-open-source-part1": "Docs está construido sobre <2>Django Rest Framework</2> y <6>Next.js</6>. También utilizamos <9>Yjs</9> y <13>BlockNote.js</13>, dos proyectos que estamos orgullosos de patrocinar.",
|
||||
"home-content-open-source-part2": "Puede autoalojar fácilmente Docs (consulte nuestra <2>documentación</2> de instalación).<br/>Docs utiliza una <7>licencia</7> (MIT) adecuada para la innovación y las empresas.<br/>Se aceptan contribuciones (consulte nuestra hoja de ruta <13>aquí</13>).",
|
||||
"home-content-open-source-part3": "Docs es el resultado de un esfuerzo conjunto llevado a cabo por los gobiernos francés 🇫🇷🥖 <1>(DINUM)</1> y alemán 🇩🇪🥨 <5>(ZenDiS)</5>."
|
||||
@@ -712,6 +717,8 @@
|
||||
"Move": "Déplacer",
|
||||
"Move document": "Déplacer le document",
|
||||
"Move to my docs": "Déplacer vers mes docs",
|
||||
"More options": "Plus d'options",
|
||||
"Add child document": "Ajouter un document enfant",
|
||||
"My docs": "Mes documents",
|
||||
"Name": "Nom",
|
||||
"New doc": "Nouveau doc",
|
||||
@@ -1151,6 +1158,8 @@
|
||||
"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.",
|
||||
"Your current document will revert to this version.": "Uw huidige document wordt teruggezet naar deze versie.",
|
||||
"Your {{format}} was downloaded succesfully": "Jouw {{format}} is succesvol gedownload",
|
||||
"More options": "Meer opties",
|
||||
"Add child document": "Onderliggend document toevoegen",
|
||||
"home-content-open-source-part1": "Docs is gebouwd op <2>Django Rest Framework</2> en <6>Next.js</6>. We gebruiken ook <9>Yjs</9> en <13>BlockNote.js</13>, twee projecten die we met trots sponsoren.",
|
||||
"home-content-open-source-part2": "U kunt Docs eenvoudig zelf hosten (zie onze <2>installatiedocumentatie</2>).<br/>Docs gebruikt een <7>licentie</7> (MIT) die is afgestemd op innovatie en ondernemingen.<br/>Bijdragen zijn welkom (zie onze routekaart <13>hier</13>).",
|
||||
"home-content-open-source-part3": "Docs is het resultaat van een gezamenlijke inspanning geleid door de Franse 🇫🇷🥖 <1>(DINUM)</1> en Duitse 🇩🇪🥨 <5>(ZenDiS)</5> overheden."
|
||||
|
||||
Reference in New Issue
Block a user