feat(Menu): drill-down functionality#659
Conversation
| <Menu.Item onClick={handleClick} menu={cMenu}> | ||
| Item C | ||
| </Menu.Item> | ||
| <Menu.Item onClick={handleClick} menu={dMenu}> |
There was a problem hiding this comment.
Apparently that wasn't selection. It was focus/blur thing. Solved via key handling (element get actually re-rendered as they should on update, so the focus is reset)
| activeMenuProps = activeChild.props.menu.props | ||
| children = [ | ||
| backButton, | ||
| ...extendMenuItemsWithChevrons( |
There was a problem hiding this comment.
So I'm thinking if this shouldn't be done inside Menu.Item directly - like if you define submenu prop - Menu.Item would know itself that it needs to add chevron
WDYT?
There was a problem hiding this comment.
regarding handling the chevron inside Menu.Item I would agree 👍
However, if to talk specifically about the piece you're referring to, is a bit different topic.
In there I'm re-defining children to be rendered inside the parent/root Menu.
If we transfer the logic of rendering sub-menus into Menu.Item, we'd end up with nested <Menu> which we don't want, right?
To summarize my current conclusion regarding this one: I'm going to move the chevron logic into the `Menu.Item. Let me know if that matches your point?
There was a problem hiding this comment.
Yes, I think we are on the same page - the only chevron could be handled there, it makes sense to keep drill down logic inside menu (probably not even possible otherwise)
There was a problem hiding this comment.
I missed that conversation, but I left the same point - to move Chevron logic inside Menu.Item 👍
There was a problem hiding this comment.
Moved Chevron logic into Menu.Item. But still end up with that extending function, just renamed.
| /* eslint-disable react/jsx-key */ | ||
| const backButton = ( | ||
| <MenuItem onClick={handleBack}> | ||
| <Container inline flex alignItems='center' style={{ flex: 1 }}> |
There was a problem hiding this comment.
Do we use flex: 1 to make Container full width? If yes - I think we should address this in a container, so users wouldn't need to add inline styles like that
cc @toptal/frontend-experience
There was a problem hiding this comment.
makes sense. What prop name would you suggest @bytasv ?
There was a problem hiding this comment.
Maybe we could reuse pattern that we use elsewhere - using width prop with variants auto and full? Check Input component. Also could ping on a channel to get the opinion from other devs
There was a problem hiding this comment.
I got rid of both containers for this case
| <MenuItem onClick={handleBack}> | ||
| <Container inline flex alignItems='center' style={{ flex: 1 }}> | ||
| <BackMinor16 /> | ||
| <Container style={{ flex: 1 }}> |
There was a problem hiding this comment.
So I'm wondering why do we need multiple containers here? Won't it work with single flex container have both - icon and text and aligning all that in the middle?
There was a problem hiding this comment.
rriiight. Just taken this from another example and didn't give it proper thought. Nice one 👍
| ] as ReactElement | ||
|
|
||
| return child | ||
| if (activeChildPath.length === 1) { |
There was a problem hiding this comment.
I would inverse this if probably:
if (!activeChildPath.length) {
return getActiveChildRecursively(
currentLevelActiveChild.props.menu.props.children,
activeChildPath.slice(1)
)
}
return currentLevelActiveChild
| onClick: (arg: number) => void | ||
| ) { | ||
| children = React.Children.toArray(children).map(child => { | ||
| return React.Children.toArray(children).map((child, idx) => { |
There was a problem hiding this comment.
let's give a full name index instead of idx, was a bit confusing before I realized it's index and not id of some kind
| } | ||
|
|
||
| /* | ||
| function getActiveChildRecursively( |
There was a problem hiding this comment.
I'm just wondering if storing references to children instead of the path wouldn't be an easier to understand solution?
Something like:
const menuTrail = [childrenLevel1, childrenLevel2, childrenLevel3]
const activeMenu = menuTrail[menuTrail.length - 1]
That way we wouldn't need to constantly do a recursive search
cc @toptal/frontend-experience
There was a problem hiding this comment.
I went ahead and tried to implement a non-recursive solution by using refs, which sounded like a good idea to me. However I envisioned it a bit differently from what you suggest it seems.
Specifically, I didn't understand how do we compute such a menuTrail? It looks like a flat array, but the menu structure is rather tree-like. How it can be rendered into a flat array? Maybe I miss the point though.
Still I thought that we could perhaps pass a ref to the parent Menu.Item in the onClick event, rather than path, so then we could store that ref as the activeItem in the Menu state.
This plan looked viable to me, however I faced couple complexities with it:
- is it a good idea to store ref in the state?
- in order to sensibly maintain keys (which is required for proper handling focus/blur issue above, the one addressed earlier) we still want to know "depth", so then we'd have to calculate it still
Smth like that. Maybe I just completely missed your point @bytasv and you meant something totally different then I thought? Let me know please.
There was a problem hiding this comment.
So I don't think that you are far from what I envisioned. I'm thinking like this:
- We have menu with some items that have submenu
- So in this case
activeContent = initial menu, right? - Now we click on one of the submenus we store
activeContentinmenuTrailarray -const menuTrail = []and thenmenuTrail.push(activeContent) - Then we change
activeContentto that of the submenu -activeContent = submenu - We now see submenu and by checkinig
menuTrail.lengthwe can see that when it'smenuTrail.length > 0we can renderBackbutton together with submenu - Now if we click
Back, what we need to do is pop last item ini themenuTrailand replace it withactiveContent:const activeContent = menuTrail.pop() - After we pop that item from
menuTrailwe can see that array is empty and we are at the very top, so we don't need to renderBackbutton anymore
Now basically if we decide to go 3-4 levels deep we would be just using flat array for that because we don't need to keep tree structure, we just need to maintain the order of the menus and by going back we just need to pop them from that array.
I don't know if that's the best approach, but at least for me, it seems like a pretty natural way to think about it and if the code can represent same - then I think it should also be easy to understand.
There was a problem hiding this comment.
Now I see what you mean. Indeed, this solution sounds simple and it's actually tempting to go ahead with it. However with such approach we're going to store (arrays of) ReactNode in the state, right? And this makes me hesitate. Earlier there used to be a paragraph on reactjs.org which is quoted in this answer on SO: https://stackoverflow.com/a/47875226/2858977
From that I would conclude that it might be not a good idea to store JSX in state, because it's kind of computed, right?
I don't have strong opinion on this though, just thinking loudly. Interested to see further feedback on this.
There was a problem hiding this comment.
Ok, let's go with an existing approach. I was actually thinking about it too - whether or not it's good idea to store references... Let's not block this PR anymore with this
There was a problem hiding this comment.
I think the non-recursive approach, in this case, will be easier because this path is simply a stack, what @bytasv explained. To avoid storing React components in the state we can simply mark them from the beginning with the path as a key (or other prop) and then when we need to find active child we can just iterate over children tree to find a component with key === path
There was a problem hiding this comment.
@denieler , I tried to go ahead with it, but soon enough ran into 2 things that I don't see any simple resolution for:
- how do we form the
key? We would at least need to move the logic of pushing the selected menu into the stack intoMenu, right? (it's now inMenu.Item) - how do we iterate through the children tree? Unless I miss something, we'll have to iterate recursively, right?
havenchyk
left a comment
There was a problem hiding this comment.
As far as I remember, the approach we discussed for this ticket should have been using context and setting value of the context inside the child component. I have nothing against checking the props of the children as well.
As for adding Chevrons, maybe it's better to extend MenuItem with that option?
|
|
||
| if (childElement.props.menu) { | ||
| child = ( | ||
| /* eslint-disable react/jsx-props-no-spreading */ |
There was a problem hiding this comment.
seems you can disable the only line here
There was a problem hiding this comment.
As far as I remember, the approach we discussed for this ticket should have been using context and setting value of the context inside the child component. I have nothing against checking the props of the children as well.
Not quite sure how you envision this via context? Also, there might be multiple menus on the page, right? Don't you mix it with SidebarMenu by a chance?
I might be just missing the point, let me know pls.
As for adding Chevrons, maybe it's better to extend MenuItem with that option?
Yep, done that
ff556d1 to
8ef45c6
Compare
|
@oshchurtt I tried to put Drill down Menu inside Dropdown and it closes when I click on item with menu. We need to investigate and find a way how to override it. |
| variant?: VariantType | ||
| } | ||
|
|
||
| export const WrappedStringMenuItemContent = withStyles(styles, { |
There was a problem hiding this comment.
| export const WrappedStringMenuItemContent = withStyles(styles, { | |
| export const StringContent = withStyles(styles, { |
or maybe StringMenuItemContent?
| name: 'MenuItem' | ||
| })( | ||
| // eslint-disable-next-line react/display-name | ||
| forwardRef<HTMLElement, Props>(function StringMenuItem( |
There was a problem hiding this comment.
and there is probably no need to forward ref, this inner component can be simplified dramatically I believe 😄
| if (activeChildPath.length) { | ||
| const activeChild = getActiveChildRecursively(children, activeChildPath) | ||
|
|
||
| /* eslint-disable react/jsx-key */ |
There was a problem hiding this comment.
what is the issue with the key here?
There was a problem hiding this comment.
linter doesn't like that I use indexes for generating them 🤔
| const backButton = ( | ||
| <MenuItem onClick={handleBack} key={activeChildPath.join('_') + 'back'}> | ||
| <BackMinor16 /> | ||
| <WrappedStringMenuItemContent>Back</WrappedStringMenuItemContent> |
There was a problem hiding this comment.
I would not reuse here WrappedStringMenuItemContent and treat BackButton as a new component in this file, which has its own styles maybe even
There was a problem hiding this comment.
hm, good shout. That would actually allow to get rid of that inner component
| Item: typeof MenuItem | ||
| } | ||
|
|
||
| function extendMenuItemsWithNavigation( |
There was a problem hiding this comment.
can we maybe instead of function create a component, something like NavigationMenuItems (we also don't need to use this component if there are no sub-menus props in the children). I believe with the 'component' approach the code becomes more clear
|
|
||
| if (activeChildPath.length > 1) { | ||
| return getActiveChildRecursively( | ||
| currentLevelActiveChild.props.menu.props.children, |
There was a problem hiding this comment.
this looks a bit overcomplicated
| children = [ | ||
| backButton, | ||
| ...extendMenuItemsWithNavigation( | ||
| activeChild.props.menu.props.children, |
There was a problem hiding this comment.
here also a bit overcomplicated
| } | ||
|
|
||
| /* | ||
| function getActiveChildRecursively( |
There was a problem hiding this comment.
I think the non-recursive approach, in this case, will be easier because this path is simply a stack, what @bytasv explained. To avoid storing React components in the state we can simply mark them from the beginning with the path as a key (or other prop) and then when we need to find active child we can just iterate over children tree to find a component with key === path
|
|
||
| let activeMenuProps = {} | ||
|
|
||
| if (activeChildPath.length) { |
There was a problem hiding this comment.
if we would keep activeChildPath.length I would add a special name here to better understand what's going on - const shouldRenderSubMenu = activeChildPath.length (or something like this)
Maybe we can use |
155564d to
076be58
Compare
| console.log('Menu item is clicked') | ||
| } | ||
|
|
||
| const cMenu = ( |
There was a problem hiding this comment.
Give better names to menus
There was a problem hiding this comment.
actually I can't think of better names :) Do you?
There was a problem hiding this comment.
Maybe RootMenu, ParentMenu, ChildMenu
There was a problem hiding this comment.
hm, hm. In the example we don't have multiple levels of nesting, we rather have multiple sibling menus if to talk about it hierarchically. I'm not quite sure I understand what you mean therefore @vedrani 🤔
| <Menu.Item onClick={handleClick}>Item E</Menu.Item> | ||
| </Menu> | ||
|
|
||
| <Dropdown |
There was a problem hiding this comment.
Please check. Do you think it's better now?
| ) | ||
|
|
||
| return ( | ||
| <div> |
There was a problem hiding this comment.
Make it have smaller width like rest of examples.
There was a problem hiding this comment.
Please check the updated version. I've split "default" and "wrapped by dropdown" cases so they take half of the width each.
|
🎉 Last commit is successfully deployed 🎉 Demo is available on: Your davinci-bot 🚀 |
| export interface Props extends StandardProps, ListNativeProps {} | ||
| export interface Props extends StandardProps, ListNativeProps { | ||
| /** whether or not to handle nested navigation */ | ||
| allowNestedNavigation?: boolean |
There was a problem hiding this comment.
why do we need this prop?
There was a problem hiding this comment.
it's for SidebarMenu. In there submenus are rendered either in accordion or staically expanded. This prop is a way to tell the Menu component to not render Back button in the submenus.
I wasn't able to come up with a better way for solving that thing with SidebarMenu.Are you?
vedrani
left a comment
There was a problem hiding this comment.
Temploy is not working for this branch, do you maybe know why?
| console.log('Menu item is clicked') | ||
| } | ||
|
|
||
| const cMenu = ( |
There was a problem hiding this comment.
Maybe RootMenu, ParentMenu, ChildMenu

UPD: this branch name wasn't compatible with temploy, so I created another one: #684
FX-436
Description
We want to allow nested
Menuand drilling down it's sub-menus.How to test
Review
propsin component with documentationexamplesfor component