diff --git a/CHANGELOG.md b/CHANGELOG.md index a21793b07c..8e2fa0f0ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts index 41098f2526..7a65a02cde 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts @@ -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' }), @@ -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( diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-search.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-search.spec.ts index 412064dd31..9fe07578b7 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-search.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-search.spec.ts @@ -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(); @@ -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(); }); }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts index ad2bea5a77..eac4862540 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts @@ -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(); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-sub-pages.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-sub-pages.ts index 92a900ab77..142f62508f 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-sub-pages.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-sub-pages.ts @@ -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(); }; diff --git a/src/frontend/apps/impress/src/components/dropdown-menu/DropdownMenu.tsx b/src/frontend/apps/impress/src/components/DropdownMenu.tsx similarity index 83% rename from src/frontend/apps/impress/src/components/dropdown-menu/DropdownMenu.tsx rename to src/frontend/apps/impress/src/components/DropdownMenu.tsx index 207c962dd0..a141d9e21f 100644 --- a/src/frontend/apps/impress/src/components/dropdown-menu/DropdownMenu.tsx +++ b/src/frontend/apps/impress/src/components/DropdownMenu.tsx @@ -2,6 +2,7 @@ import { HorizontalSeparator } from '@gouvfr-lasuite/ui-kit'; import { Fragment, PropsWithChildren, + ReactNode, useCallback, useEffect, useRef, @@ -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; @@ -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]); @@ -153,7 +167,6 @@ export const DropdownMenu = ({ return; } const isDisabled = option.disabled !== undefined && option.disabled; - const isFocused = index === focusedIndex; return ( @@ -204,17 +217,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); - `} `} > - {option.icon && ( - - )} + {option.icon && + (typeof option.icon === 'string' ? ( + + ) : ( + option.icon + ))} {option.label} diff --git a/src/frontend/apps/impress/src/components/filter/FilterDropdown.tsx b/src/frontend/apps/impress/src/components/filter/FilterDropdown.tsx index ce71f2087b..313209bf42 100644 --- a/src/frontend/apps/impress/src/components/filter/FilterDropdown.tsx +++ b/src/frontend/apps/impress/src/components/filter/FilterDropdown.tsx @@ -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[]; diff --git a/src/frontend/apps/impress/src/components/index.ts b/src/frontend/apps/impress/src/components/index.ts index cf076e9ff8..bb42672469 100644 --- a/src/frontend/apps/impress/src/components/index.ts +++ b/src/frontend/apps/impress/src/components/index.ts @@ -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'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/SimpleDocItem.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/SimpleDocItem.tsx index ff1524b651..70fedfdff5 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/components/SimpleDocItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/SimpleDocItem.tsx @@ -43,6 +43,7 @@ export const SimpleDocItem = ({ $overflow="auto" $width="100%" className="--docs--simple-doc-item" + role="presentation" >