Skip to content
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

ActionList #1078

Merged
merged 39 commits into from
Apr 5, 2021
Merged

ActionList #1078

merged 39 commits into from
Apr 5, 2021

Conversation

smockle
Copy link
Member

@smockle smockle commented Feb 24, 2021

Closes #1056

Quick Links

Preview

Minimal ActionList component preview, a list with four items

<ActionList
  items={[
    {text: 'New file'},
    ActionList.Divider,
    {text: 'Copy link'},
    {text: 'Edit file'},
    {text: 'Delete file', variant: 'danger'}
  ]}
/>

Merge checklist

  • Added or updated TypeScript definitions (index.d.ts) if necessary
  • Added/updated tests
  • Added/updated documentation
  • Tested in Chrome
  • Tested in Firefox
  • Tested in Safari
  • Tested in Edge

Take a look at the What we look for in reviews section of the contributing guidelines for more information on how we review PRs.

@vercel
Copy link

vercel bot commented Feb 24, 2021

This pull request is being automatically deployed with Vercel (learn more).
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: https://vercel.com/primer/primer-components/JBY65WCbgQHg37hP6DnvZUA4oqZb
✅ Preview: https://primer-components-git-action-list-primer.vercel.app

@vercel vercel bot temporarily deployed to Preview February 24, 2021 17:02 Inactive
@smockle smockle changed the title feat: Add 'ActionList' [WIP] Add 'ActionList' Feb 25, 2021
@smockle smockle changed the title [WIP] Add 'ActionList' [WIP] ActionList Feb 25, 2021
@changeset-bot
Copy link

changeset-bot bot commented Feb 26, 2021

⚠️ No Changeset found

Latest commit: e065a54

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel vercel bot temporarily deployed to Preview February 26, 2021 16:28 Inactive
@vercel vercel bot temporarily deployed to Preview March 4, 2021 17:14 Inactive
@vercel vercel bot temporarily deployed to Preview March 5, 2021 04:15 Inactive
@vercel vercel bot temporarily deployed to Preview March 8, 2021 16:36 Inactive
@vercel vercel bot temporarily deployed to Preview March 9, 2021 20:59 Inactive
@vercel vercel bot temporarily deployed to Preview March 9, 2021 21:57 Inactive
@vercel vercel bot temporarily deployed to Preview March 12, 2021 22:23 Inactive
@vercel vercel bot temporarily deployed to Preview March 17, 2021 01:41 Inactive
@vercel vercel bot temporarily deployed to Preview March 17, 2021 02:48 Inactive
@vercel vercel bot temporarily deployed to Preview March 18, 2021 15:18 Inactive
@vercel vercel bot temporarily deployed to Preview March 19, 2021 15:43 Inactive

export function Group({renderItem: _renderItem, ...props}: GroupProps): JSX.Element {
return <div {...props} />
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious, why do we need this component?

import styled from 'styled-components'
import {get} from '../constants'

interface ItemPropsBase extends React.ComponentPropsWithoutRef<'div'> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm going to write some TypeScript authoring guidelines but I think we should avoid including React.ComponentProps<> in the props types that we export. This will make it easier for consumers to use these types with the as prop.

For example, if someone wanted to make a wrapper around ActionList.Item that rendered a button, the div props could make things tricky:

import {ActionList, ActionListItemProps} from '@primer/components'

type MyItemProps = ActionListItemProps & React.ComponentPropsWithoutRef<'button'>
// Now MyItemProps has button and div props :(

function MyItem(props: MyItemProps) {
  return <ActionList.Item {...props} as="button" />
}
Suggested change
interface ItemPropsBase extends React.ComponentPropsWithoutRef<'div'> {
interface ItemPropsBase {

Copy link
Member Author

@smockle smockle Mar 31, 2021

Choose a reason for hiding this comment

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

I’m definitely open to removing extends React.ComponentPropsWithoutRef<'div'>. 👍

Aside: Re: the as prop, we probably don’t want to allow it for ActionList.Items, since they can contain multiple interactive elements (examples below), but buttons can’t (i.e. nested buttons is not valid HTML).

@vdepizzol and I discussed examples where the ActionList pattern (if not this PR’s implementation) could appear outside DropdownMenu and ActionMenu.

One such example is the issues/PRs list (screenshot below). Items in these lists include titles, labels, and authors; clicking these initiates three different actions. If ActionList.Items were rendered as a button (or an a), the rendered HTML would be invalid.

A list of Primer Components PRs

Another example is the repo file list (screenshot below). Similar to the issue/PR list, each item in the repo file list includes a file name and the most-recent-commit message; clicking these navigates to two different URLs:

A list of files in the Primer Components repo

@vercel vercel bot temporarily deployed to Preview March 31, 2021 03:51 Inactive
@vercel vercel bot temporarily deployed to Preview March 31, 2021 16:18 Inactive
@vercel vercel bot temporarily deployed to Preview March 31, 2021 16:33 Inactive
…gray' with 'bg.tertiary' and 'border.tertiary'.
@vercel vercel bot temporarily deployed to Preview March 31, 2021 17:08 Inactive
@vercel vercel bot temporarily deployed to Preview March 31, 2021 17:13 Inactive
@vercel vercel bot temporarily deployed to Preview March 31, 2021 17:33 Inactive
…List', 'Header' and 'Item' to vertically and horizontally indent them
@vercel vercel bot temporarily deployed to Preview March 31, 2021 17:41 Inactive
@vercel vercel bot temporarily deployed to Preview March 31, 2021 18:58 Inactive
@vercel vercel bot temporarily deployed to Preview March 31, 2021 20:03 Inactive
@vercel vercel bot temporarily deployed to Preview April 2, 2021 01:41 Inactive
@vercel vercel bot temporarily deployed to Preview April 2, 2021 01:45 Inactive
{groupId: '3'},
{groupId: '4'}
]}
items={[

Choose a reason for hiding this comment

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

Having read @T-Hugs's doc, I'd be curious to hear if we've tried implementing this component with subcomponents.

I recognize that I don't have a ton of context here, and that most of my relevant experience is on the Rails side, but several existing Primer React components use subcomponents, such as TabNav, and I'm wondering if it might be a better long-term option here.

The reason I ask is that it feels like we are passing a lot of information to ActionList about its children, effectively having it conceptually cover two different levels of hierarchy (parent and children). While it is not onerous now, I can imagine a future where we need to double the number of options for child items, leading to a significantly larger API for this single component.

Instead, I could see us having:

<ActionList groups={[
     {groupId: '0'},
     {groupId: '1', header: {title: 'Live query', variant: 'subtle'}},
     {groupId: '2', header: {title: 'Layout', variant: 'subtle'}},
     {groupId: '3'},
     {groupId: '4'}
   ]}>
  <ActionList.Item leadingVisual={TypographyIcon} text="Rename" groupId="0"/>
  // etc

By splitting the API across the two levels of hierarchy, we get to divide the number of properties between the parent and children. Having seen the breadth and depth of @vdepizzol's ideas for overlays, I expect there to be expansion in both dimensions in the future 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

From @joelhawksley in #1078 (comment):

Having read @T-Hugs's doc, I'd be curious to hear if we've tried implementing this component with subcomponents.

With a subcomponent approach, can Items be associated with Groups without resorting to React.Children.map? (cf. “The React.Children anti-pattern”)

Copy link
Member Author

@smockle smockle Apr 2, 2021

Choose a reason for hiding this comment

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

(You’re asking about coupling between parent and child types—“it feels like we are passing a lot of information to ActionList about its children, effectively having it conceptually cover two different levels of hierarchy (parent and children)”—but as an aside:) If syntax was also a concern, the ActionList API (as implemented in this PR) supports JSX in addition to JSON:

<ActionList
  groupMetadata={[
    {groupId: '0'},
    {groupId: '1', header: {title: 'Live query', variant: 'subtle'}},
    {groupId: '2', header: {title: 'Layout', variant: 'subtle'}},
    {groupId: '3'},
    {groupId: '4'}
  ]}
  items={[
    {groupId: '0', renderItem: p => <ActionList.Item leadingVisual={TypographyIcon} text="Rename" {...p} />},
    {groupId: '0', renderItem: p => <ActionList.Item leadingVisual={VersionsIcon} text="Duplicate" {...p} />},
    {groupId: '1', renderItem: p => <ActionList.Item leadingVisual={SearchIcon} text="repo:github/memex,github/github" {...p} />},
    {groupId: '2', renderItem: p => <ActionList.Item leadingVisual={NoteIcon} text="Table" description="Information-dense table optimized for operations across teams" descriptionVariant="block" {...p} />},
    {groupId: '2', renderItem: p => <ActionList.Item leadingVisual={NoteIcon} text="Board" description="Kanban-style board focused on visual states" descriptionVariant="block" {...p} />},
    {groupId: '3', renderItem: p => <ActionList.Item leadingVisual={FilterIcon} text="Save sort and filters to current view" {...p} />},
    {groupId: '3', renderItem: p => <ActionList.Item leadingVisual={FilterIcon} text="Save sort and filters to new view" {...p} />},
    {groupId: '4', renderItem: p => <ActionList.Item leadingVisual={GearIcon} text="View settings" {...p} />}
  ]}
/>

Copy link

@joelhawksley joelhawksley Apr 2, 2021

Choose a reason for hiding this comment

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

With a subcomponent approach, can Items be associated with Groups without resorting to React.Children.map?

I think I might have missed a level of abstraction here! Would this work?

<ActionList />
  <ActionList.Group title="Live query" variant="subtle"} />
    <ActionList.GroupItem leadingVisual={TypographyIcon} text="Rename"/>
   // etc

Copy link
Contributor

Choose a reason for hiding this comment

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

Personally I much prefer the data API approach here:

  1. The list owns grouping, absolving the consumer from this responsibility.
  2. Limits the ways it can be rendered without an explicit escape hatch (renderItem) while simultaneously providing a strongly-typed API.
  3. Easier to support things like loading states and extensibility (items as a Promise).
  4. Imagine a Dropdown component that uses ActionList. The Dropdown would need a say in the the items rendered by ActionList (e.g. the leading visual to show a checkmark, an onClick handler). This is easy if the Dropdown is passed items as data, which then get modified and forwarded to ActionList. But if your items come as children, you have to do a complicated traversal through the children with React.Children, using cloneElement to effect the right behaviors.

In the interest of tempo, I would like to get this PR merged so we can unblock other components.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for enumerating some of the benefits of an items-as-data approach, @T-Hugs. 👍

In the interest of tempo, I would like to get this PR merged so we can unblock other components.

Sounds good 👍 I’m merging this PR, because doing so doesn’t preclude us from supporting other component APIs in the future: ActionList could be updated to coalesce its React.Children into an items-like array side-by-side with this PR’s items prop. If whether ActionList should do so is a topic of continued interest, merging is non-disruptive.

Merging unblocks #1152 and #1135 and allows us to ship-to-learn about the ergonomics and DX of this component API outside of contrived Storybook stories, e.g. in situations where the list of items is not available when the component is initially loaded (as anticipated by @T-Hugs in “3.” above).

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Contributor

@T-Hugs T-Hugs left a comment

Choose a reason for hiding this comment

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

Thanks for the diligent work on this!

@smockle smockle merged commit 4a5b3bb into main Apr 5, 2021
@smockle smockle deleted the action-list branch April 5, 2021 16:02
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.

ActionList
5 participants