Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cuddly-rules-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

update types for button extensions
3 changes: 2 additions & 1 deletion src/ActionList/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {ActionListGroupProps, GroupContext} from './Group'
import {ActionListProps, ListContext} from './List'
import {Selection} from './Selection'
import {ActionListItemProps, Slots, TEXT_ROW_HEIGHT, getVariantStyles} from './shared'
import {defaultSxProp} from '../utils/defaultSxProp'

const LiBox = styled.li<SxProp>(sx)

Expand All @@ -21,7 +22,7 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>(
selected = undefined,
active = false,
onSelect,
sx: sxProp = {},
sx: sxProp = defaultSxProp,
id,
role,
_PrivateItemWrapper,
Expand Down
3 changes: 2 additions & 1 deletion src/ActionList/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import styled from 'styled-components'
import sx, {SxProp, merge} from '../sx'
import {AriaRole} from '../utils/types'
import {ActionListContainerContext} from './ActionListContainerContext'
import {defaultSxProp} from '../utils/defaultSxProp'

export type ActionListProps = {
/**
Expand Down Expand Up @@ -31,7 +32,7 @@ const ListBox = styled.ul<SxProp>(sx)

export const List = React.forwardRef<HTMLUListElement, ActionListProps>(
(
{variant = 'inset', selectionVariant, showDividers = false, role, sx: sxProp = {}, ...props},
{variant = 'inset', selectionVariant, showDividers = false, role, sx: sxProp = defaultSxProp, ...props},
forwardedRef,
): JSX.Element => {
const styles = {
Expand Down
48 changes: 23 additions & 25 deletions src/ActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {ActionListContainerContext} from './ActionList/ActionListContainerContex
import {Button, ButtonProps} from './Button'
import {MandateProps} from './utils/types'
import {merge, BetterSystemStyleObject} from './sx'
import {defaultSxProp} from './utils/defaultSxProp'
import {ForwardRefComponent as PolymorphicForwardRefComponent} from './utils/polymorphic'

export type MenuContextProps = Pick<
AnchoredOverlayProps,
Expand Down Expand Up @@ -68,34 +70,30 @@ const Menu: React.FC<React.PropsWithChildren<ActionMenuProps>> = ({
}

export type ActionMenuAnchorProps = {children: React.ReactElement}
const Anchor = React.forwardRef<AnchoredOverlayProps['anchorRef'], ActionMenuAnchorProps>(
({children, ...anchorProps}, anchorRef) => {
return React.cloneElement(children, {...anchorProps, ref: anchorRef})
},
)
const Anchor = React.forwardRef<HTMLElement, ActionMenuAnchorProps>(({children, ...anchorProps}, anchorRef) => {
return React.cloneElement(children, {...anchorProps, ref: anchorRef})
})

/** this component is syntactical sugar 🍭 */
export type ActionMenuButtonProps = ButtonProps
const MenuButton = React.forwardRef<AnchoredOverlayProps['anchorRef'], ButtonProps>(
({sx: sxProp = {}, ...props}, anchorRef) => {
return (
<Anchor ref={anchorRef}>
<Button
type="button"
trailingIcon={TriangleDownIcon}
sx={merge<BetterSystemStyleObject>(
{
// override the margin on caret for optical alignment
'[data-component=trailingIcon]': {marginX: -1},
},
sxProp,
)}
{...props}
/>
</Anchor>
)
},
)
const MenuButton = React.forwardRef(({sx: sxProp = defaultSxProp, ...props}, anchorRef) => {
return (
<Anchor ref={anchorRef}>
<Button
type="button"
trailingIcon={TriangleDownIcon}
sx={merge<BetterSystemStyleObject>(
{
// override the margin on caret for optical alignment
'[data-component=trailingIcon]': {marginX: -1},
},
sxProp,
)}
{...props}
/>
</Anchor>
)
}) as PolymorphicForwardRefComponent<'button', ActionMenuButtonProps>

type MenuOverlayProps = Partial<OverlayProps> &
Pick<AnchoredOverlayProps, 'align'> & {
Expand Down
7 changes: 2 additions & 5 deletions src/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@ import {ButtonProps} from './types'
import {ButtonBase} from './ButtonBase'
import {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'

const ButtonComponent: PolymorphicForwardRefComponent<'button', ButtonProps> = forwardRef<
HTMLButtonElement,
ButtonProps
>(({children, ...props}, forwardedRef): JSX.Element => {
const ButtonComponent = forwardRef(({children, ...props}, forwardedRef): JSX.Element => {
return (
<ButtonBase ref={forwardedRef} as="button" {...props}>
{children}
</ButtonBase>
)
})
}) as PolymorphicForwardRefComponent<'button', ButtonProps>

ButtonComponent.displayName = 'Button'

Expand Down
18 changes: 10 additions & 8 deletions src/Button/ButtonBase.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import React, {ComponentPropsWithRef, forwardRef, useMemo} from 'react'
import {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'
import Box from '../Box'
import {merge, SxProp} from '../sx'
import {BetterSystemStyleObject, merge} from '../sx'
import {useTheme} from '../ThemeProvider'
import {ButtonProps, StyledButton} from './types'
import {getVariantStyles, getSizeStyles, getButtonStyles} from './styles'
import {useRefObjectAsForwardedRef} from '../hooks/useRefObjectAsForwardedRef'
declare let __DEV__: boolean
import {defaultSxProp} from '../utils/defaultSxProp'

const defaultSxProp = {}
const iconWrapStyles = {
display: 'inline-block',
}
Expand All @@ -17,18 +16,18 @@ const trailingIconStyles = {
ml: 2,
}

const ButtonBase = forwardRef<HTMLElement, ButtonProps>(
const ButtonBase = forwardRef(
({children, as: Component = 'button', sx: sxProp = defaultSxProp, ...props}, forwardedRef): JSX.Element => {
const {leadingIcon: LeadingIcon, trailingIcon: TrailingIcon, variant = 'default', size = 'medium', ...rest} = props
const innerRef = React.useRef<HTMLElement>(null)
const innerRef = React.useRef<HTMLButtonElement>(null)
useRefObjectAsForwardedRef(forwardedRef, innerRef)

const {theme} = useTheme()
const baseStyles = useMemo(() => {
return merge.all([getButtonStyles(theme), getSizeStyles(size, variant, false), getVariantStyles(variant, theme)])
}, [theme, size, variant])
const sxStyles = useMemo(() => {
return merge(baseStyles, sxProp as SxProp)
const sxStyles: BetterSystemStyleObject = useMemo(() => {
return merge<BetterSystemStyleObject>(baseStyles, sxProp)
}, [baseStyles, sxProp])

if (__DEV__) {
Expand All @@ -40,7 +39,10 @@ const ButtonBase = forwardRef<HTMLElement, ButtonProps>(
*/
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(() => {
if (!(innerRef.current instanceof HTMLButtonElement) && !(innerRef.current instanceof HTMLAnchorElement)) {
if (
!(innerRef.current instanceof HTMLButtonElement) &&
!((innerRef.current as unknown) instanceof HTMLAnchorElement)
) {
// eslint-disable-next-line no-console
console.warn('This component should be an instanceof a semantic button or anchor')
}
Expand Down
3 changes: 2 additions & 1 deletion src/Button/ButtonCounter.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React from 'react'
import {SxProp} from '../sx'
import CounterLabel from '../CounterLabel'
import {defaultSxProp} from '../utils/defaultSxProp'

export type CounterProps = {
children: number
} & SxProp

const Counter = ({children, sx: sxProp = {}, ...props}: CounterProps) => {
const Counter = ({children, sx: sxProp = defaultSxProp, ...props}: CounterProps) => {
return (
<CounterLabel data-component="ButtonCounter" sx={{ml: 2, ...sxProp}} {...props}>
{children}
Expand Down
10 changes: 6 additions & 4 deletions src/Button/IconButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from 'react'
import React, {ComponentProps} from 'react'
import {EyeClosedIcon, EyeIcon, SearchIcon, XIcon, HeartIcon} from '@primer/octicons-react'
import {Story, Meta} from '@storybook/react'
import {IconButton} from '.'
import {OcticonArgType} from '../utils/story-helpers'

export default {
const meta: Meta<ComponentProps<typeof IconButton>> = {
title: 'Components/IconButton',
argTypes: {
size: {
Expand Down Expand Up @@ -33,6 +33,8 @@ export default {
'aria-label': 'Icon button description',
icon: XIcon,
},
} as Meta<typeof IconButton>
}

export const Playground: Story<typeof IconButton> = args => <IconButton {...args} />
export default meta

export const Playground: Story<ComponentProps<typeof IconButton>> = args => <IconButton {...args} />
15 changes: 11 additions & 4 deletions src/Button/IconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import {useTheme} from '../ThemeProvider'
import Box from '../Box'
import {IconButtonProps, StyledButton} from './types'
import {getBaseStyles, getSizeStyles, getVariantStyles} from './styles'
import {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'
import {defaultSxProp} from '../utils/defaultSxProp'

const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, forwardedRef): JSX.Element => {
const {variant = 'default', size = 'medium', sx: sxProp = {}, icon: Icon, ...rest} = props
const IconButton = forwardRef((props, forwardedRef): JSX.Element => {
const {variant = 'default', size = 'medium', sx: sxProp = defaultSxProp, icon: Icon, ...rest} = props
const {theme} = useTheme()
const sxStyles = merge.all([
getBaseStyles(theme),
Expand All @@ -15,12 +17,17 @@ const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, forwar
sxProp as SxProp,
])
return (
<StyledButton sx={sxStyles} {...rest} ref={forwardedRef}>
<StyledButton
sx={sxStyles}
{...rest}
// @ts-expect-error StyledButton wants both Anchor and Button refs, not one or the other
ref={forwardedRef}
>
<Box as="span" sx={{display: 'inline-block'}}>
<Icon />
</Box>
</StyledButton>
)
})
}) as PolymorphicForwardRefComponent<'button' | 'a', IconButtonProps>

export {IconButton}
15 changes: 11 additions & 4 deletions src/Button/LinkButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import {merge, SxProp} from '../sx'
import {LinkButtonProps} from './types'
import {ButtonBase, ButtonBaseProps} from './ButtonBase'
import {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'
import {defaultSxProp} from '../utils/defaultSxProp'

type MyProps = LinkButtonProps & ButtonBaseProps

const LinkButton = forwardRef<HTMLElement, MyProps>(
({children, as: Component = 'a', sx = {}, ...props}, forwardedRef): JSX.Element => {
const LinkButton = forwardRef(
({children, as: Component = 'a', sx = defaultSxProp, ...props}, forwardedRef): JSX.Element => {
const style = {
width: 'fit-content',
'&:hover:not([disabled])': {
Expand All @@ -23,11 +24,17 @@ const LinkButton = forwardRef<HTMLElement, MyProps>(
}
const sxStyle = merge.all([style, sx as SxProp])
return (
<ButtonBase as={Component} ref={forwardedRef} sx={sxStyle} {...props}>
<ButtonBase
as={Component}
// @ts-expect-error ButtonBase wants both Anchor and Button refs
ref={forwardedRef}
sx={sxStyle}
{...props}
>
{children}
</ButtonBase>
)
},
) as PolymorphicForwardRefComponent<'a', ButtonBaseProps>
) as PolymorphicForwardRefComponent<'a', MyProps>

export {LinkButton}
20 changes: 7 additions & 13 deletions src/Button/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {ComponentPropsWithRef} from 'react'
import React from 'react'
import styled from 'styled-components'
import {IconProps} from '@primer/octicons-react'
import sx, {SxProp} from '../sx'
Expand All @@ -13,14 +13,9 @@ export type VariantType = 'default' | 'primary' | 'invisible' | 'danger' | 'outl

export type Size = 'small' | 'medium' | 'large'

/**
* Remove styled-components polymorphic as prop, which conflicts with radix's
*/
type StyledButtonProps = Omit<ComponentPropsWithRef<typeof StyledButton>, 'as'>

type ButtonA11yProps =
| {'aria-label': string; 'aria-labelledby'?: never}
| {'aria-label'?: never; 'aria-labelledby': string}
| {'aria-label': string; 'aria-labelledby'?: undefined}
| {'aria-label'?: undefined; 'aria-labelledby': string}

export type ButtonBaseProps = {
/**
Expand All @@ -36,24 +31,23 @@ export type ButtonBaseProps = {
*/
disabled?: boolean
} & SxProp &
React.ButtonHTMLAttributes<HTMLButtonElement> &
StyledButtonProps
React.ButtonHTMLAttributes<HTMLButtonElement>

export type ButtonProps = {
/**
* The leading icon comes before button content
*/
leadingIcon?: React.FunctionComponent<React.PropsWithChildren<IconProps>>
leadingIcon?: React.FunctionComponent<React.PropsWithChildren<IconProps>> | null | undefined
/**
* The trailing icon comes after button content
*/
trailingIcon?: React.FunctionComponent<React.PropsWithChildren<IconProps>>
trailingIcon?: React.FunctionComponent<React.PropsWithChildren<IconProps>> | null | undefined
children: React.ReactNode
} & ButtonBaseProps

export type IconButtonProps = ButtonA11yProps & {
icon: React.FunctionComponent<React.PropsWithChildren<IconProps>>
} & ButtonBaseProps
} & Omit<ButtonBaseProps, 'aria-label' | 'aria-labelledby'>

// adopted from React.AnchorHTMLAttributes
export type LinkButtonProps = {
Expand Down
7 changes: 4 additions & 3 deletions src/NavList/NavList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import Box from '../Box'
import StyledOcticon from '../StyledOcticon'
import sx, {merge, SxProp} from '../sx'
import {defaultSxProp} from '../utils/defaultSxProp'

// ----------------------------------------------------------------------------
// NavList
Expand Down Expand Up @@ -43,7 +44,7 @@ export type NavListItemProps = {
} & SxProp

const Item = React.forwardRef<HTMLAnchorElement, NavListItemProps>(
({'aria-current': ariaCurrent, children, sx: sxProp = {}, ...props}, ref) => {
({'aria-current': ariaCurrent, children, sx: sxProp = defaultSxProp, ...props}, ref) => {
const {depth} = React.useContext(SubNavContext)

// Get SubNav from children
Expand Down Expand Up @@ -102,7 +103,7 @@ const ItemWithSubNavContext = React.createContext<{buttonId: string; subNavId: s

// TODO: ref prop
// TODO: Animate open/close transition
function ItemWithSubNav({children, subNav, sx: sxProp = {}}: ItemWithSubNavProps) {
function ItemWithSubNav({children, subNav, sx: sxProp = defaultSxProp}: ItemWithSubNavProps) {
const buttonId = useSSRSafeId()
const subNavId = useSSRSafeId()
const [isOpen, setIsOpen] = React.useState(false)
Expand Down Expand Up @@ -167,7 +168,7 @@ const SubNavContext = React.createContext<{depth: number}>({depth: 0})

// TODO: ref prop
// NOTE: SubNav must be a direct child of an Item
const SubNav = ({children, sx: sxProp = {}}: NavListSubNavProps) => {
const SubNav = ({children, sx: sxProp = defaultSxProp}: NavListSubNavProps) => {
const {buttonId, subNavId, isOpen} = React.useContext(ItemWithSubNavContext)
const {depth} = React.useContext(SubNavContext)

Expand Down
4 changes: 2 additions & 2 deletions src/PageHeader/PageHeader.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ const Template: Story = args => (
}}
>
<PageHeader.LeadingAction hidden={!args.hasLeadingAction}>
<IconButton icon={SidebarExpandIcon} variant="invisible" />{' '}
<IconButton aria-label="Expand" icon={SidebarExpandIcon} variant="invisible" />{' '}
</PageHeader.LeadingAction>
<PageHeader.LeadingVisual hidden={!args.hasLeadingVisual}>{<args.LeadingVisual />}</PageHeader.LeadingVisual>
<PageHeader.Title as={args['Title.as']} hidden={!args.hasTitle}>
Expand All @@ -227,7 +227,7 @@ const Template: Story = args => (
<Label>Beta</Label>
</PageHeader.TrailingVisual>
<PageHeader.TrailingAction hidden={!args.hasTrailingAction}>
<IconButton icon={PencilIcon} variant="invisible" />
<IconButton aria-label="Edit" icon={PencilIcon} variant="invisible" />
</PageHeader.TrailingAction>
<PageHeader.Actions hidden={!args.hasActions}>
<Hidden on={['narrow']}>
Expand Down
Loading