♿️(frontend) improve the accessibility of the upload loader block

The loader used Card, adding an unnamed region per upload, and neither
the analysis start nor the antivirus warning was announced. Use Box
again, announce both through announce(), hide the decorative warning
icon and keep upload error toasts 10s so the cause can be read.
This commit is contained in:
Anthony LC
2026-10-06 16:32:28 +02:00
parent c37df722fd
commit 20724bb85d
4 changed files with 32 additions and 9 deletions
@@ -1,5 +1,6 @@
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
width="24"
height="24"
viewBox="0 0 24 24"

Before

Width:  |  Height:  |  Size: 425 B

After

Width:  |  Height:  |  Size: 446 B

@@ -6,10 +6,11 @@ import {
StyleSchema,
} from '@blocknote/core';
import { createReactBlockSpec } from '@blocknote/react';
import { announce } from '@react-aria/live-announcer';
import { t } from 'i18next';
import { useEffect } from 'react';
import { Card, Icon, Text } from '@/components';
import { Box, Icon, Text } from '@/components';
import { useMediaUrl } from '@/core';
import { isSafeUrl } from '@/utils/url';
@@ -114,16 +115,19 @@ const UploadLoaderBlockComponent = ({
console.error('Error analyzing file:', error);
const information = t(
'The antivirus has detected an anomaly in your file.',
);
try {
editor.updateBlock(block.id, {
type: 'uploadLoader',
props: {
type: 'warning',
information: t(
'The antivirus has detected an anomaly in your file.',
),
information,
},
});
announce(information, 'assertive');
} catch {
/* During collaboration, another user might have updated the block */
}
@@ -135,14 +139,16 @@ const UploadLoaderBlockComponent = ({
}, [block, editor, mediaUrl, isEditable]);
return (
<Card
<Box
className="bn-visual-media-wrapper"
$direction="row"
$gap="xs"
$withThemeBG
$border="none"
$radius="var(--c--globals--spacings--st)"
$theme="neutral"
$scope="semantic"
$variation="tertiary"
$width="100%"
$padding="xs"
>
@@ -158,7 +164,7 @@ const UploadLoaderBlockComponent = ({
<Text $variation="tertiary" $weight={500}>
{block.props.information}
</Text>
</Card>
</Box>
);
};
@@ -69,6 +69,7 @@ describe('useUploadFile', () => {
expect(mockToast).toHaveBeenCalledWith(
'The file "video.mp4" is too large. Maximum file size is 1KB.',
'error',
{ duration: 10000 },
);
});
@@ -89,6 +90,7 @@ describe('useUploadFile', () => {
expect(mockToast).toHaveBeenCalledWith(
'A problem occurred while uploading the file, cause: File type not allowed.',
'error',
{ duration: 10000 },
);
});
});
@@ -1,5 +1,6 @@
import { Block } from '@blocknote/core';
import { VariantType } from '@gouvfr-lasuite/ui-components';
import { announce } from '@react-aria/live-announcer';
import { captureException } from '@sentry/nextjs';
import { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
@@ -16,6 +17,9 @@ import { DocsBlockNoteEditor } from '../types';
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; // Default to 10MB
// Upload errors carry a cause the user has to read: keep them longer than the default toast.
const UPLOAD_ERROR_TOAST_DURATION = 10000;
const TEXT_ALIGNMENTS = ['left', 'center', 'right', 'justify'] as const;
type TextAlignment = (typeof TEXT_ALIGNMENTS)[number];
@@ -63,6 +67,7 @@ export const useUploadFile = (docId: string) => {
},
),
VariantType.ERROR,
{ duration: UPLOAD_ERROR_TOAST_DURATION },
);
throw new Error('File is too large');
@@ -88,6 +93,7 @@ export const useUploadFile = (docId: string) => {
'A problem occurred while uploading the file, please try again.',
),
VariantType.ERROR,
{ duration: UPLOAD_ERROR_TOAST_DURATION },
);
throw error;
@@ -114,7 +120,7 @@ export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
* Replace the resource block by a uploadLoader block to show analyzing status
*/
const replaceBlockWithUploadLoader = useCallback(
(block: Block) => {
(block: Block, shouldAnnounce = false) => {
if (
!block ||
!('url' in block.props) ||
@@ -133,13 +139,15 @@ export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
const blockUploadTextAlignment = fileTextAlignment(block.props);
try {
const information = t('Analyzing the file');
editor.replaceBlocks(
[block.id],
[
{
type: 'uploadLoader',
props: {
information: t('Analyzing the file'),
information,
type: 'loading',
blockUploadName,
blockUploadType,
@@ -151,6 +159,12 @@ export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
},
],
);
// Only the user who uploaded the file hears it, not the collaborators
// nor a page reload.
if (shouldAnnounce) {
announce(information, 'polite');
}
} catch (error) {
captureException(error, {
extra: { info: 'Error replacing block for upload loader' },
@@ -194,7 +208,7 @@ export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
const innerTimeoutId = setTimeout(() => {
const block = editor.getBlock({ id: blockId });
replaceBlockWithUploadLoader(block as Block);
replaceBlockWithUploadLoader(block as Block, true);
}, 300);
return () => {