From 4f2e7f2ef7f00fdad1a4dc408d01d02112ad0baa Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Mon, 28 Oct 2019 11:50:46 +0000 Subject: [PATCH] Add `DimensionControl` component (#16791) * Adds initial component Note this is copied wholescale from original PR https://github.com/WordPress/gutenberg/pull/16730 * Remove redunant files. Refactors tests. * Updates docs * Checks callbacks are functions prior to calling * Adds temp testing example usage of component to Group Block * Updates to allow sizes as an (optionaly) prop dependency * Update default value label * Removes unnecessary InstanceId HOC usage Addresses https://github.com/WordPress/gutenberg/pull/16791#discussion_r323906696 * Remove unused abbreviation in size table * Revert "Adds temp testing example usage of component to Group Block" This reverts commit 6f9f3bfd2a7c1a08ecfab143384d414701f0c1e8. * Remove arbitrary size value from sizes list This is not required as we cannot know how the dimensions component will be used. Therefore sticking with relative values via the slugs is safer. These can be mapped on a case by case basis as required. * Remove icon label for a11y reasons Addresses https://github.com/WordPress/gutenberg/pull/16791#discussion_r324103481 * Update component docs for consistency, spelling and grammar * Tweak docblock formats * Update test snapshots to match new default value * Update API from onSpacingChange to more agnostic onChange Addresses https://github.com/WordPress/gutenberg/pull/16791#discussion_r331622801 * Update tests to cover onChange handler renamed * Update currentSize prop to value for consistency with other components * Removes onReset in favour of onChange with undefined for consistency Adddresses https://github.com/WordPress/gutenberg/pull/16791#discussion_r331624272 * Move component to @wordpress/components package * Remove invalid font sizes style import Accidentally included from rebase. * Deps update due to rebase * Remove unneeded doc blocks * Remove usage suggestion which was not helpful * Update readme docs to match current API Addresses https://github.com/WordPress/gutenberg/pull/16791#discussion_r332692714 * Export as experimental component Addresses https://github.com/WordPress/gutenberg/pull/16791#discussion_r332694561 * Revert "Deps update due to rebase" This reverts commit 95d00f39010edfaac620980e0d0e7c1001a68c98. Addresses https://github.com/WordPress/gutenberg/pull/16791#discussion_r332691520 --- docs/manifest-devhub.json | 6 + .../src/dimension-control/README.md | 120 +++++++++++++ .../components/src/dimension-control/index.js | 76 ++++++++ .../components/src/dimension-control/sizes.js | 45 +++++ .../src/dimension-control/style.scss | 22 +++ .../test/__snapshots__/index.test.js.snap | 163 ++++++++++++++++++ .../src/dimension-control/test/index.test.js | 128 ++++++++++++++ packages/components/src/index.js | 1 + packages/components/src/style.scss | 1 + 9 files changed, 562 insertions(+) create mode 100644 packages/components/src/dimension-control/README.md create mode 100644 packages/components/src/dimension-control/index.js create mode 100644 packages/components/src/dimension-control/sizes.js create mode 100644 packages/components/src/dimension-control/style.scss create mode 100644 packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap create mode 100644 packages/components/src/dimension-control/test/index.test.js diff --git a/docs/manifest-devhub.json b/docs/manifest-devhub.json index b508f649fb7c9..a8d1a369642ee 100644 --- a/docs/manifest-devhub.json +++ b/docs/manifest-devhub.json @@ -641,6 +641,12 @@ "markdown_source": "../packages/components/src/date-time/README.md", "parent": "components" }, + { + "title": "DimensionControl", + "slug": "dimension-control", + "markdown_source": "../packages/components/src/dimension-control/README.md", + "parent": "components" + }, { "title": "Disabled", "slug": "disabled", diff --git a/packages/components/src/dimension-control/README.md b/packages/components/src/dimension-control/README.md new file mode 100644 index 0000000000000..435fdf4e73b04 --- /dev/null +++ b/packages/components/src/dimension-control/README.md @@ -0,0 +1,120 @@ +DimensionControl +============================= + +`DimensionControl` is a component designed to provide a UI to control spacing and/or dimensions. + +## Usage + +In a block's `edit` implementation, render a `` component. + + +```jsx +import { registerBlockType } from '@wordpress/blocks'; +import { __ } from '@wordpress/i18n'; +import { + DimensionControl, +} from '@wordpress/block-editor'; + +registerBlockType( 'my-plugin/my-block', { + // ... + + attributes: { + // other attributes here + // ... + + paddingSize: { + type: 'string', + }, + }, + + edit( { attributes, setAttributes, clientId } ) { + + const { paddingSize } = attributes; + + + const updateSpacing = ( dimension, size, device = '' ) => { + setAttributes( { + [ `${ dimension }${ device }` ]: size, + } ); + }; + + return ( + + ); + } +} ); +``` + +_Note:_ it is recommended to partially apply the value of the Block attribute to be updated (eg: `paddingSize`, `marginSize`...etc) to your callback functions. This avoids the need to unnecessarily couple the component to the Block attribute schema. + +_Note:_ by default, if you do not provide an initial `value` prop for the current dimension value, then no value will be selected (ie: there is no default dimension set). + +## Props + +### `label` +* **Type:** `String` +* **Default:** `undefined` +* **Required:** Yes + +The human readable label for the control. + +### `value` +* **Type:** `String` +* **Default:** `''` +* **Required:** No + +The current value of the dimension UI control. If provided the UI with automatically select the value. + +### `sizes` +* **Type:** `Array` +* **Default:** See `packages/block-editor/src/components/dimension-control/sizes.js` +* **Required:** No + +An optional array of size objects in the following shape: + +``` +[ + { + name: __( 'Small' ), + slug: 'small', + }, + { + name: __( 'Medium' ), + slug: 'small', + }, + // ...etc +] +``` + +By default a set of relative sizes (`small`, `medium`...etc) are provided. See `packages/block-editor/src/components/dimension-control/sizes.js`. + +### `icon` +* **Type:** `String` +* **Default:** `undefined` +* **Required:** No + +An optional dashicon to display before to the control label. + +### `onChange` +* **Type:** `Function` +* **Default:** `undefined` +* **Required:** No +* **Arguments:**: + - `size` - a string representing the selected size (eg: `medium`) + +A callback which is triggered when a spacing size value changes (is selected/clicked). + + +### `className` +* **Type:** `String` +* **Default:** `''` +* **Required:** No + +A string of classes to be added to the control component. + + diff --git a/packages/components/src/dimension-control/index.js b/packages/components/src/dimension-control/index.js new file mode 100644 index 0000000000000..c69b80f5c49e0 --- /dev/null +++ b/packages/components/src/dimension-control/index.js @@ -0,0 +1,76 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { isFunction } from 'lodash'; + +/** + * WordPress dependencies + */ +/** + * Internal dependencies + */ +import { + Icon, + SelectControl, +} from '../'; +import { __ } from '@wordpress/i18n'; + +import { + Fragment, +} from '@wordpress/element'; + +/** + * Internal dependencies + */ +import sizesTable, { findSizeBySlug } from './sizes'; + +export function DimensionControl( props ) { + const { label, value, sizes = sizesTable, icon, onChange, className = '' } = props; + + const onChangeSpacingSize = ( val ) => { + const theSize = findSizeBySlug( sizes, val ); + + if ( ! theSize || value === theSize.slug ) { + onChange( undefined ); + } else if ( isFunction( onChange ) ) { + onChange( theSize.slug ); + } + }; + + const formatSizesAsOptions = ( theSizes ) => { + const options = theSizes.map( ( { name, slug } ) => ( { + label: name, + value: slug, + } ) ); + + return [ { + label: __( 'Default' ), + value: '', + } ].concat( options ); + }; + + const selectLabel = ( + + { icon && ( + + ) } + { label } + + ); + + return ( + + ); +} + +export default DimensionControl; diff --git a/packages/components/src/dimension-control/sizes.js b/packages/components/src/dimension-control/sizes.js new file mode 100644 index 0000000000000..41fa071702872 --- /dev/null +++ b/packages/components/src/dimension-control/sizes.js @@ -0,0 +1,45 @@ +/** + * Sizes + * + * defines the sizes used in dimension controls + * all hardcoded `size` values are based on the value of + * the Sass variable `$block-padding` from + * `packages/block-editor/src/components/dimension-control/sizes.js`. + */ + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Finds the correct size object from the provided sizes + * table by size slug (eg: `medium`) + * + * @param {Array} sizes containing objects for each size definition + * @param {string} slug a string representation of the size (eg: `medium`) + * @return {Object} the matching size definition + */ +export const findSizeBySlug = ( sizes, slug ) => sizes.find( ( size ) => slug === size.slug ); + +export default [ + { + name: __( 'None' ), + slug: 'none', + }, + { + name: __( 'Small' ), + slug: 'small', + }, + { + name: __( 'Medium' ), + slug: 'medium', + }, + { + name: __( 'Large' ), + slug: 'large', + }, { + name: __( 'Extra Large' ), + slug: 'xlarge', + }, +]; diff --git a/packages/components/src/dimension-control/style.scss b/packages/components/src/dimension-control/style.scss new file mode 100644 index 0000000000000..7f1481747dfe1 --- /dev/null +++ b/packages/components/src/dimension-control/style.scss @@ -0,0 +1,22 @@ +.block-editor-dimension-control { + + .components-base-control__field { + display: flex; + align-items: center; + } + + .components-base-control__label { + display: flex; + align-items: center; + margin-right: 1em; + margin-bottom: 0; + + .dashicon { + margin-right: 0.5em; + } + } + + &.is-manual .components-base-control__label { + width: 10em; + } +} diff --git a/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap b/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap new file mode 100644 index 0000000000000..c06662ee862c9 --- /dev/null +++ b/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap @@ -0,0 +1,163 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DimensionControl rendering renders with custom sizes 1`] = ` + + Custom Dimension + + } + onChange={[Function]} + options={ + Array [ + Object { + "label": "Default", + "value": "", + }, + Object { + "label": "Mini", + "value": "mini", + }, + Object { + "label": "Middle", + "value": "middle", + }, + Object { + "label": "Giant", + "value": "giant", + }, + ] + } +/> +`; + +exports[`DimensionControl rendering renders with defaults 1`] = ` + + Padding + + } + onChange={[Function]} + options={ + Array [ + Object { + "label": "Default", + "value": "", + }, + Object { + "label": "None", + "value": "none", + }, + Object { + "label": "Small", + "value": "small", + }, + Object { + "label": "Medium", + "value": "medium", + }, + Object { + "label": "Large", + "value": "large", + }, + Object { + "label": "Extra Large", + "value": "xlarge", + }, + ] + } +/> +`; + +exports[`DimensionControl rendering renders with icon and custom icon label 1`] = ` + + + Margin + + } + onChange={[Function]} + options={ + Array [ + Object { + "label": "Default", + "value": "", + }, + Object { + "label": "None", + "value": "none", + }, + Object { + "label": "Small", + "value": "small", + }, + Object { + "label": "Medium", + "value": "medium", + }, + Object { + "label": "Large", + "value": "large", + }, + Object { + "label": "Extra Large", + "value": "xlarge", + }, + ] + } +/> +`; + +exports[`DimensionControl rendering renders with icon and default icon label 1`] = ` + + + Margin + + } + onChange={[Function]} + options={ + Array [ + Object { + "label": "Default", + "value": "", + }, + Object { + "label": "None", + "value": "none", + }, + Object { + "label": "Small", + "value": "small", + }, + Object { + "label": "Medium", + "value": "medium", + }, + Object { + "label": "Large", + "value": "large", + }, + Object { + "label": "Extra Large", + "value": "xlarge", + }, + ] + } +/> +`; diff --git a/packages/components/src/dimension-control/test/index.test.js b/packages/components/src/dimension-control/test/index.test.js new file mode 100644 index 0000000000000..0b053a493ee12 --- /dev/null +++ b/packages/components/src/dimension-control/test/index.test.js @@ -0,0 +1,128 @@ +/** + * External dependencies + */ +import { shallow, mount } from 'enzyme'; +import { uniqueId } from 'lodash'; + +/** + * Internal dependencies + */ +import { DimensionControl } from '../'; + +describe( 'DimensionControl', () => { + const onChangeHandler = jest.fn(); + + afterEach( () => { + onChangeHandler.mockClear(); + } ); + + describe( 'rendering', () => { + it( 'renders with defaults', () => { + const wrapper = shallow( + + ); + expect( wrapper ).toMatchSnapshot(); + } ); + + it( 'renders with icon and default icon label', () => { + const wrapper = shallow( + + ); + expect( wrapper ).toMatchSnapshot(); + } ); + + it( 'renders with icon and custom icon label', () => { + const wrapper = shallow( + + ); + expect( wrapper ).toMatchSnapshot(); + } ); + + it( 'renders with custom sizes', () => { + const customSizes = [ + { + name: 'Mini', + size: 1, + slug: 'mini', + }, + { + name: 'Middle', + size: 5, + slug: 'middle', + }, + { + name: 'Giant', + size: 10, + slug: 'giant', + }, + ]; + + const wrapper = shallow( + + ); + expect( wrapper ).toMatchSnapshot(); + } ); + } ); + + describe( 'callbacks', () => { + it( 'should call onChange handler with correct args on size change', () => { + const wrapper = mount( + + ); + + wrapper.find( 'select' ).at( 0 ).simulate( 'change', { + target: { + value: 'small', + }, + } ); + + wrapper.find( 'select' ).at( 0 ).simulate( 'change', { + target: { + value: 'medium', + }, + } ); + + expect( onChangeHandler ).toHaveBeenCalledTimes( 2 ); + expect( onChangeHandler.mock.calls[ 0 ][ 0 ] ).toEqual( 'small' ); + expect( onChangeHandler.mock.calls[ 1 ][ 0 ] ).toEqual( 'medium' ); + } ); + + it( 'should call onChange handler with undefined value when no size is provided on change', () => { + const wrapper = mount( + + ); + + wrapper.find( 'select' ).at( 0 ).simulate( 'change', { + target: { + value: '', // this happens when you select the "default"