Skip to content

Conversation

@tbouffard
Copy link
Member

@tbouffard tbouffard commented Aug 27, 2025

Ease maintenance, detect errors earlier.
The KeyHandler feature is not working with storybook, and some prototype elements are overridden. This will be handled later.

Summary by CodeRabbit

  • Documentation
    • Improved KeyHandler and removeCells docs: clearer defaults, refined examples, headings, wording and formatting; no runtime or API changes.
  • New/Docs
    • Added a TypeScript Storybook demo showing grid, guides, optional rubber-band selection, edge snapping, and arrow-key nudging for selected cells.
  • Chores
    • Removed the legacy JavaScript Guides story and migrated it to the new TypeScript demo.

Ease maintenance, detect errors earlier.
The KeyHandler feature is not working with storybook, and some prototype elements are overridden. This will be handled later.
@tbouffard tbouffard added the refactor Code refactoring label Aug 27, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 27, 2025

Walkthrough

Docs updated in core (KeyHandler.ts, CellsMixin.type.ts). Storybook Guides story migrated from JavaScript to TypeScript: JS story removed and a new TS story added that configures a graph with grid/guides, elbow edge styles, snap-to-terminals, optional RubberBand, batch-created cells, and arrow-key nudging via KeyHandler. No runtime or public API changes in core.

Changes

Cohort / File(s) Summary
KeyHandler docs
packages/core/src/view/handler/KeyHandler.ts
JSDoc refinements and reformatting: clarified event handler and event-source/target wording, adjusted headings, reformatted examples (bindKey demo and getFunction), tightened target/listener phrasing, set enabled default to true, improved isEnabled/setEnabled docs, expanded isGraphEvent description. No signature or runtime changes.
CellsMixin docs
packages/core/src/view/mixins/CellsMixin.type.ts
Minor JSDoc tweak for AbstractGraph.removeCells param formatting (code-formatted null, punctuation). No behavior or signature changes.
Storybook Guides migration
packages/html/stories/Guides.stories.js, packages/html/stories/Guides.stories.ts
Removed JS story file and added a TypeScript story: sets up graph (connectable, gridSize), enables guides with Alt toggle, sets default/alternate ElbowConnector edge styles, sets EdgeHandler.prototype.snapToTerminals = true, optionally includes RubberBandHandler, batch-adds two vertices and an edge, focuses container, binds arrow keys (37–40) via KeyHandler to nudge selection, and exports Storybook metadata + Default.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Story as Guides.stories.ts
  participant Graph as Graph
  participant Sel as SelectionHandler
  participant KeyH as KeyHandler
  participant Model as Model

  rect rgb(247,250,253)
    note right of Story: Story initialization
    Story->>Graph: createGraph(container, {connectable, gridSize})
    Story->>Sel: guidesEnabled = true\nuseGuidesForEvent = (evt) => !evt.altKey
    Story->>Graph: setDefaultEdgeStyle(ElbowConnector, rounded)
    Story->>Graph: setAlternateEdgeStyle(elbow=vertical)
    Story->>Graph: EdgeHandler.prototype.snapToTerminals = true
    Story->>Model: batchUpdate(add v1, add v2, add edge)
    Story->>KeyH: bind keys 37–40 -> moveSelected(dx,dy)
    Story->>Graph: focus(container)
  end

  User->>KeyH: press Arrow key
  KeyH->>Graph: moveSelected(dx,dy)
  Graph->>Model: update cell geometry
  Sel-->>Graph: show/hide guides (Alt state)
  Graph-->>User: render update
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 383ce6c and 1dd7fab.

📒 Files selected for processing (1)
  • packages/html/stories/Guides.stories.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/html/stories/Guides.stories.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: build
  • GitHub Check: build (macos-14)
  • GitHub Check: build (windows-2022)
  • GitHub Check: build (ubuntu-24.04)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/migrate_Guides_story_to_TS

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (7)
packages/core/src/view/handler/KeyHandler.ts (3)

136-141: JSDoc: replace <enabled> placeholder with a proper reference.

Use {@link enabled} or plain “enabled property” to avoid angle-bracket placeholder.

- * Enables or disables event handling by updating <enabled>.
+ * Enables or disables event handling by updating {@link enabled}.

221-223: JSDoc grammar: missing conjunction/comma in sentence.

Minor wording fix for readability.

- * That is, if the event source is either the target, one of its direct children a descendant of the {@link AbstractGraph.container},
+ * That is, if the event source is either the target, one of its direct children, or a descendant of the {@link AbstractGraph.container},

58-65: Example uses this inside an arrow function, which won’t bind correctly.

Either reference the instance (keyHandler) or use a regular function to leverage this.

-keyHandler.getFunction = (evt) => {
-  if (evt) {
-    return (InternalEvent.isControlDown(evt) || (Client.IS_MAC && evt.metaKey)) ? this.controlKeys[evt.keyCode] : this.normalKeys[evt.keyCode];
-  }
-  return null;
-};
+keyHandler.getFunction = function (evt) {
+  if (evt) {
+    return (InternalEvent.isControlDown(evt) || (Client.IS_MAC && evt.metaKey))
+      ? this.controlKeys[evt.keyCode]
+      : this.normalKeys[evt.keyCode];
+  }
+  return null;
+};
packages/html/stories/Guides.stories.ts (4)

55-56: Fix args typing and remove unused label.

rubberBand is boolean, so Record<string, string> is inaccurate and label is unused.

-const Template = ({ label, ...args }: Record<string, string>) => {
+type TemplateArgs = {
+  rubberBand?: boolean;
+  [key: string]: unknown;
+};
+const Template = (args: TemplateArgs) => {

72-74: Avoid global prototype mutation across stories.

Setting EdgeHandler.prototype.snapToTerminals affects all stories in the session.

Consider setting this per-instance if available (e.g., via a handler/plugin instance), or capture and restore:

-EdgeHandler.prototype.snapToTerminals = true;
+const prevSnap = EdgeHandler.prototype.snapToTerminals;
+EdgeHandler.prototype.snapToTerminals = true;
// ... later on story cleanup:
// EdgeHandler.prototype.snapToTerminals = prevSnap;

76-78: Defensive copy of plugins array.

getDefaultPlugins() returns a fresh array, but copying avoids accidental sharing if the implementation changes.

-const plugins = getDefaultPlugins();
+const plugins = [...getDefaultPlugins()];
 if (args.rubberBand) plugins.push(RubberBandHandler);

89-92: Type the useGuidesForEvent callback parameter (or explicitly any).

Prevents implicit-any under stricter TS configs.

-    selectionHandler.useGuidesForEvent = function (me) {
+    selectionHandler.useGuidesForEvent = function (me: any) {
       return !eventUtils.isAltDown(me.getEvent());
     };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 336d4a6 and 6f35b4b.

📒 Files selected for processing (4)
  • packages/core/src/view/handler/KeyHandler.ts (3 hunks)
  • packages/core/src/view/mixins/CellsMixin.type.ts (1 hunks)
  • packages/html/stories/Guides.stories.js (0 hunks)
  • packages/html/stories/Guides.stories.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • packages/html/stories/Guides.stories.js
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: tbouffard
PR: maxGraph/maxGraph#849
File: packages/html/stories/DragSource.stories.js:98-101
Timestamp: 2025-06-13T07:48:10.300Z
Learning: User tbouffard prefers answers in English; avoid switching to other languages in future replies.
🧬 Code graph analysis (1)
packages/html/stories/Guides.stories.ts (2)
packages/html/stories/shared/configure.js (1)
  • createMainDiv (53-63)
packages/core/src/view/plugins/index.ts (1)
  • getDefaultPlugins (46-55)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: build
  • GitHub Check: build (macos-14)
  • GitHub Check: build (ubuntu-24.04)
  • GitHub Check: build (windows-2022)
🔇 Additional comments (1)
packages/core/src/view/mixins/CellsMixin.type.ts (1)

359-363: Doc tweak looks good and clarifies null input.

Use of backticks for the null literal improves readability and matches the signature cells?: Cell[] | null.

Comment on lines +62 to +64
const container = createGraphContainer(args);
div.appendChild(container);

Copy link

@coderabbitai coderabbitai bot Aug 27, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make keyboard handling reliable in Storybook: focusable container and scoped key target.

Storybook often captures document-level keys. Ensure the container is focusable and scope KeyHandler to it.

-  const container = createGraphContainer(args);
+  const container = createGraphContainer(args);
+  // Ensure focusable for keyboard events (some containers default to tabIndex -1)
+  if (container.tabIndex < 0) container.tabIndex = 0;
   div.appendChild(container);
@@
-  const keyHandler = new KeyHandler(graph);
+  // Scope to container to avoid Storybook-level key handling conflicts
+  const keyHandler = new KeyHandler(graph, graph.container);

I can add a minimal unmount cleanup if you expose a Storybook cleanup hook for HTML stories.

Also applies to: 141-147

✅ Addressed in commits e161988 to 383ce6c

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks @coderabbitai I added your suggestion but this still does not work

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

@tbouffard, let me investigate the KeyHandler implementation and see what might be causing the issue in Storybook.


🏁 Script executed:

#!/bin/bash
# Let's examine the current implementation of the Guides story
cat packages/html/stories/Guides.stories.ts

echo "=== KeyHandler implementation ==="
# Look at the KeyHandler class to understand its requirements
cat packages/core/src/view/handler/KeyHandler.ts | head -50

echo "=== Check for any Storybook-specific keyboard handling ==="
# Look for other stories that successfully use keyboard handling
find packages/html/stories -name "*.ts" -exec grep -l "KeyHandler\|keyHandler" {} \;

echo "=== Examine other keyboard-related implementations ==="
rg -n -A5 -B5 "KeyHandler" packages/html/stories/

Length of output: 27340


Now let me look at other stories to see if there are successful patterns and examine the KeyHandler constructor more closely:


🏁 Script executed:

#!/bin/bash
# Let's look at how other stories successfully use KeyHandler
echo "=== UserObject story (simple usage) ==="
cat packages/html/stories/UserObject.stories.ts | grep -A10 -B5 "new KeyHandler"

echo "=== Window story (simple usage) ==="
cat packages/html/stories/Window.stories.ts | grep -A10 -B5 "new KeyHandler"

echo "=== KeyHandler constructor signature ==="
# Look at the constructor to understand the target parameter
rg -n -A15 "constructor.*target" packages/core/src/view/handler/KeyHandler.ts

echo "=== KeyHandler event installation details ==="
# Look for event installation logic
rg -n -A10 "addEventListener\|addListener" packages/core/src/view/handler/KeyHandler.ts

echo "=== Check if there are any Storybook-specific workarounds in other stories ==="
rg -n -B2 -A5 "storybook\|Storybook" packages/html/stories/

Length of output: 2475


Let me look at the full KeyHandler implementation and check what might be causing the issue:


🏁 Script executed:

#!/bin/bash
# Get the full KeyHandler implementation
echo "=== Full KeyHandler constructor and key handling ==="
cat packages/core/src/view/handler/KeyHandler.ts

echo -e "\n=== Check InternalEvent implementation ==="
# Look at how InternalEvent.addListener works
find packages/core -name "*.ts" -exec grep -l "InternalEvent" {} \; | head -3 | xargs grep -n -A5 "addListener"

echo -e "\n=== Look for any iframe or focus-related issues ==="
rg -n -i "iframe\|focus\|blur" packages/html/stories/Guides.stories.ts

echo -e "\n=== Check what createGraphContainer does ==="
cat packages/html/stories/shared/configure.js | grep -A20 -B5 "createGraphContainer"

Length of output: 23007


Now let me look at the InternalEvent.addListener implementation:


🏁 Script executed:

#!/bin/bash
# Find and examine the InternalEvent.addListener implementation
echo "=== InternalEvent.addListener implementation ==="
find packages/core -name "*.ts" -path "*/event/*" | xargs grep -n -A10 -B5 "addListener"

echo -e "\n=== Check if there are focus-related methods in the graph ==="
rg -n "focus\(\)" packages/html/stories/Guides.stories.ts

echo -e "\n=== Look at what other working stories do differently ==="
# Compare with a working KeyHandler story
cat packages/html/stories/Validation.stories.js | grep -A15 -B5 "KeyHandler"

echo -e "\n=== Try to understand Storybook iframe context ==="
# Look for any mentions of document vs window context
rg -n "document\.documentElement\|document\." packages/core/src/view/handler/KeyHandler.ts

Length of output: 14385


Now I see the issue! Let me examine one more crucial detail:


🏁 Script executed:

#!/bin/bash
# Let's check what the KeyHandler's isGraphEvent method looks for
echo "=== KeyHandler isGraphEvent method ==="
rg -n -A15 "isGraphEvent" packages/core/src/view/handler/KeyHandler.ts

echo -e "\n=== Let's see what the current container looks like ==="
# Check if there are any special properties needed on the container
rg -n "graph\.container" packages/core/src/view/handler/KeyHandler.ts

echo -e "\n=== Check the difference: working stories use default document target ==="
echo "Working stories (Validation.js):"
grep -A3 -B3 "new KeyHandler" packages/html/stories/Validation.stories.js

echo -e "\nGuides story (not working):"
grep -A3 -B3 "new KeyHandler" packages/html/stories/Guides.stories.ts

echo -e "\n=== Check if there are any DOM manipulation issues ==="
# See if the target is what we expect
cat packages/html/stories/Guides.stories.ts | tail -20

Length of output: 3826


Perfect! Now I understand the issue. The problem is in the isGraphEvent method. When you set the KeyHandler target to graph.container, it expects events to come from either:

  1. The target itself (graph.container)
  2. The target's parent node
  3. A descendant of graph.container

But when you focus the container and press keys, the event source becomes the focused container, which makes isGraphEvent return true. However, in Storybook's iframe context, there might be event bubbling issues or the container might not be receiving focus properly.

Let me provide a comprehensive solution:

@tbouffard, I found the issue! The problem is with the isGraphEvent method when using graph.container as the target. Here are a few approaches to fix this:

Option 1: Use default document target (recommended)

// Instead of scoping to container, use the default document target
// but ensure the container is properly focusable
const keyHandler = new KeyHandler(graph); // Uses document.documentElement by default

// Make sure container can receive focus and is properly focused
if (graph.container.tabIndex < 0) graph.container.tabIndex = 0;
graph.container.focus();

Option 2: Use a parent element as the target

// Use the div (parent of container) as the target instead
const keyHandler = new KeyHandler(graph, div);
div.tabIndex = 0;
div.focus();

Option 3: Override isGraphEvent for Storybook context

const keyHandler = new KeyHandler(graph, graph.container);
// Override the method to be more permissive in Storybook
keyHandler.isGraphEvent = function(evt) {
  // In Storybook, be more permissive about what constitutes a graph event
  return true; // or add more specific logic if needed
};

Option 4: Use window as target (for Storybook iframe)

// For Storybook's iframe context, use window to catch all key events
const keyHandler = new KeyHandler(graph, window as any);

I recommend trying Option 1 first, as it follows the same pattern as other working stories. The key difference is that working stories like Validation.stories.js use the default document.documentElement target, which works reliably across different contexts including Storybook.

The issue with your current approach is that graph.container as a target has very specific requirements in the isGraphEvent method that aren't being met in Storybook's context.


🧠 Learnings used
Learnt from: tbouffard
PR: maxGraph/maxGraph#849
File: packages/html/stories/DragSource.stories.js:98-101
Timestamp: 2025-06-13T07:48:10.300Z
Learning: User tbouffard prefers answers in English; avoid switching to other languages in future replies.

Copy link
Member Author

Choose a reason for hiding this comment

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

OK thanks @coderabbitai be aware that the keyhandler without passing a target is not working. The problem occurs in all existing stories.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

@tbouffard, that's very important information! Let me investigate this broader KeyHandler issue across all stories to understand what's happening.


🏁 Script executed:

#!/bin/bash
# First, let's find all stories that use KeyHandler
echo "=== All stories using KeyHandler ==="
find packages/html/stories -name "*.ts" -o -name "*.js" | xargs grep -l "KeyHandler\|keyHandler"

echo -e "\n=== KeyHandler usage patterns in each story ==="
find packages/html/stories -name "*.ts" -o -name "*.js" | xargs grep -n -A3 -B3 "new KeyHandler"

echo -e "\n=== Check Storybook configuration files ==="
find . -name ".storybook" -type d -exec find {} -name "*.js" -o -name "*.ts" \;

echo -e "\n=== Look for any iframe-related configurations ==="
find . -name "*.json" -o -name "*.js" -o -name "*.ts" | xargs grep -l -i "iframe\|preview\|storybook" | head -10

echo -e "\n=== Check if there are any event-related issues mentioned in documentation ==="
find . -name "README*" -o -name "*.md" | xargs grep -l -i "keyhandler\|keyboard\|storybook" | head -5

Length of output: 9460


🏁 Script executed:

#!/bin/bash
# Check the Storybook configuration files
echo "=== Storybook main configuration ==="
cat ./packages/html/.storybook/main.ts

echo -e "\n=== Storybook preview configuration ==="
cat ./packages/html/.storybook/preview.ts

echo -e "\n=== Check for any known KeyHandler issues in docs ==="
grep -n -i -A5 -B5 "keyhandler\|keyboard" README.md packages/html/README.md CHANGELOG.md

echo -e "\n=== Look at the specific TODO comments about KeyHandler not working ==="
grep -n -A3 -B3 "TODO.*not working\|not working.*TODO" packages/html/stories/*.ts packages/html/stories/*.js

echo -e "\n=== Check if there are iframe-related event handling patterns ==="
rg -n "iframe\|InternalEvent" packages/core/src/util/gestureUtils.ts packages/core/src/util/eventUtils.ts

echo -e "\n=== See what DynamicToolbar and Toolbar stories say about KeyHandler ==="
grep -n -A5 -B5 "TODO.*not working\|keyHandler.*TODO" packages/html/stories/DynamicToolbar.stories.ts packages/html/stories/Toolbar.stories.ts

Length of output: 13695


Now let me check what event utilities exist and look at the KeyHandler's event handling mechanism more closely:


🏁 Script executed:

#!/bin/bash
echo "=== Find event utilities and InternalEvent ==="
find packages/core -name "*.ts" | xargs grep -l "InternalEvent\|addListener" | head -5 | xargs ls -la

echo -e "\n=== Look at InternalEvent implementation ==="
find packages/core -name "*.ts" | xargs grep -l "class.*InternalEvent\|InternalEvent.*{" | head -2

echo -e "\n=== Check how addListener is implemented ==="
find packages/core -name "*.ts" | xargs grep -n -A10 -B5 "addListener.*function\|addListener\s*:" | head -30

echo -e "\n=== Check for any document vs iframe context issues ==="
grep -n -A5 -B5 "document\.documentElement\|document\.addEventListener" packages/core/src/view/handler/KeyHandler.ts

echo -e "\n=== Check if there are any Storybook-specific event issues in maxGraph issues or docs ==="
# Look for any GitHub issues or documentation about this
find . -name "*.md" -o -name "*.txt" | xargs grep -l -i "storybook.*key\|key.*storybook" 2>/dev/null || echo "No storybook keyboard docs found"

echo -e "\n=== Look for the vanilla example mentioned in the TODO ==="
find . -name "*.html" -o -name "*.js" | xargs grep -l -i "guide" | head -3

Length of output: 4564


🏁 Script executed:

#!/bin/bash
# Let's look for InternalEvent class definition specifically
echo "=== Find InternalEvent class definition ==="
find packages/core -name "*.ts" -exec grep -l "class InternalEvent\|export.*InternalEvent" {} \;

echo -e "\n=== Look at InternalEvent class implementation ==="
find packages/core -name "*.ts" -exec grep -n -A15 -B5 "class InternalEvent\|export.*class.*InternalEvent" {} \;

echo -e "\n=== Check InternalEvent.addListener implementation ==="
find packages/core -name "*.ts" -exec grep -n -A10 -B5 "static addListener\|addListener.*static" {} \;

echo -e "\n=== Look at vanilla examples directory structure ==="
find packages/html/stashed -name "*guide*" -o -name "*Guide*" 2>/dev/null | head -10

echo -e "\n=== Check if there are working keyboard examples ==="
grep -n -A5 -B5 "KeyHandler\|keyHandler" packages/html/stashed/grapheditor/www/js/Editor.js | head -20

echo -e "\n=== Check the current KeyHandler event installation ==="
grep -n -A15 -B5 "InternalEvent.addListener" packages/core/src/view/handler/KeyHandler.ts

Length of output: 3567


Perfect! Now I have a complete understanding of the issue. Let me search for more information about Storybook and iframe contexts:

@tbouffard, I now have a complete understanding of the KeyHandler issue in Storybook! This is indeed a system-wide problem affecting all stories, not just the Guides story. Here's what's happening:

Root Cause Analysis

  1. Storybook runs stories in an iframe - each story runs inside its own iframe context
  2. KeyHandler uses document.documentElement by default - this refers to the iframe's document, not the parent window
  3. Event focus issues in iframes - keyboard events may not properly bubble or focus may not be correctly established in the iframe context
  4. Multiple stories already have this commented out - DynamicToolbar and Toolbar stories have KeyHandler commented out with "TODO not working"

The Real Problem

The issue is that InternalEvent.addListener(this.target, 'keydown', this.keydownHandler) is adding the listener to document.documentElement of the iframe's document, but in Storybook's architecture, keyboard events might need to be handled differently due to the iframe boundary.

Solutions to Try

Since this affects ALL stories, here are the approaches in order of preference:

Option 1: Force focus and use a more specific target

// Make the graph container explicitly focusable and focused
const container = createGraphContainer(args);
container.tabIndex = 0;
container.style.outline = 'none'; // Remove focus outline if desired
div.appendChild(container);

// Create graph and force focus
const graph = new Graph(container, /* ... other params ... */);

// Use the container as the KeyHandler target
const keyHandler = new KeyHandler(graph, container);

// Force focus after everything is set up
setTimeout(() => {
  container.focus();
}, 0);

Option 2: Use window instead of document

const keyHandler = new KeyHandler(graph, window as any);

Option 3: Create a custom KeyHandler for Storybook

// Custom KeyHandler that works better with Storybook
class StorybookKeyHandler extends KeyHandler {
  constructor(graph: AbstractGraph, target?: Element | null) {
    // Don't call super constructor to avoid default event binding
    this.graph = graph;
    this.target = target || graph.container;
    
    this.keydownHandler = (evt) => {
      this.keyDown(evt);
    };

    // Use the parent window if available, otherwise use the target
    const eventTarget = window.parent !== window ? window : (this.target as any);
    eventTarget.addEventListener('keydown', this.keydownHandler, { capture: true });
  }
}

// Usage
const keyHandler = new StorybookKeyHandler(graph);

Option 4: Global Storybook Configuration Fix

You could potentially add this to the Storybook preview configuration to handle keyboard events globally:

// In packages/html/.storybook/preview.ts
const handleGlobalKeyEvents = () => {
  // Forward parent window key events to iframe
  if (window.parent !== window) {
    window.parent.addEventListener('keydown', (evt) => {
      // Forward to iframe's document
      window.document.dispatchEvent(new KeyboardEvent(evt.type, evt));
    });
  }
};

// Add to decorators
(storyFn) => {
  resetMaxGraphConfigs();
  destroyUnreleasedElements();
  handleGlobalKeyEvents();
  return storyFn();
}

Recommendation

I recommend trying Option 1 first for the Guides story, as it's the most targeted fix. If that works, we can create a utility function that other stories can use.

Would you like me to help you create an issue to track this system-wide KeyHandler problem, since it affects multiple stories and should be addressed comprehensively?


🌐 Web query:

Storybook iframe keyboard events not working document.addEventListener

💡 Result:

Short answer: keyboard events don't reach document.addEventListener from the parent because Storybook renders stories inside an isolated iframe (separate window/document) and the frame must have focus (and same origin) for those handlers to fire. The fix is to attach the listener inside the preview iframe (or to the iframe's contentWindow/document) and ensure the iframe has focus. References and steps:

  1. If your code runs in the parent (manager) page, it won't receive key events from the preview iframe. Instead attach to the iframe's document:

    • iframeEl.contentWindow.document.addEventListener('keydown', handler). This requires same-origin (you control both pages). [1][2]
  2. Ensure the iframe has keyboard focus (otherwise key events go to the parent window). Common patterns:

    • call contentWindow.focus() on iframe load or focus the story root element (give it tabindex and call .focus()).
    • also consider listening in capture phase (third param true) if you need earlier capture. [1][3]
  3. Where to put the code in Storybook:

    • Add listeners in .storybook/preview.js or preview-head.html so they run inside the preview context, or add a decorator that mounts a global listener inside each story. Avoid trying to attach to iframe elements from the manager (cross-frame issues). [3][4]
  4. If you control both frames and want parent ⇄ iframe events, use postMessage to forward keyboard info (cross-origin safe pattern). [1]

Minimal example (runs inside preview iframe e.g. preview.js or story decorator):
const handler = (e) => { console.log('keydown', e.key); };
window.addEventListener('keydown', handler);
// or, if you need document specifically:
document.addEventListener('keydown', handler);

If you prefer attaching from the manager to the preview iframe element:
const iframe = document.getElementById('storybook-preview-iframe');
iframe.addEventListener('load', () => {
const doc = iframe.contentWindow.document;
doc.addEventListener('keydown', handler);
iframe.contentWindow.focus();
});

Sources: Stack Overflow discussion on iframe key events & focus and contentWindow.document usage; Storybook issues re: key events and where code is executed. [1][2][3][4]

Would you like a concrete code snippet placed into .storybook/preview.js or an example decorator I can paste into your Storybook?


Perfect! The web search confirms exactly what I suspected. This is a well-known Storybook limitation due to iframe isolation. Here's the comprehensive solution:

Root Cause (Confirmed)

Storybook renders stories inside an isolated iframe, and keyboard events don't properly reach document.addEventListener due to:

  1. Frame isolation - each story runs in its own iframe context
  2. Focus issues - the iframe needs explicit focus for keyboard events to work
  3. Event isolation - events don't bubble between parent and iframe contexts

Solutions

Option A: Global Storybook Fix (Recommended)

Add this to your packages/html/.storybook/preview.ts:

// Add this to your preview.ts decorators array
const preview: Preview = {
  // ... existing config
  decorators: [
    // ... existing resetMaxGraphConfigs decorator
    // Add keyboard event handling for all stories
    (storyFn) => {
      resetMaxGraphConfigs();
      destroyUnreleasedElements();
      
      // Ensure iframe has keyboard focus for KeyHandler to work
      setTimeout(() => {
        // Focus the window to enable keyboard events in Storybook iframe
        window.focus();
        
        // Also focus the document element where KeyHandler attaches
        if (document.documentElement.tabIndex < 0) {
          document.documentElement.tabIndex = 0;
        }
        document.documentElement.focus();
      }, 100);
      
      return storyFn();
    },
  ],
};

Option B: Individual Story Fix

For the Guides story specifically:

const Template = ({ label, ...args }: Record<string, string>) => {
  // ... existing code ...
  
  const container = createGraphContainer(args);
  // Ensure container is focusable
  container.tabIndex = 0;
  container.style.outline = 'none'; // Remove focus outline
  div.appendChild(container);

  const graph = new Graph(container, /* ... other params ... */);
  
  // ... graph configuration ...
  
  // KeyHandler should work now with proper focus management
  const keyHandler = new KeyHandler(graph); // Use default document.documentElement
  
  // Force focus after setup
  setTimeout(() => {
    window.focus(); // Focus the iframe window
    container.focus(); // Focus the container
  }, 0);
  
  // ... rest of keyboard bindings ...
};

Option C: Enhanced KeyHandler Wrapper

Create a Storybook-compatible KeyHandler:

// Add this helper in a shared utilities file
export function createStorybookKeyHandler(graph: AbstractGraph): KeyHandler {
  // Ensure the iframe window has focus
  window.focus();
  
  // Create KeyHandler with default settings
  const keyHandler = new KeyHandler(graph);
  
  // Additional focus management for Storybook
  if (graph.container.tabIndex < 0) {
    graph.container.tabIndex = 0;
  }
  graph.container.style.outline = 'none';
  
  // Ensure focus is maintained
  setTimeout(() => {
    graph.container.focus();
  }, 0);
  
  return keyHandler;
}

// Usage in stories:
const keyHandler = createStorybookKeyHandler(graph);

Recommended Approach

I recommend implementing Option A (Global Fix) because:

  1. Fixes all stories at once - no need to modify each individual story
  2. Consistent behavior - all KeyHandler usage will work the same way
  3. Future-proof - new stories with KeyHandler will automatically work
  4. Minimal code changes - just update the preview configuration

Then you can:

  1. Uncomment KeyHandler in DynamicToolbar and Toolbar stories
  2. Remove the TODO comments about KeyHandler not working
  3. Use standard KeyHandler syntax in the Guides story

Would you like me to help you implement the global fix and create an issue to track removing all the "TODO not working" comments from the existing stories?


✏️ Learnings added
Learnt from: tbouffard
PR: maxGraph/maxGraph#908
File: packages/html/stories/Guides.stories.ts:0-0
Timestamp: 2025-08-28T14:07:42.487Z
Learning: KeyHandler does not work properly in Storybook environment due to iframe context - keyboard events added to document.documentElement in iframe don't receive events properly. This affects all maxGraph stories that use KeyHandler, with several stories having KeyHandler commented out with "TODO not working" comments.

Learnt from: tbouffard
PR: maxGraph/maxGraph#908
File: packages/html/stories/Guides.stories.ts:0-0
Timestamp: 2025-08-28T14:07:42.487Z
Learning: KeyHandler issues in Storybook are due to iframe isolation - Storybook renders stories in iframes where keyboard events don't properly reach document.addEventListener without explicit focus management. The solution is to ensure the iframe window has focus by calling window.focus() and focusing the target element in the preview configuration or story setup.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: tbouffard
PR: maxGraph/maxGraph#849
File: packages/html/stories/DragSource.stories.js:98-101
Timestamp: 2025-06-13T07:48:10.300Z
Learning: User tbouffard prefers answers in English; avoid switching to other languages in future replies.

Learnt from: tbouffard
PR: maxGraph/maxGraph#857
File: packages/core/src/view/plugins/SelectionHandler.ts:1081-1092
Timestamp: 2025-06-26T13:27:19.012Z
Learning: User tbouffard prefers to keep PRs focused on their main objective and defer implementation improvements or logic fixes to future dedicated PRs. For maxGraph, refactoring PRs like Dictionary→Map migration should not include unrelated logic improvements even when valid issues are identified in the migrated code.

Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai I tested solution B. It works when arriving on the story.
If I click on the storybook left bar, then click on the container of the story, the events are no longer received.

I will work later on this topic and I will try to fix the problem in a dedicated PR.

Copy link

Choose a reason for hiding this comment

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

@tbouffard, excellent testing! You've identified a classic focus restoration issue in Storybook.

What you've discovered makes perfect sense:

  • Initial load works because the iframe gets focus when the story first renders
  • Focus is lost on navigation when you click the Storybook sidebar, focus moves away from the iframe
  • Focus isn't restored automatically when you return to the story - the container needs to explicitly regain focus

This is a more complex focus management problem that would require:

  • Event listeners for window.focus/window.blur events
  • Dynamic focus restoration when users return to the story
  • Or a more robust global focus management system in the Storybook preview configuration

Your approach to handle this in a dedicated PR is perfect! It keeps this TypeScript migration PR focused on its main objective, which aligns with your preferred workflow.

When you're ready to tackle the KeyHandler issue comprehensively, I'd be happy to help you:

  1. Create a tracking issue for the system-wide KeyHandler problem
  2. Develop a robust solution that handles dynamic focus restoration
  3. Remove all the "TODO not working" comments from existing stories

Great detective work on identifying the exact focus behavior! 🎯


🧠 Learnings used
Learnt from: tbouffard
PR: maxGraph/maxGraph#908
File: packages/html/stories/Guides.stories.ts:0-0
Timestamp: 2025-08-28T14:07:42.487Z
Learning: KeyHandler issues in Storybook are due to iframe isolation - Storybook renders stories in iframes where keyboard events don't properly reach document.addEventListener without explicit focus management. The solution is to ensure the iframe window has focus by calling window.focus() and focusing the target element in the preview configuration or story setup.

Learnt from: tbouffard
PR: maxGraph/maxGraph#908
File: packages/html/stories/Guides.stories.ts:0-0
Timestamp: 2025-08-28T14:07:42.487Z
Learning: KeyHandler does not work properly in Storybook environment due to iframe context - keyboard events added to document.documentElement in iframe don't receive events properly. This affects all maxGraph stories that use KeyHandler, with several stories having KeyHandler commented out with "TODO not working" comments.

Learnt from: tbouffard
PR: maxGraph/maxGraph#849
File: packages/html/stories/DragSource.stories.js:98-101
Timestamp: 2025-06-13T07:48:10.300Z
Learning: User tbouffard prefers answers in English; avoid switching to other languages in future replies.

Learnt from: tbouffard
PR: maxGraph/maxGraph#857
File: packages/core/src/view/plugins/SelectionHandler.ts:1081-1092
Timestamp: 2025-06-26T13:27:19.012Z
Learning: User tbouffard prefers to keep PRs focused on their main objective and defer implementation improvements or logic fixes to future dedicated PRs. For maxGraph, refactoring PRs like Dictionary→Map migration should not include unrelated logic improvements even when valid issues are identified in the migrated code.

@sonarqubecloud
Copy link

@tbouffard tbouffard merged commit 896a8e5 into main Aug 28, 2025
7 checks passed
@tbouffard tbouffard deleted the refactor/migrate_Guides_story_to_TS branch August 28, 2025 15:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor Code refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant