Skip to content

Commit

Permalink
Components: Add render prop support to DropdownMenu
Browse files Browse the repository at this point in the history
  • Loading branch information
aduth committed Apr 5, 2019
1 parent 7a54cec commit 743e4da
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,9 @@
transform: rotate(90deg);
}

// Popout menu
.block-editor-block-settings-menu__popover {
&::before,
&::after {
margin-left: 2px;
}

.block-editor-block-settings-menu__content {
padding: ($grid-size - $border-width) 0;
}

.block-editor-block-settings-menu__separator {
margin-top: $grid-size;
margin-bottom: $grid-size;
margin-left: 0;
margin-right: 0;
border-top: $border-width solid $light-gray-500;

// Check if the separator is the last child in the node and if so, hide itself
&:last-child {
display: none;
}
}

.block-editor-block-settings-menu__title {
display: block;
padding: 6px;
color: $dark-gray-300;
}

// Menu items
.block-editor-block-settings-menu__control {
width: 100%;
justify-content: flex-start;
background: none;
outline: none;
border-radius: 0;
color: $dark-gray-500;
text-align: left;
cursor: pointer;
@include menu-style__neutral;

&:hover:not(:disabled):not([aria-disabled="true"]) {
@include menu-style__hover;
}

&:focus:not(:disabled):not([aria-disabled="true"]) {
@include menu-style__focus;
}
.block-editor-block-settings-menu .components-icon-button.has-text svg {
margin-right: 0;
}

.dashicon {
margin-right: 5px;
Expand Down
51 changes: 50 additions & 1 deletion packages/components/src/dropdown-menu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,44 @@ const MyDropdownMenu = () => (
);
```

Alternatively, specify a `children` function which returns elements valid for use in a DropdownMenu: `MenuItem`, `MenuItemsChoice`, `MenuGroup`, or `DropdownMenuSeparator`.

```jsx
import { Fragment } from '@wordpress/element';
import { DropdownMenu, DropdownMenuSeparator, MenuItem } from '@wordpress/components';

const MyDropdownMenu = () => (
<DropdownMenu
icon="move"
label="Select a direction"
>
{ ( { onClose } ) => (
<Fragment>
<MenuItem
icon="arrow-up-alt"
onClick={ onClose }
>
Move Up
</MenuItem>
<MenuItem
icon="arrow-down-alt"
onClick={ onClose }
>
Move Down
</MenuItem>
<DropdownMenuSeparator />
<MenuItem
icon="trash"
onClick={ onClose }
>
Remove
</MenuItem>
</Fragment>
) }
</DropdownMenu>
);
```

### Props

The component accepts the following props:
Expand Down Expand Up @@ -133,8 +171,19 @@ An array of objects describing the options to be shown in the expanded menu.

Each object should include an `icon` [Dashicon](https://developer.wordpress.org/resource/dashicons/) slug string, a human-readable `title` string, `isDisabled` boolean flag and an `onClick` function callback to invoke when the option is selected.

A valid DropdownMenu must specify one or the other of a `controls` or `children` prop.

- Type: `Array`
- Required: Yes
- Required: No

#### children

A [function render prop](https://reactjs.org/docs/render-props.html#using-props-other-than-render) which should return an element or elements valid for use in a DropdownMenu: `MenuItem`, `MenuItemsChoice`, `MenuGroup`, or `DropdownMenuSeparator`. Its first argument is a props object including the same values as given to a [`Dropdown`'s `renderContent`](/packages/components/src/dropdown#rendercontent) (`isOpen`, `onToggle`, `onClose`).

A valid DropdownMenu must specify one or the other of a `controls` or `children` prop.

- Type: `Function`
- Required: No

See also: [https://developer.wordpress.org/resource/dashicons/](https://developer.wordpress.org/resource/dashicons/)

Expand Down
26 changes: 19 additions & 7 deletions packages/components/src/dropdown-menu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* External dependencies
*/
import classnames from 'classnames';
import { flatMap } from 'lodash';
import { flatMap, isEmpty, isFunction } from 'lodash';

/**
* WordPress dependencies
Expand All @@ -12,6 +12,7 @@ import { DOWN } from '@wordpress/keycodes';
/**
* Internal dependencies
*/
import DropdownMenuSeparator from './separator';
import IconButton from '../icon-button';
import Dropdown from '../dropdown';
import { NavigableMenu } from '../navigable-container';
Expand All @@ -23,15 +24,19 @@ function DropdownMenu( {
controls,
className,
position,
children,
} ) {
if ( ! controls || ! controls.length ) {
if ( isEmpty( controls ) && ! isFunction( children ) ) {
return null;
}

// Normalize controls to nested array of objects (sets of controls)
let controlSets = controls;
if ( ! Array.isArray( controlSets[ 0 ] ) ) {
controlSets = [ controlSets ];
let controlSets;
if ( ! isEmpty( controls ) ) {
controlSets = controls;
if ( ! Array.isArray( controlSets[ 0 ] ) ) {
controlSets = [ controlSets ];
}
}

return (
Expand Down Expand Up @@ -62,20 +67,25 @@ function DropdownMenu( {
</IconButton>
);
} }
renderContent={ ( { onClose } ) => {
renderContent={ ( props ) => {
return (
<NavigableMenu
className="components-dropdown-menu__menu"
role="menu"
aria-label={ menuLabel }
>
{
isFunction( children ) ?
children( props ) :
null
}
{ flatMap( controlSets, ( controlSet, indexOfSet ) => (
controlSet.map( ( control, indexOfControl ) => (
<IconButton
key={ [ indexOfSet, indexOfControl ].join() }
onClick={ ( event ) => {
event.stopPropagation();
onClose();
props.onClose();
if ( control.onClick ) {
control.onClick();
}
Expand All @@ -102,4 +112,6 @@ function DropdownMenu( {
);
}

export { DropdownMenuSeparator };

export default DropdownMenu;
12 changes: 12 additions & 0 deletions packages/components/src/dropdown-menu/separator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Hoisted shared static reference to dropdown menu separator element.
*
* @link https://babeljs.io/docs/en/next/babel-plugin-transform-react-constant-elements.html
*
* @type {WPElement}
*/
const SEPARATOR_ELEMENT = <div className="components-dropdown-menu__separator" />;

const DropdownMenuSeparator = () => SEPARATOR_ELEMENT;

export default DropdownMenuSeparator;
19 changes: 17 additions & 2 deletions packages/components/src/dropdown-menu/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,20 @@
}
}
}

.components-dropdown-menu__popover .components-popover__content {
width: 200px;
}

.components-dropdown-menu__menu {
width: 100%;
padding: 9px;
padding: ($grid-size - $border-width) 0;
font-family: $default-font;
font-size: $default-font-size;
line-height: $default-line-height;

.components-dropdown-menu__menu-item {
.components-dropdown-menu__menu-item,
.components-menu-item {
width: 100%;
padding: 6px;
outline: none;
Expand Down Expand Up @@ -93,3 +95,16 @@
}
}
}

.components-dropdown-menu__separator {
margin-top: $grid-size;
margin-bottom: $grid-size;
margin-left: 0;
margin-right: 0;
border-top: $border-width solid $light-gray-500;

// Check if the separator is the last child in the node and if so, hide itself
&:last-child {
display: none;
}
}
66 changes: 36 additions & 30 deletions packages/components/src/dropdown-menu/test/index.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
/**
* External dependencies
*/
import { shallow } from 'enzyme';
import TestUtils from 'react-dom/test-utils';
import { shallow, mount } from 'enzyme';

/**
* WordPress dependencies
*/
import { DOWN } from '@wordpress/keycodes';
import { Component } from '@wordpress/element';

/**
* Internal dependencies
*/
import DropdownMenu from '../';
import Popover from '../../popover';
import { IconButton, MenuItem, NavigableMenu } from '../../';

describe( 'DropdownMenu', () => {
const children = ( { onClose } ) => <MenuItem onClick={ onClose } />;

let controls;
beforeEach( () => {
controls = [
Expand Down Expand Up @@ -44,42 +44,48 @@ describe( 'DropdownMenu', () => {
} );

describe( 'basic rendering', () => {
it( 'should render a null element when controls are not assigned', () => {
it( 'should render a null element when neither controls nor children are assigned', () => {
const wrapper = shallow( <DropdownMenu /> );

expect( wrapper.type() ).toBeNull();
} );

it( 'should render a null element when controls are empty', () => {
it( 'should render a null element when controls are empty and children is not specified', () => {
const wrapper = shallow( <DropdownMenu controls={ [] } /> );

expect( wrapper.type() ).toBeNull();
} );

it( 'should open menu on arrow down', () => {
// needed because TestUtils.renderIntoDocument returns null for stateless
// components
class Menu extends Component {
render() {
return <DropdownMenu { ...this.props } />;
}
}
const wrapper = TestUtils.renderIntoDocument( <Menu controls={ controls } /> );
const buttonElement = TestUtils.findRenderedDOMComponentWithClass(
wrapper,
'components-dropdown-menu__toggle'
);
// Close menu by keyup
TestUtils.Simulate.keyDown(
buttonElement,
{
stopPropagation: () => {},
preventDefault: () => {},
keyCode: DOWN,
}
);

expect( TestUtils.scryRenderedComponentsWithType( wrapper, Popover ) ).toHaveLength( 1 );
it( 'should open menu on arrow down (controls)', () => {
const wrapper = mount( <DropdownMenu controls={ controls } /> );
const button = wrapper.find( IconButton ).filter( '.components-dropdown-menu__toggle' );

button.simulate( 'keydown', {
stopPropagation: () => {},
preventDefault: () => {},
keyCode: DOWN,
} );

expect( wrapper.find( NavigableMenu ) ).toHaveLength( 1 );
expect( wrapper.find( IconButton ).filter( '.components-dropdown-menu__menu-item' ) ).toHaveLength( controls.length );
} );

it( 'should open menu on arrow down (children)', () => {
const wrapper = mount( <DropdownMenu children={ children } /> );
const button = wrapper.find( IconButton ).filter( '.components-dropdown-menu__toggle' );

button.simulate( 'keydown', {
stopPropagation: () => {},
preventDefault: () => {},
keyCode: DOWN,
} );

expect( wrapper.find( NavigableMenu ) ).toHaveLength( 1 );

wrapper.find( MenuItem ).props().onClick();
wrapper.update();

expect( wrapper.find( NavigableMenu ) ).toHaveLength( 0 );
} );
} );
} );
2 changes: 1 addition & 1 deletion packages/components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export { default as Draggable } from './draggable';
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 DropdownMenu, DropdownMenuSeparator } from './dropdown-menu';
export { default as ExternalLink } from './external-link';
export { default as FocalPointPicker } from './focal-point-picker';
export { default as FocusableIframe } from './focusable-iframe';
Expand Down

0 comments on commit 743e4da

Please sign in to comment.