Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to
### Changed

- ♻️(backend) reset collaboration connection in cascade for all children #2507
- ♿️(frontend) limit share modal opening announcement for screen readers #2452

### Fixed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ import {
import { QuickSearchGroupMember } from './DocShareMember';
import { DocShareModalFooter } from './DocShareModalFooter';

const SEARCH_QUERY_DEBOUNCE_MS = 300;
const ACCESSIBILITY_REEXPOSURE_DELAY_MS = 300;

const ShareModalStyle = createGlobalStyle`
.--docs--doc-share-modal [cmdk-item] {
cursor: auto;
Expand Down Expand Up @@ -84,6 +87,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {

const [listHeight, setListHeight] = useState<string>('400px');
const canShare = doc.abilities.accesses_manage && isRootDoc;
const [isContentAccessible, setIsContentAccessible] = useState(canShare);
const canViewAccesses = doc.abilities.accesses_view;
const showMemberSection = inputValue === '' && selectedUsers.length === 0;
const showFooter = selectedUsers.length === 0 && !inputValue;
Expand Down Expand Up @@ -119,7 +123,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {

const onFilter = useDebouncedCallback((str: string) => {
setUserQuery(str);
}, 300);
}, SEARCH_QUERY_DEBOUNCE_MS);

const onRemoveUser = (row: User) => {
setSelectedUsers((prevState) => {
Expand Down Expand Up @@ -161,6 +165,23 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
const showInheritedShareContent =
inheritedAccesses.length > 0 && showMemberSection && !isRootDoc;

// When the search input is hidden, keep the modal content out of the
// accessibility tree during the opening announcement, then restore it.
useEffect(() => {
if (canShare) {
setIsContentAccessible(true);
return;
}

setIsContentAccessible(false);

const id = window.setTimeout(() => {
setIsContentAccessible(true);
}, ACCESSIBILITY_REEXPOSURE_DELAY_MS);

return () => window.clearTimeout(id);
}, [canShare]);
Comment on lines +170 to +183

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

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

echo '== outline =='
ast-grep outline "$file" --view expanded || true

echo
echo '== line count =='
wc -l "$file"

echo
echo '== relevant ranges =='
sed -n '1,280p' "$file" | cat -n

Repository: suitenumerique/docs

Length of output: 11433


🏁 Script executed:

#!/bin/bash
set -euo pipefail
file='src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx'
nl -ba "$file" | sed -n '1,320p'

Repository: suitenumerique/docs

Length of output: 197


🏁 Script executed:

#!/bin/bash
set -euo pipefail

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

echo '== focus-related symbols =='
grep -nE 'focus|autoFocus|ref=' "$file" || true

echo
echo '== canShare / isContentAccessible usage =='
grep -nE 'canShare|isContentAccessible|aria-hidden' "$file" || true

echo
echo '== lower half of file =='
sed -n '280,421p' "$file" | cat -n

Repository: suitenumerique/docs

Length of output: 6166


Move focus out of the share content before canShare hides it. autoFocus={!canShare} only runs on mount, so a true → false transition can leave focus inside an aria-hidden subtree. Shift focus to the close button on that transition and make the hidden state update synchronously from canShare instead of waiting for the effect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx`
around lines 170 - 183, Update the DocShareModal focus and visibility logic
around the canShare-driven useEffect: synchronously derive the
hidden/accessibility state from canShare, and when canShare transitions from
true to false, move focus to the close button before the share content becomes
aria-hidden. Preserve the delayed re-exposure behavior and cleanup for the
accessibility timeout.


Comment thread
Ovgodd marked this conversation as resolved.
// Invalidate relevant queries to ensure fresh data on modal open
useEffect(() => {
[
Expand Down Expand Up @@ -197,6 +218,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
{t('Share the document')}
</Text>
<ButtonCloseModal
autoFocus={!canShare}
aria-label={t('Close the share modal')}
onClick={onClose}
/>
Expand All @@ -205,7 +227,11 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
hideCloseButton
>
<ShareModalStyle />
{/* aria-hidden is temporary (300ms) to prevent NVDA from reading
the entire modal body on open when the search input is absent.
autoFocus alone on the close button is not enough. */}
<Box
aria-hidden={isContentAccessible ? undefined : true}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure to understand why do we do that ? Seems strange to hide a part of the modal a few milliseconds, without apparent reason.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes good point, I should have added a comment, just pushed one!

I first tried with just autoFocus on the close button but it was not working, NVDA was still reading all the modal body on open (Suggestions, link settings, Copy link…). After several tests I found that this temporary aria-hidden (300ms) is the only way to block this initial reading. After the timeout it's removed and everything works normally with Tab navigation.

$height="auto"
$maxHeight={canViewAccesses ? modalContentHeight : 'none'}
$overflow="hidden"
Expand Down
Loading