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: Merge feature branch #24920

Closed
wants to merge 13 commits into from
Closed

Conversation

psealock
Copy link
Contributor

@psealock psealock commented Aug 31, 2020

Description

Expose a <Navigation /> component from @wordpress/components to render an array of menu items in a hierarchal fashion. The component is highly composable, accepting a child function that allows full control to the consuming application. It is also free from excessive styling so it can be adapted to many UIs.

This component will be useful in the Full Site Editing Sidebar project and WooCommerce's Navigation project.

How has this been tested?

This has been tested using Storybook and unit tests. In order to test the component, run the following:

npm run storybook:dev

Next, go to Components > Navigation and see the component in use.

Screenshots

Screen Shot 2020-08-31 at 3 33 57 PM

Screen Shot 2020-08-31 at 3 34 05 PM

Types of changes

New feature: This PR introduces a new component.

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 and others added 9 commits August 31, 2020 13:19
* Initial working tree display

* add styles

* add styles

* dark theme

* back button

* secondary

* BEM style classnames

* remove back button styles

* add menu: 'primary' data propery

* get even more working

* remove extra styles

* add README

* manifest
* Break nav components into smaller pieces

* Get visible menu items based on menu id and active state

* Allow null and root text in back button

* Allow root text for navigation title

* Add line break to make use of multiple menus more obvious

* Fix back button case at root level

* Remove secondary nav styling

* Simplify component props

* Pass parsed data back to navigation children

* Move nav menu item logic into menu

* Simplify nav components to bare essentials

* Allow menu level navigation without changing active item

* Handle PR feedback
* Migrate styles to css-in-js

* Add spacing to navigation child items

* Remove opinionated styling

* Rename styling components
* Migrate styles to css-in-js

* Add spacing to navigation child items

* Add badge property and styles to navigation items

* Update UI component naming

* newLine

Co-authored-by: Paul Sealock <psealock@gmail.com>
* Allow custom link tag and props for menu items

* Let Button handle logic and add LinkComponent

* Add back in link tag const to allow sharing of props

Co-authored-by: Paul Sealock <psealock@gmail.com>
* Navigation Component: Remove setActiveLevel child function arg (#24704)

* remove setActiveLevel from public api

* change to NavigationBackButton

* return null for backButton if no parentLevel

* Navigation Component: Expose __experimentalNavigation component (#24706)

* Expose __experimentalNavigation component

* fix typo

* expose menut and menu-item as well

* Loop over navigation levels and wrap with animate

* Use map and set to organize items and levels

* Simplify level and item logic

* Remove unnecessary key prop

* Fix parent level assignment

* Add back in key prop to force animation

* Use useMemo to map item data

Co-authored-by: Paul Sealock <psealock@gmail.com>
* Update readme

* first attempt to define data

* Update packages/components/src/navigation/README.md

Co-authored-by: Joshua T Flowers <joshuatf@gmail.com>

* Update packages/components/src/navigation/README.md

Co-authored-by: Joshua T Flowers <joshuatf@gmail.com>

* better declaration

* Add onClick prop

* note parent default

* Update packages/components/src/navigation/README.md

Co-authored-by: Joshua T Flowers <joshuatf@gmail.com>

Co-authored-by: Joshua T Flowers <joshuatf@gmail.com>
* Don't trigger onClick prop when not provided

* Add class names for external styling
* Add navigation component tests

* Test cleanup

* Add link and custom component tests

* href should be undefined, not null

Co-authored-by: Paul Sealock <psealock@gmail.com>
@psealock psealock added the [Feature] Navigation Component A navigational waterfall component for hierarchy of items. label Aug 31, 2020
@github-actions
Copy link

github-actions bot commented Aug 31, 2020

Size Change: +9.63 kB (0%)

Total Size: 1.2 MB

Filename Size Change
build/a11y/index.js 1.14 kB +1 B
build/annotations/index.js 3.67 kB -1 B
build/api-fetch/index.js 3.41 kB -34 B (0%)
build/block-directory/index.js 8.5 kB +507 B (5%) 🔍
build/block-editor/index.js 128 kB +1.95 kB (1%)
build/block-editor/style-rtl.css 11.1 kB +336 B (3%)
build/block-editor/style.css 11.1 kB +334 B (3%)
build/block-library/editor-rtl.css 8.64 kB +114 B (1%)
build/block-library/editor.css 8.64 kB +113 B (1%)
build/block-library/index.js 138 kB +1.92 kB (1%)
build/block-library/style-rtl.css 7.6 kB +130 B (1%)
build/block-library/style.css 7.6 kB +127 B (1%)
build/block-library/theme-rtl.css 754 B +13 B (1%)
build/block-library/theme.css 754 B +12 B (1%)
build/blocks/index.js 47.7 kB +8 B (0%)
build/components/index.js 202 kB +2.06 kB (1%)
build/components/style-rtl.css 15.5 kB -237 B (1%)
build/components/style.css 15.5 kB -237 B (1%)
build/compose/index.js 9.68 kB +3 B (0%)
build/core-data/index.js 12.3 kB -3 B (0%)
build/data/index.js 8.56 kB +4 B (0%)
build/edit-navigation/index.js 11.7 kB +13 B (0%)
build/edit-post/index.js 305 kB +614 B (0%)
build/edit-post/style-rtl.css 6.26 kB +646 B (10%) ⚠️
build/edit-post/style.css 6.25 kB +639 B (10%) ⚠️
build/edit-site/index.js 17.1 kB +122 B (0%)
build/edit-widgets/index.js 12 kB +133 B (1%)
build/edit-widgets/style-rtl.css 2.46 kB +5 B (0%)
build/edit-widgets/style.css 2.45 kB +5 B (0%)
build/editor/index.js 45.6 kB +335 B (0%)
build/element/index.js 4.65 kB +3 B (0%)
build/format-library/index.js 7.71 kB +1 B
build/list-reusable-blocks/index.js 3.12 kB -2 B (0%)
build/media-utils/index.js 5.32 kB +2 B (0%)
build/plugins/index.js 2.56 kB -1 B
build/redux-routine/index.js 2.85 kB -3 B (0%)
build/rich-text/index.js 13.9 kB +6 B (0%)
build/server-side-render/index.js 2.77 kB -4 B (0%)
build/url/index.js 4.06 kB +1 B
ℹ️ View Unchanged
Filename Size Change
build/autop/index.js 2.82 kB 0 B
build/blob/index.js 620 B 0 B
build/block-directory/style-rtl.css 953 B 0 B
build/block-directory/style.css 952 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/data-controls/index.js 1.29 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/style-rtl.css 1.16 kB 0 B
build/edit-navigation/style.css 1.16 kB 0 B
build/edit-site/style-rtl.css 3.06 kB 0 B
build/edit-site/style.css 3.06 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.81 kB 0 B
build/editor/style.css 3.81 kB 0 B
build/escape-html/index.js 733 B 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 622 B 0 B
build/i18n/index.js 3.57 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/style-rtl.css 476 B 0 B
build/list-reusable-blocks/style.css 476 B 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/primitives/index.js 1.41 kB 0 B
build/priority-queue/index.js 789 B 0 B
build/shortcode/index.js 1.7 kB 0 B
build/token-list/index.js 1.27 kB 0 B
build/viewport/index.js 1.85 kB 0 B
build/warning/index.js 1.13 kB 0 B
build/wordcount/index.js 1.17 kB 0 B

compressed-size-action

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.

I've got two minor complaints and one comment, but otherwise this works as expected!

I think this component can be a solid foundation to be improved upon by integrating style and features suggested, for example, by @ItsJonQ in ItsJonQ/g2#20.

packages/components/src/navigation/stories/index.js Outdated Show resolved Hide resolved
packages/components/src/navigation/README.md Outdated Show resolved Hide resolved

export default Navigation;
export { default as NavigationMenu } from './menu';
export { default as NavigationMenuItem } from './menu-item';
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a super fan of having default and named exports together, but I'd argue this is fine since we would end up importing from the package index anyway. 🤷

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree. I'm following a pattern already in place though:

export { default as TreeGridRow } from './row';
export { default as TreeGridCell } from './cell';
export { default as TreeGridItem } from './item';

Copy link
Contributor

Choose a reason for hiding this comment

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

Ha! I've tried opening a bunch of random index.js files and never found the same pattern, but I suspected it was there somewhere. 😄

@ItsJonQ
Copy link

ItsJonQ commented Aug 31, 2020

Thanks for your continued efforts on all! I checked out the code and I poked around with the Storybook example.

It looks like the (infinite?) nesting feature is now working :D, which is great! The menu items are tabbable and can be "clicked" with keyboard presses, which is nice.

A couple of observations from my end:

Needs to render more than just links

One of FSE's sidebar designs features a UI where the elements that appear within the menu are not just links. Instead it's a searchable list (of pages). This particular view is a nested one (maybe level 2 or 3?)

At the moment, it appears that nested level renderings are handled via id properties (id and parent).

Below is a GIF prototyping that interaction:

It comes from a G2 Components storybook prototype:
https://g2-components.xyz/iframe.html?id=components-navigator--sidebar&viewMode=story

FSE appears to be taking advantage of the new Sidebar designs. The <Navigation /> component needs to be flexible + composable enough to (easily) render various experiences.

#23939 (comment)

Sub-Nav Rendering Lifecycles

Continuing with the note above (on making sub-nav content more flexible)... <Navigation /> should support a way for these sub-navs to have their own lifecycle rendering hooks (e.g. useEffect, useState, etc...).

For example, the FSE Template mock (above) may require fetching/loading of templates via useEffect. Perhaps loading additional templates via a "Load More" interaction, which it needs to keep track of in state (useState).

A simpler example would perhaps be async fetching of badge counts.

(Avoid) Animation on mount

The menu slides on initial mount. Either the <Animate /> component or the mechanism handling the animations within <Navigation /> needs to animate when menu item is engaged with (clicked).


Hope this feedback helps!

@Copons
Copy link
Contributor

Copons commented Aug 31, 2020

Needs to render more than just links

@ItsJonQ It is currently possible to render anything in a list with the LinkComponent prop.

Right now you can see it used with a custom Button in the storybook:

const CustomRouterLink = ( { children, onClick } ) => {
	return <Button onClick={ onClick }>{ children }</Button>;
};

// ...

const data = {
	title: 'Internal link',
	id: 'item-5',
	LinkComponent: CustomRouterLink,
};

Nothing (visual glitches notwhitstanding) stops us from rendering something else in there:

const CustomImageLink = ( { onClick } ) => {
	return ( <a onClick={ onClick }>
		<img src="page-template.jpg" />
	</a> );
}

Regarding the search.
The current implementation renders the menu title as a simple <h1>.
We can update it to be its own component that turns into a search field on click, as suggested in #23939 (comment).

Using that new "SearchTitle" component could be opt-in, so if you don't need it you can keep using <h1>.

@psealock
Copy link
Contributor Author

Thanks for the review @ItsJonQ !

Continuing with the note above (on making sub-nav content more flexible)... should support a way for these sub-navs to have their own lifecycle rendering hooks (e.g. useEffect, useState, etc...).

As noted above, there is the ability to pass in custom components, complete with their own lifecycle hooks.

For example, the FSE Template mock (above) may require fetching/loading of templates via useEffect. Perhaps loading additional templates via a "Load More" interaction, which it needs to keep track of in state (useState).

Interesting use case, thanks for noting. This would make sense for paginated data. I'm fairly sure the composibility affords this possibility. I'm wondering if a more involved Storybook exploration makes sense here. It may be a good idea for demonstrating a search input as well.

A simpler example would perhaps be async fetching of badge counts.

Excellent point. Here is an issue to track that task: #24950

(Avoid) Animation on mount

Indeed: #24951

@Copons
Copy link
Contributor

Copons commented Sep 1, 2020

Another thing that comes to mind, when thinking about customizability, is that we can extend the data items with more properties as needed.

For example, in #23939 (comment), there are two interesting proposal for the FSE navigation: groups and previews on hover.
Whereas now items have just the essential props, we might want to eventually explore additional properties that would automatically enable more complicated features, without having to "overload" the LinkComponent prop.

The simplest example could be having a type property that handles what kind of output we want from NavigationMenuItem.
Currently, it only either renders a text button, or an entirely custom component.
With a type prop, we could provide a set of pre-configured components, so that consumers won't have to figure it out by themselves, leading to inconsistent experiences.

type could even be "triggered" by other properties. E.g. having an image property with an image URL would force the menu item to be type: 'image', or something like that.

I guess what I have in mind is something similar to the block type definition, with all those supports, example, etc. properties.

@ZebulanStanphill ZebulanStanphill added the Needs Accessibility Feedback Need input from accessibility label Sep 1, 2020
@psealock
Copy link
Contributor Author

psealock commented Sep 1, 2020

Thanks for the thoughtful feedback @Copons !

Another thing that comes to mind, when thinking about customizability, is that we can extend the data items with more properties as needed.

This is the strategy employed on the WooCommerce side:

https://github.com/woocommerce/navigation/blob/3001848b8798f0f81060d2160adcb9e84692ab4d/client/navigation-container.js#L55-L60

A menuId is added to items with primary|secondary to give a sectioned experience:

Screen Shot 2020-08-28 at 11 20 59 AM

Because we opted to provide a minimalist solution, it made sense to offload that logic to the consumer. If this is a feature required by FSE as well, it could pay to investigate a solution where sections are dictated in a more formal way, such as via the data array or perhaps something else entirely.

Whereas now items have just the essential props, we might want to eventually explore additional properties that would automatically enable more complicated features, without having to "overload" the LinkComponent prop.

Interesting idea here. Although the LinkComponent prop does afford the ability to pass in something that shows a preview on hover, for example, you are right in that LinkComponent could be over used. In addition to the example you provided, a next step could be to compile use cases to identify redundant ones.

With a type prop, we could provide a set of pre-configured components, so that consumers won't have to figure it out by themselves, leading to inconsistent experiences.

Nice! This exploration would lend itself well to a follow up PR

* apply effect on activeItem change

* Update packages/components/src/navigation/stories/index.js

Co-authored-by: Joshua T Flowers <joshuatf@gmail.com>

Co-authored-by: Joshua T Flowers <joshuatf@gmail.com>
@Copons
Copy link
Contributor

Copons commented Sep 2, 2020

I've noticed a bit of a odd behaviour with e652c32. It might not be a big deal, but let me point it out anyway.

The Back button doesn't update the active item.

It's noticeable with the new "non-navigation link" which only shows up if the active item is not child-2.
Clicking it will navigate inside the sub-menu, and activate the sub-menu item child-2; the link correctly disappears.
Then, going back to the root menu, the active item remains child-2 (which is not part of the current menu), and the non-navigation link doesn't show up until we manually click/activate a menu item.

I wonder if it would make sense for the Back button to also clear the active item.

E.g.

// stories/index.js
const [ active, setActive ] = useState( 'item-1' );

return (
	<NavigationBackButton setActive={ setActive }>
		<Icon icon={ arrowLeft } />
		{ parentLevel.title }
	</NavigationBackButton>
);

// navigation/index.js
const NavigationBackButton = ( { children: backButtonChildren, setActive } ) => {
	if ( ! parentLevel ) {
		return null;
	}

	const onClick = () => {
		setActiveLevelId( parentLevel.id );
		setActive( undefined );
	};

	return (
		<Button
			className="components-navigation__back-button"
			isPrimary
			onClick={ onClick }
		>
			{ backButtonChildren }
		</Button>
	);
};

@ItsJonQ
Copy link

ItsJonQ commented Sep 2, 2020

Haii. Wanted to give a heads up. It looks like Global Styles designs is exploring the potential of having nested navigation as well, but for the right sidebar:

Screen Shot 2020-09-01 at 5 10 21 PM

Figma: https://www.figma.com/file/oEkcAyhIvPFMVEAO8EImvA/Global-Styles?node-id=421%3A4508

I did an early prototype here:
https://g2-components.xyz/iframe.html?id=examples-wip-globalstylessidebar--default&viewMode=story

Ideally, this Navigation component/system should be flexible enough to create this right sidebar/nav experience as well, since the mechanics are very similar.

@ItsJonQ
Copy link

ItsJonQ commented Sep 2, 2020

I'm wondering if a more involved Storybook exploration makes sense here. It may be a good idea for demonstrating a search input as well.

@psealock It would be awesome to see this solution tackling more complex cases 💪 .
Storybook prototypes would be a great place to test and see.

Inspiration/use-case examples may be tricky to draw from as FSE + Global Styles designs are still in flux.

* don't animate on mount

* better way using useRef

* fix

* reverse isMounted:
@psealock
Copy link
Contributor Author

psealock commented Sep 3, 2020

@Copons

Then, going back to the root menu, the active item remains child-2 (which is not part of the current menu), and the non-navigation link doesn't show up until we manually click/activate a menu item.

This is by design. Previous designs called for navigation to a set of child items (forward or backward) to set the active item as the first item, but this has been abandoned so that users can navigate the navigation without actually changing the page. Only when an item is clicked does the active item change and page navigation occurs.

Then, going back to the root menu, the active item remains child-2 (which is not part of the current menu), and the non-navigation link doesn't show up until we manually click/activate a menu item.

Right, another way to look at it is you'd be on the "Child 2" page already, so the link wouldn't be there until you navigated away.

@@ -67,6 +67,14 @@ const Navigation = ( { activeItemId, children, data, rootTitle } ) => {
}
}, [ activeItem ] );

const isMounted = useRef( false );
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this doesn't need an initial value, e.g. const isMounted = useRef(); should work just as well.

@Copons Copons self-requested a review September 3, 2020 10:53
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.

Nitpick aside, this looks and works fine to me!

I'm proposing to merge it, and proceed by iterating on it in smaller standalone PRs.
Working in feature branches will only make this much more complicated to review eventually.

We might also be more clear that this is a really experimental component.
I'm not sure if there's an established approach for this, but maybe we could add a note in the readme, or something like that. 🤔

* Update nav styles to match new core designs

* Add menu title prop and menu title styling

* Add dark styling

* Add secondary menu to nav story

* Add tests for menu titles

* Fix back button style component name
@afercia
Copy link
Contributor

afercia commented Sep 7, 2020

A quick note on the "sliding sidebars" pattern. This pattern is not new in WordPress and it's, more or less, like the Customizer sidebars. Those are known to be an accessibility anti-pattern and are a terrible experience for keyboard users and assistive technologies users. When a new sidebar "slides in", the previous content disappears producing a loss of context that's hard to understand for users, especially screen readers users.

This was also quickly discussed during the latest weekly accessibility meeting and probably the team will need to discuss it again bt I'm pretty sure the team has big concerns on the usage of this pattern, being also, as said, a known a11y issue.

@Copons Copons self-requested a review September 7, 2020 17:57
@Copons
Copy link
Contributor

Copons commented Sep 7, 2020

A quick note on the "sliding sidebars" pattern. This pattern is not new in WordPress and it's, more or less, like the Customizer sidebars. Those are known to be an accessibility anti-pattern and are a terrible experience for keyboard users and assistive technologies users. When a new sidebar "slides in", the previous content disappears producing a loss of context that's hard to understand for users, especially screen readers users.

This was also quickly discussed during the latest weekly accessibility meeting and probably the team will need to discuss it again bt I'm pretty sure the team has big concerns on the usage of this pattern, being also, as said, a known a11y issue.

@afercia Thanks for chiming in!
I was thinking about a11y in my experimental proposal (#25057), which has a very different data handling than this PR, but its user-facing behaviour is roughly the same.

When the user navigates from a level to another, the former level un-renders and the latter renders.
In other words, the previous level is completely replaced by the next.

Would it make sense if on level change we automatically focused the first item of the new level?
Or, if it's a nested level, the back button (which is positioned above the list, so it would have the first tabindex by default)?

Should we instead try to work with always-rendered nested lists, and only toggle their visibility (which is kinda complicated, but that's not the point here 🙂)?

@afercia
Copy link
Contributor

afercia commented Sep 8, 2020

@Copons thanks for the ping.

When the user navigates from a level to another, the former level un-renders and the latter renders.
In other words, the previous level is completely replaced by the next.

I'm not sure that makes a difference. Whether a part of a UI that is currently used by users gets hidden or removed from the DOM, that's a big accessibility problem. There's a complete loss of context. Information on the number of items in the list and their nesting level is broken. Screen reader users wouldn't have a clue that a part of the UI just doesn't exist any longer (why should they?). Moving focus programmatically should be rare and only implemented when it's the expected behavior for some ARIA pattern. In my years of experience in contributing to the accessibility team I can say with no doubt that the Customizer sidebar is one of the patterns we received a lot of complaints about. Also, I would like to note that in other parallel issues about sidebars, there are ongoing explorations and discussions on how to move away from the concept of sidebars.

@Copons
Copy link
Contributor

Copons commented Sep 11, 2020

Closing as replaced by #25057.

Thank you so much @psealock and @joshuatf for working on this!
The other approach couldn't have been conceived without the excellent concept and groundwork laid out here — not to mention unit tests and other bits and pieces that I always forget to add!

I've also especially appreciated how gracefully y'all reacted to me casually showing up out of the blue and trampling all over the place. 🙇‍♂️

@Copons Copons closed this Sep 11, 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. Needs Accessibility Feedback Need input from accessibility
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants