Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<section>` 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:
Expand Down
13 changes: 11 additions & 2 deletions i18n/i18n.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions i18n/translations/en.reactodia-translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/coreUtils/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export const TranslationContext = React.createContext<Translation | null>(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;
}
Expand Down
3 changes: 2 additions & 1 deletion src/diagram/canvasArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ export function CanvasArea(props: {
<SvgPaperLayer layerRef={linkLayerRef}
className={`${CLASS_NAME}__linkGeometry`}
style={{overflow: 'visible'}}
paperTransform={paperTransform}>
paperTransform={paperTransform}
role='figure'>
<LinkMarkers model={model}
renderingState={renderingState}
/>
Expand Down
3 changes: 2 additions & 1 deletion src/diagram/elementLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ export class ElementLayer extends React.Component<ElementLayerProps, State> {
<HtmlPaperLayer key={version}
layerRef={layerRef}
className='reactodia-element-layer'
paperTransform={paperTransform}>
paperTransform={paperTransform}
role='figure'>
{elementsToRender.map(state => {
let overlaidElement = memoizedElements.get(state);
if (!overlaidElement) {
Expand Down
1 change: 1 addition & 0 deletions src/diagram/linkLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@ export function LinkLabelLayer(props: {
<HtmlPaperLayer paperTransform={paperTransform}
className='reactodia-label-layer'
layerRef={onMount}
role='figure'
/>
);
}
Expand Down
1 change: 1 addition & 0 deletions src/paper/paperLayers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export function SvgPaperLayer(props: SvgPaperLayerProps) {
width={scaledWidth}
height={scaledHeight}
style={svgStyle}
role='none'
{...otherProps}>
<g transform={`scale(${scale},${scale})translate(${originX},${originY})`}>
{children}
Expand Down
2 changes: 2 additions & 0 deletions src/widgets/linkAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}>
Expand Down
2 changes: 2 additions & 0 deletions src/widgets/selectionAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
3 changes: 2 additions & 1 deletion src/widgets/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ export function Toolbar(props: ToolbarProps) {
<ViewportDock dock={dock}
dockOffsetX={dockOffsetX}
dockOffsetY={dockOffsetY}>
<div className={cx(CLASS_NAME, dropUp ? `${CLASS_NAME}--drop-up` : undefined)}>
<div className={cx(CLASS_NAME, dropUp ? `${CLASS_NAME}--drop-up` : undefined)}
role='toolbar'>
{menu ? (
<DropdownMenu className={`${CLASS_NAME}__menu`}
direction={dropUp ? 'up' : 'down'}
Expand Down
4 changes: 4 additions & 0 deletions src/widgets/toolbarAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ export function ToolbarAction(props: ToolbarActionProps) {
return insideDropdown ? (
<DropdownMenuItem className={className}
title={titleWithHotkey}
aria-label={title}
aria-keyshortcuts={actionKey?.text}
disabled={disabled}
onSelect={onSelect}>
{children}
Expand All @@ -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}
Expand Down
19 changes: 15 additions & 4 deletions src/widgets/unifiedSearch/unifiedSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand All @@ -277,6 +277,7 @@ export function UnifiedSearch(props: UnifiedSearchProps) {
<Dropdown className={CLASS_NAME}
direction={direction}
expanded={expanded}
aria-keyshortcuts={actionFocus?.text}
onClickOutside={onClickOutside}
toggle={
<SearchToggle inputRef={toggleInputRef}
Expand Down Expand Up @@ -385,12 +386,14 @@ function SearchToggle(props: {
<div className={cx(`${CLASS_NAME}__toggle`)}
style={{
width: panelSize?.width,
}}>
}}
role='search'>
<input
ref={
/* For compatibility with React 19 typings */
inputRef as React.RefObject<HTMLInputElement>
}
role='searchbox'
type='text'
className={`${CLASS_NAME}__search-input`}
style={{minWidth}}
Expand Down Expand Up @@ -529,14 +532,17 @@ function SearchContent(props: {
}
};

const sectionPanelId = (section: SectionWithContext) =>
`reactodia-unified-search-panel-${section.key}`;

return (
<div ref={panelRef}
className={`${CLASS_NAME}__panel`}
style={{
width: size.width,
height: size.height,
}}>
<div className={`${CLASS_NAME}__section-tabs`}>
<div className={`${CLASS_NAME}__section-tabs`} role='tablist'>
{sections.map(section => (
<button key={section.key}
className={cx(
Expand All @@ -545,6 +551,10 @@ function SearchContent(props: {
'reactodia-btn-default',
section.key === activeSectionKey ? 'active' : undefined,
)}
role='tab'
aria-selected={section.key === activeSectionKey}
aria-controls={sectionPanelId(section)}
aria-label={section.title}
title={section.title}
onClick={() => onActivateSection(section.key)}>
{section.label}
Expand All @@ -554,7 +564,8 @@ function SearchContent(props: {
{sections.map(section => (
<UnifiedSearchSectionContext.Provider key={section.key}
value={section.context}>
<div
<div id={sectionPanelId(section)}
role='tabpanel'
className={cx(
`${CLASS_NAME}__section`,
section.context.isSectionActive
Expand Down
31 changes: 19 additions & 12 deletions src/widgets/utility/dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as React from 'react';
*
* @see {@link Dropdown}
*/
export interface DropdownProps {
export interface DropdownProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* Additional CSS class for the component.
*/
Expand Down Expand Up @@ -45,8 +45,11 @@ const CLASS_NAME = 'reactodia-dropdown';
* @category Components
*/
export function Dropdown(props: DropdownProps) {
const {className, direction = 'down', expanded, toggle, onClickOutside, children} = props;
const menuRef = React.useRef<HTMLElement | null>(null);
const {
className, direction = 'down', expanded, toggle, onClickOutside, children,
...otherProps
} = props;
const menuRef = React.useRef<HTMLDivElement | null>(null);

React.useLayoutEffect(() => {
if (onClickOutside && expanded) {
Expand All @@ -62,19 +65,20 @@ export function Dropdown(props: DropdownProps) {
}, [onClickOutside, expanded]);

return (
<nav ref={menuRef}
<div {...otherProps}
ref={menuRef}
className={cx(
className,
CLASS_NAME,
direction === 'down' ? `${CLASS_NAME}--down` : `${CLASS_NAME}--up`,
expanded ? `${CLASS_NAME}--expanded` : `${CLASS_NAME}--collapsed`
)}>
{direction === 'down' ? toggle : null}
<div className={`${CLASS_NAME}__content`}>
<div className={`${CLASS_NAME}__content`} aria-hidden={!expanded}>
{children}
</div>
{direction === 'up' ? toggle : null}
</nav>
</div>
);
}

Expand All @@ -83,7 +87,7 @@ export function Dropdown(props: DropdownProps) {
*
* @see {@link DropdownMenu}
*/
export interface DropdownMenuProps {
export interface DropdownMenuProps extends React.HTMLAttributes<HTMLElement> {
/**
* Additional CSS class for the component.
*/
Expand Down Expand Up @@ -112,7 +116,7 @@ const MENU_CLASS_NAME = 'reactodia-dropdown-menu';
* @category Components
*/
export function DropdownMenu(props: DropdownMenuProps) {
const {className, direction, title, children} = props;
const {className, direction, title, children, ...otherProps} = props;
const [expanded, setExpanded] = React.useState(false);
const providedContext = React.useMemo(
(): DropdownMenuContext => ({expanded, setExpanded}),
Expand All @@ -121,7 +125,9 @@ export function DropdownMenu(props: DropdownMenuProps) {
const onClickOutside = React.useCallback(() => setExpanded(false), [setExpanded]);
return (
<DropdownMenuContext.Provider value={providedContext}>
<Dropdown className={cx(className, MENU_CLASS_NAME)}
<Dropdown {...otherProps}
className={cx(className, MENU_CLASS_NAME)}
aria-haspopup='menu'
direction={direction}
expanded={expanded}
toggle={
Expand Down Expand Up @@ -175,7 +181,7 @@ function DropdownMenuToggleButton(props: { title?: string }) {
*
* @see {@link DropdownMenuItem}
*/
export interface DropdownMenuItemProps {
export interface DropdownMenuItemProps extends React.HTMLAttributes<HTMLElement> {
/**
* Additional CSS class for the component.
*/
Expand Down Expand Up @@ -208,7 +214,7 @@ const ITEM_CLASS_NAME = 'reactodia-dropdown-menu-item';
* @category Components
*/
export function DropdownMenuItem(props: DropdownMenuItemProps) {
const {className, title, disabled, onSelect, children} = props;
const {className, title, disabled, onSelect, children, ...otherProps} = props;
const menuContext = useDropdownMenu();

const wrappedOnClick = React.useCallback(() => {
Expand All @@ -217,7 +223,8 @@ export function DropdownMenuItem(props: DropdownMenuItemProps) {
}, [onSelect, menuContext]);

return (
<li role='menuitem'
<li {...otherProps}
role='menuitem'
className={cx(
className,
ITEM_CLASS_NAME,
Expand Down
3 changes: 2 additions & 1 deletion src/widgets/utility/searchInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ export function SearchInput(props: SearchInputProps) {
const mode = useObservedProperty(store.events, 'changeMode', () => store.mode);

return (
<div
<div role='search'
className={cx(
CLASS_NAME,
mode === 'explicit' ? `${CLASS_NAME}--has-submit` : undefined,
className
)}>
<input {...inputProps}
role='searchbox'
type={inputProps.type ?? 'text'}
className={cx(
`${CLASS_NAME}__input`,
Expand Down
2 changes: 1 addition & 1 deletion src/widgets/zoomControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function ZoomControl(props: ZoomControlProps) {
<ViewportDock dock={dock}
dockOffsetX={dockOffsetX}
dockOffsetY={dockOffsetY}>
<div className={CLASS_NAME}>
<div className={CLASS_NAME} role='toolbar'>
<button type='button'
className={cx(
`${CLASS_NAME}__zoom-in-button`,
Expand Down
Loading
Loading