Skip to content

Commit

Permalink
Components: Extract a generic dropdown component
Browse files Browse the repository at this point in the history
  • Loading branch information
youknowriad committed Oct 6, 2017
1 parent 3a9048c commit 3889e81
Show file tree
Hide file tree
Showing 10 changed files with 373 additions and 325 deletions.
125 changes: 46 additions & 79 deletions blocks/color-palette/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -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 (
<div className="blocks-color-palette">
{ 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 (
<div className="blocks-color-palette">
{ colors.map( ( color ) => {
const style = { color: color };
const className = classnames( 'blocks-color-palette__item', { 'is-active': value === color } );

return (
<div key={ color } className="blocks-color-palette__item-wrapper">
<button
type="button"
className={ className }
style={ style }
onClick={ () => onChange( value === color ? undefined : color ) }
aria-label={ sprintf( __( 'Color: %s' ), color ) }
aria-pressed={ value === color }
/>
</div>
);
} ) }
return (
<div key={ color } className="blocks-color-palette__item-wrapper">
<button
type="button"
className={ className }
style={ style }
onClick={ () => onChange( value === color ? undefined : color ) }
aria-label={ sprintf( __( 'Color: %s' ), color ) }
aria-pressed={ value === color }
/>
</div>
);
} ) }

<div className="blocks-color-palette__item-wrapper blocks-color-palette__custom-color">
<Dropdown
className="blocks-color-palette__item-wrapper blocks-color-palette__custom-color"
contentClassName="blocks-color-palette__picker "
renderToggle={ ( { isOpen, onToggle } ) => (
<button
type="button"
aria-expanded={ this.state.opened }
aria-expanded={ isOpen }
className="blocks-color-palette__item"
onClick={ this.togglePicker }
ref={ this.bindToggleNode }
onClick={ onToggle }
aria-label={ __( 'Custom color picker' ) }
>
<span className="blocks-color-palette__custom-color-gradient" />
</button>
<Popover
isOpen={ this.state.opened }
onClickOutside={ this.closeOnClickOutside }
className="blocks-color-palette__picker"
>
<ChromePicker
color={ value }
onChangeComplete={ ( color ) => {
onChange( color.hex );
this.togglePicker();
} }
style={ { width: '100%' } }
disableAlpha
/>
</Popover>
</div>
) }
renderContent={ () => (
<ChromePicker
color={ value }
onChangeComplete={ ( color ) => onChange( color.hex ) }
style={ { width: '100%' } }
disableAlpha
/>
) }
/>

<div className="blocks-color-palette__item-wrapper blocks-color-palette__clear-color">
<button
className="blocks-color-palette__item"
onClick={ () => onChange( undefined ) }
aria-label={ __( 'Remove color' ) }
>
<span className="blocks-color-palette__clear-color-line" />
</button>
</div>
<div className="blocks-color-palette__item-wrapper blocks-color-palette__clear-color">
<button
className="blocks-color-palette__item"
onClick={ () => onChange( undefined ) }
aria-label={ __( 'Remove color' ) }
>
<span className="blocks-color-palette__clear-color-line" />
</button>
</div>
);
}
</div>
);
}

export default withEditorSettings(
Expand Down
77 changes: 77 additions & 0 deletions components/dropdown/README.md
Original file line number Diff line number Diff line change
@@ -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 (
<Dropdown
className="my-container-class-name"
contentClassName="my-popover-content-classname"
position="bottom right"
renderToggle={ ( { isOpen, onToggle } ) => (
<button onClick={ onToggle } aria-expanded={ isOpen }>
Toggle Popover!
</button>
) }
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
63 changes: 63 additions & 0 deletions components/dropdown/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className={ className } ref={ this.bindContainer }>
{ renderToggle( args ) }
<Popover
className={ contentClassName }
isOpen={ isOpen }
position={ position }
onClickOutside={ this.clickOutside }
>
{ renderContent( args ) }
</Popover>
</div>
);
}
}

export default Dropdown;
53 changes: 53 additions & 0 deletions components/dropdown/test/index.js
Original file line number Diff line number Diff line change
@@ -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( <Dropdown
className="container"
contentClassName="content"
renderToggle={ ( { isOpen, onToggle } ) => (
<button aria-expanded={ isOpen } onClick={ onToggle }>Toggleee</button>
) }
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( <Dropdown
className="container"
contentClassName="content"
renderToggle={ ( { isOpen, onToggle, onClose } ) => [
<button key="open" className="open" aria-expanded={ isOpen } onClick={ onToggle }>Toggleee</button>,
<button key="close" className="close" onClick={ onClose } >closee</button>,
] }
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 );
} );
} );
1 change: 1 addition & 0 deletions components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading

0 comments on commit 3889e81

Please sign in to comment.