Skip to content
Merged
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 @@ -19,6 +19,7 @@ and this project adheres to

- ♿️(frontend) hide mobile left panel from screen readers when collapsed #2450
- ♿️(frontend) enable blocknote heading ids for toc anchors #2449
- ♿️(frontend) focus export modal on format select #2421

## [v5.3.0] - 2026-06-19

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ import { DocumentProps, pdf } from '@react-pdf/renderer';
import jsonemoji from 'emoji-datasource-apple' with { type: 'json' };
import i18next from 'i18next';
import JSZip from 'jszip';
import { cloneElement, isValidElement, useState } from 'react';
import {
cloneElement,
isValidElement,
useEffect,
useRef,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';

Expand Down Expand Up @@ -59,6 +65,17 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
);
const { untitledDocument } = useTrans();
const mediaUrl = useMediaUrl();
const selectRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const frameId = requestAnimationFrame(() => {
const button = selectRef.current?.querySelector<HTMLButtonElement>(
'button, [role="combobox"]',
);
button?.focus();
});
return () => cancelAnimationFrame(frameId);
}, []);
Comment on lines +70 to +78

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for Modal lifecycle props in Cunningham or similar focus patterns in the codebase

# Search for Modal usage with lifecycle callbacks
rg -n -C3 'from.*cunningham.*Modal' --type=tsx --type=ts

# Look for other focus management patterns after modal open
rg -n -C3 'requestAnimationFrame.*focus' --type=tsx --type=ts

Repository: suitenumerique/docs

Length of output: 119


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect the exact focus effect in the target file
echo "=== ModalExport.tsx (lines ~50-95) ==="
nl -ba src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx | sed -n '45,105p'

# 2) Find how Cunningham Modal is imported/used in this repo (tsx/js)
echo "=== Cunningham Modal imports/usages ==="
rg -n --glob='*.tsx' --glob='*.ts' -C2 'from\s+["'\''].*cunningham-react.*Modal["'\'']' || true
rg -n --glob='*.tsx' --glob='*.ts' -C2 '\bModal\b' src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx || true

# 3) Look for common lifecycle props/handlers around Modal
echo "=== Modal lifecycle prop names in TS/TSX ==="
rg -n --glob='*.tsx' --glob='*.ts' -C2 '\b(onAfterOpen|onOpened|onOpen|onAfterClose|onClosed|onClose)\b' src/frontend/apps/impress/src/features/docs/doc-export/components || true
rg -n --glob='*.tsx' --glob='*.ts' -C2 '\b(onAfterOpen|onOpened|onOpen|onAfterClose|onClosed|onClose)\b' src/frontend/apps/impress/src || true

# 4) Check for other requestAnimationFrame + focus patterns in the repo
echo "=== requestAnimationFrame .* focus patterns ==="
rg -n --glob='*.tsx' --glob='*.ts' -C2 'requestAnimationFrame.*focus|focus.*requestAnimationFrame' src/frontend/apps/impress/src || true

# 5) Ensure we didn't miss other Cunningham Modal references elsewhere
echo "=== Any Cunningham Modal usage outside doc-export ==="
rg -n --glob='*.tsx' --glob='*.ts' -C2 'cunningham-react' src/frontend/apps/impress/src || true

Repository: suitenumerique/docs

Length of output: 163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx"

# 1) Inspect the exact focus effect in the target file (with line numbers via awk)
echo "=== ModalExport.tsx (lines 50-105) ==="
awk 'NR>=45 && NR<=105 {printf "%6d  %s\n", NR, $0}' "$FILE" | sed -n '1,200p'

# 2) Find how Cunningham Modal is imported/used in this repo
echo "=== Cunningham Modal imports/usages (impress app) ==="
rg -n --glob='*.tsx' --glob='*.ts' -C2 'from\s+["'\''\`].*cunningham-react.*["'\''\`]' src/frontend/apps/impress/src || true

echo "=== Cunningham Modal usage in doc-export components ==="
rg -n --glob='*.tsx' --glob='*.ts' -C2 '\bModal\b' src/frontend/apps/impress/src/features/docs/doc-export/components || true

# 3) Look for common lifecycle prop names around Modal usage
echo "=== Modal lifecycle prop names (impress app) ==="
rg -n --glob='*.tsx' --glob='*.ts' -C2 '\b(onAfterOpen|onOpened|onOpen|onAfterClose|onClosed|onClose)\b' src/frontend/apps/impress/src || true

# 4) Check for other requestAnimationFrame + focus patterns in the repo
echo "=== requestAnimationFrame .* focus patterns (impress app) ==="
rg -n --glob='*.tsx' --glob='*.ts' -C2 'requestAnimationFrame.*focus|focus.*requestAnimationFrame' src/frontend/apps/impress/src || true

Repository: suitenumerique/docs

Length of output: 50509


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx"
DIR="src/frontend/apps/impress/src/features/docs/doc-export/components"

echo "=== ModalExport.tsx imports (top of file) ==="
awk 'NR>=1 && NR<=60 {printf "%6d  %s\n", NR, $0}' "$FILE"

echo "=== Find cunningham-react usage (impress app) ==="
rg -n --glob='*.ts' --glob='*.tsx' 'cunningham-react' src/frontend/apps/impress/src || true

echo "=== Search modal lifecycle prop names ==="
rg -n --glob='*.ts' --glob='*.tsx' '\bonAfterOpen\b|\bonOpened\b|\bonAfterClose\b|\bonClosed\b|\bonOpen\b|\bonClose\b' src/frontend/apps/impress/src/features || true

echo "=== requestAnimationFrame + focus patterns in doc-export/components ==="
rg -n --glob='*.ts' --glob='*.tsx' 'requestAnimationFrame\([^\)]*\).*focus|focus.*requestAnimationFrame' "$DIR" || true

Repository: suitenumerique/docs

Length of output: 27415


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx"

# Print the part where selectRef is assigned and the Select is rendered
awk 'NR>=60 && NR<=150 {printf "%6d  %s\n", NR, $0}' "$FILE"
awk 'NR>=200 && NR<=290 {printf "%6d  %s\n", NR, $0}' "$FILE"

Repository: suitenumerique/docs

Length of output: 6653


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx"

echo "=== Find selectRef usage ==="
rg -n --glob='*.tsx' 'selectRef' "$FILE"

echo "=== Show around the Select rendering ==="
rg -n --glob='*.tsx' '(<Select\b|role="combobox")' "$FILE"
# Print a couple sections around first select usage
LINE=$(rg -n '(<Select\b|role="combobox")' "$FILE" | head -n1 | cut -d: -f1)
START=$((LINE-25))
END=$((LINE+60))
awk -v s="$START" -v e="$END" 'NR>=s && NR<=e {printf "%6d  %s\n", NR, $0}' "$FILE"

Repository: suitenumerique/docs

Length of output: 3610


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx"

# Print around the selectRef attachment and the Select component
awk 'NR>=280 && NR<=360 {printf "%6d  %s\n", NR, $0}' "$FILE"

# Check for autoFocus usage in this file
echo "=== autoFocus in ModalExport.tsx ==="
rg -n --glob='*.tsx' '\bautoFocus\b' "$FILE" || true

Repository: suitenumerique/docs

Length of output: 1606


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for requestAnimationFrame-based focus anywhere in impress app
rg -n --glob='*.ts' --glob='*.tsx' 'requestAnimationFrame\([^)]*\).*focus|focus.*requestAnimationFrame' src/frontend/apps/impress/src || true

# Search for autoFocus usage (often used for deterministic initial focus)
rg -n --glob='*.ts' --glob='*.tsx' '\bautoFocus\b' src/frontend/apps/impress/src || true

# Search for useModal usage (may provide focus/restore helpers)
rg -n --glob='*.ts' --glob='*.tsx' '\buseModal\b' src/frontend/apps/impress/src/features || true

Repository: suitenumerique/docs

Length of output: 3594


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect a few modal components that use autoFocus/useModal/FocusScope for initial focus behavior
FILES=(
  "src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteToolBar/ModalConfirmDownloadUnsafe.tsx"
  "src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalSelectVersion.tsx"
  "src/frontend/apps/impress/src/features/docs/doc-management/components/ModalRemoveDoc.tsx"
  "src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterOverlay.tsx"
  "src/frontend/apps/impress/src/components/modal/AlertModal.tsx"
)

for f in "${FILES[@]}"; do
  echo "===== $f ====="
  # Print relevant region around the first autoFocus occurrence
  line=$(rg -n --glob='*.tsx' 'autoFocus' "$f" | head -n1 | cut -d: -f1 || true)
  if [ -n "${line:-}" ]; then
    start=$((line-20))
    end=$((line+40))
    awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%6d  %s\n", NR, $0}' "$f" | sed -n '1,200p'
  else
    # fallback: print top part
    awk 'NR>=1 && NR<=120 {printf "%6d  %s\n", NR, $0}' "$f" | sed -n '1,200p'
  fi
done

# Specifically inspect how useModal is used in DocMoveModal (focus restore patterns)
echo "===== DocMoveModal useModal snippet ====="
DOCMOV="src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx"
awk 'NR>=60 && NR<=180 {printf "%6d  %s\n", NR, $0}' "$DOCMOV" | sed -n '1,220p'

Repository: suitenumerique/docs

Length of output: 14753


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find other code focusing a combobox/select inside modals
rg -n --glob='*.ts' --glob='*.tsx' '\[role="combobox"\]' src/frontend/apps/impress/src || true

# Look for the same focus-by-querySelector pattern
rg -n --glob='*.ts' --glob='*.tsx' 'querySelector<.*>\(.*role="combobox"|requestAnimationFrame\([^\)]*\)\s*=>\s*\{[^\}]*querySelector' src/frontend/apps/impress/src || true

# Look for requestAnimationFrame focusing something inside modals
rg -n --glob='*.ts' --glob='*.tsx' 'requestAnimationFrame\([^\)]*\).*\.focus\(\)' src/frontend/apps/impress/src || true

Repository: suitenumerique/docs

Length of output: 185


ModalExport focus timing: no Cunningham lifecycle hooks in-repo; RAF matches existing patterns.

  • No onAfterOpen/onOpened (or similar) Cunningham Modal lifecycle props are used anywhere in this codebase, so the focus timing can’t be improved by swapping to a supported hook here.
  • The requestAnimationFrame focus pattern is consistent with other modals in this repo (e.g., ModalRemoveDoc uses requestAnimationFrame because focus restoration from other components can race with autoFocus).

Minor hardening: since selectRef only wraps the format Select, tighten the selector to [role="combobox"] (instead of button, [role="combobox"]) to avoid focusing an unintended nested button.

🤖 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-export/components/ModalExport.tsx`
around lines 70 - 78, The focus-selector in ModalExport's useEffect is too broad
and may hit unintended nested buttons; update the query inside the
requestAnimationFrame callback to only target the Select component via
selectRef.current?.querySelector<HTMLButtonElement>('[role="combobox"]') (keep
the existing requestAnimationFrame/cancelAnimationFrame logic and the useEffect
as-is) so only the format Select gains focus; locate this change in the
useEffect inside the ModalExport component where selectRef is used.


const formatOptions = [
{ label: t('PDF'), value: DocDownloadFormat.PDF },
Expand Down Expand Up @@ -227,7 +244,6 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
aria-label={t('Cancel the download')}
variant="secondary"
fullWidth
autoFocus
onClick={() => onClose()}
>
{t('Cancel')}
Expand Down Expand Up @@ -282,16 +298,18 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
'Export your document to print or download in .docx, .odt, .pdf or .html(zip) format.',
)}
</Text>
<Select
clearable={false}
fullWidth
label={t('Format')}
options={formatOptions}
value={format}
onChange={(options) =>
setFormat(options.target.value as DocDownloadFormat)
}
/>
<Box ref={selectRef}>
<Select
clearable={false}
fullWidth
label={t('Format')}
options={formatOptions}
value={format}
onChange={(options) =>
setFormat(options.target.value as DocDownloadFormat)
}
/>
</Box>

{isExporting && (
<Box
Expand Down
Loading