A11y: ARIA-Semantik (#164) und Reflow 320px (#165) #173

Merged
stwaidele merged 2 commits from a11y-aria into main 2026-07-21 16:29:20 +02:00
8 changed files with 45 additions and 7 deletions
Showing only changes of commit 057992faaf - Show all commits

View File

@ -121,7 +121,7 @@ export function MentionAutocomplete({ editor }: { editor: Editor }): React.JSX.E
style={{ position: 'fixed', left: state.coords.left, top: state.coords.bottom + 4 }} style={{ position: 'fixed', left: state.coords.left, top: state.coords.bottom + 4 }}
> >
{suggestions.map((user, index) => ( {suggestions.map((user, index) => (
<li key={user.id}> <li key={user.id} role="presentation">
<button <button
type="button" type="button"
role="option" role="option"

View File

@ -80,6 +80,22 @@ function ImageInsertButton({
); );
} }
/** Arrow-key navigation between toolbar controls (#164, role="toolbar"
* convention). Native selects keep their own arrow-key handling. */
function onToolbarArrowKey(event: React.KeyboardEvent<HTMLDivElement>): void {
if (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft') return;
const target = event.target as HTMLElement;
if (target.tagName === 'SELECT' || target.tagName === 'INPUT') return;
const controls = [
...event.currentTarget.querySelectorAll<HTMLElement>('button:not([disabled]), select'),
];
const index = controls.indexOf(target);
if (index === -1) return;
event.preventDefault();
const step = event.key === 'ArrowRight' ? 1 : -1;
controls[(index + step + controls.length) % controls.length]?.focus();
}
/** Keyboard-accessible toolbar for the page editor (issue #25). Table row/ /** Keyboard-accessible toolbar for the page editor (issue #25). Table row/
* column controls stay visible but disabled outside a table, so the * column controls stay visible but disabled outside a table, so the
* toolbar's layout and tab order never shift while typing. */ * toolbar's layout and tab order never shift while typing. */
@ -118,7 +134,12 @@ export function Toolbar({
}); });
return ( return (
<div className="editor-toolbar" role="toolbar" aria-label={t('toolbar.paragraph')}> <div
className="editor-toolbar"
role="toolbar"
aria-label={t('toolbar.label')}
onKeyDown={onToolbarArrowKey}
>
<div className="editor-toolbar__group"> <div className="editor-toolbar__group">
<ToolbarButton <ToolbarButton
label={t('toolbar.paragraph')} label={t('toolbar.paragraph')}

View File

@ -158,7 +158,7 @@ export function WikilinkAutocomplete({ editor }: { editor: Editor }): React.JSX.
style={{ position: 'fixed', left: state.coords.left, top: state.coords.bottom + 4 }} style={{ position: 'fixed', left: state.coords.left, top: state.coords.bottom + 4 }}
> >
{suggestions.map((item, index) => ( {suggestions.map((item, index) => (
<li key={`${item.kind}:${item.slug}`}> <li key={`${item.kind}:${item.slug}`} role="presentation">
<button <button
type="button" type="button"
role="option" role="option"

View File

@ -64,6 +64,7 @@ export function Sidebar({ collapsed }: SidebarProps): React.JSX.Element {
<nav <nav
className={collapsed ? 'sidebar sidebar--collapsed' : 'sidebar'} className={collapsed ? 'sidebar sidebar--collapsed' : 'sidebar'}
aria-hidden={collapsed} aria-hidden={collapsed}
inert={collapsed}
aria-label={t('layout.sidebar.label')} aria-label={t('layout.sidebar.label')}
> >
{!pond.data ? ( {!pond.data ? (

View File

@ -209,7 +209,19 @@ function PageEditor({
useLayoutEffect(() => { useLayoutEffect(() => {
editor?.setEditable(canEdit); editor?.setEditable(canEdit);
}, [editor, canEdit]); // TipTap rendert die Fläche als role="textbox" — ohne Namen, und im
// Lesemodus ist "Textfeld" schlicht falsch (#164, WCAG 4.1.2). Im
// Lesemodus wird sie zum benannten Dokument-Bereich.
editor?.setOptions({
editorProps: {
attributes: {
role: canEdit ? 'textbox' : 'document',
'aria-label': t('contentLabel'),
...(canEdit ? { 'aria-multiline': 'true' } : {}),
},
},
});
}, [editor, canEdit, t]);
// Entering edit mode drops the caret straight into the content, so writing // Entering edit mode drops the caret straight into the content, so writing
// starts without an extra — and on empty pages pixel-precise — click. Skip // starts without an extra — and on empty pages pixel-precise — click. Skip

View File

@ -208,7 +208,7 @@ export function SearchPalette({ onClose }: { onClose: () => void }): React.JSX.E
) : ( ) : (
<ul className="search-results" role="listbox" aria-label={t('resultsLabel')}> <ul className="search-results" role="listbox" aria-label={t('resultsLabel')}>
{hits.map((hit, index) => ( {hits.map((hit, index) => (
<li key={hit.pageId}> <li key={hit.pageId} role="presentation">
<button <button
type="button" type="button"
role="option" role="option"

View File

@ -38,6 +38,7 @@
"discardConfirm": "Die auf diesem Gerät gespeicherten Änderungen verwerfen? Das kann nicht rückgängig gemacht werden." "discardConfirm": "Die auf diesem Gerät gespeicherten Änderungen verwerfen? Das kann nicht rückgängig gemacht werden."
}, },
"toolbar": { "toolbar": {
"label": "Formatierung",
"paragraph": "Absatz", "paragraph": "Absatz",
"heading1": "Überschrift 1", "heading1": "Überschrift 1",
"heading2": "Überschrift 2", "heading2": "Überschrift 2",
@ -189,5 +190,6 @@
"dateMarker": { "dateMarker": {
"due": "Zieldatum", "due": "Zieldatum",
"start": "Startdatum" "start": "Startdatum"
} },
"contentLabel": "Seiteninhalt"
} }

View File

@ -38,6 +38,7 @@
"discardConfirm": "Discard the changes saved on this device? This cannot be undone." "discardConfirm": "Discard the changes saved on this device? This cannot be undone."
}, },
"toolbar": { "toolbar": {
"label": "Formatting",
"paragraph": "Paragraph", "paragraph": "Paragraph",
"heading1": "Heading 1", "heading1": "Heading 1",
"heading2": "Heading 2", "heading2": "Heading 2",
@ -189,5 +190,6 @@
"dateMarker": { "dateMarker": {
"due": "Due date", "due": "Due date",
"start": "Start date" "start": "Start date"
} },
"contentLabel": "Page content"
} }