-
Notifications
You must be signed in to change notification settings - Fork 377
feat(pf4-nav): add nav component #626
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
Conversation
karelhala
left a comment
There was a problem hiding this 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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
|
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. |
|
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
|
Build: http://pf-nav.surge.sh/components/nav |
| const propTypes = { | ||
| id: PropTypes.string.isRequired, | ||
| title: PropTypes.string.isRequired, | ||
| ariaLabel: PropTypes.string.isRequired, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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'; | |||
There was a problem hiding this comment.
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.
|
Hey @jschuler, great job on this! There are just a couple of things I noticed:
|
|
One more thing... Please remove
We don't need it, so let's just remove it. |
|
Hey @jschuler, Do you think it would be possible to go ahead and include those updates in this PR, too? |
|
Great work @jschuler! Here's what I found. All
|
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? |
|
@mattnolting |
|
@jschuler ok by me! |
Pull Request Test Coverage Report for Build 2225
💛 - Coveralls |
|
Updated output at: http://pf-nav.surge.sh/components/nav |
| @@ -0,0 +1 @@ | |||
| export { default as Avatar, AvatarProps } from './Avatar'; | |||
There was a problem hiding this comment.
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'> { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ;)
There was a problem hiding this comment.
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 */ |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. ?
There was a problem hiding this comment.
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>.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 documentation deployment: https://626-pr-patternfly-react-patternfly.surge.sh |
karelhala
left a comment
There was a problem hiding this 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 => |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) => |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)} |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 }) => ({ |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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 | ||
| }; | ||
|
|
There was a problem hiding this comment.
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.
|
@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. |
b2e0d45 to
e8acd30
Compare
affects: @patternfly/react-core, @patternfly/react-docs ISSUES CLOSED: patternfly#547
80a7743 to
31a5b50
Compare
|
What is the migration path from patternfly-react vertical navigation component to this? |
|
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? |
|
@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. |
affects: @patternfly/react-core @patternfly/react-docs
ISSUES CLOSED: #547
Adds the nav component to pf4 react