-
Notifications
You must be signed in to change notification settings - Fork 526
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
492 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@primer/components': minor | ||
--- | ||
|
||
Adds a Radio component |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
--- | ||
title: Radio | ||
description: Use radios when a user needs to select one option from a list | ||
status: Alpha | ||
source: https://github.com/primer/react/blob/main/src/Radio.tsx | ||
storybook: '/react/storybook?path=/story/forms-radio-button--default' | ||
--- | ||
|
||
## Default example | ||
|
||
```jsx live | ||
<> | ||
<Radio value="one" name="radio-group-name" /> | ||
<Radio value="two" name="radio-group-name" /> | ||
<Radio disabled value="three" name="radio-group-name" /> | ||
</> | ||
``` | ||
|
||
<Note> | ||
Please use a [Checkbox](/Checkbox) if the user needs to select more than one option in a list | ||
</Note> | ||
<Note variant="warning"> | ||
Radio components should always be accompanied by a corresponding label to improve support for assistive technologies. | ||
</Note> | ||
|
||
## Grouping Radio components | ||
|
||
Use the `name` prop to group together related `Radio` components in a list. | ||
|
||
```jsx live | ||
<form> | ||
<Box sx={{p: 1, display: 'flex', alignItems: 'center'}}> | ||
<Radio id="radio-0" value="1" name="radio-example" /> | ||
<Text as="label" htmlFor="radio-0" sx={{fontSize: 1, marginLeft: 1}}> | ||
<Text sx={{display: 'block'}}>Radio 1</Text> | ||
</Text> | ||
</Box> | ||
<Box sx={{p: 1, display: 'flex', alignItems: 'center'}}> | ||
<Radio id="radio-1" value="2" name="radio-example" /> | ||
<Text as="label" htmlFor="radio-1" sx={{fontSize: 1, marginLeft: 1}}> | ||
<Text sx={{display: 'block'}}>Radio 2</Text> | ||
</Text> | ||
</Box> | ||
<Box sx={{p: 1, display: 'flex', alignItems: 'center'}}> | ||
<Radio id="radio-2" value="3" name="radio-example" /> | ||
<Text as="label" htmlFor="radio-2" sx={{fontSize: 1, marginLeft: 1}}> | ||
<Text sx={{display: 'block'}}>Radio 3</Text> | ||
</Text> | ||
</Box> | ||
<Box sx={{p: 1, display: 'flex', alignItems: 'center'}}> | ||
<Radio id="radio-3" value="4" name="radio-example" /> | ||
<Text as="label" htmlFor="radio-3" sx={{fontSize: 1, marginLeft: 1}}> | ||
<Text sx={{display: 'block'}}>Radio 4</Text> | ||
</Text> | ||
</Box> | ||
</form> | ||
``` | ||
|
||
## Component props | ||
|
||
Native `<input>` attributes are forwarded to the underlying React `input` component and are not listed below. | ||
|
||
| Name | Type | Default | Description | | ||
| :------------- | :---------- | :-------: | :--------------------------------------------------------------------------------------- | | ||
| value | String | undefined | Required. A unique value that is never shown to the user. | | ||
| name | String | undefined | Required. Used for grouping multiple radios | | ||
| checked | Boolean | undefined | Optional. Modifies true/false value of the native radio | | ||
| defaultChecked | Boolean | undefined | Optional. Selects the radio by default in uncontrolled mode | | ||
| onChange | ChangeEvent | undefined | Optional. A callback function that is triggered when the checked state has been changed. | | ||
| disabled | Boolean | undefined | Optional. Modifies the native disabled state of the native radio | | ||
|
||
## Component status | ||
|
||
<ComponentChecklist | ||
items={{ | ||
propsDocumented: true, | ||
noUnnecessaryDeps: true, | ||
adaptsToThemes: true, | ||
adaptsToScreenSizes: true, | ||
fullTestCoverage: true, | ||
usedInProduction: false, | ||
usageExamplesDocumented: false, | ||
designReviewed: false, | ||
a11yReviewed: false, | ||
stableApi: false, | ||
addressedApiFeedback: false, | ||
hasDesignGuidelines: false, | ||
hasFigmaComponent: false | ||
}} | ||
/> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import styled from 'styled-components' | ||
import React, {InputHTMLAttributes, ReactElement} from 'react' | ||
import sx, {SxProp} from './sx' | ||
|
||
export type RadioProps = { | ||
/** | ||
* A unique value that is never shown to the user. | ||
* Used during form submission and to identify which radio button in a group is selected | ||
*/ | ||
value: string | ||
/** | ||
* Name attribute of the input element. Required for grouping radio inputs | ||
*/ | ||
name: string | ||
/** | ||
* Apply inactive visual appearance to the radio button | ||
*/ | ||
disabled?: boolean | ||
/** | ||
* Indicates whether the radio button is selected | ||
*/ | ||
checked?: boolean | ||
/** | ||
* Forward a ref to the underlying input element | ||
*/ | ||
ref?: React.RefObject<HTMLInputElement> | ||
/** | ||
* Indicates whether the radio button must be checked before the form can be submitted | ||
*/ | ||
required?: boolean | ||
/** | ||
* Indicates whether the radio button validation state is non-standard | ||
*/ | ||
validationStatus?: 'error' | 'success' // TODO: hoist to Validation typings | ||
} & InputHTMLAttributes<HTMLInputElement> & | ||
SxProp | ||
|
||
const StyledRadio = styled.input` | ||
cursor: pointer; | ||
${props => props.disabled && `cursor: not-allowed;`} | ||
${sx} | ||
` | ||
|
||
/** | ||
* An accessible, native radio component for selecting one option from a list. | ||
*/ | ||
const Radio = React.forwardRef<HTMLInputElement, RadioProps>( | ||
( | ||
{checked, disabled, sx: sxProp, required, validationStatus, value, name, ...rest}: RadioProps, | ||
ref | ||
): ReactElement => { | ||
return ( | ||
<StyledRadio | ||
type="radio" | ||
value={value} | ||
name={name} | ||
ref={ref} | ||
disabled={disabled} | ||
aria-disabled={disabled ? 'true' : 'false'} | ||
checked={checked} | ||
aria-checked={checked ? 'true' : 'false'} | ||
required={required} | ||
aria-required={required ? 'true' : 'false'} | ||
aria-invalid={validationStatus === 'error' ? 'true' : 'false'} | ||
sx={sxProp} | ||
{...rest} | ||
/> | ||
) | ||
} | ||
) | ||
|
||
Radio.displayName = 'Radio' | ||
|
||
export default Radio |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
import React from 'react' | ||
import {Radio} from '..' | ||
import {behavesAsComponent, checkExports} from '../utils/testing' | ||
import {render, cleanup, fireEvent} from '@testing-library/react' | ||
import {toHaveNoViolations} from 'jest-axe' | ||
import 'babel-polyfill' | ||
import '@testing-library/jest-dom' | ||
|
||
expect.extend(toHaveNoViolations) | ||
|
||
describe('Radio', () => { | ||
const defaultProps = { | ||
name: 'mock', | ||
value: 'mock value' | ||
} | ||
|
||
beforeEach(() => { | ||
jest.resetAllMocks() | ||
cleanup() | ||
}) | ||
|
||
behavesAsComponent({Component: Radio}) | ||
|
||
checkExports('Radio', { | ||
default: Radio | ||
}) | ||
|
||
it('renders a valid radio input', () => { | ||
const {getByRole} = render(<Radio {...defaultProps} />) | ||
|
||
const radio = getByRole('radio') | ||
|
||
expect(radio).toBeDefined() | ||
}) | ||
|
||
it('renders an unchecked radio by default', () => { | ||
const {getByRole} = render(<Radio {...defaultProps} />) | ||
|
||
const radio = getByRole('radio') as HTMLInputElement | ||
|
||
expect(radio.checked).toEqual(false) | ||
}) | ||
|
||
it('accepts and applies value and name attributes', () => { | ||
const {getByRole} = render(<Radio {...defaultProps} />) | ||
|
||
const radio = getByRole('radio') as HTMLInputElement | ||
|
||
expect(radio).toHaveAttribute('name', defaultProps.name) | ||
expect(radio).toHaveAttribute('value', defaultProps.value) | ||
}) | ||
|
||
it('renders an active radio when checked attribute is passed', () => { | ||
const handleChange = jest.fn() | ||
const {getByRole} = render(<Radio {...defaultProps} checked onChange={handleChange} />) | ||
|
||
const radio = getByRole('radio') as HTMLInputElement | ||
|
||
expect(radio.checked).toEqual(true) | ||
}) | ||
|
||
it('accepts a change handler that can alter a single radio state', () => { | ||
const handleChange = jest.fn() | ||
const {getByRole} = render(<Radio {...defaultProps} onChange={handleChange} />) | ||
|
||
const radio = getByRole('radio') as HTMLInputElement | ||
|
||
expect(radio.checked).toEqual(false) | ||
|
||
fireEvent.click(radio) | ||
expect(handleChange).toHaveBeenCalled() | ||
expect(radio.checked).toEqual(true) | ||
}) | ||
|
||
it('renders correct behavior for multiple radio buttons in a group', () => { | ||
const handleChange = jest.fn() | ||
const RadioGroup = () => ( | ||
<form> | ||
<Radio {...defaultProps} value="radio-one" onChange={handleChange} /> | ||
<Radio {...defaultProps} value="radio-two" onChange={handleChange} /> | ||
</form> | ||
) | ||
const {getByDisplayValue} = render(<RadioGroup />) | ||
|
||
const radioOne = getByDisplayValue('radio-one') as HTMLInputElement | ||
const radioTwo = getByDisplayValue('radio-two') as HTMLInputElement | ||
|
||
expect(radioOne).not.toBeChecked() | ||
expect(radioTwo).not.toBeChecked() | ||
|
||
fireEvent.click(radioOne) | ||
|
||
expect(radioOne).toBeChecked() | ||
expect(radioTwo).not.toBeChecked() | ||
|
||
fireEvent.click(radioTwo) | ||
|
||
expect(radioOne).not.toBeChecked() | ||
expect(radioTwo).toBeChecked() | ||
}) | ||
|
||
it('renders an inactive radio state correctly', () => { | ||
const handleChange = jest.fn() | ||
const {getByRole, rerender} = render(<Radio {...defaultProps} disabled onChange={handleChange} />) | ||
|
||
const radio = getByRole('radio') as HTMLInputElement | ||
|
||
expect(radio.disabled).toEqual(true) | ||
expect(radio).not.toBeChecked() | ||
expect(radio).toHaveAttribute('aria-disabled', 'true') | ||
|
||
fireEvent.change(radio) | ||
|
||
expect(radio.disabled).toEqual(true) | ||
expect(radio).not.toBeChecked() | ||
expect(radio).toHaveAttribute('aria-disabled', 'true') | ||
|
||
// remove disabled attribute and retest | ||
rerender(<Radio {...defaultProps} onChange={handleChange} />) | ||
|
||
expect(radio).toHaveAttribute('aria-disabled', 'false') | ||
}) | ||
|
||
it('renders an uncontrolled component correctly', () => { | ||
const {getByRole} = render(<Radio {...defaultProps} defaultChecked />) | ||
|
||
const radio = getByRole('radio') as HTMLInputElement | ||
|
||
expect(radio.checked).toEqual(true) | ||
}) | ||
|
||
it('renders an aria-checked attribute correctly', () => { | ||
const handleChange = jest.fn() | ||
const {getByRole, rerender} = render(<Radio {...defaultProps} checked={false} onChange={handleChange} />) | ||
|
||
const radio = getByRole('radio') as HTMLInputElement | ||
|
||
expect(radio).toHaveAttribute('aria-checked', 'false') | ||
|
||
rerender(<Radio {...defaultProps} checked={true} onChange={handleChange} />) | ||
|
||
expect(radio).toHaveAttribute('aria-checked', 'true') | ||
}) | ||
|
||
it('renders an invalid aria state when validation prop indicates an error', () => { | ||
const handleChange = jest.fn() | ||
const {getByRole, rerender} = render(<Radio {...defaultProps} onChange={handleChange} />) | ||
|
||
const radio = getByRole('radio') as HTMLInputElement | ||
|
||
expect(radio).toHaveAttribute('aria-invalid', 'false') | ||
|
||
rerender(<Radio {...defaultProps} onChange={handleChange} validationStatus="success" />) | ||
|
||
expect(radio).toHaveAttribute('aria-invalid', 'false') | ||
|
||
rerender(<Radio {...defaultProps} onChange={handleChange} validationStatus="error" />) | ||
|
||
expect(radio).toHaveAttribute('aria-invalid', 'true') | ||
}) | ||
|
||
it('renders an aria state indicating the field is required', () => { | ||
const handleChange = jest.fn() | ||
const {getByRole, rerender} = render(<Radio {...defaultProps} onChange={handleChange} />) | ||
|
||
const radio = getByRole('radio') as HTMLInputElement | ||
|
||
expect(radio).toHaveAttribute('aria-required', 'false') | ||
|
||
rerender(<Radio {...defaultProps} onChange={handleChange} required />) | ||
|
||
expect(radio).toHaveAttribute('aria-required', 'true') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`Radio renders consistently 1`] = ` | ||
.c0 { | ||
cursor: pointer; | ||
} | ||
<input | ||
aria-checked="false" | ||
aria-disabled="false" | ||
aria-invalid="false" | ||
aria-required="false" | ||
className="c0" | ||
type="radio" | ||
/> | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.