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.Item accepts a polymorphic 'as' prop #1463

Merged
merged 7 commits into from
Sep 30, 2021
Merged

Conversation

jfuchs
Copy link
Contributor

@jfuchs jfuchs commented Sep 24, 2021

This PR adds an as prop to ActionList.Item so that Primer users can put HTML Anchors, Next.js Links, React Router Links, or whatever else in their action lists.

I am adding a dependency (@radix-ui/react-polymorphic) for some type definitions. The library is:

  • MIT licensed
  • Pretty widely used
  • Has no dependences
  • Shouldn't add any bytes to bundles as it is just types

Closes #1413
Closes #1454

Merge checklist

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

@changeset-bot
Copy link

changeset-bot bot commented Sep 24, 2021

🦋 Changeset detected

Latest commit: 7cfe964

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/components Minor

Not sure what this means? Click here to learn what changesets are.

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

return (
<ItemComponent
showDivider={props.showItemDividers}
selectionVariant={props.selectionVariant}
{...itemProps}
key={key}
sx={{...itemStyle, ...itemProps.sx}}
item={item}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@dgreif after my changes, tsc was complaining about this line, and I honestly couldn't figure out what the intent was here. Any ideas? Is it safe to remove?

Copy link
Member

Choose a reason for hiding this comment

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

It's used by the callback handlers for onAction if I remember correctly. This comes back to the object equivalence issue for selection. We couldn't just pass an item "like" the one that was selected, we need the actual item that caused the ItemComponent to get rendered in the first place. That whole system has made a lot of things more complicated then I would like them to be 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ohh that makes sense. I was wondering about that callback too. Any idea why the item in the callback was necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(that is, I can't picture the usage where this is helping simplify things...why wouldn't the user just keep their item in the closure of the callback?)

Copy link
Member

Choose a reason for hiding this comment

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

I think the item in the callback was for the same reason. Higher components listening to the callback need the exact item to store which thing got selected

@github-actions
Copy link
Contributor

github-actions bot commented Sep 24, 2021

size-limit report 📦

Path Size
dist/browser.esm.js 55.41 KB (+0.03% 🔺)
dist/browser.umd.js 55.73 KB (+0.02% 🔺)

@@ -162,7 +164,7 @@ export const List = React.forwardRef<HTMLDivElement, ListProps>((props, forwarde
const renderItem = (itemProps: ItemInput, item: ItemInput, itemIndex: number) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const ItemComponent = ('renderItem' in itemProps && itemProps.renderItem) || props.renderItem || Item
const key = itemProps.key ?? itemProps.id?.toString() ?? itemIndex.toString()
const key = itemProps.id?.toString() ?? itemIndex.toString()
Copy link
Member

Choose a reason for hiding this comment

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

Non-blocking: Out of curiosity, why was this change to key necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch — this was my mistake. TS caught that key wasn't on ItemInput so I removed the line intending to circle back to it. Will fix!

Comment on lines 381 to 383
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return React.cloneElement(child, childProps)
Copy link
Member

Choose a reason for hiding this comment

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

Does this accomplish the same result without a @ts-ignore?

Suggested change
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return React.cloneElement(child, childProps)
return (
<>
{React.isValidElement(child)
? React.cloneElement(child, childProps)
: null}
</>
);

| showItemDividers | `boolean` | `false` | Optional. If `true` dividers will be displayed above each `ActionList.Item` which does not follow an `ActionList.Header` or `ActionList.Divider` |
| Name | Type | Default | Description |
| :--------------- | :---------------------------------- | :---------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |
| items | `Array<ItemProps | (props: ItemProps) => JSX.Element>` | `undefined` | Required. A list of item objects conforming to the `ActionList.Item` props interface. |
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like the | in the type is confusing Markdown:
CleanShot 2021-09-29 at 09 05 53

/**
* An item to pass back in the `onAction` callback, meant as
*/
item?: ItemInput
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you explain why we needed to add these props?

Copy link
Contributor Author

@jfuchs jfuchs Sep 29, 2021

Choose a reason for hiding this comment

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

These three fields were because tsc told me to :-) ... but also, they all make sense and I wonder if they only worked before because of gaps in the type coverage

}
}) as PolymorphicForwardRefComponent<'div', ItemProps>

export {Item}
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason you wrote the export like this instead of doing: export const Item ... on line 318

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, will fix.

export type ItemInput = ItemProps | (Partial<ItemProps> & {renderItem: typeof Item})
type RenderItemFn = (props: ItemProps) => React.ReactElement

export type ItemInput = ItemProps | ((Partial<ItemProps> & {renderItem: RenderItemFn}) & {key?: Key})
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, what's the reason for including key here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same thing as the new props for item — added because tsc caught issues that it wasn't catching before. in this case, it's something we allow in an ItemInput but remove before the ItemInput becomes ItemProps.

docs/content/ActionList.mdx Outdated Show resolved Hide resolved
docs/content/ActionList.mdx Outdated Show resolved Hide resolved
Comment on lines +392 to +413
<ActionList
items={[
{
text: 'A. Vanilla action',
renderItem: props => <ActionList.Item onAction={() => alert('hi?')} {...props} />
},
{
text: 'B. Vanilla link',
renderItem: props => <ActionList.Item as="a" href="/about" {...props} />
},
{
text: 'C. React Router link',
renderItem: props => <ActionList.Item as={ReactRouterLikeLink} to="/about" {...props} />
},
{
text: 'D. NextJS style',
renderItem: props => (
<NextJSLikeLink href="/about">
<ActionList.Item as="a" {...props} />
</NextJSLikeLink>
)
}
]}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we add a Jest test that will fail CI if ActionList.Item doesn't render the element passed through the as prop correctly?

I want to make sure we don't accidentally break this if we refactor ActionList.Item (which will likely happen soon)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just added a behavesAsComponent test

jfuchs and others added 5 commits September 29, 2021 14:00
Copy link
Contributor

@colebemis colebemis left a comment

Choose a reason for hiding this comment

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

Looks great. Excellent work, @jfuchs

docs/content/ActionList.mdx Outdated Show resolved Hide resolved
src/ActionList/Item.tsx Outdated Show resolved Hide resolved
Co-authored-by: Cole Bemis <colebemis@github.com>
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.Item should accept a ref Allow ActionList.Item to be an anchor
4 participants