-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Blocks: (Re-)enable selection of embed block by clicking #5730
Changes from all commits
ad0f3df
010ca4f
09e8645
bbc75ad
1bbeefd
834bb42
5f21a8f
8a6c493
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
Focusable Iframe | ||
================ | ||
|
||
`<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 | ||
|
||
Use as you would a standard `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 { FocusableIframe } from '@wordpress/components'; | ||
|
||
function MyIframe() { | ||
return ( | ||
<FocusableIframe | ||
src="https://example.com" | ||
onFocus={ /* ... */ } | ||
/> | ||
); | ||
} | ||
``` | ||
|
||
## Props | ||
|
||
Any props aside from those listed below will be passed to the `FocusableIframe` will be passed through to the underlying `iframe` element. | ||
|
||
### `onFocus` | ||
|
||
- Type: `Function` | ||
- Required: No | ||
|
||
Callback to invoke when iframe receives focus. Passes an emulated `FocusEvent` object as the first argument. | ||
|
||
### `iframeRef` | ||
|
||
- Type: `wp.element.Ref` | ||
- Required: No | ||
|
||
If a reference to the underlying DOM element is needed, pass `iframeRef` as the result of a `wp.element.createRef` called from your component. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { omit } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { Component, createRef } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import withGlobalEvents from '../higher-order/with-global-events'; | ||
|
||
/** | ||
* Browser dependencies | ||
*/ | ||
|
||
const { FocusEvent } = window; | ||
|
||
class FocusableIframe extends Component { | ||
constructor( props ) { | ||
super( ...arguments ); | ||
|
||
this.checkFocus = this.checkFocus.bind( this ); | ||
|
||
this.node = props.iframeRef || createRef(); | ||
} | ||
|
||
/** | ||
* Checks whether the iframe is the activeElement, inferring that it has | ||
* then received focus, and calls the `onFocus` prop callback. | ||
*/ | ||
checkFocus() { | ||
const iframe = this.node.current; | ||
|
||
if ( document.activeElement !== iframe ) { | ||
return; | ||
} | ||
|
||
const focusEvent = new FocusEvent( 'focus', { bubbles: true } ); | ||
iframe.dispatchEvent( focusEvent ); | ||
|
||
const { onFocus } = this.props; | ||
if ( onFocus ) { | ||
onFocus( focusEvent ); | ||
} | ||
} | ||
|
||
render() { | ||
// 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. | ||
|
||
/* eslint-disable jsx-a11y/iframe-has-title */ | ||
return ( | ||
<iframe | ||
ref={ this.node } | ||
{ ...omit( this.props, [ 'iframeRef', 'onFocus' ] ) } | ||
/> | ||
); | ||
/* eslint-enable jsx-a11y/iframe-has-title */ | ||
} | ||
} | ||
|
||
export default withGlobalEvents( { | ||
blur: 'checkFocus', | ||
} )( FocusableIframe ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
withGlobalEvents | ||
================ | ||
|
||
`withGlobalEvents` is a higher-order component used to facilitate responding to global events, where one would otherwise use `window.addEventListener`. | ||
|
||
On behalf of the consuming developer, the higher-order component manages: | ||
|
||
- Unbinding when the component unmounts. | ||
- Binding at most a single event handler for the entire application. | ||
|
||
## Usage | ||
|
||
Pass an object where keys correspond to the DOM event type, the value the name of the method on the original component's instance which handles the event. | ||
|
||
```js | ||
import { withGlobalEvents } from '@wordpress/components'; | ||
|
||
class ResizingComponent extends Component { | ||
handleResize() { | ||
// ... | ||
} | ||
|
||
render() { | ||
// ... | ||
} | ||
} | ||
|
||
export default withGlobalEvents( { | ||
resize: 'handleResize', | ||
} )( ResizingComponent ); | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { forEach } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { | ||
Component, | ||
createRef, | ||
createHigherOrderComponent, | ||
} from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import Listener from './listener'; | ||
|
||
/** | ||
* Listener instance responsible for managing document event handling. | ||
* | ||
* @type {Listener} | ||
*/ | ||
const listener = new Listener(); | ||
|
||
function withGlobalEvents( eventTypesToHandlers ) { | ||
return createHigherOrderComponent( ( WrappedComponent ) => { | ||
return class extends Component { | ||
constructor() { | ||
super( ...arguments ); | ||
|
||
this.handleEvent = this.handleEvent.bind( this ); | ||
|
||
this.ref = createRef(); | ||
} | ||
|
||
componentDidMount() { | ||
forEach( eventTypesToHandlers, ( handler, eventType ) => { | ||
listener.add( eventType, this ); | ||
} ); | ||
} | ||
|
||
componentWillUnmount() { | ||
forEach( eventTypesToHandlers, ( handler, eventType ) => { | ||
listener.remove( eventType, this ); | ||
} ); | ||
} | ||
|
||
handleEvent( event ) { | ||
const handler = eventTypesToHandlers[ event.type ]; | ||
if ( typeof this.ref.current[ handler ] === 'function' ) { | ||
this.ref.current[ handler ]( event ); | ||
} | ||
} | ||
|
||
render() { | ||
return <WrappedComponent ref={ this.ref } { ...this.props } />; | ||
} | ||
}; | ||
}, 'withGlobalEvents' ); | ||
} | ||
|
||
export default withGlobalEvents; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { forEach, without } from 'lodash'; | ||
|
||
/** | ||
* Class responsible for orchestrating event handling on the global window, | ||
* binding a single event to be shared across all handling instances, and | ||
* removing the handler when no instances are listening for the event. | ||
*/ | ||
class Listener { | ||
constructor() { | ||
this.listeners = {}; | ||
|
||
this.handleEvent = this.handleEvent.bind( this ); | ||
} | ||
|
||
add( eventType, instance ) { | ||
if ( ! this.listeners[ eventType ] ) { | ||
// Adding first listener for this type, so bind event. | ||
window.addEventListener( eventType, this.handleEvent ); | ||
this.listeners[ eventType ] = []; | ||
} | ||
|
||
this.listeners[ eventType ].push( instance ); | ||
} | ||
|
||
remove( eventType, instance ) { | ||
this.listeners[ eventType ] = without( this.listeners[ eventType ], instance ); | ||
|
||
if ( ! this.listeners[ eventType ].length ) { | ||
// Removing last listener for this type, so unbind event. | ||
window.removeEventListener( eventType, this.handleEvent ); | ||
delete this.listeners[ eventType ]; | ||
} | ||
} | ||
|
||
handleEvent( event ) { | ||
forEach( this.listeners[ event.type ], ( instance ) => { | ||
instance.handleEvent( event ); | ||
} ); | ||
} | ||
} | ||
|
||
export default Listener; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { mount } from 'enzyme'; | ||
|
||
/** | ||
* External dependencies | ||
*/ | ||
import { Component } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import withGlobalEvents from '../'; | ||
import Listener from '../listener'; | ||
|
||
jest.mock( '../listener', () => { | ||
const ActualListener = require.requireActual( '../listener' ).default; | ||
|
||
return class extends ActualListener { | ||
constructor() { | ||
super( ...arguments ); | ||
|
||
this.constructor._instance = this; | ||
|
||
jest.spyOn( this, 'add' ); | ||
jest.spyOn( this, 'remove' ); | ||
} | ||
}; | ||
} ); | ||
|
||
describe( 'withGlobalEvents', () => { | ||
let wrapper; | ||
|
||
class OriginalComponent extends Component { | ||
handleResize( event ) { | ||
this.props.onResize( event ); | ||
} | ||
|
||
render() { | ||
return <div>{ this.props.children }</div>; | ||
} | ||
} | ||
|
||
beforeAll( () => { | ||
jest.spyOn( OriginalComponent.prototype, 'handleResize' ); | ||
} ); | ||
|
||
beforeEach( () => { | ||
jest.clearAllMocks(); | ||
} ); | ||
|
||
afterEach( () => { | ||
if ( wrapper ) { | ||
wrapper.unmount(); | ||
wrapper = null; | ||
} | ||
} ); | ||
|
||
function mountEnhancedComponent( props ) { | ||
const EnhancedComponent = withGlobalEvents( { | ||
resize: 'handleResize', | ||
} )( OriginalComponent ); | ||
|
||
wrapper = mount( <EnhancedComponent { ...props }>Hello</EnhancedComponent> ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I started to think if we should introduce our own There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let wrapper;
export const mount = ( ...params ) => {
wrapper = originalMount( ...params );
return wrapper;
};
afterEach( () => {
if ( wrapper ) {
wrapper.unmount();
wrapper = null;
}
} ); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'd like this 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will check it next week. |
||
} | ||
|
||
it( 'renders with original component', () => { | ||
mountEnhancedComponent(); | ||
|
||
expect( wrapper.childAt( 0 ).childAt( 0 ).type() ).toBe( 'div' ); | ||
expect( wrapper.childAt( 0 ).text() ).toBe( 'Hello' ); | ||
} ); | ||
|
||
it( 'binds events from passed object', () => { | ||
mountEnhancedComponent(); | ||
|
||
expect( Listener._instance.add ).toHaveBeenCalledWith( 'resize', wrapper.instance() ); | ||
} ); | ||
|
||
it( 'handles events', () => { | ||
const onResize = jest.fn(); | ||
|
||
mountEnhancedComponent( { onResize } ); | ||
|
||
const event = { type: 'resize' }; | ||
|
||
Listener._instance.handleEvent( event ); | ||
|
||
expect( OriginalComponent.prototype.handleResize ).toHaveBeenCalledWith( event ); | ||
expect( onResize ).toHaveBeenCalledWith( event ); | ||
} ); | ||
} ); |
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.
I like this component, especially this part 👍 Nice job