From ba3434fb6be9484888e13eb011c4069a5de508f4 Mon Sep 17 00:00:00 2001 From: Alexey Morozov Date: Sat, 1 Nov 2025 02:37:33 +0300 Subject: [PATCH] Support keyboard hotkeys for `LinkAction` components: * Fix being able to execute disabled `SelectionAction` via keyboard hotkey. --- CHANGELOG.md | 4 +++- src/editor/overlayController.tsx | 6 ++++-- src/widgets/linkAction.tsx | 32 +++++++++++++++++++++++++++----- src/widgets/selectionAction.tsx | 2 +- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e31f86ba..f5193234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p * Resizable elements display "box with handles" in the `Halo` to change the size; * Changed element sizes are captured and restored by `RestoreGeometry` command. - Allow to customize link template separately for each link instead of only based on its link type IRI in `linkTemplateResolver` for `Canvas`. -- Add `EditorController.applyAuthoringChanges()` method to apply current authoring changes to the diagram (i.e. change entity data, delete relations, etc) and reset the change state to be empty. +- Support keyboard hotkeys for `LinkAction` components to act on a currently selected link. #### ⏱ Performance - **[💥Breaking]** Canvas widgets are not automatically updated when parent canvas is rendered to reduce unnecessary re-renders, and now require explicit subscriptions: @@ -51,12 +51,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p * Change CSS class for default link template from `reactodia-default-link` to `reactodia-standard-link`. - Move "expand/collapse on double click" global element behavior to `StandardEntity` and `ClassicEntity` implementation only. - Add `setTemplateProperty()` utility function to easily set or unset template state property. +- Add `EditorController.applyAuthoringChanges()` method to apply current authoring changes to the diagram (i.e. change entity data, delete relations, etc) and reset the change state to be empty. #### 🐛 Fixed - Fix `HaloLink` and visual authoring link path highlight being rendered on top on elements by placing it onto `overLinkGeometry` widget layer instead. - Fix element template state not being restored when ungrouping entities. - Fix missing element decorations after re-importing the same diagram. - Fix `DraggableHandle` to avoid using stale `onDragHandle` and `onEndDragHandle` prop values. +- Fix being able to execute disabled `SelectionAction` via keyboard hotkey. #### 🔧 Maintenance - Use small subset of [carbon design icons](https://github.com/carbon-design-system/carbon-icons) for various buttons. diff --git a/src/editor/overlayController.tsx b/src/editor/overlayController.tsx index 6083efe6..6e84b759 100644 --- a/src/editor/overlayController.tsx +++ b/src/editor/overlayController.tsx @@ -146,8 +146,10 @@ export class OverlayController { target.focus(); } else if (target instanceof Link) { this.model.setSelection([target]); + canvas.focus(); } else if (target instanceof LinkVertex) { this.model.setSelection([target.link]); + canvas.focus(); } else if (!target && triggerAsClick) { this.model.setSelection([]); this.hideDialog(); @@ -432,8 +434,8 @@ interface OverlayWithInternalApi { interface OverlayControllerInternalApi { readonly events: Events; readonly overlays: Set; - onCanvasPointerUp(event: CanvasPointerUpEvent): void; - onCanvasKeydown(event: CanvasKeyboardEvent): void; + readonly onCanvasPointerUp: (event: CanvasPointerUpEvent) => void; + readonly onCanvasKeydown: (event: CanvasKeyboardEvent) => void; dialog: React.ReactElement | null; spinner: React.ReactElement | null; } diff --git a/src/widgets/linkAction.tsx b/src/widgets/linkAction.tsx index 78b21592..8d84ba07 100644 --- a/src/widgets/linkAction.tsx +++ b/src/widgets/linkAction.tsx @@ -5,10 +5,12 @@ import { mapAbortedToNull } from '../coreUtils/async'; import { useEventStore, useObservedProperty, useFrameDebouncedStore, useSyncStore, } from '../coreUtils/hooks'; +import type { HotkeyString } from '../coreUtils/hotkey'; import type { MetadataCanModifyRelation } from '../data/metadataProvider'; import { useCanvas } from '../diagram/canvasApi'; +import { useCanvasHotkey } from '../diagram/canvasHotkey'; import { Link } from '../diagram/elements'; import { GraphStructure } from '../diagram/model'; import { HtmlSpinner } from '../diagram/spinner'; @@ -88,6 +90,12 @@ export interface LinkActionStyleProps { * Title for the action button. */ title?: string; + /** + * Keyboard hotkey for the action when it's mounted. + * + * Passing `null` disables a default hotkey if there is one. + */ + hotkey?: HotkeyString | null; } /** @@ -122,14 +130,21 @@ const CLASS_NAME = 'reactodia-link-action'; * @category Components */ export function LinkAction(props: LinkActionProps) { - const {dockSide, dockIndex, disabled, className, title, onSelect, onMouseDown, children} = props; + const { + dockSide, dockIndex, disabled, className, title, hotkey, + onSelect, onMouseDown, children, + } = props; + const {getPosition} = useLinkActionContext(); + const actionKey = useCanvasHotkey(hotkey, disabled ? undefined : onSelect); + const titleWithHotkey = title && actionKey ? `${title} (${actionKey.text})` : title; + return (