Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add experimental ResponsiveBlockControl component #16790

Merged
merged 43 commits into from
Nov 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
23807cf
Basic component skeleton
getdave Jul 29, 2019
95bfae8
Adds component demo to Group Block
getdave Jul 29, 2019
dd64a53
Make toggle control label dynamic and translatable
getdave Jul 29, 2019
8116762
Adds styles
getdave Jul 29, 2019
4ca0fc5
Automatically render responsive controls based on default control
getdave Sep 14, 2019
fe7c77d
Wrap each field group in a `fieldset` element
getdave Sep 15, 2019
e051305
Invert toggle and language to be on by default.
getdave Sep 15, 2019
9c6e8b2
Adds initial tests
getdave Sep 15, 2019
f2d1bfc
Update tests to resemble how a user interacts with Component
getdave Sep 16, 2019
aa1ec5f
Add toggle state test
getdave Sep 16, 2019
eb5e9ba
Update to switch toggle to preceed controls
getdave Sep 16, 2019
bec9001
Update individual control labels to fully describe control for a11y
getdave Sep 17, 2019
1907640
Fixes form ui to improve alignment
getdave Sep 26, 2019
17eec18
Improves i18n of generated control label
getdave Oct 2, 2019
c9c5e08
Improve a11y by leaving DOM intact between state changes.
getdave Oct 2, 2019
eb9c5f3
Adds aria description to toggle control for improved a11y context
getdave Oct 2, 2019
238d541
Update tests
getdave Oct 4, 2019
1af87b9
Update toggle of responsive controls internal to the component
getdave Oct 4, 2019
51b43a6
Adds test for responsiveControlsActive setting prop
getdave Oct 4, 2019
20a95b4
Add tests to cover custom labels and custom device sets
getdave Oct 4, 2019
a64c2cd
Fix to only build default repsonsive controls when necessary
getdave Oct 4, 2019
9926612
Adds tests to cover rendering of custom responsive controls
getdave Oct 4, 2019
d210cfe
Update to make label component part of component’s internal API
getdave Oct 8, 2019
b81e605
Adds callback prop to fire when responsive mode state changes
getdave Oct 8, 2019
8605087
Update to utilise withInstanceId HOC
getdave Oct 8, 2019
3ac118d
Removes unused export
getdave Oct 28, 2019
045dd49
Mark as “experimental”
getdave Oct 28, 2019
b5bfc06
Remove non exposed component API doc
getdave Oct 28, 2019
5c9841c
Extracts label to be a dedicated component
getdave Oct 28, 2019
b2bea8c
Updates to completely switch out DOM when responsive mode is toggled
getdave Oct 28, 2019
827bb82
Update to useCallback to avoid expensive DOM re-renders
getdave Oct 28, 2019
059b5a5
Updates i18n to provide better descriptions for translators
getdave Oct 28, 2019
ce3be6f
Updates devices list to contain unique non-translatable slug
getdave Oct 30, 2019
195aef5
Ensure renderDefault controls render prop has access to device details
getdave Oct 30, 2019
e821dc9
Begin documentation
getdave Oct 30, 2019
863783c
Rename “legend” prop to “title”
getdave Oct 30, 2019
8ebbd2c
Fix incorrect usage of _x and replace with translator comments
getdave Oct 30, 2019
64e719f
Update internal nomenclature from “device” to “viewport”
getdave Oct 30, 2019
e6aad72
Refactor to make component fully controlled
getdave Nov 1, 2019
48529f7
Adds custom hook useResponsiveBlockControl
getdave Nov 1, 2019
d72e6af
Revert "Adds custom hook useResponsiveBlockControl"
getdave Nov 1, 2019
a46f406
Remove testing implementation in Group Block
getdave Nov 1, 2019
15da1b3
Docs update
getdave Nov 1, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/block-editor/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export { default as MediaUpload } from './media-upload';
export { default as MediaUploadCheck } from './media-upload/check';
export { default as PanelColorSettings } from './panel-color-settings';
export { default as PlainText } from './plain-text';
export { default as __experimentalResponsiveBlockControl } from './responsive-block-control';
export {
default as RichText,
RichTextShortcut,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
ResponsiveBlockControl
=============================

`ResponsiveBlockControl` provides a standardised interface for the creation of Block controls that require **different settings per viewport** (ie: "responsive" settings).

For example, imagine your Block provides a control which affords the ability to change a "padding" value used in the Block display. Consider that whilst this setting may work well on "large" screens, the same value may not work well on smaller screens (it may be too large for example). As a result, you now need to provide a padding control _per viewport/screensize_.

`ResponsiveBlockControl` provides a standardised component for the creation of such interfaces within Gutenberg.

Complete control over rendering the controls is provided and the viewport sizes used are entirely customisable.

Note that `ResponsiveBlockControl` does not handle any persistence of your control values. The control you provide to `ResponsiveBlockControl` as the `renderDefaultControl` prop should take care of this.

## Usage

In a block's `edit` implementation, render a `<ResponsiveBlockControl />` component passing the required props plus:

1. a `renderDefaultControl` function which renders an interface control.
2. an boolean state for `isResponsive` (see "Props" below).
3. a handler function for `onIsResponsiveChange` (see "Props" below).


By default the default control will be used to render the default (ie: "All") setting _as well as_ the per-viewport responsive settings.

```jsx
import { registerBlockType } from '@wordpress/blocks';
import {
InspectorControls,
ResponsiveBlockControl,
} from '@wordpress/block-editor';

import { useState } from '@wordpress/element';

import {
DimensionControl,
} from '@wordpress/components';

registerBlockType( 'my-plugin/my-block', {
// ...

edit( { attributes, setAttributes } ) {

const [ isResponsive, setIsResponsive ] = useState( false );

// Used for example purposes only
const sizeOptions = [
{
label: 'Small',
value: 'small',
},
{
label: 'Medium',
value: 'medium',
},
{
label: 'Large',
value: 'large',
},
];

const { paddingSize } = attributes;


// Your custom control can be anything you'd like to use.
// You are not restricted to `DimensionControl`s, but this
// makes life easier if dealing with standard CSS values.
// see `packages/components/src/dimension-control/README.md`
const paddingControl = ( labelComponent, viewport ) => {
return (
<DimensionControl
label={ viewport.label }
onChange={ // handle update to padding value here }
value={ paddingSize }
/>
);
};

return (
<>
<InspectorControls>
<ResponsiveBlockControl
title='Block Padding'
property='padding'
renderDefaultControl={paddingControl}
isResponsive={ isResponsive }
onIsResponsiveChange={ () => {
setIsResponsive( ! isResponsive );
} }
/>
</InspectorControls>
<div>
// your Block here
</div>
</>
);
}
} );
```

## Props

### `title`
* **Type:** `String`
* **Default:** `undefined`
* **Required:** `true`

The title of the control group used in the `fieldset`'s `legend` element to label the _entire_ set of controls.

### `property`
* **Type:** `String`
* **Default:** `undefined`
* **Required:** `true`

Used to build accessible labels and ARIA roles for the control group. Should represent the layout property which the component controls (eg: `padding`, `margin`...etc).

### `isResponsive`
* **Type:** `Boolean`
* **Default:** `false` )
* **Required:** `false`

Determines whether the component displays the default or responsive controls. Updates the state of the toggle control. See also `onIsResponsiveChange` below.

### `onIsResponsiveChange`
* **Type:** `Function`
* **Default:** `undefined`
* **Required:** `true`

A callback function invoked when the component's toggle value is changed between responsive and non-responsive mode. Should be used to update the value of the `isResponsive` prop to reflect the current state of the toggle control.

### `renderDefaultControl`
* **Type:** `Function`
* **Default:** `undefined`
* **Required:** `true`
* **Args:**
- **labelComponent:** (`Function`) - a rendered `ResponsiveBlockControlLabel` component for your control.
- **viewport:** (`Object`) - an object representing viewport attributes for your control.

A render function (prop) used to render the control for which you would like to display per viewport settings.

For example, if you have a `SelectControl` which controls padding size, then pass this component as `renderDefaultControl` and it will be used to render both default and "responsive" controls for "padding".

The component you return from this function will be used to render the control displayed for the (default) "All" state and (if the `renderResponsiveControls` is not provided) the individual responsive controls when in "responsive" mode.

It is passed a pre-created, accessible `<label>`. Your control may also use the contextual information provided by the `viewport` argument to ensure your component renders appropriately depending on the `viewport` setting currently being rendered (eg: `All` or one of the responsive variants).

__Note:__ you are required to handle persisting any state produced by the component you pass as `renderDefaultControl`. `ResponsiveBlockControl` is "controlled" and does not persist state in any form.

```jsx
const renderDefaultControl = ( labelComponent, viewport ) => {
const { id, label } = viewport;
// eg:
// {
// id: 'small',
// label: 'All'
// }
return (
<DimensionControl
label={ labelComponent }
/>
);
};
```

### `renderResponsiveControls`
* **Type:** `Function`
* **Default:** `undefined`
* **Required:** `false`
* **Args:**
- **viewports:** (`Array`) - an array of viewport `Object`s, each with an `id` and `label` property.


An optional render function (prop) used to render the controls for the _responsive_ settings. If not provided, by default, responsive controls will be _automatically_ rendered using the component returned by the `renderDefaultControl` prop. For _complete_ control over the output of the responsive controls, you may return a component here and it will be rendered when the control group is in "responsive" mode.

```jsx
const renderResponsiveControls = (viewports) => {
const inputId = uniqueId(); // lodash

return viewports.map( ( { id, label } ) => {
return (
<Fragment key={ `${ inputId }-${ id }` }>
<label htmlFor={ `${ inputId }-${ id }` }>Custom Viewport { label }</label>
<input
id={ `${ inputId }-${ id }` }
defaultValue={ label }
type="range"
/>
</Fragment>
);
} );
}
```

### `toggleLabel`
* **Type:** `String`
* **Default:** `Use the same %s on all screensizes.` (where "%s" is the `property` prop - see above )
* **Required:** `false`

Optional label used for the toggle control which switches the interface between showing responsive controls or not.

### `defaultLabel`
* **Type:** `Object`
* **Default:**
```js
{
id: 'all',
label: 'All',
}
```
* **Required:** `false`

Optional object describing the attributes of the default value. By default this is `All` which indicates the control will affect "all viewports/screensizes".

### `viewports`
* **Type:** `Array`
* **Default:**
```js
[
{
id: 'small',
label: 'Small screens',
},
{
id: 'medium',
label: 'Medium screens',
},
{
id: 'large',
label: 'Large screens',
},
]
```
* **Required:** `false`

An array of viewport objects, each describing a configuration for a particular viewport size. These are used to determine the number of responsive controls to display and the configuration of each.



Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* WordPress dependencies
*/
import { __, sprintf } from '@wordpress/i18n';

import { Fragment } from '@wordpress/element';

import {
ToggleControl,
} from '@wordpress/components';

/**
* Internal dependencies
*/
import ResponsiveBlockControlLabel from './label';

function ResponsiveBlockControl( props ) {
const {
title,
property,
toggleLabel,
onIsResponsiveChange,
renderDefaultControl,
renderResponsiveControls,
isResponsive = false,
defaultLabel = {
id: 'all',
label: __( 'All' ), /* translators: 'Label. Used to signify a layout property (eg: margin, padding) will apply uniformly to all screensizes.' */
},
viewports = [
{
id: 'small',
label: __( 'Small screens' ),
},
{
id: 'medium',
label: __( 'Medium screens' ),
},
{
id: 'large',
label: __( 'Large screens' ),
},
],
} = props;

if ( ! title || ! property || ! renderDefaultControl ) {
return null;
}

/* translators: 'Toggle control label. Should the property be the same across all screen sizes or unique per screen size.'. %s property value for the control (eg: margin, padding...etc) */
const toggleControlLabel = toggleLabel || sprintf( __( 'Use the same %s on all screensizes.', ), property );

/* translators: 'Help text for the responsive mode toggle control.' */
const toggleHelpText = __( 'Toggle between using the same value for all screen sizes or using a unique value per screen size.' );

const defaultControl = renderDefaultControl( <ResponsiveBlockControlLabel property={ property } viewport={ defaultLabel } />, defaultLabel );

const defaultResponsiveControls = () => {
return viewports.map( ( viewport ) => (
<Fragment key={ viewport.id }>
{ renderDefaultControl( <ResponsiveBlockControlLabel property={ property } viewport={ viewport } />, viewport ) }
</Fragment>
) );
};

return (

<fieldset className="block-editor-responsive-block-control">
<legend className="block-editor-responsive-block-control__title">{ title }</legend>

<div className="block-editor-responsive-block-control__inner">
<ToggleControl
className="block-editor-responsive-block-control__toggle"
label={ toggleControlLabel }
checked={ ! isResponsive }
onChange={ onIsResponsiveChange }
help={ toggleHelpText }
/>

getdave marked this conversation as resolved.
Show resolved Hide resolved
{ ! isResponsive && (
<div className="block-editor-responsive-block-control__group block-editor-responsive-block-control__group--default" >
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same note here about BEM convention as at: #17846 (comment)

Also:

  • These classes are only referenced in tests. It's not strictly problematic to do so, but ideally the runtime implementation needn't be conformed specifically to tailor to tests.
  • I think in representing this as some sort of on/off state (it either is or isn't "responsive"), we'd not need to refer to this explicitly as "default" but rather as the absence of "responsive". In other words, I'd suggest using modifier class is-responsive for the other condition, and for this one, simply omitting it.

The second point would actually enable us to simplify this implementation, where currently there is code duplication for block-editor-responsive-block-control__group largely because of how this class is assigned. With the above changes, it could become a unified wrapper of:

<div className={ classnames( 'block-editor-responsive-block-control__group', { 'is-responsive': isResponsive } ) } hidden={ ! isResponsive }>

{ defaultControl }
</div>
) }

{ isResponsive && (
<div className="block-editor-responsive-block-control__group block-editor-responsive-block-control__group--responsive" hidden={ ! isResponsive }>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assignment of hidden here is effectively dead code, because this div is only rendered inside the condition of isResponsive && <div />.

{ ( renderResponsiveControls ? renderResponsiveControls( viewports ) : defaultResponsiveControls() ) }
</div>
) }

</div>
</fieldset>
);
}

export default ResponsiveBlockControl;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* WordPress dependencies
*/
import { withInstanceId } from '@wordpress/compose';
import { _x, sprintf } from '@wordpress/i18n';
import { Fragment } from '@wordpress/element';

const ResponsiveBlockControlLabel = ( { instanceId, property, viewport, desc } ) => {
const accessibleLabel = desc || sprintf( _x( 'Controls the %1$s property for %2$s viewports.', 'Text labelling a interface as controlling a given layout property (eg: margin) for a given screen size.' ), property, viewport.label );
return (
<Fragment>
<span aria-describedby={ `rbc-desc-${ instanceId }` }>
{ viewport.label }
</span>
<span className="screen-reader-text" id={ `rbc-desc-${ instanceId }` }>{ accessibleLabel }</span>
</Fragment>
);
};

export default withInstanceId( ResponsiveBlockControlLabel );

Loading