Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added accessible announcement for the TextAnnotatorPopup appearance #141

Draft
wants to merge 37 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5253915
Added navigation hint announcement on store's idling
oleksandr-danylchenko Aug 22, 2024
c3e4698
Removed message repetition on subsequent floating openings
oleksandr-danylchenko Aug 22, 2024
ce63a24
Made navigation message configurable
oleksandr-danylchenko Aug 22, 2024
fc9bc7a
Removed keyboard event limitation for the live area
oleksandr-danylchenko Aug 22, 2024
7b61c22
Removed live area init when there's no message present
oleksandr-danylchenko Aug 22, 2024
f09e612
Revert "Removed keyboard event limitation for the live area"
oleksandr-danylchenko Aug 22, 2024
1b6c1fe
Added navigation hint disabled based on the event
oleksandr-danylchenko Aug 22, 2024
1fff844
Added random generator into the hook
oleksandr-danylchenko Aug 22, 2024
b3691fd
Merge branch 'keyboard-event-selection' into keyboard-selection-annou…
oleksandr-danylchenko Aug 23, 2024
dd45bcd
Made `idle` timeout configurable
oleksandr-danylchenko Aug 23, 2024
1f3ba29
Added reaction to the `disabled` change
oleksandr-danylchenko Aug 23, 2024
faa30fe
Added navigation announce dismissal when the floating is already focused
oleksandr-danylchenko Aug 23, 2024
1e54fbc
Merge branch 'keyboard-event-selection' into keyboard-selection-annou…
oleksandr-danylchenko Aug 28, 2024
cacc575
Removed testing log
oleksandr-danylchenko Aug 28, 2024
281ec00
Merge branch 'keyboard-event-selection' into keyboard-selection-annou…
oleksandr-danylchenko Aug 28, 2024
1c97011
Added back global hooks re-export
oleksandr-danylchenko Aug 28, 2024
5180bfd
Merge branch 'keyboard-event-selection' into keyboard-selection-annou…
oleksandr-danylchenko Aug 29, 2024
9ad1da7
Merge branch 'keyboard-event-selection' into keyboard-selection-annou…
oleksandr-danylchenko Aug 29, 2024
978261a
Merge branch 'keyboard-event-selection' into keyboard-selection-annou…
oleksandr-danylchenko Aug 30, 2024
6b97c27
Merge branch 'keyboard-event-selection' into keyboard-selection-annou…
oleksandr-danylchenko Sep 2, 2024
2ec67d5
Merge branch 'keyboard-event-selection' into keyboard-selection-annou…
oleksandr-danylchenko Sep 2, 2024
7b6e0bf
Merge branch 'keyboard-event-selection' into keyboard-selection-annou…
oleksandr-danylchenko Sep 3, 2024
b2c938f
Merge branch 'keyboard-event-selection' into keyboard-selection-annou…
oleksandr-danylchenko Sep 10, 2024
f6fc802
Merge branch 'keyboard-event-selection' into keyboard-selection-annou…
oleksandr-danylchenko Sep 26, 2024
89900a1
Merge branch 'main' into keyboard-selection-announcement
oleksandr-danylchenko Sep 30, 2024
f5c8f78
Merge branch 'main' into keyboard-selection-announcement
oleksandr-danylchenko Oct 7, 2024
37cdb13
Renamed `popupNavigationMessage` -> `ariaNavigationMessage`
oleksandr-danylchenko Oct 7, 2024
820c628
Added missing `useCallback` for the focus handlers
oleksandr-danylchenko Oct 10, 2024
a931b69
Merge branch 'main' into keyboard-selection-announcement
oleksandr-danylchenko Oct 29, 2024
0f0f8c3
Added proper event handlers population to the floating
oleksandr-danylchenko Oct 29, 2024
4d4347e
Merge branch 'main' into keyboard-selection-announcement
oleksandr-danylchenko Nov 4, 2024
c9a0727
Added spacing
oleksandr-danylchenko Nov 4, 2024
794d3b4
Merge branch 'main' into keyboard-selection-announcement
oleksandr-danylchenko Nov 11, 2024
9456ade
Removed react-specific pointer event usage
oleksandr-danylchenko Nov 11, 2024
a9fd42a
Merge branch 'main' into keyboard-selection-announcement
oleksandr-danylchenko Nov 21, 2024
f8029ae
Formatting
oleksandr-danylchenko Nov 21, 2024
d15ae61
Merge branch 'main' into keyboard-selection-announcement
oleksandr-danylchenko Dec 16, 2024
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
40 changes: 39 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions packages/text-annotator-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@
"@annotorious/core": "^3.0.14",
"@annotorious/react": "^3.0.14",
"@floating-ui/react": "^0.27.1",
"@react-aria/live-announcer": "^3.3.4",
"@recogito/text-annotator": "3.0.0-rc.53",
"@recogito/text-annotator-tei": "3.0.0-rc.53",
"CETEIcean": "^1.9.3"
"CETEIcean": "^1.9.3",
"unique-random": "^4.0.0"
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { FC, ReactNode, useCallback, useRef, useEffect, useMemo, useState } from 'react';

import { useAnnotator, useSelection } from '@annotorious/react';
import {
NOT_ANNOTATABLE_CLASS,
Expand All @@ -7,7 +8,6 @@ import {
type TextAnnotator,
} from '@recogito/text-annotator';

import { isMobile } from './isMobile';
import {
arrow,
autoUpdate,
Expand All @@ -25,10 +25,15 @@ import {
useRole
} from '@floating-ui/react';

import { isMobile } from './isMobile';
import { useAnnouncePopupNavigation } from '../hooks';

import './TextAnnotationPopup.css';

interface TextAnnotationPopupProps {

ariaNavigationMessage?: string;

ariaCloseWarning?: string;

arrow?: boolean;
Expand All @@ -55,7 +60,7 @@ const toViewportBounds = (annotationBounds: DOMRect, container: HTMLElement): DO
return new DOMRect(left + containerBounds.left, top + containerBounds.top, right - left, bottom - top);
}

export const TextAnnotationPopup = (props: TextAnnotationPopupProps) => {
export const TextAnnotationPopup: FC<TextAnnotationPopupProps> = (props) => {

const r = useAnnotator<TextAnnotator>();

Expand All @@ -64,6 +69,14 @@ export const TextAnnotationPopup = (props: TextAnnotationPopupProps) => {
const annotation = selected[0]?.annotation;

const [isOpen, setOpen] = useState(selected?.length > 0);
const handleClose = () => r?.cancelSelected();

const [isFocused, setFocused] = useState(false);
const handleFocus = useCallback(() => setFocused(true), []);
const handleBlur = useCallback(() => setFocused(false), []);
useEffect(() => {
if (!isOpen) handleBlur();
}, [isOpen, handleBlur]);

const arrowRef = useRef(null);

Expand All @@ -73,7 +86,7 @@ export const TextAnnotationPopup = (props: TextAnnotationPopupProps) => {
onOpenChange: (open, _event, reason) => {
if (!open && (reason === 'escape-key' || reason === 'focus-out')) {
setOpen(open);
r?.cancelSelected();
handleClose();
}
},
middleware: [
Expand Down Expand Up @@ -143,7 +156,15 @@ export const TextAnnotationPopup = (props: TextAnnotationPopupProps) => {
return (event?.type === 'keyup' || event?.type === 'contextmenu' || isMobile()) ? -1 : 0;
}, [event]);

const onClose = () => r?.cancelSelected();
/**
* Announce the navigation hint only on the keyboard selection,
* because the focus isn't shifted to the popup automatically then
*/
useAnnouncePopupNavigation({
disabled: isFocused,
floatingOpen: isOpen,
message: props.ariaNavigationMessage,
});

return isOpen && annotation ? (
<FloatingPortal>
Expand All @@ -157,21 +178,24 @@ export const TextAnnotationPopup = (props: TextAnnotationPopupProps) => {
className={`a9s-popup r6o-popup annotation-popup r6o-text-popup ${NOT_ANNOTATABLE_CLASS}`}
ref={refs.setFloating}
style={floatingStyles}
{...getFloatingProps(getStopEventsPropagationProps())}>
{...getFloatingProps({
onFocus: handleFocus,
onBlur: handleBlur,
...getStopEventsPropagationProps()
})}>
{props.popup({
annotation: selected[0].annotation,
editable: selected[0].editable,
event
})}

{props.arrow && (
<FloatingArrow
ref={arrowRef}
context={context}
{...(props.arrowProps || {})} />
)}

<button className="r6o-popup-sr-only" aria-live="assertive" onClick={onClose}>
<button className="r6o-popup-sr-only" aria-live="assertive" onClick={handleClose}>
{props.ariaCloseWarning || 'Click or leave this dialog to close it.'}
</button>
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/text-annotator-react/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useAnnouncePopupNavigation';
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';

import { Origin, useAnnotationStore, useSelection } from '@annotorious/react';
import type { TextAnnotation } from '@recogito/text-annotator';
import { announce, destroyAnnouncer } from '@react-aria/live-announcer';
import { exhaustiveUniqueRandom } from 'unique-random';

interface AnnouncePopupOpeningArgs {
message?: string;
idle?: number;
floatingOpen: boolean;
floatingFocused?: boolean;
disabled?: boolean;
}

export const useAnnouncePopupNavigation = (args: AnnouncePopupOpeningArgs) => {
const {
message = 'Press Tab to move to Notes Dialog',
idle = 700,
floatingOpen,
disabled = false
} = args;

const store = useAnnotationStore();
const { event } = useSelection<TextAnnotation>();

// Generate random numbers that do not repeat until the entire 1-10000 range has appeared
const uniqueRandom = useCallback(exhaustiveUniqueRandom(1, 10000), []);

/**
* Initialize the `LiveAnnouncer` class and
* its `polite` announcements live area
*/
useLayoutEffect(() => {
if (disabled || !message) return;

announce('', 'polite');
return () => destroyAnnouncer();
}, [disabled, message]);

/**
* Screen reader requires messages to always be unique!
* Otherwise, the hint will be announced once per page.
*/
const announcementSeed = useMemo(() => floatingOpen ? uniqueRandom() : 0, [floatingOpen]);

const announcePopupNavigation = useCallback(() => {
/**
* To imitate the uniqueness of the announced message
* w/o mutating it - we can append spaces at the end.
*/
const uniqueSpaces = Array.from({ length: announcementSeed }).map(() => ' ').join('');
announce(`${message} ${uniqueSpaces}`, 'polite');
}, [message, announcementSeed]);

const idleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

useEffect(() => {
if (disabled || !floatingOpen || !message) return;

const scheduleIdleAnnouncement = () => {
clearTimeout(idleTimeoutRef.current);
idleTimeoutRef.current = setTimeout(announcePopupNavigation, idle);
};

scheduleIdleAnnouncement();
store.observe(scheduleIdleAnnouncement, { origin: Origin.LOCAL });

return () => {
clearTimeout(idleTimeoutRef.current);
store.unobserve(scheduleIdleAnnouncement);
};
}, [disabled, floatingOpen, event?.type, message, announcePopupNavigation, store]);

};
1 change: 1 addition & 0 deletions packages/text-annotator-react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './tei';
export * from './hooks';
export * from './TextAnnotator';
export * from './TextAnnotationPopup';
export * from './TextAnnotatorPlugin';
Expand Down