Skip to content

fix(app): forward keyboard shortcuts from sandbox iframes#839

Open
50rayn wants to merge 2 commits intohistoire-dev:mainfrom
50rayn:fix/keyboard-shortcuts-iframe
Open

fix(app): forward keyboard shortcuts from sandbox iframes#839
50rayn wants to merge 2 commits intohistoire-dev:mainfrom
50rayn:fix/keyboard-shortcuts-iframe

Conversation

@50rayn
Copy link
Copy Markdown
Contributor

@50rayn 50rayn commented May 2, 2026

Why I started this

Every time i focus a story preview the global shortcuts (search, dark-mode toggle, etc.) silently die. On Safari meta+shift+d even gets stolen by "Add to Reading List". I diagnosed it in #350 back in 2023 — the parent's key listeners only see events fired on the parent window, so anything fired while the iframe has focus is lost.

What changed

  • keyboard.ts exports a trackWindow(target) so additional windows can opt into the same shortcut tracking
  • iframe wrapper calls it on contentWindow after load, cleans up on URL change / unmount
  • event.preventDefault() now runs inside the iframe, blocking Safari's reading-list grab

Closes #350.

@bolt-new-by-stackblitz
Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@netlify
Copy link
Copy Markdown

netlify Bot commented May 2, 2026

Deploy Preview for histoire-examples-svelte3 ready!

Name Link
🔨 Latest commit e5ebe03
🔍 Latest deploy log https://app.netlify.com/projects/histoire-examples-svelte3/deploys/69f64edc96fdb7000881eca6
😎 Deploy Preview https://deploy-preview-839--histoire-examples-svelte3.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link
Copy Markdown

netlify Bot commented May 2, 2026

Deploy Preview for histoire-controls ready!

Name Link
🔨 Latest commit e5ebe03
🔍 Latest deploy log https://app.netlify.com/projects/histoire-controls/deploys/69f64edcbb5a1e0008ae7d85
😎 Deploy Preview https://deploy-preview-839--histoire-controls.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link
Copy Markdown

netlify Bot commented May 2, 2026

Deploy Preview for histoire-examples-vue3 ready!

Name Link
🔨 Latest commit e5ebe03
🔍 Latest deploy log https://app.netlify.com/projects/histoire-examples-vue3/deploys/69f64edce31e95000870fb7c
😎 Deploy Preview https://deploy-preview-839--histoire-examples-vue3.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link
Copy Markdown

netlify Bot commented May 2, 2026

Deploy Preview for histoire-site ready!

Name Link
🔨 Latest commit e5ebe03
🔍 Latest deploy log https://app.netlify.com/projects/histoire-site/deploys/69f64edc0df3380008b84927
😎 Deploy Preview https://deploy-preview-839--histoire-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces keyboard shortcut forwarding from sandbox iframes to the main application, ensuring that shortcuts remain functional when the iframe is focused. The implementation refactors the keyboard utility to manage multiple tracked windows and integrates this into the story preview component. Review feedback identifies a potential security exception when interacting with cross-origin iframes and a race condition affecting modifier key states during window transitions, both of which require additional safeguards.

Comment on lines +65 to +73
export function trackWindow(target: Window): () => void {
if (trackedWindows.value.includes(target)) return () => {}
const cleanup = bindTracking(target)
trackedWindows.value = [...trackedWindows.value, target]
return () => {
cleanup()
trackedWindows.value = trackedWindows.value.filter(w => w !== target)
}
})
}
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.

high

Accessing or adding event listeners to a cross-origin iframe's contentWindow will throw a security error (DOMException). Since Histoire sandboxes can be hosted on different origins (e.g., to prevent CSS/JS leakage), we should wrap the tracking initialization in a try-catch block to prevent the application from crashing when a cross-origin iframe is loaded.

export function trackWindow(target: Window): () => void {
  if (trackedWindows.value.includes(target)) return () => {}
  try {
    const cleanup = bindTracking(target)
    trackedWindows.value = [...trackedWindows.value, target]
    return () => {
      cleanup()
      trackedWindows.value = trackedWindows.value.filter(w => w !== target)
    }
  } catch (e) {
    return () => {}
  }
}

Comment on lines +77 to +83
export function onKeyboardShortcut(shortcut: KeyboardShortcut | Ref<KeyboardShortcut>, handler: KeyboardHandler, options: KeyboardShortcutOptions = {}) {
useEventListener(trackedWindows, options.event ?? 'keydown', (event) => {
if (isMatchingShortcut(isRef(shortcut) ? shortcut.value : shortcut)) {
handler(event)
}
})
}
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.

high

The global modifiers state is cleared whenever a tracked window blurs (see onBlur at line 48). This creates a race condition: if a user holds a modifier (like Cmd) and clicks into the iframe, the main window blurs, clearing the state before the shortcut key is pressed in the iframe. To fix this, we should synchronize the modifiers state with the current event's modifier properties (ctrlKey, metaKey, etc.) before checking the shortcut.

export function onKeyboardShortcut(shortcut: KeyboardShortcut | Ref<KeyboardShortcut>, handler: KeyboardHandler, options: KeyboardShortcutOptions = {}) {
  useEventListener(trackedWindows, options.event ?? 'keydown', (event: KeyboardEvent) => {
    modifiers.ctrl.pressed = event.ctrlKey
    modifiers.alt.pressed = event.altKey
    modifiers.shift.pressed = event.shiftKey
    modifiers.meta.pressed = event.metaKey
    if (isMatchingShortcut(isRef(shortcut) ? shortcut.value : shortcut)) {
      handler(event)
    }
  })
}

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 2, 2026

Open in StackBlitz

histoire

npm i https://pkg.pr.new/histoire@839

@histoire/app

npm i https://pkg.pr.new/@histoire/app@839

@histoire/controls

npm i https://pkg.pr.new/@histoire/controls@839

@histoire/plugin-nuxt

npm i https://pkg.pr.new/@histoire/plugin-nuxt@839

@histoire/plugin-percy

npm i https://pkg.pr.new/@histoire/plugin-percy@839

@histoire/plugin-screenshot

npm i https://pkg.pr.new/@histoire/plugin-screenshot@839

@histoire/plugin-svelte

npm i https://pkg.pr.new/@histoire/plugin-svelte@839

@histoire/plugin-vue

npm i https://pkg.pr.new/@histoire/plugin-vue@839

@histoire/shared

npm i https://pkg.pr.new/@histoire/shared@839

@histoire/vendors

npm i https://pkg.pr.new/@histoire/vendors@839

commit: e5ebe03

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.

Dark Mode Toggle Shortcut on Safari

1 participant