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

Block: Outline when interacting with Toolbar Block Type/Movers #20938

Merged
merged 16 commits into from
Mar 23, 2020

Conversation

ItsJonQ
Copy link

@ItsJonQ ItsJonQ commented Mar 16, 2020

Description

Screen Shot 2020-03-16 at 3 58 55 PM

This update enhances the new G2-based Toolbar interactions to provide a highlighted outline for a block when interacting with the Block Type/Mover actions.

How has this been tested?

  • Tested locally in Gutenberg

Screenshots

Screen Capture on 2020-03-16 at 15-49-00

The GIF (above) demonstrates the outline when engaging with the Block Type/Mover actions.

Interaction

The outline shows when:

  • Block Type is focused or hovered
  • Any mover is focused or hovered

The style of the highlight is the same as the block's navigation mode styles. This can be seen by pressing ESC when on a block.

Top Toolbar

Screen Shot 2020-03-16 at 4 13 44 PM

The highlight feature still works for Top Toolbar (as well as smaller viewports)

Types of changes

  • Adding new blockHighlighted state to track the highlighted block, responding to Block Toolbar interactions
  • Added reducer, action, and selector to accompany the blockHighlighted state
  • Hook up the action with BlockToolbar
  • Hook up state to render with block-list/block

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.
  • I've updated all React Native files affected by any refactorings/renamings in this PR.

Resolves: #20875

@ItsJonQ ItsJonQ added [Type] Enhancement A suggestion for improvement. [Feature] Blocks Overall functionality of blocks [Feature] Writing Flow Block selection, navigation, splitting, merging, deletion... labels Mar 16, 2020
@ItsJonQ ItsJonQ self-assigned this Mar 16, 2020
@ItsJonQ ItsJonQ changed the title Toolbar: Outline block when interacting with Block Type/Movers Block: Outline when interacting with Toolbar Block Type/Movers Mar 16, 2020
@ItsJonQ ItsJonQ requested a review from mtias March 16, 2020 20:14
@github-actions
Copy link

github-actions bot commented Mar 16, 2020

Size Change: +394 B (0%)

Total Size: 857 kB

Filename Size Change
build/block-editor/index.js 101 kB +216 B (0%)
build/block-editor/style-rtl.css 11 kB +89 B (0%)
build/block-editor/style.css 11 kB +89 B (0%)
ℹ️ View Unchanged
Filename Size Change
build/a11y/index.js 998 B 0 B
build/annotations/index.js 3.43 kB 0 B
build/api-fetch/index.js 3.39 kB 0 B
build/autop/index.js 2.58 kB 0 B
build/blob/index.js 620 B 0 B
build/block-directory/index.js 6.02 kB 0 B
build/block-directory/style-rtl.css 760 B 0 B
build/block-directory/style.css 760 B 0 B
build/block-library/editor-rtl.css 7.24 kB 0 B
build/block-library/editor.css 7.24 kB 0 B
build/block-library/index.js 110 kB 0 B
build/block-library/style-rtl.css 7.41 kB 0 B
build/block-library/style.css 7.42 kB 0 B
build/block-library/theme-rtl.css 669 B 0 B
build/block-library/theme.css 671 B 0 B
build/block-serialization-default-parser/index.js 1.65 kB 0 B
build/block-serialization-spec-parser/index.js 3.1 kB 0 B
build/blocks/index.js 57.5 kB 0 B
build/components/index.js 191 kB 0 B
build/components/style-rtl.css 15.8 kB 0 B
build/components/style.css 15.7 kB 0 B
build/compose/index.js 6.21 kB 0 B
build/core-data/index.js 10.6 kB 0 B
build/data-controls/index.js 1.04 kB 0 B
build/data/index.js 8.2 kB 0 B
build/date/index.js 5.37 kB 0 B
build/deprecated/index.js 771 B 0 B
build/dom-ready/index.js 568 B 0 B
build/dom/index.js 3.06 kB 0 B
build/edit-post/index.js 91.2 kB 0 B
build/edit-post/style-rtl.css 8.47 kB 0 B
build/edit-post/style.css 8.46 kB 0 B
build/edit-site/index.js 5.95 kB 0 B
build/edit-site/style-rtl.css 2.69 kB 0 B
build/edit-site/style.css 2.69 kB 0 B
build/edit-widgets/index.js 4.43 kB 0 B
build/edit-widgets/style-rtl.css 2.58 kB 0 B
build/edit-widgets/style.css 2.58 kB 0 B
build/editor/editor-styles-rtl.css 381 B 0 B
build/editor/editor-styles.css 382 B 0 B
build/editor/index.js 43.8 kB 0 B
build/editor/style-rtl.css 4 kB 0 B
build/editor/style.css 3.98 kB 0 B
build/element/index.js 4.44 kB 0 B
build/escape-html/index.js 733 B 0 B
build/format-library/index.js 6.95 kB 0 B
build/format-library/style-rtl.css 502 B 0 B
build/format-library/style.css 502 B 0 B
build/hooks/index.js 1.93 kB 0 B
build/html-entities/index.js 622 B 0 B
build/i18n/index.js 3.49 kB 0 B
build/is-shallow-equal/index.js 710 B 0 B
build/keyboard-shortcuts/index.js 2.3 kB 0 B
build/keycodes/index.js 1.69 kB 0 B
build/list-reusable-blocks/index.js 2.99 kB 0 B
build/list-reusable-blocks/style-rtl.css 226 B 0 B
build/list-reusable-blocks/style.css 226 B 0 B
build/media-utils/index.js 4.84 kB 0 B
build/notices/index.js 1.57 kB 0 B
build/nux/index.js 3.01 kB 0 B
build/nux/style-rtl.css 616 B 0 B
build/nux/style.css 613 B 0 B
build/plugins/index.js 2.54 kB 0 B
build/primitives/index.js 1.5 kB 0 B
build/priority-queue/index.js 781 B 0 B
build/redux-routine/index.js 2.84 kB 0 B
build/rich-text/index.js 14.4 kB 0 B
build/server-side-render/index.js 2.55 kB 0 B
build/shortcode/index.js 1.7 kB 0 B
build/token-list/index.js 1.27 kB 0 B
build/url/index.js 4.01 kB 0 B
build/viewport/index.js 1.61 kB 0 B
build/warning/index.js 1.14 kB 0 B
build/wordcount/index.js 1.18 kB 0 B

compressed-size-action

@mtias
Copy link
Member

mtias commented Mar 17, 2020

This feels very good, thanks for putting it together. A few notes:

  • Let's try without the debounce for the highlight — it adds a delay which makes it hard to figure out when the outlines are showing based on your actions. It seems it uses the same debounce as the movers, perhaps we could decouple them, or bring them both down to 100ms. (I also feel the movers remain open a bit too stubbornly.)
  • Moving blocks is a bit rough now (the movers close as soon as you click on them and the initial click often doesn't trigger moving because they hide).
  • Is there a way to simplify the "states" (is-highlighted, is-focused, etc) somehow or do we need them separate?

@jasmussen
Copy link
Contributor

This is what I see:

highlight

The principle here feels very solid, and it is extra helpful when moving blocks:

movers

I would echo Matías comments exactly, by the way. As you can see in the GIF above, the mover control also seems to collapse whenever a moving action is taken. The mover control should just remain visible whenever focus is inside.

To Matías point about simplifying the states, it I wonder if you can take inspiration from his PR on adding a fade-effect between edit and select modes: #20933 — that very same fade would be really nice to have here as well!

@ItsJonQ
Copy link
Author

ItsJonQ commented Mar 17, 2020

Thanks for the feedback all!

Let's try without the debounce for the highlight

@mtias I can give that a try

the movers close as soon as you click on them and the initial click

I noticed this yesterday.

Reason: When the move happens, something in the code is forcing focus from the button to the body and back again. This brief focus shift messes with the show/hide logic.

It's tricky. I'll figure something out.

Is there a way to simplify the "states" (is-highlighted, is-focused, etc) somehow or do we need them separate

I believe we can simplify 💪

I wonder if you can take inspiration from his PR on adding a fade-effect between edit and select modes

@jasmussen I can look into the fade effect! Thanks for the suggestion 🤗

@jasmussen
Copy link
Contributor

Reason: When the move happens, something in the code is forcing focus from the button to the body and back again. This brief focus shift messes with the show/hide logic.

Is this also the case in master?

Awesome work!

@ItsJonQ
Copy link
Author

ItsJonQ commented Mar 17, 2020

Is this also the case in master?

@jasmussen It is not (phew). I think the state updates to render the outline is making it unhappy.

Will work on refining 💪

@ItsJonQ
Copy link
Author

ItsJonQ commented Mar 17, 2020

@jasmussen + @mtias I pushed up changes that improve the experience as suggested (above).

If you folks would test it again, that would be lovely!

Is there a way to simplify the "states" (is-highlighted, is-focused, etc) somehow or do we need them separate

I took a look. Although these states render the same UI (borders around the block, rendered with :pseudo elements), the meaning behind them are different.

State Mode Description Visual
is-focused Focus mode Current highlight opacity: 1
is-selected Navigation mode Current item border
is-multi-selected Multi-select mode Selected item border
is-highlighted Edit mode Toolbar interaction (New) border

With that being said, I think keeping them separate is okay for now.
(Hopefully the table breakdown above helps!)

@jasmussen
Copy link
Contributor

This is what I see:

show outline

This feels REALLY solid. I was about to suggest that you could reduce those 250ms to 0 for fading the outline out, but I realize it's very nice that the outline fades out at the same time as the mover control collapses.

Overall this feels like a substantial improvement purely on behavior, and I'd love for us to ship this as fast as we can. In that vein, the only remaining discussion is the matrix of states. I do wish we could collapse some of those. It seems like the focus state might be leveraged with the "isolation" interface that FSE might enter when editing a template part. I also wonder if we can merge is-selected with is-multi-selected, with the toolbar differentiation happening elsewhere. But this is sort of high level and separate.

If there isn't realy a way to attach the highlight without this extra state class, I'm personally fine with it.

Nice work Q!

@ItsJonQ
Copy link
Author

ItsJonQ commented Mar 18, 2020

This feels REALLY solid.

@jasmussen Thank you!!

very nice that the outline fades out at the same time as the mover control collapses.

Awesome! Is there anything you think we need to adjust?

If there isn't really a way to attach the highlight without this extra state class

Assuming all the various state className are purely for styles and have no semantic meaning (or things depending on them)... we could do something like this:

Screen Shot 2020-03-18 at 9 51 18 AM

Where a new singular is-outlined className is triggered by any of these conditions.

I think it would be out of scope for this PR. Just sharing for consideration :)

@jasmussen
Copy link
Contributor

I generally like the sound of that! Agreed probably out of scope for this one, but a good thought.

Copy link
Member

@mtias mtias left a comment

Choose a reason for hiding this comment

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

Works great for me now. Just left a couple comments on naming of functions that could be improved.

*
* @return {boolean} Whether the block is currently highlighted.
*/
export function getIsBlockHighlighted( state, clientId ) {
Copy link
Member

Choose a reason for hiding this comment

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

This should be named just isBlockHighlighted for consistency.

*
* @return {string} Updated state.
*/
export function blockHighlighted( state = '', action ) {
Copy link
Member

Choose a reason for hiding this comment

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

This one read a bit odd to me — perhaps highlightedBlock is better?

@mtias
Copy link
Member

mtias commented Mar 18, 2020

@ItsJonQ actually I'm seeing one small thing — after moving a block, the highlighted state seems stuck. Initially it makes sense because focus moves to the movers, but if I focus on another element of the toolbar it still doesn't go away.

image

@ItsJonQ
Copy link
Author

ItsJonQ commented Mar 18, 2020

@mtias Awesome! Thank you for that feedback! I'll work on refining that as soon as I can 💪

@ItsJonQ
Copy link
Author

ItsJonQ commented Mar 18, 2020

Why is Travis so mad at these updates 😭

@aduth
Copy link
Member

aduth commented Mar 18, 2020

Couple quick questions:

  • Could this not leverage the existing "is typing" flag, rather than specifically a separate new interaction of typing vs. not typing vs. interacting with the block toolbar?
  • Is a toolbar rendered within the context of a block such that we don't need a separate global state, but rather that we could share some state between the block and the toolbar to know that the block itself should be highlighted?

@ItsJonQ
Copy link
Author

ItsJonQ commented Mar 18, 2020

@aduth Thank you for replying 🙏

Could this not leverage the existing "is typing" flag...

I don't think so, as it only engages when interacting specifically with these 2 elements:

Screen Shot 2020-03-18 at 15 39 35

Can you confirm this behavior should apply when using "Top Toolbar" mode as well?

Yes. I believe it should.

Is a toolbar rendered within the context of a block such that we don't need a separate global state

Ah! If there's a way to do that (that you know of), that would be lovely.
I don't necessarily think global state would be needed for this interaction.
I just wasn't sure if there was something else I could hook into.

@aduth
Copy link
Member

aduth commented Mar 18, 2020

Could this not leverage the existing "is typing" flag...

I don't think so, as it only engages when interacting specifically with these 2 elements:

Ah, I guess then the question is more of a "should it", but I trust the judgment of y'all here.

Is a toolbar rendered within the context of a block such that we don't need a separate global state

Ah! If there's a way to do that (that you know of), that would be lovely.
I don't necessarily think global state would be needed for this interaction.
I just wasn't sure if there was something else I could hook into.

I had looked briefly at the code. It may be pretty straight-forward with the contextual form of the block toolbar, I'm not quite as sure with the "top toolbar" variant of it (which is rendered in the toolbar, and not within the block itself).

@aduth
Copy link
Member

aduth commented Mar 18, 2020

Specifically, there is a context object which is provided by a block so that descendent components can make use of certain pieces relevant to the block's own state:

https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/block-edit/context.js

@aduth
Copy link
Member

aduth commented Mar 18, 2020

Thanks @mtias and @ItsJonQ . Where I may be getting caught up is that in many blocks, many of its toolbar buttons also apply to the block as a whole (list style, column vertical alignment, heading level, paragraph alignment). It wasn't previously clear to me that the mover and block type button were "linked" already, so at least I see the consistency in this approach.

@ItsJonQ
Copy link
Author

ItsJonQ commented Mar 19, 2020

@aduth Thank you for your suggestions on the TOGGLE_BLOCK_HIGHLIGHT action/reducer! I just pushed an update with your proposed changes.

(My example could perhaps be expressed more elegantly)

I think you expressed it just fine 😊

Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

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

I left one comment worth considering. Otherwise, this is looking in good shape 👍

}
},
[ showMovers ]
[ handleOnChange, showMovers ]
Copy link
Member

Choose a reason for hiding this comment

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

handleOnChange will change every render (it's created in scope as a new function). Are we really benefiting from memoizing the callback in that case?

Additionally, we include showMovers as a dependency (presumably because it's an implicit dependency via handleONChange, but not onChange, which is likewise an implicit dependency.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for your feedback!

In this case, the state of showMovers works as a guard for both setShowMovers and onChange. We only want to make the state change if it's different.

Re: handleOnChange. Are you suggesting something like this?

const handleOnChange = useCallback(( nextIsFocused ) => {
    setShowMovers( nextIsFocused );
    onChange( nextIsFocused );
}, [ onChange ])

And with this, I think we can remove it from being a dependency from the debounced Show/Hide functions

🤔

Copy link
Member

@aduth aduth Mar 20, 2020

Choose a reason for hiding this comment

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

Probably more the opposite, to instead remove useCallback from debouncedShowMovers. Over time, I've come to see useCallback as something of a code smell.

i.e.

function debouncedShowMovers( event ) {
	if ( event ) {
		event.stopPropagation();
	}

	const timeout = timeoutRef.current;

	if ( timeout && clearTimeout ) {
		clearTimeout( timeout );
	}
	if ( ! showMovers ) {
		handleOnChange( true );
	}
}

Would you see a problem with this alternative? For what reason do we want useCallback ?

Copy link
Member

@aduth aduth Mar 20, 2020

Choose a reason for hiding this comment

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

In this case, the state of showMovers works as a guard for both setShowMovers and onChange. We only want to make the state change if it's different.

But it's only incidentally accounting for changes in onChange on account for the fact that handleOnChange is a new function each render, which counteracts any benefits we might otherwise get from using useCallback. In other words, debouncedShowMovers will also be a new reference on every render in the current implementation, and would therefore be effectively no different than if we don't use useCallback at all.

Copy link
Author

Choose a reason for hiding this comment

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

Would you see a problem with this alternative? For what reason do we want useCallback ?

No problems 😊 . I was using useCallback as that was something I've seen in many places in our codebase, and it felt like a good idea to continue. But like you've suggested, it may be pre-optimizing, and perhaps causing more trouble and it's worth.

Copy link
Member

Choose a reason for hiding this comment

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

The way I've understood it is that useCallback is useful if and only if there's an important reason to avoid changing references, and that in all other cases it's not only premature, but often yields worse performance.

Related: https://kentcdodds.com/blog/usememo-and-usecallback

Copy link
Author

Choose a reason for hiding this comment

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

KCD saves the day!

},
[ isFocused ]
);
timeoutRef.current = setTimeout( () => {
Copy link
Member

Choose a reason for hiding this comment

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

It existed previously, but: Is there any chance that this function is called twice before the callback is invoked? Because it could cause a problem where the timeoutRef.current is assigned to whichever setTimeout was scheduled last, and only the last one would be unscheduled in the useEffect unsubscribe callback (i.e. the first one would still trigger its callback even after the component unmounts, causing a warning to be logged if it calls to set state).

Copy link
Author

Choose a reason for hiding this comment

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

Oh! Hmm... Perhaps we should clear the timeout before setting it in this debounce function.

Comment on lines +168 to +177
// Sequences state change to enable editor updates (e.g. cursor
// position) to render correctly.
requestAnimationFrame( () => {
Copy link
Member

Choose a reason for hiding this comment

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

I don't have any other suggestions, but this code gives me vibes that it could be fragile (race conditions).

(requestAnimationFrame and setTimeout are often misused to address issues where we should be acting as a side effect of some specific state transitions, and only be lucky circumstance can the same effect be achieved with scheduled callbacks)

Copy link
Author

Choose a reason for hiding this comment

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

This was my only work around :(. I'm not comfortable with the fragile vibes either. There's something happening (elsewhere in the editor) that does something with focus (or perhaps caret position). Without rAF the interaction breaks.

I realized this after the E2E tests kept failing (earlier)

Open to suggestions! 🙏

Copy link
Contributor

Choose a reason for hiding this comment

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

I personally wonder if we need this unmount behavior at all. It could be just a matter of including a reducer logic where the flag should be set to false when we start typing or when the block is unselected. (Unless I'm not understanding the purpose of this hook)

Copy link
Contributor

Choose a reason for hiding this comment

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

I merged but if we find a better solution, let's follow-up on it.

@ItsJonQ ItsJonQ force-pushed the try/toolbar-outline-block-on-focus branch from b5f7d33 to 4194cd2 Compare March 23, 2020 14:08
@ItsJonQ
Copy link
Author

ItsJonQ commented Mar 23, 2020

Rebased with latest master

@youknowriad youknowriad merged commit 3bbd057 into master Mar 23, 2020
@youknowriad youknowriad deleted the try/toolbar-outline-block-on-focus branch March 23, 2020 15:38
@github-actions github-actions bot added this to the Gutenberg 7.8 milestone Mar 23, 2020
@mtias
Copy link
Member

mtias commented Mar 23, 2020

Thanks for getting this one in, it adds a very meaningful detail to the experience.

@ItsJonQ
Copy link
Author

ItsJonQ commented Mar 23, 2020

Thank you all! 🙏 !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Blocks Overall functionality of blocks [Feature] Writing Flow Block selection, navigation, splitting, merging, deletion... [Type] Enhancement A suggestion for improvement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Show block outline when focusing / hovering the block type
5 participants