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 standardised `ResponsiveBlockControl` component #16790

Open
wants to merge 25 commits into
base: master
from

Conversation

@getdave
Copy link
Contributor

commented Jul 29, 2019

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:

  • Standardised, accessible markup using fieldset and legend
  • Complete control over the rendered controls via render props
  • Render prop to render a default control (non-responsive mode)
  • Toggle to turn responsive controls on/off
  • Render prop to render a Responsive control(s) (responsive mode)

How has this been tested?

I've temporarily hacked and implementation into the core/group Block which utilises <select> inputs and temporary state for demo purposes.

Note that this will be removed before merging.

Screenshots

Default

Screen Shot 2019-07-29 at 11 48 09

Responsive (toggled)

Screen Shot 2019-07-29 at 11 48 18

Types of changes

New feature (non-breaking change which adds functionality)

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • My code has proper inline documentation.
  • I've included developer documentation if appropriate.
@karmatosed

This comment has been minimized.

Copy link
Member

commented Aug 2, 2019

Adding a design review here there are a few points come to mind:

image

  • It struck me why no large option. I found that super interesting to experience. I think that's covered in another PR though #16791 maybe?
  • I have to admit it was hard to guess what small/medium would be.
  • I can see people wanting to set the spacing themselves. This is where I think we might be getting into a slippery slope of mammoth configurations but I am noting it.
  • I wonder if using term mobile is future proof? You use your iPad not just mobile.
@karmatosed karmatosed self-requested a review Aug 2, 2019
@getdave getdave requested a review from shaunandrews Aug 2, 2019
@shaunandrews shaunandrews referenced this pull request Aug 2, 2019
5 of 5 tasks complete
@shaunandrews

This comment has been minimized.

Copy link

commented Aug 2, 2019

Took some time to work up a design.

image

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.

image

--

I mentioned some related changes to the placeholder value over in #16791

@shaunandrews

This comment has been minimized.

Copy link

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.

@getdave

This comment has been minimized.

Copy link
Contributor Author

commented Aug 12, 2019

Took some time to work up a design.

Thanks for much for taking the time here @shaunandrews. It is much appreciated.

I've reverse the default state of the Switch to be on by default. Switching it off would show the multiple rows.

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?

...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.

Nice exploration. I agree this doesn't scale well though.

At some point we may end up with a "mobile" switch for each of these settings. This is going to be problematic.

Yes that could get a little...noisy UI-wise!

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.

A good idea. I wonder if @youknowriad is aware of any efforts in this area?

I realize this is a much larger change than you initial proposed, but I think its the better way of handling the UI.

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?

@mapk

This comment has been minimized.

Copy link
Contributor

commented Aug 14, 2019

provides a UI for Block controls that need to have responsive settings.

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.

@getdave

This comment has been minimized.

Copy link
Contributor Author

commented Aug 14, 2019

@mapk Thanks very much for your feedback.

I think we came super close with this particular design from @kjellr : #13363 (comment)

@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.

@mapk

This comment has been minimized.

Copy link
Contributor

commented Aug 14, 2019

Are you able to advise how you feel about the designs @shaunandrews has proposed

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.

Screen Shot 2019-08-14 at 8 29 54 AM

  1. Looking at this, I first thought the dropdown "Small" was a reference to device size due to the label. To curtail this confusion, we repeated a lot of words in the other mockups for Columns.
  2. I think there will be an a11y problem with switching out the label of the dropdown when toggling on. This will probably not work well with screen-readers.
  3. That second point leads me to think if we move the toggle to the top, and keep the dropdowns below it, that might work better. I'm sure some language would have to be changed to support this, but when toggling the top control, it feels okay to have the bottom controls reflect the change.
@getdave

This comment has been minimized.

Copy link
Contributor Author

commented Sep 12, 2019

@mapk Thanks for this and sorry for the mega delay in responding. I've been away a lot recently.

I first thought the dropdown "Small" was a reference to device size due to the label. To curtail this confusion, we repeated a lot of words in the other mockups for Columns.

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 there will be an a11y problem with switching out the label of the dropdown when toggling on.

I think you're right. The solution should be

  • don't add/remove from the DOM - use show/hide
  • to set the disabled attribute on the element that has been hidden (or even the hidden attribute) to better semantically communicate the change to screenreaders
  • consider which aria roles can communicate a change of focus area

...if we move the toggle to the top, and keep the dropdowns below it, that might work bette

I can do that. I don't think this precludes making the other a11y changes though.

@getdave

This comment has been minimized.

Copy link
Contributor Author

commented Sep 14, 2019

@afercia Regarding this comment, I tried adding fieldset around the fields on a per device basis. However, this leads to a lot of visual and markup duplication of the same term.

See below

Screen Shot 2019-09-15 at 08 39 05

Screen Shot 2019-09-15 at 08 38 46

Notice how the legend and the label are repeating the same word.

Questions

  1. Would you say it's overkill to have a fieldset when there is only a single field (ie: for example, we may not want both margin and padding so it's entirely possible that we'll just have this control for padding).

  2. If no to 1, then how would you recommend resolving the duplication of text/term between label and legend?

  3. When the toggle is activated how would you recommend changing the focus to the new fieldsets and marking the All as no longer "active"? I was previously adding/removing the fields from the DOM on toggle, but I felt that perhaps that wasn't a very accessible approach. I now retain both fieldsets in the DOM and so I thought perhaps toggling a hidden attribute on the parent fieldset might be the best way of indicating the change of state? You may need to try this out to understand what I'm talking about.

Also, are there any other ways I could look to improve this?

@getdave getdave force-pushed the add/responsive-block-controls-component branch from ad2c465 to 7da8329 Sep 14, 2019
getdave added a commit that referenced this pull request Sep 15, 2019
Addresses points raised in #16790
@afercia

This comment has been minimized.

Copy link
Contributor

commented Sep 16, 2019

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 <fieldset> and <legend> elements to group the settings based on devices. Pseudo code:

fieldset
    visible legend: All devices
    DimensionControl: Margin
    DimensionControl: Padding
/fieldset
fieldset
    visible legend: Small screen devices
    DimensionControl: Margin
    DimensionControl: Padding
/fieldset
etc.
@afercia

This comment has been minimized.

Copy link
Contributor

commented Sep 16, 2019

@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.

  • Label = "Desktop/Tablet/Mobile" doesn't tell what the values within the select are about
  • semantically, that actually means: "Select a small/medium Desktop" which is confusing
  • the selects are meant to set the padding values: they contain padding values therefore they should be labelled "Padding"
  • To clarify what this "padding" relates to, the fieldset legend would give context by clarifying it's about "Desktop"
  • This makes even more sense when there are multiple selects within the same group (e.g. padding, margin)

Would you say it's overkill to have a fieldset when there is only a single field

It really depends on the text used for the labels.
In that case, the visible label should also clarify what the "padding" is about, e.g.:
<label for...>Padding for desktop devices</label>

It is OK to remove all the fieldsets if each label provides all the necessary information.

When the toggle is activated how would you recommend changing the focus to the new fieldsets

Couple of considerations:

  • I'm not sure a switch toggle is the most appropriate control: switch toggles should be used to set a setting that gets immediately saved without the need of submitting. In this case, it justs switches the visibility of a UI section. I'd consider to use a different control in the first place. /Cc @mapk
  • I'd move the toggle control (whatever it is) before all the selects. This way, focus can stay on the toggle and the UI after that changes. Users can explore the content after the toggle as they would do normally. Instead, if the UI that changes is placed before the toggle, they won't have a clue something has changed.
getdave added a commit that referenced this pull request Sep 17, 2019
getdave added a commit that referenced this pull request Sep 17, 2019
Address concerns raised in #16790
@getdave

This comment has been minimized.

Copy link
Contributor Author

commented Sep 17, 2019

@afercia Thanks for your feedback.

Here's a screenshot

Screen Shot 2019-09-17 at 15 50 28

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.

I agree. So I've removed the fieldsets but kept it open for developers to utilise these when it is most appropriate. The rendering of the control itself if fully in the control of the developer so they can choose to use fieldsets where that makes sense.

It really depends on the text used for the labels.
In that case, the visible label should also clarify what the "padding" is about, e.g.:

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'm not sure a switch toggle is the most appropriate control

I'll leave that to you and the design team. @mapk could you confirm a direction for us here please? Much appreciated.

I'd move the toggle control (whatever it is) before all the selects.

Done 👍

getdave added 22 commits Jul 29, 2019
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.
@getdave getdave force-pushed the add/responsive-block-controls-component branch from 3d452b3 to ff209aa Oct 4, 2019
Copy link
Member

left a comment

Hi @getdave nice work here, things well to me I just left some comments/questions.
I also think we should mark the components we are exposing as __experimental, we are in a starting phase and I would prefer to not lockin a specific API yet allowing us to change things more easily.

);
};

const handleToggle = ( isChecked ) => {

This comment has been minimized.

Copy link
@jorgefilipecosta

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.

Copy link
@getdave

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.

Copy link
@jorgefilipecosta

jorgefilipecosta Oct 8, 2019

Member

Hi @getdave, I answered this question on #16791 (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.

Copy link
@jorgefilipecosta

jorgefilipecosta Oct 4, 2019

Member

We need a translator comment here, specifying what each %s means.

This comment has been minimized.

Copy link
@jorgefilipecosta

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;

This comment has been minimized.

Copy link
@jorgefilipecosta

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.

Copy link
@getdave

getdave Oct 7, 2019

Author Contributor

Great point. I will sort this.

const defaultResponsiveControls = () => {
return devices.map( ( deviceLabel, index ) => (
<Fragment key={ index }>
{ renderDefaultControl( deviceLabel ) }

This comment has been minimized.

Copy link
@jorgefilipecosta

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.

Copy link
@jorgefilipecosta

jorgefilipecosta Oct 4, 2019

Member

Why hiding instead of rendering one option or the other depending on isResponsiveMode?

This comment has been minimized.

Copy link
@getdave

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.

Copy link
@jorgefilipecosta

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.

Copy link
@jorgefilipecosta

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.

Copy link
@getdave

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.

Copy link
@jorgefilipecosta

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.

Copy link
@getdave

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.

Copy link
@getdave

getdave Oct 8, 2019

Author Contributor

I've revised this a little. Let me know what you think.

This comment has been minimized.

Copy link
@jorgefilipecosta

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.

} 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.

Copy link
@jorgefilipecosta

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.

Copy link
@jorgefilipecosta

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.

Copy link
@jorgefilipecosta

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.

Copy link
@jorgefilipecosta

jorgefilipecosta Oct 8, 2019

Member

I think we could remove this export.

@rodrigodagostino

This comment has been minimized.

Copy link

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?
Designing based on device sizes is a bad practice that should be avoided. I think small, medium and large would be a better approach than mobile, tablet and desktop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.