@charcoal-ui/react 1.0.0 → 2.0.0-alpha.10

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.
Files changed (49) hide show
  1. package/dist/_lib/compat.d.ts +2 -3
  2. package/dist/_lib/compat.d.ts.map +1 -1
  3. package/dist/components/Icon/index.d.ts +12 -0
  4. package/dist/components/Icon/index.d.ts.map +1 -0
  5. package/dist/components/Icon/index.story.d.ts +24 -0
  6. package/dist/components/Icon/index.story.d.ts.map +1 -0
  7. package/dist/components/Radio/index.d.ts +2 -2
  8. package/dist/components/Radio/index.d.ts.map +1 -1
  9. package/dist/components/Radio/index.story.d.ts +14 -5
  10. package/dist/components/Radio/index.story.d.ts.map +1 -1
  11. package/dist/components/Select/context.d.ts +14 -0
  12. package/dist/components/Select/context.d.ts.map +1 -0
  13. package/dist/components/Select/index.d.ts +24 -0
  14. package/dist/components/Select/index.d.ts.map +1 -0
  15. package/dist/components/Select/index.story.d.ts +75 -0
  16. package/dist/components/Select/index.story.d.ts.map +1 -0
  17. package/dist/components/Select/index.test.d.ts +2 -0
  18. package/dist/components/Select/index.test.d.ts.map +1 -0
  19. package/dist/components/TextField/index.d.ts +4 -0
  20. package/dist/components/TextField/index.d.ts.map +1 -1
  21. package/dist/components/TextField/index.story.d.ts +11 -4
  22. package/dist/components/TextField/index.story.d.ts.map +1 -1
  23. package/dist/index.cjs +1 -1
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.modern.js +97 -35
  28. package/dist/index.modern.js.map +1 -1
  29. package/dist/index.module.js +1 -1
  30. package/dist/index.module.js.map +1 -1
  31. package/dist/styled.d.ts +10 -0
  32. package/dist/styled.d.ts.map +1 -1
  33. package/package.json +11 -6
  34. package/src/_lib/compat.ts +1 -4
  35. package/src/components/Icon/index.story.tsx +29 -0
  36. package/src/components/Icon/index.tsx +33 -0
  37. package/src/components/IconButton/index.tsx +1 -0
  38. package/src/components/Radio/index.story.tsx +16 -17
  39. package/src/components/Radio/index.test.tsx +15 -16
  40. package/src/components/Radio/index.tsx +4 -7
  41. package/src/components/Select/context.ts +23 -0
  42. package/src/components/Select/index.story.tsx +153 -0
  43. package/src/components/Select/index.test.tsx +281 -0
  44. package/src/components/Select/index.tsx +210 -0
  45. package/src/components/TextField/index.story.tsx +10 -0
  46. package/src/components/TextField/index.tsx +106 -24
  47. package/src/components/a11y.test.tsx +32 -24
  48. package/src/index.ts +7 -0
  49. package/src/type.d.ts +0 -4
@@ -75,6 +75,16 @@ HasCount.args = {
75
75
  maxLength: 100,
76
76
  }
77
77
 
78
+ export const HasAffix: Story<Partial<SingleLineTextFieldProps>> = (args) => (
79
+ <TextField label="Label" placeholder="path/to/your/file" {...args} />
80
+ )
81
+ HasAffix.args = {
82
+ showCount: true,
83
+ maxLength: 200,
84
+ prefix: '/home/john/',
85
+ suffix: '.png',
86
+ }
87
+
78
88
  export const AutoHeight: Story<Partial<MultiLineTextFieldProps>> = (args) => (
79
89
  <TextField label="Label" placeholder="Multi Line" {...args} multiline />
80
90
  )
@@ -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 countStringInCodePoints(string: string) {
60
- return [...string].length
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 [count, setCount] = useState(countStringInCodePoints(props.value ?? ''))
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 = countStringInCodePoints(value)
112
+ const count = countCodePointsInString(value)
100
113
  if (maxLength !== undefined && count > maxLength) {
101
114
  return
102
115
  }
103
- setCount(count)
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
- {showCount && maxLength && (
141
- <SingleLineCounter>
142
- {count}/{maxLength}
143
- </SingleLineCounter>
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(countStringInCodePoints(props.value ?? ''))
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 = countStringInCodePoints(value)
245
+ const count = countCodePointsInString(value)
197
246
  if (maxLength !== undefined && count > maxLength) {
198
247
  return
199
248
  }
200
- setCount(count)
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
  {
@@ -235,7 +290,7 @@ const MultiLineTextField = React.forwardRef<
235
290
  required={required}
236
291
  subLabel={subLabel}
237
292
  {...labelProps}
238
- {...(showLabel ? visuallyHiddenProps : {})}
293
+ {...(!showLabel ? visuallyHiddenProps : {})}
239
294
  />
240
295
  <StyledTextareaContainer rows={rows}>
241
296
  <StyledTextarea
@@ -275,19 +330,50 @@ const StyledInputContainer = styled.div`
275
330
  position: relative;
276
331
  `
277
332
 
278
- const StyledInput = styled.input<{ invalid: boolean }>`
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(40px / 0.875);
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) calc(8px / 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
- const stories = glob
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((filename) =>
16
- Object.entries(
17
- // eslint-disable-next-line @typescript-eslint/no-var-requires
18
- require(`./${path.relative(__dirname, filename)}`) as Record<
19
- string,
20
- Story<any>
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, story]) =>
25
- exportName !== 'default' && typeof story === 'function'
33
+ ([exportName, exportValue]) =>
34
+ exportName !== 'default' && typeof exportValue === 'function'
26
35
  )
27
- .map<[string, string, Story<any>]>(([exportName, story]) => [
28
- path.relative(__dirname, filename),
29
- exportName,
30
- story,
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(%s).add(%s)',
64
- (_filename, _exportName, story) => {
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
- {story(story.args)}
78
+ <Story {...args} />
71
79
  </ComponentAbstraction>
72
80
  </ThemeProvider>
73
81
  )
package/src/index.ts CHANGED
@@ -19,8 +19,15 @@ 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,
25
31
  type TextFieldProps,
26
32
  } from './components/TextField'
33
+ export { default as Icon, type IconProps } from './components/Icon'
package/src/type.d.ts CHANGED
@@ -10,7 +10,3 @@ declare module 'react' {
10
10
  css?: CSSProp<DefaultTheme>
11
11
  }
12
12
  }
13
-
14
- declare global {
15
- const __DEV__: object | undefined // actually object|false, but using undefined allows ! assertion
16
- }