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

Navigation Component: Custom item content #25281

Merged
merged 5 commits into from
Sep 16, 2020

Conversation

psealock
Copy link
Contributor

@psealock psealock commented Sep 14, 2020

Fixes #25244

Description

Instead of rendering NavigationItem content inside a button, make it completely up to the consumer to handle contents if children are supplied. Normal navigation items and navigation links can be designated via props and will be rendered with a button. Otherwise, if a child element is supplied, render that and leave controlling it to the consumer.

This is a first stab at a solution. I think the level of control given to the consumer is up to debate. How much should the component handle? How much to be offloaded to the consumer?

From the WooCommerce perspective, custom item content will be used to pass in internal links for navigation without a page refresh: ie, a Link component tied to the page's React Router instance. There won't be a need for passing an onClick method to specify activeItem nor will that logic be required internally via setActiveItem. Instead a click will cause React Router to change locations and the top level Navigation will be passed the activeItem prop via the new url.

import { Link } from '@woocommerce/components';
import { getNewPath } from '@woocommerce/navigation';

...

<NavigationItem>
	<Link
		href={ getNewPath( '/analytics/products', {
			filter: 'single_product',
			products: product.id,
		} ) }
		type="wc-admin"
	>
		{ product.name }
	</Link>
</NavigationItem>

How has this been tested?

  1. npm run storybook:dev
  2. Ensure clicking on the WP logo correctly sets the active item.

Screenshots

Screen Shot 2020-09-14 at 2 44 34 PM

Types of changes

Changes the API of Navigation component such that NavigationItem rendering children handle all responsibility of navigation.

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.

@psealock psealock added the [Feature] Navigation Component A navigational waterfall component for hierarchy of items. label Sep 14, 2020
@github-actions
Copy link

github-actions bot commented Sep 14, 2020

Size Change: +97 B (0%)

Total Size: 1.2 MB

Filename Size Change
build/block-editor/index.js 128 kB +35 B (0%)
build/block-library/editor-rtl.css 8.69 kB +17 B (0%)
build/block-library/editor.css 8.69 kB +20 B (0%)
build/block-library/index.js 139 kB +26 B (0%)
build/block-library/style-rtl.css 7.6 kB +3 B (0%)
build/block-library/style.css 7.59 kB +1 B
build/components/index.js 201 kB -6 B (0%)
build/core-data/index.js 12.2 kB -9 B (0%)
build/data-controls/index.js 1.28 kB -8 B (0%)
build/edit-site/index.js 19 kB +20 B (0%)
build/editor/index.js 45.3 kB -2 B (0%)
ℹ️ View Unchanged
Filename Size Change
build/a11y/index.js 1.14 kB 0 B
build/annotations/index.js 3.67 kB 0 B
build/api-fetch/index.js 3.41 kB 0 B
build/autop/index.js 2.82 kB 0 B
build/blob/index.js 620 B 0 B
build/block-directory/index.js 8.53 kB 0 B
build/block-directory/style-rtl.css 943 B 0 B
build/block-directory/style.css 942 B 0 B
build/block-editor/style-rtl.css 11 kB 0 B
build/block-editor/style.css 11 kB 0 B
build/block-library/theme-rtl.css 741 B 0 B
build/block-library/theme.css 741 B 0 B
build/block-serialization-default-parser/index.js 1.88 kB 0 B
build/block-serialization-spec-parser/index.js 3.1 kB 0 B
build/blocks/index.js 47.8 kB 0 B
build/components/style-rtl.css 15.5 kB 0 B
build/components/style.css 15.4 kB 0 B
build/compose/index.js 9.68 kB 0 B
build/data/index.js 8.55 kB 0 B
build/date/index.js 31.9 kB 0 B
build/deprecated/index.js 772 B 0 B
build/dom-ready/index.js 568 B 0 B
build/dom/index.js 4.48 kB 0 B
build/edit-navigation/index.js 10.7 kB 0 B
build/edit-navigation/style-rtl.css 868 B 0 B
build/edit-navigation/style.css 871 B 0 B
build/edit-post/index.js 305 kB 0 B
build/edit-post/style-rtl.css 6.24 kB 0 B
build/edit-post/style.css 6.22 kB 0 B
build/edit-site/style-rtl.css 3.13 kB 0 B
build/edit-site/style.css 3.13 kB 0 B
build/edit-widgets/index.js 12.2 kB 0 B
build/edit-widgets/style-rtl.css 2.55 kB 0 B
build/edit-widgets/style.css 2.55 kB 0 B
build/editor/editor-styles-rtl.css 492 B 0 B
build/editor/editor-styles.css 493 B 0 B
build/editor/style-rtl.css 3.8 kB 0 B
build/editor/style.css 3.8 kB 0 B
build/element/index.js 4.65 kB 0 B
build/escape-html/index.js 733 B 0 B
build/format-library/index.js 7.71 kB 0 B
build/format-library/style-rtl.css 547 B 0 B
build/format-library/style.css 548 B 0 B
build/hooks/index.js 2.13 kB 0 B
build/html-entities/index.js 621 B 0 B
build/i18n/index.js 3.56 kB 0 B
build/is-shallow-equal/index.js 711 B 0 B
build/keyboard-shortcuts/index.js 2.52 kB 0 B
build/keycodes/index.js 1.94 kB 0 B
build/list-reusable-blocks/index.js 3.12 kB 0 B
build/list-reusable-blocks/style-rtl.css 476 B 0 B
build/list-reusable-blocks/style.css 476 B 0 B
build/media-utils/index.js 5.31 kB 0 B
build/notices/index.js 1.79 kB 0 B
build/nux/index.js 3.4 kB 0 B
build/nux/style-rtl.css 671 B 0 B
build/nux/style.css 668 B 0 B
build/plugins/index.js 2.56 kB 0 B
build/primitives/index.js 1.41 kB 0 B
build/priority-queue/index.js 789 B 0 B
build/redux-routine/index.js 2.85 kB 0 B
build/rich-text/index.js 13.9 kB 0 B
build/server-side-render/index.js 2.77 kB 0 B
build/shortcode/index.js 1.69 kB 0 B
build/token-list/index.js 1.27 kB 0 B
build/url/index.js 4.06 kB 0 B
build/viewport/index.js 1.85 kB 0 B
build/warning/index.js 1.14 kB 0 B
build/wordcount/index.js 1.17 kB 0 B

compressed-size-action

</Button>
{ navigateToMenu && <Icon icon={ chevronRight } /> }
</Button>
) }
Copy link
Contributor

Choose a reason for hiding this comment

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

This approach has one caveat: custom components would need the whole external state to handle internal navigation/item activation (see the storybook change).

I'd love to keep the component as self-contained as possible by default, leaving the external state as an optional feature, only used if strictly needed.

What if we did some kind of hybrid, and wrap custom components in a div with just the onClick handler?
E.g.

{ children && <div onClick={ onItemClick }>{ children }</div> }
{ ! children && <Button /> }

This would have some pros...

  • Clicking the item would activate it without the need of an external state.
  • Consumers could define an onClick for NavigationItem if needed, and a different one for the custom component.
  • navigateToMenu would work as expected.

and cons...

  • There would be no way to prevent setActiveItem on click — unless defining the href prop... Maybe we could have another prop to optionally disable the activation behaviour? (Sounds overkill 🤔 )
  • navigateToMenu on custom components would not render the chevron icon. Not a big deal, but it's an inconsistency.

Copy link
Contributor

Choose a reason for hiding this comment

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

This approach has one caveat: custom components would need the whole external state to handle internal navigation/item activation (see the storybook change).

I do agree that this is a problem and for the WC nav we're building it makes things pretty complicated. How do we check and control the active state when we have menu items all with various logic to determine active status:

  • Traditional wp links
  • WooCommerce Admin links
  • Other router-based links
  • Third party plugin links
  • External links

Clicking the item would activate it without the need of an external state.

I like the simplicity of this and it works for most cases. One small issue is that clicking does not necessarily guarantee that an item will become active. One more common case for this might be external links.

I do have an open PR that uses the current route as a single source of truth for determining active state (would need a rebase if we went that direction). #24985

Pros

  • Single source of truth for active state.
  • External components can be used without click handlers.
  • Works across any type of router.

Cons

  • Far more opinionated on what is deemed active and requires a route change (or at least a hash change) to trigger changes to the active state of items.
  • Lots of testing needed to confirm this works for all use cases.

Copy link
Contributor Author

@psealock psealock Sep 15, 2020

Choose a reason for hiding this comment

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

Thanks for the feedback @Copons!

What if we did some kind of hybrid, and wrap custom components in a div with just the onClick handler?
E.g.

The linter will complain about a div with an onClick because it will need role="button" applied as well, which sort of defeats the purpose because semantically an onClick should only be applied to buttons.

I suppose if you have setActiveItem called internally as well as updated from top level props via a change in the url you'd just get an unnecessary render. However, this seems like an indication that something isn't right in the underlying code.

Thanks also @joshuatf

One small issue is that clicking does not necessarily guarantee that an item will become active. One more common case for this might be external links.

In the case of external links, no navigation actually occurs in the originating page, so I think this is the expected behaviour.

I do have an open PR that uses the current route as a single source of truth for determining active state

This logic is going to be needed by all applications using the Nav component, so I do see the value in bring that in-house. I'm hesitant to advocate for this because it goes beyond the responsibility of the Nav component, which is to render links in a hierarchy. I suppose it's a question of whose responsibility this is. My opinion is why not make a dumb component now and look into adding this later?

Copy link
Contributor

Choose a reason for hiding this comment

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

In the case of external links, no navigation actually occurs in the originating page, so I think this is the expected behaviour.

In the example above with an internal state, the onClick would still fire and would mark this item as "active" which I would consider unexpected behavior since the originating page did not navigate.

I'm hesitant to advocate for this because it goes beyond the responsibility of the Nav component, which is to render links in a hierarchy. I suppose it's a question of whose responsibility this is.

I get this and also am a little conflicted about it, but if every consumer is using this logic, I think it should be coupled with the nav. If we see value in the route detection, but can see use cases for setting active state without it, then I agree that separation is warranted.

My opinion is why not make a dumb component now and look into adding this later?

I'd argue that this logic is necessary to make the component usable. While I like the idea of a dumb component, if every consumer is going to write their own route detection logic, why not add it sooner rather than later and make this component more readily usable?

The alternative is just allowing the isActive prop to passed to menu items and having consumers pass their own route detection to any registered items.

@psealock
Copy link
Contributor Author

I'd love to keep the component as self-contained as possible by default

I think we should revisit this requirement. Let's look at WooCommerce's needs for this component, as pointed out above by @joshuatf :

Traditional wp links
WooCommerce Admin links
Other router-based links
Third party plugin links
External links

These are all links. As such, every single variation will render an <a /> tag. These represent navigational experiences that affect the location of the entire page via one mechanism or another. There is no need for a side affect, other than perhaps an onClick for the purposes of recording a Tracks event. By keeping an internal state, we potentially introduce multiple sources of truth.

On the other hand, lets examine the use case for FSE, #23939. It appears that every click on a side bar item also represents a navigation. Even the custom components that render a preview on hover will ultimately be wrapped in an anchor tag as they too will represent some kind of url navigation.

editor sidebar

Keeping an internal state such that the Nav component works "out of the box" is catering to a solution without a use case. Even the more ambitious uses, such as the entire wp-admin sidebar is also merely a collection of links. Now, with the question at hand in #25281 (comment), we are contemplating complicating the API or even bringing the entire state in-house for the sake of a use case that has not yet materialized.

What if we freed ourselves from an internal activeItem state?

  • Links become the basis of the experience.
  • We can handle href props on the developer's behalf.
  • NavigationItem children become opaque sources of logic free from side affects.

@psealock psealock force-pushed the add/Nav-component-item-content branch from 4c00203 to 2c3e9c8 Compare September 15, 2020 04:44
@psealock
Copy link
Contributor Author

fb6cbe7 changes the story to illustrate an example with a Link as custom component. I don't think we need to rip out the internal state, but its become apparent to me that we've been thinking about this Nav component in terms of buttons because we've been developing it in Storybook instead of a real application. As a result, how to handle the variety of links has been an after thought instead of a primary focus.

@Copons
Copy link
Contributor

Copons commented Sep 15, 2020

Keeping an internal state such that the Nav component works "out of the box" is catering to a solution without a use case.

This makes total sense, developing in the Storybook really went to my head!

I'll recap a little so that we are all on the same page.

  • Navigation renders a hierarchicaly list of items: not just links, and not even just "navigation items" but they could be any control really. In a way, the block sidebar could be a Navigation as well.

  • The activeItem state is a concern of the consumer, as it's tied to an external event: navigate to a page, change which template is used in the editor. Basically, if NavigationItem activates something outside the sidebar, it should be displayed as active as well.
    If the NavigationItem is not supposed to navigate/activate stuff, but it's just a control, it doesn't need the active state.

  • The activeMenu on the other hand is a concern of the component.

This said, I think there's just no reason to keep the activeItem/setActiveItem logic in the component. 🤔
We can simply pass activeItem as a prop, and add it to NavigationContext to be consumed by the items.
For items with href, the consumer will just match the current URL with the activeItem.
For other items that change stuff in the same page, consumers will be responsible to onClick={ () => setActiveItem( 'item' ) }.

Am I way off base or am I slowly getting this right? 😅

@joshuatf
Copy link
Contributor

Am I way off base or am I slowly getting this right? 😅

@Copons I believe you've got it; looks correct on all bullet points.

This said, I think there's just no reason to keep the activeItem/setActiveItem logic in the component. 🤔

Agreed.

We can simply pass activeItem as a prop, and add it to NavigationContext to be consumed by the items.

👍

For items with href, the consumer will just match the current URL with the activeItem.

I'm not sure I love the idea of every consumer rewriting the same logic to check the routes against URLs, but yes that would work and on the upside it keeps this component very "dumb."

@psealock psealock force-pushed the add/Nav-component-item-content branch from c003a14 to 13038b9 Compare September 16, 2020 00:52
@psealock
Copy link
Contributor Author

Thanks for the feedback team.

Update

See 13038b9

  • Internal references to activeItem come either directly from the top level prop or the navigation context, which is also populated by the top level prop.
  • The consumer is now fully responsible for keeping track of the activeItem.
  • Stories example reflects the use of content items to supply links of various types as well as custom content.
  • A fully internal NavigationItem is still available, but an onClick must be supplied to update the active item.

Copy link
Contributor

@Copons Copons left a comment

Choose a reason for hiding this comment

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

LGTM!

Just FYI I've pushed a commit that removes onActivateItem from the readme, but since it doesn't change the actual code, I'll approve this anyway! 🙂

Thank y'all so much for bearing with me on this.
For as much as I liked conceptually to keep the whole thing self-controlled, it just didn't make sense in real life, and even worse, it could get in the way of various use cases.

@psealock psealock merged commit 075df57 into master Sep 16, 2020
@psealock psealock deleted the add/Nav-component-item-content branch September 16, 2020 19:06
@github-actions github-actions bot added this to the Gutenberg 9.1 milestone Sep 16, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Navigation Component A navigational waterfall component for hierarchy of items.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Navigation Component: Custom Item Content
3 participants