@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.
@@ -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=&quot;true&quot;</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=&quot;false&quot;</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=&quot;true&quot;</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
+ }