Skip to content

Conversation

@jschuler
Copy link
Collaborator

@jschuler jschuler commented Sep 18, 2018

affects: @patternfly/react-core @patternfly/react-docs

ISSUES CLOSED: #547

Adds the nav component to pf4 react

Copy link
Contributor

@karelhala karelhala left a comment

Choose a reason for hiding this comment

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

Great PR, I think that this component will be needed by many projects. We should really focus on correct delivery of this component.


shouldComponentUpdate(nextProps, nextState) {
const { activeItem, activeGroup } = this.state;
if (activeItem === nextState.activeItem && activeGroup === nextState.activeGroup) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do I understand it correctly, that when consumers want to update the nav items they can only update if some item is active or not? What if there are some conditional items which will be shown only after some event happens? If so, is this correct behavior?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removing this


// Called back from NavListItem
updateActive(groupId, itemId) {
this.setState({
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we should hold which group and item is active, but rather send these information to consumer so he can react on these events, set corresponding active item and this component will be re-rendered.

With this approach the navigation will be activated no matter what. But what if consumer wants to ignore such thing (maybe there is form which needs to be filled before changing the navigation, or consumer has to check if user has access to such nav item)

const { children, className, ariaLabel, ...props } = this.props;

// Init the group and item states if unknown
let { activeItem, activeGroup } = this.state;
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think that shis should be our responsibility. If user forgets to set activeGroup and activeItem via props he should either be notified about it or nothing special should happen. But let's not do something magical like selecting first group and first item.

For instance app navigation should not be affected if user enters config, such config is still part of application, but is not present in app navigation. These applications exists and we should have them in mind as well.

const reactElement = React.isValidElement(children);
let clonedChild;
if (reactElement) {
// This could be a react-router Link for example
Copy link
Contributor

Choose a reason for hiding this comment

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

onUpdate function should handle all these things and you shouldn't have to clone the children and such.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I need to clone here to attach the update function to a react element

@karelhala
Copy link
Contributor

I might be getting the purpose of of-react components wrongly, so feel free to correct me. But I got an impression that we should just provide components which are as simple as possible. But I don't think this fits the equation.

Few important notes, do not hold the state of active items/groups rather notify consumer on what was clicked and let him decide what to do with such information.

Toggling is fine I guess, but notify the consumer about such event as well, there might be some items which are fetched from server so the RBAC is checked everytime user is expanding/collapsing group.

Also we shouldn't try to guess which items should be active as default since there might be parts of application which are not accessible trough app nav, but trough other parts of application and user would be really confused if first item would be active.

Don't get me wrong @jschuler this PR is awesome! I just think that this is the first component which consumers would use so make it as usable as possible. If you have any questions, just ping me on slack and we can talk about more examples and why I think they are important.

@jschuler
Copy link
Collaborator Author

Thanks for the great feedback @karelhala, i think you're right i will provide callback functions to user so they can decide what to do once an item is clicked and how to init the nav. Will modify the code to be less stateful and examples to show that the interaction decisions are driven from consumer side.

/** Additional classes added to the Nav */
className: PropTypes.string,
/** Non-visible label to describe the nav */
ariaLabel: PropTypes.string
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 we want to at least warn the consumer that an aria label is required here. Maybe a function similar to TextInput.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, we have been using aria-label for the prop name in other components

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll remove this prop and let the user pass in their own aria-label if they choose.
@jgiardino should aria-label be required for the nav? didn't see it mentioned in the core nav docs.

@jschuler
Copy link
Collaborator Author

Build: http://pf-nav.surge.sh/components/nav
Will manually update this url when i make the code changes
@jgiardino

const propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
ariaLabel: PropTypes.string.isRequired,
Copy link
Contributor

Choose a reason for hiding this comment

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

should be aria-label.
I have the same question here about the prop not being required. Should we default to something meaningful, or generate a warning?

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 we should generate a warning here for aria-label

Copy link
Contributor

Choose a reason for hiding this comment

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

I actually think that ariaLabel can be removed here . In the design it looks like it is only using aria-labelledby .

};

const NavGroup = ({
id,
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems the purpose for the id is for the 'aria-labeledby' attribute. If that is the case we probably don't want to force the consumer to provide one. We can generate the id similar to how it is don in the Modal Component.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

NavGroup is an internal component that is not exposed to consumer. It is used by NavList and passed all the props. id is not required and default is generated as you've mentioned

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep. I see it now. Looks good!

>
<a
className={css(styles.navLink)}
id={ariaLabel ? '' : id}
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand why we would want to assign the ariaLabel to the id here?

Copy link
Collaborator Author

@jschuler jschuler Sep 25, 2018

Choose a reason for hiding this comment

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

Depending on if aria-label is passed in, it will be used as text in an invisible <h2> section below and this id will not be added to the dom. If a custom aria-label is not passed in, then the id is set here and referenced for aria-labelledby

/** Additional classes added to the Nav */
className: PropTypes.string,
/** Non-visible label to describe the nav */
ariaLabel: PropTypes.string
Copy link
Contributor

@amarie401 amarie401 Sep 19, 2018

Choose a reason for hiding this comment

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

should be aria-label here as we do with the other components

/** Title shown for sections or expandable lists */
title: PropTypes.string,
/** Non-visible label to describe the expandable list */
ariaLabel: PropTypes.string,
Copy link
Contributor

Choose a reason for hiding this comment

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

aria-label here.

@@ -0,0 +1,44 @@
import React from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

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

can you remove the example. it is commented out.

@jgiardino
Copy link
Contributor

Hey @jschuler, great job on this! There are just a couple of things I noticed:

  • aria-current="page" should only be applied to the link with class pf-m-current
  • In the Grouped Nav example, the html structure doesn't match the core example. The core example has the <ul> as a child of the <section> for a group. Where as the react implementation has the <section> as a child to the <ul>.
  • In the expandable nav examples, the html structure also doesn't match what's in core. Each first-level list item should be a child of the same <ul>. But instead there are separate <ul> elements for each first-level list item.
  • In the Expandable Nav (w/subnav titles) example, when I place focus on the hidden <h2> element, it announces that it's clickable, but I can't find anything in the html that would indicate that it's clickable. Do you happen to know why JAWS thinks these headers are clickable?

@jgiardino
Copy link
Contributor

One more thing...

Please remove aria-hidden="false" from the <section> elements. According to wai-aria:

At the time of this writing, aria-hidden="false" is known to work inconsistently in browsers. As future implementations improve, use caution and test thoroughly before relying on this approach.

We don't need it, so let's just remove it.

@jgiardino
Copy link
Contributor

Hey @jschuler,
I just opened an issue to update a couple of attributes in the pf-core example of nav: patternfly/patternfly#746

Do you think it would be possible to go ahead and include those updates in this PR, too?

@mattnolting
Copy link
Contributor

Great work @jschuler! Here's what I found.

All <nav> elements

  • <nav class="pf-c-nav" aria-label> - aria-labels are missing values
  • I think our active and current states are mixed up (my fault). It shouldn't affect your implementation, but would rather be a css update in core.

Expandable nav

  • looks like <section class="pf-c-nav__subnav"> is being removed from the DOM. We need to keep that in the DOM, toggle class pf-m-expanded, then after animation has finished add hidden attr. There will definitely be more conversation about how we animate, but for now we can rely on css.
    • on collapse - remove pf-m-expanded, setTimout() to ~ 550ms and add hidden
    • on expand - remove hidden, setTimout() to ~ 20ms and add pf-m-expanded (allows animation to happen)

As mentioned by @jgiardino

Grouped nav

<nav>
  <section>
    <h2>
    <ul>
      <li>
        <a>

Expandable nav

<nav>
  <ul>
    <li>
      <a>
      <section>
        <ul>
          <li>

@jschuler
Copy link
Collaborator Author

jschuler commented Sep 25, 2018

@jgiardino @mattnolting

In the Expandable Nav (w/subnav titles) example, when I place focus on the hidden

element, it announces that it's clickable, but I can't find anything in the html that would indicate that it's clickable. Do you happen to know why JAWS thinks these headers are clickable?

I image the same issue would be present in the core version. I have not looked further into it. Maybe we can make this a followup issue?

@jschuler
Copy link
Collaborator Author

@mattnolting
Do you mind if we create a followup issue to track the animation for the expandable nav?

@mattnolting
Copy link
Contributor

@jschuler ok by me!

@coveralls
Copy link

coveralls commented Sep 25, 2018

Pull Request Test Coverage Report for Build 2225

  • 82 of 89 (92.13%) changed or added relevant lines in 6 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.3%) to 79.435%

Changes Missing Coverage Covered Lines Changed/Added Lines %
packages/patternfly-4/react-core/src/components/Nav/NavList.js 19 20 95.0%
packages/patternfly-4/react-core/src/components/Nav/Nav.js 28 34 82.35%
Totals Coverage Status
Change from base Build 2210: 0.3%
Covered Lines: 3132
Relevant Lines: 3683

💛 - Coveralls

amarie401
amarie401 previously approved these changes Sep 25, 2018
@jschuler
Copy link
Collaborator Author

Updated output at: http://pf-nav.surge.sh/components/nav

amarie401
amarie401 previously approved these changes Sep 25, 2018
@@ -0,0 +1 @@
export { default as Avatar, AvatarProps } from './Avatar';
Copy link
Contributor

Choose a reason for hiding this comment

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

thanks for adding this

import { SFC, HTMLProps, FormEvent } from 'react';
import { Omit } from '../../typeUtils';

export interface TextAreaProps extends Omit<HTMLProps<HTMLInputElement>, 'onChange'> {
Copy link
Contributor

Choose a reason for hiding this comment

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

thanks for adding this!

variant?: OneOf<typeof NavVariants, keyof typeof NavVariants>;
children?: ReactNode;
className?: string;
ariaLabel?: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see a ariaLabel prop on the Nav.js
I think you should have an aria-Label prop according to the Accessibility guidance in the docs.

Copy link
Contributor

Choose a reason for hiding this comment

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

I am not a TypeScript expert, but are you missing onChange as a prop here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

aria-prop can be added to the Nav component if the user wishes to since i add the ...props to it. Don't think it has to be required?

/** True to make the list expandable */
expandable: PropTypes.bool,
/** Flag indicating whether the list is expanded */
expanded: PropTypes.bool,
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 for consistency we should rename this to isExpanded.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, although personally I must say I am not a fan ;)

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 have no issues with it :) . I did however get dinged on a review for similar names due to wanting consistency.

innerClassName: PropTypes.string,
/** Title shown for sections or expandable lists */
title: PropTypes.string,
/** True to make the list expandable */
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 for consistency we should rename this to isExpandable.

/** Flag indicating whether the list is expanded */
expanded: PropTypes.bool,
/** Flag indicating whether the list is active */
active: PropTypes.bool
Copy link
Contributor

Choose a reason for hiding this comment

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

should be isActive for consistency.

const propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
ariaLabel: PropTypes.string.isRequired,
Copy link
Contributor

Choose a reason for hiding this comment

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

I actually think that ariaLabel can be removed here . In the design it looks like it is only using aria-labelledby .

>
<a
className={css(styles.navLink)}
id={ariaLabel ? null : id}
Copy link
Contributor

Choose a reason for hiding this comment

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

can you just set this to id

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ariaLabel is used in the <h2> below. And this id attribute should only be added to the DOM if ariaLabel is not set. If ariaLabel is not set, aria-labelledby gets the value from this <a> tag. If ariaLabel is set, aria-labelledby gets the value from the <h2> tag.

Copy link
Contributor

@tlabaj tlabaj Sep 26, 2018

Choose a reason for hiding this comment

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

I think it is the naming that is throwing me off. Maybe rename it to sectionTitle since that is what it really is. ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the h2 section is not visible and only there for screen readers. The <section> always has aria-labelledby with an id either from the <a> tag or from the <h2>.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Also, this is always set since the id is generated so the connection for screen readers will always be there.

Copy link
Contributor

Choose a reason for hiding this comment

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

I also talked to Jenn, other suggestions are srText or srHeading for the prop name since this is technically not an aria-label.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sounds good, I can rename it to srText

children: PropTypes.node.isRequired,
innerClassName: PropTypes.string.isRequired,
groupId: PropTypes.number.isRequired,
active: PropTypes.bool.isRequired
Copy link
Contributor

Choose a reason for hiding this comment

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

rename props for consistency
expandable -> isExpandable
active -> isActive
exapanded -> isExpanded

e.stopPropagation();
}
// Callback to Nav
updateActive(groupId, id);
Copy link
Contributor

Choose a reason for hiding this comment

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

should we add updateActive to the props?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No since it is only exposed on the Nav component

@patternfly-build
Copy link
Collaborator

PatternFly documentation deployment: https://626-pr-patternfly-react-patternfly.surge.sh

Copy link
Contributor

@karelhala karelhala left a comment

Choose a reason for hiding this comment

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

Looks really great! One major concern about strictly setting NavList for groupped navigation. Plus perhaps we could improve the overall render logic with react contexts, but that is optional.

render() {
const { variant, children, className, ...props } = this.props;

const getVariantStyle = type =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Optional: I love this object mapping! It can be simplified, that you don't really have to call function.

const variantStyle = {
  [NavVariants.default]: styles.navList,
  [NavVariants.simple]: styles.navSimpleList,
  [NavVariants.horizontal]: styles.navHorizontalList,
  [NavVariants.tertiary]: styles.navTertiaryList
}

And usage would be variantStyle[style] instead of getVariantStyle(type).

let variantStyle;
// Figure out if we have groups
let isGroup = false;
if (Array.isArray(children)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know if I understand this correctly, but if you are checking for groups and apply navList style for any navigation with groups this should be handled by default props.

If consumer wants to render tertiary nav with groups we should allow him to do it, he probably knows what he's doing and wants to do it anyways.

I'd remove this if statemenet altogether and just apply styles based on nav variant.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removing the variant style logic from the conditional. Groups do not have a variant style applied to them anyways so even if the user passes it in it won't have an effect. I still need to check if the user has passed in groups tho since the html structure is different for those.


let innerNav;
if (isGroup) {
innerNav = renderLists(
Copy link
Contributor

Choose a reason for hiding this comment

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

This copying children should be avoided, I used it in dropdown as well and am planing to remove it. It's not wrong, it just makes the rendering login a bit harder and it takes more time to render. To pass down parent's onToggle function you could use react-context so only parent Nav component will have onToggle function and child components can trigger such function as well. Here is great example https://hackernoon.com/how-to-use-the-new-react-context-api-fce011e7d87

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added context for the callback functions, but I still need to clone children to add indices.

Copy link
Contributor

Choose a reason for hiding this comment

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

You might want to check out https://reactjs.org/docs/react-api.html#reactchildren children structure should not be assumed. You will also want to us the isValidElement check if you plan to dive into props. It is possible a user will return null, undefined, false, stings, numbers, or functions all of which will throw on the property access.

isActive: false
};

const renderChildren = (children, groupId, onSelect) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

If you're gonna use react context you don't have to copy the children once again and just call onSelect from children right away.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not sure how to add index to the children without cloning? I did add context for the onSelect tho.

const defaultLink = (
<a
href={to}
onClick={e => this.onUpdate(e)}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can be simplified to

onClick={this.onUpdate}

class NavListItem extends React.Component {
onUpdate(e) {
const { onSelect, groupId, id } = this.props;
if (e) {
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to check for the event to be present, but if you really want to be defensive this can be simpliified with shortcircuit

e && e.stopPropagation();

e.stopPropagation();
}
// Callback to Nav
onSelect(groupId, id, e);
Copy link
Contributor

Choose a reason for hiding this comment

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

Usually if you pass down the event it should be as first parameter.

};

handleToggle = e => {
this.setState(({ value }) => ({
Copy link
Contributor

Choose a reason for hiding this comment

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

Optional: No need to use the function gimmick of setState, because you do not call it more than once.

const { value } = this.state;
const { groupId, onToggle } = this.props;
this.setState({
  value: !value;
});
onToggle && onToggle(e, groupId, !value);

The function call in setState might confuse someone, with this destructing of state and props you are mutating the state at the very end of function executin. But that's just my preference.

simple: 'simple';
horizontal: 'horizontal';
tertiary: 'tertiary';
}; No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing EOL.

>
<a
className={css(styles.navLink)}
id={ariaLabel ? null : id}
Copy link
Contributor

Choose a reason for hiding this comment

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

I also talked to Jenn, other suggestions are srText or srHeading for the prop name since this is technically not an aria-label.

/** Callback for when a list is expanded or collapsed */
onToggle: PropTypes.func
};

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add a warning similar to other components for the a missing aria-label.

@jschuler
Copy link
Collaborator Author

@karelhala @tlabaj @amarie401 @dmiller9911 Refactored and made usage more explicit so user can pass in ids. Removed much of the conditional logic and cloning that was present before.

affects: @patternfly/react-core, @patternfly/react-docs

ISSUES CLOSED: patternfly#547
@jeff-phillips-18
Copy link
Member

What is the migration path from patternfly-react vertical navigation component to this?

@jschuler
Copy link
Collaborator Author

jschuler commented Oct 1, 2018

I'm not sure @jeff-phillips-18, we'll need to compare and contrast and see where we fall short and where we want to make changes to the component to make the transition easier. Is there a workflow for this @LHinson ? Maybe a followup issue?

@dmiller9911
Copy link
Contributor

@jeff-phillips-18 I am definitely not trying to downplay migration for this, but aside from documentation is there much more needed? I assume that each application needing to migrate will only have 1 some navigation component along with the links. Migration for this component should be much lower effort than others.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.