Skip to content

feat(Menu): drill-down functionality#659

Closed
oshchurtt wants to merge 16 commits into
masterfrom
feat/menu-drill-down
Closed

feat(Menu): drill-down functionality#659
oshchurtt wants to merge 16 commits into
masterfrom
feat/menu-drill-down

Conversation

@oshchurtt
Copy link
Copy Markdown
Contributor

@oshchurtt oshchurtt commented Sep 10, 2019

UPD: this branch name wasn't compatible with temploy, so I created another one: #684

FX-436

Description

We want to allow nested Menu and drilling down it's sub-menus.

How to test

  • go to Menu story and check out Drill Down example

Review

  • Annotate all props in component with documentation
  • Create examples for component
  • Ensure that deployed demo has expected results and good examples
  • Ensure that visuals specs are green See the documentation

<Menu.Item onClick={handleClick} menu={cMenu}>
Item C
</Menu.Item>
<Menu.Item onClick={handleClick} menu={dMenu}>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you also check if we can remove menu item selection when drilling down? I noticed that in this example if I click to Item D in the next sub-menu I already have preselected item:
image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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)

Comment thread src/components/Menu/Menu.tsx Outdated
activeMenuProps = activeChild.props.menu.props
children = [
backButton,
...extendMenuItemsWithChevrons(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I missed that conversation, but I left the same point - to move Chevron logic inside Menu.Item 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Moved Chevron logic into Menu.Item. But still end up with that extending function, just renamed.

Comment thread src/components/Menu/Menu.tsx Outdated
/* eslint-disable react/jsx-key */
const backButton = (
<MenuItem onClick={handleBack}>
<Container inline flex alignItems='center' style={{ flex: 1 }}>
Copy link
Copy Markdown
Contributor

@bytasv bytasv Sep 11, 2019

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

makes sense. What prop name would you suggest @bytasv ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I got rid of both containers for this case

Comment thread src/components/Menu/Menu.tsx Outdated
<MenuItem onClick={handleBack}>
<Container inline flex alignItems='center' style={{ flex: 1 }}>
<BackMinor16 />
<Container style={{ flex: 1 }}>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

rriiight. Just taken this from another example and didn't give it proper thought. Nice one 👍

Comment thread src/components/Menu/Menu.tsx Outdated
] as ReactElement

return child
if (activeChildPath.length === 1) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would inverse this if probably:

if (!activeChildPath.length) {
    return getActiveChildRecursively(
      currentLevelActiveChild.props.menu.props.children,
      activeChildPath.slice(1)
    )
}

return currentLevelActiveChild

Comment thread src/components/Menu/Menu.tsx Outdated
onClick: (arg: number) => void
) {
children = React.Children.toArray(children).map(child => {
return React.Children.toArray(children).map((child, idx) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment thread src/components/Menu/Menu.tsx Outdated
}

/*
function getActiveChildRecursively(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

will try it out

Copy link
Copy Markdown
Contributor Author

@oshchurtt oshchurtt Sep 11, 2019

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So I don't think that you are far from what I envisioned. I'm thinking like this:

  1. We have menu with some items that have submenu
  2. So in this case activeContent = initial menu, right?
  3. Now we click on one of the submenus we store activeContent in menuTrail array - const menuTrail = [] and then menuTrail.push(activeContent)
  4. Then we change activeContent to that of the submenu - activeContent = submenu
  5. We now see submenu and by checkinig menuTrail.length we can see that when it's menuTrail.length > 0 we can render Back button together with submenu
  6. Now if we click Back, what we need to do is pop last item ini the menuTrail and replace it with activeContent: const activeContent = menuTrail.pop()
  7. After we pop that item from menuTrail we can see that array is empty and we are at the very top, so we don't need to render Back button 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@denieler , I tried to go ahead with it, but soon enough ran into 2 things that I don't see any simple resolution for:

  1. how do we form the key? We would at least need to move the logic of pushing the selected menu into the stack into Menu, right? (it's now in Menu.Item)
  2. how do we iterate through the children tree? Unless I miss something, we'll have to iterate recursively, right?

Copy link
Copy Markdown
Contributor

@havenchyk havenchyk left a comment

Choose a reason for hiding this comment

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

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?

Comment thread src/components/Menu/Menu.tsx Outdated

if (childElement.props.menu) {
child = (
/* eslint-disable react/jsx-props-no-spreading */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

seems you can disable the only line here

Copy link
Copy Markdown
Contributor Author

@oshchurtt oshchurtt Sep 11, 2019

Choose a reason for hiding this comment

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

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

Comment thread src/components/Menu/Menu.tsx Outdated
Comment thread src/components/MenuItem/MenuItem.tsx
@oshchurtt oshchurtt marked this pull request as ready for review September 12, 2019 04:20
@oshchurtt oshchurtt requested a review from a team September 12, 2019 04:20
@vedrani
Copy link
Copy Markdown
Collaborator

vedrani commented Sep 12, 2019

@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.

Comment thread src/components/MenuItem/MenuItem.tsx Outdated
variant?: VariantType
}

export const WrappedStringMenuItemContent = withStyles(styles, {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
export const WrappedStringMenuItemContent = withStyles(styles, {
export const StringContent = withStyles(styles, {

or maybe StringMenuItemContent?

Comment thread src/components/MenuItem/MenuItem.tsx Outdated
name: 'MenuItem'
})(
// eslint-disable-next-line react/display-name
forwardRef<HTMLElement, Props>(function StringMenuItem(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

and there is probably no need to forward ref, this inner component can be simplified dramatically I believe 😄

Comment thread src/components/Menu/Menu.tsx Outdated
if (activeChildPath.length) {
const activeChild = getActiveChildRecursively(children, activeChildPath)

/* eslint-disable react/jsx-key */
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

what is the issue with the key here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

linter doesn't like that I use indexes for generating them 🤔

Comment thread src/components/Menu/Menu.tsx Outdated
const backButton = (
<MenuItem onClick={handleBack} key={activeChildPath.join('_') + 'back'}>
<BackMinor16 />
<WrappedStringMenuItemContent>Back</WrappedStringMenuItemContent>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would not reuse here WrappedStringMenuItemContent and treat BackButton as a new component in this file, which has its own styles maybe even

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

hm, good shout. That would actually allow to get rid of that inner component

Comment thread src/components/Menu/Menu.tsx Outdated
Item: typeof MenuItem
}

function extendMenuItemsWithNavigation(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

let me try that

Comment thread src/components/Menu/Menu.tsx Outdated

if (activeChildPath.length > 1) {
return getActiveChildRecursively(
currentLevelActiveChild.props.menu.props.children,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this looks a bit overcomplicated

Comment thread src/components/Menu/Menu.tsx Outdated
children = [
backButton,
...extendMenuItemsWithNavigation(
activeChild.props.menu.props.children,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

here also a bit overcomplicated

Comment thread src/components/Menu/Menu.tsx Outdated
}

/*
function getActiveChildRecursively(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Comment thread src/components/Menu/Menu.tsx Outdated

let activeMenuProps = {}

if (activeChildPath.length) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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)

@bytasv
Copy link
Copy Markdown
Contributor

bytasv commented Sep 12, 2019

@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.

Maybe we can use stopPropagation in such case?

Comment thread src/components/MenuItem/MenuItem.tsx
@oshchurtt oshchurtt changed the title Feat/menu drill down feat(Menu): drill-down functionality Sep 13, 2019
Comment thread src/components/Menu/Menu.tsx
Comment thread src/components/Menu/Menu.tsx Outdated
console.log('Menu item is clicked')
}

const cMenu = (
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Give better names to menus

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

actually I can't think of better names :) Do you?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe RootMenu, ParentMenu, ChildMenu

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Divide better in UI.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Please check. Do you think it's better now?

)

return (
<div>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Make it have smaller width like rest of examples.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Please check the updated version. I've split "default" and "wrapped by dropdown" cases so they take half of the width each.

@toptal-devbot
Copy link
Copy Markdown
Contributor

🎉 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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why do we need this prop?

Copy link
Copy Markdown
Contributor Author

@oshchurtt oshchurtt Sep 16, 2019

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Collaborator

@vedrani vedrani left a comment

Choose a reason for hiding this comment

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

Temploy is not working for this branch, do you maybe know why?

console.log('Menu item is clicked')
}

const cMenu = (
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe RootMenu, ParentMenu, ChildMenu

Comment thread src/components/Menu/story/Drilldown.example.jsx
Comment thread src/components/Menu/story/Drilldown.example.jsx
Comment thread src/components/Menu/Menu.tsx
Comment thread src/components/Menu/Menu.tsx
Comment thread src/components/MenuItem/MenuItem.tsx
@oshchurtt oshchurtt closed this Sep 16, 2019
@havenchyk havenchyk deleted the feat/menu-drill-down branch October 11, 2019 15:11
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.

6 participants