@charcoal-ui/react 2.0.0-alpha.0 → 2.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_lib/compat.d.ts +2 -3
- package/dist/_lib/compat.d.ts.map +1 -1
- package/dist/components/Radio/index.story.d.ts +4 -2
- package/dist/components/Radio/index.story.d.ts.map +1 -1
- package/dist/components/Select/context.d.ts +14 -0
- package/dist/components/Select/context.d.ts.map +1 -0
- package/dist/components/Select/index.d.ts +24 -0
- package/dist/components/Select/index.d.ts.map +1 -0
- package/dist/components/Select/index.story.d.ts +75 -0
- package/dist/components/Select/index.story.d.ts.map +1 -0
- package/dist/components/Select/index.test.d.ts +2 -0
- package/dist/components/Select/index.test.d.ts.map +1 -0
- package/dist/components/TextField/index.d.ts.map +1 -1
- package/dist/components/TextField/index.story.d.ts +9 -3
- package/dist/components/TextField/index.story.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.modern.js +76 -31
- package/dist/index.modern.js.map +1 -1
- package/dist/index.module.js +1 -1
- package/dist/index.module.js.map +1 -1
- package/dist/styled.d.ts +2 -0
- package/dist/styled.d.ts.map +1 -1
- package/package.json +11 -6
- package/src/_lib/compat.ts +1 -4
- package/src/components/Select/context.ts +23 -0
- package/src/components/Select/index.story.tsx +153 -0
- package/src/components/Select/index.test.tsx +281 -0
- package/src/components/Select/index.tsx +210 -0
- package/src/components/TextField/index.tsx +26 -10
- package/src/components/a11y.test.tsx +2 -2
- package/src/index.ts +6 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { ThemeProvider } from 'styled-components'
|
|
4
|
+
import { default as Select, SelectGroup } from '.'
|
|
5
|
+
import { light } from '@charcoal-ui/theme'
|
|
6
|
+
|
|
7
|
+
describe('Select', () => {
|
|
8
|
+
describe('in development mode', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
process.env.NODE_ENV = 'development'
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('when `<Select />` is used without `<SelectGroup />`', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
// eslint-disable-next-line no-console
|
|
16
|
+
console.error = jest.fn()
|
|
17
|
+
|
|
18
|
+
render(
|
|
19
|
+
<ThemeProvider theme={light}>
|
|
20
|
+
<Select value="a" />
|
|
21
|
+
</ThemeProvider>
|
|
22
|
+
)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('emits error message', () => {
|
|
26
|
+
// eslint-disable-next-line no-console
|
|
27
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
28
|
+
expect.stringMatching(
|
|
29
|
+
/Perhaps you forgot to wrap with <SelectGroup>/u
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('none of the options selected', () => {
|
|
37
|
+
let option1: HTMLInputElement
|
|
38
|
+
let option2: HTMLInputElement
|
|
39
|
+
let option3: HTMLInputElement
|
|
40
|
+
let allOptions: HTMLInputElement[]
|
|
41
|
+
let parent: HTMLDivElement
|
|
42
|
+
const childOnChange = jest.fn()
|
|
43
|
+
const parentOnChange = jest.fn()
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
render(
|
|
47
|
+
<TestComponent
|
|
48
|
+
selected={[]}
|
|
49
|
+
childOnChange={childOnChange}
|
|
50
|
+
parentOnChange={parentOnChange}
|
|
51
|
+
/>
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
option1 = screen.getByDisplayValue('option1')
|
|
55
|
+
option2 = screen.getByDisplayValue('option2')
|
|
56
|
+
option3 = screen.getByDisplayValue('option3')
|
|
57
|
+
allOptions = [option1, option2, option3]
|
|
58
|
+
parent = screen.getByTestId('SelectGroup')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('options have correct name', () => {
|
|
62
|
+
allOptions.forEach((element) => expect(element.name).toBe('defaultName'))
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('parent have correct aria-label', () => {
|
|
66
|
+
expect(parent.getAttribute('aria-label')).toBe('defaultAriaLabel')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('none of the options are selected', () => {
|
|
70
|
+
allOptions.forEach((element) => expect(element.checked).toBeFalsy())
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('selecting option1', () => {
|
|
74
|
+
it('childOnChange is called', () => {
|
|
75
|
+
fireEvent.click(option1)
|
|
76
|
+
expect(childOnChange).toHaveBeenCalledWith({
|
|
77
|
+
value: 'option1',
|
|
78
|
+
selected: true,
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('parentOnChange is called', () => {
|
|
83
|
+
fireEvent.click(option1)
|
|
84
|
+
expect(parentOnChange).toHaveBeenCalledWith(['option1'])
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('option2 is selected', () => {
|
|
90
|
+
let option1: HTMLInputElement
|
|
91
|
+
let option2: HTMLInputElement
|
|
92
|
+
let option3: HTMLInputElement
|
|
93
|
+
const childOnChange = jest.fn()
|
|
94
|
+
const parentOnChange = jest.fn()
|
|
95
|
+
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
render(
|
|
98
|
+
<TestComponent
|
|
99
|
+
selected={['option2']}
|
|
100
|
+
childOnChange={childOnChange}
|
|
101
|
+
parentOnChange={parentOnChange}
|
|
102
|
+
/>
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
option1 = screen.getByDisplayValue('option1')
|
|
106
|
+
option2 = screen.getByDisplayValue('option2')
|
|
107
|
+
option3 = screen.getByDisplayValue('option3')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('only option2 is selected', () => {
|
|
111
|
+
expect(option2.checked).toBeTruthy()
|
|
112
|
+
;[option1, option3].forEach((element) =>
|
|
113
|
+
expect(element.checked).toBeFalsy()
|
|
114
|
+
)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('selecting option1', () => {
|
|
118
|
+
it('parentOnChange is called', () => {
|
|
119
|
+
fireEvent.click(option1)
|
|
120
|
+
expect(parentOnChange).toHaveBeenCalledWith(['option2', 'option1'])
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('de-selecting option2', () => {
|
|
125
|
+
it('childOnChange is called', () => {
|
|
126
|
+
fireEvent.click(option2)
|
|
127
|
+
expect(childOnChange).toHaveBeenCalledWith({
|
|
128
|
+
value: 'option2',
|
|
129
|
+
selected: false,
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('parentOnChange is called', () => {
|
|
134
|
+
fireEvent.click(option2)
|
|
135
|
+
expect(parentOnChange).toHaveBeenCalledWith([])
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('the group is disabled', () => {
|
|
141
|
+
let option1: HTMLInputElement
|
|
142
|
+
let option2: HTMLInputElement
|
|
143
|
+
let option3: HTMLInputElement
|
|
144
|
+
let allOptions: HTMLInputElement[]
|
|
145
|
+
|
|
146
|
+
beforeEach(() => {
|
|
147
|
+
render(<TestComponent selected={['option1']} parentDisabled={true} />)
|
|
148
|
+
|
|
149
|
+
option1 = screen.getByDisplayValue('option1')
|
|
150
|
+
option2 = screen.getByDisplayValue('option2')
|
|
151
|
+
option3 = screen.getByDisplayValue('option3')
|
|
152
|
+
allOptions = [option1, option2, option3]
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('all the options are disabled', () => {
|
|
156
|
+
allOptions.forEach((element) => expect(element.disabled).toBeTruthy())
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('the group is readonly', () => {
|
|
161
|
+
let option1: HTMLInputElement
|
|
162
|
+
let option2: HTMLInputElement
|
|
163
|
+
let option3: HTMLInputElement
|
|
164
|
+
let allOptions: HTMLInputElement[]
|
|
165
|
+
|
|
166
|
+
beforeEach(() => {
|
|
167
|
+
render(<TestComponent selected={['option1']} readonly={true} />)
|
|
168
|
+
|
|
169
|
+
option1 = screen.getByDisplayValue('option1')
|
|
170
|
+
option2 = screen.getByDisplayValue('option2')
|
|
171
|
+
option3 = screen.getByDisplayValue('option3')
|
|
172
|
+
allOptions = [option1, option2, option3]
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('all the options are disabled', () => {
|
|
176
|
+
allOptions.forEach((element) => expect(element.disabled).toBeTruthy())
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
describe('the group has error', () => {
|
|
181
|
+
let option1: HTMLInputElement
|
|
182
|
+
let option2: HTMLInputElement
|
|
183
|
+
let option3: HTMLInputElement
|
|
184
|
+
let allOptions: HTMLInputElement[]
|
|
185
|
+
|
|
186
|
+
beforeEach(() => {
|
|
187
|
+
render(<TestComponent selected={['option1']} hasError={true} />)
|
|
188
|
+
|
|
189
|
+
option1 = screen.getByDisplayValue('option1')
|
|
190
|
+
option2 = screen.getByDisplayValue('option2')
|
|
191
|
+
option3 = screen.getByDisplayValue('option3')
|
|
192
|
+
allOptions = [option1, option2, option3]
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('all the options have `aria-invalid="true"`', () => {
|
|
196
|
+
allOptions.forEach((element) =>
|
|
197
|
+
expect(element.getAttribute('aria-invalid')).toBeTruthy()
|
|
198
|
+
)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('option1 is force checked', () => {
|
|
203
|
+
let option1: HTMLInputElement
|
|
204
|
+
|
|
205
|
+
beforeEach(() => {
|
|
206
|
+
render(<TestComponent selected={[]} firstOptionForceChecked={true} />)
|
|
207
|
+
|
|
208
|
+
option1 = screen.getByDisplayValue('option1')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('option1 is force checked', () => {
|
|
212
|
+
expect(option1.checked).toBeTruthy()
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
describe('option1 is disabled', () => {
|
|
217
|
+
let option1: HTMLInputElement
|
|
218
|
+
let option2: HTMLInputElement
|
|
219
|
+
|
|
220
|
+
beforeEach(() => {
|
|
221
|
+
render(<TestComponent selected={[]} firstOptionDisabled={true} />)
|
|
222
|
+
|
|
223
|
+
option1 = screen.getByDisplayValue('option1')
|
|
224
|
+
option2 = screen.getByDisplayValue('option2')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('only option1 is disabled', () => {
|
|
228
|
+
expect(option1.disabled).toBeTruthy()
|
|
229
|
+
expect(option2.disabled).toBeFalsy()
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
const TestComponent = ({
|
|
235
|
+
selected,
|
|
236
|
+
parentOnChange = () => {
|
|
237
|
+
return
|
|
238
|
+
},
|
|
239
|
+
childOnChange,
|
|
240
|
+
parentDisabled = false,
|
|
241
|
+
readonly = false,
|
|
242
|
+
hasError = false,
|
|
243
|
+
firstOptionForceChecked = false,
|
|
244
|
+
firstOptionDisabled = false,
|
|
245
|
+
}: {
|
|
246
|
+
selected: string[]
|
|
247
|
+
parentOnChange?: (selected: string[]) => void
|
|
248
|
+
childOnChange?: (payload: { value: string; selected: boolean }) => void
|
|
249
|
+
parentDisabled?: boolean
|
|
250
|
+
readonly?: boolean
|
|
251
|
+
hasError?: boolean
|
|
252
|
+
firstOptionForceChecked?: boolean
|
|
253
|
+
firstOptionDisabled?: boolean
|
|
254
|
+
}) => {
|
|
255
|
+
return (
|
|
256
|
+
<ThemeProvider theme={light}>
|
|
257
|
+
<SelectGroup
|
|
258
|
+
name="defaultName"
|
|
259
|
+
ariaLabel="defaultAriaLabel"
|
|
260
|
+
disabled={parentDisabled}
|
|
261
|
+
onChange={parentOnChange}
|
|
262
|
+
{...{ selected, readonly, hasError }}
|
|
263
|
+
>
|
|
264
|
+
<Select
|
|
265
|
+
value="option1"
|
|
266
|
+
disabled={firstOptionDisabled}
|
|
267
|
+
forceChecked={firstOptionForceChecked}
|
|
268
|
+
onChange={childOnChange}
|
|
269
|
+
>
|
|
270
|
+
Option 1
|
|
271
|
+
</Select>
|
|
272
|
+
<Select value="option2" onChange={childOnChange}>
|
|
273
|
+
Option 2
|
|
274
|
+
</Select>
|
|
275
|
+
<Select value="option3" onChange={childOnChange}>
|
|
276
|
+
Option 3
|
|
277
|
+
</Select>
|
|
278
|
+
</SelectGroup>
|
|
279
|
+
</ThemeProvider>
|
|
280
|
+
)
|
|
281
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import React, { ChangeEvent, useCallback, useContext } from 'react'
|
|
2
|
+
import styled, { css } from 'styled-components'
|
|
3
|
+
import warning from 'warning'
|
|
4
|
+
import { theme } from '../../styled'
|
|
5
|
+
import { disabledSelector, px } from '@charcoal-ui/utils'
|
|
6
|
+
|
|
7
|
+
import { SelectGroupContext } from './context'
|
|
8
|
+
|
|
9
|
+
export type SelectProps = React.PropsWithChildren<{
|
|
10
|
+
value: string
|
|
11
|
+
forceChecked?: boolean
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
variant?: 'default' | 'overlay'
|
|
14
|
+
onChange?: (payload: { value: string; selected: boolean }) => void
|
|
15
|
+
}>
|
|
16
|
+
|
|
17
|
+
export default function Select({
|
|
18
|
+
value,
|
|
19
|
+
forceChecked = false,
|
|
20
|
+
disabled = false,
|
|
21
|
+
onChange,
|
|
22
|
+
variant = 'default',
|
|
23
|
+
children,
|
|
24
|
+
}: SelectProps) {
|
|
25
|
+
const {
|
|
26
|
+
name,
|
|
27
|
+
selected,
|
|
28
|
+
disabled: parentDisabled,
|
|
29
|
+
readonly,
|
|
30
|
+
hasError,
|
|
31
|
+
onChange: parentOnChange,
|
|
32
|
+
} = useContext(SelectGroupContext)
|
|
33
|
+
|
|
34
|
+
warning(
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
36
|
+
name !== undefined,
|
|
37
|
+
`"name" is not Provided for <Select>. Perhaps you forgot to wrap with <SelectGroup> ?`
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const isSelected = selected.includes(value) || forceChecked
|
|
41
|
+
const isDisabled = disabled || parentDisabled || readonly
|
|
42
|
+
|
|
43
|
+
const handleChange = useCallback(
|
|
44
|
+
(event: ChangeEvent<HTMLInputElement>) => {
|
|
45
|
+
if (!(event.currentTarget instanceof HTMLInputElement)) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
if (onChange) onChange({ value, selected: event.currentTarget.checked })
|
|
49
|
+
parentOnChange({ value, selected: event.currentTarget.checked })
|
|
50
|
+
},
|
|
51
|
+
[onChange, parentOnChange, value]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<SelectRoot aria-disabled={isDisabled}>
|
|
56
|
+
<SelectInput
|
|
57
|
+
{...{
|
|
58
|
+
name,
|
|
59
|
+
value,
|
|
60
|
+
hasError,
|
|
61
|
+
}}
|
|
62
|
+
checked={isSelected}
|
|
63
|
+
disabled={isDisabled}
|
|
64
|
+
onChange={handleChange}
|
|
65
|
+
overlay={variant === 'overlay'}
|
|
66
|
+
aria-invalid={hasError}
|
|
67
|
+
/>
|
|
68
|
+
<SelectInputOverlay
|
|
69
|
+
overlay={variant === 'overlay'}
|
|
70
|
+
hasError={hasError}
|
|
71
|
+
aria-hidden={true}
|
|
72
|
+
>
|
|
73
|
+
<pixiv-icon name="24/Check" unsafe-non-guideline-scale={16 / 24} />
|
|
74
|
+
</SelectInputOverlay>
|
|
75
|
+
{Boolean(children) && <SelectLabel>{children}</SelectLabel>}
|
|
76
|
+
</SelectRoot>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const SelectRoot = styled.label`
|
|
81
|
+
display: grid;
|
|
82
|
+
grid-template-columns: auto 1fr;
|
|
83
|
+
align-items: center;
|
|
84
|
+
position: relative;
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
${disabledSelector} {
|
|
87
|
+
cursor: default;
|
|
88
|
+
}
|
|
89
|
+
gap: ${({ theme }) => px(theme.spacing[4])};
|
|
90
|
+
${theme((o) => o.disabled)}
|
|
91
|
+
`
|
|
92
|
+
|
|
93
|
+
const SelectLabel = styled.div`
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
${theme((o) => [o.typography(14), o.font.text1])}
|
|
97
|
+
`
|
|
98
|
+
|
|
99
|
+
const SelectInput = styled.input.attrs({ type: 'checkbox' })<{
|
|
100
|
+
hasError: boolean
|
|
101
|
+
overlay: boolean
|
|
102
|
+
}>`
|
|
103
|
+
&[type='checkbox'] {
|
|
104
|
+
appearance: none;
|
|
105
|
+
display: block;
|
|
106
|
+
width: 20px;
|
|
107
|
+
height: 20px;
|
|
108
|
+
margin: 0;
|
|
109
|
+
|
|
110
|
+
&:checked {
|
|
111
|
+
${theme((o) => o.bg.brand.hover.press)}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
${({ hasError, overlay }) =>
|
|
115
|
+
theme((o) => [
|
|
116
|
+
o.bg.text3.hover.press,
|
|
117
|
+
o.borderRadius('oval'),
|
|
118
|
+
hasError && !overlay && o.outline.assertive,
|
|
119
|
+
overlay && o.bg.surface4,
|
|
120
|
+
])};
|
|
121
|
+
}
|
|
122
|
+
`
|
|
123
|
+
|
|
124
|
+
const SelectInputOverlay = styled.div<{ overlay: boolean; hasError: boolean }>`
|
|
125
|
+
position: absolute;
|
|
126
|
+
top: -2px;
|
|
127
|
+
left: -2px;
|
|
128
|
+
box-sizing: border-box;
|
|
129
|
+
display: flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
justify-content: center;
|
|
132
|
+
|
|
133
|
+
${({ hasError, overlay }) =>
|
|
134
|
+
theme((o) => [
|
|
135
|
+
o.width.px(24),
|
|
136
|
+
o.height.px(24),
|
|
137
|
+
o.borderRadius('oval'),
|
|
138
|
+
o.font.text5,
|
|
139
|
+
hasError && overlay && o.outline.assertive,
|
|
140
|
+
])}
|
|
141
|
+
|
|
142
|
+
${({ overlay }) =>
|
|
143
|
+
overlay &&
|
|
144
|
+
css`
|
|
145
|
+
border-color: ${({ theme }) => theme.color.text5};
|
|
146
|
+
border-width: 2px;
|
|
147
|
+
border-style: solid;
|
|
148
|
+
`}
|
|
149
|
+
`
|
|
150
|
+
|
|
151
|
+
export type SelectGroupProps = React.PropsWithChildren<{
|
|
152
|
+
className?: string
|
|
153
|
+
name: string
|
|
154
|
+
ariaLabel: string
|
|
155
|
+
selected: string[]
|
|
156
|
+
onChange: (selected: string[]) => void
|
|
157
|
+
disabled?: boolean
|
|
158
|
+
readonly?: boolean
|
|
159
|
+
hasError?: boolean
|
|
160
|
+
}>
|
|
161
|
+
|
|
162
|
+
export function SelectGroup({
|
|
163
|
+
className,
|
|
164
|
+
name,
|
|
165
|
+
ariaLabel,
|
|
166
|
+
selected,
|
|
167
|
+
onChange,
|
|
168
|
+
disabled = false,
|
|
169
|
+
readonly = false,
|
|
170
|
+
hasError = false,
|
|
171
|
+
children,
|
|
172
|
+
}: SelectGroupProps) {
|
|
173
|
+
const handleChange = useCallback(
|
|
174
|
+
(payload: { value: string; selected: boolean }) => {
|
|
175
|
+
const index = selected.indexOf(payload.value)
|
|
176
|
+
|
|
177
|
+
if (payload.selected) {
|
|
178
|
+
if (index < 0) {
|
|
179
|
+
onChange([...selected, payload.value])
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
if (index >= 0) {
|
|
183
|
+
onChange([...selected.slice(0, index), ...selected.slice(index + 1)])
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
[onChange, selected]
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<SelectGroupContext.Provider
|
|
192
|
+
value={{
|
|
193
|
+
name,
|
|
194
|
+
selected: Array.from(new Set(selected)),
|
|
195
|
+
disabled,
|
|
196
|
+
readonly,
|
|
197
|
+
hasError,
|
|
198
|
+
onChange: handleChange,
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
<div
|
|
202
|
+
className={className}
|
|
203
|
+
aria-label={ariaLabel}
|
|
204
|
+
data-testid="SelectGroup"
|
|
205
|
+
>
|
|
206
|
+
{children}
|
|
207
|
+
</div>
|
|
208
|
+
</SelectGroupContext.Provider>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
@@ -60,8 +60,10 @@ function mergeRefs<T>(...refs: React.Ref<T>[]): React.RefCallback<T> {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function
|
|
64
|
-
|
|
63
|
+
function countCodePointsInString(string: string) {
|
|
64
|
+
// [...string] とするとproduction buildで動かなくなる
|
|
65
|
+
// cf. https://twitter.com/f_subal/status/1497214727511891972
|
|
66
|
+
return Array.from(string).length
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
const TextField = React.forwardRef<TextFieldElement, TextFieldProps>(
|
|
@@ -100,22 +102,29 @@ const SingleLineTextField = React.forwardRef<
|
|
|
100
102
|
const ariaRef = useRef<HTMLInputElement>(null)
|
|
101
103
|
const prefixRef = useRef<HTMLSpanElement>(null)
|
|
102
104
|
const suffixRef = useRef<HTMLSpanElement>(null)
|
|
103
|
-
const [count, setCount] = useState(
|
|
105
|
+
const [count, setCount] = useState(countCodePointsInString(props.value ?? ''))
|
|
104
106
|
const [prefixWidth, setPrefixWidth] = useState(0)
|
|
105
107
|
const [suffixWidth, setSuffixWidth] = useState(0)
|
|
106
108
|
|
|
109
|
+
const nonControlled = props.value === undefined
|
|
107
110
|
const handleChange = useCallback(
|
|
108
111
|
(value: string) => {
|
|
109
|
-
const count =
|
|
112
|
+
const count = countCodePointsInString(value)
|
|
110
113
|
if (maxLength !== undefined && count > maxLength) {
|
|
111
114
|
return
|
|
112
115
|
}
|
|
113
|
-
|
|
116
|
+
if (nonControlled) {
|
|
117
|
+
setCount(count)
|
|
118
|
+
}
|
|
114
119
|
onChange?.(value)
|
|
115
120
|
},
|
|
116
|
-
[maxLength, onChange]
|
|
121
|
+
[maxLength, nonControlled, onChange]
|
|
117
122
|
)
|
|
118
123
|
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
setCount(countCodePointsInString(props.value ?? ''))
|
|
126
|
+
}, [props.value])
|
|
127
|
+
|
|
119
128
|
const { inputProps, labelProps, descriptionProps, errorMessageProps } =
|
|
120
129
|
useTextField(
|
|
121
130
|
{
|
|
@@ -217,7 +226,7 @@ const MultiLineTextField = React.forwardRef<
|
|
|
217
226
|
const { visuallyHiddenProps } = useVisuallyHidden()
|
|
218
227
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
219
228
|
const ariaRef = useRef<HTMLTextAreaElement>(null)
|
|
220
|
-
const [count, setCount] = useState(
|
|
229
|
+
const [count, setCount] = useState(countCodePointsInString(props.value ?? ''))
|
|
221
230
|
const [rows, setRows] = useState(initialRows)
|
|
222
231
|
|
|
223
232
|
const syncHeight = useCallback(
|
|
@@ -230,21 +239,28 @@ const MultiLineTextField = React.forwardRef<
|
|
|
230
239
|
[initialRows]
|
|
231
240
|
)
|
|
232
241
|
|
|
242
|
+
const nonControlled = props.value === undefined
|
|
233
243
|
const handleChange = useCallback(
|
|
234
244
|
(value: string) => {
|
|
235
|
-
const count =
|
|
245
|
+
const count = countCodePointsInString(value)
|
|
236
246
|
if (maxLength !== undefined && count > maxLength) {
|
|
237
247
|
return
|
|
238
248
|
}
|
|
239
|
-
|
|
249
|
+
if (nonControlled) {
|
|
250
|
+
setCount(count)
|
|
251
|
+
}
|
|
240
252
|
if (autoHeight && textareaRef.current !== null) {
|
|
241
253
|
syncHeight(textareaRef.current)
|
|
242
254
|
}
|
|
243
255
|
onChange?.(value)
|
|
244
256
|
},
|
|
245
|
-
[autoHeight, maxLength, onChange, syncHeight]
|
|
257
|
+
[autoHeight, maxLength, nonControlled, onChange, syncHeight]
|
|
246
258
|
)
|
|
247
259
|
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
setCount(countCodePointsInString(props.value ?? ''))
|
|
262
|
+
}, [props.value])
|
|
263
|
+
|
|
248
264
|
const { inputProps, labelProps, descriptionProps, errorMessageProps } =
|
|
249
265
|
useTextField(
|
|
250
266
|
{
|
|
@@ -69,13 +69,13 @@ describe.each(themes)('using %s theme', (_name, theme) => {
|
|
|
69
69
|
describe.each(links)('using %s component', (_name, link) => {
|
|
70
70
|
describe.each(stories)(
|
|
71
71
|
'storiesOf($filename).add($name)',
|
|
72
|
-
({ story, args }) => {
|
|
72
|
+
({ story: Story, args }) => {
|
|
73
73
|
it('has no accessibility violations', async () => {
|
|
74
74
|
expect(() => {
|
|
75
75
|
render(
|
|
76
76
|
<ThemeProvider theme={theme}>
|
|
77
77
|
<ComponentAbstraction components={{ Link: link }}>
|
|
78
|
-
{
|
|
78
|
+
<Story {...args} />
|
|
79
79
|
</ComponentAbstraction>
|
|
80
80
|
</ThemeProvider>
|
|
81
81
|
)
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,12 @@ export {
|
|
|
19
19
|
RadioGroup,
|
|
20
20
|
type RadioGroupProps,
|
|
21
21
|
} from './components/Radio'
|
|
22
|
+
export {
|
|
23
|
+
default as Select,
|
|
24
|
+
type SelectProps,
|
|
25
|
+
SelectGroup,
|
|
26
|
+
type SelectGroupProps,
|
|
27
|
+
} from './components/Select'
|
|
22
28
|
export { default as Switch, type SwitchProps } from './components/Switch'
|
|
23
29
|
export {
|
|
24
30
|
default as TextField,
|