Skip to content

dbeaver/cloudbeaver#4159 Add tree keyboard expand and open actions - #4658

Closed
sergeyteleshev wants to merge 10 commits into
develfrom
dbeaver/cloudbeaver#4159-keyboard-navigation
Closed

sergeyteleshev wants to merge 10 commits into
develfrom
dbeaver/cloudbeaver#4159-keyboard-navigation

Conversation

@sergeyteleshev

@sergeyteleshev sergeyteleshev commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Closes #4159

Summary

  • Delegate keyboard actions to one keydown listener per tree using a shared hook, connected to both ElementsTree and TreeNew.
  • Register node contexts through refs instead of creating focus observers and hotkey hooks for every node.
  • Right expands collapsed branches or opens leaves; Enter toggles branches or opens leaves.
  • Left collapses expanded branches or focuses the parent, staying in place at the top level and respecting nested tree boundaries.
  • Propagate the TreeNew leaf state so Enter and Right open leaves instead of expanding them.
  • Preserve nested input handling, modified Enter selection, loading/processing guards, and list navigation for unhandled keys.
  • Cover keyboard behavior, listener lifecycle, parent navigation, nested trees, and TreeNew leaf actions with regression tests.

Supersedes #4640.

Validation

  • yarn workspace @cloudbeaver/plugin-navigation-tree build passed, including core-blocks dependencies.
  • yarn workspace @cloudbeaver/core-blocks test: 171 passed, 17 skipped.
  • yarn workspace @cloudbeaver/plugin-navigation-tree test: 2 passed; one existing suite skipped.
  • Focused ESLint and formatting checks passed for the keyboard implementation and tests. Broader lint reports existing errors in barrel exports and container interface names.
  • git diff --check passed.
  • Regression checks failed before the parent-navigation fix and passed afterward.

Companion PR

https://github.com/dbeaver/cloudbeaver-ee/pull/2679

@sergeyteleshev sergeyteleshev linked an issue Sep 14, 2026 that may be closed by this pull request
@sergeyteleshev
sergeyteleshev requested review from SychevAndrey and devnaumov and a lite review from Copilot September 14, 2026 17:45
@codacy-production

codacy-production Bot commented Sep 14, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 49 complexity

Metric Results
Complexity 49

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

TreeNew leaf state must be propagated to avoid incorrect expand behavior; add coverage for TreeNew leaves.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds keyboard expand, collapse, and open actions to the shared tree node control while preserving navigation and input handling.

Changes:

  • Adds focus-scoped ArrowRight, ArrowLeft, and Enter handling.
  • Covers keyboard actions and edge cases with 14 regression tests.
File summaries
File Summary
webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.tsx Implements shared keyboard tree actions.
webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.test.tsx Tests keyboard interactions, focus, navigation, and unavailable states.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +91 to +95
const expand =
!context.leaf &&
!context.externalExpanded &&
(event.key === 'Enter' || (event.key === 'ArrowRight' && !context.expanded) || (event.key === 'ArrowLeft' && context.expanded));
const open = context.leaf && (event.key === 'Enter' || event.key === 'ArrowRight');
const innerRef = useRef<HTMLDivElement>(null);
const mergedRef = useMergeRefs(innerRef, ref);
const [focusRef, focusState] = useFocus<HTMLDivElement>({});
const hotkeysRef = useHotkeys<HTMLDivElement>('ArrowRight,ArrowLeft,Enter', handleKeyboardAction, { enabled: focusState.focus, useKey: true });

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think more clear approach would be to define callback separately and move logic to hook

function useTreeNodeKeyboardActions(
  context: ITreeNodeContext,
  innerRef: React.RefObject<HTMLDivElement | null>,
  enabled: boolean,
) {
  const options = { enabled, useKey: true };

  useHotkeys('ArrowRight', handleArrowRight, options);
  useHotkeys('ArrowLeft', handleArrowLeft, options);
  useHotkeys('Enter', handleEnter, options);

  function handleArrowRight(event: KeyboardEvent) {
    if (context.leaf) {
      return handleAction(event, context.open);
    }

    if (!context.expanded && !context.externalExpanded) {
      return handleAction(event, context.expand);
    }
  }

  function handleArrowLeft(event: KeyboardEvent) {
    if (context.expanded && !context.leaf && !context.externalExpanded) {
      return handleAction(event, context.expand);
    }
  }

  function handleEnter(event: KeyboardEvent) {
    if (context.leaf) {
      return handleAction(event, context.open);
    }

    if (!context.externalExpanded) {
      return handleAction(event, context.expand);
    }
  }

  async function handleAction(event: KeyboardEvent, action: () => Promise<void>) {
    if (
      event.target !== innerRef.current ||
      event.defaultPrevented ||
      EventContext.has(event, EventKeyboardNavigationFlag, EventStopPropagationFlag)
    ) {
      return;
    }

    EventContext.set(event, EventKeyboardNavigationFlag);
    EventContext.set(event, EventTreeNodeExpandFlag);
    event.preventDefault();

    if (context.disabled || context.loading || context.processing) {
      return;
    }

    await action();
  }
}
const [focusRef, focusState] = useFocus<HTMLDivElement>({});

useTreeNodeKeyboardActions(context, innerRef, focusState.focus);

const mergedRef = useMergeRefs(innerRef, focusRef, ref);

@sergeyteleshev
sergeyteleshev deleted the dbeaver/cloudbeaver#4159-keyboard-navigation branch September 20, 2026 20:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keyboard Navigation

3 participants