diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js
index 19c1b610580166..67b01a4963b0c2 100644
--- a/packages/block-editor/src/components/iframe/index.js
+++ b/packages/block-editor/src/components/iframe/index.js
@@ -31,6 +31,30 @@ import { useWritingFlow } from '../writing-flow';
import { useCompatibilityStyles } from './use-compatibility-styles';
import { store as blockEditorStore } from '../../store';
+function bubbleEvent( event, Constructor, frame ) {
+ const init = {};
+
+ for ( const key in event ) {
+ init[ key ] = event[ key ];
+ }
+
+ if ( event instanceof frame.ownerDocument.defaultView.MouseEvent ) {
+ const rect = frame.getBoundingClientRect();
+ init.clientX += rect.left;
+ init.clientY += rect.top;
+ }
+
+ const newEvent = new Constructor( event.type, init );
+ if ( init.defaultPrevented ) {
+ newEvent.preventDefault();
+ }
+ const cancelled = ! frame.dispatchEvent( newEvent );
+
+ if ( cancelled ) {
+ event.preventDefault();
+ }
+}
+
/**
* Bubbles some event types (keydown, keypress, and dragover) to parent document
* document to ensure that the keyboard shortcuts and drag and drop work.
@@ -39,42 +63,30 @@ import { store as blockEditorStore } from '../../store';
* should be context dependent, e.g. actions on blocks like Cmd+A should not
* work globally outside the block editor.
*
- * @param {Document} doc Document to attach listeners to.
+ * @param {Document} iframeDocument Document to attach listeners to.
*/
-function bubbleEvents( doc ) {
- const { defaultView } = doc;
- const { frameElement } = defaultView;
-
- function bubbleEvent( event ) {
- const prototype = Object.getPrototypeOf( event );
- const constructorName = prototype.constructor.name;
- const Constructor = window[ constructorName ];
-
- const init = {};
-
- for ( const key in event ) {
- init[ key ] = event[ key ];
+function useBubbleEvents( iframeDocument ) {
+ return useRefEffect( ( body ) => {
+ const { defaultView } = iframeDocument;
+ const { frameElement } = defaultView;
+ const eventTypes = [ 'dragover', 'mousemove' ];
+ const handlers = {};
+ for ( const name of eventTypes ) {
+ handlers[ name ] = ( event ) => {
+ const prototype = Object.getPrototypeOf( event );
+ const constructorName = prototype.constructor.name;
+ const Constructor = window[ constructorName ];
+ bubbleEvent( event, Constructor, frameElement );
+ };
+ body.addEventListener( name, handlers[ name ] );
}
- if ( event instanceof defaultView.MouseEvent ) {
- const rect = frameElement.getBoundingClientRect();
- init.clientX += rect.left;
- init.clientY += rect.top;
- }
-
- const newEvent = new Constructor( event.type, init );
- const cancelled = ! frameElement.dispatchEvent( newEvent );
-
- if ( cancelled ) {
- event.preventDefault();
- }
- }
-
- const eventTypes = [ 'dragover', 'mousemove' ];
-
- for ( const name of eventTypes ) {
- doc.addEventListener( name, bubbleEvent );
- }
+ return () => {
+ for ( const name of eventTypes ) {
+ body.removeEventListener( name, handlers[ name ] );
+ }
+ };
+ } );
}
function Iframe( {
@@ -117,7 +129,6 @@ function Iframe( {
const { documentElement } = contentDocument;
iFrameDocument = contentDocument;
- bubbleEvents( contentDocument );
clearerRef( documentElement );
// Ideally ALL classes that are added through get_body_class should
@@ -182,6 +193,7 @@ function Iframe( {
const disabledRef = useDisabled( { isDisabled: ! readonly } );
const bodyRef = useMergeRefs( [
+ useBubbleEvents( iframeDocument ),
contentRef,
clearerRef,
writingFlowRef,
@@ -251,6 +263,9 @@ function Iframe( {
>
{ iframeDocument &&
createPortal(
+ // We want to prevent React events from bubbling throught the iframe
+ // we bubble these manually.
+ /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */
{
+ // This stopPropagation call ensures React doesn't create a syncthetic event to bubble this event
+ // which would result in two React events being bubbled throught the iframe.
+ event.stopPropagation();
+ const { defaultView } = iframeDocument;
+ const { frameElement } = defaultView;
+ bubbleEvent(
+ event,
+ window.KeyboardEvent,
+ frameElement
+ );
+ } }
>
{ contentResizeListener }
diff --git a/packages/customize-widgets/src/components/customize-widgets/index.js b/packages/customize-widgets/src/components/customize-widgets/index.js
index 3306e438f80e94..d206108398283a 100644
--- a/packages/customize-widgets/src/components/customize-widgets/index.js
+++ b/packages/customize-widgets/src/components/customize-widgets/index.js
@@ -3,7 +3,6 @@
*/
import { useState, useEffect, useRef, createPortal } from '@wordpress/element';
import { SlotFillProvider, Popover } from '@wordpress/components';
-import { ShortcutProvider } from '@wordpress/keyboard-shortcuts';
/**
* Internal dependencies
@@ -68,21 +67,16 @@ export default function CustomizeWidgets( {
);
return (
-
-
-
-
- { activeSidebar }
- { popover }
-
-
-
-
+
+
+
+ { activeSidebar }
+ { popover }
+
+
+
);
}
diff --git a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/index.js b/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/index.js
index 6d8982501d1376..0380e648a17331 100644
--- a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/index.js
+++ b/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/index.js
@@ -7,7 +7,6 @@ import { render, screen } from '@testing-library/react';
* WordPress dependencies
*/
import { EditorKeyboardShortcutsRegister } from '@wordpress/editor';
-import { ShortcutProvider } from '@wordpress/keyboard-shortcuts';
/**
* Internal dependencies
@@ -19,10 +18,10 @@ const noop = () => {};
describe( 'KeyboardShortcutHelpModal', () => {
it( 'should match snapshot when the modal is active', () => {
render(
-
+ <>
-
+ >
);
expect(
@@ -34,13 +33,13 @@ describe( 'KeyboardShortcutHelpModal', () => {
it( 'should not render the modal when inactive', () => {
render(
-
+ <>
-
+ >
);
expect(
diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js
index 00bc911dd7626e..6668001b76773d 100644
--- a/packages/edit-post/src/editor.js
+++ b/packages/edit-post/src/editor.js
@@ -12,7 +12,6 @@ import {
import { useMemo } from '@wordpress/element';
import { SlotFillProvider } from '@wordpress/components';
import { store as coreStore } from '@wordpress/core-data';
-import { ShortcutProvider } from '@wordpress/keyboard-shortcuts';
import { store as preferencesStore } from '@wordpress/preferences';
import { CommandMenu } from '@wordpress/commands';
import { privateApis as coreCommandsPrivateApis } from '@wordpress/core-commands';
@@ -156,25 +155,23 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) {
}
return (
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/packages/edit-site/src/components/app/index.js b/packages/edit-site/src/components/app/index.js
index ca1893c3c17750..cad76b3ea1fb83 100644
--- a/packages/edit-site/src/components/app/index.js
+++ b/packages/edit-site/src/components/app/index.js
@@ -3,7 +3,6 @@
*/
import { SlotFillProvider } from '@wordpress/components';
import { UnsavedChangesWarning } from '@wordpress/editor';
-import { ShortcutProvider } from '@wordpress/keyboard-shortcuts';
import { store as noticesStore } from '@wordpress/notices';
import { useDispatch } from '@wordpress/data';
import { __, sprintf } from '@wordpress/i18n';
@@ -35,16 +34,14 @@ export default function App() {
}
return (
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
}
diff --git a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js
index 899cc02766480f..f3a7b84d5ded68 100644
--- a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js
+++ b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js
@@ -15,7 +15,6 @@ import {
privateApis as blockEditorPrivateApis,
} from '@wordpress/block-editor';
import { privateApis as editPatternsPrivateApis } from '@wordpress/patterns';
-import { ShortcutProvider } from '@wordpress/keyboard-shortcuts';
import { store as preferencesStore } from '@wordpress/preferences';
/**
@@ -98,21 +97,19 @@ export default function WidgetAreasBlockEditorProvider( {
);
return (
-
+
-
-
- { children }
-
-
-
-
+
+ { children }
+
+
+
);
}
diff --git a/packages/keyboard-shortcuts/README.md b/packages/keyboard-shortcuts/README.md
index 4138b44a8e685f..281ef698526e71 100644
--- a/packages/keyboard-shortcuts/README.md
+++ b/packages/keyboard-shortcuts/README.md
@@ -18,7 +18,7 @@ _This package assumes that your code will run in an **ES2015+** environment. If
### ShortcutProvider
-Handles callbacks added to context by `useShortcut`.
+Handles callbacks added to context by `useShortcut`. Adding a provider allows to register contextual shortcuts that are only active when a certain part of the UI is focused.
_Parameters_
diff --git a/packages/keyboard-shortcuts/src/components/shortcut-provider.js b/packages/keyboard-shortcuts/src/components/shortcut-provider.js
index bebad586268a72..9fabf392738f61 100644
--- a/packages/keyboard-shortcuts/src/components/shortcut-provider.js
+++ b/packages/keyboard-shortcuts/src/components/shortcut-provider.js
@@ -12,6 +12,8 @@ const { Provider } = context;
/**
* Handles callbacks added to context by `useShortcut`.
+ * Adding a provider allows to register contextual shortcuts
+ * that are only active when a certain part of the UI is focused.
*
* @param {Object} props Props to pass to `div`.
*
diff --git a/packages/keyboard-shortcuts/src/context.js b/packages/keyboard-shortcuts/src/context.js
index bb885af5c721f3..f2f8846751291f 100644
--- a/packages/keyboard-shortcuts/src/context.js
+++ b/packages/keyboard-shortcuts/src/context.js
@@ -3,4 +3,24 @@
*/
import { createContext } from '@wordpress/element';
-export const context = createContext();
+const globalShortcuts = new Set();
+const globalListener = ( event ) => {
+ for ( const keyboardShortcut of globalShortcuts ) {
+ keyboardShortcut( event );
+ }
+};
+
+export const context = createContext( {
+ add: ( shortcut ) => {
+ if ( globalShortcuts.size === 0 ) {
+ document.addEventListener( 'keydown', globalListener );
+ }
+ globalShortcuts.add( shortcut );
+ },
+ delete: ( shortcut ) => {
+ globalShortcuts.delete( shortcut );
+ if ( globalShortcuts.size === 0 ) {
+ document.removeEventListener( 'keydown', globalListener );
+ }
+ },
+} );
diff --git a/packages/keyboard-shortcuts/src/hooks/use-shortcut.js b/packages/keyboard-shortcuts/src/hooks/use-shortcut.js
index c60f84cc58da74..b6252ebc3680cd 100644
--- a/packages/keyboard-shortcuts/src/hooks/use-shortcut.js
+++ b/packages/keyboard-shortcuts/src/hooks/use-shortcut.js
@@ -17,11 +17,18 @@ import { context } from '../context';
* @param {Object} options Shortcut options.
* @param {boolean} options.isDisabled Whether to disable to shortut.
*/
-export default function useShortcut( name, callback, { isDisabled } = {} ) {
+export default function useShortcut(
+ name,
+ callback,
+ { isDisabled = false } = {}
+) {
const shortcuts = useContext( context );
const isMatch = useShortcutEventMatch();
const callbackRef = useRef();
- callbackRef.current = callback;
+
+ useEffect( () => {
+ callbackRef.current = callback;
+ }, [ callback ] );
useEffect( () => {
if ( isDisabled ) {
@@ -34,9 +41,9 @@ export default function useShortcut( name, callback, { isDisabled } = {} ) {
}
}
- shortcuts.current.add( _callback );
+ shortcuts.add( _callback );
return () => {
- shortcuts.current.delete( _callback );
+ shortcuts.delete( _callback );
};
- }, [ name, isDisabled ] );
+ }, [ name, isDisabled, shortcuts ] );
}
diff --git a/storybook/stories/playground/index.story.js b/storybook/stories/playground/index.story.js
index 380a95f4b79a99..c95da2b4c365ba 100644
--- a/storybook/stories/playground/index.story.js
+++ b/storybook/stories/playground/index.story.js
@@ -10,7 +10,6 @@ import {
WritingFlow,
} from '@wordpress/block-editor';
import { registerCoreBlocks } from '@wordpress/block-library';
-import { ShortcutProvider } from '@wordpress/keyboard-shortcuts';
import '@wordpress/format-library';
/**
@@ -35,26 +34,24 @@ function App() {
return (
-
-
-
-
-
-
-
-
+
+
+
+
+
+
);
}
diff --git a/test/integration/helpers/integration-test-editor.js b/test/integration/helpers/integration-test-editor.js
index 7c2fba15060b45..c8694f88f1fb59 100644
--- a/test/integration/helpers/integration-test-editor.js
+++ b/test/integration/helpers/integration-test-editor.js
@@ -16,7 +16,6 @@ import {
WritingFlow,
} from '@wordpress/block-editor';
import { registerCoreBlocks } from '@wordpress/block-library';
-import { ShortcutProvider } from '@wordpress/keyboard-shortcuts';
import '@wordpress/format-library';
import {
createBlock,
@@ -66,21 +65,19 @@ export function Editor( { testBlocks, settings = {} } ) {
}, [] );
return (
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
);
}