mirror of
https://github.com/suitenumerique/docs.git
synced 2026-09-12 12:47:55 +02:00
♿️(frontend) announce slide position in presenter mode
Announce slide position via react-aria on change; drop bar live region.
This commit is contained in:
-12
@@ -97,18 +97,6 @@ export const PresenterFloatingBar = ({
|
||||
<Text as="span" $size="sm" $color="neutral" aria-hidden="true">
|
||||
{index + 1} / {total}
|
||||
</Text>
|
||||
<Text
|
||||
as="span"
|
||||
className="sr-only"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>
|
||||
{t('Slide {{current}} of {{total}}', {
|
||||
current: index + 1,
|
||||
total,
|
||||
})}
|
||||
</Text>
|
||||
<Button
|
||||
size="small"
|
||||
color="neutral"
|
||||
|
||||
+17
-1
@@ -1,3 +1,4 @@
|
||||
import { announce } from '@react-aria/live-announcer';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -10,7 +11,7 @@ import { Doc } from '@/docs/doc-management';
|
||||
import { PRESENTER_WINDOW_RADIUS } from '../constants';
|
||||
import { useBrowserFullscreen } from '../hooks/useBrowserFullscreen';
|
||||
import { usePresenterShortcuts } from '../hooks/usePresenterShortcuts';
|
||||
import { useSlides } from '../hooks/useSlides';
|
||||
import { getSlideTitle, useSlides } from '../hooks/useSlides';
|
||||
|
||||
import { PresenterFloatingBar } from './PresenterFloatingBar';
|
||||
import { PresenterSlide } from './PresenterSlide';
|
||||
@@ -105,6 +106,21 @@ export const PresenterOverlay = ({
|
||||
|
||||
const frameRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const title = getSlideTitle(slides[currentIndex] ?? []);
|
||||
const message = title
|
||||
? t('Slide {{current}} of {{total}}: {{title}}', {
|
||||
current: currentIndex + 1,
|
||||
total,
|
||||
title,
|
||||
})
|
||||
: t('Slide {{current}} of {{total}}', {
|
||||
current: currentIndex + 1,
|
||||
total,
|
||||
});
|
||||
announce(message, 'polite');
|
||||
}, [slides, currentIndex, total, t]);
|
||||
|
||||
if (typeof document === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -40,3 +40,41 @@ export const splitBlocksIntoSlides = <T extends Block>(blocks: T[]): T[][] => {
|
||||
export const useSlides = <T extends Block>(blocks: T[]): T[][] => {
|
||||
return useMemo(() => splitBlocksIntoSlides(blocks), [blocks]);
|
||||
};
|
||||
|
||||
const extractInlineText = (content: unknown): string => {
|
||||
if (typeof content === 'string') {
|
||||
return content;
|
||||
}
|
||||
if (!Array.isArray(content)) {
|
||||
return '';
|
||||
}
|
||||
return content
|
||||
.map((node) => {
|
||||
if (typeof node === 'string') {
|
||||
return node;
|
||||
}
|
||||
if (node && typeof node === 'object') {
|
||||
const inline = node as { text?: unknown; content?: unknown };
|
||||
if (typeof inline.text === 'string') {
|
||||
return inline.text;
|
||||
}
|
||||
if (inline.content !== undefined) {
|
||||
return extractInlineText(inline.content);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.join('');
|
||||
};
|
||||
|
||||
/** First heading text, or first block with text — for SR announcements. */
|
||||
export const getSlideTitle = (
|
||||
blocks: { type: string; content?: unknown }[],
|
||||
): string => {
|
||||
const heading = blocks.find((block) => block.type === 'heading');
|
||||
const source =
|
||||
heading ??
|
||||
blocks.find((block) => extractInlineText(block.content).trim().length > 0);
|
||||
|
||||
return source ? extractInlineText(source.content).trim() : '';
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user