Skip to content

Icon button fixes: Removes iconLabel and adds aria-label to the type #1945

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

Merged
merged 13 commits into from
Mar 21, 2022
Merged
5 changes: 5 additions & 0 deletions .changeset/large-owls-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": patch
---

Icon button fixes: Removes iconLabel and adds aria-label to the type
42 changes: 18 additions & 24 deletions docs/content/Button.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,58 @@
componentId: button
title: Button
status: Alpha
source: https://github.com/primer/react/tree/main/src/Button2
storybook: '/react/storybook?path=/story/composite-components-button2'
source: https://github.com/primer/react/tree/main/src/Button
storybook: '/react/storybook?path=/story/composite-components-button'
description: Use button for the main actions on a page or form.
---

import {Button, IconButton, LinkButton} from '@primer/react/drafts'
import {Button, IconButton, LinkButton} from '@primer/react'

## Usage

### Installation

```js
import {Button} from '@primer/react/drafts'
import {Button} from '@primer/react'
```

### Default button

This is the default variant for the `Button` component.

```jsx live drafts
```jsx live
<Button>Default</Button>
```

### Danger button

The `danger` variant of `Button` is used to warn users about potentially destructive actions

```jsx live drafts
```jsx live
<Button variant="danger">Danger</Button>
```

### Outline button

The `outline` variant of `Button` is typically used as a secondary button

```jsx live drafts
```jsx live
<Button variant="outline">Outline</Button>
```

### Invisible button

The `invisible` variant of `Button` indicates that the action is a low priority one.

```jsx live drafts
```jsx live
<Button variant="invisible">Invisible</Button>
```

### Different sized buttons

`Button` component supports three different sizes. `small`, `medium`, `large`.

```jsx live drafts
```jsx live
<>
<Button size="small">Search</Button>
<Button sx={{mt: 2}}>Search</Button>
Expand All @@ -68,7 +68,7 @@ The `invisible` variant of `Button` indicates that the action is a low priority
We can place an icon inside the `Button` in either the leading or the trailing position to enhance the visual context.
It is recommended to use an octicon here.

```jsx live drafts
```jsx live
<>
<Button leadingIcon={SearchIcon}>Search</Button>
<Button trailingIcon={SearchIcon} sx={{mt: 2}}>
Expand All @@ -84,25 +84,19 @@ It is recommended to use an octicon here.

A separate component called `IconButton` is used if the action shows only an icon with no text. This button will remain square in shape.

```jsx live drafts
<IconButton icon={SearchIcon}>Search</IconButton>
```jsx live
<IconButton aria-label="Search" icon={SearchIcon} />
```

### Different sized icon buttons

`IconButton` also supports the three different sizes. `small`, `medium`, `large`.

```jsx live drafts
```jsx live
<>
<IconButton icon={SearchIcon} size="small">
Search
</IconButton>
<IconButton sx={{ml: 2}} icon={SearchIcon}>
Search
</IconButton>
<IconButton sx={{ml: 2}} icon={SearchIcon} size="large">
Search
</IconButton>
<IconButton aria-label="Search" size="small" icon={SearchIcon} />
Copy link
Member

Choose a reason for hiding this comment

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

oops, my bad!

<IconButton aria-label="Search" icon={SearchIcon} sx={{ml: 2}} />
<IconButton aria-label="Search" size="large" icon={SearchIcon} sx={{ml: 2}} />
</>
```

Expand All @@ -112,7 +106,7 @@ A common use case for primer is a button with a counter component which shows th
We provide `Button.Counter` as a composite component which requires you to provide a number as child.
The counter will match the `variant` styles of the parent button.

```jsx live drafts
```jsx live
<Button>
Watch
<Button.Counter>1</Button.Counter>
Expand All @@ -124,7 +118,7 @@ The counter will match the `variant` styles of the parent button.
A button can be styled in an appropriate manner using the `sx` prop. This may be to change width, or to add margins etc.
Here's an example of a block button which takes 100% of available width. Checkout [styled-system](https://styled-system.com/) to see what you can send in an `sx` prop.

```jsx live drafts
```jsx live
<Button sx={{width: '100%'}}>Block</Button>
```

Expand Down
24 changes: 9 additions & 15 deletions docs/content/IconButton.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
title: IconButton
componentId: icon_button
status: Alpha
source: https://github.com/primer/react/tree/main/src/Button2
storybook: '/react/storybook?path=/story/composite-components-button2'
source: https://github.com/primer/react/tree/main/src/Button
storybook: '/react/storybook?path=/story/composite-components-button'
description: An accessible button component with no text and only icon.
---

Expand All @@ -12,32 +12,26 @@ description: An accessible button component with no text and only icon.
### Installation

```js
import {IconButton} from '@primer/react/drafts'
import {IconButton} from '@primer/react'
```

### Icon only button

A separate component called `IconButton` is used if the action shows only an icon with no text. This button will remain square in shape.

```jsx live drafts
<IconButton icon={SearchIcon}>Search</IconButton>
```jsx live
<IconButton aria-label="Search" icon={SearchIcon} />
```

### Different sized icon buttons

`IconButton` also supports the three different sizes. `small`, `medium`, `large`.

```jsx live drafts
```jsx live
<>
<IconButton icon={SearchIcon} size="small">
Search
</IconButton>
<IconButton sx={{ml: 2}} icon={SearchIcon}>
Search
</IconButton>
<IconButton sx={{ml: 2}} icon={SearchIcon} size="large">
Search
</IconButton>
<IconButton aria-label="Search" icon={SearchIcon} size="small" />
<IconButton aria-label="Search" sx={{ml: 2}} icon={SearchIcon} />
<IconButton aria-label="Search" sx={{ml: 2}} icon={SearchIcon} size="large" />
</>
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {BaseStyles, ThemeProvider} from '..'
import Box from '../Box'

export default {
title: 'Composite components/Button2',
title: 'Composite components/Button',

decorators: [
Story => {
Expand Down Expand Up @@ -75,19 +75,19 @@ export const iconButton = ({...args}: ButtonProps) => {
return (
<>
<Box mb={2}>
<IconButton icon={XIcon} iconLabel="Close" {...args} />
<IconButton icon={XIcon} aria-label="Close" {...args} />
</Box>
<Box mb={2}>
<IconButton icon={XIcon} iconLabel="Close" {...args} variant="invisible" sx={{mt: 2}} />
<IconButton icon={XIcon} aria-label="Close" {...args} variant="invisible" sx={{mt: 2}} />
</Box>
<Box mb={2}>
<IconButton icon={XIcon} iconLabel="Close" {...args} variant="danger" />
<IconButton icon={XIcon} aria-label="Close" {...args} variant="danger" />
</Box>
<Box mb={2}>
<IconButton icon={XIcon} iconLabel="Close" {...args} variant="primary" />
<IconButton icon={XIcon} aria-label="Close" {...args} variant="primary" />
</Box>
<Box mb={2}>
<IconButton icon={XIcon} iconLabel="Close" {...args} variant="outline" />
<IconButton icon={XIcon} aria-label="Close" {...args} variant="outline" />
</Box>
</>
)
Expand Down Expand Up @@ -193,7 +193,7 @@ export const DisabledButton = ({...args}: ButtonProps) => {
</Button>
</Box>
<Box mb={2}>
<IconButton disabled icon={() => <XIcon />} iconLabel="Close" {...args} />
<IconButton disabled icon={() => <XIcon />} aria-label="Close" {...args} />
</Box>
</>
)
Expand Down
11 changes: 2 additions & 9 deletions src/Button/IconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ import {useTheme} from '../ThemeProvider'
import Box from '../Box'
import {IconButtonProps, StyledButton} from './types'
import {getBaseStyles, getSizeStyles, getVariantStyles} from './styles'
import {useSSRSafeId} from '@react-aria/ssr'

const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, forwardedRef): JSX.Element => {
const {variant = 'default', size = 'medium', sx: sxProp = {}, icon: Icon, iconLabel, ...rest} = props
const iconLabelId = useSSRSafeId()
const {variant = 'default', size = 'medium', sx: sxProp = {}, icon: Icon, ...rest} = props
const {theme} = useTheme()
const sxStyles = merge.all([
getBaseStyles(theme),
Expand All @@ -17,12 +15,7 @@ const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, forwar
sxProp as SxProp
])
return (
<StyledButton aria-labelledby={iconLabelId} sx={sxStyles} {...rest} ref={forwardedRef}>
{iconLabel && (
<span id={iconLabelId} hidden={true}>
{iconLabel}
</span>
)}
<StyledButton sx={sxStyles} {...rest} ref={forwardedRef}>
<Box as="span" sx={{display: 'inline-block'}}>
<Icon />
</Box>
Expand Down
8 changes: 3 additions & 5 deletions src/Button/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export type Size = 'small' | 'medium' | 'large'

type StyledButtonProps = ComponentPropsWithRef<typeof StyledButton>

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

export type ButtonBaseProps = {
/**
* Determine's the styles on a button one of 'default' | 'primary' | 'invisible' | 'danger'
Expand Down Expand Up @@ -40,12 +42,8 @@ export type ButtonProps = {
children: React.ReactNode
} & ButtonBaseProps

export type IconButtonProps = {
/**
* This is to be used if it is an icon-only button. Will make text visually hidden
*/
export type IconButtonProps = ButtonA11yProps & {
icon: React.FunctionComponent<IconProps>
iconLabel: string
} & ButtonBaseProps

// adopted from React.AnchorHTMLAttributes
Expand Down
52 changes: 29 additions & 23 deletions src/__tests__/Button.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ expect.extend(toHaveNoViolations)
describe('Button', () => {
behavesAsComponent({Component: Button, options: {skipAs: true}})

it('renders a <button>', async () => {
it('renders a <button>', () => {
const container = render(<Button>Default</Button>)
const button = await container.findByRole('button')
const button = container.getByRole('button')
expect(button.textContent).toEqual('Default')
})

Expand All @@ -23,76 +23,82 @@ describe('Button', () => {
cleanup()
})

it('preserves "onClick" prop', async () => {
it('preserves "onClick" prop', () => {
const onClick = jest.fn()
const container = render(<Button onClick={onClick}>Noop</Button>)
const button = await container.findByRole('button')
const button = container.getByRole('button')
fireEvent.click(button)
expect(onClick).toHaveBeenCalledTimes(1)
})

it('respects width props', async () => {
it('respects width props', () => {
const container = render(<Button sx={{width: 200}}>Block</Button>)
const button = await container.findByRole('button')
const button = container.getByRole('button')
expect(button).toHaveStyleRule('width', '200px')
})

it('respects the "disabled" prop', async () => {
it('respects the "disabled" prop', () => {
const onClick = jest.fn()
const container = render(
<Button onClick={onClick} disabled>
Disabled
</Button>
)
const button = await container.findByRole('button')
const button = container.getByRole('button')
expect(button.hasAttribute('disabled')).toEqual(true)
fireEvent.click(button)
expect(onClick).toHaveBeenCalledTimes(0)
})

it('respects the "variant" prop', async () => {
it('respects the "variant" prop', () => {
const container = render(<Button size="small">Smol</Button>)
const button = await container.findByRole('button')
const button = container.getByRole('button')
expect(button).toHaveStyleRule('font-size', '12px')
})

it('respects the "fontSize" prop over the "variant" prop', async () => {
it('respects the "fontSize" prop over the "variant" prop', () => {
const container = render(
<Button size="small" sx={{fontSize: 20}}>
Big Smol
</Button>
)
const button = await container.findByRole('button')
const button = container.getByRole('button')
expect(button).toHaveStyleRule('font-size', '20px')
})

it('styles primary button appropriately', async () => {
it('styles primary button appropriately', () => {
const container = render(<Button variant="primary">Primary</Button>)
const button = await container.findByRole('button')
const button = container.getByRole('button')
expect(button).toMatchSnapshot()
})

it('styles invisible button appropriately', async () => {
it('styles invisible button appropriately', () => {
const container = render(<Button variant="invisible">Invisible</Button>)
const button = await container.findByRole('button')
const button = container.getByRole('button')
expect(button).toMatchSnapshot()
})

it('styles danger button appropriately', async () => {
it('styles danger button appropriately', () => {
const container = render(<Button variant="danger">Danger</Button>)
const button = await container.findByRole('button')
const button = container.getByRole('button')
expect(button).toMatchSnapshot()
})

it('styles outline button appropriately', async () => {
it('styles outline button appropriately', () => {
const container = render(<Button variant="outline">Outline</Button>)
const button = await container.findByRole('button')
const button = container.getByRole('button')
expect(button).toMatchSnapshot()
})

it('styles icon only button to make it a square', async () => {
const container = render(<IconButton icon={SearchIcon} iconLabel="Search icon only button" />)
const IconOnlyButton = await container.findByRole('button')
it('styles icon only button to make it a square', () => {
const container = render(<IconButton icon={SearchIcon} aria-label="Search button" />)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should have a separate test for the aria-label / aria-labelledby. thoughts?

also a suggestion for your matcher, to be more specific around looking for the aria-label attributes:

- findByRole('button')
+ getByRole('button', {name: 'Search button'} ) 

I don't think you need use an async matcher? 🤔

const IconOnlyButton = container.getByRole('button')
expect(IconOnlyButton).toHaveStyleRule('padding-right', '8px')
expect(IconOnlyButton).toMatchSnapshot()
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like aria attributes fall under logic, so I think it's worth adding a separate test for it like..

expect(IconOnlyButton).toHaveAttribute('aria-label', labelThingy)

})
it('makes sure icon button has an aria-label', () => {
const container = render(<IconButton icon={SearchIcon} aria-label="Search button" />)
const IconOnlyButton = container.getByLabelText('Search button')
expect(IconOnlyButton).toBeTruthy()
})
})
Loading