-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add experimental
ResponsiveBlockControl
component (#16790)
* 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
Showing
8 changed files
with
761 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
237 changes: 237 additions & 0 deletions
237
packages/block-editor/src/components/responsive-block-control/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
97 changes: 97 additions & 0 deletions
97
packages/block-editor/src/components/responsive-block-control/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
21 changes: 21 additions & 0 deletions
21
packages/block-editor/src/components/responsive-block-control/label.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ); | ||
|
Oops, something went wrong.