@charcoal-ui/react 3.0.0-beta.3 → 3.0.0-beta.4
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 +18 -0
- package/dist/_lib/compat.d.ts.map +1 -1
- package/dist/_lib/index.d.ts +7 -0
- package/dist/_lib/index.d.ts.map +1 -1
- package/dist/components/Checkbox/index.d.ts +1 -0
- package/dist/components/Checkbox/index.d.ts.map +1 -1
- package/dist/components/Checkbox/index.story.d.ts +1 -0
- package/dist/components/Checkbox/index.story.d.ts.map +1 -1
- package/dist/components/DropdownSelector/index.d.ts.map +1 -1
- package/dist/components/LoadingSpinner/index.d.ts +8 -6
- package/dist/components/LoadingSpinner/index.d.ts.map +1 -1
- package/dist/components/LoadingSpinner/index.story.d.ts +1 -2
- package/dist/components/LoadingSpinner/index.story.d.ts.map +1 -1
- package/dist/components/Modal/index.d.ts +17 -26
- package/dist/components/Modal/index.d.ts.map +1 -1
- package/dist/components/Modal/index.story.d.ts +12 -2
- package/dist/components/Modal/index.story.d.ts.map +1 -1
- package/dist/components/MultiSelect/index.d.ts +14 -1
- package/dist/components/MultiSelect/index.d.ts.map +1 -1
- package/dist/components/MultiSelect/index.story.d.ts +14 -2
- package/dist/components/MultiSelect/index.story.d.ts.map +1 -1
- package/dist/components/Radio/index.d.ts +12 -5
- package/dist/components/Radio/index.d.ts.map +1 -1
- package/dist/components/Radio/index.story.d.ts +10 -6
- package/dist/components/Radio/index.story.d.ts.map +1 -1
- package/dist/components/SegmentedControl/index.d.ts +1 -0
- package/dist/components/SegmentedControl/index.d.ts.map +1 -1
- package/dist/components/Switch/index.d.ts +2 -1
- package/dist/components/Switch/index.d.ts.map +1 -1
- package/dist/components/Switch/index.story.d.ts +1 -2
- package/dist/components/Switch/index.story.d.ts.map +1 -1
- package/dist/components/TextArea/index.d.ts +3 -10
- package/dist/components/TextArea/index.d.ts.map +1 -1
- package/dist/components/TextField/TextField.story.d.ts +4 -5
- package/dist/components/TextField/TextField.story.d.ts.map +1 -1
- package/dist/components/TextField/index.d.ts +6 -29
- package/dist/components/TextField/index.d.ts.map +1 -1
- package/dist/components/TextField/index.story.d.ts +5 -4
- package/dist/components/TextField/index.story.d.ts.map +1 -1
- package/dist/index.cjs.js +636 -594
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +604 -563
- package/dist/index.esm.js.map +1 -1
- package/package.json +6 -6
- package/src/_lib/compat.ts +19 -0
- package/src/_lib/index.ts +23 -0
- package/src/components/Checkbox/index.story.tsx +1 -0
- package/src/components/Checkbox/index.tsx +2 -1
- package/src/components/DropdownSelector/DropdownMenuItem.tsx +1 -1
- package/src/components/DropdownSelector/ListItem/index.story.tsx +1 -1
- package/src/components/DropdownSelector/ListItem/index.tsx +1 -1
- package/src/components/DropdownSelector/MenuItem/index.tsx +0 -1
- package/src/components/DropdownSelector/MenuItem/internals/useMenuItemHandleKeyDown.tsx +1 -1
- package/src/components/DropdownSelector/MenuItemGroup/index.tsx +0 -1
- package/src/components/DropdownSelector/MenuList/index.story.tsx +0 -1
- package/src/components/DropdownSelector/MenuList/index.tsx +1 -1
- package/src/components/DropdownSelector/MenuList/internals/getValuesRecursive.tsx +1 -1
- package/src/components/DropdownSelector/Popover/index.story.tsx +1 -1
- package/src/components/DropdownSelector/Popover/index.tsx +1 -1
- package/src/components/DropdownSelector/index.tsx +16 -14
- package/src/components/DropdownSelector/utils/findPreviewRecursive.tsx +2 -1
- package/src/components/LoadingSpinner/index.story.tsx +7 -1
- package/src/components/LoadingSpinner/index.tsx +27 -11
- package/src/components/Modal/index.tsx +18 -12
- package/src/components/MultiSelect/index.story.tsx +16 -4
- package/src/components/MultiSelect/index.tsx +70 -60
- package/src/components/Radio/index.story.tsx +7 -8
- package/src/components/Radio/index.test.tsx +3 -3
- package/src/components/Radio/index.tsx +23 -23
- package/src/components/SegmentedControl/index.tsx +6 -1
- package/src/components/Switch/index.tsx +37 -32
- package/src/components/TextArea/TextArea.story.tsx +61 -0
- package/src/components/TextArea/index.tsx +246 -0
- package/src/components/TextField/{index.story.tsx → TextField.story.tsx} +6 -28
- package/src/components/TextField/index.tsx +146 -371
- package/src/index.ts +1 -2
- package/dist/components/DropdownSelector/OptionItem.d.ts +0 -7
- package/dist/components/DropdownSelector/OptionItem.d.ts.map +0 -1
- package/dist/components/DropdownSelector/utils/focusIfHTMLLIElement.d.ts +0 -6
- package/dist/components/DropdownSelector/utils/focusIfHTMLLIElement.d.ts.map +0 -1
- package/dist/components/DropdownSelector/utils/handleFocusByKeyBoard.d.ts +0 -6
- package/dist/components/DropdownSelector/utils/handleFocusByKeyBoard.d.ts.map +0 -1
- package/dist/types/CustomJSXElement.d.ts +0 -3
- package/dist/types/CustomJSXElement.d.ts.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useContext } from 'react'
|
|
1
|
+
import { memo, forwardRef, useCallback, useContext } from 'react'
|
|
2
2
|
import * as React from 'react'
|
|
3
3
|
import styled from 'styled-components'
|
|
4
4
|
import warning from 'warning'
|
|
@@ -7,22 +7,20 @@ import { px } from '@charcoal-ui/utils'
|
|
|
7
7
|
|
|
8
8
|
export type RadioProps = React.PropsWithChildren<{
|
|
9
9
|
value: string
|
|
10
|
-
forceChecked?: boolean
|
|
11
10
|
disabled?: boolean
|
|
11
|
+
className?: string
|
|
12
12
|
}>
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
value,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
children,
|
|
19
|
-
}: RadioProps) {
|
|
14
|
+
const Radio = forwardRef<HTMLInputElement, RadioProps>(function RadioInner(
|
|
15
|
+
{ value, disabled = false, children, className },
|
|
16
|
+
ref
|
|
17
|
+
) {
|
|
20
18
|
const {
|
|
21
19
|
name,
|
|
22
20
|
selected,
|
|
23
21
|
disabled: isParentDisabled,
|
|
24
22
|
readonly,
|
|
25
|
-
|
|
23
|
+
invalid,
|
|
26
24
|
onChange,
|
|
27
25
|
} = useContext(RadioGroupContext)
|
|
28
26
|
|
|
@@ -44,19 +42,22 @@ export default function Radio({
|
|
|
44
42
|
)
|
|
45
43
|
|
|
46
44
|
return (
|
|
47
|
-
<RadioRoot aria-disabled={isDisabled || isReadonly}>
|
|
45
|
+
<RadioRoot aria-disabled={isDisabled || isReadonly} className={className}>
|
|
48
46
|
<RadioInput
|
|
49
47
|
name={name}
|
|
50
48
|
value={value}
|
|
51
|
-
checked={
|
|
52
|
-
|
|
49
|
+
checked={isSelected}
|
|
50
|
+
invalid={invalid}
|
|
53
51
|
onChange={handleChange}
|
|
54
52
|
disabled={isDisabled || isReadonly}
|
|
53
|
+
ref={ref}
|
|
55
54
|
/>
|
|
56
55
|
{children != null && <RadioLabel>{children}</RadioLabel>}
|
|
57
56
|
</RadioRoot>
|
|
58
57
|
)
|
|
59
|
-
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
export default memo(Radio)
|
|
60
61
|
|
|
61
62
|
const RadioRoot = styled.label`
|
|
62
63
|
display: grid;
|
|
@@ -69,7 +70,7 @@ const RadioRoot = styled.label`
|
|
|
69
70
|
`
|
|
70
71
|
|
|
71
72
|
export const RadioInput = styled.input.attrs({ type: 'radio' })<{
|
|
72
|
-
|
|
73
|
+
invalid?: boolean
|
|
73
74
|
}>`
|
|
74
75
|
/** Make prior to browser default style */
|
|
75
76
|
&[type='radio'] {
|
|
@@ -82,14 +83,13 @@ export const RadioInput = styled.input.attrs({ type: 'radio' })<{
|
|
|
82
83
|
|
|
83
84
|
width: 20px;
|
|
84
85
|
height: 20px;
|
|
85
|
-
|
|
86
86
|
cursor: pointer;
|
|
87
87
|
|
|
88
|
-
${({
|
|
88
|
+
${({ invalid = false }) =>
|
|
89
89
|
theme((o) => [
|
|
90
90
|
o.borderRadius('oval'),
|
|
91
91
|
o.bg.surface1.hover.press,
|
|
92
|
-
|
|
92
|
+
invalid && o.outline.assertive,
|
|
93
93
|
])};
|
|
94
94
|
|
|
95
95
|
&:not(:checked) {
|
|
@@ -131,7 +131,7 @@ export type RadioGroupProps = React.PropsWithChildren<{
|
|
|
131
131
|
onChange(next: string): void
|
|
132
132
|
disabled?: boolean
|
|
133
133
|
readonly?: boolean
|
|
134
|
-
|
|
134
|
+
invalid?: boolean
|
|
135
135
|
}>
|
|
136
136
|
|
|
137
137
|
// TODO: use (or polyfill) flex gap
|
|
@@ -146,7 +146,7 @@ interface RadioGroupContext {
|
|
|
146
146
|
selected?: string
|
|
147
147
|
disabled: boolean
|
|
148
148
|
readonly: boolean
|
|
149
|
-
|
|
149
|
+
invalid: boolean
|
|
150
150
|
onChange: (next: string) => void
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -155,7 +155,7 @@ const RadioGroupContext = React.createContext<RadioGroupContext>({
|
|
|
155
155
|
selected: undefined,
|
|
156
156
|
disabled: false,
|
|
157
157
|
readonly: false,
|
|
158
|
-
|
|
158
|
+
invalid: false,
|
|
159
159
|
onChange() {
|
|
160
160
|
throw new Error(
|
|
161
161
|
'Cannot find onChange() handler. Perhaps you forgot to wrap with <RadioGroup> ?'
|
|
@@ -171,7 +171,7 @@ export function RadioGroup({
|
|
|
171
171
|
onChange,
|
|
172
172
|
disabled,
|
|
173
173
|
readonly,
|
|
174
|
-
|
|
174
|
+
invalid,
|
|
175
175
|
children,
|
|
176
176
|
}: RadioGroupProps) {
|
|
177
177
|
const handleChange = useCallback(
|
|
@@ -188,7 +188,7 @@ export function RadioGroup({
|
|
|
188
188
|
selected: value,
|
|
189
189
|
disabled: disabled ?? false,
|
|
190
190
|
readonly: readonly ?? false,
|
|
191
|
-
|
|
191
|
+
invalid: invalid ?? false,
|
|
192
192
|
onChange: handleChange,
|
|
193
193
|
}}
|
|
194
194
|
>
|
|
@@ -196,7 +196,7 @@ export function RadioGroup({
|
|
|
196
196
|
role="radiogroup"
|
|
197
197
|
aria-orientation="vertical"
|
|
198
198
|
aria-label={label}
|
|
199
|
-
aria-invalid={
|
|
199
|
+
aria-invalid={invalid}
|
|
200
200
|
className={className}
|
|
201
201
|
>
|
|
202
202
|
{children}
|
|
@@ -25,6 +25,7 @@ export type SegmentedControlProps = {
|
|
|
25
25
|
readonly disabled?: boolean
|
|
26
26
|
readonly readonly?: boolean
|
|
27
27
|
readonly required?: boolean
|
|
28
|
+
readonly className?: string
|
|
28
29
|
|
|
29
30
|
readonly value?: string
|
|
30
31
|
readonly defaultValue?: string
|
|
@@ -55,7 +56,11 @@ const SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
|
|
|
55
56
|
}, [props.data])
|
|
56
57
|
|
|
57
58
|
return (
|
|
58
|
-
<SegmentedControlRoot
|
|
59
|
+
<SegmentedControlRoot
|
|
60
|
+
ref={ref}
|
|
61
|
+
{...radioGroupProps}
|
|
62
|
+
className={props.className}
|
|
63
|
+
>
|
|
59
64
|
<RadioProvider value={state}>
|
|
60
65
|
{segmentedControlItems.map((item) => (
|
|
61
66
|
<Segmented
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { useSwitch } from '@react-aria/switch'
|
|
2
2
|
import type { AriaSwitchProps } from '@react-types/switch'
|
|
3
|
-
import {
|
|
3
|
+
import { useMemo, memo, forwardRef } from 'react'
|
|
4
4
|
import * as React from 'react'
|
|
5
5
|
import { useToggleState } from 'react-stately'
|
|
6
6
|
import styled from 'styled-components'
|
|
7
7
|
import { theme } from '../../styled'
|
|
8
8
|
import { disabledSelector } from '@charcoal-ui/utils'
|
|
9
|
+
import { useObjectRef } from '@react-aria/utils'
|
|
9
10
|
|
|
10
11
|
export type SwitchProps = {
|
|
11
12
|
name: string
|
|
@@ -24,37 +25,41 @@ export type SwitchProps = {
|
|
|
24
25
|
}
|
|
25
26
|
)
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
28
|
+
const SwitchCheckbox = forwardRef<HTMLInputElement, SwitchProps>(
|
|
29
|
+
function SwitchCheckboxInner(props, external) {
|
|
30
|
+
const { disabled, className } = props
|
|
31
|
+
|
|
32
|
+
const ariaSwitchProps: AriaSwitchProps = useMemo(
|
|
33
|
+
() => ({
|
|
34
|
+
...props,
|
|
35
|
+
|
|
36
|
+
// children がいない場合は aria-label をつけないといけない
|
|
37
|
+
'aria-label': 'children' in props ? undefined : props.label,
|
|
38
|
+
isDisabled: props.disabled,
|
|
39
|
+
isSelected: props.checked,
|
|
40
|
+
}),
|
|
41
|
+
[props]
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const state = useToggleState(ariaSwitchProps)
|
|
45
|
+
const ref = useObjectRef<HTMLInputElement>(external)
|
|
46
|
+
const {
|
|
47
|
+
inputProps: { className: _className, type: _type, ...rest },
|
|
48
|
+
} = useSwitch(ariaSwitchProps, state, ref)
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Label className={className} aria-disabled={disabled}>
|
|
52
|
+
<SwitchInput {...rest} ref={ref} />
|
|
53
|
+
{'children' in props ? (
|
|
54
|
+
// eslint-disable-next-line react/destructuring-assignment
|
|
55
|
+
<LabelInner>{props.children}</LabelInner>
|
|
56
|
+
) : undefined}
|
|
57
|
+
</Label>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
export default memo(SwitchCheckbox)
|
|
58
63
|
|
|
59
64
|
const Label = styled.label`
|
|
60
65
|
display: inline-grid;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { action } from '@storybook/addon-actions'
|
|
2
|
+
import styled from 'styled-components'
|
|
3
|
+
import { Story } from '../../_lib/compat'
|
|
4
|
+
import Clickable from '../Clickable'
|
|
5
|
+
import TextArea, { TextAreaProps } from '.'
|
|
6
|
+
import { px } from '@charcoal-ui/utils'
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
title: 'TextArea',
|
|
10
|
+
component: TextArea,
|
|
11
|
+
argTypes: {},
|
|
12
|
+
args: {
|
|
13
|
+
showLabel: false,
|
|
14
|
+
label: 'Label',
|
|
15
|
+
assistiveText: '',
|
|
16
|
+
disabled: false,
|
|
17
|
+
required: false,
|
|
18
|
+
invalid: false,
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const Container = styled.div`
|
|
23
|
+
display: grid;
|
|
24
|
+
gap: ${({ theme }) => px(theme.spacing[24])};
|
|
25
|
+
`
|
|
26
|
+
|
|
27
|
+
const Template: Story<Partial<TextAreaProps>> = (args) => (
|
|
28
|
+
<Container>
|
|
29
|
+
<TextArea
|
|
30
|
+
label="Label"
|
|
31
|
+
requiredText="*必須"
|
|
32
|
+
subLabel={
|
|
33
|
+
<Clickable onClick={action('label-click')}>Text Link</Clickable>
|
|
34
|
+
}
|
|
35
|
+
placeholder="Text Area"
|
|
36
|
+
{...args}
|
|
37
|
+
/>
|
|
38
|
+
</Container>
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
export const Default = Template.bind({})
|
|
42
|
+
|
|
43
|
+
export const HasLabel = Template.bind({})
|
|
44
|
+
HasLabel.args = {
|
|
45
|
+
showLabel: true,
|
|
46
|
+
assistiveText: 'Assistive text',
|
|
47
|
+
required: true,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const HasCount = Template.bind({})
|
|
51
|
+
HasCount.args = {
|
|
52
|
+
showCount: true,
|
|
53
|
+
maxLength: 100,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const AutoHeight: Story<Partial<TextAreaProps>> = (args) => (
|
|
57
|
+
<TextArea label="Label" placeholder="TextArea" {...args} />
|
|
58
|
+
)
|
|
59
|
+
AutoHeight.args = {
|
|
60
|
+
autoHeight: true,
|
|
61
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { useTextField } from '@react-aria/textfield'
|
|
2
|
+
import { useVisuallyHidden } from '@react-aria/visually-hidden'
|
|
3
|
+
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react'
|
|
4
|
+
import styled, { css } from 'styled-components'
|
|
5
|
+
import FieldLabel, { FieldLabelProps } from '../FieldLabel'
|
|
6
|
+
import { countCodePointsInString, mergeRefs } from '../../_lib'
|
|
7
|
+
import { ReactAreaUseTextFieldCompat } from '../../_lib/compat'
|
|
8
|
+
import { theme } from '../../styled'
|
|
9
|
+
|
|
10
|
+
type DOMProps = Omit<
|
|
11
|
+
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
|
12
|
+
// react-ariaのhookは、onChangeが`(v: string) => void`想定
|
|
13
|
+
| 'onChange'
|
|
14
|
+
// ReactAreaUseTextFieldCompatに書いてあるような事情で、ここにあるイベントハンドラの型をゆるめる
|
|
15
|
+
| keyof ReactAreaUseTextFieldCompat
|
|
16
|
+
>
|
|
17
|
+
|
|
18
|
+
export interface TextAreaProps
|
|
19
|
+
extends Pick<FieldLabelProps, 'label' | 'requiredText' | 'subLabel'>,
|
|
20
|
+
DOMProps,
|
|
21
|
+
ReactAreaUseTextFieldCompat {
|
|
22
|
+
readonly autoHeight?: boolean
|
|
23
|
+
readonly rows?: number
|
|
24
|
+
|
|
25
|
+
// <input> 要素は number とか string[] もありうるが、今はこれしか想定してない
|
|
26
|
+
readonly defaultValue?: string
|
|
27
|
+
readonly value?: string
|
|
28
|
+
readonly onChange?: (value: string) => void
|
|
29
|
+
|
|
30
|
+
// react-ariaの型定義のせいでHTMLTextAreaElementにできない
|
|
31
|
+
readonly onKeyDown?: (event: React.KeyboardEvent<Element>) => void
|
|
32
|
+
readonly onFocus?: (event: React.FocusEvent<Element>) => void
|
|
33
|
+
readonly onBlur?: (event: React.FocusEvent<Element>) => void
|
|
34
|
+
|
|
35
|
+
readonly showCount?: boolean
|
|
36
|
+
readonly showLabel?: boolean
|
|
37
|
+
readonly assistiveText?: string
|
|
38
|
+
readonly invalid?: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
|
42
|
+
function TextAreaInner({ onChange, ...props }, forwardRef) {
|
|
43
|
+
const {
|
|
44
|
+
className,
|
|
45
|
+
showCount = false,
|
|
46
|
+
showLabel = false,
|
|
47
|
+
label,
|
|
48
|
+
requiredText,
|
|
49
|
+
subLabel,
|
|
50
|
+
disabled = false,
|
|
51
|
+
required,
|
|
52
|
+
invalid = false,
|
|
53
|
+
assistiveText,
|
|
54
|
+
maxLength,
|
|
55
|
+
autoHeight = false,
|
|
56
|
+
rows: initialRows = 4,
|
|
57
|
+
} = props
|
|
58
|
+
|
|
59
|
+
const { visuallyHiddenProps } = useVisuallyHidden()
|
|
60
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
61
|
+
const ariaRef = useRef<HTMLTextAreaElement>(null)
|
|
62
|
+
const [count, setCount] = useState(
|
|
63
|
+
countCodePointsInString(props.value ?? '')
|
|
64
|
+
)
|
|
65
|
+
const [rows, setRows] = useState(initialRows)
|
|
66
|
+
|
|
67
|
+
const syncHeight = useCallback(
|
|
68
|
+
(textarea: HTMLTextAreaElement) => {
|
|
69
|
+
const rows = (`${textarea.value}\n`.match(/\n/gu)?.length ?? 0) || 1
|
|
70
|
+
setRows(initialRows <= rows ? rows : initialRows)
|
|
71
|
+
},
|
|
72
|
+
[initialRows]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const nonControlled = props.value === undefined
|
|
76
|
+
const handleChange = useCallback(
|
|
77
|
+
(value: string) => {
|
|
78
|
+
const count = countCodePointsInString(value)
|
|
79
|
+
if (maxLength !== undefined && count > maxLength) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
if (nonControlled) {
|
|
83
|
+
setCount(count)
|
|
84
|
+
}
|
|
85
|
+
if (autoHeight && textareaRef.current !== null) {
|
|
86
|
+
syncHeight(textareaRef.current)
|
|
87
|
+
}
|
|
88
|
+
onChange?.(value)
|
|
89
|
+
},
|
|
90
|
+
[autoHeight, maxLength, nonControlled, onChange, syncHeight]
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
setCount(countCodePointsInString(props.value ?? ''))
|
|
95
|
+
}, [props.value])
|
|
96
|
+
|
|
97
|
+
const { inputProps, labelProps, descriptionProps, errorMessageProps } =
|
|
98
|
+
useTextField(
|
|
99
|
+
{
|
|
100
|
+
inputElementType: 'textarea',
|
|
101
|
+
isDisabled: disabled,
|
|
102
|
+
isRequired: required,
|
|
103
|
+
validationState: invalid ? 'invalid' : 'valid',
|
|
104
|
+
description: !invalid && assistiveText,
|
|
105
|
+
errorMessage: invalid && assistiveText,
|
|
106
|
+
onChange: handleChange,
|
|
107
|
+
...props,
|
|
108
|
+
},
|
|
109
|
+
ariaRef
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (autoHeight && textareaRef.current !== null) {
|
|
114
|
+
syncHeight(textareaRef.current)
|
|
115
|
+
}
|
|
116
|
+
}, [autoHeight, syncHeight])
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<TextFieldRoot className={className} isDisabled={disabled}>
|
|
120
|
+
<TextFieldLabel
|
|
121
|
+
label={label}
|
|
122
|
+
requiredText={requiredText}
|
|
123
|
+
required={required}
|
|
124
|
+
subLabel={subLabel}
|
|
125
|
+
{...labelProps}
|
|
126
|
+
{...(!showLabel ? visuallyHiddenProps : {})}
|
|
127
|
+
/>
|
|
128
|
+
<StyledTextareaContainer
|
|
129
|
+
invalid={invalid}
|
|
130
|
+
rows={showCount ? rows + 1 : rows}
|
|
131
|
+
>
|
|
132
|
+
<StyledTextarea
|
|
133
|
+
ref={mergeRefs(textareaRef, forwardRef, ariaRef)}
|
|
134
|
+
rows={rows}
|
|
135
|
+
noBottomPadding={showCount}
|
|
136
|
+
{...inputProps}
|
|
137
|
+
/>
|
|
138
|
+
{showCount && (
|
|
139
|
+
<MultiLineCounter>
|
|
140
|
+
{maxLength !== undefined ? `${count}/${maxLength}` : count}
|
|
141
|
+
</MultiLineCounter>
|
|
142
|
+
)}
|
|
143
|
+
</StyledTextareaContainer>
|
|
144
|
+
{assistiveText != null && assistiveText.length !== 0 && (
|
|
145
|
+
<AssistiveText
|
|
146
|
+
invalid={invalid}
|
|
147
|
+
{...(invalid ? errorMessageProps : descriptionProps)}
|
|
148
|
+
>
|
|
149
|
+
{assistiveText}
|
|
150
|
+
</AssistiveText>
|
|
151
|
+
)}
|
|
152
|
+
</TextFieldRoot>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
export default TextArea
|
|
158
|
+
|
|
159
|
+
const TextFieldRoot = styled.div<{ isDisabled: boolean }>`
|
|
160
|
+
display: flex;
|
|
161
|
+
flex-direction: column;
|
|
162
|
+
|
|
163
|
+
${(p) => p.isDisabled && { opacity: p.theme.elementEffect.disabled.opacity }}
|
|
164
|
+
`
|
|
165
|
+
|
|
166
|
+
const TextFieldLabel = styled(FieldLabel)`
|
|
167
|
+
${theme((o) => o.margin.bottom(8))}
|
|
168
|
+
`
|
|
169
|
+
|
|
170
|
+
const StyledTextareaContainer = styled.div<{ rows: number; invalid: boolean }>`
|
|
171
|
+
position: relative;
|
|
172
|
+
overflow: hidden;
|
|
173
|
+
padding: 0 8px;
|
|
174
|
+
|
|
175
|
+
${(p) =>
|
|
176
|
+
theme((o) => [
|
|
177
|
+
o.bg.surface3.hover,
|
|
178
|
+
p.invalid && o.outline.assertive,
|
|
179
|
+
o.font.text2,
|
|
180
|
+
o.borderRadius(4),
|
|
181
|
+
])}
|
|
182
|
+
|
|
183
|
+
&:focus-within {
|
|
184
|
+
${(p) =>
|
|
185
|
+
theme((o) => (p.invalid ? o.outline.assertive : o.outline.default))}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
${({ rows }) => css`
|
|
189
|
+
height: calc(22px * ${rows} + 18px);
|
|
190
|
+
`};
|
|
191
|
+
`
|
|
192
|
+
|
|
193
|
+
const StyledTextarea = styled.textarea<{ noBottomPadding: boolean }>`
|
|
194
|
+
border: none;
|
|
195
|
+
outline: none;
|
|
196
|
+
resize: none;
|
|
197
|
+
font-family: inherit;
|
|
198
|
+
color: inherit;
|
|
199
|
+
|
|
200
|
+
/* Prevent zooming for iOS Safari */
|
|
201
|
+
transform-origin: top left;
|
|
202
|
+
transform: scale(0.875);
|
|
203
|
+
width: calc(100% / 0.875);
|
|
204
|
+
font-size: calc(14px / 0.875);
|
|
205
|
+
line-height: calc(22px / 0.875);
|
|
206
|
+
padding: calc(9px / 0.875) 0 ${(p) => (p.noBottomPadding ? 0 : '')};
|
|
207
|
+
|
|
208
|
+
${({ rows = 1 }) => css`
|
|
209
|
+
height: calc(22px / 0.875 * ${rows});
|
|
210
|
+
`};
|
|
211
|
+
|
|
212
|
+
/* Display box-shadow for iOS Safari */
|
|
213
|
+
appearance: none;
|
|
214
|
+
|
|
215
|
+
background: none;
|
|
216
|
+
|
|
217
|
+
&::placeholder {
|
|
218
|
+
${theme((o) => o.font.text3)}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* Hide scrollbar for Chrome, Safari and Opera */
|
|
222
|
+
&::-webkit-scrollbar {
|
|
223
|
+
display: none;
|
|
224
|
+
}
|
|
225
|
+
/* Hide scrollbar for IE, Edge and Firefox */
|
|
226
|
+
-ms-overflow-style: none; /* IE and Edge */
|
|
227
|
+
scrollbar-width: none; /* Firefox */
|
|
228
|
+
`
|
|
229
|
+
|
|
230
|
+
const MultiLineCounter = styled.span`
|
|
231
|
+
position: absolute;
|
|
232
|
+
bottom: 9px;
|
|
233
|
+
right: 8px;
|
|
234
|
+
|
|
235
|
+
${theme((o) => [o.typography(14).preserveHalfLeading, o.font.text3])}
|
|
236
|
+
`
|
|
237
|
+
|
|
238
|
+
const AssistiveText = styled.p<{ invalid: boolean }>`
|
|
239
|
+
${(p) =>
|
|
240
|
+
theme((o) => [
|
|
241
|
+
o.typography(14),
|
|
242
|
+
o.margin.top(8),
|
|
243
|
+
o.margin.bottom(0),
|
|
244
|
+
o.font[p.invalid ? 'assertive' : 'text1'],
|
|
245
|
+
])}
|
|
246
|
+
`
|
|
@@ -2,11 +2,7 @@ import { action } from '@storybook/addon-actions'
|
|
|
2
2
|
import styled from 'styled-components'
|
|
3
3
|
import { Story } from '../../_lib/compat'
|
|
4
4
|
import Clickable from '../Clickable'
|
|
5
|
-
import TextField, {
|
|
6
|
-
MultiLineTextFieldProps,
|
|
7
|
-
SingleLineTextFieldProps,
|
|
8
|
-
TextFieldProps,
|
|
9
|
-
} from '.'
|
|
5
|
+
import TextField, { TextFieldProps } from '.'
|
|
10
6
|
import { px } from '@charcoal-ui/utils'
|
|
11
7
|
import IconButton from '../IconButton'
|
|
12
8
|
|
|
@@ -37,19 +33,8 @@ const Template: Story<Partial<TextFieldProps>> = (args) => (
|
|
|
37
33
|
subLabel={
|
|
38
34
|
<Clickable onClick={action('label-click')}>Text Link</Clickable>
|
|
39
35
|
}
|
|
40
|
-
placeholder="
|
|
41
|
-
{...
|
|
42
|
-
multiline={false}
|
|
43
|
-
/>
|
|
44
|
-
<TextField
|
|
45
|
-
label="Label"
|
|
46
|
-
requiredText="*必須"
|
|
47
|
-
subLabel={
|
|
48
|
-
<Clickable onClick={action('label-click')}>Text Link</Clickable>
|
|
49
|
-
}
|
|
50
|
-
placeholder="Multi Line"
|
|
51
|
-
{...(args as Partial<MultiLineTextFieldProps>)}
|
|
52
|
-
multiline
|
|
36
|
+
placeholder="TextField"
|
|
37
|
+
{...args}
|
|
53
38
|
/>
|
|
54
39
|
</Container>
|
|
55
40
|
)
|
|
@@ -69,7 +54,7 @@ HasCount.args = {
|
|
|
69
54
|
maxLength: 100,
|
|
70
55
|
}
|
|
71
56
|
|
|
72
|
-
export const HasAffix: Story<Partial<
|
|
57
|
+
export const HasAffix: Story<Partial<TextFieldProps>> = (args) => (
|
|
73
58
|
<TextField label="Label" placeholder="path/to/your/file" {...args} />
|
|
74
59
|
)
|
|
75
60
|
HasAffix.args = {
|
|
@@ -79,14 +64,7 @@ HasAffix.args = {
|
|
|
79
64
|
suffix: '.png',
|
|
80
65
|
}
|
|
81
66
|
|
|
82
|
-
export const
|
|
83
|
-
<TextField label="Label" placeholder="Multi Line" {...args} multiline />
|
|
84
|
-
)
|
|
85
|
-
AutoHeight.args = {
|
|
86
|
-
autoHeight: true,
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export const PrefixIcon: Story<Partial<SingleLineTextFieldProps>> = (args) => (
|
|
67
|
+
export const PrefixIcon: Story<Partial<TextFieldProps>> = (args) => (
|
|
90
68
|
<TextField
|
|
91
69
|
label="Label"
|
|
92
70
|
placeholder="Icon prefix"
|
|
@@ -101,7 +79,7 @@ export const PrefixIcon: Story<Partial<SingleLineTextFieldProps>> = (args) => (
|
|
|
101
79
|
)
|
|
102
80
|
|
|
103
81
|
const PrefixIconWrap = styled.div`
|
|
104
|
-
color: ${({ theme }) => theme.color.
|
|
82
|
+
color: ${({ theme }) => theme.color.text3};
|
|
105
83
|
margin-top: 2px;
|
|
106
84
|
margin-right: 4px;
|
|
107
85
|
`
|