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

Move navigation and selection logic to WritingFlow #19397

Merged
merged 16 commits into from
Jan 8, 2020
Merged

Conversation

ellatrix
Copy link
Member

@ellatrix ellatrix commented Jan 3, 2020

Description

This PR moves selection and navigation mode logic (event listeners) to WritingFlow.

  • This removes a lot of event listeners that are currently added to each block.
  • The functions are recreated less often since WritingFlow is rerendered less often. These functions don't depend on any block data, only on selection state.
  • This removes the need for the IgnoreNestedEvents component.
  • Creates a RootContainer component where I moved some common block logic and useMultiSelection (which is now only rendering once).
  • It simplifies the BlockListBlock component. The absence of the event handlers will also make it easier later on to remove the div wrappers.
  • We may want to refactor WritingFlow a little to separate navigation and selection logic.

How has this been tested?

Screenshots

Types of changes

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

@ellatrix ellatrix added the [Status] In Progress Tracking issues with work in progress label Jan 3, 2020
@ellatrix ellatrix added the [Type] Performance Related to performance efforts label Jan 3, 2020
@@ -469,18 +401,15 @@ function BlockListBlock( {
);

return (
<IgnoreNestedEvents
Copy link
Member Author

Choose a reason for hiding this comment

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

@aduth Might be of interest to you as you originally created this component.

@ellatrix ellatrix requested a review from aduth January 3, 2020 13:04
@ellatrix ellatrix added [Type] Code Quality Issues or PRs that relate to code quality and removed [Status] In Progress Tracking issues with work in progress labels Jan 3, 2020
@ellatrix ellatrix force-pushed the try/move-nav-logic branch from 7e7faf5 to 96fd5fb Compare January 3, 2020 13:34
@ellatrix ellatrix changed the title [WIP] Move navigation and selection logic to WritingFlow Move navigation and selection logic to WritingFlow Jan 3, 2020
@youknowriad
Copy link
Contributor

Did you test the performance impact of this PR. It seems like it has potential for improving the numbers.

@ellatrix
Copy link
Member Author

ellatrix commented Jan 3, 2020

@youknowriad I did a quick check and I think it does improve slightly, but I have to say that, on my computer, the number vary even when I test the master branch a few times in a row. 🤷‍♀️

@aduth
Copy link
Member

aduth commented Jan 3, 2020

Related (would make redundant): #14707

@aduth
Copy link
Member

aduth commented Jan 3, 2020

One thing this has me wondering is whether we need to start considering to bake in WritingFlow as part of the default rendering of a BlockEditor. Right now, it's a separate, optional component. But for a naive rendering of a block editor, the changes here would have the effect that a lot of the interactivity is lost.

I guess we document this as more-or-less assumed to be part of a standard rendering hierarchy, so the expectation is already there. Furthermore, we rely on this in the top-level post editor to be able to include the post title as part of the consideration of the writing flow.

@aduth
Copy link
Member

aduth commented Jan 3, 2020

One thing this appears to regress on is that clicking the button block appender in a column block will first select that block before allowing the inserter to be shown. This has been a source of some trouble in the past (e.g. #18329), where the intended behavior is that clicking the appender button should immediately open the inserter.

Steps to reproduce:

  1. Navigate to Posts > Add New
  2. Insert a Columns block
  3. Select any columns arrangement
  4. Unselect the Columns block
  5. Click one of the appender buttons in any of the columns

Expected: The inserter is opened.
Actual: The block is selected. A second click is required to open the inserter.

Related: #19135, #18379

There are end-to-end in both of those pull requests. I think it might be that the navigation block works correctly (does appear to in my testing, not sure why it's any different). The tests modified in #19135 are primarily to test against regressions to the expected behavior described in #18928. It might be that we lack a test case specifically verifying this columns (button) appender behavior.

@aduth
Copy link
Member

aduth commented Jan 3, 2020

I generally like the direction this is taking, and am especially fond of the idea of removing the complexities surrounding IgnoreNestedEvents in favor of handling block selection events further up the virtual tree. One worry I might have is whether WritingFlow is starting to do too much, at least in being able to succinctly define its purpose. It's conflicting to me because I do think some of the refactoring could make sense in WritingFlow (e.g. multi-selection, navigation mode toggling). I'm not sure of it all, though, such as the focus handling, at least if we consider WritingFlow as having primarily served the purpose of handling keyboard interactions with respect to their implications on block selection.

Maybe this is a matter of my own unclear expectations around what this component is meant to be responsible for handling. I think it's worth clarifying in any case (ideally, though not necessarily as part of this pull request, as documentation for the component).

Alternatively, it's something where we could consider splitting off some of the specific functionality into separate components. This could be relevant to my previous point about "default" behaviors, where if we don't want to bake-in all of WritingFlow, we could still split out some of the functionality to become default. In my head, I'm thinking of components like MultiSelectScrollIntoView, which itself is somewhat related to the functionality of WritingFlow, but isolated for the purpose of improving clarity around what it's expected to be handling.

@aduth
Copy link
Member

aduth commented Jan 3, 2020

On the topic of #19397 (comment) and default behaviors, I wonder if this might have any impact on whether we will be able to implement nested block editors (e.g. #14715, I think also Widgets screen might have some nested editors?). My worry is: How does selection work for those inner editors, if it's meant to be handled by WritingFlow? Does it mean there would need to be multiple WritingFlow, one for each of the BlockEditorProviders?

@ellatrix
Copy link
Member Author

ellatrix commented Jan 3, 2020

I think it might be good to split WritingFlow in multiple components, e.g. one for multi-selection, one for navigation, and one for focus handling. Currently WritingFlow already does some focus handling for moving it in and out the block.

I also like baking WritingFlow, or some parts of it, in the block editor.

I'm sot sure how nested editors would work, and I'm sure we'd have to revise the currently selection logic and WritingFlow as well. Should you be able to select and navigate across editors? If yes, then there should probably be one WritingFlow component wrapping everything.

@ellatrix
Copy link
Member Author

ellatrix commented Jan 3, 2020

I'll fix the remaining issue with the column inserter and attempt to split WritingFlow in parts.

@aduth
Copy link
Member

aduth commented Jan 3, 2020

Should you be able to select and navigate across editors? If yes, then there should probably be one WritingFlow component wrapping everything.

I don't think it's necessarily a requirement, especially considering that if each editor is responsible for maintaining its own blocks state, I don't know how a top-level WritingFlow could possibly track the selection for those blocks (since, at that level, the blocks state is not known).

@ellatrix
Copy link
Member Author

ellatrix commented Jan 4, 2020

I've now moved the focus handling to the root BlockList so it is backed in. The other stuff like navigation mode and multi selection logic do make sense in WritingFlow so I left it there.

@@ -72,18 +71,17 @@ function BlockList( {
hasMultiSelection,
enableAnimation,
} = useSelect( selector, [ rootClientId ] );
const ref = useRef();
const onSelectionStart = useMultiSelection( { ref, rootClientId } );
Copy link
Member Author

Choose a reason for hiding this comment

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

Notice that this is no longer rendered for every block list, but rather only for the root block list.

} = useDispatch( 'core/block-editor' );

function onMouseDown( event ) {
Copy link
Member Author

Choose a reason for hiding this comment

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

The logic below belongs here in my opinion, as this component handles multi selection as well as navigation mode already. Perhaps we can split this is the future, but a lot of it seems tied together.

tabIndex={ -1 }
// Prevent the block from being selected when the appender is
// clicked.
onFocus={ stopPropagation }
Copy link
Member Author

Choose a reason for hiding this comment

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

@aduth This fixed the issue you found (#19397 (comment)). Does everything now work correctly?

Copy link
Member

Choose a reason for hiding this comment

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

Initial impression: I imagine this could work, yes, considering that IgnoreNestedEvents was in many ways a glorified stopPropagation anyways. Will test as part of a broader re-review.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks! It would be nice the eventually get rid of this as well. I'll look into it sometime separately.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks! It would be nice the eventually get rid of this as well. I'll look into it sometime separately.

Yeah, that's fair. I'm very reluctant about any stopPropagation, but I think it's fair to say it's not really "introduced" here, since it was more-or-less the same effect previously.

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, same here. At least it's now explicit that propagation is stopped here. IgnoreNestedEvents kind of made it look like it's more ok.

Copy link
Member

Choose a reason for hiding this comment

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

Right, same here. At least it's now explicit that propagation is stopped here. IgnoreNestedEvents kind of made it look like it's more ok.

It's a give and take, I think. One nice thing about IgnoreNestedEvents is that it wouldn't stop all propagation, only that which it expressly cares about, thus potentially avoiding issues with other (intended) event handling further up the element hierarchy.

@ellatrix ellatrix force-pushed the try/move-nav-logic branch from 47a414f to 6c6acc0 Compare January 6, 2020 14:58
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 ran through a number of scenarios relating to the affected code. There's a lot of potential impact here, but as best I can tell, it's all in good shape.

While I'm approving, I do think at the very least we should resolve the issue with Node.ELEMENT_NODE vs. node.ELEMENT_NODE.

It's a little concerning to me how much we rely on the DOM as the source of truth here with the introduction and reliance on getBlockClientId, but I also don't think it contradicts how we've approached WritingFlow to date, and it might be worth the compromise for all the benefits/simplification it brings.

hasMultiSelection,
} = useSelect( selector, [] );
const { selectBlock } = useDispatch( 'core/block-editor' );
const onSelectionStart = useMultiSelection( ref );
Copy link
Member

Choose a reason for hiding this comment

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

(Thinking out loud, since it seems fine, but worth confirming) I suppose that because useMultiSelection returns a value via useCallback (with memoization) that this shouldn't be a performance concern in changing values in the provided value below (reference)?

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, it's memoized with useCallback, so it should be fine.

getBlockParents,
};
}
function selector( select ) {
Copy link
Member

Choose a reason for hiding this comment

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

Aside: While the separate function declaration still catches me off guard, one thing which is nice here is demonstrating how, if you are able to move it outside the component declaration altogether, there's a safer guarantee that you don't need to provide any dependencies to the useSelect call (since there's no longer the component scope from which to inherit dependencies).

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree. It's maybe a bit strange to put it outside the component, but why not if you can prevent creating an additional function on every render.

}
// Allow user to escape out of a multi-selection to a singular
// selection of a block via click. This is handled here since
// onFocus excludes blocks involved in a multiselection, as
Copy link
Member

Choose a reason for hiding this comment

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

Nit: This was copied from BlockListBlock, but since "onFocus" doesn't exist in this file, it's a bit confusing to read now.

packages/block-editor/src/utils/dom.js Outdated Show resolved Hide resolved
* @return {?string} Client ID or undefined if the node is not part of a block.
*/
export function getBlockClientId( node ) {
if ( node.nodeType !== node.ELEMENT_NODE ) {
Copy link
Member

Choose a reason for hiding this comment

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

Unless I'm mistaken, this should be:

Suggested change
if ( node.nodeType !== node.ELEMENT_NODE ) {
if ( node.nodeType !== Node.ELEMENT_NODE ) {

https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType#Constants

Currently, I believe it will always be triggering the condition, since node.ELEMENT_NODE would evaluate to undefined.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this is correct. Every node instance has these properties.

Copy link
Member

@aduth aduth Jan 7, 2020

Choose a reason for hiding this comment

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

I think this is correct. Every node instance has these properties.

Ah, interesting. I did not know this.

(For posterity's sake: https://dom.spec.whatwg.org/#interface-node)

Comment on lines +474 to +475
// Only allow selection to be started from a selected block.
onMouseLeave={ isSelected ? onMouseLeave : undefined }
Copy link
Member

Choose a reason for hiding this comment

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

Is this related to multi-selection? And if it is, would any of it be expected to be handled in WritingFlow instead? Considering you mention it at https://github.com/WordPress/gutenberg/pull/19397/files#r363036970 as being partly responsible for handling multi-selection.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's right, this is for multi selection. It would be nice to move this to WritingFlow as well. I didn't do it here to avoid a huge PR, as it seems a little more tricky to solve.

Comment on lines +68 to +69
// Prevent the block from being selected when the appender is
// clicked.
Copy link
Member

Choose a reason for hiding this comment

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

I was actually surprised to find that in the latest version of the plugin / core version, the behavior of clicking on the inserter in the column block is such that it requires a second click to open. I don't think it's always been like this, since I distinctly remember trying to require only a single click. One potential difference now is that, while it does only require a single click, it doesn't actually select the block. I don't particularly see this as being problematic, and in some ways it's actually an ideal behavior (since the user's intent is to insert, not necessarily to select the block).

@ellatrix
Copy link
Member Author

ellatrix commented Jan 7, 2020

It's a little concerning to me how much we rely on the DOM as the source of truth here with the introduction and reliance on getBlockClientId, but I also don't think it contradicts how we've approached WritingFlow to date, and it might be worth the compromise for all the benefits/simplification it brings.

I agree. I have some future improvements in mind around keeping the block references higher up the tree that could eliminate having to query the DOM. Not sure if and how it will work, but I generally agree that it would be nice to avoid any queries.

@ellatrix ellatrix merged commit 4ba6ca0 into master Jan 8, 2020
@ellatrix ellatrix deleted the try/move-nav-logic branch January 8, 2020 08:55
@aduth
Copy link
Member

aduth commented Jan 8, 2020

I was actually surprised to find that in the latest version of the plugin / core version, the behavior of clicking on the inserter in the column block is such that it requires a second click to open. I don't think it's always been like this, since I distinctly remember trying to require only a single click. One potential difference now is that, while it does only require a single click, it doesn't actually select the block. I don't particularly see this as being problematic, and in some ways it's actually an ideal behavior (since the user's intent is to insert, not necessarily to select the block).

One thing I've noticed about the new behavior: When inserting a Columns block and immediately proceeding to try to adjust the widths of individual columns, it can now be more difficult to select the columns, where previously it would be enough to click the button inserter, even if it wasn't the intention to actually insert a block. Granted, "Clicking the inserter" for the purpose of selecting a block is itself a bit of an awkward expectation and probably not the desired behavior, but for lack of another option, it was at least more consistent than it is currently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Type] Code Quality Issues or PRs that relate to code quality [Type] Performance Related to performance efforts
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants