From 87732a2a1c2b6734aed49a87d75c83163facc6d5 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Sat, 27 Oct 2018 14:44:13 +0300 Subject: [PATCH] Block API: Add support for icons for block categories (#10651) --- docs/manifest.json | 6 + lib/client-assets.php | 6 + packages/components/src/icon/README.md | 84 ++++++++++ packages/components/src/icon/index.js | 48 ++++++ packages/components/src/icon/test/index.js | 146 ++++++++++++++++++ packages/components/src/index.js | 1 + packages/components/src/panel/body.js | 4 +- packages/components/src/panel/style.scss | 2 +- .../editor/src/components/block-icon/index.js | 37 +---- .../src/components/block-icon/test/index.js | 42 +++-- .../editor/src/components/inserter/menu.js | 1 + 11 files changed, 319 insertions(+), 58 deletions(-) create mode 100644 packages/components/src/icon/README.md create mode 100644 packages/components/src/icon/index.js create mode 100644 packages/components/src/icon/test/index.js diff --git a/docs/manifest.json b/docs/manifest.json index 6025552e527e7..efab957c872c6 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -737,6 +737,12 @@ "markdown_source": "https://github.com/raw/WordPress/gutenberg/master/packages/components/src/icon-button/README.md", "parent": "components" }, + { + "title": "Icon", + "slug": "icon", + "markdown_source": "https://github.com/raw/WordPress/gutenberg/master/packages/components/src/icon/README.md", + "parent": "components" + }, { "title": "KeyboardShortcuts", "slug": "keyboard-shortcuts", diff --git a/lib/client-assets.php b/lib/client-assets.php index 4d5a44372f948..a0ac0b594bd23 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -1306,26 +1306,32 @@ function gutenberg_get_block_categories( $post ) { array( 'slug' => 'common', 'title' => __( 'Common Blocks', 'gutenberg' ), + 'icon' => 'screenoptions', ), array( 'slug' => 'formatting', 'title' => __( 'Formatting', 'gutenberg' ), + 'icon' => null, ), array( 'slug' => 'layout', 'title' => __( 'Layout Elements', 'gutenberg' ), + 'icon' => null, ), array( 'slug' => 'widgets', 'title' => __( 'Widgets', 'gutenberg' ), + 'icon' => null, ), array( 'slug' => 'embed', 'title' => __( 'Embeds', 'gutenberg' ), + 'icon' => null, ), array( 'slug' => 'reusable', 'title' => __( 'Reusable Blocks', 'gutenberg' ), + 'icon' => null, ), ); diff --git a/packages/components/src/icon/README.md b/packages/components/src/icon/README.md new file mode 100644 index 0000000000000..de36accebd375 --- /dev/null +++ b/packages/components/src/icon/README.md @@ -0,0 +1,84 @@ +# Icon + +Allows you to render a raw icon without any initial styling or wrappers. + +## Usage + +#### With a Dashicon + +```jsx +import { Icon } from '@wordpress/components'; + +const MyIcon = () => ( + +); +``` + +#### With a function + +```jsx +import { Icon } from '@wordpress/components'; + +const MyIcon = () => ( + } /> +); +``` + +#### With a Component + +```jsx +import { MyIconComponent } from '../my-icon-component'; +import { Icon } from '@wordpress/components'; + +const MyIcon = () => ( + +); +``` + +#### With an SVG + +```jsx +import { Icon } from '@wordpress/components'; + +const MyIcon = () => ( + } /> +); +``` + +#### Specifying a className + +```jsx +import { Icon } from '@wordpress/components'; + +const MyIcon = () => ( + +); +``` + +## Props + +The component accepts the following props: + +### icon + +The icon to render. Supported values are: Dashicons (specified as strings), functions, WPComponent instances and `null`. + +- Type: `String|Function|WPComponent|null` +- Required: No +- Default: `null` + +### size + +The size (width and height) of the icon. + +- Type: `Number` +- Required: No +- Default: `20` when a Dashicon is rendered, `24` for all other icons. + +### className + +An optional additional class name to apply to the rendered icon. + +- Type: `String` +- Required: No +- Default: `null` diff --git a/packages/components/src/icon/index.js b/packages/components/src/icon/index.js new file mode 100644 index 0000000000000..fbb2c1bc49df7 --- /dev/null +++ b/packages/components/src/icon/index.js @@ -0,0 +1,48 @@ +/** + * WordPress dependencies + */ +import { cloneElement, createElement, Component, isValidElement } from '@wordpress/element'; +import { Dashicon, SVG } from '../'; + +function Icon( { icon = null, size, className } ) { + let iconSize; + + if ( 'string' === typeof icon ) { + // Dashicons should be 20x20 by default + iconSize = size || 20; + return ; + } + + // Any other icons should be 24x24 by default + iconSize = size || 24; + + if ( 'function' === typeof icon ) { + if ( icon.prototype instanceof Component ) { + return createElement( icon, { className, size: iconSize } ); + } + + return icon(); + } + + if ( icon && ( icon.type === 'svg' || icon.type === SVG ) ) { + const appliedProps = { + className, + width: iconSize, + height: iconSize, + ...icon.props, + }; + + return ; + } + + if ( isValidElement( icon ) ) { + return cloneElement( icon, { + className, + size: iconSize, + } ); + } + + return icon; +} + +export default Icon; diff --git a/packages/components/src/icon/test/index.js b/packages/components/src/icon/test/index.js new file mode 100644 index 0000000000000..a7fea71cb9fac --- /dev/null +++ b/packages/components/src/icon/test/index.js @@ -0,0 +1,146 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { Path, SVG } from '../../'; + +/** + * Internal dependencies + */ +import Icon from '../'; + +describe( 'Icon', () => { + const className = 'example-class'; + const svg = ; + + it( 'renders nothing when icon omitted', () => { + const wrapper = shallow( ); + + expect( wrapper.type() ).toBeNull(); + } ); + + it( 'renders a dashicon by slug', () => { + const wrapper = shallow( ); + + expect( wrapper.find( 'Dashicon' ).prop( 'icon' ) ).toBe( 'format-image' ); + } ); + + it( 'renders a dashicon and passes the classname to it', () => { + const wrapper = shallow( ); + + expect( wrapper.find( 'Dashicon' ).prop( 'className' ) ).toBe( 'example-class' ); + } ); + + it( 'renders a dashicon and with a default size of 20', () => { + const wrapper = shallow( ); + + expect( wrapper.find( 'Dashicon' ).prop( 'size' ) ).toBe( 20 ); + } ); + + it( 'renders a dashicon and passes the size to it', () => { + const wrapper = shallow( ); + + expect( wrapper.find( 'Dashicon' ).prop( 'size' ) ).toBe( 32 ); + } ); + + it( 'renders a function', () => { + const wrapper = shallow( } /> ); + + expect( wrapper.name() ).toBe( 'span' ); + } ); + + it( 'renders an element', () => { + const wrapper = shallow( } /> ); + + expect( wrapper.name() ).toBe( 'span' ); + } ); + + it( 'renders an element and passes the classname to it', () => { + const wrapper = shallow( } className={ className } /> ); + + expect( wrapper.prop( 'className' ) ).toBe( 'example-class' ); + } ); + + it( 'renders an element and passes the size to it', () => { + const wrapper = shallow( ); + + expect( wrapper.prop( 'size' ) ).toBe( 32 ); + } ); + + it( 'renders an svg element', () => { + const wrapper = shallow( ); + + expect( wrapper.name() ).toBe( 'SVG' ); + } ); + + it( 'renders an svg element and passes the classname to it', () => { + const wrapper = shallow( ); + + expect( wrapper.prop( 'className' ) ).toBe( 'example-class' ); + } ); + + it( 'renders an svg element with a default width and height of 24', () => { + const wrapper = shallow( ); + + expect( wrapper.prop( 'width' ) ).toBe( 24 ); + expect( wrapper.prop( 'height' ) ).toBe( 24 ); + } ); + + it( 'renders an svg element and passes the size as its width and height', () => { + const wrapper = shallow( } size={ 32 } /> ); + + expect( wrapper.prop( 'width' ) ).toBe( 64 ); + expect( wrapper.prop( 'height' ) ).toBe( 64 ); + } ); + + it( 'renders an svg element and does not override width and height if already specified', () => { + const wrapper = shallow( ); + + expect( wrapper.prop( 'width' ) ).toBe( 32 ); + expect( wrapper.prop( 'height' ) ).toBe( 32 ); + } ); + + it( 'renders a component', () => { + class MyComponent extends Component { + render() { + return ; + } + } + const wrapper = shallow( + + ); + + expect( wrapper.name() ).toBe( 'MyComponent' ); + } ); + + it( 'renders a component and passes the classname to it', () => { + class MyComponent extends Component { + render( ) { + return ; + } + } + const wrapper = shallow( + + ); + + expect( wrapper.prop( 'className' ) ).toBe( 'example-class' ); + } ); + + it( 'renders a component and passes the size to it', () => { + class MyComponent extends Component { + render( ) { + return ; + } + } + const wrapper = shallow( + + ); + + expect( wrapper.prop( 'size' ) ).toBe( 32 ); + } ); +} ); diff --git a/packages/components/src/index.js b/packages/components/src/index.js index a1a36393346d4..5af9d24cd7077 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -24,6 +24,7 @@ export { default as FontSizePicker } from './font-size-picker'; export { default as FormFileUpload } from './form-file-upload'; export { default as FormToggle } from './form-toggle'; export { default as FormTokenField } from './form-token-field'; +export { default as Icon } from './icon'; export { default as IconButton } from './icon-button'; export { default as KeyboardShortcuts } from './keyboard-shortcuts'; export { default as MenuGroup } from './menu-group'; diff --git a/packages/components/src/panel/body.js b/packages/components/src/panel/body.js index d5795d51a0c12..f23533a6dea34 100644 --- a/packages/components/src/panel/body.js +++ b/packages/components/src/panel/body.js @@ -12,7 +12,7 @@ import { Component } from '@wordpress/element'; * Internal dependencies */ import Button from '../button'; -import Dashicon from '../dashicon'; +import Icon from '../icon'; import { G, Path, SVG } from '../primitives'; class PanelBody extends Component { @@ -61,8 +61,8 @@ class PanelBody extends Component { } - { icon && } { title } + { icon && } ) } diff --git a/packages/components/src/panel/style.scss b/packages/components/src/panel/style.scss index bbe0ae78c9462..e4f2d58af2645 100644 --- a/packages/components/src/panel/style.scss +++ b/packages/components/src/panel/style.scss @@ -114,7 +114,7 @@ .components-panel__icon { color: $dark-gray-500; - margin: -2px 6px -2px 0; + margin: -2px 0 -2px 6px; } .components-panel__body-toggle-icon { diff --git a/packages/editor/src/components/block-icon/index.js b/packages/editor/src/components/block-icon/index.js index 7314dc6375eda..7b68ce97128ed 100644 --- a/packages/editor/src/components/block-icon/index.js +++ b/packages/editor/src/components/block-icon/index.js @@ -6,46 +6,19 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { Dashicon, Path, SVG } from '@wordpress/components'; -import { createElement, Component } from '@wordpress/element'; +import { Path, Icon, SVG } from '@wordpress/components'; -function renderIcon( icon ) { - if ( 'string' === typeof icon ) { - if ( icon !== 'block-default' ) { - return ; - } - icon = ; - } - if ( 'function' === typeof icon ) { - if ( icon.prototype instanceof Component ) { - return createElement( icon ); - } - - return icon(); - } - if ( icon && ( icon.type === 'svg' || icon.type === SVG ) ) { - const appliedProps = { - width: 24, - height: 24, - ...icon.props, - }; - return ; +export default function BlockIcon( { icon, showColors = false, className } ) { + if ( icon === 'block-default' ) { + return ; } - return icon || null; -} - -export default function BlockIcon( { icon, showColors = false, className } ) { - const renderedIcon = renderIcon( icon && icon.src ? icon.src : icon ); + const renderedIcon = ; const style = showColors ? { backgroundColor: icon && icon.background, color: icon && icon.foreground, } : {}; - if ( ! renderedIcon ) { - return null; - } - return (
{ - it( 'renders nothing when icon omitted', () => { - const wrapper = shallow( ); + it( 'renders a Icon', () => { + const wrapper = shallow( ); - expect( wrapper.type() ).toBeNull(); + expect( wrapper.containsMatchingElement( ) ).toBe( true ); } ); - it( 'renders a dashicon by slug', () => { + it( 'renders a div without the has-colors classname', () => { const wrapper = shallow( ); - expect( wrapper.find( 'Dashicon' ).prop( 'icon' ) ).toBe( 'format-image' ); + expect( wrapper.find( 'div' ).hasClass( 'has-colors' ) ).toBe( false ); } ); - it( 'renders a function', () => { - const wrapper = shallow( } /> ); + it( 'renders a div with the has-colors classname', () => { + const wrapper = shallow( ); - expect( wrapper.childAt( 0 ).name() ).toBe( 'span' ); + expect( wrapper.find( 'div' ).hasClass( 'has-colors' ) ).toBe( true ); } ); - it( 'renders an element', () => { - const wrapper = shallow( } /> ); + it( 'skips adding background and foreground styles when colors are not enabled', () => { + const wrapper = shallow( ); - expect( wrapper.childAt( 0 ).name() ).toBe( 'span' ); + expect( wrapper.find( 'div' ).prop( 'style' ) ).toEqual( {} ); } ); - it( 'renders a component', () => { - class MyComponent extends Component { - render() { - return ; - } - } - const wrapper = shallow( - - ); - - expect( wrapper.childAt( 0 ).name() ).toBe( 'MyComponent' ); + it( 'adds background and foreground styles when colors are enabled', () => { + const wrapper = shallow( ); + + expect( wrapper.find( 'div' ).prop( 'style' ) ).toEqual( { + backgroundColor: 'white', + color: 'black', + } ); } ); } ); diff --git a/packages/editor/src/components/inserter/menu.js b/packages/editor/src/components/inserter/menu.js index 21f8279c80ddf..7af57b6af6add 100644 --- a/packages/editor/src/components/inserter/menu.js +++ b/packages/editor/src/components/inserter/menu.js @@ -295,6 +295,7 @@ export class InserterMenu extends Component {