Doist / reactist

Open source React components made with ❤️ by Doist
http://doist.github.io/reactist
MIT License
251 stars 22 forks source link

NavigationElement #641

Open gnapse opened 2 years ago

gnapse commented 2 years ago

🚀 Feature request

There's a common pattern in a few places of our products: a list element that acts as a link, but also has some optional information and/or contextual action buttons on its right side. A few examples:

I wonder if we can come up with a component that would serve as a generic blueprint to implement this pattern once and for all.

Motivation

I've had this in mind for a long time now. But what really made me request it now is that this pattern is not trivial to achieve in a way that's done right. In particular, the fact that it is a link that visually has some other interactive elements "inside" it (the optional action buttons to the right). But link elements are not allowed to have interactive elements inside it. So what really needs to be done is to create a markup that's correct, with the link and action buttons being "siblings", but visually place everything so that it gives the impression that the link contains everything.

See for instance https://github.com/Doist/twist-web/pull/4658, where @pauloslund had to refactor our Twist thread links to be like this, because having the buttons inside the link was having a really adverse consequence for Firefox users.

Requirements

These navigation elements have a few things in common:

This list above may not be exhaustive yet.

Example

An example for how this feature would be used.

  1. It could use a props-based approach to provide all the possible slots of content that can be customized:

    <NavigationElement
        icon={<ThreadIcon />}
        mainContent={
            <Box>
                <Text>{thread.title}</Text>
                <Text tone="secondary" size="caption">{thread.snippet}</Text>
            </Box>
        }
        secondaryContent={
            <ThreadChannelInfo />
        }
        actions={
            <ThreadActionButtons />
        }
    />
  2. Alternatively, it could provide "sub-components" to fill in the slots, similarly to how our modal works:

    <NavigationElement>
        <NavigationElementContent icon={<ThreadIcon />}>
            <Text>{thread.title}</Text>
            <Text tone="secondary" size="caption">{thread.snippet}</Text>
        </NavigationElementContent>
        <NavigationElementSecondaryContent>
            <ThreadChannelInfo />
        </NavigationElementSecondaryContent>
        <NavigationElementActions>
            <ThreadActionButtons />
        </NavigationElementActions>
    </NavigationElement>
  3. An alternative that applies to both cases above is how to deal with the duality of content shown on the right. That is, the alternation between showing some content on the right, vs. action buttons when hovered, replacing the regular content on the right. Instead of providing them both as in the previous two examples, we could consider giving the consumer of this component the ability to decide, via a render prop:

    <NavigationContent
        {...props}
        secondaryContent={
            isActive =>
                isActive ? <ThreadActionButtons /> : <ThreadChannelInfo />}
    />
    
    // or with the components approach
    <NavigationElement>
        {...}
        <NavigationElementSecondaryContent>
            {isActive =>
                isActive ? <ThreadActionButtons /> : <ThreadChannelInfo />}
        </NavigationElementSecondaryContent>
    </NavigationElement>
  4. Some other alternative? Let me know.

Please, keep in mind that the above suggestions are just that, and that someone actually jumping in on implementing this should weight the pros and cons, and also consider all this against the actual practicalities of making this component work in all the situations that this pattern appears in our apps. Names and specifics of components need not be exactly like any of the above examples. We can discuss specifics below, as this progresses towards execution.

henningmu commented 2 years ago

Option 2 sounds good and looks inline with our other components (modal, tabs, etc.), but, as you mentioned, I'd leave this to the one implementing this component.


Do you think the NavigationElement could become a bit of catch-all component with tons of nuanced use cases? For example, would a TaskListItem in Todoist use the NavigationElement as well? Or should we restrict the use cases from the get go to start icon, primary and secondary text, and end actions?

gnapse commented 2 years ago

Do you think the NavigationElement could become a bit of catch-all component with tons of nuanced use cases? For example, would a TaskListItem in Todoist use the NavigationElement as well? Or should we restrict the use cases from the get go to start icon, primary and secondary text, and end actions?

Indeed, something to consider. I also acknowledge that trying to achieve too much with it could make it cumbersome. At the very least, I think that threre's definitely potential to make one that would power all the sidebar navigation items. Whether we can expand it to twist-like thread link elements (with a different left-side icon area, and a different border when focused), it may prove to be too much for a single component. And Todoist's task list elements are even more different. I would not try to make it support that (TD task list elements even have a different layout, with the action elements overlapping the content with some gradient transparency, the overflow menu button lies outside the list element boundaries, and they also have a drag-and-drop handle also floating outside the box boundaries).