-
Notifications
You must be signed in to change notification settings - Fork 199
refactor: migrate the Guides story to TypeScript
#908
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Ease maintenance, detect errors earlier. The KeyHandler feature is not working with storybook, and some prototype elements are overridden. This will be handled later.
WalkthroughDocs 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
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
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 detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ 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)
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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 usesthisinside an arrow function, which won’t bind correctly.Either reference the instance (
keyHandler) or use a regular function to leveragethis.-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 unusedlabel.
rubberBandis boolean, soRecord<string, string>is inaccurate andlabelis 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.snapToTerminalsaffects 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 theuseGuidesForEventcallback parameter (or explicitlyany).Prevents implicit-
anyunder 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.
📒 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 clarifiesnullinput.Use of backticks for the
nullliteral improves readability and matches the signaturecells?: Cell[] | null.
| const container = createGraphContainer(args); | ||
| div.appendChild(container); | ||
|
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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:
- The target itself (
graph.container) - The target's parent node
- 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
- Storybook runs stories in an iframe - each story runs inside its own iframe context
- KeyHandler uses
document.documentElementby default - this refers to the iframe's document, not the parent window - Event focus issues in iframes - keyboard events may not properly bubble or focus may not be correctly established in the iframe context
- 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:
-
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]
-
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]
-
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]
-
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:
- Frame isolation - each story runs in its own iframe context
- Focus issues - the iframe needs explicit focus for keyboard events to work
- 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:
- Fixes all stories at once - no need to modify each individual story
- Consistent behavior - all KeyHandler usage will work the same way
- Future-proof - new stories with KeyHandler will automatically work
- Minimal code changes - just update the preview configuration
Then you can:
- Uncomment KeyHandler in DynamicToolbar and Toolbar stories
- Remove the TODO comments about KeyHandler not working
- 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.blurevents - 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:
- Create a tracking issue for the system-wide KeyHandler problem
- Develop a robust solution that handles dynamic focus restoration
- 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.
|



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