Skip to content

Commit ee3b05c

Browse files
committed
✨(frontend) improve NVDA navigation in DocShareModal
fix NVDA focus and announcement issues in search modal combobox Signed-off-by: Cyril <c.gromoff@gmail.com>
1 parent c23ff54 commit ee3b05c

File tree

9 files changed

+60
-55
lines changed

9 files changed

+60
-55
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# Changelog
21

32
All notable changes to this project will be documented in this file.
43

@@ -27,6 +26,12 @@ and this project adheres to
2726
- 🐛(frontend) fix legacy role computation #1376
2827
- 🐛(frontend) scroll back to top when navigate to a document #1406
2928

29+
### Changed
30+
31+
- ♿(frontend) improve accessibility:
32+
- ♿improve NVDA navigation in DocShareModal #1396
33+
34+
3035
## [3.7.0] - 2025-09-12
3136

3237
### Added

src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -757,15 +757,21 @@ test.describe('Doc Editor', () => {
757757
await expect(searchContainer.getByText(docChild2)).toBeVisible();
758758
await expect(searchContainer.getByText(randomDoc)).toBeHidden();
759759

760-
// use keydown to select the second result
760+
await page.keyboard.press('ArrowDown');
761761
await page.keyboard.press('ArrowDown');
762762
await page.keyboard.press('Enter');
763763

764-
const interlink = page.getByRole('link', {
765-
name: 'child-2',
764+
// Wait for the search container to disappear, indicating selection was made
765+
await expect(searchContainer).toBeHidden();
766+
767+
// Wait for the interlink to be created and rendered
768+
const editor = page.locator('.ProseMirror.bn-editor');
769+
770+
const interlink = editor.getByRole('link', {
771+
name: docChild2,
766772
});
767773

768-
await expect(interlink).toBeVisible();
774+
await expect(interlink).toBeVisible({ timeout: 10000 });
769775
await interlink.click();
770776

771777
await verifyDocName(page, docChild2);

src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@ test.describe('Document create member', () => {
2626

2727
await page.getByRole('button', { name: 'Share' }).click();
2828

29-
const inputSearch = page.getByRole('combobox', {
30-
name: 'Quick search input',
31-
});
29+
const inputSearch = page.getByTestId('quick-search-input');
30+
3231
await expect(inputSearch).toBeVisible();
3332

3433
// Select user 1 and verify tag
@@ -118,9 +117,7 @@ test.describe('Document create member', () => {
118117

119118
await page.getByRole('button', { name: 'Share' }).click();
120119

121-
const inputSearch = page.getByRole('combobox', {
122-
name: 'Quick search input',
123-
});
120+
const inputSearch = page.getByTestId('quick-search-input');
124121

125122
const [email] = randomName('test@test.fr', browserName, 1);
126123
await inputSearch.fill(email);
@@ -168,9 +165,7 @@ test.describe('Document create member', () => {
168165

169166
await page.getByRole('button', { name: 'Share' }).click();
170167

171-
const inputSearch = page.getByRole('combobox', {
172-
name: 'Quick search input',
173-
});
168+
const inputSearch = page.getByTestId('quick-search-input');
174169

175170
const email = randomName('test@test.fr', browserName, 1)[0];
176171
await inputSearch.fill(email);

src/frontend/apps/e2e/__tests__/app-impress/utils-share.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ export const addNewMember = async (
2323
response.status() === 200,
2424
);
2525

26-
const inputSearch = page.getByRole('combobox', {
27-
name: 'Quick search input',
28-
});
26+
const inputSearch = page.getByTestId('quick-search-input');
2927

3028
// Select a new user
3129
await inputSearch.fill(fillText);

src/frontend/apps/e2e/__tests__/app-impress/utils-sub-pages.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export const clickOnAddRootSubPage = async (page: Page) => {
6464
const rootItem = page.getByTestId('doc-tree-root-item');
6565
await expect(rootItem).toBeVisible();
6666
await rootItem.hover();
67-
await rootItem.getByRole('button', { name: /add subpage/i }).click();
67+
await rootItem.getByTestId('doc-tree-item-actions-add-child').click();
6868
};
6969

7070
export const navigateToPageFromTree = async ({

src/frontend/apps/impress/src/components/quick-search/QuickSearch.tsx

Lines changed: 21 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
import { Command } from 'cmdk';
2-
import {
3-
PropsWithChildren,
4-
ReactNode,
5-
useEffect,
6-
useRef,
7-
useState,
8-
} from 'react';
2+
import { PropsWithChildren, ReactNode, useId, useRef, useState } from 'react';
93

104
import { hasChildrens } from '@/utils/children';
115

@@ -49,32 +43,23 @@ export const QuickSearch = ({
4943
children,
5044
}: PropsWithChildren<QuickSearchProps>) => {
5145
const ref = useRef<HTMLDivElement | null>(null);
52-
const [selectedValue, setSelectedValue] = useState<string>('');
46+
const listId = useId();
47+
const NO_SELECTION_VALUE = '__none__';
48+
const [userInteracted, setUserInteracted] = useState(false);
49+
const [selectedValue, setSelectedValue] = useState(NO_SELECTION_VALUE);
50+
const isExpanded = userInteracted;
5351

54-
// Auto-select first item when children change
55-
useEffect(() => {
56-
if (!children) {
57-
setSelectedValue('');
58-
return;
52+
const handleValueChange = (val: string) => {
53+
if (userInteracted) {
54+
setSelectedValue(val);
5955
}
56+
};
6057

61-
// Small delay for DOM to update
62-
const timeoutId = setTimeout(() => {
63-
const firstItem = ref.current?.querySelector('[cmdk-item]');
64-
if (firstItem) {
65-
const value =
66-
firstItem.getAttribute('data-value') ||
67-
firstItem.getAttribute('value') ||
68-
firstItem.textContent?.trim() ||
69-
'';
70-
if (value) {
71-
setSelectedValue(value);
72-
}
73-
}
74-
}, 50);
75-
76-
return () => clearTimeout(timeoutId);
77-
}, [children]);
58+
const handleUserInteract = () => {
59+
if (!userInteracted) {
60+
setUserInteracted(true);
61+
}
62+
};
7863

7964
return (
8065
<>
@@ -84,9 +69,9 @@ export const QuickSearch = ({
8469
label={label}
8570
shouldFilter={false}
8671
ref={ref}
87-
value={selectedValue}
88-
onValueChange={setSelectedValue}
8972
tabIndex={0}
73+
value={selectedValue}
74+
onValueChange={handleValueChange}
9075
>
9176
{showInput && (
9277
<QuickSearchInput
@@ -95,11 +80,14 @@ export const QuickSearch = ({
9580
inputValue={inputValue}
9681
onFilter={onFilter}
9782
placeholder={placeholder}
83+
listId={listId}
84+
isExpanded={isExpanded}
85+
onUserInteract={handleUserInteract}
9886
>
9987
{inputContent}
10088
</QuickSearchInput>
10189
)}
102-
<Command.List>
90+
<Command.List id={listId} aria-label={label} role="listbox">
10391
<Box>{children}</Box>
10492
</Command.List>
10593
</Command>

src/frontend/apps/impress/src/components/quick-search/QuickSearchInput.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ type Props = {
1616
placeholder?: string;
1717
children?: ReactNode;
1818
withSeparator?: boolean;
19+
listId?: string;
20+
onUserInteract?: () => void;
21+
isExpanded?: boolean;
1922
};
2023
export const QuickSearchInput = ({
2124
loading,
@@ -24,6 +27,9 @@ export const QuickSearchInput = ({
2427
placeholder,
2528
children,
2629
withSeparator: separator = true,
30+
listId,
31+
onUserInteract,
32+
isExpanded,
2733
}: Props) => {
2834
const { t } = useTranslation();
2935
const { spacingsTokens } = useCunninghamTheme();
@@ -57,14 +63,19 @@ export const QuickSearchInput = ({
5763
<Command.Input
5864
autoFocus={true}
5965
aria-label={t('Quick search input')}
66+
aria-expanded={isExpanded}
67+
aria-controls={listId}
6068
onClick={(e) => {
6169
e.stopPropagation();
70+
onUserInteract?.();
6271
}}
72+
onKeyDown={() => onUserInteract?.()}
6373
value={inputValue}
6474
role="combobox"
6575
placeholder={placeholder ?? t('Search')}
6676
onValueChange={onFilter}
6777
maxLength={254}
78+
data-testid="quick-search-input"
6879
/>
6980
</Box>
7081
{separator && <HorizontalSeparator $withPadding={false} />}

src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,9 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
135135
isOpen
136136
closeOnClickOutside
137137
data-testid="doc-share-modal"
138-
aria-describedby="doc-share-modal-title"
138+
aria-labelledby="doc-share-modal-title"
139139
size={isDesktop ? ModalSize.LARGE : ModalSize.FULL}
140+
aria-modal="true"
140141
onClose={onClose}
141142
title={
142143
<Box $direction="row" $justify="space-between" $align="center">
@@ -160,13 +161,13 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
160161
>
161162
<ShareModalStyle />
162163
<Box
163-
role="dialog"
164-
aria-label={t('Share modal content')}
165164
$height="auto"
166165
$maxHeight={canViewAccesses ? modalContentHeight : 'none'}
167166
$overflow="hidden"
168167
className="--docs--doc-share-modal noPadding "
169168
$justify="space-between"
169+
role="dialog"
170+
aria-label={t('Share modal content')}
170171
>
171172
<Box
172173
$flex={1}
@@ -223,6 +224,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
223224
)}
224225
{canViewAccesses && (
225226
<QuickSearch
227+
label={t('Search results')}
226228
onFilter={(str) => {
227229
setInputValue(str);
228230
onFilter(str);

src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTreeItemActions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ export const DocTreeItemActions = ({
181181
});
182182
}}
183183
color="primary"
184-
aria-label={t('Add subpage')}
184+
data-testid="doc-tree-item-actions-add-child"
185185
>
186186
<Icon
187187
variant="filled"

0 commit comments

Comments
 (0)