From 2ee61fca87d3d546d20f66fbaf1e14070e9c9d1e Mon Sep 17 00:00:00 2001 From: Alexey Morozov Date: Sun, 29 Mar 2026 04:38:18 +0300 Subject: [PATCH] Improve ARIA-attributes and other accessibility interaction: * Allow to resize and toggle `WorkspaceLayout*` with a keyboard; * Change `WorkspaceLayout*` to be `
` elements with `aria-label` (i.e. regions); * Add `role` for `Canvas` layers, `Toolbar`, `ZoomControl`, `UnifiedSearch` (and other search inputs); * Add `aria-label` and `aria-keyshortcuts` for `ToolbarAction`, `SelectionAction`, `LinkAction`. --- CHANGELOG.md | 7 ++ i18n/i18n.schema.json | 13 ++- .../en.reactodia-translation.json | 9 +- src/coreUtils/i18n.tsx | 2 +- src/diagram/canvasArea.tsx | 3 +- src/diagram/elementLayer.tsx | 3 +- src/diagram/linkLayer.tsx | 1 + src/paper/paperLayers.tsx | 1 + src/widgets/linkAction.tsx | 2 + src/widgets/selectionAction.tsx | 2 + src/widgets/toolbar.tsx | 3 +- src/widgets/toolbarAction.tsx | 4 + src/widgets/unifiedSearch/unifiedSearch.tsx | 19 +++- src/widgets/utility/dropdown.tsx | 31 ++++--- src/widgets/utility/searchInput.tsx | 3 +- src/widgets/zoomControl.tsx | 2 +- src/workspace/accordionItem.tsx | 93 ++++++++++++++----- src/workspace/classicWorkspace.tsx | 2 +- src/workspace/workspaceLayout.tsx | 24 ++++- styles/workspace/_accordion.scss | 18 ++-- 20 files changed, 182 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04600dc7..dec96753 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Fix stale/non-saved data when calling `applyChanges()` immediately after `updateData()` in `EntityEditor` and `RelationEditor`. - Fix `SelectionAction*` components not updating on selection change when provided as children to `Halo`. +#### 💅 Polish +- Improve ARIA-attributes and other accessibility interaction: + * Allow to resize and toggle `WorkspaceLayout*` with a keyboard; + * Change `WorkspaceLayout*` to be `
` elements with `aria-label` (i.e. regions); + * Add `role` for `Canvas` layers, `Toolbar`, `ZoomControl`, `UnifiedSearch` (and other search inputs); + * Add `aria-label` and `aria-keyshortcuts` for `ToolbarAction`, `SelectionAction`, `LinkAction`. + ## [0.34.0] - 2026-03-25 #### 🚀 New Features - Support proper graph manipulation on touchscreen devices: diff --git a/i18n/i18n.schema.json b/i18n/i18n.schema.json index 698c5535..453cadf3 100644 --- a/i18n/i18n.schema.json +++ b/i18n/i18n.schema.json @@ -34,9 +34,10 @@ "$ref": "#/$defs/Group", "additionalProperties": false, "properties": { + "canvas.label": { "$ref": "#/$defs/Value" }, "class_tree.heading": { "$ref": "#/$defs/Value" }, - "instances.heading": { "$ref": "#/$defs/Value" }, - "connections.heading": { "$ref": "#/$defs/Value" } + "connections.heading": { "$ref": "#/$defs/Value" }, + "instances.heading": { "$ref": "#/$defs/Value" } } }, "commands": { @@ -446,6 +447,14 @@ "ungroup_entities.command": { "$ref": "#/$defs/Value" } } }, + "workspace_layout": { + "$ref": "#/$defs/Group", + "additionalProperties": false, + "properties": { + "toggle_collapse.title": { "$ref": "#/$defs/Value" }, + "toggle_expand.title": { "$ref": "#/$defs/Value" } + } + }, "zoom_control": { "$ref": "#/$defs/Group", "additionalProperties": false, diff --git a/i18n/translations/en.reactodia-translation.json b/i18n/translations/en.reactodia-translation.json index ffef204e..1be1f2e6 100644 --- a/i18n/translations/en.reactodia-translation.json +++ b/i18n/translations/en.reactodia-translation.json @@ -19,9 +19,10 @@ "relation_delete.label": "Delete" }, "classic_workspace": { + "canvas.label": "Canvas", "class_tree.heading": "Classes", - "instances.heading": "Instances", - "connections.heading": "Connections" + "connections.heading": "Connections", + "instances.heading": "Instances" }, "commands": { "change_entity.title": "Change entity data", @@ -323,6 +324,10 @@ "perform_layout.command": "Graph layout", "ungroup_entities.command": "Ungroup entities" }, + "workspace_layout": { + "toggle_collapse.title": "Collapse panel", + "toggle_expand.title": "Expand panel" + }, "zoom_control": { "pointer_mode.title": "Toggle selection mode:\nDrag pointer to select, Shift + Pointer to pan the canvas", "zoom_in.title": "Zoom In", diff --git a/src/coreUtils/i18n.tsx b/src/coreUtils/i18n.tsx index e8d939fa..3106aaa0 100644 --- a/src/coreUtils/i18n.tsx +++ b/src/coreUtils/i18n.tsx @@ -188,7 +188,7 @@ export const TranslationContext = React.createContext(null); export function useTranslation(): Translation { const translation = React.useContext(TranslationContext); if (!translation) { - throw new Error('Missing Reactodia translation context'); + throw new Error('Reactodia: missing translation context'); } return translation; } diff --git a/src/diagram/canvasArea.tsx b/src/diagram/canvasArea.tsx index 49054c0a..48e6262b 100644 --- a/src/diagram/canvasArea.tsx +++ b/src/diagram/canvasArea.tsx @@ -135,7 +135,8 @@ export function CanvasArea(props: { + paperTransform={paperTransform} + role='figure'> diff --git a/src/diagram/elementLayer.tsx b/src/diagram/elementLayer.tsx index 7ffeeffd..cb7312af 100644 --- a/src/diagram/elementLayer.tsx +++ b/src/diagram/elementLayer.tsx @@ -94,7 +94,8 @@ export class ElementLayer extends React.Component { + paperTransform={paperTransform} + role='figure'> {elementsToRender.map(state => { let overlaidElement = memoizedElements.get(state); if (!overlaidElement) { diff --git a/src/diagram/linkLayer.tsx b/src/diagram/linkLayer.tsx index dc1d28ef..449f5a9e 100644 --- a/src/diagram/linkLayer.tsx +++ b/src/diagram/linkLayer.tsx @@ -435,6 +435,7 @@ export function LinkLabelLayer(props: { ); } diff --git a/src/paper/paperLayers.tsx b/src/paper/paperLayers.tsx index 20098b13..7b0305bc 100644 --- a/src/paper/paperLayers.tsx +++ b/src/paper/paperLayers.tsx @@ -100,6 +100,7 @@ export function SvgPaperLayer(props: SvgPaperLayerProps) { width={scaledWidth} height={scaledHeight} style={svgStyle} + role='none' {...otherProps}> {children} diff --git a/src/widgets/linkAction.tsx b/src/widgets/linkAction.tsx index b0814860..213e5435 100644 --- a/src/widgets/linkAction.tsx +++ b/src/widgets/linkAction.tsx @@ -152,6 +152,8 @@ export function LinkAction(props: LinkActionProps) { style={getPosition(dockSide, dockIndex)} disabled={disabled} title={titleWithHotkey} + aria-label={title} + aria-keyshortcuts={actionKey?.text} onClick={onSelect} onMouseDown={onMouseDown} onPointerDown={onPointerDown}> diff --git a/src/widgets/selectionAction.tsx b/src/widgets/selectionAction.tsx index b2e26445..db33357e 100644 --- a/src/widgets/selectionAction.tsx +++ b/src/widgets/selectionAction.tsx @@ -124,6 +124,8 @@ export function SelectionAction(props: SelectionActionProps) { className={cx(CLASS_NAME, getDockClass(dock), className)} style={getDockStyle(dockRow, dockColumn)} title={titleWithHotkey} + aria-label={title} + aria-keyshortcuts={actionKey?.text} disabled={disabled} onClick={onSelect} onMouseDown={onMouseDown} diff --git a/src/widgets/toolbar.tsx b/src/widgets/toolbar.tsx index 725e6b60..7a8ba801 100644 --- a/src/widgets/toolbar.tsx +++ b/src/widgets/toolbar.tsx @@ -55,7 +55,8 @@ export function Toolbar(props: ToolbarProps) { -
+
{menu ? ( {children} @@ -93,6 +95,8 @@ export function ToolbarAction(props: ToolbarActionProps) { 'reactodia-btn reactodia-btn-default' )} title={titleWithHotkey} + aria-label={title} + aria-keyshortcuts={actionKey?.text} disabled={disabled} onClick={onSelect}> {children} diff --git a/src/widgets/unifiedSearch/unifiedSearch.tsx b/src/widgets/unifiedSearch/unifiedSearch.tsx index 2f6a4563..afb7d138 100644 --- a/src/widgets/unifiedSearch/unifiedSearch.tsx +++ b/src/widgets/unifiedSearch/unifiedSearch.tsx @@ -260,7 +260,7 @@ export function UnifiedSearch(props: UnifiedSearchProps) { return () => commands.off('focus', onFocus); }, [commands, onFocus]); - useCanvasHotkey(hotkeyFocus, () => onFocus({})); + const actionFocus = useCanvasHotkey(hotkeyFocus, () => onFocus({})); const hasSearchQuery = ( searchTerm.length > 0 || @@ -277,6 +277,7 @@ export function UnifiedSearch(props: UnifiedSearchProps) { + }} + role='search'> } + role='searchbox' type='text' className={`${CLASS_NAME}__search-input`} style={{minWidth}} @@ -529,6 +532,9 @@ function SearchContent(props: { } }; + const sectionPanelId = (section: SectionWithContext) => + `reactodia-unified-search-panel-${section.key}`; + return (
-
+
{sections.map(section => ( + ) : null}
{children && isMounted ? children @@ -102,7 +103,10 @@ export class AccordionItem extends React.Component {
{shouldRenderHandle ? ( - { this.setState({resizing: true}); onBeginDragHandle(); @@ -111,10 +115,55 @@ export class AccordionItem extends React.Component { onEndDragHandle={e => { this.setState({resizing: false}); onEndDragHandle(); - }}/> + }} + onKeyDown={e => { + const step = AccordionItem.ARROW_SHIFT_STEP; + let shift = 0; + + if (this.isVertical) { + if (e.key === 'ArrowUp') { + shift -= step; + } else if (e.key === 'ArrowDown') { + shift += step; + } + } else { + if (e.key === 'ArrowLeft') { + shift -= step; + } else if (e.key === 'ArrowRight') { + shift += step; + } + } + + if (shift !== 0) { + e.preventDefault(); + onBeginDragHandle(); + onDragHandle(shift, shift); + onEndDragHandle(); + } + }} + /> ) : null} {this.renderToggleButton()} -
+
+ ); + } + + private renderToggleButton() { + const { + collapsed, dockSide, titleDockExpand, titleDockCollapse, onChangeCollapsed, + } = this.props; + if (!dockSide) { + return null; + } + const side = dockSide === DockSide.Left ? 'left' : 'right'; + const label = collapsed ? titleDockExpand : titleDockCollapse; + return ( +