Perfect your code
With built-in code review tools, GitHub makes it easy to raise the quality bar before you ship. Join the 40 million developers who've merged over 200 million pull requests.
Sign up for free See pricing for teams and enterprisesAdd standardised `ResponsiveBlockControl` component #16790
Conversation
This comment has been minimized.
This comment has been minimized.
Adding a design review here there are a few points come to mind:
|
This comment has been minimized.
This comment has been minimized.
shaunandrews
commented
Aug 2, 2019
Took some time to work up a design. One thing you'll notice is I've reverse the default state of the Switch to be on by default. Switching it off would show the multiple rows. An alternative ideas is to list out all available options with a Switch for each row. This tends to work great if we plan to have more than 2 options, but otherwise feels a little busy and overwhelming. -- I mentioned some related changes to the placeholder value over in #16791 |
This comment has been minimized.
This comment has been minimized.
shaunandrews
commented
Aug 6, 2019
Another consideration here is that its likely we'll have many responsive settings across blocks and even within settings for a single block. For example, consider the need to adjust not only basic spacing (like we have above) but also font-size, or color, or general layout. At some point we may end up with a "mobile" switch for each of these settings. This is going to be problematic. What we might want to do instead, is have some sort of Switch for the entire editor. Something along the lines of a "mobile mode" or a "tablet mode" that would allow changes to be made to that specific view, without affecting the "desktop mode" display. I realize this is a much larger change than you initial proposed, but I think its the better way of handling the UI. |
This comment has been minimized.
This comment has been minimized.
Thanks for much for taking the time here @shaunandrews. It is much appreciated.
I see why you've done this. Playing devil's advocate here for a second. Does it feel a little odd to be turning something "off" to enable a feature?
Nice exploration. I agree this doesn't scale well though.
Yes that could get a little...noisy UI-wise!
A good idea. I wonder if @youknowriad is aware of any efforts in this area?
I agree although as you've identified it's a lot more complex. @youknowriad @mtias are you able to advise as to whether you feel the direction we're proposing here is suitable or will it be wasted effort in the long run if you have a "mobile mode" in the works? |
This comment has been minimized.
This comment has been minimized.
This seems very related to the responsive block controls that went through several iterations here: #13363 I think we came super close with this particular design from @kjellr : #13363 (comment) I understand the UI in #13363 is primarily for columns, but can help inform the design behind spacing too hopefully. |
This comment has been minimized.
This comment has been minimized.
@mapk Thanks very much for your feedback.
@mapk This is what I was building on having touched base with @kjellr earlier. However there have been a few iterations since and @shaunandrews has provided some more input. Are you able to advise how you feel about the designs @shaunandrews has proposed (cc @kjellr )? I'd like to avoid costly refactors in code because it takes a long time. Ideally, I've love ot get consensus from the design before I write any more code here. Much appreciated. |
This comment has been minimized.
This comment has been minimized.
For sure! Sorry to have let this stall. Regarding @shaunandrews' mockups, I definitely prefer the first concept, so I'll comment on that one specifically.
|
This comment has been minimized.
This comment has been minimized.
@mapk Thanks for this and sorry for the mega delay in responding. I've been away a lot recently.
I'm not completely clear on what you're recommending we do here to avoid this confusion. I've previously been asked to remove any additional "explainer" text to simplify the UI. I'd love to see/hear more detail here to help ensure I"m on the same page.
I think you're right. The solution should be
I can do that. I don't think this precludes making the other a11y changes though. |
This comment has been minimized.
This comment has been minimized.
@afercia Regarding this comment, I tried adding See below Notice how the Questions
Also, are there any other ways I could look to improve this? |
ad2c465
to
7da8329
This comment has been minimized.
This comment has been minimized.
Quoting from #16791 (comment) my understanding, this control is meant to add padding / margin settings to the group block, with the ability to specify this setting for different types of devices. If this is the case, I'd suggest to use
|
This comment has been minimized.
This comment has been minimized.
@getdave I see only now the screenshot and questions before my previous comment Yes, in your example the nested fieldset + legend aren't ideal. That would require to change the HTML structure and the text used for legend / labels. As general rules:
In the example above I'm not sure the labels for the select are ideal.
It really depends on the text used for the labels. It is OK to remove all the fieldsets if each label provides all the necessary information.
Couple of considerations:
|
This comment has been minimized.
This comment has been minimized.
@afercia Thanks for your feedback. Here's a screenshot
I agree. So I've removed the
I believe the label per device approach is better in this case. As a result I've amended the label text to include a full description as you advise. However, previously design feedback is that including the full text is too visually "verbose". Therefore I suggest a compromise of providing the full text to screen readers whilst providing a shortened version for sighted users. I think with the context of the "Padding" fieldset legend surrounding all the controls it is clear enough that the device names refer to the setting for "padding" for that device.
I'll leave that to you and the design team. @mapk could you confirm a direction for us here please? Much appreciated.
Done |
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.
Addresses #16790 (comment)
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
3d452b3
to
ff209aa
Hi @getdave nice work here, things well to me I just left some comments/questions. |
); | ||
}; | ||
|
||
const handleToggle = ( isChecked ) => { |
This comment has been minimized.
This comment has been minimized.
jorgefilipecosta
Oct 4, 2019
Member
Could we wrap this inside useCallback to avoid creating a new function on each re-render? (same for the previous function)
This comment has been minimized.
This comment has been minimized.
getdave
Oct 7, 2019
Author
Contributor
Can I check why you are suggesting we use useCallback here? My understanding that it was effectively to "memoize" computationally expensive functions and/or to ensure referential equality.
I assume you're coming at this with a perf lense? If so do you feel that the function is sufficiently intensive to warrant a useCallback
?
If I'm missed something I'd be open to an explanation - I'm always looking to improve my understanding, especially in the field of hooks!
This comment has been minimized.
This comment has been minimized.
} from '@wordpress/components'; | ||
|
||
export function ResponsiveBlockControlLabel( { property, device } ) { | ||
const accessibleLabel = sprintf( __( 'Controls the %s property for %s devices.' ), property, device ); |
This comment has been minimized.
This comment has been minimized.
jorgefilipecosta
Oct 4, 2019
Member
We need a translator comment here, specifying what each %s means.
This comment has been minimized.
This comment has been minimized.
jorgefilipecosta
Oct 8, 2019
Member
I forgot to tell we also need to number the strings so the comment can refer them %1$s, %2$s.
packages/block-editor/src/components/responsive-block-control/index.js
Outdated
Show resolved
Hide resolved
} | ||
|
||
function ResponsiveBlockControl( props ) { | ||
const { legend, property, toggleLabel, responsiveControlsActive = false, renderDefaultControl, defaultLabel = __( 'All' ), devices = [ __( 'Desktop' ), __( 'Tablet' ), __( 'Mobile' ) ], renderResponsiveControls } = props; |
This comment has been minimized.
This comment has been minimized.
jorgefilipecosta
Oct 4, 2019
Member
I think the devices need an identifier like a slug that we have for colors. Right now they are only identified with a label that is tralatable we need a unique way to refer a device that does not change with the translation.
This comment has been minimized.
This comment has been minimized.
const defaultResponsiveControls = () => { | ||
return devices.map( ( deviceLabel, index ) => ( | ||
<Fragment key={ index }> | ||
{ renderDefaultControl( deviceLabel ) } |
This comment has been minimized.
This comment has been minimized.
jorgefilipecosta
Oct 4, 2019
Member
We only pass the label to the renderDefaultControl, in a real case if we were storing the value somewhere e.g: attribute we would need to know an identifier of the size we are referring to. So, besides passing a label I think we need to pass something like a device id.
onChange={ handleToggle } | ||
help={ toggleHelpText } | ||
/> | ||
|
This comment has been minimized.
This comment has been minimized.
jorgefilipecosta
Oct 4, 2019
Member
Why hiding instead of rendering one option or the other depending on isResponsiveMode?
This comment has been minimized.
This comment has been minimized.
getdave
Oct 7, 2019
Author
Contributor
I was thinking about a11y. I was thinking if you were focused on one set of controls and then you toggled to the other "mode" (eg: "Default" -> "Responsive"). I felt it might be a bit confusing or jarring to have the original controls completely removed from the DOM and a load of new controls suddenly appear.
What do you think @afercia?
This comment has been minimized.
This comment has been minimized.
jorgefilipecosta
Oct 8, 2019
Member
I felt it might be a bit confusing or jarring to have the original controls completely removed from the DOM and a load of new controls suddenly appear.
If we leave things on the dom, screen reader users may still be able to interact with the controls? If that's the case, they will interact with controls that make nothing that seems worst.
We have at least another case exactly like this the hasParallax prop in the Cover block. That case does not renders on the dom so I guess we should follow the same approach here. If we find something better from the a11y point of view in this PR I think as a follow-up we should update the other PR.
responsiveControlsActive={ true } | ||
renderDefaultControl={ ( label ) => ( | ||
<SelectControl | ||
label={ <ResponsiveBlockControlLabel property="Padding" device={ label } /> } |
This comment has been minimized.
This comment has been minimized.
jorgefilipecosta
Oct 4, 2019
Member
Can't the label passed as prop already include the component ResponsiveBlockControlLabel rendered, so we don't need to expose ResponsiveBlockControlLabel at all?
This comment has been minimized.
This comment has been minimized.
getdave
Oct 7, 2019
Author
Contributor
I've been wrestling with this one.
I've just realised I could pass the <ResponsiveBlockControlLabel />
component as an argument to the renderDefaultControl
render prop. This allow the render prop function to consume the label as a component and utilise that however it needs.
In the example case of a <SelectControl />
we simply pass the label component as the label
prop and everything "just works". A more complex renderDefaultControl
use case could use the component directly in a more custom manner.
Thanks for prompting me to reconsider my approach.
Note I've left ResponsiveBlockControlLabel
exposed as the moment but I guess we could remove the export
?
function ResponsiveBlockControl( props ) { | ||
const { legend, property, toggleLabel, responsiveControlsActive = false, renderDefaultControl, defaultLabel = __( 'All' ), devices = [ __( 'Desktop' ), __( 'Tablet' ), __( 'Mobile' ) ], renderResponsiveControls } = props; | ||
|
||
const [ isResponsiveMode, setIsResponsiveMode ] = useState( responsiveControlsActive ); |
This comment has been minimized.
This comment has been minimized.
jorgefilipecosta
Oct 4, 2019
•
Member
We are keeping the responsive mode as a local state, but when the flag is toggled the parent needs to know about this state change, otherwise how is the change going to be persisted?
I think the parent should pass something like isResponsiveMode, and an onIsResponsiveMode change and the parent should be responsible for handling and storing changes.
This comment has been minimized.
This comment has been minimized.
getdave
Oct 7, 2019
Author
Contributor
I've also been wrestling with this one.
You'll note until recently this was controlled by the consuming "parent" component. Then in this commit, I moved it to become component/local state.
This was because it felt odd that the consumer needed to provide a means to toggle the UI. I felt the state of the toggle should be internal (thereby avoiding the need for the user to have to know about this implementation detail) but we should expose a callback to notify when it changes and also a means to set the default state.
Note that currently, I've tried to find the best of both worlds. By default you don't need to handle the toggle manually when utilising the component - it is handled internally and "just works" (this is what I'd expect to happen as a user). However, you can optionally set the boolean responsiveControlsActive
prop to determine the initial state of the UI (ie: open/closed responsive controls). This allows us to persist an open/closed state between renders and/or editor initialisations. I probably need a decent test case for this scenario.
It's simple enough to roll this back to my original implementation (which is what I think you are recommending) but I am concerned about making this component too hard to utilise.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
jorgefilipecosta
Oct 8, 2019
Member
This was because it felt odd that the consumer needed to provide a means to toggle the UI.
If the consumer passes isResponsiveMode and onIsResponsiveModeChange the consumer is not passing a means to toggle the UI it is passing if the current mode is responsive and a handler to change it. It is not even aware that the mode is changed by using a toggle.
If responsiveControlsActive changes, the component will not reflect that change, this seems problematic is it makes it impossible to apply a change in the flag from a third party plugin if the component is rendered.
Addresses #16790 (comment). Also updates tests.
Addresses https://github.com/jorgefilipecosta
Addresses #16790 (comment)
} from '@wordpress/components'; | ||
|
||
export function ResponsiveBlockControlLabel( { property, device } ) { | ||
const accessibleLabel = sprintf( __( 'Controls the %s property for %s devices.' ), property, device ); |
This comment has been minimized.
This comment has been minimized.
jorgefilipecosta
Oct 8, 2019
Member
I forgot to tell we also need to number the strings so the comment can refer them %1$s, %2$s.
function ResponsiveBlockControl( props ) { | ||
const { legend, property, toggleLabel, responsiveControlsActive = false, renderDefaultControl, defaultLabel = __( 'All' ), devices = [ __( 'Desktop' ), __( 'Tablet' ), __( 'Mobile' ) ], renderResponsiveControls } = props; | ||
|
||
const [ isResponsiveMode, setIsResponsiveMode ] = useState( responsiveControlsActive ); |
This comment has been minimized.
This comment has been minimized.
jorgefilipecosta
Oct 8, 2019
Member
This was because it felt odd that the consumer needed to provide a means to toggle the UI.
If the consumer passes isResponsiveMode and onIsResponsiveModeChange the consumer is not passing a means to toggle the UI it is passing if the current mode is responsive and a handler to change it. It is not even aware that the mode is changed by using a toggle.
If responsiveControlsActive changes, the component will not reflect that change, this seems problematic is it makes it impossible to apply a change in the flag from a third party plugin if the component is rendered.
@@ -43,6 +65,19 @@ function GroupEdit( { | |||
}, | |||
] } | |||
/> | |||
|
|||
<PanelBody title={ __( 'Spacing' ) }> | |||
<ResponsiveBlockControl |
This comment has been minimized.
This comment has been minimized.
jorgefilipecosta
Oct 8, 2019
Member
I guess we should mark the component as experimental for some time.
ToggleControl, | ||
} from '@wordpress/components'; | ||
|
||
export const ResponsiveBlockControlLabel = withInstanceId( function ResponsiveBlockControlLabel( { instanceId, property, device } ) { |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
rodrigodagostino
commented
Oct 14, 2019
Seems I'm a bit late to the discussion, but I still would like to ask: wouldn't it be better to have the control be content-oriented instead of device-oriented? |
getdave commentedJul 29, 2019
•
edited
PLEASE READ THIS BEFORE REVIEWING😀
This PR isn't in any way wired up to Block state. Therefore changing the values in the UI doesn't do anything or persist any settings whatsoever.
This is purely a PR for the UI only to allow multiple controls for different viewports only. It is a sub PR of a wider effort to allow the Group Block to define spacing controls (eg: margin / padding). See below for more details.
Description
Sub PR of Adds basic dimension controls to Group Block.
Adds a standardised
<ResponsiveBlockControl>
component which provides a UI for Block controls that need to have responsive settings.Features:
fieldset
andlegend
How has this been tested?
I've temporarily hacked and implementation into the
core/group
Block which utilises<select>
inputs and temporarystate
for demo purposes.Note that this will be removed before merging.
Screenshots
Default
Responsive (toggled)
Types of changes
New feature (non-breaking change which adds functionality)
Checklist: