@charcoal-ui/react 1.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/LICENSE +201 -0
- package/README.md +18 -0
- package/package.json +75 -0
- package/src/_lib/compat.ts +15 -0
- package/src/_lib/index.ts +35 -0
- package/src/components/Button/index.story.tsx +187 -0
- package/src/components/Button/index.tsx +122 -0
- package/src/components/Clickable/index.story.tsx +18 -0
- package/src/components/Clickable/index.tsx +108 -0
- package/src/components/FieldLabel/index.tsx +73 -0
- package/src/components/IconButton/index.story.tsx +51 -0
- package/src/components/IconButton/index.tsx +118 -0
- package/src/components/Radio/index.story.tsx +79 -0
- package/src/components/Radio/index.test.tsx +149 -0
- package/src/components/Radio/index.tsx +202 -0
- package/src/components/Switch/index.story.tsx +43 -0
- package/src/components/Switch/index.tsx +116 -0
- package/src/components/TextField/index.story.tsx +83 -0
- package/src/components/TextField/index.tsx +386 -0
- package/src/components/a11y.test.tsx +82 -0
- package/src/core/ComponentAbstraction.tsx +47 -0
- package/src/index.ts +21 -0
- package/src/styled.ts +3 -0
- package/src/type.d.ts +16 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { useTextField } from '@react-aria/textfield'
|
|
2
|
+
import { useVisuallyHidden } from '@react-aria/visually-hidden'
|
|
3
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
4
|
+
import styled, { css } from 'styled-components'
|
|
5
|
+
import FieldLabel, { FieldLabelProps } from '../FieldLabel'
|
|
6
|
+
import createTheme from '@charcoal-ui/styled'
|
|
7
|
+
|
|
8
|
+
const theme = createTheme(styled)
|
|
9
|
+
|
|
10
|
+
interface TextFieldBaseProps
|
|
11
|
+
extends Pick<FieldLabelProps, 'label' | 'requiredText' | 'subLabel'> {
|
|
12
|
+
readonly className?: string
|
|
13
|
+
readonly defaultValue?: string
|
|
14
|
+
readonly value?: string
|
|
15
|
+
readonly onChange?: (value: string) => void
|
|
16
|
+
readonly showCount?: boolean
|
|
17
|
+
readonly showLabel?: boolean
|
|
18
|
+
readonly placeholder?: string
|
|
19
|
+
readonly assistiveText?: string
|
|
20
|
+
readonly disabled?: boolean
|
|
21
|
+
readonly required?: boolean
|
|
22
|
+
readonly invalid?: boolean
|
|
23
|
+
readonly maxLength?: number
|
|
24
|
+
/**
|
|
25
|
+
* tab-indexがー1かどうか
|
|
26
|
+
*/
|
|
27
|
+
readonly excludeFromTabOrder?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SingleLineTextFieldProps extends TextFieldBaseProps {
|
|
31
|
+
readonly autoHeight?: never
|
|
32
|
+
readonly multiline?: false
|
|
33
|
+
readonly rows?: never
|
|
34
|
+
readonly type?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MultiLineTextFieldProps extends TextFieldBaseProps {
|
|
38
|
+
readonly autoHeight?: boolean
|
|
39
|
+
readonly multiline: true
|
|
40
|
+
readonly rows?: number
|
|
41
|
+
readonly type?: never
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type TextFieldProps = SingleLineTextFieldProps | MultiLineTextFieldProps
|
|
45
|
+
type TextFieldElement = HTMLInputElement & HTMLTextAreaElement
|
|
46
|
+
|
|
47
|
+
function mergeRefs<T>(...refs: React.Ref<T>[]): React.RefCallback<T> {
|
|
48
|
+
return (value) => {
|
|
49
|
+
for (const ref of refs) {
|
|
50
|
+
if (typeof ref === 'function') {
|
|
51
|
+
ref(value)
|
|
52
|
+
} else if (ref !== null) {
|
|
53
|
+
;(ref as React.MutableRefObject<T | null>).current = value
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function countStringInCodePoints(string: string) {
|
|
60
|
+
return [...string].length
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const TextField = React.forwardRef<TextFieldElement, TextFieldProps>(
|
|
64
|
+
function TextField(props, ref) {
|
|
65
|
+
return props.multiline !== undefined && props.multiline ? (
|
|
66
|
+
<MultiLineTextField ref={ref} {...props} />
|
|
67
|
+
) : (
|
|
68
|
+
<SingleLineTextField ref={ref} {...props} />
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
export default TextField
|
|
74
|
+
|
|
75
|
+
const SingleLineTextField = React.forwardRef<
|
|
76
|
+
HTMLInputElement,
|
|
77
|
+
SingleLineTextFieldProps
|
|
78
|
+
>(function SingleLineTextFieldInner({ onChange, ...props }, forwardRef) {
|
|
79
|
+
const {
|
|
80
|
+
className,
|
|
81
|
+
showLabel = false,
|
|
82
|
+
showCount = false,
|
|
83
|
+
label,
|
|
84
|
+
requiredText,
|
|
85
|
+
subLabel,
|
|
86
|
+
disabled = false,
|
|
87
|
+
required,
|
|
88
|
+
invalid = false,
|
|
89
|
+
assistiveText,
|
|
90
|
+
maxLength,
|
|
91
|
+
} = props
|
|
92
|
+
|
|
93
|
+
const { visuallyHiddenProps } = useVisuallyHidden()
|
|
94
|
+
const ariaRef = useRef<HTMLInputElement>(null)
|
|
95
|
+
const [count, setCount] = useState(countStringInCodePoints(props.value ?? ''))
|
|
96
|
+
|
|
97
|
+
const handleChange = useCallback(
|
|
98
|
+
(value: string) => {
|
|
99
|
+
const count = countStringInCodePoints(value)
|
|
100
|
+
if (maxLength !== undefined && count > maxLength) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
setCount(count)
|
|
104
|
+
onChange?.(value)
|
|
105
|
+
},
|
|
106
|
+
[maxLength, onChange]
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
const { inputProps, labelProps, descriptionProps, errorMessageProps } =
|
|
110
|
+
useTextField(
|
|
111
|
+
{
|
|
112
|
+
inputElementType: 'input',
|
|
113
|
+
isDisabled: disabled,
|
|
114
|
+
isRequired: required,
|
|
115
|
+
validationState: invalid ? 'invalid' : 'valid',
|
|
116
|
+
description: !invalid && assistiveText,
|
|
117
|
+
errorMessage: invalid && assistiveText,
|
|
118
|
+
onChange: handleChange,
|
|
119
|
+
...props,
|
|
120
|
+
},
|
|
121
|
+
ariaRef
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<TextFieldRoot className={className} isDisabled={disabled}>
|
|
126
|
+
<TextFieldLabel
|
|
127
|
+
label={label}
|
|
128
|
+
requiredText={requiredText}
|
|
129
|
+
required={required}
|
|
130
|
+
subLabel={subLabel}
|
|
131
|
+
{...labelProps}
|
|
132
|
+
{...(!showLabel ? visuallyHiddenProps : {})}
|
|
133
|
+
/>
|
|
134
|
+
<StyledInputContainer>
|
|
135
|
+
<StyledInput
|
|
136
|
+
ref={mergeRefs(forwardRef, ariaRef)}
|
|
137
|
+
invalid={invalid}
|
|
138
|
+
{...inputProps}
|
|
139
|
+
/>
|
|
140
|
+
{showCount && maxLength && (
|
|
141
|
+
<SingleLineCounter>
|
|
142
|
+
{count}/{maxLength}
|
|
143
|
+
</SingleLineCounter>
|
|
144
|
+
)}
|
|
145
|
+
</StyledInputContainer>
|
|
146
|
+
{assistiveText != null && assistiveText.length !== 0 && (
|
|
147
|
+
<AssistiveText
|
|
148
|
+
invalid={invalid}
|
|
149
|
+
{...(invalid ? errorMessageProps : descriptionProps)}
|
|
150
|
+
>
|
|
151
|
+
{assistiveText}
|
|
152
|
+
</AssistiveText>
|
|
153
|
+
)}
|
|
154
|
+
</TextFieldRoot>
|
|
155
|
+
)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const MultiLineTextField = React.forwardRef<
|
|
159
|
+
HTMLTextAreaElement,
|
|
160
|
+
MultiLineTextFieldProps
|
|
161
|
+
>(function MultiLineTextFieldInner({ onChange, ...props }, forwardRef) {
|
|
162
|
+
const {
|
|
163
|
+
className,
|
|
164
|
+
showCount = false,
|
|
165
|
+
showLabel = false,
|
|
166
|
+
label,
|
|
167
|
+
requiredText,
|
|
168
|
+
subLabel,
|
|
169
|
+
disabled = false,
|
|
170
|
+
required,
|
|
171
|
+
invalid = false,
|
|
172
|
+
assistiveText,
|
|
173
|
+
maxLength,
|
|
174
|
+
autoHeight = false,
|
|
175
|
+
rows: initialRows = 4,
|
|
176
|
+
} = props
|
|
177
|
+
|
|
178
|
+
const { visuallyHiddenProps } = useVisuallyHidden()
|
|
179
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
180
|
+
const ariaRef = useRef<HTMLTextAreaElement>(null)
|
|
181
|
+
const [count, setCount] = useState(countStringInCodePoints(props.value ?? ''))
|
|
182
|
+
const [rows, setRows] = useState(initialRows)
|
|
183
|
+
|
|
184
|
+
const syncHeight = useCallback(
|
|
185
|
+
(textarea: HTMLTextAreaElement) => {
|
|
186
|
+
const rows = `${textarea.value}\n`.match(/\n/gu)?.length ?? 1
|
|
187
|
+
if (initialRows <= rows) {
|
|
188
|
+
setRows(rows)
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
[initialRows]
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
const handleChange = useCallback(
|
|
195
|
+
(value: string) => {
|
|
196
|
+
const count = countStringInCodePoints(value)
|
|
197
|
+
if (maxLength !== undefined && count > maxLength) {
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
setCount(count)
|
|
201
|
+
if (autoHeight && textareaRef.current !== null) {
|
|
202
|
+
syncHeight(textareaRef.current)
|
|
203
|
+
}
|
|
204
|
+
onChange?.(value)
|
|
205
|
+
},
|
|
206
|
+
[autoHeight, maxLength, onChange, syncHeight]
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
const { inputProps, labelProps, descriptionProps, errorMessageProps } =
|
|
210
|
+
useTextField(
|
|
211
|
+
{
|
|
212
|
+
inputElementType: 'textarea',
|
|
213
|
+
isDisabled: disabled,
|
|
214
|
+
isRequired: required,
|
|
215
|
+
validationState: invalid ? 'invalid' : 'valid',
|
|
216
|
+
description: !invalid && assistiveText,
|
|
217
|
+
errorMessage: invalid && assistiveText,
|
|
218
|
+
onChange: handleChange,
|
|
219
|
+
...props,
|
|
220
|
+
},
|
|
221
|
+
ariaRef
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
if (autoHeight && textareaRef.current !== null) {
|
|
226
|
+
syncHeight(textareaRef.current)
|
|
227
|
+
}
|
|
228
|
+
}, [autoHeight, syncHeight])
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<TextFieldRoot className={className} isDisabled={disabled}>
|
|
232
|
+
<TextFieldLabel
|
|
233
|
+
label={label}
|
|
234
|
+
requiredText={requiredText}
|
|
235
|
+
required={required}
|
|
236
|
+
subLabel={subLabel}
|
|
237
|
+
{...labelProps}
|
|
238
|
+
{...(showLabel ? visuallyHiddenProps : {})}
|
|
239
|
+
/>
|
|
240
|
+
<StyledTextareaContainer rows={rows}>
|
|
241
|
+
<StyledTextarea
|
|
242
|
+
ref={mergeRefs(textareaRef, forwardRef, ariaRef)}
|
|
243
|
+
invalid={invalid}
|
|
244
|
+
rows={rows}
|
|
245
|
+
{...inputProps}
|
|
246
|
+
/>
|
|
247
|
+
{showCount && <MultiLineCounter>{count}</MultiLineCounter>}
|
|
248
|
+
</StyledTextareaContainer>
|
|
249
|
+
{assistiveText != null && assistiveText.length !== 0 && (
|
|
250
|
+
<AssistiveText
|
|
251
|
+
invalid={invalid}
|
|
252
|
+
{...(invalid ? errorMessageProps : descriptionProps)}
|
|
253
|
+
>
|
|
254
|
+
{assistiveText}
|
|
255
|
+
</AssistiveText>
|
|
256
|
+
)}
|
|
257
|
+
</TextFieldRoot>
|
|
258
|
+
)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
const TextFieldRoot = styled.div<{ isDisabled: boolean }>`
|
|
262
|
+
display: flex;
|
|
263
|
+
flex-direction: column;
|
|
264
|
+
|
|
265
|
+
${(p) => p.isDisabled && { opacity: p.theme.elementEffect.disabled.opacity }}
|
|
266
|
+
`
|
|
267
|
+
|
|
268
|
+
const TextFieldLabel = styled(FieldLabel)`
|
|
269
|
+
${theme((o) => o.margin.bottom(8))}
|
|
270
|
+
`
|
|
271
|
+
|
|
272
|
+
const StyledInputContainer = styled.div`
|
|
273
|
+
height: 40px;
|
|
274
|
+
display: grid;
|
|
275
|
+
position: relative;
|
|
276
|
+
`
|
|
277
|
+
|
|
278
|
+
const StyledInput = styled.input<{ invalid: boolean }>`
|
|
279
|
+
border: none;
|
|
280
|
+
box-sizing: border-box;
|
|
281
|
+
outline: none;
|
|
282
|
+
|
|
283
|
+
/* Prevent zooming for iOS Safari */
|
|
284
|
+
transform-origin: top left;
|
|
285
|
+
transform: scale(0.875);
|
|
286
|
+
width: calc(100% / 0.875);
|
|
287
|
+
height: calc(40px / 0.875);
|
|
288
|
+
font-size: calc(14px / 0.875);
|
|
289
|
+
line-height: calc(22px / 0.875);
|
|
290
|
+
padding: calc(9px / 0.875) calc(8px / 0.875);
|
|
291
|
+
border-radius: calc(4px / 0.875);
|
|
292
|
+
|
|
293
|
+
/* Display box-shadow for iOS Safari */
|
|
294
|
+
appearance: none;
|
|
295
|
+
|
|
296
|
+
${(p) =>
|
|
297
|
+
theme((o) => [
|
|
298
|
+
o.bg.surface3.hover,
|
|
299
|
+
o.outline.default.focus,
|
|
300
|
+
p.invalid && o.outline.assertive,
|
|
301
|
+
o.font.text2,
|
|
302
|
+
])}
|
|
303
|
+
|
|
304
|
+
&::placeholder {
|
|
305
|
+
${theme((o) => o.font.text3)}
|
|
306
|
+
}
|
|
307
|
+
`
|
|
308
|
+
|
|
309
|
+
const StyledTextareaContainer = styled.div<{ rows: number }>`
|
|
310
|
+
display: grid;
|
|
311
|
+
position: relative;
|
|
312
|
+
|
|
313
|
+
${({ rows }) => css`
|
|
314
|
+
max-height: calc(22px * ${rows} + 18px);
|
|
315
|
+
`};
|
|
316
|
+
`
|
|
317
|
+
|
|
318
|
+
const StyledTextarea = styled.textarea<{ invalid: boolean }>`
|
|
319
|
+
border: none;
|
|
320
|
+
box-sizing: border-box;
|
|
321
|
+
outline: none;
|
|
322
|
+
resize: none;
|
|
323
|
+
|
|
324
|
+
/* Prevent zooming for iOS Safari */
|
|
325
|
+
transform-origin: top left;
|
|
326
|
+
transform: scale(0.875);
|
|
327
|
+
width: calc(100% / 0.875);
|
|
328
|
+
font-size: calc(14px / 0.875);
|
|
329
|
+
line-height: calc(22px / 0.875);
|
|
330
|
+
padding: calc(9px / 0.875) calc(8px / 0.875);
|
|
331
|
+
border-radius: calc(4px / 0.875);
|
|
332
|
+
|
|
333
|
+
${({ rows }) => css`
|
|
334
|
+
height: calc(22px / 0.875 * ${rows} + 18px / 0.875);
|
|
335
|
+
`};
|
|
336
|
+
|
|
337
|
+
/* Display box-shadow for iOS Safari */
|
|
338
|
+
appearance: none;
|
|
339
|
+
|
|
340
|
+
${(p) =>
|
|
341
|
+
theme((o) => [
|
|
342
|
+
o.bg.surface3.hover,
|
|
343
|
+
o.outline.default.focus,
|
|
344
|
+
p.invalid && o.outline.assertive,
|
|
345
|
+
o.font.text2,
|
|
346
|
+
])}
|
|
347
|
+
|
|
348
|
+
&::placeholder {
|
|
349
|
+
${theme((o) => o.font.text3)}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/* Hide scrollbar for Chrome, Safari and Opera */
|
|
353
|
+
&::-webkit-scrollbar {
|
|
354
|
+
display: none;
|
|
355
|
+
}
|
|
356
|
+
/* Hide scrollbar for IE, Edge and Firefox */
|
|
357
|
+
-ms-overflow-style: none; /* IE and Edge */
|
|
358
|
+
scrollbar-width: none; /* Firefox */
|
|
359
|
+
`
|
|
360
|
+
|
|
361
|
+
const SingleLineCounter = styled.span`
|
|
362
|
+
position: absolute;
|
|
363
|
+
top: 50%;
|
|
364
|
+
right: 8px;
|
|
365
|
+
transform: translateY(-50%);
|
|
366
|
+
|
|
367
|
+
${theme((o) => [o.typography(14).preserveHalfLeading, o.font.text3])}
|
|
368
|
+
`
|
|
369
|
+
|
|
370
|
+
const MultiLineCounter = styled.span`
|
|
371
|
+
position: absolute;
|
|
372
|
+
bottom: 9px;
|
|
373
|
+
right: 8px;
|
|
374
|
+
|
|
375
|
+
${theme((o) => [o.typography(14).preserveHalfLeading, o.font.text3])}
|
|
376
|
+
`
|
|
377
|
+
|
|
378
|
+
const AssistiveText = styled.p<{ invalid: boolean }>`
|
|
379
|
+
${(p) =>
|
|
380
|
+
theme((o) => [
|
|
381
|
+
o.typography(14),
|
|
382
|
+
o.margin.top(8),
|
|
383
|
+
o.margin.bottom(0),
|
|
384
|
+
o.font[p.invalid ? 'assertive' : 'text1'],
|
|
385
|
+
])}
|
|
386
|
+
`
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import glob from 'glob'
|
|
3
|
+
import { axe, toHaveNoViolations } from 'jest-axe'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { render } from '@testing-library/react'
|
|
6
|
+
import { ThemeProvider } from 'styled-components'
|
|
7
|
+
import { Story } from '../_lib/compat'
|
|
8
|
+
import ComponentAbstraction, { DefaultLink } from '../core/ComponentAbstraction'
|
|
9
|
+
import createTheme from '@charcoal-ui/styled'
|
|
10
|
+
const { light, dark } = createTheme
|
|
11
|
+
|
|
12
|
+
expect.extend(toHaveNoViolations)
|
|
13
|
+
|
|
14
|
+
const stories = glob
|
|
15
|
+
.sync(path.resolve(__dirname, '**/*.story.tsx'))
|
|
16
|
+
.flatMap((filename) =>
|
|
17
|
+
Object.entries(
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
19
|
+
require(`./${path.relative(__dirname, filename)}`) as Record<
|
|
20
|
+
string,
|
|
21
|
+
Story<any>
|
|
22
|
+
>
|
|
23
|
+
)
|
|
24
|
+
.filter(
|
|
25
|
+
([exportName, story]) =>
|
|
26
|
+
exportName !== 'default' && typeof story === 'function'
|
|
27
|
+
)
|
|
28
|
+
.map<[string, string, Story<any>]>(([exportName, story]) => [
|
|
29
|
+
path.relative(__dirname, filename),
|
|
30
|
+
exportName,
|
|
31
|
+
story,
|
|
32
|
+
])
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const themes = Object.entries({
|
|
36
|
+
light,
|
|
37
|
+
dark,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const links = Object.entries({
|
|
41
|
+
DefaultLink,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const div = document.body.appendChild(document.createElement('div'))
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
48
|
+
// @ts-expect-error
|
|
49
|
+
global.__DEV__ = {}
|
|
50
|
+
|
|
51
|
+
global.IntersectionObserver = jest.fn().mockImplementation(() => ({
|
|
52
|
+
observe() {
|
|
53
|
+
return null
|
|
54
|
+
},
|
|
55
|
+
disconnect() {
|
|
56
|
+
return null
|
|
57
|
+
},
|
|
58
|
+
}))
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe.each(themes)('using %s theme', (_name, theme) => {
|
|
62
|
+
describe.each(links)('using %s component', (_name, link) => {
|
|
63
|
+
describe.each(stories)(
|
|
64
|
+
'storiesOf(%s).add(%s)',
|
|
65
|
+
(_filename, _exportName, story) => {
|
|
66
|
+
it('has no accessibility violations', async () => {
|
|
67
|
+
expect(() => {
|
|
68
|
+
render(
|
|
69
|
+
<ThemeProvider theme={theme}>
|
|
70
|
+
<ComponentAbstraction components={{ Link: link }}>
|
|
71
|
+
{story(story.args)}
|
|
72
|
+
</ComponentAbstraction>
|
|
73
|
+
</ThemeProvider>
|
|
74
|
+
)
|
|
75
|
+
}).not.toThrow()
|
|
76
|
+
|
|
77
|
+
expect(await axe(div)).toHaveNoViolations()
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React, { useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
export type LinkProps = {
|
|
4
|
+
/**
|
|
5
|
+
* リンクのURL
|
|
6
|
+
*/
|
|
7
|
+
to: string
|
|
8
|
+
} & Omit<React.ComponentPropsWithoutRef<'a'>, 'href'>
|
|
9
|
+
|
|
10
|
+
export const DefaultLink = React.forwardRef<HTMLAnchorElement, LinkProps>(
|
|
11
|
+
function DefaultLink({ to, children, ...rest }, ref) {
|
|
12
|
+
return (
|
|
13
|
+
<a href={to} ref={ref} {...rest}>
|
|
14
|
+
{children}
|
|
15
|
+
</a>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
interface Components {
|
|
21
|
+
Link: React.ComponentType<React.ComponentPropsWithRef<typeof DefaultLink>>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DefaultValue: Components = {
|
|
25
|
+
Link: DefaultLink,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ComponentAbstractionContext = React.createContext(DefaultValue)
|
|
29
|
+
|
|
30
|
+
interface Props {
|
|
31
|
+
children: React.ReactNode
|
|
32
|
+
components: Partial<Components>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function ComponentAbstraction({ children, components }: Props) {
|
|
36
|
+
return (
|
|
37
|
+
<ComponentAbstractionContext.Provider
|
|
38
|
+
value={{ ...DefaultValue, ...components }}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
</ComponentAbstractionContext.Provider>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function useComponentAbstraction() {
|
|
46
|
+
return useContext(ComponentAbstractionContext)
|
|
47
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export {
|
|
2
|
+
default as ComponentAbstraction,
|
|
3
|
+
useComponentAbstraction,
|
|
4
|
+
type LinkProps,
|
|
5
|
+
} from './core/ComponentAbstraction'
|
|
6
|
+
export { default as Button, type ButtonProps } from './components/Button'
|
|
7
|
+
export {
|
|
8
|
+
default as Clickable,
|
|
9
|
+
type ClickableProps,
|
|
10
|
+
type ClickableElement,
|
|
11
|
+
} from './components/Clickable'
|
|
12
|
+
export {
|
|
13
|
+
default as IconButton,
|
|
14
|
+
type IconButtonProps,
|
|
15
|
+
} from './components/IconButton'
|
|
16
|
+
export { default as Radio, type RadioProps } from './components/Radio'
|
|
17
|
+
export { default as Switch, type SwitchProps } from './components/Switch'
|
|
18
|
+
export {
|
|
19
|
+
default as TextField,
|
|
20
|
+
type TextFieldProps,
|
|
21
|
+
} from './components/TextField'
|
package/src/styled.ts
ADDED
package/src/type.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CSSProp, DefaultTheme } from 'styled-components'
|
|
2
|
+
import { ElementsTheme as Theme } from '@charcoal-ui/styled'
|
|
3
|
+
|
|
4
|
+
declare module 'styled-components' {
|
|
5
|
+
export interface DefaultTheme extends Theme {}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
declare module 'react' {
|
|
9
|
+
interface Attributes {
|
|
10
|
+
css?: CSSProp<DefaultTheme>
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare global {
|
|
15
|
+
const __DEV__: object | undefined // actually object|false, but using undefined allows ! assertion
|
|
16
|
+
}
|