Skip to content

Commit

Permalink
Add experimental ResponsiveBlockControl component (#16790)
Browse files Browse the repository at this point in the history
* Basic component skeleton

* Adds component demo to Group Block

* Make toggle control label dynamic and translatable

* Adds styles

* Automatically render responsive controls based on default control

Continue to allow full customisation of the responsive controls, but by default render the responsive fields using the same markup as the default control. This avoids duplication when consuming the component.

* Wrap each field group in a `fieldset` element

* Invert toggle and language to be on by default.

Addresses points raised in #16790

* Adds initial tests

* Update tests to resemble how a user interacts with Component

* Add toggle state test

* Update to switch toggle to preceed controls

Addresses concern raised in #16790

* Update individual control labels to fully describe control for a11y

Address concerns raised in #16790

* Fixes form ui to improve alignment

Addresses #16790 (comment)

* Improves i18n of generated control label

Addresses #16790 (comment).

Note that aria-describedby is the correct type of aria role for this use case https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-describedby_attribute

* Improve a11y by leaving DOM intact between state changes.

* Adds aria description to toggle control for improved a11y context

* Update tests

* Update toggle of responsive controls internal to the component

* Adds test for responsiveControlsActive setting prop

* Add tests to cover custom labels and custom device sets

* Fix to only build default repsonsive controls when necessary

* Adds tests to cover rendering of custom responsive controls

* Update to make label component part of component’s internal API

Addresses #16790 (comment).

Also updates tests.

* Adds callback prop to fire when responsive mode state changes

Addresses https://github.com/jorgefilipecosta

* Update to utilise withInstanceId HOC

Addresses #16790 (comment)

* Removes unused export

This component is now provided by internal API and doesn’t need exposing.

Addresses #16790 (comment)

* Mark as “experimental”

Addresses #16790 (comment)

* Remove non exposed component API doc

* Extracts label to be a dedicated component

* Updates to completely switch out DOM when responsive mode is toggled

Please see #16790 (comment)

* Update to useCallback to avoid expensive DOM re-renders

Addresses #16790 (comment)

See also #16791 (comment)

* Updates i18n to provide better descriptions for translators

Addresses #16790 (comment)

* Updates devices list to contain unique non-translatable slug

This addresses concern that we need to be able to identify a “size” by a value that does not change due to translations.

See #16790 (comment)

* Ensure renderDefault controls render prop has access to device details

Addresses #16790 (comment)

* Begin documentation

Incomplete.

* Rename “legend” prop to “title”

This prop was named based on implementation details. Switching out for more agnostic term.

* Fix incorrect usage of _x and replace with translator comments

Addresses #16790 (comment)

Noting that this type of comment seems to be undocumented in Gutenberg…

* Update internal nomenclature from “device” to “viewport”

This is a small terminology change which could have a big change on the way developers think about repsonsive settings. We shouldn’t be thinking about “devices” but rather “viewports” / screen sizes. We can still present to the user as “devices” but the developer should not be tying layout changes to specific devices (or conceptual groups of devices - eg: “Mobile”).

* Refactor to make component fully controlled

Addresses #16790 (comment) by making the state of the responsive mode controlled by the consuming component. This completes the process of making the component fully controlled.

* Adds custom hook useResponsiveBlockControl

Attempt to relieve some of the overhead associated with having ResponsiveBlockControl be a fully controlled component. By consuming this hook, a developer can wire up a default handler for toggling responsive mode without having to worry about creating their own useState-based hooks.

* Revert "Adds custom hook useResponsiveBlockControl"

This reverts commit 48529f7.

* Remove testing implementation in Group Block

* Docs update
  • Loading branch information
getdave authored Nov 1, 2019
1 parent a353bc9 commit afdd6df
Show file tree
Hide file tree
Showing 8 changed files with 761 additions and 0 deletions.
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 }
/>

{ ! isResponsive && (
<div className="block-editor-responsive-block-control__group block-editor-responsive-block-control__group--default" >
{ defaultControl }
</div>
) }

{ isResponsive && (
<div className="block-editor-responsive-block-control__group block-editor-responsive-block-control__group--responsive" hidden={ ! isResponsive }>
{ ( 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

0 comments on commit afdd6df

Please sign in to comment.