diff --git a/blocks/color-palette/index.js b/blocks/color-palette/index.js
index 476f6110ffda78..fdb12c5d4ce03f 100644
--- a/blocks/color-palette/index.js
+++ b/blocks/color-palette/index.js
@@ -7,8 +7,7 @@ import { ChromePicker } from 'react-color';
/**
* WordPress dependencies
*/
-import { Component } from '@wordpress/element';
-import { Popover } from '@wordpress/components';
+import { Dropdown } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
/**
@@ -17,94 +16,62 @@ import { __, sprintf } from '@wordpress/i18n';
import './style.scss';
import withEditorSettings from '../with-editor-settings';
-class ColorPalette extends Component {
- constructor() {
- super( ...arguments );
- this.state = {
- opened: false,
- };
- this.togglePicker = this.togglePicker.bind( this );
- this.closeOnClickOutside = this.closeOnClickOutside.bind( this );
- this.bindToggleNode = this.bindToggleNode.bind( this );
- }
+function ColorPalette( { colors, value, onChange } ) {
+ return (
+
+ { colors.map( ( color ) => {
+ const style = { color: color };
+ const className = classnames( 'blocks-color-palette__item', { 'is-active': value === color } );
- togglePicker() {
- this.setState( ( state ) => ( { opened: ! state.opened } ) );
- }
-
- closeOnClickOutside( event ) {
- const { opened } = this.state;
- if ( opened && ! this.toggleNode.contains( event.target ) ) {
- this.togglePicker();
- }
- }
-
- bindToggleNode( node ) {
- this.toggleNode = node;
- }
-
- render() {
- const { colors, value, onChange } = this.props;
- return (
-
- { colors.map( ( color ) => {
- const style = { color: color };
- const className = classnames( 'blocks-color-palette__item', { 'is-active': value === color } );
-
- return (
-
- onChange( value === color ? undefined : color ) }
- aria-label={ sprintf( __( 'Color: %s' ), color ) }
- aria-pressed={ value === color }
- />
-
- );
- } ) }
+ return (
+
+ onChange( value === color ? undefined : color ) }
+ aria-label={ sprintf( __( 'Color: %s' ), color ) }
+ aria-pressed={ value === color }
+ />
+
+ );
+ } ) }
-
+
(
-
- {
- onChange( color.hex );
- this.togglePicker();
- } }
- style={ { width: '100%' } }
- disableAlpha
- />
-
-
+ ) }
+ renderContent={ () => (
+
onChange( color.hex ) }
+ style={ { width: '100%' } }
+ disableAlpha
+ />
+ ) }
+ />
-
- onChange( undefined ) }
- aria-label={ __( 'Remove color' ) }
- >
-
-
-
+
+ onChange( undefined ) }
+ aria-label={ __( 'Remove color' ) }
+ >
+
+
- );
- }
+
+ );
}
export default withEditorSettings(
diff --git a/components/dropdown/README.md b/components/dropdown/README.md
new file mode 100644
index 00000000000000..df196372b96346
--- /dev/null
+++ b/components/dropdown/README.md
@@ -0,0 +1,77 @@
+Popover
+=======
+
+Dropdown is a React component to render a button that opens a floating content modal when clicked.
+This components takes care of updating the state of the dropdown menu (opened/closed), handles closing the menu when clicking outside
+and uses render props to render the button and the content.
+
+## Usage
+
+
+```jsx
+import { Dropdown } from '@wordpress/components';
+
+function MyDropdownMenu() {
+ return (
+
(
+
+ Toggle Popover!
+
+ ) }
+ renderContent={ () => (
+ This is the content of the popover.
+ ) }
+ >
+ );
+}
+```
+
+## Props
+
+The component accepts the following props. Props not included in this set will be applied to the element wrapping Popover content.
+
+### className
+
+className of the global container
+
+- Type: `String`
+- Required: No
+
+### contentClassName
+
+If you want to target the dropdown menu for styling purposes, you need to provide a contentClassName because it's not being rendered as a children of the container nodee.
+
+- Type: `String`
+- Required: No
+
+### position
+
+The direction in which the popover should open relative to its parent node. Specify y- and x-axis as a space-separated string. Supports `"top"`, `"bottom"` y axis, and `"left"`, `"center"`, `"right"` x axis.
+
+- Type: `String`
+- Required: No
+- Default: `"top center"`
+
+## renderToggle
+
+A callback invoked to render the Dropdown Toggle Button.
+
+- Type: `Function`
+- Required: Yes
+
+The first argument of the callback is an object containing the following properties:
+
+ - `isOpen`: whether the dropdown menu is opened or not
+ - `onToggle`: A function switching the dropdown menu's state from open to closed and vice versa
+ - `onClose`: A function that closes the menu if invoked
+
+## renderContent
+
+A callback invoked to render the content of the dropdown menu. Its first argument is the same as the `renderToggle` prop.
+
+- Type: `Function`
+- Required: Yes
diff --git a/components/dropdown/index.js b/components/dropdown/index.js
new file mode 100644
index 00000000000000..8d932b393d15f2
--- /dev/null
+++ b/components/dropdown/index.js
@@ -0,0 +1,63 @@
+/**
+ * WordPress Dependeencies
+ */
+import { Component } from '@wordpress/element';
+
+/**
+ * Internal Dependencies
+ */
+import Popover from '../popover';
+
+class Dropdown extends Component {
+ constructor() {
+ super( ...arguments );
+ this.toggle = this.toggle.bind( this );
+ this.close = this.close.bind( this );
+ this.clickOutside = this.clickOutside.bind( this );
+ this.bindContainer = this.bindContainer.bind( this );
+ this.state = {
+ isOpen: false,
+ };
+ }
+
+ bindContainer( ref ) {
+ this.container = ref;
+ }
+
+ toggle() {
+ this.setState( ( state ) => ( {
+ isOpen: ! state.isOpen,
+ } ) );
+ }
+
+ clickOutside( event ) {
+ if ( ! this.container.contains( event.target ) ) {
+ this.close();
+ }
+ }
+
+ close() {
+ this.setState( { isOpen: false } );
+ }
+
+ render() {
+ const { isOpen } = this.state;
+ const { renderContent, renderToggle, position = 'bottom', className, contentClassName } = this.props;
+ const args = { isOpen, onToggle: this.toggle, onClose: this.close };
+ return (
+
+ { renderToggle( args ) }
+
+ { renderContent( args ) }
+
+
+ );
+ }
+}
+
+export default Dropdown;
diff --git a/components/dropdown/test/index.js b/components/dropdown/test/index.js
new file mode 100644
index 00000000000000..9c936342703bad
--- /dev/null
+++ b/components/dropdown/test/index.js
@@ -0,0 +1,53 @@
+/**
+ * External dependencies
+ */
+import { mount } from 'enzyme';
+
+/**
+ * Internal dependencies
+ */
+import Dropdown from '../';
+
+describe( 'Dropdown', () => {
+ it( 'should toggle the dropdown properly', () => {
+ const wrapper = mount( (
+ Toggleee
+ ) }
+ renderContent={ () => 'content' }
+ /> );
+
+ const button = wrapper.find( 'button' );
+ const popover = wrapper.find( 'Popover' );
+
+ expect( button ).toHaveLength( 1 );
+ expect( popover.prop( 'isOpen' ) ).toBe( false );
+ expect( button.prop( 'aria-expanded' ) ).toBe( false );
+ button.simulate( 'click' );
+ expect( popover.prop( 'isOpen' ) ).toBe( true );
+ expect( button.prop( 'aria-expanded' ) ).toBe( true );
+ } );
+
+ it( 'should close the dropdown when calling onClose', () => {
+ const wrapper = mount( [
+ Toggleee ,
+ closee ,
+ ] }
+ renderContent={ () => 'content' }
+ /> );
+
+ const openButton = wrapper.find( '.open' );
+ const closeButton = wrapper.find( '.close' );
+ const popover = wrapper.find( 'Popover' );
+ expect( popover.prop( 'isOpen' ) ).toBe( false );
+ openButton.simulate( 'click' );
+ expect( popover.prop( 'isOpen' ) ).toBe( true );
+ closeButton.simulate( 'click' );
+ expect( popover.prop( 'isOpen' ) ).toBe( false );
+ } );
+} );
diff --git a/components/index.js b/components/index.js
index c58a683f234da1..1eb3dd2516e595 100644
--- a/components/index.js
+++ b/components/index.js
@@ -6,6 +6,7 @@ export { default as ClipboardButton } from './clipboard-button';
export { default as Dashicon } from './dashicon';
export { default as DropZone } from './drop-zone';
export { default as DropZoneProvider } from './drop-zone/provider';
+export { default as Dropdown } from './dropdown';
export { default as DropdownMenu } from './dropdown-menu';
export { default as ExternalLink } from './external-link';
export { default as FormFileUpload } from './form-file-upload';
diff --git a/editor/inserter/index.js b/editor/inserter/index.js
index 6929b1e3d8ac3c..c67c8f3ca3bf4c 100644
--- a/editor/inserter/index.js
+++ b/editor/inserter/index.js
@@ -7,8 +7,7 @@ import { connect } from 'react-redux';
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { Component } from '@wordpress/element';
-import { Popover, IconButton } from '@wordpress/components';
+import { Dropdown, IconButton } from '@wordpress/components';
import { createBlock } from '@wordpress/blocks';
/**
@@ -18,84 +17,37 @@ import InserterMenu from './menu';
import { getBlockInsertionPoint, getEditorMode } from '../selectors';
import { insertBlock, hideInsertionPoint } from '../actions';
-class Inserter extends Component {
- constructor() {
- super( ...arguments );
-
- this.toggle = this.toggle.bind( this );
- this.close = this.close.bind( this );
- this.closeOnClickOutside = this.closeOnClickOutside.bind( this );
- this.bindNode = this.bindNode.bind( this );
- this.insertBlock = this.insertBlock.bind( this );
-
- this.state = {
- opened: false,
- };
- }
-
- toggle() {
- this.setState( ( state ) => ( {
- opened: ! state.opened,
- } ) );
- }
-
- close() {
- this.setState( {
- opened: false,
- } );
- }
-
- closeOnClickOutside( event ) {
- if ( ! this.node.contains( event.target ) ) {
- this.close();
- }
- }
-
- bindNode( node ) {
- this.node = node;
- }
-
- insertBlock( name ) {
- const {
- insertionPoint,
- onInsertBlock,
- } = this.props;
-
- onInsertBlock(
- name,
- insertionPoint
- );
-
- this.close();
- }
-
- render() {
- const { opened } = this.state;
- const { position, children } = this.props;
-
- return (
-
+function Inserter( { position, children, onInsertBlock, insertionPoint } ) {
+ return (
+
(
{ children }
-
-
-
-
- );
- }
+ ) }
+ renderContent={ ( { onClose } ) => {
+ const onInsert = ( name ) => {
+ onInsertBlock(
+ name,
+ insertionPoint
+ );
+
+ onClose();
+ };
+
+ return ;
+ } }
+ />
+ );
}
export default connect(
diff --git a/editor/sidebar/post-schedule/index.js b/editor/sidebar/post-schedule/index.js
index 51b81ffad80cba..be659a250d6dea 100644
--- a/editor/sidebar/post-schedule/index.js
+++ b/editor/sidebar/post-schedule/index.js
@@ -10,9 +10,8 @@ import { flowRight } from 'lodash';
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { Component } from '@wordpress/element';
import { dateI18n, settings } from '@wordpress/date';
-import { PanelRow, Popover, withAPIData } from '@wordpress/components';
+import { PanelRow, Dropdown, withAPIData } from '@wordpress/components';
/**
* Internal dependencies
@@ -22,82 +21,62 @@ import PostScheduleClock from './clock';
import { getEditedPostAttribute } from '../../selectors';
import { editPost } from '../../actions';
-export class PostSchedule extends Component {
- constructor() {
- super( ...arguments );
-
- this.toggleDialog = this.toggleDialog.bind( this );
- this.closeDialog = this.closeDialog.bind( this );
-
- this.state = {
- opened: false,
- };
- }
-
- toggleDialog() {
- this.setState( ( state ) => ( { opened: ! state.opened } ) );
- }
-
- closeDialog() {
- this.setState( { opened: false } );
+export function PostSchedule( { date, onUpdateDate, user } ) {
+ if ( ! user.data || ! user.data.capabilities.publish_posts ) {
+ return null;
}
- render() {
- const { date, onUpdateDate, user } = this.props;
-
- if ( ! user.data || ! user.data.capabilities.publish_posts ) {
- return null;
- }
-
- const momentDate = date ? moment( date ) : moment();
- const label = date
- ? dateI18n( settings.formats.datetime, date )
- : __( 'Immediately' );
- const handleChange = ( newDate ) => {
- onUpdateDate( newDate.format( 'YYYY-MM-DDTHH:mm:ss' ) );
- };
+ const momentDate = date ? moment( date ) : moment();
+ const label = date
+ ? dateI18n( settings.formats.datetime, date )
+ : __( 'Immediately' );
+ const handleChange = ( newDate ) => {
+ onUpdateDate( newDate.format( 'YYYY-MM-DDTHH:mm:ss' ) );
+ };
// To know if the current timezone is a 12 hour time with look for "a" in the time format
// We also make sure this a is not escaped by a "/"
- const is12HourTime = /a(?!\\)/i.test(
- settings.formats.time
- .toLowerCase() // Test only the lower case a
- .replace( /\\\\/g, '' ) // Replace "//" with empty strings
- .split( '' ).reverse().join( '' ) // Reverse the string and test for "a" not followed by a slash
- );
+ const is12HourTime = /a(?!\\)/i.test(
+ settings.formats.time
+ .toLowerCase() // Test only the lower case a
+ .replace( /\\\\/g, '' ) // Replace "//" with empty strings
+ .split( '' ).reverse().join( '' ) // Reverse the string and test for "a" not followed by a slash
+ );
- return (
-
- { __( 'Publish' ) }
-
- { label }
-
+ { __( 'Publish' ) }
+ (
+
-
-
-
-
-
- );
- }
+ { label }
+
+ ) }
+ renderContent={ () => ( [
+ ,
+ ,
+ ] ) }
+ />
+
+ );
}
const applyConnect = connect(
diff --git a/editor/sidebar/post-visibility/index.js b/editor/sidebar/post-visibility/index.js
index 21c61498263646..438a4808820575 100644
--- a/editor/sidebar/post-visibility/index.js
+++ b/editor/sidebar/post-visibility/index.js
@@ -9,7 +9,7 @@ import { find, flowRight } from 'lodash';
*/
import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
-import { PanelRow, Popover, withInstanceId, withAPIData } from '@wordpress/components';
+import { PanelRow, Dropdown, withInstanceId, withAPIData } from '@wordpress/components';
/**
* Internal Dependencies
@@ -25,42 +25,15 @@ export class PostVisibility extends Component {
constructor( props ) {
super( ...arguments );
- this.toggleDialog = this.toggleDialog.bind( this );
- this.stopPropagation = this.stopPropagation.bind( this );
- this.closeOnClickOutside = this.closeOnClickOutside.bind( this );
- this.close = this.close.bind( this );
this.setPublic = this.setPublic.bind( this );
this.setPrivate = this.setPrivate.bind( this );
this.setPasswordProtected = this.setPasswordProtected.bind( this );
- this.bindButtonNode = this.bindButtonNode.bind( this );
this.state = {
- opened: false,
hasPassword: !! props.password,
};
}
- toggleDialog() {
- this.setState( ( state ) => ( { opened: ! state.opened } ) );
- }
-
- stopPropagation( event ) {
- event.stopPropagation();
- }
-
- closeOnClickOutside( event ) {
- if ( ! this.buttonNode.contains( event.target ) ) {
- this.close();
- }
- }
-
- close() {
- const { opened } = this.state;
- if ( opened ) {
- this.toggleDialog();
- }
- }
-
setPublic() {
const { visibility, onUpdateVisibility, status } = this.props;
@@ -87,10 +60,6 @@ export class PostVisibility extends Component {
this.setState( { hasPassword: true } );
}
- bindButtonNode( node ) {
- this.buttonNode = node;
- }
-
render() {
const { status, visibility, password, onUpdateVisibility, instanceId, user } = this.props;
const canEdit = user.data && user.data.capabilities.publish_posts;
@@ -129,23 +98,21 @@ export class PostVisibility extends Component {
{ __( 'Visibility' ) }
{ ! canEdit && { getVisibilityLabel( visibility ) } }
{ canEdit && (
-
- { getVisibilityLabel( visibility ) }
-
-
+ (
+
+ { getVisibilityLabel( visibility ) }
+
+ ) }
+ renderContent={ () => ( [
+
{ __( 'Post Visibility' ) }
@@ -170,9 +137,9 @@ export class PostVisibility extends Component {
{ { info }
}
) ) }
-
- { this.state.hasPassword &&
-
+ ,
+ this.state.hasPassword && (
+
- }
-
-
+ ),
+ ] ) }
+ />
) }
);
diff --git a/editor/sidebar/post-visibility/test/index.js b/editor/sidebar/post-visibility/test/index.js
index f123d729ace526..6f3559f0c12004 100644
--- a/editor/sidebar/post-visibility/test/index.js
+++ b/editor/sidebar/post-visibility/test/index.js
@@ -23,11 +23,11 @@ describe( 'PostVisibility', () => {
wrapper = shallow(
);
- expect( wrapper.find( 'button' ) ).toHaveLength( 0 );
+ expect( wrapper.find( 'Dropdown' ) ).toHaveLength( 0 );
} );
it( 'should render if the user has the correct capability', () => {
const wrapper = shallow(
);
- expect( wrapper.find( 'button' ) ).not.toHaveLength( 0 );
+ expect( wrapper.find( 'Dropdown' ) ).not.toHaveLength( 0 );
} );
} );
diff --git a/editor/table-of-contents/index.js b/editor/table-of-contents/index.js
index 7ee23a3055455c..c87dfce5eaffbc 100644
--- a/editor/table-of-contents/index.js
+++ b/editor/table-of-contents/index.js
@@ -8,8 +8,7 @@ import { filter } from 'lodash';
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { Dashicon, Popover } from '@wordpress/components';
-import { Component } from '@wordpress/element';
+import { Dashicon, Dropdown } from '@wordpress/components';
/**
* Internal dependencies
@@ -20,57 +19,47 @@ import WordCount from '../word-count';
import { getBlocks } from '../selectors';
import { selectBlock } from '../actions';
-class TableOfContents extends Component {
- constructor() {
- super( ...arguments );
- this.state = {
- showPopover: false,
- };
- }
+function TableOfContents( { blocks } ) {
+ const headings = filter( blocks, ( block ) => block.name === 'core/heading' );
- render() {
- const { blocks } = this.props;
- const headings = filter( blocks, ( block ) => block.name === 'core/heading' );
-
- return (
-
+ return (
+
(
this.setState( { showPopover: ! this.state.showPopover } ) }
+ onClick={ onToggle }
>
- this.setState( { showPopover: false } ) }
- >
-
-
-
- { __( 'Word Count' ) }
-
-
- { blocks.length }
- { __( 'Blocks' ) }
-
-
- { headings.length }
- { __( 'Headings' ) }
-
+ ) }
+ renderContent={ () => ( [
+
+
+
+ { __( 'Word Count' ) }
- { headings.length > 0 &&
-
-
- { __( 'Table of Contents' ) }
-
-
- }
-
-
- );
- }
+
+ { blocks.length }
+ { __( 'Blocks' ) }
+
+
+ { headings.length }
+ { __( 'Headings' ) }
+
+
,
+ headings.length > 0 && (
+
+
+ { __( 'Table of Contents' ) }
+
+
+ ),
+ ] ) }
+ />
+ );
}
export default connect(