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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ All notable changes to the Reactodia will be documented in this document.
The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
#### 🚀 New Features
- Make tree- and list-like components more accessible:
* Change `ClassTree`, `InstancesSearch`, `ConnectionsMenu` and `SearchResults` to be "focus group" components with support for keyboard interaction (arrow keys to move focus or toggle tree items, space to select);
* Add proper `aria-*` attributes for "focus group" containers and children e.g. `tree` and `treeitem` roles;

#### 🐛 Fixed
- Fix partially or fully hidden outlines for `WorkspaceLayoutItem` headers and `Navigator` toggle button.

#### 💅 Polish
- Allow to configure `SearchResults` utility component with `isItemDisabled` and `multiSelection` props:
* Remove `singleSelectOnClick` mode from `SearchResults` as it mostly superseded by `multiSelection`.
- Extend `ListElementView` utility component to accept any other additional HTML props.
- Always display ungroup buttons on `StandardGroup` when the element is single-selected.

## [0.34.1] - 2026-03-30
#### 🐛 Fixed
Expand Down
77 changes: 77 additions & 0 deletions src/coreUtils/dom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
export function findNextWithin(
from: Element,
parent: Element,
condition: (element: Element) => boolean
): Element | undefined {
let current: Element = from;
let allowDescent = true;
do {
while (true) {
if (allowDescent && current.firstElementChild) {
current = current.firstElementChild;
break;
} else if (current.nextElementSibling) {
current = current.nextElementSibling;
allowDescent = true;
break;
} else if (current.parentElement === parent) {
if (parent.firstElementChild) {
current = parent.firstElementChild;
allowDescent = true;
break;
} else {
return undefined;
}
} else if (current.parentElement) {
current = current.parentElement;
allowDescent = false;
} else {
return undefined;
}
}

if (condition(current)) {
return current;
}
} while (current !== from);
}

export function findPreviousWithin(
from: Element,
parent: Element,
condition: (element: Element) => boolean
): Element | undefined {
let current: Element = from;
let descent = false;
do {
while (true) {
if (descent) {
if (current.lastElementChild) {
current = current.lastElementChild;
} else {
descent = false;
break;
}
} else if (current.previousElementSibling) {
current = current.previousElementSibling;
descent = true;
} else if (current.parentElement === parent) {
if (parent.lastElementChild) {
current = parent.lastElementChild;
descent = true;
} else {
return undefined;
}
} else if (current.parentElement) {
current = current.parentElement;
break;
} else {
return undefined;
}
}

if (condition(current)) {
return current;
}
} while (current !== from);
}
62 changes: 28 additions & 34 deletions src/widgets/classTree/classTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ import {
} from '../../workspace/commandBusTopic';
import { WorkspaceContext, useWorkspace } from '../../workspace/workspaceContext';

import { TreeNode } from './treeModel';
import { ClassTreeContext, Forest } from './leaf';
import { ClassTreeResults, type ClassTreeSelection, TreeNode } from './classTreeResults';

/**
* Props for {@link ClassTree} component.
Expand Down Expand Up @@ -132,7 +131,7 @@ interface State {
roots: ReadonlyArray<TreeNode>;
filteredRoots: ReadonlyArray<TreeNode>;
appliedSearchText?: string;
selectedNode?: TreeNode;
selection?: ClassTreeSelection;
constructibleClasses: ReadonlyMap<ElementTypeIri, boolean>;
showOnlyConstructible: boolean;
}
Expand Down Expand Up @@ -177,7 +176,7 @@ class ClassTreeInner extends React.Component<ClassTreeInnerProps, State> {
draggableItems = true, workspace: {editor}, translation: t,
} = this.props;
const {
fetchedGraph, refreshingState, appliedSearchText, roots, filteredRoots, selectedNode,
fetchedGraph, refreshingState, appliedSearchText, roots, filteredRoots, selection,
constructibleClasses, showOnlyConstructible
} = this.state;
// highlight search term only if actual tree is already filtered by current or previous term:
Expand Down Expand Up @@ -217,35 +216,30 @@ class ClassTreeInner extends React.Component<ClassTreeInnerProps, State> {
title={t.text('search_element_types.refresh_progress.title')}
/>
{fetchedGraph?.classTree ? (
<ClassTreeContext.Provider
value={{
searchText,
selectedNode,
onSelect: this.onSelectNode,
creatableClasses: editor.inAuthoringMode
? constructibleClasses : EMPTY_CREATABLE_TYPES,
onClickCreate: this.onCreateInstance,
onDragCreate: this.onDragCreate,
draggableItems,
}}>
<Forest className={`${CLASS_NAME}__tree reactodia-scrollable`}
nodes={filteredRoots}
root={true}
footer={
filteredRoots.length === 0 ? (
<NoSearchResults className={`${CLASS_NAME}__no-results`}
hasQuery={filteredRoots !== roots}
minSearchTermLength={minSearchTermLength}
message={
roots.length === 0
? t.text('search_element_types.no_results')
: undefined
}
/>
) : null
<div className={`${CLASS_NAME}__tree reactodia-scrollable`} tabIndex={-1}>
<ClassTreeResults nodes={filteredRoots}
searchText={searchText}
selection={selection}
onSelect={this.onSelectNode}
creatableClasses={
editor.inAuthoringMode ? constructibleClasses : EMPTY_CREATABLE_TYPES
}
onClickCreate={this.onCreateInstance}
onDragCreate={this.onDragCreate}
draggableItems={draggableItems}
/>
</ClassTreeContext.Provider>
{filteredRoots.length === 0 ? (
<NoSearchResults className={`${CLASS_NAME}__no-results`}
hasQuery={filteredRoots !== roots}
minSearchTermLength={minSearchTermLength}
message={
roots.length === 0
? t.text('search_element_types.no_results')
: undefined
}
/>
) : null}
</div>
) : (
<div className={`${CLASS_NAME}__spinner`}>
<HtmlSpinner width={30} height={30}
Expand Down Expand Up @@ -349,12 +343,12 @@ class ClassTreeInner extends React.Component<ClassTreeInnerProps, State> {
));
};

private onSelectNode = (node: TreeNode) => {
private onSelectNode = (selection: ClassTreeSelection) => {
const {workspace: {getCommandBus}} = this.props;
this.setState({selectedNode: node}, () => {
this.setState({selection}, () => {
getCommandBus(InstancesSearchTopic)
.trigger('setCriteria', {
criteria: {elementType: node.iri},
criteria: {elementType: selection.node.iri},
});
});
};
Expand Down
Loading
Loading