@charcoal-ui/react 2.0.0-alpha.2 → 2.0.0-alpha.21
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/README.md +16 -0
- 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 +6 -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/core/SSRProvider.d.ts +2 -0
- package/dist/core/SSRProvider.d.ts.map +1 -0
- package/dist/index.cjs +1060 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.modern.js +944 -45
- package/dist/index.modern.js.map +1 -1
- package/dist/index.module.js +1039 -1
- package/dist/index.module.js.map +1 -1
- package/package.json +11 -7
- 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 +31 -16
- package/src/components/TextField/index.tsx +51 -30
- package/src/components/a11y.test.tsx +11 -0
- package/src/core/SSRProvider.tsx +1 -0
- package/src/index.ts +4 -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,23 +25,20 @@ 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="*必須"
|
|
37
38
|
subLabel={
|
|
38
|
-
<Clickable
|
|
39
|
-
Text Link
|
|
40
|
-
</Clickable>
|
|
39
|
+
<Clickable onClick={action('label-click')}>Text Link</Clickable>
|
|
41
40
|
}
|
|
42
41
|
placeholder="Single Line"
|
|
43
|
-
onChange={action('change')}
|
|
44
42
|
{...(args as Partial<SingleLineTextFieldProps>)}
|
|
45
43
|
multiline={false}
|
|
46
44
|
/>
|
|
@@ -48,16 +46,13 @@ const Template: Story<Partial<TextFieldProps>> = (args) => (
|
|
|
48
46
|
label="Label"
|
|
49
47
|
requiredText="*必須"
|
|
50
48
|
subLabel={
|
|
51
|
-
<Clickable
|
|
52
|
-
Text Link
|
|
53
|
-
</Clickable>
|
|
49
|
+
<Clickable onClick={action('label-click')}>Text Link</Clickable>
|
|
54
50
|
}
|
|
55
51
|
placeholder="Multi Line"
|
|
56
|
-
onChange={action('change')}
|
|
57
52
|
{...(args as Partial<MultiLineTextFieldProps>)}
|
|
58
53
|
multiline
|
|
59
54
|
/>
|
|
60
|
-
</
|
|
55
|
+
</Container>
|
|
61
56
|
)
|
|
62
57
|
|
|
63
58
|
export const Default = Template.bind({})
|
|
@@ -91,3 +86,23 @@ export const AutoHeight: Story<Partial<MultiLineTextFieldProps>> = (args) => (
|
|
|
91
86
|
AutoHeight.args = {
|
|
92
87
|
autoHeight: true,
|
|
93
88
|
}
|
|
89
|
+
|
|
90
|
+
export const PrefixIcon: Story<Partial<SingleLineTextFieldProps>> = (args) => (
|
|
91
|
+
<TextField
|
|
92
|
+
label="Label"
|
|
93
|
+
placeholder="Icon prefix"
|
|
94
|
+
prefix={
|
|
95
|
+
<PrefixIconWrap>
|
|
96
|
+
<pixiv-icon name="16/Search" />
|
|
97
|
+
</PrefixIconWrap>
|
|
98
|
+
}
|
|
99
|
+
suffix={<IconButton variant="Overlay" icon={'16/Remove'} size="XS" />}
|
|
100
|
+
{...args}
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const PrefixIconWrap = styled.div`
|
|
105
|
+
color: ${({ theme }) => theme.color.text4};
|
|
106
|
+
margin-top: 2px;
|
|
107
|
+
margin-right: 4px;
|
|
108
|
+
`
|
|
@@ -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
|
|
|
@@ -13,6 +19,9 @@ interface TextFieldBaseProps
|
|
|
13
19
|
readonly defaultValue?: string
|
|
14
20
|
readonly value?: string
|
|
15
21
|
readonly onChange?: (value: string) => void
|
|
22
|
+
readonly onKeyDown?: (event: React.KeyboardEvent<Element>) => void
|
|
23
|
+
readonly onFocus?: (event: React.FocusEvent<Element>) => void
|
|
24
|
+
readonly onBlur?: (event: React.FocusEvent<Element>) => void
|
|
16
25
|
readonly showCount?: boolean
|
|
17
26
|
readonly showLabel?: boolean
|
|
18
27
|
readonly placeholder?: string
|
|
@@ -32,8 +41,8 @@ export interface SingleLineTextFieldProps extends TextFieldBaseProps {
|
|
|
32
41
|
readonly multiline?: false
|
|
33
42
|
readonly rows?: never
|
|
34
43
|
readonly type?: string
|
|
35
|
-
readonly prefix?:
|
|
36
|
-
readonly suffix?:
|
|
44
|
+
readonly prefix?: ReactNode
|
|
45
|
+
readonly suffix?: ReactNode
|
|
37
46
|
}
|
|
38
47
|
|
|
39
48
|
export interface MultiLineTextFieldProps extends TextFieldBaseProps {
|
|
@@ -94,8 +103,8 @@ const SingleLineTextField = React.forwardRef<
|
|
|
94
103
|
invalid = false,
|
|
95
104
|
assistiveText,
|
|
96
105
|
maxLength,
|
|
97
|
-
prefix =
|
|
98
|
-
suffix =
|
|
106
|
+
prefix = null,
|
|
107
|
+
suffix = null,
|
|
99
108
|
} = props
|
|
100
109
|
|
|
101
110
|
const { visuallyHiddenProps } = useVisuallyHidden()
|
|
@@ -184,9 +193,9 @@ const SingleLineTextField = React.forwardRef<
|
|
|
184
193
|
/>
|
|
185
194
|
<SuffixContainer ref={suffixRef}>
|
|
186
195
|
<Affix>{suffix}</Affix>
|
|
187
|
-
{showCount &&
|
|
196
|
+
{showCount && (
|
|
188
197
|
<SingleLineCounter>
|
|
189
|
-
{count}
|
|
198
|
+
{maxLength !== undefined ? `${count}/${maxLength}` : count}
|
|
190
199
|
</SingleLineCounter>
|
|
191
200
|
)}
|
|
192
201
|
</SuffixContainer>
|
|
@@ -290,16 +299,23 @@ const MultiLineTextField = React.forwardRef<
|
|
|
290
299
|
required={required}
|
|
291
300
|
subLabel={subLabel}
|
|
292
301
|
{...labelProps}
|
|
293
|
-
{...(showLabel ? visuallyHiddenProps : {})}
|
|
302
|
+
{...(!showLabel ? visuallyHiddenProps : {})}
|
|
294
303
|
/>
|
|
295
|
-
<StyledTextareaContainer
|
|
304
|
+
<StyledTextareaContainer
|
|
305
|
+
invalid={invalid}
|
|
306
|
+
rows={showCount ? rows + 1 : rows}
|
|
307
|
+
>
|
|
296
308
|
<StyledTextarea
|
|
297
309
|
ref={mergeRefs(textareaRef, forwardRef, ariaRef)}
|
|
298
|
-
invalid={invalid}
|
|
299
310
|
rows={rows}
|
|
311
|
+
noBottomPadding={showCount}
|
|
300
312
|
{...inputProps}
|
|
301
313
|
/>
|
|
302
|
-
{showCount &&
|
|
314
|
+
{showCount && (
|
|
315
|
+
<MultiLineCounter>
|
|
316
|
+
{maxLength !== undefined ? `${count}/${maxLength}` : count}
|
|
317
|
+
</MultiLineCounter>
|
|
318
|
+
)}
|
|
303
319
|
</StyledTextareaContainer>
|
|
304
320
|
{assistiveText != null && assistiveText.length !== 0 && (
|
|
305
321
|
<AssistiveText
|
|
@@ -370,8 +386,6 @@ const StyledInput = styled.input<{
|
|
|
370
386
|
height: calc(100% / 0.875);
|
|
371
387
|
font-size: calc(14px / 0.875);
|
|
372
388
|
line-height: calc(22px / 0.875);
|
|
373
|
-
padding-top: calc(9px / 0.875);
|
|
374
|
-
padding-bottom: calc(9px / 0.875);
|
|
375
389
|
padding-left: calc((8px + ${(p) => p.extraLeftPadding}px) / 0.875);
|
|
376
390
|
padding-right: calc((8px + ${(p) => p.extraRightPadding}px) / 0.875);
|
|
377
391
|
border-radius: calc(4px / 0.875);
|
|
@@ -392,21 +406,35 @@ const StyledInput = styled.input<{
|
|
|
392
406
|
}
|
|
393
407
|
`
|
|
394
408
|
|
|
395
|
-
const StyledTextareaContainer = styled.div<{ rows: number }>`
|
|
396
|
-
display: grid;
|
|
409
|
+
const StyledTextareaContainer = styled.div<{ rows: number; invalid: boolean }>`
|
|
397
410
|
position: relative;
|
|
411
|
+
overflow: hidden;
|
|
412
|
+
padding: 0 8px;
|
|
413
|
+
|
|
414
|
+
${(p) =>
|
|
415
|
+
theme((o) => [
|
|
416
|
+
o.bg.surface3.hover,
|
|
417
|
+
p.invalid && o.outline.assertive,
|
|
418
|
+
o.font.text2,
|
|
419
|
+
o.borderRadius(4),
|
|
420
|
+
])}
|
|
421
|
+
|
|
422
|
+
&:focus-within {
|
|
423
|
+
${(p) =>
|
|
424
|
+
theme((o) => (p.invalid ? o.outline.assertive : o.outline.default))}
|
|
425
|
+
}
|
|
398
426
|
|
|
399
427
|
${({ rows }) => css`
|
|
400
|
-
|
|
428
|
+
height: calc(22px * ${rows} + 18px);
|
|
401
429
|
`};
|
|
402
430
|
`
|
|
403
431
|
|
|
404
|
-
const StyledTextarea = styled.textarea<{
|
|
432
|
+
const StyledTextarea = styled.textarea<{ noBottomPadding: boolean }>`
|
|
405
433
|
border: none;
|
|
406
|
-
box-sizing: border-box;
|
|
407
434
|
outline: none;
|
|
408
435
|
resize: none;
|
|
409
436
|
font-family: inherit;
|
|
437
|
+
color: inherit;
|
|
410
438
|
|
|
411
439
|
/* Prevent zooming for iOS Safari */
|
|
412
440
|
transform-origin: top left;
|
|
@@ -414,23 +442,16 @@ const StyledTextarea = styled.textarea<{ invalid: boolean }>`
|
|
|
414
442
|
width: calc(100% / 0.875);
|
|
415
443
|
font-size: calc(14px / 0.875);
|
|
416
444
|
line-height: calc(22px / 0.875);
|
|
417
|
-
padding: calc(9px / 0.875)
|
|
418
|
-
border-radius: calc(4px / 0.875);
|
|
445
|
+
padding: calc(9px / 0.875) 0 ${(p) => (p.noBottomPadding ? 0 : '')};
|
|
419
446
|
|
|
420
|
-
${({ rows }) => css`
|
|
421
|
-
height: calc(22px / 0.875 * ${rows}
|
|
447
|
+
${({ rows = 1 }) => css`
|
|
448
|
+
height: calc(22px / 0.875 * ${rows});
|
|
422
449
|
`};
|
|
423
450
|
|
|
424
451
|
/* Display box-shadow for iOS Safari */
|
|
425
452
|
appearance: none;
|
|
426
453
|
|
|
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
|
-
])}
|
|
454
|
+
background: none;
|
|
434
455
|
|
|
435
456
|
&::placeholder {
|
|
436
457
|
${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) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SSRProvider } from '@react-aria/ssr'
|