Skip to content

Commit

Permalink
Rewrite FocusableIframe as hook API (#26753)
Browse files Browse the repository at this point in the history
  • Loading branch information
ellatrix authored Sep 15, 2021
1 parent 4fc88f8 commit 33e4f1f
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 118 deletions.
103 changes: 41 additions & 62 deletions packages/block-library/src/embed/wp-embed-preview.js
Original file line number Diff line number Diff line change
@@ -1,96 +1,75 @@
/**
* WordPress dependencies
*/
import { useMergeRefs, useFocusableIframe } from '@wordpress/compose';
import { useRef, useEffect, useMemo } from '@wordpress/element';

/** @typedef {import('@wordpress/element').WPSyntheticEvent} WPSyntheticEvent */

const attributeMap = {
class: 'className',
frameborder: 'frameBorder',
marginheight: 'marginHeight',
marginwidth: 'marginWidth',
};

export default function WpEmbedPreview( { html } ) {
const ref = useRef();
const props = useMemo( () => {
const doc = new window.DOMParser().parseFromString( html, 'text/html' );
const iframe = doc.querySelector( 'iframe' );
const iframeProps = {};

if ( ! iframe ) return iframeProps;

Array.from( iframe.attributes ).forEach( ( { name, value } ) => {
if ( name === 'style' ) return;
iframeProps[ attributeMap[ name ] || name ] = value;
} );

return iframeProps;
}, [ html ] );

useEffect( () => {
const { ownerDocument } = ref.current;
const { defaultView } = ownerDocument;

/**
* Checks for WordPress embed events signaling the height change when iframe
* content loads or iframe's window is resized. The event is sent from
* WordPress core via the window.postMessage API.
* Checks for WordPress embed events signaling the height change when
* iframe content loads or iframe's window is resized. The event is
* sent from WordPress core via the window.postMessage API.
*
* References:
* window.postMessage: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
* WordPress core embed-template on load: https://github.com/WordPress/WordPress/blob/HEAD/wp-includes/js/wp-embed-template.js#L143
* WordPress core embed-template on resize: https://github.com/WordPress/WordPress/blob/HEAD/wp-includes/js/wp-embed-template.js#L187
* window.postMessage:
* https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
* WordPress core embed-template on load:
* https://github.com/WordPress/WordPress/blob/HEAD/wp-includes/js/wp-embed-template.js#L143
* WordPress core embed-template on resize:
* https://github.com/WordPress/WordPress/blob/HEAD/wp-includes/js/wp-embed-template.js#L187
*
* @param {WPSyntheticEvent} event Message event.
* @param {MessageEvent} event Message event.
*/
function resizeWPembeds( { data: { secret, message, value } = {} } ) {
if (
[ secret, message, value ].some(
( attribute ) => ! attribute
) ||
message !== 'height'
) {
return;
}

ownerDocument
.querySelectorAll( `iframe[data-secret="${ secret }"` )
.forEach( ( iframe ) => {
if ( +iframe.height !== value ) {
iframe.height = value;
}
} );
}

/**
* Checks whether the wp embed iframe is the activeElement,
* if it is dispatch a focus event.
*/
function checkFocus() {
const { activeElement } = ownerDocument;

if (
activeElement.tagName !== 'IFRAME' ||
activeElement.parentNode !== ref.current
) {
if ( message !== 'height' || secret !== props[ 'data-secret' ] ) {
return;
}

activeElement.focus();
ref.current.height = value;
}

defaultView.addEventListener( 'message', resizeWPembeds );
defaultView.addEventListener( 'blur', checkFocus );

return () => {
defaultView.removeEventListener( 'message', resizeWPembeds );
defaultView.removeEventListener( 'blur', checkFocus );
};
}, [] );

const __html = useMemo( () => {
const doc = new window.DOMParser().parseFromString( html, 'text/html' );
const iframe = doc.querySelector( 'iframe' );

if ( iframe ) {
iframe.removeAttribute( 'style' );
}

const blockQuote = doc.querySelector( 'blockquote' );

if ( blockQuote ) {
blockQuote.style.display = 'none';
}

return doc.body.innerHTML;
}, [ html ] );

return (
<div
ref={ ref }
className="wp-block-embed__wrapper"
dangerouslySetInnerHTML={ { __html } }
/>
<div className="wp-block-embed__wrapper">
<iframe
ref={ useMergeRefs( [ ref, useFocusableIframe() ] ) }
title={ props.title }
{ ...props }
/>
</div>
);
}
2 changes: 2 additions & 0 deletions packages/components/src/focusable-iframe/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Focusable Iframe

**Deprecated**

`<FocusableIframe />` is a component rendering an `iframe` element enhanced to support focus events. By default, it is not possible to detect when an iframe is focused or clicked within. This enhanced component uses a technique which checks whether the target of a window `blur` event is the iframe, inferring that this has resulted in the focus of the iframe. It dispatches an emulated [`FocusEvent`](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent) on the iframe element with event bubbling, so a parent component binding its own `onFocus` event will account for focus transitioning within the iframe.

## Usage
Expand Down
35 changes: 6 additions & 29 deletions packages/components/src/focusable-iframe/index.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,14 @@
/**
* WordPress dependencies
*/
import { useEffect, useRef } from '@wordpress/element';
import { useMergeRefs } from '@wordpress/compose';
import { useMergeRefs, useFocusableIframe } from '@wordpress/compose';
import deprecated from '@wordpress/deprecated';

export default function FocusableIframe( { iframeRef, ...props } ) {
const fallbackRef = useRef();
const ref = useMergeRefs( [ iframeRef, fallbackRef ] );

useEffect( () => {
const iframe = fallbackRef.current;
const { ownerDocument } = iframe;
const { defaultView } = ownerDocument;

/**
* Checks whether the iframe is the activeElement, inferring that it has
* then received focus, and calls the `onFocus` prop callback.
*/
function checkFocus() {
if ( ownerDocument.activeElement !== iframe ) {
return;
}

iframe.focus();
}

defaultView.addEventListener( 'blur', checkFocus );

return () => {
defaultView.removeEventListener( 'blur', checkFocus );
};
}, [] );

const ref = useMergeRefs( [ iframeRef, useFocusableIframe() ] );
deprecated( 'wp.components.FocusableIframe', {
alternative: 'wp.compose.useFocusableIframe',
} );
// Disable reason: The rendered iframe is a pass-through component,
// assigning props inherited from the rendering parent. It's the
// responsibility of the parent to assign a title.
Expand Down
19 changes: 0 additions & 19 deletions packages/components/src/focusable-iframe/stories/index.js

This file was deleted.

10 changes: 3 additions & 7 deletions packages/components/src/sandbox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ import {
useState,
useEffect,
} from '@wordpress/element';

/**
* Internal dependencies
*/
import FocusableIframe from '../focusable-iframe';
import { useFocusableIframe, useMergeRefs } from '@wordpress/compose';

const observeAndResizeJS = `
( function() {
Expand Down Expand Up @@ -238,8 +234,8 @@ export default function Sandbox( {
}, [ html ] );

return (
<FocusableIframe
iframeRef={ ref }
<iframe
ref={ useMergeRefs( [ ref, useFocusableIframe() ] ) }
title={ title }
className="components-sandbox"
sandbox="allow-scripts allow-same-origin allow-presentation"
Expand Down
9 changes: 9 additions & 0 deletions packages/compose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,15 @@ _Returns_

- `import('lodash').DebouncedFunc<TFunc>`: Debounced function.

### useFocusableIframe

Dispatches a bubbling focus event when the iframe receives focus. Use
`onFocus` as usual on the iframe or a parent element.

_Returns_

- `Object`: Ref to pass to the iframe.

### useFocusOnMount

Hook used to focus the first tabbable element on mount.
Expand Down
29 changes: 29 additions & 0 deletions packages/compose/src/hooks/use-focusable-iframe/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# useFocusableIframe

By default, it is not possible to detect when an iframe is focused or clicked
within. This hook uses a technique which checks whether the target of a window
`blur` event is the iframe, inferring that this has resulted in the focus of the
iframe. It dispatches an emulated
[`FocusEvent`](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent) on
the iframe element with event bubbling, so a parent component binding its own
`onFocus` event will account for focus transitioning within the iframe.

## Usage

Use with an `iframe`. You may pass `onFocus` directly as the callback to be
invoked when the iframe receives focus, or on an ancestor component since the
event will bubble.

```jsx
import { useFocusableIframe } from '@wordpress/compose';

const MyFocusableIframe = () => {
return(
<iframe
ref={ useFocusableIframe() }
src="/my-iframe-url"
onFocus={ () => console.log( 'iframe is focused' ) }
/>
);
};
```
34 changes: 34 additions & 0 deletions packages/compose/src/hooks/use-focusable-iframe/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Internal dependencies
*/
import useRefEffect from '../use-ref-effect';

/**
* Dispatches a bubbling focus event when the iframe receives focus. Use
* `onFocus` as usual on the iframe or a parent element.
*
* @return {Object} Ref to pass to the iframe.
*/
export default function useFocusableIframe() {
return useRefEffect( ( element ) => {
const { ownerDocument } = element;
if ( ! ownerDocument ) return;
const { defaultView } = ownerDocument;
if ( ! defaultView ) return;

/**
* Checks whether the iframe is the activeElement, inferring that it has
* then received focus, and dispatches a focus event.
*/
function checkFocus() {
if ( ownerDocument && ownerDocument.activeElement === element ) {
/** @type {HTMLElement} */ ( element ).focus();
}
}

defaultView.addEventListener( 'blur', checkFocus );
return () => {
defaultView.removeEventListener( 'blur', checkFocus );
};
}, [] );
}
1 change: 1 addition & 0 deletions packages/compose/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ export { default as useThrottle } from './hooks/use-throttle';
export { default as useMergeRefs } from './hooks/use-merge-refs';
export { default as useRefEffect } from './hooks/use-ref-effect';
export { default as __experimentalUseDropZone } from './hooks/use-drop-zone';
export { default as useFocusableIframe } from './hooks/use-focusable-iframe';
2 changes: 1 addition & 1 deletion packages/e2e-tests/specs/editor/various/embedding.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
const MOCK_EMBED_WORDPRESS_SUCCESS_RESPONSE = {
url: 'https://wordpress.org/gutenberg/handbook/block-api/attributes/',
html:
'<div class="wp-embedded-content" data-secret="shhhh it is a secret">WordPress embed</div>',
'<div class="wp-embedded-content" data-secret="shhhh it is a secret"></div>',
type: 'rich',
provider_name: 'WordPress',
provider_url: 'https://wordpress.org',
Expand Down

0 comments on commit 33e4f1f

Please sign in to comment.