@charcoal-ui/react 2.0.0-alpha.2 → 2.0.0-alpha.20
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/components/Icon/index.d.ts +12 -0
- package/dist/components/Icon/index.d.ts.map +1 -0
- package/dist/components/Icon/index.story.d.ts +24 -0
- package/dist/components/Icon/index.story.d.ts.map +1 -0
- package/dist/components/Modal/ModalPlumbing.d.ts +5 -0
- package/dist/components/Modal/ModalPlumbing.d.ts.map +1 -0
- package/dist/components/Modal/index.d.ts +16 -0
- package/dist/components/Modal/index.d.ts.map +1 -0
- package/dist/components/Modal/index.story.d.ts +33 -0
- package/dist/components/Modal/index.story.d.ts.map +1 -0
- package/dist/components/TextField/index.d.ts +3 -3
- package/dist/components/TextField/index.d.ts.map +1 -1
- package/dist/components/TextField/index.story.d.ts +1 -0
- 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 +35 -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/package.json +9 -6
- package/src/components/FieldLabel/index.tsx +1 -1
- package/src/components/Icon/index.story.tsx +29 -0
- package/src/components/Icon/index.tsx +33 -0
- package/src/components/Modal/ModalPlumbing.tsx +47 -0
- package/src/components/Modal/index.story.tsx +195 -0
- package/src/components/Modal/index.tsx +226 -0
- package/src/components/TextField/index.story.tsx +29 -8
- package/src/components/TextField/index.tsx +48 -30
- package/src/components/a11y.test.tsx +11 -0
- package/src/index.ts +1 -0
- package/src/styled.ts +1 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Story } from '../../_lib/compat'
|
|
3
|
+
import Modal, { ModalDismissButton, Props } from '.'
|
|
4
|
+
import { OverlayProvider } from '@react-aria/overlays'
|
|
5
|
+
import { useOverlayTriggerState } from 'react-stately'
|
|
6
|
+
import Button from '../Button'
|
|
7
|
+
import {
|
|
8
|
+
ModalAlign,
|
|
9
|
+
ModalBody,
|
|
10
|
+
ModalButtons,
|
|
11
|
+
ModalHeader,
|
|
12
|
+
} from './ModalPlumbing'
|
|
13
|
+
import styled from 'styled-components'
|
|
14
|
+
import { theme } from '../../styled'
|
|
15
|
+
import TextField from '../TextField'
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
title: 'Modal',
|
|
19
|
+
component: Modal,
|
|
20
|
+
args: {
|
|
21
|
+
title: 'Title',
|
|
22
|
+
},
|
|
23
|
+
argTypes: {
|
|
24
|
+
size: {
|
|
25
|
+
options: ['S', 'M', 'L'],
|
|
26
|
+
control: {
|
|
27
|
+
type: 'inline-radio',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
bottomSheet: {
|
|
31
|
+
options: ['full', 'true', 'false'],
|
|
32
|
+
mapping: { full: 'full', true: true, false: false },
|
|
33
|
+
control: {
|
|
34
|
+
type: 'inline-radio',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const DefaultStory = (args: Props) => {
|
|
41
|
+
const state = useOverlayTriggerState({})
|
|
42
|
+
return (
|
|
43
|
+
// Application must be wrapped in an OverlayProvider so that it can be
|
|
44
|
+
// hidden from screen readers when a modal opens.
|
|
45
|
+
<OverlayProvider>
|
|
46
|
+
<Button onClick={() => state.open()}>Open Modal</Button>
|
|
47
|
+
|
|
48
|
+
<Modal
|
|
49
|
+
isOpen={state.isOpen}
|
|
50
|
+
onClose={() => state.close()}
|
|
51
|
+
isDismissable
|
|
52
|
+
{...args}
|
|
53
|
+
>
|
|
54
|
+
<ModalHeader />
|
|
55
|
+
<ModalBody>
|
|
56
|
+
<ModalVStack>
|
|
57
|
+
<StyledModalText>
|
|
58
|
+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quod
|
|
59
|
+
placeat tenetur, necessitatibus laudantium cumque exercitationem
|
|
60
|
+
provident. Quaerat iure enim, eveniet dolores earum odio quo
|
|
61
|
+
possimus fugiat aspernatur, numquam, commodi repellat.
|
|
62
|
+
</StyledModalText>
|
|
63
|
+
<ModalAlign>
|
|
64
|
+
<TextField
|
|
65
|
+
showLabel
|
|
66
|
+
label="Name"
|
|
67
|
+
placeholder="Nagisa"
|
|
68
|
+
></TextField>
|
|
69
|
+
</ModalAlign>
|
|
70
|
+
<ModalAlign>
|
|
71
|
+
<TextField
|
|
72
|
+
showLabel
|
|
73
|
+
label="Country"
|
|
74
|
+
placeholder="Tokyo"
|
|
75
|
+
></TextField>
|
|
76
|
+
</ModalAlign>
|
|
77
|
+
</ModalVStack>
|
|
78
|
+
<ModalButtons>
|
|
79
|
+
<Button variant="Primary" onClick={() => state.close()} fixed>
|
|
80
|
+
Apply
|
|
81
|
+
</Button>
|
|
82
|
+
<Button onClick={() => state.close()} fixed>
|
|
83
|
+
Cancel
|
|
84
|
+
</Button>
|
|
85
|
+
</ModalButtons>
|
|
86
|
+
</ModalBody>
|
|
87
|
+
</Modal>
|
|
88
|
+
</OverlayProvider>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const ModalVStack = styled.div`
|
|
93
|
+
display: grid;
|
|
94
|
+
gap: 24px;
|
|
95
|
+
`
|
|
96
|
+
|
|
97
|
+
const StyledModalText = styled(ModalAlign)`
|
|
98
|
+
${theme((o) => [o.font.text2, o.typography(14)])}
|
|
99
|
+
`
|
|
100
|
+
|
|
101
|
+
export const Default: Story<Props> = DefaultStory.bind({})
|
|
102
|
+
|
|
103
|
+
const FullBottomSheetStory = (args: Props) => {
|
|
104
|
+
const state = useOverlayTriggerState({})
|
|
105
|
+
return (
|
|
106
|
+
// Application must be wrapped in an OverlayProvider so that it can be
|
|
107
|
+
// hidden from screen readers when a modal opens.
|
|
108
|
+
<OverlayProvider>
|
|
109
|
+
<Button onClick={() => state.open()}>Open Modal</Button>
|
|
110
|
+
|
|
111
|
+
<Modal
|
|
112
|
+
isOpen={state.isOpen}
|
|
113
|
+
onClose={() => state.close()}
|
|
114
|
+
isDismissable
|
|
115
|
+
bottomSheet="full"
|
|
116
|
+
{...args}
|
|
117
|
+
>
|
|
118
|
+
<ModalHeader />
|
|
119
|
+
<ModalBody>
|
|
120
|
+
<ModalVStack>
|
|
121
|
+
<StyledModalText>
|
|
122
|
+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quod
|
|
123
|
+
placeat tenetur, necessitatibus laudantium cumque exercitationem
|
|
124
|
+
provident. Quaerat iure enim, eveniet dolores earum odio quo
|
|
125
|
+
possimus fugiat aspernatur, numquam, commodi repellat.
|
|
126
|
+
</StyledModalText>
|
|
127
|
+
<ModalAlign>
|
|
128
|
+
<TextField
|
|
129
|
+
showLabel
|
|
130
|
+
label="Name"
|
|
131
|
+
placeholder="Nagisa"
|
|
132
|
+
></TextField>
|
|
133
|
+
</ModalAlign>
|
|
134
|
+
<ModalAlign>
|
|
135
|
+
<TextField
|
|
136
|
+
showLabel
|
|
137
|
+
label="Country"
|
|
138
|
+
placeholder="Tokyo"
|
|
139
|
+
></TextField>
|
|
140
|
+
</ModalAlign>
|
|
141
|
+
</ModalVStack>
|
|
142
|
+
<ModalButtons>
|
|
143
|
+
<Button variant="Primary" onClick={() => state.close()} fixed>
|
|
144
|
+
Apply
|
|
145
|
+
</Button>
|
|
146
|
+
<Button onClick={() => state.close()} fixed>
|
|
147
|
+
Cancel
|
|
148
|
+
</Button>
|
|
149
|
+
</ModalButtons>
|
|
150
|
+
</ModalBody>
|
|
151
|
+
</Modal>
|
|
152
|
+
</OverlayProvider>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export const FullBottomSheet: Story<Props> = FullBottomSheetStory.bind({})
|
|
157
|
+
|
|
158
|
+
const BottomSheetStory = (args: Props) => {
|
|
159
|
+
const state = useOverlayTriggerState({})
|
|
160
|
+
return (
|
|
161
|
+
// Application must be wrapped in an OverlayProvider so that it can be
|
|
162
|
+
// hidden from screen readers when a modal opens.
|
|
163
|
+
<OverlayProvider>
|
|
164
|
+
<Button onClick={() => state.open()}>Open Modal</Button>
|
|
165
|
+
|
|
166
|
+
<Modal
|
|
167
|
+
isOpen={state.isOpen}
|
|
168
|
+
onClose={() => state.close()}
|
|
169
|
+
bottomSheet
|
|
170
|
+
isDismissable
|
|
171
|
+
{...args}
|
|
172
|
+
>
|
|
173
|
+
<ModalHeader />
|
|
174
|
+
<ModalBody>
|
|
175
|
+
<ModalVStack>
|
|
176
|
+
<StyledModalText>
|
|
177
|
+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quod
|
|
178
|
+
placeat tenetur, necessitatibus laudantium cumque exercitationem
|
|
179
|
+
provident. Quaerat iure enim, eveniet dolores earum odio quo
|
|
180
|
+
possimus fugiat aspernatur, numquam, commodi repellat.
|
|
181
|
+
</StyledModalText>
|
|
182
|
+
</ModalVStack>
|
|
183
|
+
<ModalButtons>
|
|
184
|
+
<Button variant="Danger" onClick={() => state.close()} fixed>
|
|
185
|
+
削除する
|
|
186
|
+
</Button>
|
|
187
|
+
<ModalDismissButton>キャンセル</ModalDismissButton>
|
|
188
|
+
</ModalButtons>
|
|
189
|
+
</ModalBody>
|
|
190
|
+
</Modal>
|
|
191
|
+
</OverlayProvider>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export const BottomSheet: Story<Props> = BottomSheetStory.bind({})
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import React, { useContext, useRef } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
OverlayContainer,
|
|
4
|
+
OverlayProps,
|
|
5
|
+
useModal,
|
|
6
|
+
useOverlay,
|
|
7
|
+
usePreventScroll,
|
|
8
|
+
} from '@react-aria/overlays'
|
|
9
|
+
import styled, { css, useTheme } from 'styled-components'
|
|
10
|
+
import { theme } from '../../styled'
|
|
11
|
+
import { FocusScope } from '@react-aria/focus'
|
|
12
|
+
import { useDialog } from '@react-aria/dialog'
|
|
13
|
+
import { AriaDialogProps } from '@react-types/dialog'
|
|
14
|
+
import { columnSystem, COLUMN_UNIT, GUTTER_UNIT } from '@charcoal-ui/foundation'
|
|
15
|
+
import { unreachable } from '../../_lib'
|
|
16
|
+
import { maxWidth } from '@charcoal-ui/utils'
|
|
17
|
+
import { useMedia } from '@charcoal-ui/styled'
|
|
18
|
+
import { animated, useTransition, easings } from 'react-spring'
|
|
19
|
+
import Button, { ButtonProps } from '../Button'
|
|
20
|
+
import IconButton from '../IconButton'
|
|
21
|
+
|
|
22
|
+
export type Props = OverlayProps &
|
|
23
|
+
AriaDialogProps & {
|
|
24
|
+
children: React.ReactNode
|
|
25
|
+
zIndex?: number
|
|
26
|
+
title: string
|
|
27
|
+
size?: 'S' | 'M' | 'L'
|
|
28
|
+
bottomSheet?: boolean | 'full'
|
|
29
|
+
|
|
30
|
+
// NOTICE: デフォルト値を与えてはならない
|
|
31
|
+
// (たとえば document.body をデフォルト値にすると SSR できなくなる)
|
|
32
|
+
portalContainer?: HTMLElement
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_Z_INDEX = 10
|
|
36
|
+
|
|
37
|
+
export default function Modal({
|
|
38
|
+
children,
|
|
39
|
+
zIndex = DEFAULT_Z_INDEX,
|
|
40
|
+
portalContainer,
|
|
41
|
+
...props
|
|
42
|
+
}: Props) {
|
|
43
|
+
const {
|
|
44
|
+
title,
|
|
45
|
+
size = 'M',
|
|
46
|
+
bottomSheet = false,
|
|
47
|
+
isDismissable,
|
|
48
|
+
onClose,
|
|
49
|
+
isOpen = false,
|
|
50
|
+
} = props
|
|
51
|
+
|
|
52
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
53
|
+
const { overlayProps, underlayProps } = useOverlay(props, ref)
|
|
54
|
+
|
|
55
|
+
usePreventScroll()
|
|
56
|
+
const { modalProps } = useModal()
|
|
57
|
+
|
|
58
|
+
const { dialogProps, titleProps } = useDialog(props, ref)
|
|
59
|
+
|
|
60
|
+
const theme = useTheme()
|
|
61
|
+
const isMobile = useMedia(maxWidth(theme.breakpoint.screen1)) ?? false
|
|
62
|
+
const transitionEnabled = isMobile && bottomSheet !== false
|
|
63
|
+
const transition = useTransition(isOpen, {
|
|
64
|
+
from: {
|
|
65
|
+
transform: 'translateY(100%)',
|
|
66
|
+
backgroundColor: 'rgba(0, 0, 0, 0)',
|
|
67
|
+
},
|
|
68
|
+
enter: {
|
|
69
|
+
transform: 'translateY(0%)',
|
|
70
|
+
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
|
71
|
+
},
|
|
72
|
+
leave: {
|
|
73
|
+
transform: 'translateY(100%)',
|
|
74
|
+
backgroundColor: 'rgba(0, 0, 0, 0)',
|
|
75
|
+
},
|
|
76
|
+
config: transitionEnabled
|
|
77
|
+
? { duration: 400, easing: easings.easeOutQuart }
|
|
78
|
+
: { duration: 0 },
|
|
79
|
+
})
|
|
80
|
+
const showDismiss = !isMobile || bottomSheet !== true
|
|
81
|
+
|
|
82
|
+
return transition(
|
|
83
|
+
({ backgroundColor, transform }, item) =>
|
|
84
|
+
item && (
|
|
85
|
+
<OverlayContainer portalContainer={portalContainer}>
|
|
86
|
+
<ModalBackground
|
|
87
|
+
zIndex={zIndex}
|
|
88
|
+
{...underlayProps}
|
|
89
|
+
style={transitionEnabled ? { backgroundColor } : {}}
|
|
90
|
+
>
|
|
91
|
+
<FocusScope contain restoreFocus autoFocus>
|
|
92
|
+
<ModalDialog
|
|
93
|
+
ref={ref}
|
|
94
|
+
{...overlayProps}
|
|
95
|
+
{...modalProps}
|
|
96
|
+
{...dialogProps}
|
|
97
|
+
style={transitionEnabled ? { transform } : {}}
|
|
98
|
+
size={size}
|
|
99
|
+
bottomSheet={bottomSheet}
|
|
100
|
+
>
|
|
101
|
+
<ModalContext.Provider
|
|
102
|
+
value={{ titleProps, title, close: onClose, showDismiss }}
|
|
103
|
+
>
|
|
104
|
+
{children}
|
|
105
|
+
{isDismissable === true && (
|
|
106
|
+
<ModalCrossButton
|
|
107
|
+
size="S"
|
|
108
|
+
icon="24/Close"
|
|
109
|
+
onClick={onClose}
|
|
110
|
+
/>
|
|
111
|
+
)}
|
|
112
|
+
</ModalContext.Provider>
|
|
113
|
+
</ModalDialog>
|
|
114
|
+
</FocusScope>
|
|
115
|
+
</ModalBackground>
|
|
116
|
+
</OverlayContainer>
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const ModalContext = React.createContext<{
|
|
122
|
+
titleProps: React.HTMLAttributes<HTMLElement>
|
|
123
|
+
title: string
|
|
124
|
+
close?: () => void
|
|
125
|
+
showDismiss: boolean
|
|
126
|
+
}>({
|
|
127
|
+
titleProps: {},
|
|
128
|
+
title: '',
|
|
129
|
+
close: undefined,
|
|
130
|
+
showDismiss: true,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const ModalBackground = animated(styled.div<{ zIndex: number }>`
|
|
134
|
+
z-index: ${({ zIndex }) => zIndex};
|
|
135
|
+
position: fixed;
|
|
136
|
+
top: 0;
|
|
137
|
+
left: 0;
|
|
138
|
+
width: 100%;
|
|
139
|
+
height: 100%;
|
|
140
|
+
|
|
141
|
+
${theme((o) => [o.bg.surface4])}
|
|
142
|
+
`)
|
|
143
|
+
|
|
144
|
+
const ModalDialog = animated(styled.div<{
|
|
145
|
+
size: 'S' | 'M' | 'L'
|
|
146
|
+
bottomSheet: boolean | 'full'
|
|
147
|
+
}>`
|
|
148
|
+
position: absolute;
|
|
149
|
+
top: 50%;
|
|
150
|
+
left: 50%;
|
|
151
|
+
transform: translate(-50%, -50%);
|
|
152
|
+
width: ${(p) =>
|
|
153
|
+
p.size === 'S'
|
|
154
|
+
? columnSystem(3, COLUMN_UNIT, GUTTER_UNIT) + GUTTER_UNIT * 2
|
|
155
|
+
: p.size === 'M'
|
|
156
|
+
? columnSystem(4, COLUMN_UNIT, GUTTER_UNIT) + GUTTER_UNIT * 2
|
|
157
|
+
: // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
158
|
+
p.size === 'L'
|
|
159
|
+
? columnSystem(6, COLUMN_UNIT, GUTTER_UNIT) + GUTTER_UNIT * 2
|
|
160
|
+
: unreachable(p.size)}px;
|
|
161
|
+
|
|
162
|
+
${theme((o) => [o.bg.background1, o.borderRadius(24)])}
|
|
163
|
+
|
|
164
|
+
@media ${({ theme }) => maxWidth(theme.breakpoint.screen1)} {
|
|
165
|
+
${(p) =>
|
|
166
|
+
p.bottomSheet === 'full'
|
|
167
|
+
? css`
|
|
168
|
+
top: auto;
|
|
169
|
+
bottom: 0;
|
|
170
|
+
left: 0;
|
|
171
|
+
transform: none;
|
|
172
|
+
border-radius: 0;
|
|
173
|
+
width: 100%;
|
|
174
|
+
height: 100%;
|
|
175
|
+
`
|
|
176
|
+
: p.bottomSheet
|
|
177
|
+
? css`
|
|
178
|
+
top: auto;
|
|
179
|
+
bottom: 0;
|
|
180
|
+
left: 0;
|
|
181
|
+
transform: none;
|
|
182
|
+
border-radius: 0;
|
|
183
|
+
width: 100%;
|
|
184
|
+
`
|
|
185
|
+
: css`
|
|
186
|
+
width: calc(100% - 48px);
|
|
187
|
+
`}
|
|
188
|
+
}
|
|
189
|
+
`)
|
|
190
|
+
|
|
191
|
+
const ModalCrossButton = styled(IconButton)`
|
|
192
|
+
position: absolute;
|
|
193
|
+
top: 8px;
|
|
194
|
+
right: 8px;
|
|
195
|
+
|
|
196
|
+
${theme((o) => [o.font.text3.hover.press])}
|
|
197
|
+
`
|
|
198
|
+
|
|
199
|
+
export function ModalTitle(props: React.HTMLAttributes<HTMLHeadingElement>) {
|
|
200
|
+
const { titleProps, title } = useContext(ModalContext)
|
|
201
|
+
return (
|
|
202
|
+
<ModalHeading {...titleProps} {...props}>
|
|
203
|
+
{title}
|
|
204
|
+
</ModalHeading>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const ModalHeading = styled.h3`
|
|
209
|
+
margin: 0;
|
|
210
|
+
font-weight: inherit;
|
|
211
|
+
font-size: inherit;
|
|
212
|
+
`
|
|
213
|
+
|
|
214
|
+
export function ModalDismissButton({ children, ...props }: ButtonProps) {
|
|
215
|
+
const { close, showDismiss } = useContext(ModalContext)
|
|
216
|
+
|
|
217
|
+
if (!showDismiss) {
|
|
218
|
+
return null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<Button {...props} onClick={close} fixed>
|
|
223
|
+
{children}
|
|
224
|
+
</Button>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { action } from '@storybook/addon-actions'
|
|
2
2
|
import React from 'react'
|
|
3
|
-
import
|
|
3
|
+
import styled from 'styled-components'
|
|
4
4
|
import { Story } from '../../_lib/compat'
|
|
5
5
|
import Clickable from '../Clickable'
|
|
6
6
|
import TextField, {
|
|
@@ -9,6 +9,7 @@ import TextField, {
|
|
|
9
9
|
TextFieldProps,
|
|
10
10
|
} from '.'
|
|
11
11
|
import { px } from '@charcoal-ui/utils'
|
|
12
|
+
import IconButton from '../IconButton'
|
|
12
13
|
|
|
13
14
|
export default {
|
|
14
15
|
title: 'TextField',
|
|
@@ -24,13 +25,13 @@ export default {
|
|
|
24
25
|
},
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
const Container = styled.div`
|
|
29
|
+
display: grid;
|
|
30
|
+
gap: ${({ theme }) => px(theme.spacing[24])};
|
|
31
|
+
`
|
|
32
|
+
|
|
27
33
|
const Template: Story<Partial<TextFieldProps>> = (args) => (
|
|
28
|
-
<
|
|
29
|
-
css={css`
|
|
30
|
-
display: grid;
|
|
31
|
-
gap: ${({ theme }) => px(theme.spacing[24])};
|
|
32
|
-
`}
|
|
33
|
-
>
|
|
34
|
+
<Container>
|
|
34
35
|
<TextField
|
|
35
36
|
label="Label"
|
|
36
37
|
requiredText="*必須"
|
|
@@ -57,7 +58,7 @@ const Template: Story<Partial<TextFieldProps>> = (args) => (
|
|
|
57
58
|
{...(args as Partial<MultiLineTextFieldProps>)}
|
|
58
59
|
multiline
|
|
59
60
|
/>
|
|
60
|
-
</
|
|
61
|
+
</Container>
|
|
61
62
|
)
|
|
62
63
|
|
|
63
64
|
export const Default = Template.bind({})
|
|
@@ -91,3 +92,23 @@ export const AutoHeight: Story<Partial<MultiLineTextFieldProps>> = (args) => (
|
|
|
91
92
|
AutoHeight.args = {
|
|
92
93
|
autoHeight: true,
|
|
93
94
|
}
|
|
95
|
+
|
|
96
|
+
export const PrefixIcon: Story<Partial<SingleLineTextFieldProps>> = (args) => (
|
|
97
|
+
<TextField
|
|
98
|
+
label="Label"
|
|
99
|
+
placeholder="Icon prefix"
|
|
100
|
+
prefix={
|
|
101
|
+
<PrefixIconWrap>
|
|
102
|
+
<pixiv-icon name="16/Search" />
|
|
103
|
+
</PrefixIconWrap>
|
|
104
|
+
}
|
|
105
|
+
suffix={<IconButton variant="Overlay" icon={'16/Remove'} size="XS" />}
|
|
106
|
+
{...args}
|
|
107
|
+
/>
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const PrefixIconWrap = styled.div`
|
|
111
|
+
color: ${({ theme }) => theme.color.text4};
|
|
112
|
+
margin-top: 2px;
|
|
113
|
+
margin-right: 4px;
|
|
114
|
+
`
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { useTextField } from '@react-aria/textfield'
|
|
2
2
|
import { useVisuallyHidden } from '@react-aria/visually-hidden'
|
|
3
|
-
import React, {
|
|
3
|
+
import React, {
|
|
4
|
+
ReactNode,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react'
|
|
4
10
|
import styled, { css } from 'styled-components'
|
|
5
11
|
import FieldLabel, { FieldLabelProps } from '../FieldLabel'
|
|
6
|
-
import createTheme from '@charcoal-ui/styled'
|
|
12
|
+
import { createTheme } from '@charcoal-ui/styled'
|
|
7
13
|
|
|
8
14
|
const theme = createTheme(styled)
|
|
9
15
|
|
|
@@ -32,8 +38,8 @@ export interface SingleLineTextFieldProps extends TextFieldBaseProps {
|
|
|
32
38
|
readonly multiline?: false
|
|
33
39
|
readonly rows?: never
|
|
34
40
|
readonly type?: string
|
|
35
|
-
readonly prefix?:
|
|
36
|
-
readonly suffix?:
|
|
41
|
+
readonly prefix?: ReactNode
|
|
42
|
+
readonly suffix?: ReactNode
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
export interface MultiLineTextFieldProps extends TextFieldBaseProps {
|
|
@@ -94,8 +100,8 @@ const SingleLineTextField = React.forwardRef<
|
|
|
94
100
|
invalid = false,
|
|
95
101
|
assistiveText,
|
|
96
102
|
maxLength,
|
|
97
|
-
prefix =
|
|
98
|
-
suffix =
|
|
103
|
+
prefix = null,
|
|
104
|
+
suffix = null,
|
|
99
105
|
} = props
|
|
100
106
|
|
|
101
107
|
const { visuallyHiddenProps } = useVisuallyHidden()
|
|
@@ -184,9 +190,9 @@ const SingleLineTextField = React.forwardRef<
|
|
|
184
190
|
/>
|
|
185
191
|
<SuffixContainer ref={suffixRef}>
|
|
186
192
|
<Affix>{suffix}</Affix>
|
|
187
|
-
{showCount &&
|
|
193
|
+
{showCount && (
|
|
188
194
|
<SingleLineCounter>
|
|
189
|
-
{count}
|
|
195
|
+
{maxLength !== undefined ? `${count}/${maxLength}` : count}
|
|
190
196
|
</SingleLineCounter>
|
|
191
197
|
)}
|
|
192
198
|
</SuffixContainer>
|
|
@@ -290,16 +296,23 @@ const MultiLineTextField = React.forwardRef<
|
|
|
290
296
|
required={required}
|
|
291
297
|
subLabel={subLabel}
|
|
292
298
|
{...labelProps}
|
|
293
|
-
{...(showLabel ? visuallyHiddenProps : {})}
|
|
299
|
+
{...(!showLabel ? visuallyHiddenProps : {})}
|
|
294
300
|
/>
|
|
295
|
-
<StyledTextareaContainer
|
|
301
|
+
<StyledTextareaContainer
|
|
302
|
+
invalid={invalid}
|
|
303
|
+
rows={showCount ? rows + 1 : rows}
|
|
304
|
+
>
|
|
296
305
|
<StyledTextarea
|
|
297
306
|
ref={mergeRefs(textareaRef, forwardRef, ariaRef)}
|
|
298
|
-
invalid={invalid}
|
|
299
307
|
rows={rows}
|
|
308
|
+
noBottomPadding={showCount}
|
|
300
309
|
{...inputProps}
|
|
301
310
|
/>
|
|
302
|
-
{showCount &&
|
|
311
|
+
{showCount && (
|
|
312
|
+
<MultiLineCounter>
|
|
313
|
+
{maxLength !== undefined ? `${count}/${maxLength}` : count}
|
|
314
|
+
</MultiLineCounter>
|
|
315
|
+
)}
|
|
303
316
|
</StyledTextareaContainer>
|
|
304
317
|
{assistiveText != null && assistiveText.length !== 0 && (
|
|
305
318
|
<AssistiveText
|
|
@@ -370,8 +383,6 @@ const StyledInput = styled.input<{
|
|
|
370
383
|
height: calc(100% / 0.875);
|
|
371
384
|
font-size: calc(14px / 0.875);
|
|
372
385
|
line-height: calc(22px / 0.875);
|
|
373
|
-
padding-top: calc(9px / 0.875);
|
|
374
|
-
padding-bottom: calc(9px / 0.875);
|
|
375
386
|
padding-left: calc((8px + ${(p) => p.extraLeftPadding}px) / 0.875);
|
|
376
387
|
padding-right: calc((8px + ${(p) => p.extraRightPadding}px) / 0.875);
|
|
377
388
|
border-radius: calc(4px / 0.875);
|
|
@@ -392,21 +403,35 @@ const StyledInput = styled.input<{
|
|
|
392
403
|
}
|
|
393
404
|
`
|
|
394
405
|
|
|
395
|
-
const StyledTextareaContainer = styled.div<{ rows: number }>`
|
|
396
|
-
display: grid;
|
|
406
|
+
const StyledTextareaContainer = styled.div<{ rows: number; invalid: boolean }>`
|
|
397
407
|
position: relative;
|
|
408
|
+
overflow: hidden;
|
|
409
|
+
padding: 0 8px;
|
|
410
|
+
|
|
411
|
+
${(p) =>
|
|
412
|
+
theme((o) => [
|
|
413
|
+
o.bg.surface3.hover,
|
|
414
|
+
p.invalid && o.outline.assertive,
|
|
415
|
+
o.font.text2,
|
|
416
|
+
o.borderRadius(4),
|
|
417
|
+
])}
|
|
418
|
+
|
|
419
|
+
&:focus-within {
|
|
420
|
+
${(p) =>
|
|
421
|
+
theme((o) => (p.invalid ? o.outline.assertive : o.outline.default))}
|
|
422
|
+
}
|
|
398
423
|
|
|
399
424
|
${({ rows }) => css`
|
|
400
|
-
|
|
425
|
+
height: calc(22px * ${rows} + 18px);
|
|
401
426
|
`};
|
|
402
427
|
`
|
|
403
428
|
|
|
404
|
-
const StyledTextarea = styled.textarea<{
|
|
429
|
+
const StyledTextarea = styled.textarea<{ noBottomPadding: boolean }>`
|
|
405
430
|
border: none;
|
|
406
|
-
box-sizing: border-box;
|
|
407
431
|
outline: none;
|
|
408
432
|
resize: none;
|
|
409
433
|
font-family: inherit;
|
|
434
|
+
color: inherit;
|
|
410
435
|
|
|
411
436
|
/* Prevent zooming for iOS Safari */
|
|
412
437
|
transform-origin: top left;
|
|
@@ -414,23 +439,16 @@ const StyledTextarea = styled.textarea<{ invalid: boolean }>`
|
|
|
414
439
|
width: calc(100% / 0.875);
|
|
415
440
|
font-size: calc(14px / 0.875);
|
|
416
441
|
line-height: calc(22px / 0.875);
|
|
417
|
-
padding: calc(9px / 0.875)
|
|
418
|
-
border-radius: calc(4px / 0.875);
|
|
442
|
+
padding: calc(9px / 0.875) 0 ${(p) => (p.noBottomPadding ? 0 : '')};
|
|
419
443
|
|
|
420
|
-
${({ rows }) => css`
|
|
421
|
-
height: calc(22px / 0.875 * ${rows}
|
|
444
|
+
${({ rows = 1 }) => css`
|
|
445
|
+
height: calc(22px / 0.875 * ${rows});
|
|
422
446
|
`};
|
|
423
447
|
|
|
424
448
|
/* Display box-shadow for iOS Safari */
|
|
425
449
|
appearance: none;
|
|
426
450
|
|
|
427
|
-
|
|
428
|
-
theme((o) => [
|
|
429
|
-
o.bg.surface3.hover,
|
|
430
|
-
o.outline.default.focus,
|
|
431
|
-
p.invalid && o.outline.assertive,
|
|
432
|
-
o.font.text2,
|
|
433
|
-
])}
|
|
451
|
+
background: none;
|
|
434
452
|
|
|
435
453
|
&::placeholder {
|
|
436
454
|
${theme((o) => o.font.text3)}
|
|
@@ -63,6 +63,17 @@ beforeEach(() => {
|
|
|
63
63
|
return null
|
|
64
64
|
},
|
|
65
65
|
}))
|
|
66
|
+
|
|
67
|
+
global.matchMedia = jest.fn().mockImplementation(() => ({
|
|
68
|
+
matches: true,
|
|
69
|
+
media: '(max-width: 600px)',
|
|
70
|
+
addEventListener() {
|
|
71
|
+
// Do Nothing
|
|
72
|
+
},
|
|
73
|
+
removeEventListener() {
|
|
74
|
+
// Do Nothing
|
|
75
|
+
},
|
|
76
|
+
}))
|
|
66
77
|
})
|
|
67
78
|
|
|
68
79
|
describe.each(themes)('using %s theme', (_name, theme) => {
|
package/src/index.ts
CHANGED
package/src/styled.ts
CHANGED