Skip to content

Commit 3cb4eeb

Browse files
committed
ActionList.Item accepts a polymorphic 'as' prop
1 parent 7c23dbc commit 3cb4eeb

File tree

5 files changed

+85
-10
lines changed

5 files changed

+85
-10
lines changed

package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"dependencies": {
4646
"@primer/octicons-react": "^13.0.0",
4747
"@primer/primitives": "4.7.1",
48+
"@radix-ui/react-polymorphic": "0.0.14",
4849
"@react-aria/ssr": "3.1.0",
4950
"@styled-system/css": "5.1.5",
5051
"@styled-system/props": "5.1.5",

src/ActionList/Item.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
isActiveDescendantAttribute
1515
} from '../behaviors/focusZone'
1616
import {useSSRSafeId} from '@react-aria/ssr'
17+
import type * as Polymorphic from '@radix-ui/react-polymorphic'
18+
import {AriaRole} from '../utils/types'
1719

1820
/**
1921
* These colors are not yet in our default theme. Need to remove this once they are added.
@@ -48,7 +50,7 @@ const customItemThemes = {
4850
/**
4951
* Contract for props passed to the `Item` component.
5052
*/
51-
export interface ItemProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'id'>, SxProp {
53+
export interface ItemProps extends SxProp {
5254
/**
5355
* Primary text which names an `Item`.
5456
*/
@@ -124,6 +126,10 @@ export interface ItemProps extends Omit<React.ComponentPropsWithoutRef<'div'>, '
124126
* An id associated with this item. Should be unique between items
125127
*/
126128
id?: number | string
129+
130+
children?: React.ReactNode
131+
132+
role?: AriaRole
127133
}
128134

129135
const getItemVariant = (variant = 'default', disabled?: boolean) => {
@@ -191,6 +197,7 @@ const StyledItem = styled.div<
191197
color: ${({variant, item}) => getItemVariant(variant, item?.disabled).color};
192198
// 2 frames on a 60hz monitor
193199
transition: background 33.333ms linear;
200+
text-decoration: none;
194201
195202
@media (hover: hover) and (pointer: fine) {
196203
:hover {
@@ -315,8 +322,9 @@ const MultiSelectInput = styled.input`
315322
/**
316323
* An actionable or selectable `Item` with an optional icon and description.
317324
*/
318-
export function Item(itemProps: Partial<ItemProps> & {item?: ItemInput}): JSX.Element {
325+
const Item = React.forwardRef((itemProps, ref) => {
319326
const {
327+
as: Component,
320328
text,
321329
description,
322330
descriptionVariant = 'inline',
@@ -379,6 +387,8 @@ export function Item(itemProps: Partial<ItemProps> & {item?: ItemInput}): JSX.El
379387

380388
return (
381389
<StyledItem
390+
ref={ref}
391+
as={Component}
382392
tabIndex={disabled ? undefined : -1}
383393
variant={variant}
384394
showDivider={showDivider}
@@ -457,4 +467,6 @@ export function Item(itemProps: Partial<ItemProps> & {item?: ItemInput}): JSX.El
457467
</DividedContent>
458468
</StyledItem>
459469
)
460-
}
470+
}) as Polymorphic.ForwardRefComponent<'div', ItemProps>
471+
472+
export {Item}

src/ActionList/List.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import {get} from '../constants'
88
import {SystemCssProperties} from '@styled-system/css'
99
import {hasActiveDescendantAttribute} from '../behaviors/focusZone'
1010

11-
export type ItemInput = ItemProps | (Partial<ItemProps> & {renderItem: typeof Item})
11+
type RenderItemFn = (props: ItemProps) => React.ReactElement
12+
13+
export type ItemInput = ItemProps | (Partial<ItemProps> & {renderItem: RenderItemFn})
1214

1315
/**
1416
* Contract for props passed to the `List` component.
@@ -34,7 +36,7 @@ export interface ListPropsBase {
3436
* without a `Group`-level or `Item`-level custom `Item` renderer will be
3537
* rendered using this function component.
3638
*/
37-
renderItem?: typeof Item
39+
renderItem?: RenderItemFn
3840

3941
/**
4042
* A `List`-level custom `Group` renderer. Every `Group` within this `List`
@@ -72,14 +74,14 @@ export interface GroupedListProps extends ListPropsBase {
7274
*/
7375
groupMetadata: ((
7476
| Omit<GroupProps, 'items'>
75-
| Omit<Partial<GroupProps> & {renderItem?: typeof Item; renderGroup?: typeof Group}, 'items'>
77+
| Omit<Partial<GroupProps> & {renderItem?: RenderItemFn; renderGroup?: typeof Group}, 'items'>
7678
) & {groupId: string})[]
7779

7880
/**
7981
* A collection of `Item` props, plus associated group identifiers
8082
* and `Item`-level custom `Item` renderers.
8183
*/
82-
items: ((ItemProps | (Partial<ItemProps> & {renderItem: typeof Item})) & {groupId: string})[]
84+
items: ((ItemProps | (Partial<ItemProps> & {renderItem: RenderItemFn})) & {groupId: string})[]
8385
}
8486

8587
/**
@@ -162,15 +164,14 @@ export const List = React.forwardRef<HTMLDivElement, ListProps>((props, forwarde
162164
const renderItem = (itemProps: ItemInput, item: ItemInput, itemIndex: number) => {
163165
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
164166
const ItemComponent = ('renderItem' in itemProps && itemProps.renderItem) || props.renderItem || Item
165-
const key = itemProps.key ?? itemProps.id?.toString() ?? itemIndex.toString()
167+
const key = itemProps.id?.toString() ?? itemIndex.toString()
166168
return (
167169
<ItemComponent
168170
showDivider={props.showItemDividers}
169171
selectionVariant={props.selectionVariant}
170172
{...itemProps}
171173
key={key}
172174
sx={{...itemStyle, ...itemProps.sx}}
173-
item={item}
174175
/>
175176
)
176177
}

src/stories/ActionList.stories.tsx

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
ArrowLeftIcon
1313
} from '@primer/octicons-react'
1414
import {Meta} from '@storybook/react'
15-
import React from 'react'
15+
import React, {forwardRef} from 'react'
1616
import styled from 'styled-components'
1717
import {Label, ThemeProvider} from '..'
1818
import {ActionList as _ActionList} from '../ActionList'
@@ -362,3 +362,59 @@ export function SizeStressTestingStory(): JSX.Element {
362362
)
363363
}
364364
SizeStressTestingStory.storyName = 'Size Stress Testing'
365+
366+
type ReactRouterLikeLinkProps = {to: string; children: React.ReactNode}
367+
const ReactRouterLikeLink = forwardRef<HTMLAnchorElement, ReactRouterLikeLinkProps>(
368+
({to, ...props}: {to: string; children: React.ReactNode}, ref) => {
369+
// eslint-disable-next-line jsx-a11y/anchor-has-content
370+
return <a ref={ref} href={to} {...props} />
371+
}
372+
)
373+
374+
const NextJSLikeLink = forwardRef(
375+
({href, children}: {href: string; children: React.ReactNode}, ref): React.ReactElement => {
376+
const child = React.Children.only(children)
377+
const childProps = {
378+
ref,
379+
href
380+
}
381+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
382+
// @ts-ignore
383+
return React.cloneElement(child, childProps)
384+
}
385+
)
386+
387+
export function LinkItemStory(): JSX.Element {
388+
return (
389+
<>
390+
<h1>Simple List</h1>
391+
<ErsatzOverlay>
392+
<ActionList
393+
items={[
394+
{
395+
text: 'A. Vanilla action',
396+
renderItem: props => <ActionList.Item onAction={() => alert('hi?')} {...props} />
397+
},
398+
{
399+
text: 'B. Vanilla link',
400+
renderItem: props => <ActionList.Item as="a" href="/about" {...props} />
401+
},
402+
{
403+
text: 'C. React Router link',
404+
renderItem: props => <ActionList.Item as={ReactRouterLikeLink} to="/about" {...props} />
405+
},
406+
{
407+
text: 'D. NextJS style',
408+
renderItem: props => (
409+
<NextJSLikeLink href="/about">
410+
<ActionList.Item as="a" {...props} />
411+
</NextJSLikeLink>
412+
)
413+
}
414+
]}
415+
/>
416+
</ErsatzOverlay>
417+
</>
418+
)
419+
}
420+
LinkItemStory.storyName = 'List with a link item'

0 commit comments

Comments
 (0)