-
Notifications
You must be signed in to change notification settings - Fork 143
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
docs(SideNav): API Decision #2149
Conversation
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
✅ PR title follows Conventional Commits specification. |
<Button | ||
href="/payouts/create" | ||
icon={PlusIcon} | ||
size="xsmall" | ||
variant="tertiary" | ||
/> |
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.
We can put a tooltip here as well, we should update the screenshot with an 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.
I think both trailing and item itself can have tooltip so we can keep the one that we have below. I'll add tooltip example on trailing
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.
so all the trailing will only be visible on hover by default?
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.
yes. And currently only button is accepted in trailing slot
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.
We should mention somewhere in documentation and here in prop table that trailing is only visible on hover
<Tooltip content="Action Name (Cmd + P)"> | ||
<SideNavLink | ||
icon={LayoutIcon} | ||
title="L1 Item Name" | ||
href="/new-item-link" | ||
/> | ||
</Tooltip> |
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 will cause a lot of issues while iterating over the nested items, because now you have to map over the Tooltip and allow it as part of the composite element.
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.
We can instead have this as a first class feature.
`<SideNavLink tooltip={{ content: "Tooltip content" }} />
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 will cause a lot of issues while iterating over the nested items, because now you have to map over the Tooltip and allow it as part of the composite element.
Trying out approach that doesn't require iterating over items
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.
`<SideNavLink tooltip={{ content: "Tooltip content" }} />
This makes sense. Lets go with this
|
||
<SideNav routerLink={NavLink}> | ||
{/* L1 Items */} | ||
<SideNavLink title="Home" icon={HomeIcon} href="/" /> |
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.
how can we make a particular nav item selected by default? also, it's not just default if someone lands on a URL say one merchant sharing it with another/our support team when we land on that URL the nav item shall be selected by default so shall we also have controlled API?
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.
As discussed in office, even if we support adding isSelected or isActive prop, it will only select item and not change the route itself. With the current approach, they can move to their selected route and that will select item
|
||
<!-- prettier-ignore --> | ||
```jsx | ||
<Tooltip content="Action Name (Cmd + P)"> |
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.
is it also possible to have an info icon and show the tooltip on that? can we add an example of that?
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 shall we not have tooltip on the entire nav link and have on info icon? because a11y?
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 will be there on overall item (similar to our Tooltip with Button example). Because tooltip represents what happens on click of item.
Anurag suggested having tooltip inside SideNavLink component as first-class feature so will explore that
{/* L1 Items */} | ||
<SideNavLink title="L1 Item" /> | ||
|
||
<SideNavLink title="L2 Trigger"> |
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.
what if we just have NavLink and provide onClick
and href
as 2 props and if devs want nested levels they can use onClick and if its href that's a navigation? its the same terminology as link as button and link as nav
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.
Currently clicking item also changes the route so need href there
```jsx | ||
<SideNav> | ||
{/* L1 Items */} | ||
<SideNavLink title="L1 Item" /> |
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.
how are we deciding when to open the sub nav(L2) and when to expand it(L3)? using context?
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.
yup using context
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 elaborate more on this?
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.
Basically I pass level
in context, then in next context I pass level + 1
so it gives the nesting of context providers from that I can tell if its L2 or L3
|
||
| **Props** | **Description** | **Type** | **Default Value** | | ||
| --------- | ----------------------------------------------------- | -------- | ----------------- | | ||
| title | Only applicable in L2. Becomes the title of the Level | 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.
we need to give a way for product teams to track analytics for every sub-component of Nav being clicked.
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.
onClick on SideNavLink should be able to cover most usecases 🤔
| --------------- | ------------------------------------------------------------ | -------- | ----------------- | | ||
| title | title of the section | string | | | ||
| maxVisibleItems | Number of items visible (rest go inside +x more collapsible) | number | undefined | | ||
| children | Children slot. For SideNavLink children items | JSX | | |
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.
when a user clicks the +13 more we need to give them a trigger to track analytics
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.
yup right. Will add this
/> | ||
<SideNavLink as={NavLink} title="Edit" icon={UserIcon} href="/accounts/settings"> | ||
{/* L3 */} | ||
<SideNavLevel> |
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 we really need to have sidelevel here? this kind of is leading to confusion because I can't have title on level 3. Also, the SideNavLink /accounts/settings
you won't navigate right when you're on L2 and want to access L3 items because in this case the Edit
its just acting like a collapsible item?
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 we really need to have sidelevel here? this kind of is leading to confusion because I can't have title on level 3
I missed removing title
prop from SideNavLevel in this example. Had removed it from props table. So yup we won't have title
prop in both L2 and L3.
I kept SideNavLevel here because it then follows the same structure in L2 and L3 so people can recursively loop through the object and render items without any additional logic
Also, the SideNavLink /accounts/settings you won't navigate right when you're on L2 and want to access L3 items because in this case the Edit its just acting like a collapsible item?
This is true. Not sure what is alternative. Currently I have kept href
optional so if you don't have href value here, it opens collapsible without navigation. But if you have href, then it navigates and opens collapsible at the same time (kind of like https://react.dev/reference/react-dom/client)
<Button | ||
href="/payouts/create" | ||
icon={PlusIcon} | ||
size="xsmall" | ||
variant="tertiary" | ||
/> |
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.
so all the trailing will only be visible on hover by default?
|
||
</table> | ||
|
||
### SideNavLevel |
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 hope we will handle the edge case where people try to add L4 sidenav level and throw some meaningful error
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.
yup added in implementation
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 API decision doc is nicely put together and was fun to read through 🚀
Have dropped a few comments
|
||
#### Why not config-driven API? | ||
|
||
1. Config-Driven API requires you to have a low-level compound API anyways since compound APIs give higher flexibility into adding leading, trailing, descriptions, and other items when needed. |
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.
how do compound APIs facilitate in providing higher flexibility?
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.
Lets say you want to add leading, trailing, descriptions, etc to items. With config-driven API we'll have to keep on adding that to the config schema. So only things that we allow in our config schema are valid rest are invalid.
With compound API, on lower level you can put any JSX and that will be rendered. So if any new usecase comes, in a lot of scenarios we might be able to write custom JSX that allows adding that usecase without opting out of entire SideNav.
<SideNav>
<Box>Random other text</Box>
</SideNav>
|
||
#### Why not this API? | ||
|
||
1. While its the easiest to implement considering how close it is to final DOM structure 🙈, it is complex in understanding and the backend schema we have has nested L1, L2 JSON. So its more complex to loop through a data like that and render this structure |
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.
agreed it's intuitional to read through the config and render the nested structure
```jsx | ||
import { NavLink } from 'react-router-dom'; | ||
|
||
<SideNav routerLink={NavLink}>{/* children */}</SideNav>; |
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 have a nav item change/select handler on the top-level component that can listen to selections by the user and can then be used for something like triggering relevant analytics events?
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.
We have onClick
on items so that can be used for analytics. E.g. If onClick on L2 trigger item is called then its L2 open event for analytics
| rel | anchor tag rel attribute [rel - MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#rel) | AnchorRelType | target === ' \_blank ' ? ' noreferrer noopener ' : undefined | | ||
| onClick | Click handler on item | (e: React.MouseEvent) => void | | | ||
| icon | Blade's Icon Component | IconComponent | | | ||
| trailing | Trailing Slot of Item. Can be used for adding Quick Shortcut Button, Trailing Text | JSX | | |
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.
How will trailing and titleSuffix sit together?
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.
Added the screenshots in table below. titleSuffix will come next to title and trailing will be on the right. However depending on usecase and how it looks, size of title, etc. Designers can take call whether they really want to show both or if just one is fine
| onClick | Click handler on item | (e: React.MouseEvent) => void | | | ||
| icon | Blade's Icon Component | IconComponent | | | ||
| trailing | Trailing Slot of Item. Can be used for adding Quick Shortcut Button, Trailing Text | JSX | | | ||
| titleSuffix | Slot after the title to add Badge, Counter | JSX | | |
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.
since this slot has a designated function of displaying badges and counters, instead of titleSuffix
can we call it something on the lines of supplementSlot
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.
titleSuffix
is the standard name we follow in components like AccordionHeader, Card, DropdownHeader, etc to make API more predictable across blade so following the same thing here
{/* L1 Items */} | ||
<SideNavLink title="L1 Item" /> | ||
|
||
<SideNavLink title="L2 Trigger"> |
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.
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.
| **Props** | **Description** | **Type** | **Default Value** | | ||
| --------------- | ------------------------------------------------------------ | -------- | ----------------- | | ||
| title | title of the section | string | | | ||
| maxVisibleItems | Number of items visible (rest go inside +x more collapsible) | number | undefined | |
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 maxVisibleItems is not provided will it be in expanded state by default?
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.
yup in that case all items are always visible and that "Show More" link itself won't exist
|
||
## Accessibility | ||
|
||
1. All items should be accessible by `TAB`. Including going between levels L1, L2, L3 |
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.
when the SideNav mounts will the initially active item be in focus by default?
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 the focus will be controlled on page level so assuming the top-nav would have first focus and then sidenav
## Accessibility | ||
|
||
1. All items should be accessible by `TAB`. Including going between levels L1, L2, L3 | ||
2. We should use Blade's `SkipNav` utility to provide option of skipping nav and going to content |
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.
what is the SkipNav utility can you share more context about it?
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.
We have this utility in blade - https://blade.razorpay.com/?path=/docs/components-accessibility-skipnav--docs
So if you start tabbing, you see a Skip to content link which you can click to skip navigation and go to content directly
Earlier we started the API by saying that marking of the link as active based on route will be handled internally in blade component. There are some implemetation constraints when we go with this approach of handling active link internally | ||
|
||
1. We don't have access to hooks like `useLocation` or utilities like `matchPath` inside Blade since blade is library indepedent of the routing logic | ||
2. Although React Router's NavLink automatically adds `aria-current="page"` to active link which can be used to change colors in CSS, it doesn't give us access to handling state internally for our L1 -> L2 navigations [Here's the related proposal I created in React Router's Repo](https://github.com/remix-run/react-router/discussions/11543). |
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.
NavLink internally maintains the isActive based on route defined right? Can we not make use of that?
it doesn't give us access to handling state internally for our L1 -> L2 navigations
can you please elaborate on this a little?
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.
NavLink internally maintains the isActive based on route defined right? Can we not make use of that?
It doesn't give any isActive
in JS / React state. It adds aria-current="page" to link which can be used for CSS styling but because we have multiple menus where lets say you want to open L2 from L1, you need a way to know which item is Active in the react state (Added a bit more context in that React Router's proposal)
NavLink internally maintains the isActive based on route defined right
Basically the issue is its "only" internally maintaining that active state inside React Router's NavLink. Its not giving us a way to access that active state in our component's state
// sets the submenu as active | ||
isActive={true} | ||
href="/accounts" |
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.
Why does a submenu has an href?
Shouldn't the href of the first children of this submenu be auto selected?
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 can't tell which is the first element without doing React.Children.map
which we can't do because people will create wrapper NavLink which handles react router 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.
Oh so what happens if the Submenu href and the first items href doesn't match?
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 still works. Just L2 menu won't have any selection and it will take you to submenu's href
✨ Formatted Document ✨
Changelog of API
First Class Tooltip support as suggested by @anuraghazra
as prop on SideNavLink as suggested by @kamleshchandnani
Active state won't be handled internally
Consumers have to explicitly mark item as Active based on their router state. Checkout formatted doc for more info
<SideNavLink as={NavLink} + isActive={true} />
SideNavItem component addition
Instead of asking consumers to build custom item with Box, we give paddings and default styles pre-configured in SideNavItem. We can use it for Test Mode usecase now
SideNavBody component addition
We needed to add SideNavBody to add scroll in the items