Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 677
Add Checkbox form component#1606
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
519563c2b1777a82271fcb7ac7b116726de3be89c8596a7d747b3d1f8ad350151abf80f0d70fa0095ea8c8292e725e231e0544d4144612049eed07598c9c9f4e0565699645fb1646d58876cb66a09c39e09ae898b35d3b3ff964d63f1f8b8File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@primer/components': minor | ||
| --- | ||
| Adds a new Checkbox form component |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| --- | ||
| title: Checkbox | ||
| description: Use checkboxes to toggle between checked and unchecked states in a list or as a standalone form field | ||
| status: Alpha | ||
| source: https://github.com/primer/react/blob/main/src/Checklist.tsx | ||
| storybook: '/react/storybook?path=/story/forms-checkbox--default' | ||
| --- | ||
| import {ComponentChecklist} from '../src/component-checklist' | ||
| ## Default example | ||
| The `Checkbox` component can be used in controlled and uncontrolled modes. | ||
| ```jsx live | ||
| <> | ||
| <Box as="form" sx={{p: 3, pt: 0, display: 'flex', alignItems: 'center'}}> | ||
| <Checkbox id="default-checkbox" /> | ||
| <Text as="label" htmlFor="default-checkbox" sx={{fontSize: 2, fontWeight: 'bold', marginLeft: 1}}> | ||
| Default checkbox | ||
| </Text> | ||
| </Box> | ||
| <Box as="form" sx={{p: 3, display: 'flex', alignItems: 'center'}}> | ||
| <Checkbox id="always-checked-checkbox" checked={true} /> | ||
| <Text as="label" htmlFor="always-checked-checkbox" sx={{fontSize: 2, fontWeight: 'bold', marginLeft: 1}}> | ||
| Always checked | ||
| </Text> | ||
| </Box> | ||
| <Box as="form" sx={{p: 3, display: 'flex', alignItems: 'center'}}> | ||
| <Checkbox id="always-unchecked-checkbox" checked={false} /> | ||
| <Text as="label" htmlFor="always-unchecked-checkbox" sx={{fontSize: 2, fontWeight: 'bold', marginLeft: 1}}> | ||
| Always unchecked | ||
| </Text> | ||
| </Box> | ||
| <Box as="form" sx={{p: 3, display: 'flex', alignItems: 'center'}}> | ||
| <Checkbox id="inactive-checkbox" checked={true} disabled /> | ||
| <Text as="label" htmlFor="inactive-checkbox" sx={{fontSize: 2, fontWeight: 'bold', marginLeft: 1}}> | ||
| Inactive | ||
| </Text> | ||
| </Box> | ||
| </> | ||
| ``` | ||
| <Note variant="warning"> | ||
| Checkbox components should always be accompanied by a corresponding label to improve support for assistive technologies. | ||
| </Note> | ||
| ## Indeterminate example | ||
| An `indeterminate` checkbox state should be used if the input value is neither true nor false. This can be useful in situations where you are required to display an incomplete state, or one that is dependent on other input selections to determine a value. | ||
| ```jsx live | ||
| <> | ||
| <Box as="form" sx={{p: 3, pt: 0, pb: 1, display: 'flex', alignItems: 'center'}}> | ||
| <Checkbox id="indeterminate-checkbox" onChange={() => {}} indeterminate={true} /> | ||
| <Text as="label" sx={{fontSize: 2, fontWeight: 'bold', marginLeft: 1}} htmlFor="controlled-checkbox"> | ||
| <Text sx={{display: 'block'}}>Default checkbox</Text> | ||
| </Text> | ||
| </Box> | ||
| <Box key={`sub-checkbox-0`} as="form" sx={{p: 1, pl: 6, display: 'flex', alignItems: 'center'}}> | ||
| <Checkbox id={`sub-checkbox-0`} checked={true} onChange={() => {}} /> | ||
| <Text as="label" sx={{fontSize: 2, fontWeight: 'bold', marginLeft: 1}} htmlFor={`sub-checkbox-0`}> | ||
| <Text sx={{display: 'block'}}>Checkbox 1</Text> | ||
| </Text> | ||
| </Box> | ||
| <Box key={`sub-checkbox-1`} as="form" sx={{p: 1, pl: 6, display: 'flex', alignItems: 'center'}}> | ||
| <Checkbox id={`sub-checkbox-1`} checked={false} onChange={() => {}} /> | ||
| <Text as="label" sx={{fontSize: 2, fontWeight: 'bold', marginLeft: 1}} htmlFor={`sub-checkbox-1`}> | ||
| <Text sx={{display: 'block'}}>Checkbox 2</Text> | ||
| </Text> | ||
| </Box> | ||
| <Box key={`sub-checkbox-2`} as="form" sx={{p: 1, pl: 6, display: 'flex', alignItems: 'center'}}> | ||
| <Checkbox id={`sub-checkbox-2`} checked={false} onChange={() => {}} /> | ||
| <Text as="label" sx={{fontSize: 2, fontWeight: 'bold', marginLeft: 1}} htmlFor={`sub-checkbox-2`}> | ||
| <Text sx={{display: 'block'}}>Checkbox 3</Text> | ||
| </Text> | ||
| </Box> | ||
| <Box key={`sub-checkbox-3`} as="form" sx={{p: 1, pl: 6, display: 'flex', alignItems: 'center'}}> | ||
| <Checkbox id={`sub-checkbox-3`} checked={false} onChange={() => {}} /> | ||
| <Text as="label" sx={{fontSize: 2, fontWeight: 'bold', marginLeft: 1}} htmlFor={`sub-checkbox-3`}> | ||
| <Text sx={{display: 'block'}}>Checkbox 4</Text> | ||
| </Text> | ||
| </Box> | ||
| </> | ||
| ``` | ||
| ## Component props | ||
| Native `<input>` attributes are forwarded to the underlying React `input` component and are not listed below. | ||
| | Name | Type | Default | Description | | ||
| | :------------- | :---------- | :-------: | :------------------------------------------------------------------------------------------------------------------------------------------------------ | | ||
| | checked | Boolean | undefined | Optional. Modifies true/false value of the native checkbox | | ||
| | defaultChecked | Boolean | undefined | Optional. Checks the input 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 checkbox | | ||
| | indeterminate | Boolean | undefined | Optional. Applies an [indeterminate](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#attr-indeterminate) state to the checkbox | | ||
| ## 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 | ||
| }} | ||
| /> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import styled from 'styled-components' | ||
| import {useProvidedRefOrCreate} from './hooks' | ||
| import React, {InputHTMLAttributes, ReactElement, useLayoutEffect} from 'react' | ||
| import sx, {SxProp} from './sx' | ||
| export type CheckboxProps = { | ||
| /** | ||
| * Apply indeterminate visual appearance to the checkbox | ||
| */ | ||
| indeterminate?: boolean | ||
| /** | ||
| * Apply inactive visual appearance to the checkbox | ||
| */ | ||
| disabled?: boolean | ||
| /** | ||
| * Forward a ref to the underlying input element | ||
| */ | ||
| ref?: React.RefObject<HTMLInputElement> | ||
| /** | ||
| * Indicates whether the checkbox must be checked | ||
| */ | ||
| required?: boolean | ||
| /** | ||
| * Indicates whether the checkbox validation state | ||
| */ | ||
| validationStatus?: 'error' | 'success' // TODO: hoist to Validation typings | ||
| } & InputHTMLAttributes<HTMLInputElement> & | ||
| SxProp | ||
| const StyledCheckbox = styled.input` | ||
| cursor: pointer; | ||
| ${props => props.disabled && `cursor: not-allowed;`} | ||
| ${sx} | ||
| ` | ||
| /** | ||
| * An accessible, native checkbox component | ||
| */ | ||
| const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>( | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need to use ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's there to make the checkbox more flexible when used in uncontrolled mode. | ||
| ( | ||
| {checked, indeterminate, disabled, sx: sxProp, required, validationStatus, ...rest}: CheckboxProps, | ||
| ref | ||
| ): ReactElement => { | ||
| const checkboxRef = useProvidedRefOrCreate(ref as React.RefObject<HTMLInputElement>) | ||
| useLayoutEffect(() => { | ||
| if (checkboxRef.current) { | ||
| checkboxRef.current.indeterminate = indeterminate || false | ||
| } | ||
| }, [indeterminate, checked, checkboxRef]) | ||
| return ( | ||
| <StyledCheckbox | ||
| type="checkbox" | ||
| disabled={disabled} | ||
| aria-disabled={disabled ? 'true' : 'false'} | ||
| ref={ref || checkboxRef} | ||
| checked={indeterminate ? false : checked} | ||
| aria-checked={indeterminate ? 'mixed' : checked ? 'true' : 'false'} | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice | ||
| sx={sxProp} | ||
| required={required} | ||
| aria-required={required ? 'true' : 'false'} | ||
| aria-invalid={validationStatus === 'error' ? 'true' : 'false'} | ||
| {...rest} | ||
| /> | ||
| ) | ||
| } | ||
| ) | ||
| Checkbox.displayName = 'Checkbox' | ||
| export default Checkbox | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| import React from 'react' | ||
| import {Checkbox} from '..' | ||
| import {behavesAsComponent, checkExports} from '../utils/testing' | ||
| import {render, cleanup} from '@testing-library/react' | ||
| import {toHaveNoViolations} from 'jest-axe' | ||
| import 'babel-polyfill' | ||
| import '@testing-library/jest-dom' | ||
| import userEvent from '@testing-library/user-event' | ||
| expect.extend(toHaveNoViolations) | ||
| describe('Checkbox', () => { | ||
| beforeEach(() => { | ||
| jest.resetAllMocks() | ||
| cleanup() | ||
| }) | ||
| behavesAsComponent({Component: Checkbox}) | ||
| checkExports('Checkbox', { | ||
| default: Checkbox | ||
| }) | ||
| it('renders a valid checkbox input', () => { | ||
| const {getByRole} = render(<Checkbox />) | ||
| const checkbox = getByRole('checkbox') | ||
| expect(checkbox).toBeDefined() | ||
| }) | ||
| it('renders an unchecked checkbox by default', () => { | ||
| const {getByRole} = render(<Checkbox />) | ||
| const checkbox = getByRole('checkbox') as HTMLInputElement | ||
| expect(checkbox.checked).toEqual(false) | ||
| }) | ||
| it('renders an active checkbox when checked attribute is passed', () => { | ||
| const handleChange = jest.fn() | ||
| const {getByRole} = render(<Checkbox checked onChange={handleChange} />) | ||
| const checkbox = getByRole('checkbox') as HTMLInputElement | ||
| expect(checkbox.checked).toEqual(true) | ||
| }) | ||
| it('accepts a change handler that can alter the checkbox state', () => { | ||
| const handleChange = jest.fn() | ||
| const {getByRole} = render(<Checkbox onChange={handleChange} />) | ||
| const checkbox = getByRole('checkbox') as HTMLInputElement | ||
| expect(checkbox.checked).toEqual(false) | ||
| userEvent.click(checkbox) | ||
| expect(handleChange).toHaveBeenCalled() | ||
| expect(checkbox.checked).toEqual(true) | ||
| userEvent.click(checkbox) | ||
| expect(handleChange).toHaveBeenCalled() | ||
| expect(checkbox.checked).toEqual(false) | ||
| }) | ||
| it('renders an indeterminate prop correctly', () => { | ||
| const handleChange = jest.fn() | ||
| const {getByRole} = render(<Checkbox indeterminate checked onChange={handleChange} />) | ||
| const checkbox = getByRole('checkbox') as HTMLInputElement | ||
| expect(checkbox.indeterminate).toEqual(true) | ||
| expect(checkbox.checked).toEqual(false) | ||
| }) | ||
| it('renders an inactive checkbox state correctly', () => { | ||
| const handleChange = jest.fn() | ||
| const {getByRole, rerender} = render(<Checkbox disabled onChange={handleChange} />) | ||
| const checkbox = getByRole('checkbox') as HTMLInputElement | ||
| expect(checkbox.disabled).toEqual(true) | ||
| expect(checkbox.checked).toEqual(false) | ||
| expect(checkbox).toHaveAttribute('aria-disabled', 'true') | ||
| userEvent.click(checkbox) | ||
| expect(checkbox.disabled).toEqual(true) | ||
| expect(checkbox.checked).toEqual(false) | ||
| expect(checkbox).toHaveAttribute('aria-disabled', 'true') | ||
| // remove disabled attribute and retest | ||
| rerender(<Checkbox onChange={handleChange} />) | ||
| expect(checkbox).toHaveAttribute('aria-disabled', 'false') | ||
| }) | ||
| it('renders an uncontrolled component correctly', () => { | ||
| const {getByRole} = render(<Checkbox defaultChecked />) | ||
| const checkbox = getByRole('checkbox') as HTMLInputElement | ||
| expect(checkbox.checked).toEqual(true) | ||
| userEvent.click(checkbox) | ||
| expect(checkbox.checked).toEqual(false) | ||
| }) | ||
| it('renders an aria-checked attribute correctly', () => { | ||
| const handleChange = jest.fn() | ||
| const {getByRole, rerender} = render(<Checkbox checked={false} onChange={handleChange} />) | ||
| const checkbox = getByRole('checkbox') as HTMLInputElement | ||
| expect(checkbox).toHaveAttribute('aria-checked', 'false') | ||
| rerender(<Checkbox checked={true} onChange={handleChange} />) | ||
| expect(checkbox).toHaveAttribute('aria-checked', 'true') | ||
| rerender(<Checkbox indeterminate checked onChange={handleChange} />) | ||
| expect(checkbox).toHaveAttribute('aria-checked', 'mixed') | ||
| }) | ||
| it('renders an invalid aria state when validation prop indicates an error', () => { | ||
| const handleChange = jest.fn() | ||
| const {getByRole, rerender} = render(<Checkbox onChange={handleChange} />) | ||
| const checkbox = getByRole('checkbox') as HTMLInputElement | ||
| expect(checkbox).toHaveAttribute('aria-invalid', 'false') | ||
| rerender(<Checkbox onChange={handleChange} validationStatus="success" />) | ||
| expect(checkbox).toHaveAttribute('aria-invalid', 'false') | ||
| rerender(<Checkbox onChange={handleChange} validationStatus="error" />) | ||
| expect(checkbox).toHaveAttribute('aria-invalid', 'true') | ||
| }) | ||
| it('renders an aria state indicating the field is required', () => { | ||
| const handleChange = jest.fn() | ||
| const {getByRole, rerender} = render(<Checkbox onChange={handleChange} />) | ||
| const checkbox = getByRole('checkbox') as HTMLInputElement | ||
| expect(checkbox).toHaveAttribute('aria-required', 'false') | ||
| rerender(<Checkbox onChange={handleChange} required />) | ||
| expect(checkbox).toHaveAttribute('aria-required', 'true') | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| // Jest Snapshot v1, https://goo.gl/fbAQLP | ||
| exports[`Checkbox renders consistently 1`] = ` | ||
| .c0 { | ||
| cursor: pointer; | ||
| } | ||
| <input | ||
| aria-checked="false" | ||
| aria-disabled="false" | ||
| aria-invalid="false" | ||
| aria-required="false" | ||
| className="c0" | ||
| type="checkbox" | ||
| /> | ||
| `; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -169,4 +169,7 @@ export type {TruncateProps} from './Truncate' | ||
| export {default as UnderlineNav} from './UnderlineNav' | ||
| export type {UnderlineNavProps, UnderlineNavLinkProps} from './UnderlineNav' | ||
| export {default as Checkbox} from './Checkbox' | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any ideas how we could guard against engineers rendering a I have some ideas, but it would be much easier to show over Zoom instead of text and pseudocode. Let me know if you want to chat on Monday. Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Chatted with @rezrah about this: we're just going to check for an associated label with a linter instead of doing anything in the runtime. | ||
| export type {CheckboxProps} from './Checkbox' | ||
| export {SSRProvider, useSSRSafeId} from './utils/ssr' | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.