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

Components: Introduce portal-based slot / fill alternative, render Popover as slot / fill #2966

Merged
merged 3 commits into from
Oct 16, 2017
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
7 changes: 1 addition & 6 deletions blocks/block-controls/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
/**
* External dependencies
*/
import { Fill } from 'react-slot-fill';

/**
* WordPress dependencies
*/
import { Toolbar } from '@wordpress/components';
import { Fill, Toolbar } from '@wordpress/components';

export default function BlockControls( { controls, children } ) {
return (
Expand Down
2 changes: 1 addition & 1 deletion blocks/editable/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ import {
noop,
} from 'lodash';
import { nodeListToReact } from 'dom-react';
import { Fill } from 'react-slot-fill';
import 'element-closest';

/**
* WordPress dependencies
*/
import { createElement, Component, renderToString } from '@wordpress/element';
import { keycodes } from '@wordpress/utils';
import { Fill } from '@wordpress/components';

/**
* Internal dependencies
Expand Down
4 changes: 2 additions & 2 deletions blocks/inspector-controls/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
* WordPress dependencies
*/
import { Fill } from 'react-slot-fill';
import { Fill } from '@wordpress/components';

/**
* Internal dependencies
Expand Down
2 changes: 1 addition & 1 deletion components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ export { default as PanelHeader } from './panel/header';
export { default as PanelRow } from './panel/row';
export { default as Placeholder } from './placeholder';
export { default as Popover } from './popover';
export { default as PopoverProvider } from './popover/provider';
export { default as ResponsiveWrapper } from './responsive-wrapper';
export { default as SandBox } from './sandbox';
export { default as Spinner } from './spinner';
export { default as Toolbar } from './toolbar';
export { default as Tooltip } from './tooltip';
export { Slot, Fill, Provider as SlotFillProvider } from './slot-fill';

// Higher-Order Components
export { default as withAPIData } from './higher-order/with-api-data';
Expand Down
15 changes: 8 additions & 7 deletions components/popover/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,20 @@ function ToggleButton( { isVisible, toggleVisible } ) {
}
```

If you want Popover elementss to render to a specific location on the page to allow style cascade to take effect, you must render a `PopoverProvider` further up the element tree, specifying a target:
If you want Popover elementss to render to a specific location on the page to allow style cascade to take effect, you must render a `Popover.Slot` further up the element tree:

```
```jsx
import { render } from '@wordpress/element';
import { PopoverContext } from '@wordpress/components';
import App from './app';
import { Popover } from '@wordpress/components';
import Content from './Content';

const app = document.getElementById( 'app' );

render(
<PopoverContext target={ app }>
<App />
</PopoverContext>,
<div>
<Content />
<Popover.Slot />
</div>,
app
);
```
Expand Down
65 changes: 39 additions & 26 deletions components/popover/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import { isEqual, noop } from 'lodash';
/**
* WordPress dependencies
*/
import { createPortal, Component } from '@wordpress/element';
import { Component } from '@wordpress/element';
import { focus, keycodes } from '@wordpress/utils';

/**
* Internal dependencies
*/
import './style.scss';
import PopoverDetectOutside from './detect-outside';
import { Slot, Fill } from '../slot-fill';

const { ESCAPE } = keycodes;

Expand All @@ -25,7 +26,14 @@ const { ESCAPE } = keycodes;
*/
const ARROW_OFFSET = 20;

export class Popover extends Component {
/**
* Name of slot in which popover should fill.
*
* @type {String}
*/
const SLOT_NAME = 'Popover';

class Popover extends Component {
constructor() {
super( ...arguments );

Expand Down Expand Up @@ -222,7 +230,6 @@ export class Popover extends Component {
return null;
}

const { popoverTarget = document.body } = this.context;
const classes = classnames(
'components-popover',
className,
Expand All @@ -234,35 +241,41 @@ export class Popover extends Component {
// within popover as inferring close intent.

/* eslint-disable jsx-a11y/no-static-element-interactions */
return (
<span ref={ this.bindNode( 'anchor' ) }>
{ createPortal(
<PopoverDetectOutside onClickOutside={ onClickOutside }>
<div
ref={ this.bindNode( 'popover' ) }
className={ classes }
tabIndex="0"
{ ...contentProps }
onKeyDown={ this.maybeClose }
>
<div
ref={ this.bindNode( 'content' ) }
className="components-popover__content"
>
{ children }
</div>
</div>
</PopoverDetectOutside>,
popoverTarget
) }
</span>
let content = (
<PopoverDetectOutside onClickOutside={ onClickOutside }>
<div
ref={ this.bindNode( 'popover' ) }
className={ classes }
tabIndex="0"
{ ...contentProps }
onKeyDown={ this.maybeClose }
>
<div
ref={ this.bindNode( 'content' ) }
className="components-popover__content"
>
{ children }
</div>
</div>
</PopoverDetectOutside>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */

// In case there is no slot context in which to render, default to an
// in-place rendering.
const { getSlot } = this.context;
if ( getSlot && getSlot( SLOT_NAME ) ) {
content = <Fill name={ SLOT_NAME }>{ content }</Fill>;
}

return <span ref={ this.bindNode( 'anchor' ) }>{ content }</span>;
}
}

Popover.contextTypes = {
popoverTarget: noop,
getSlot: noop,
};

Popover.Slot = () => <Slot name={ SLOT_NAME } />;

export default Popover;
27 changes: 0 additions & 27 deletions components/popover/provider.js

This file was deleted.

17 changes: 1 addition & 16 deletions components/popover/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import { shallow, mount } from 'enzyme';
/**
* Internal dependencies
*/
import { Popover } from '../';
import PopoverProvider from '../provider';
import Popover from '../';

describe( 'Popover', () => {
describe( '#componentDidUpdate()', () => {
Expand Down Expand Up @@ -273,19 +272,5 @@ describe( 'Popover', () => {

expect( wrapper.find( '.components-popover' ).prop( 'role' ) ).toBe( 'tooltip' );
} );

it( 'should render into provider context', () => {
const element = require( '@wordpress/element' );
jest.spyOn( element, 'createPortal' );
const target = document.createElement( 'div' );

mount(
<PopoverProvider target={ target }>
<Popover isOpen>Hello</Popover>
</PopoverProvider>
);

expect( element.createPortal.mock.calls[ 0 ][ 1 ] ).toBe( target );
} );
} );
} );
49 changes: 49 additions & 0 deletions components/slot-fill/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
Slot Fill
=========

Slot and Fill are a pair of components which enable developers to render elsewhere in a React element tree, a pattern often referred to as "portal" rendering. It is a pattern for component extensibility, where a single Slot may be occupied by an indeterminate number of Fills elsewhere in the application.

Slot Fill is heavily inspired by the [`react-slot-fill` library](https://github.com/camwest/react-slot-fill), but uses React's own portal rendering API, exposed as an unstable API in React 16 and slated to be promoted to a stable API in React 17.

## Usage

At the root of your application, you must render a `SlotFillProvider` which coordinates Slot and Fill rendering.

```jsx
import { SlotFillProvider } from '@wordpress/components';
import { render } from '@wordpress/element';
import App from './app';

render(
<SlotFillProvider>
<App />
</SlotFillProvider>,
document.getElementById( 'app' )
);
```

Then, render a Slot component anywhere in your application, giving it a name:

```jsx
const Toolbar = () => (
<div className="toolbar">
<Slot name="Toolbar" />
</div>
);

Toolbar.Item = ( { children } ) => (
<Fill name="Toolbar">
{ children }
</Fill>
);
```

Any Fill will automatically occupy this Slot space, even if rendered elsewhere in the application.

You can either use the Fill component directly, or a wrapper component type as in the above example to abstract the slot name from consumer awareness.

## Props

The `SlotFillProvider` component does not accept any props.

Both `Slot` and `Fill` accept a `name` string prop, where a `Slot` with a given `name` will render the `children` of any associated `Fill`s.
53 changes: 53 additions & 0 deletions components/slot-fill/fill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* External dependencies
*/
import { noop } from 'lodash';

/**
* WordPress dependencies
*/
import { Component, createPortal } from '@wordpress/element';

class Fill extends Component {
componentDidMount() {
const { registerFill = noop } = this.context;

registerFill( this.props.name, this );
}

componentWillUnmount() {
const { unregisterFill = noop } = this.context;

unregisterFill( this.props.name, this );
}

componentWillReceiveProps( nextProps ) {
const { name } = nextProps;
const {
unregisterFill = noop,
registerFill = noop,
} = this.context;

if ( this.props.name !== name ) {
unregisterFill( this.props.name, this );
registerFill( name, this );
}
}

render() {
const { getSlot = noop } = this.context;
const { name, children } = this.props;

const slot = getSlot( name );

return slot ? createPortal( children, slot ) : null;
}
}

Fill.contextTypes = {
getSlot: noop,
registerFill: noop,
unregisterFill: noop,
};

export default Fill;
12 changes: 12 additions & 0 deletions components/slot-fill/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Internal dependencies
*/
import Slot from './slot';
import Fill from './fill';
import Provider from './provider';

export { Slot };
export { Fill };
export { Provider };

export default { Slot, Fill, Provider };
Loading