Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
53ceb7a
⚡️(frontend) improve accessibility of selected document's sub-menu
Ovgodd Aug 1, 2025
575a57e
✨(frontend) add keyboard navigation for subdocs with focus activation
Ovgodd Aug 5, 2025
cb79ed0
✨(frontend) make components accessible to screen readers
Ovgodd Aug 5, 2025
45eb6c5
fixup! ✨(frontend) make components accessible to screen readers
Ovgodd Aug 8, 2025
d28a7e0
fixup! ✨(frontend) make components accessible to screen readers
Ovgodd Aug 8, 2025
33e94c5
fixup! ✨(frontend) make components accessible to screen readers
Ovgodd Aug 8, 2025
4dd24cf
fixup! ✨(frontend) add keyboard navigation for subdocs with focus act…
Ovgodd Aug 8, 2025
b3ee8ec
fixup! ⚡️(frontend) improve accessibility of selected document's sub-…
Ovgodd Aug 8, 2025
ba5f46f
fixup! ✨(frontend) make components accessible to screen readers
Ovgodd Aug 8, 2025
6f80282
fixup! ✨(frontend) make components accessible to screen readers
Ovgodd Aug 12, 2025
3d6823b
fixup! ✨(frontend) make components accessible to screen readers
Ovgodd Aug 12, 2025
528352b
fixup! ✨(frontend) add keyboard navigation for subdocs with focus act…
Ovgodd Aug 12, 2025
c45fe88
fixup! ✨(frontend) add keyboard navigation for subdocs with focus act…
Ovgodd Aug 12, 2025
81eff83
fixup! ⚡️(frontend) improve accessibility of selected document's sub-…
Ovgodd Aug 13, 2025
6eb9036
fixup! ✨(frontend) add keyboard navigation for subdocs with focus act…
Ovgodd Aug 13, 2025
46f6274
fixup! ✨(frontend) make components accessible to screen readers
Ovgodd Aug 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ and this project adheres to
- 🐛(makefile) Windows compatibility fix for Docker volume mounting #1264
- 🐛(minio) fix user permission error with Minio and Windows #1264

- #1261


## [3.5.0] - 2025-07-31

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ test.describe('Doc Editor', () => {
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();

await page.getByRole('button', { name: 'AI' }).click();
await page.locator('[data-test="ai-actions"]').click();

await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
Expand Down Expand Up @@ -393,11 +393,11 @@ test.describe('Doc Editor', () => {
/* eslint-disable playwright/no-conditional-expect */
/* eslint-disable playwright/no-conditional-in-test */
if (!ai_transform && !ai_translate) {
await expect(page.getByRole('button', { name: 'AI' })).toBeHidden();
await expect(page.locator('[data-test="ai-actions"]')).toBeHidden();
return;
}

await page.getByRole('button', { name: 'AI' }).click();
await page.locator('[data-test="ai-actions"]').click();

if (ai_transform) {
await expect(
Expand Down
10 changes: 5 additions & 5 deletions src/frontend/apps/e2e/__tests__/app-impress/doc-search.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,10 @@ test.describe('Document search', () => {

// Expect to find the first doc
await expect(
page.getByRole('presentation').getByLabel(firstDocTitle),
page.getByRole('presentation').getByText(firstDocTitle),
).toBeVisible();
await expect(
page.getByRole('presentation').getByLabel(secondDocTitle),
page.getByRole('presentation').getByText(secondDocTitle),
).toBeVisible();

await page.getByRole('button', { name: 'close' }).click();
Expand All @@ -196,13 +196,13 @@ test.describe('Document search', () => {

// Now there is a sub page - expect to have the focus on the current doc
await expect(
page.getByRole('presentation').getByLabel(secondDocTitle),
page.getByRole('presentation').getByText(secondDocTitle),
).toBeVisible();
await expect(
page.getByRole('presentation').getByLabel(secondChildDocTitle),
page.getByRole('presentation').getByText(secondChildDocTitle),
).toBeVisible();
await expect(
page.getByRole('presentation').getByLabel(firstDocTitle),
page.getByRole('presentation').getByText(firstDocTitle),
).toBeHidden();
});
});
78 changes: 78 additions & 0 deletions src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,81 @@ test.describe('Doc Tree: Inheritance', () => {
await expect(docTree.getByText(docParent)).toBeVisible();
});
});

test.describe('Doc tree keyboard interactions (subdocs)', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('navigates in the tree and actions with keyboard and toggles menu (options and create childDoc)', async ({
page,
browserName,
}) => {
const [rootDocTitle] = await createDoc(
page,
'doc-tree-keyboard',
browserName,
1,
);
await verifyDocName(page, rootDocTitle);

const { name: childTitle } = await createRootSubPage(
page,
browserName,
'subdoc-tree-actions',
);

await verifyDocName(page, childTitle);

const docTree = page.getByTestId('doc-tree');

const actionsGroup = page.getByRole('group', {
name: `Actions for ${childTitle}`,
});
await expect(actionsGroup).toBeVisible();

const moreOptions = actionsGroup.getByRole('button', {
name: `More options for ${childTitle}`,
});
await expect(moreOptions).toBeVisible();

await moreOptions.focus();
await expect(moreOptions).toBeFocused();

await page.keyboard.press('ArrowRight');
const addChild = actionsGroup.getByTestId('add-child-doc');
await expect(addChild).toBeFocused();

await page.keyboard.press('ArrowLeft');
await expect(moreOptions).toBeFocused();

await page.keyboard.press('Enter');
await expect(page.getByText('Copy link')).toBeVisible();

await page.keyboard.press('Escape');
await expect(page.getByText('Copy link')).toBeHidden();

await page.keyboard.press('ArrowRight');
await expect(addChild).toBeFocused();

const responsePromise = page.waitForResponse(
(response) =>
response.url().includes('/documents/') &&
response.url().includes('/children/') &&
response.request().method() === 'POST',
);

await page.keyboard.press('Enter');

const response = await responsePromise;
expect(response.ok()).toBeTruthy();
const newChildDoc = (await response.json()) as { id: string };

const childButton = page.getByTestId(`doc-sub-page-item-${newChildDoc.id}`);
const childTreeItem = docTree
.locator('.c__tree-view--row')
.filter({ has: childButton })
.first();

await childTreeItem.focus();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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();
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { HorizontalSeparator } from '@gouvfr-lasuite/ui-kit';
import {
Fragment,
PropsWithChildren,
ReactNode,
useCallback,
useEffect,
useRef,
Expand All @@ -11,11 +12,10 @@ import { css } from 'styled-components';

import { Box, BoxButton, BoxProps, DropButton, Icon, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';

import { useDropdownKeyboardNav } from './hook/useDropdownKeyboardNav';
import { useDropdownKeyboardNav } from '@/hook/useDropdownKeyboardNav';

export type DropdownMenuOption = {
icon?: string;
icon?: string | ReactNode;
label: string;
testId?: string;
value?: string;
Expand Down Expand Up @@ -79,14 +79,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]);

Expand Down Expand Up @@ -153,7 +167,6 @@ export const DropdownMenu = ({
return;
}
const isDisabled = option.disabled !== undefined && option.disabled;
const isFocused = index === focusedIndex;

return (
<Fragment key={option.label}>
Expand Down Expand Up @@ -204,32 +217,26 @@ 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
$direction="row"
$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>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { css } from 'styled-components';

import { Box } from '../Box';
import { DropdownMenu, DropdownMenuOption } from '../DropdownMenu';
import { Icon } from '../Icon';
import { Text } from '../Text';
import {
DropdownMenu,
DropdownMenuOption,
} from '../dropdown-menu/DropdownMenu';

export type FilterDropdownProps = {
options: DropdownMenuOption[];
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/apps/impress/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export * from './Box';
export * from './BoxButton';
export * from './Card';
export * from './DropButton';
export * from './dropdown-menu/DropdownMenu';
export * from './DropdownMenu';
export * from './quick-search';
export * from './Icon';
export * from './InfiniteScroll';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const SimpleDocItem = ({
$overflow="auto"
$width="100%"
className="--docs--simple-doc-item"
role="presentation"
>
<Box
$direction="row"
Expand All @@ -53,6 +54,7 @@ export const SimpleDocItem = ({
`}
$padding={`${spacingsTokens['3xs']} 0`}
data-testid={isPinned ? `doc-pinned-${doc.id}` : undefined}
aria-hidden="true"
>
{isPinned ? (
<PinnedDocumentIcon
Expand All @@ -70,12 +72,11 @@ export const SimpleDocItem = ({
</Box>
<Box $justify="center" $overflow="auto">
<Text
aria-describedby="doc-title"
aria-label={doc.title}
$size="sm"
$variation="1000"
$weight="500"
$css={ItemTextCss}
aria-describedby="doc-title"
>
{doc.title || untitledDocument}
</Text>
Expand All @@ -85,6 +86,7 @@ export const SimpleDocItem = ({
$align="center"
$gap={spacingsTokens['3xs']}
$margin={{ top: '-2px' }}
aria-hidden="true"
>
<Text $variation="600" $size="xs">
{DateTime.fromISO(doc.updated_at).toRelative()}
Expand Down
Loading
Loading