Skip to content
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

Rewrite FocusableIframe as hook API #26753

Merged
merged 4 commits into from
Sep 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
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