@primer/components 32.0.2-rc.859381a1 → 32.1.0-rc.6f5d2b00
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +5 -1
- package/dist/browser.esm.js +7 -1
- package/dist/browser.esm.js.map +1 -1
- package/dist/browser.umd.js +7 -1
- package/dist/browser.umd.js.map +1 -1
- package/docs/content/Checkbox.md +118 -0
- package/docs/src/@primer/gatsby-theme-doctocat/nav.yml +2 -0
- package/lib/Checkbox.d.ts +29 -0
- package/lib/Checkbox.js +64 -0
- package/lib/__tests__/Checkbox.test.d.ts +2 -0
- package/lib/__tests__/Checkbox.test.js +189 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +8 -0
- package/lib/stories/Checkbox.stories.js +227 -0
- package/lib-esm/Checkbox.d.ts +29 -0
- package/lib-esm/Checkbox.js +44 -0
- package/lib-esm/__tests__/Checkbox.test.d.ts +2 -0
- package/lib-esm/__tests__/Checkbox.test.js +169 -0
- package/lib-esm/index.d.ts +2 -0
- package/lib-esm/index.js +1 -0
- package/lib-esm/stories/Checkbox.stories.js +197 -0
- package/package.json +1 -1
- package/src/Checkbox.tsx +75 -0
- package/src/__tests__/Checkbox.test.tsx +155 -0
- package/src/__tests__/__snapshots__/Checkbox.test.tsx.snap +16 -0
- package/src/index.ts +3 -0
- package/src/stories/Checkbox.stories.tsx +164 -0
- package/stats.html +1 -1
@@ -0,0 +1,155 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
import {Checkbox} from '..'
|
3
|
+
import {behavesAsComponent, checkExports} from '../utils/testing'
|
4
|
+
import {render, cleanup} from '@testing-library/react'
|
5
|
+
import {toHaveNoViolations} from 'jest-axe'
|
6
|
+
import 'babel-polyfill'
|
7
|
+
import '@testing-library/jest-dom'
|
8
|
+
import userEvent from '@testing-library/user-event'
|
9
|
+
|
10
|
+
expect.extend(toHaveNoViolations)
|
11
|
+
|
12
|
+
describe('Checkbox', () => {
|
13
|
+
beforeEach(() => {
|
14
|
+
jest.resetAllMocks()
|
15
|
+
cleanup()
|
16
|
+
})
|
17
|
+
behavesAsComponent({Component: Checkbox})
|
18
|
+
|
19
|
+
checkExports('Checkbox', {
|
20
|
+
default: Checkbox
|
21
|
+
})
|
22
|
+
|
23
|
+
it('renders a valid checkbox input', () => {
|
24
|
+
const {getByRole} = render(<Checkbox />)
|
25
|
+
|
26
|
+
const checkbox = getByRole('checkbox')
|
27
|
+
|
28
|
+
expect(checkbox).toBeDefined()
|
29
|
+
})
|
30
|
+
|
31
|
+
it('renders an unchecked checkbox by default', () => {
|
32
|
+
const {getByRole} = render(<Checkbox />)
|
33
|
+
|
34
|
+
const checkbox = getByRole('checkbox') as HTMLInputElement
|
35
|
+
|
36
|
+
expect(checkbox.checked).toEqual(false)
|
37
|
+
})
|
38
|
+
|
39
|
+
it('renders an active checkbox when checked attribute is passed', () => {
|
40
|
+
const handleChange = jest.fn()
|
41
|
+
const {getByRole} = render(<Checkbox checked onChange={handleChange} />)
|
42
|
+
|
43
|
+
const checkbox = getByRole('checkbox') as HTMLInputElement
|
44
|
+
|
45
|
+
expect(checkbox.checked).toEqual(true)
|
46
|
+
})
|
47
|
+
|
48
|
+
it('accepts a change handler that can alter the checkbox state', () => {
|
49
|
+
const handleChange = jest.fn()
|
50
|
+
const {getByRole} = render(<Checkbox onChange={handleChange} />)
|
51
|
+
|
52
|
+
const checkbox = getByRole('checkbox') as HTMLInputElement
|
53
|
+
|
54
|
+
expect(checkbox.checked).toEqual(false)
|
55
|
+
|
56
|
+
userEvent.click(checkbox)
|
57
|
+
expect(handleChange).toHaveBeenCalled()
|
58
|
+
expect(checkbox.checked).toEqual(true)
|
59
|
+
|
60
|
+
userEvent.click(checkbox)
|
61
|
+
expect(handleChange).toHaveBeenCalled()
|
62
|
+
expect(checkbox.checked).toEqual(false)
|
63
|
+
})
|
64
|
+
|
65
|
+
it('renders an indeterminate prop correctly', () => {
|
66
|
+
const handleChange = jest.fn()
|
67
|
+
const {getByRole} = render(<Checkbox indeterminate checked onChange={handleChange} />)
|
68
|
+
|
69
|
+
const checkbox = getByRole('checkbox') as HTMLInputElement
|
70
|
+
|
71
|
+
expect(checkbox.indeterminate).toEqual(true)
|
72
|
+
expect(checkbox.checked).toEqual(false)
|
73
|
+
})
|
74
|
+
|
75
|
+
it('renders an inactive checkbox state correctly', () => {
|
76
|
+
const handleChange = jest.fn()
|
77
|
+
const {getByRole, rerender} = render(<Checkbox disabled onChange={handleChange} />)
|
78
|
+
|
79
|
+
const checkbox = getByRole('checkbox') as HTMLInputElement
|
80
|
+
|
81
|
+
expect(checkbox.disabled).toEqual(true)
|
82
|
+
expect(checkbox.checked).toEqual(false)
|
83
|
+
expect(checkbox).toHaveAttribute('aria-disabled', 'true')
|
84
|
+
|
85
|
+
userEvent.click(checkbox)
|
86
|
+
|
87
|
+
expect(checkbox.disabled).toEqual(true)
|
88
|
+
expect(checkbox.checked).toEqual(false)
|
89
|
+
expect(checkbox).toHaveAttribute('aria-disabled', 'true')
|
90
|
+
|
91
|
+
// remove disabled attribute and retest
|
92
|
+
rerender(<Checkbox onChange={handleChange} />)
|
93
|
+
|
94
|
+
expect(checkbox).toHaveAttribute('aria-disabled', 'false')
|
95
|
+
})
|
96
|
+
|
97
|
+
it('renders an uncontrolled component correctly', () => {
|
98
|
+
const {getByRole} = render(<Checkbox defaultChecked />)
|
99
|
+
|
100
|
+
const checkbox = getByRole('checkbox') as HTMLInputElement
|
101
|
+
|
102
|
+
expect(checkbox.checked).toEqual(true)
|
103
|
+
|
104
|
+
userEvent.click(checkbox)
|
105
|
+
|
106
|
+
expect(checkbox.checked).toEqual(false)
|
107
|
+
})
|
108
|
+
|
109
|
+
it('renders an aria-checked attribute correctly', () => {
|
110
|
+
const handleChange = jest.fn()
|
111
|
+
const {getByRole, rerender} = render(<Checkbox checked={false} onChange={handleChange} />)
|
112
|
+
|
113
|
+
const checkbox = getByRole('checkbox') as HTMLInputElement
|
114
|
+
|
115
|
+
expect(checkbox).toHaveAttribute('aria-checked', 'false')
|
116
|
+
|
117
|
+
rerender(<Checkbox checked={true} onChange={handleChange} />)
|
118
|
+
|
119
|
+
expect(checkbox).toHaveAttribute('aria-checked', 'true')
|
120
|
+
|
121
|
+
rerender(<Checkbox indeterminate checked onChange={handleChange} />)
|
122
|
+
|
123
|
+
expect(checkbox).toHaveAttribute('aria-checked', 'mixed')
|
124
|
+
})
|
125
|
+
|
126
|
+
it('renders an invalid aria state when validation prop indicates an error', () => {
|
127
|
+
const handleChange = jest.fn()
|
128
|
+
const {getByRole, rerender} = render(<Checkbox onChange={handleChange} />)
|
129
|
+
|
130
|
+
const checkbox = getByRole('checkbox') as HTMLInputElement
|
131
|
+
|
132
|
+
expect(checkbox).toHaveAttribute('aria-invalid', 'false')
|
133
|
+
|
134
|
+
rerender(<Checkbox onChange={handleChange} validationStatus="success" />)
|
135
|
+
|
136
|
+
expect(checkbox).toHaveAttribute('aria-invalid', 'false')
|
137
|
+
|
138
|
+
rerender(<Checkbox onChange={handleChange} validationStatus="error" />)
|
139
|
+
|
140
|
+
expect(checkbox).toHaveAttribute('aria-invalid', 'true')
|
141
|
+
})
|
142
|
+
|
143
|
+
it('renders an aria state indicating the field is required', () => {
|
144
|
+
const handleChange = jest.fn()
|
145
|
+
const {getByRole, rerender} = render(<Checkbox onChange={handleChange} />)
|
146
|
+
|
147
|
+
const checkbox = getByRole('checkbox') as HTMLInputElement
|
148
|
+
|
149
|
+
expect(checkbox).toHaveAttribute('aria-required', 'false')
|
150
|
+
|
151
|
+
rerender(<Checkbox onChange={handleChange} required />)
|
152
|
+
|
153
|
+
expect(checkbox).toHaveAttribute('aria-required', 'true')
|
154
|
+
})
|
155
|
+
})
|
@@ -0,0 +1,16 @@
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
2
|
+
|
3
|
+
exports[`Checkbox renders consistently 1`] = `
|
4
|
+
.c0 {
|
5
|
+
cursor: pointer;
|
6
|
+
}
|
7
|
+
|
8
|
+
<input
|
9
|
+
aria-checked="false"
|
10
|
+
aria-disabled="false"
|
11
|
+
aria-invalid="false"
|
12
|
+
aria-required="false"
|
13
|
+
className="c0"
|
14
|
+
type="checkbox"
|
15
|
+
/>
|
16
|
+
`;
|
package/src/index.ts
CHANGED
@@ -169,4 +169,7 @@ export type {TruncateProps} from './Truncate'
|
|
169
169
|
export {default as UnderlineNav} from './UnderlineNav'
|
170
170
|
export type {UnderlineNavProps, UnderlineNavLinkProps} from './UnderlineNav'
|
171
171
|
|
172
|
+
export {default as Checkbox} from './Checkbox'
|
173
|
+
export type {CheckboxProps} from './Checkbox'
|
174
|
+
|
172
175
|
export {SSRProvider, useSSRSafeId} from './utils/ssr'
|
@@ -0,0 +1,164 @@
|
|
1
|
+
import React, {useLayoutEffect, useRef, useState} from 'react'
|
2
|
+
import {Meta} from '@storybook/react'
|
3
|
+
import styled from 'styled-components'
|
4
|
+
|
5
|
+
import {BaseStyles, Box, Checkbox, CheckboxProps, Text, ThemeProvider} from '..'
|
6
|
+
import {action} from '@storybook/addon-actions'
|
7
|
+
import {COMMON, get} from '../constants'
|
8
|
+
|
9
|
+
export default {
|
10
|
+
title: 'Forms/Checkbox',
|
11
|
+
component: Checkbox,
|
12
|
+
decorators: [
|
13
|
+
Story => {
|
14
|
+
return (
|
15
|
+
<ThemeProvider>
|
16
|
+
<BaseStyles>
|
17
|
+
<Box paddingTop={5}>{Story()}</Box>
|
18
|
+
</BaseStyles>
|
19
|
+
</ThemeProvider>
|
20
|
+
)
|
21
|
+
}
|
22
|
+
],
|
23
|
+
argTypes: {
|
24
|
+
sx: {
|
25
|
+
table: {
|
26
|
+
disable: true
|
27
|
+
}
|
28
|
+
},
|
29
|
+
disabled: {
|
30
|
+
name: 'Disabled',
|
31
|
+
defaultValue: false,
|
32
|
+
control: {
|
33
|
+
type: 'boolean'
|
34
|
+
}
|
35
|
+
}
|
36
|
+
}
|
37
|
+
} as Meta
|
38
|
+
|
39
|
+
const StyledLabel = styled.label`
|
40
|
+
user-select: none;
|
41
|
+
font-weight: 600;
|
42
|
+
font-size: 14px;
|
43
|
+
line-height: 18px;
|
44
|
+
margin-left: 16px;
|
45
|
+
${COMMON}
|
46
|
+
`
|
47
|
+
|
48
|
+
const StyledSubLabel = styled(Text)`
|
49
|
+
color: ${get('colors.fg.muted')};
|
50
|
+
font-size: 13px;
|
51
|
+
${COMMON}
|
52
|
+
`
|
53
|
+
|
54
|
+
export const Default = (args: CheckboxProps) => {
|
55
|
+
const [isChecked, setChecked] = useState<boolean>(false)
|
56
|
+
|
57
|
+
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
58
|
+
setChecked(event.target.checked)
|
59
|
+
action('Change event triggered')
|
60
|
+
}
|
61
|
+
|
62
|
+
return (
|
63
|
+
<>
|
64
|
+
<Box as="form" sx={{p: 3, display: 'flex', alignItems: 'flex-start'}}>
|
65
|
+
<Checkbox id="controlled-checkbox" onChange={handleChange} checked={isChecked} {...args} />
|
66
|
+
<StyledLabel htmlFor="controlled-checkbox">
|
67
|
+
<Text sx={{display: 'block'}}>Default checkbox</Text>
|
68
|
+
<StyledSubLabel>controlled</StyledSubLabel>
|
69
|
+
</StyledLabel>
|
70
|
+
</Box>
|
71
|
+
<Box as="form" sx={{p: 3, display: 'flex', alignItems: 'flex-start'}}>
|
72
|
+
<Checkbox id="always-checked-checkbox" checked {...args} />
|
73
|
+
<StyledLabel htmlFor="always-checked-checkbox">
|
74
|
+
<Text sx={{display: 'block'}}>Always checked</Text>
|
75
|
+
<StyledSubLabel>checked="true"</StyledSubLabel>
|
76
|
+
</StyledLabel>
|
77
|
+
</Box>
|
78
|
+
<Box as="form" sx={{p: 3, display: 'flex', alignItems: 'flex-start'}}>
|
79
|
+
<Checkbox id="always-unchecked-checkbox" checked={false} {...args} />
|
80
|
+
<StyledLabel htmlFor="always-unchecked-checkbox">
|
81
|
+
<Text sx={{display: 'block'}}>Always unchecked</Text>
|
82
|
+
<StyledSubLabel>checked="false"</StyledSubLabel>
|
83
|
+
</StyledLabel>
|
84
|
+
</Box>
|
85
|
+
<Box as="form" sx={{p: 3, display: 'flex', alignItems: 'flex-start'}}>
|
86
|
+
<Checkbox id="disabled-checkbox" disabled checked={false} />
|
87
|
+
<StyledLabel htmlFor="disabled-checkbox">
|
88
|
+
<Text sx={{display: 'block'}}>Inactive</Text>
|
89
|
+
<StyledSubLabel>disabled="true"</StyledSubLabel>
|
90
|
+
</StyledLabel>
|
91
|
+
</Box>
|
92
|
+
</>
|
93
|
+
)
|
94
|
+
}
|
95
|
+
|
96
|
+
export const Uncontrolled = (args: CheckboxProps) => {
|
97
|
+
const checkboxRef = useRef<HTMLInputElement | null>(null)
|
98
|
+
|
99
|
+
useLayoutEffect(() => {
|
100
|
+
if (checkboxRef.current) {
|
101
|
+
checkboxRef.current.checked = true
|
102
|
+
}
|
103
|
+
}, [])
|
104
|
+
|
105
|
+
return (
|
106
|
+
<Box as="form" sx={{p: 3, display: 'flex', alignItems: 'flex-start'}}>
|
107
|
+
<Checkbox id="uncontrolled-checkbox" ref={checkboxRef} {...args} />
|
108
|
+
<StyledLabel htmlFor="uncontrolled-checkbox">
|
109
|
+
<Text sx={{display: 'block'}}>Uncontrolled checkbox</Text>
|
110
|
+
<StyledSubLabel>Checked by default</StyledSubLabel>
|
111
|
+
</StyledLabel>
|
112
|
+
</Box>
|
113
|
+
)
|
114
|
+
}
|
115
|
+
|
116
|
+
export const Indeterminate = (args: CheckboxProps) => {
|
117
|
+
const [checkboxes, setCheckboxes] = useState<boolean[]>([false, false, false, false])
|
118
|
+
|
119
|
+
const handleChange = (_: React.ChangeEvent<HTMLInputElement>, index: number) => {
|
120
|
+
const newCheckboxes = [...checkboxes]
|
121
|
+
newCheckboxes[index] = !checkboxes[index]
|
122
|
+
setCheckboxes(newCheckboxes)
|
123
|
+
}
|
124
|
+
|
125
|
+
const handleIndeterminateChange = () => {
|
126
|
+
if (checkboxes.every(checkbox => checkbox)) {
|
127
|
+
return setCheckboxes(checkboxes.map(() => false))
|
128
|
+
}
|
129
|
+
|
130
|
+
const newCheckboxes = checkboxes.map(() => true)
|
131
|
+
setCheckboxes(newCheckboxes)
|
132
|
+
}
|
133
|
+
|
134
|
+
return (
|
135
|
+
<>
|
136
|
+
<Box as="form" sx={{p: 3, display: 'flex', alignItems: 'flex-start'}}>
|
137
|
+
<Checkbox
|
138
|
+
id="indeterminate-checkbox"
|
139
|
+
checked={checkboxes.every(Boolean)}
|
140
|
+
onChange={handleIndeterminateChange}
|
141
|
+
indeterminate={!checkboxes.every(Boolean)}
|
142
|
+
/>
|
143
|
+
<StyledLabel htmlFor="controlled-checkbox">
|
144
|
+
<Text sx={{display: 'block'}}>Default checkbox</Text>
|
145
|
+
<StyledSubLabel>controlled</StyledSubLabel>
|
146
|
+
</StyledLabel>
|
147
|
+
</Box>
|
148
|
+
|
149
|
+
{checkboxes.map((field, index) => (
|
150
|
+
<Box key={`sub-checkbox-${index}`} as="form" sx={{p: 1, pl: 7, display: 'flex', alignItems: 'flex-start'}}>
|
151
|
+
<Checkbox
|
152
|
+
id={`sub-checkbox-${index}`}
|
153
|
+
checked={checkboxes[index]}
|
154
|
+
onChange={event => handleChange(event, index)}
|
155
|
+
{...args}
|
156
|
+
/>
|
157
|
+
<StyledLabel htmlFor={`sub-checkbox-${index}`}>
|
158
|
+
<Text sx={{display: 'block'}}>Checkbox {index + 1}</Text>
|
159
|
+
</StyledLabel>
|
160
|
+
</Box>
|
161
|
+
))}
|
162
|
+
</>
|
163
|
+
)
|
164
|
+
}
|