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

Blocks: (Re-)enable selection of embed block by clicking #5730

Merged
merged 8 commits into from
Apr 13, 2018
2 changes: 2 additions & 0 deletions blocks/library/embed/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ function getEmbedBlockSettings( { title, icon, category = 'embed', transforms, k
edit: class extends Component {
constructor() {
super( ...arguments );

this.doServerSideRender = this.doServerSideRender.bind( this );

this.state = {
html: '',
type: '',
Expand Down
39 changes: 39 additions & 0 deletions components/focusable-iframe/README.md
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.
69 changes: 69 additions & 0 deletions components/focusable-iframe/index.js
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 );
31 changes: 31 additions & 0 deletions components/higher-order/with-global-events/README.md
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.
Copy link
Contributor

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


## 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 );
```
64 changes: 64 additions & 0 deletions components/higher-order/with-global-events/index.js
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;
45 changes: 45 additions & 0 deletions components/higher-order/with-global-events/listener.js
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;
93 changes: 93 additions & 0 deletions components/higher-order/with-global-events/test/index.js
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> );
Copy link
Member

Choose a reason for hiding this comment

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

I started to think if we should introduce our own mount or further enrich enzyme to call unmount behind the scenes when the test finishes.

Copy link
Member

Choose a reason for hiding this comment

The 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;
	}
} );

Copy link
Member Author

Choose a reason for hiding this comment

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

I started to think if we should introduce our own mount or further enrich enzyme to call unmount behind the scenes when the test finishes.

I'd like this 👍

Copy link
Member

Choose a reason for hiding this comment

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

I will check it next week. jest.mount( element ) or jest.shallow( element ) might also save some keystrokes :)

}

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 );
} );
} );
Loading