@charcoal-ui/react 1.0.0 → 2.0.0-alpha.2
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.d.ts +2 -2
- package/dist/components/Radio/index.d.ts.map +1 -1
- package/dist/components/Radio/index.story.d.ts +14 -5
- 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 +4 -0
- package/dist/components/TextField/index.d.ts.map +1 -1
- package/dist/components/TextField/index.story.d.ts +11 -4
- 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 +97 -35
- 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 +10 -0
- package/dist/styled.d.ts.map +1 -1
- package/package.json +11 -6
- package/src/_lib/compat.ts +1 -4
- package/src/components/IconButton/index.tsx +1 -0
- package/src/components/Radio/index.story.tsx +16 -17
- package/src/components/Radio/index.test.tsx +15 -16
- package/src/components/Radio/index.tsx +4 -7
- 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.story.tsx +10 -0
- package/src/components/TextField/index.tsx +105 -23
- package/src/components/a11y.test.tsx +32 -24
- package/src/index.ts +6 -0
- package/src/type.d.ts +0 -4
|
@@ -32,6 +32,8 @@ export interface SingleLineTextFieldProps extends TextFieldBaseProps {
|
|
|
32
32
|
readonly multiline?: false
|
|
33
33
|
readonly rows?: never
|
|
34
34
|
readonly type?: string
|
|
35
|
+
readonly prefix?: string
|
|
36
|
+
readonly suffix?: string
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export interface MultiLineTextFieldProps extends TextFieldBaseProps {
|
|
@@ -39,6 +41,8 @@ export interface MultiLineTextFieldProps extends TextFieldBaseProps {
|
|
|
39
41
|
readonly multiline: true
|
|
40
42
|
readonly rows?: number
|
|
41
43
|
readonly type?: never
|
|
44
|
+
readonly prefix?: never
|
|
45
|
+
readonly suffix?: never
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
export type TextFieldProps = SingleLineTextFieldProps | MultiLineTextFieldProps
|
|
@@ -56,8 +60,10 @@ function mergeRefs<T>(...refs: React.Ref<T>[]): React.RefCallback<T> {
|
|
|
56
60
|
}
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
function
|
|
60
|
-
|
|
63
|
+
function countCodePointsInString(string: string) {
|
|
64
|
+
// [...string] とするとproduction buildで動かなくなる
|
|
65
|
+
// cf. https://twitter.com/f_subal/status/1497214727511891972
|
|
66
|
+
return Array.from(string).length
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
const TextField = React.forwardRef<TextFieldElement, TextFieldProps>(
|
|
@@ -88,24 +94,37 @@ const SingleLineTextField = React.forwardRef<
|
|
|
88
94
|
invalid = false,
|
|
89
95
|
assistiveText,
|
|
90
96
|
maxLength,
|
|
97
|
+
prefix = '',
|
|
98
|
+
suffix = '',
|
|
91
99
|
} = props
|
|
92
100
|
|
|
93
101
|
const { visuallyHiddenProps } = useVisuallyHidden()
|
|
94
102
|
const ariaRef = useRef<HTMLInputElement>(null)
|
|
95
|
-
const
|
|
103
|
+
const prefixRef = useRef<HTMLSpanElement>(null)
|
|
104
|
+
const suffixRef = useRef<HTMLSpanElement>(null)
|
|
105
|
+
const [count, setCount] = useState(countCodePointsInString(props.value ?? ''))
|
|
106
|
+
const [prefixWidth, setPrefixWidth] = useState(0)
|
|
107
|
+
const [suffixWidth, setSuffixWidth] = useState(0)
|
|
96
108
|
|
|
109
|
+
const nonControlled = props.value === undefined
|
|
97
110
|
const handleChange = useCallback(
|
|
98
111
|
(value: string) => {
|
|
99
|
-
const count =
|
|
112
|
+
const count = countCodePointsInString(value)
|
|
100
113
|
if (maxLength !== undefined && count > maxLength) {
|
|
101
114
|
return
|
|
102
115
|
}
|
|
103
|
-
|
|
116
|
+
if (nonControlled) {
|
|
117
|
+
setCount(count)
|
|
118
|
+
}
|
|
104
119
|
onChange?.(value)
|
|
105
120
|
},
|
|
106
|
-
[maxLength, onChange]
|
|
121
|
+
[maxLength, nonControlled, onChange]
|
|
107
122
|
)
|
|
108
123
|
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
setCount(countCodePointsInString(props.value ?? ''))
|
|
126
|
+
}, [props.value])
|
|
127
|
+
|
|
109
128
|
const { inputProps, labelProps, descriptionProps, errorMessageProps } =
|
|
110
129
|
useTextField(
|
|
111
130
|
{
|
|
@@ -121,6 +140,27 @@ const SingleLineTextField = React.forwardRef<
|
|
|
121
140
|
ariaRef
|
|
122
141
|
)
|
|
123
142
|
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const prefixObserver = new ResizeObserver((entries) => {
|
|
145
|
+
setPrefixWidth(entries[0].contentRect.width)
|
|
146
|
+
})
|
|
147
|
+
const suffixObserver = new ResizeObserver((entries) => {
|
|
148
|
+
setSuffixWidth(entries[0].contentRect.width)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
if (prefixRef.current !== null) {
|
|
152
|
+
prefixObserver.observe(prefixRef.current)
|
|
153
|
+
}
|
|
154
|
+
if (suffixRef.current !== null) {
|
|
155
|
+
suffixObserver.observe(suffixRef.current)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return () => {
|
|
159
|
+
suffixObserver.disconnect()
|
|
160
|
+
prefixObserver.disconnect()
|
|
161
|
+
}
|
|
162
|
+
}, [])
|
|
163
|
+
|
|
124
164
|
return (
|
|
125
165
|
<TextFieldRoot className={className} isDisabled={disabled}>
|
|
126
166
|
<TextFieldLabel
|
|
@@ -132,16 +172,24 @@ const SingleLineTextField = React.forwardRef<
|
|
|
132
172
|
{...(!showLabel ? visuallyHiddenProps : {})}
|
|
133
173
|
/>
|
|
134
174
|
<StyledInputContainer>
|
|
175
|
+
<PrefixContainer ref={prefixRef}>
|
|
176
|
+
<Affix>{prefix}</Affix>
|
|
177
|
+
</PrefixContainer>
|
|
135
178
|
<StyledInput
|
|
136
179
|
ref={mergeRefs(forwardRef, ariaRef)}
|
|
137
180
|
invalid={invalid}
|
|
181
|
+
extraLeftPadding={prefixWidth}
|
|
182
|
+
extraRightPadding={suffixWidth}
|
|
138
183
|
{...inputProps}
|
|
139
184
|
/>
|
|
140
|
-
{
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
185
|
+
<SuffixContainer ref={suffixRef}>
|
|
186
|
+
<Affix>{suffix}</Affix>
|
|
187
|
+
{showCount && maxLength && (
|
|
188
|
+
<SingleLineCounter>
|
|
189
|
+
{count}/{maxLength}
|
|
190
|
+
</SingleLineCounter>
|
|
191
|
+
)}
|
|
192
|
+
</SuffixContainer>
|
|
145
193
|
</StyledInputContainer>
|
|
146
194
|
{assistiveText != null && assistiveText.length !== 0 && (
|
|
147
195
|
<AssistiveText
|
|
@@ -178,7 +226,7 @@ const MultiLineTextField = React.forwardRef<
|
|
|
178
226
|
const { visuallyHiddenProps } = useVisuallyHidden()
|
|
179
227
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
180
228
|
const ariaRef = useRef<HTMLTextAreaElement>(null)
|
|
181
|
-
const [count, setCount] = useState(
|
|
229
|
+
const [count, setCount] = useState(countCodePointsInString(props.value ?? ''))
|
|
182
230
|
const [rows, setRows] = useState(initialRows)
|
|
183
231
|
|
|
184
232
|
const syncHeight = useCallback(
|
|
@@ -191,21 +239,28 @@ const MultiLineTextField = React.forwardRef<
|
|
|
191
239
|
[initialRows]
|
|
192
240
|
)
|
|
193
241
|
|
|
242
|
+
const nonControlled = props.value === undefined
|
|
194
243
|
const handleChange = useCallback(
|
|
195
244
|
(value: string) => {
|
|
196
|
-
const count =
|
|
245
|
+
const count = countCodePointsInString(value)
|
|
197
246
|
if (maxLength !== undefined && count > maxLength) {
|
|
198
247
|
return
|
|
199
248
|
}
|
|
200
|
-
|
|
249
|
+
if (nonControlled) {
|
|
250
|
+
setCount(count)
|
|
251
|
+
}
|
|
201
252
|
if (autoHeight && textareaRef.current !== null) {
|
|
202
253
|
syncHeight(textareaRef.current)
|
|
203
254
|
}
|
|
204
255
|
onChange?.(value)
|
|
205
256
|
},
|
|
206
|
-
[autoHeight, maxLength, onChange, syncHeight]
|
|
257
|
+
[autoHeight, maxLength, nonControlled, onChange, syncHeight]
|
|
207
258
|
)
|
|
208
259
|
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
setCount(countCodePointsInString(props.value ?? ''))
|
|
262
|
+
}, [props.value])
|
|
263
|
+
|
|
209
264
|
const { inputProps, labelProps, descriptionProps, errorMessageProps } =
|
|
210
265
|
useTextField(
|
|
211
266
|
{
|
|
@@ -275,19 +330,50 @@ const StyledInputContainer = styled.div`
|
|
|
275
330
|
position: relative;
|
|
276
331
|
`
|
|
277
332
|
|
|
278
|
-
const
|
|
333
|
+
const PrefixContainer = styled.span`
|
|
334
|
+
position: absolute;
|
|
335
|
+
top: 50%;
|
|
336
|
+
left: 8px;
|
|
337
|
+
transform: translateY(-50%);
|
|
338
|
+
`
|
|
339
|
+
|
|
340
|
+
const SuffixContainer = styled.span`
|
|
341
|
+
position: absolute;
|
|
342
|
+
top: 50%;
|
|
343
|
+
right: 8px;
|
|
344
|
+
transform: translateY(-50%);
|
|
345
|
+
|
|
346
|
+
display: flex;
|
|
347
|
+
gap: 8px;
|
|
348
|
+
`
|
|
349
|
+
|
|
350
|
+
const Affix = styled.span`
|
|
351
|
+
user-select: none;
|
|
352
|
+
|
|
353
|
+
${theme((o) => [o.typography(14).preserveHalfLeading, o.font.text2])}
|
|
354
|
+
`
|
|
355
|
+
|
|
356
|
+
const StyledInput = styled.input<{
|
|
357
|
+
invalid: boolean
|
|
358
|
+
extraLeftPadding: number
|
|
359
|
+
extraRightPadding: number
|
|
360
|
+
}>`
|
|
279
361
|
border: none;
|
|
280
362
|
box-sizing: border-box;
|
|
281
363
|
outline: none;
|
|
364
|
+
font-family: inherit;
|
|
282
365
|
|
|
283
366
|
/* Prevent zooming for iOS Safari */
|
|
284
367
|
transform-origin: top left;
|
|
285
368
|
transform: scale(0.875);
|
|
286
369
|
width: calc(100% / 0.875);
|
|
287
|
-
height: calc(
|
|
370
|
+
height: calc(100% / 0.875);
|
|
288
371
|
font-size: calc(14px / 0.875);
|
|
289
372
|
line-height: calc(22px / 0.875);
|
|
290
|
-
padding: calc(9px / 0.875)
|
|
373
|
+
padding-top: calc(9px / 0.875);
|
|
374
|
+
padding-bottom: calc(9px / 0.875);
|
|
375
|
+
padding-left: calc((8px + ${(p) => p.extraLeftPadding}px) / 0.875);
|
|
376
|
+
padding-right: calc((8px + ${(p) => p.extraRightPadding}px) / 0.875);
|
|
291
377
|
border-radius: calc(4px / 0.875);
|
|
292
378
|
|
|
293
379
|
/* Display box-shadow for iOS Safari */
|
|
@@ -320,6 +406,7 @@ const StyledTextarea = styled.textarea<{ invalid: boolean }>`
|
|
|
320
406
|
box-sizing: border-box;
|
|
321
407
|
outline: none;
|
|
322
408
|
resize: none;
|
|
409
|
+
font-family: inherit;
|
|
323
410
|
|
|
324
411
|
/* Prevent zooming for iOS Safari */
|
|
325
412
|
transform-origin: top left;
|
|
@@ -359,11 +446,6 @@ const StyledTextarea = styled.textarea<{ invalid: boolean }>`
|
|
|
359
446
|
`
|
|
360
447
|
|
|
361
448
|
const SingleLineCounter = styled.span`
|
|
362
|
-
position: absolute;
|
|
363
|
-
top: 50%;
|
|
364
|
-
right: 8px;
|
|
365
|
-
transform: translateY(-50%);
|
|
366
|
-
|
|
367
449
|
${theme((o) => [o.typography(14).preserveHalfLeading, o.font.text3])}
|
|
368
450
|
`
|
|
369
451
|
|
|
@@ -10,26 +10,38 @@ import { light, dark } from '@charcoal-ui/theme'
|
|
|
10
10
|
|
|
11
11
|
expect.extend(toHaveNoViolations)
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
interface StoryWithMetadata<ArgsType = any> {
|
|
15
|
+
filename: string
|
|
16
|
+
name: string
|
|
17
|
+
story: Story<ArgsType>
|
|
18
|
+
args: ArgsType
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const stories: StoryWithMetadata[] = glob
|
|
14
22
|
.sync(path.resolve(__dirname, '**/*.story.tsx'))
|
|
15
|
-
.flatMap((
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
.flatMap((filePath) => {
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
25
|
+
const exports = require(`./${path.relative(
|
|
26
|
+
__dirname,
|
|
27
|
+
filePath
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
)}`) as Record<string, any>
|
|
30
|
+
|
|
31
|
+
return Object.entries(exports)
|
|
23
32
|
.filter(
|
|
24
|
-
([exportName,
|
|
25
|
-
exportName !== 'default' && typeof
|
|
33
|
+
([exportName, exportValue]) =>
|
|
34
|
+
exportName !== 'default' && typeof exportValue === 'function'
|
|
26
35
|
)
|
|
27
|
-
.map
|
|
28
|
-
path.relative(__dirname,
|
|
29
|
-
exportName,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
.map(([exportName, exportValue]) => ({
|
|
37
|
+
filename: path.relative(__dirname, filePath),
|
|
38
|
+
name: exportName,
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
+
story: exportValue as Story<any>,
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
|
42
|
+
args: { ...exports.default.args, ...exportValue.args },
|
|
43
|
+
}))
|
|
44
|
+
})
|
|
33
45
|
|
|
34
46
|
const themes = Object.entries({
|
|
35
47
|
light,
|
|
@@ -43,10 +55,6 @@ const links = Object.entries({
|
|
|
43
55
|
const div = document.body.appendChild(document.createElement('div'))
|
|
44
56
|
|
|
45
57
|
beforeEach(() => {
|
|
46
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
47
|
-
// @ts-expect-error
|
|
48
|
-
global.__DEV__ = {}
|
|
49
|
-
|
|
50
58
|
global.IntersectionObserver = jest.fn().mockImplementation(() => ({
|
|
51
59
|
observe() {
|
|
52
60
|
return null
|
|
@@ -60,14 +68,14 @@ beforeEach(() => {
|
|
|
60
68
|
describe.each(themes)('using %s theme', (_name, theme) => {
|
|
61
69
|
describe.each(links)('using %s component', (_name, link) => {
|
|
62
70
|
describe.each(stories)(
|
|
63
|
-
'storiesOf(
|
|
64
|
-
(
|
|
71
|
+
'storiesOf($filename).add($name)',
|
|
72
|
+
({ story: Story, args }) => {
|
|
65
73
|
it('has no accessibility violations', async () => {
|
|
66
74
|
expect(() => {
|
|
67
75
|
render(
|
|
68
76
|
<ThemeProvider theme={theme}>
|
|
69
77
|
<ComponentAbstraction components={{ Link: link }}>
|
|
70
|
-
{
|
|
78
|
+
<Story {...args} />
|
|
71
79
|
</ComponentAbstraction>
|
|
72
80
|
</ThemeProvider>
|
|
73
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,
|