Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d84a9cb
⚡️(frontend) improve accessibility of selected document's sub-menu
Ovgodd Aug 1, 2025
a5e22fd
✨(frontend) add keyboard navigation for subdocs with focus activation
Ovgodd Aug 5, 2025
5b0e2e3
✨(frontend) make components accessible to screen readers
Ovgodd Aug 5, 2025
74b7afa
fixup! ✨(frontend) make components accessible to screen readers
Ovgodd Aug 8, 2025
cf83510
fixup! ✨(frontend) make components accessible to screen readers
Ovgodd Aug 8, 2025
922d05d
fixup! ✨(frontend) make components accessible to screen readers
Ovgodd Aug 8, 2025
2f4cd67
fixup! ✨(frontend) add keyboard navigation for subdocs with focus act…
Ovgodd Aug 8, 2025
4456e8b
fixup! ⚡️(frontend) improve accessibility of selected document's sub-…
Ovgodd Aug 8, 2025
ef9411d
fixup! ✨(frontend) make components accessible to screen readers
Ovgodd Aug 8, 2025
48ebb5a
fixup! ✨(frontend) make components accessible to screen readers
Ovgodd Aug 12, 2025
057539e
fixup! ✨(frontend) make components accessible to screen readers
Ovgodd Aug 12, 2025
66869c2
fixup! ✨(frontend) add keyboard navigation for subdocs with focus act…
Ovgodd Aug 12, 2025
9cdd0bd
fixup! ✨(frontend) add keyboard navigation for subdocs with focus act…
Ovgodd Aug 12, 2025
e6e024a
fixup! ⚡️(frontend) improve accessibility of selected document's sub-…
Ovgodd Aug 13, 2025
a88fa5a
fixup! ✨(frontend) add keyboard navigation for subdocs with focus act…
Ovgodd Aug 13, 2025
360b83b
fixup! ✨(frontend) make components accessible to screen readers
Ovgodd Aug 13, 2025
ae9f348
fixup! ✨(frontend) make components accessible to screen readers
Ovgodd Sep 4, 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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,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 @@ -400,11 +400,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 @@ -315,3 +315,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('toolbar', {
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 @@ -81,14 +81,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 @@ -156,7 +170,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 @@ -207,32 +220,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
@@ -1,4 +1,5 @@
import { DateTime } from 'luxon';
import { useRouter } from 'next/navigation';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';

Expand Down Expand Up @@ -26,17 +27,28 @@ type SimpleDocItemProps = {
doc: Doc;
isPinned?: boolean;
showAccesses?: boolean;
onActivate?: () => void;
};

export const SimpleDocItem = ({
doc,
isPinned = false,
showAccesses = false,
onActivate,
}: SimpleDocItemProps) => {
const { t } = useTranslation();
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const { isDesktop } = useResponsiveStore();
const { untitledDocument } = useTrans();
const router = useRouter();

const handleActivate = () => {
if (onActivate) {
onActivate();
} else {
router.push(`/docs/${doc.id}`);
}
};

const { emoji, titleWithoutEmoji: displayTitle } = getEmojiAndTitle(
doc.title || untitledDocument,
Expand All @@ -49,6 +61,9 @@ export const SimpleDocItem = ({
$overflow="auto"
$width="100%"
className="--docs--simple-doc-item"
role="presentation"
onClick={handleActivate}
aria-label={`${t('Open document')} ${doc.title || untitledDocument}`}
>
<Box
$direction="row"
Expand All @@ -59,6 +74,7 @@ export const SimpleDocItem = ({
`}
$padding={`${spacingsTokens['3xs']} 0`}
data-testid={isPinned ? `doc-pinned-${doc.id}` : undefined}
aria-hidden="true"
>
{isPinned ? (
<PinnedDocumentIcon
Expand Down Expand Up @@ -97,6 +113,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