@charcoal-ui/react 2.0.0-alpha.9 → 2.0.0-rc.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/README.md +16 -0
- package/dist/components/Checkbox/index.d.ts +21 -0
- package/dist/components/Checkbox/index.d.ts.map +1 -0
- package/dist/components/Checkbox/index.story.d.ts +16 -0
- package/dist/components/Checkbox/index.story.d.ts.map +1 -0
- package/dist/components/DropdownSelector/Listbox.d.ts +10 -0
- package/dist/components/DropdownSelector/Listbox.d.ts.map +1 -0
- package/dist/components/DropdownSelector/Popover.d.ts +10 -0
- package/dist/components/DropdownSelector/Popover.d.ts.map +1 -0
- package/dist/components/DropdownSelector/index.d.ts +32 -0
- package/dist/components/DropdownSelector/index.d.ts.map +1 -0
- package/dist/components/DropdownSelector/index.story.d.ts +22 -0
- package/dist/components/DropdownSelector/index.story.d.ts.map +1 -0
- package/dist/components/Icon/index.story.d.ts +1 -1
- package/dist/components/LoadingSpinner/index.d.ts +15 -0
- package/dist/components/LoadingSpinner/index.d.ts.map +1 -0
- package/dist/components/LoadingSpinner/index.story.d.ts +10 -0
- package/dist/components/LoadingSpinner/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/{Select → MultiSelect}/context.d.ts +2 -2
- package/dist/components/MultiSelect/context.d.ts.map +1 -0
- package/dist/components/MultiSelect/index.d.ts +24 -0
- package/dist/components/MultiSelect/index.d.ts.map +1 -0
- package/dist/components/{Select → MultiSelect}/index.story.d.ts +2 -2
- package/dist/components/MultiSelect/index.story.d.ts.map +1 -0
- package/dist/components/{Select → MultiSelect}/index.test.d.ts +0 -0
- package/dist/components/MultiSelect/index.test.d.ts.map +1 -0
- package/dist/components/SegmentedControl/RadioGroupContext.d.ts +9 -0
- package/dist/components/SegmentedControl/RadioGroupContext.d.ts.map +1 -0
- package/dist/components/SegmentedControl/index.d.ts +20 -0
- package/dist/components/SegmentedControl/index.d.ts.map +1 -0
- package/dist/components/SegmentedControl/index.story.d.ts +11 -0
- package/dist/components/SegmentedControl/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 +22686 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.modern.js +21612 -45
- package/dist/index.modern.js.map +1 -1
- package/dist/index.module.js +22641 -1
- package/dist/index.module.js.map +1 -1
- package/package.json +15 -8
- package/src/components/Checkbox/index.story.tsx +64 -0
- package/src/components/Checkbox/index.tsx +122 -0
- package/src/components/DropdownSelector/Listbox.tsx +127 -0
- package/src/components/DropdownSelector/Popover.tsx +46 -0
- package/src/components/DropdownSelector/index.story.tsx +134 -0
- package/src/components/DropdownSelector/index.tsx +214 -0
- package/src/components/FieldLabel/index.tsx +1 -1
- package/src/components/LoadingSpinner/index.story.tsx +52 -0
- package/src/components/LoadingSpinner/index.tsx +87 -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/{Select → MultiSelect}/context.ts +3 -3
- package/src/components/{Select → MultiSelect}/index.story.tsx +16 -12
- package/src/components/{Select → MultiSelect}/index.test.tsx +13 -13
- package/src/components/{Select → MultiSelect}/index.tsx +24 -21
- package/src/components/SegmentedControl/RadioGroupContext.tsx +22 -0
- package/src/components/SegmentedControl/index.story.tsx +36 -0
- package/src/components/SegmentedControl/index.tsx +157 -0
- package/src/components/TextField/index.story.tsx +31 -16
- package/src/components/TextField/index.tsx +53 -34
- package/src/components/a11y.test.tsx +11 -0
- package/src/core/SSRProvider.tsx +1 -0
- package/src/index.ts +22 -5
- package/src/styled.ts +1 -1
- package/dist/components/Select/context.d.ts.map +0 -1
- package/dist/components/Select/index.d.ts +0 -24
- package/dist/components/Select/index.d.ts.map +0 -1
- package/dist/components/Select/index.story.d.ts.map +0 -1
- package/dist/components/Select/index.test.d.ts.map +0 -1
|
@@ -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 ModalProps = 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
|
+
}: ModalProps) {
|
|
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 { createContext } from 'react'
|
|
2
2
|
|
|
3
|
-
type
|
|
3
|
+
type MultiSelectGroupContext = {
|
|
4
4
|
name: string
|
|
5
5
|
selected: string[]
|
|
6
6
|
disabled: boolean
|
|
@@ -9,7 +9,7 @@ type SelectGroupContext = {
|
|
|
9
9
|
onChange: ({ value, selected }: { value: string; selected: boolean }) => void
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export const
|
|
12
|
+
export const MultiSelectGroupContext = createContext<MultiSelectGroupContext>({
|
|
13
13
|
name: undefined as never,
|
|
14
14
|
selected: [],
|
|
15
15
|
disabled: false,
|
|
@@ -17,7 +17,7 @@ export const SelectGroupContext = createContext<SelectGroupContext>({
|
|
|
17
17
|
hasError: false,
|
|
18
18
|
onChange() {
|
|
19
19
|
throw new Error(
|
|
20
|
-
'Cannot find `onChange()` handler. Perhaps you forgot to wrap it with `<
|
|
20
|
+
'Cannot find `onChange()` handler. Perhaps you forgot to wrap it with `<MultiSelectGroup />` ?'
|
|
21
21
|
)
|
|
22
22
|
},
|
|
23
23
|
})
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
2
|
import { Story } from '../../_lib/compat'
|
|
3
3
|
import styled from 'styled-components'
|
|
4
|
-
import {
|
|
4
|
+
import { MultiSelectGroup, default as MultiSelect } from '.'
|
|
5
5
|
|
|
6
6
|
export default {
|
|
7
|
-
title: '
|
|
8
|
-
component:
|
|
7
|
+
title: 'MultiSelect',
|
|
8
|
+
component: MultiSelect,
|
|
9
9
|
argTypes: {
|
|
10
10
|
name: {
|
|
11
11
|
control: {
|
|
@@ -63,7 +63,7 @@ type Props = {
|
|
|
63
63
|
variant?: 'default' | 'overlay'
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
const
|
|
66
|
+
const StyledMultiSelectGroup = styled(MultiSelectGroup)`
|
|
67
67
|
display: grid;
|
|
68
68
|
grid-template-columns: 1fr;
|
|
69
69
|
gap: 8px;
|
|
@@ -81,7 +81,7 @@ const Template: Story<Props> = ({
|
|
|
81
81
|
variant,
|
|
82
82
|
}) => {
|
|
83
83
|
return (
|
|
84
|
-
<
|
|
84
|
+
<StyledMultiSelectGroup
|
|
85
85
|
{...{
|
|
86
86
|
name,
|
|
87
87
|
ariaLabel,
|
|
@@ -94,16 +94,16 @@ const Template: Story<Props> = ({
|
|
|
94
94
|
selected={selected ? ['選択肢1', '選択肢3'] : []}
|
|
95
95
|
>
|
|
96
96
|
{[1, 2, 3, 4].map((idx) => (
|
|
97
|
-
<
|
|
97
|
+
<MultiSelect
|
|
98
98
|
value={`選択肢${idx}`}
|
|
99
99
|
forceChecked={firstOptionForceChecked && idx === 1}
|
|
100
100
|
variant={variant}
|
|
101
101
|
key={idx}
|
|
102
102
|
>
|
|
103
103
|
選択肢{idx}
|
|
104
|
-
</
|
|
104
|
+
</MultiSelect>
|
|
105
105
|
))}
|
|
106
|
-
</
|
|
106
|
+
</StyledMultiSelectGroup>
|
|
107
107
|
)
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -134,13 +134,17 @@ export const Playground: Story<PlaygroundProps> = (props) => {
|
|
|
134
134
|
const [selected, setSelected] = useState<string[]>([])
|
|
135
135
|
|
|
136
136
|
return (
|
|
137
|
-
<
|
|
137
|
+
<StyledMultiSelectGroup
|
|
138
|
+
{...props}
|
|
139
|
+
selected={selected}
|
|
140
|
+
onChange={setSelected}
|
|
141
|
+
>
|
|
138
142
|
{[1, 2, 3, 4].map((idx) => (
|
|
139
|
-
<
|
|
143
|
+
<MultiSelect value={`選択肢${idx}`} variant={props.variant} key={idx}>
|
|
140
144
|
選択肢{idx}
|
|
141
|
-
</
|
|
145
|
+
</MultiSelect>
|
|
142
146
|
))}
|
|
143
|
-
</
|
|
147
|
+
</StyledMultiSelectGroup>
|
|
144
148
|
)
|
|
145
149
|
}
|
|
146
150
|
Playground.args = {
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import { fireEvent, render, screen } from '@testing-library/react'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { ThemeProvider } from 'styled-components'
|
|
4
|
-
import { default as
|
|
4
|
+
import { default as MultiSelect, MultiSelectGroup } from '.'
|
|
5
5
|
import { light } from '@charcoal-ui/theme'
|
|
6
6
|
|
|
7
|
-
describe('
|
|
7
|
+
describe('MultiSelect', () => {
|
|
8
8
|
describe('in development mode', () => {
|
|
9
9
|
beforeEach(() => {
|
|
10
10
|
process.env.NODE_ENV = 'development'
|
|
11
11
|
})
|
|
12
12
|
|
|
13
|
-
describe('when `<
|
|
13
|
+
describe('when `<MultiSelect />` is used without `<MultiSelectGroup />`', () => {
|
|
14
14
|
beforeEach(() => {
|
|
15
15
|
// eslint-disable-next-line no-console
|
|
16
16
|
console.error = jest.fn()
|
|
17
17
|
|
|
18
18
|
render(
|
|
19
19
|
<ThemeProvider theme={light}>
|
|
20
|
-
<
|
|
20
|
+
<MultiSelect value="a" />
|
|
21
21
|
</ThemeProvider>
|
|
22
22
|
)
|
|
23
23
|
})
|
|
@@ -26,7 +26,7 @@ describe('Select', () => {
|
|
|
26
26
|
// eslint-disable-next-line no-console
|
|
27
27
|
expect(console.error).toHaveBeenCalledWith(
|
|
28
28
|
expect.stringMatching(
|
|
29
|
-
/Perhaps you forgot to wrap with <
|
|
29
|
+
/Perhaps you forgot to wrap with <MultiSelectGroup>/u
|
|
30
30
|
)
|
|
31
31
|
)
|
|
32
32
|
})
|
|
@@ -254,28 +254,28 @@ const TestComponent = ({
|
|
|
254
254
|
}) => {
|
|
255
255
|
return (
|
|
256
256
|
<ThemeProvider theme={light}>
|
|
257
|
-
<
|
|
257
|
+
<MultiSelectGroup
|
|
258
258
|
name="defaultName"
|
|
259
259
|
ariaLabel="defaultAriaLabel"
|
|
260
260
|
disabled={parentDisabled}
|
|
261
261
|
onChange={parentOnChange}
|
|
262
262
|
{...{ selected, readonly, hasError }}
|
|
263
263
|
>
|
|
264
|
-
<
|
|
264
|
+
<MultiSelect
|
|
265
265
|
value="option1"
|
|
266
266
|
disabled={firstOptionDisabled}
|
|
267
267
|
forceChecked={firstOptionForceChecked}
|
|
268
268
|
onChange={childOnChange}
|
|
269
269
|
>
|
|
270
270
|
Option 1
|
|
271
|
-
</
|
|
272
|
-
<
|
|
271
|
+
</MultiSelect>
|
|
272
|
+
<MultiSelect value="option2" onChange={childOnChange}>
|
|
273
273
|
Option 2
|
|
274
|
-
</
|
|
275
|
-
<
|
|
274
|
+
</MultiSelect>
|
|
275
|
+
<MultiSelect value="option3" onChange={childOnChange}>
|
|
276
276
|
Option 3
|
|
277
|
-
</
|
|
278
|
-
</
|
|
277
|
+
</MultiSelect>
|
|
278
|
+
</MultiSelectGroup>
|
|
279
279
|
</ThemeProvider>
|
|
280
280
|
)
|
|
281
281
|
}
|
|
@@ -4,9 +4,9 @@ import warning from 'warning'
|
|
|
4
4
|
import { theme } from '../../styled'
|
|
5
5
|
import { disabledSelector, px } from '@charcoal-ui/utils'
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { MultiSelectGroupContext } from './context'
|
|
8
8
|
|
|
9
|
-
export type
|
|
9
|
+
export type MultiSelectProps = React.PropsWithChildren<{
|
|
10
10
|
value: string
|
|
11
11
|
forceChecked?: boolean
|
|
12
12
|
disabled?: boolean
|
|
@@ -14,14 +14,14 @@ export type SelectProps = React.PropsWithChildren<{
|
|
|
14
14
|
onChange?: (payload: { value: string; selected: boolean }) => void
|
|
15
15
|
}>
|
|
16
16
|
|
|
17
|
-
export default function
|
|
17
|
+
export default function MultiSelect({
|
|
18
18
|
value,
|
|
19
19
|
forceChecked = false,
|
|
20
20
|
disabled = false,
|
|
21
21
|
onChange,
|
|
22
22
|
variant = 'default',
|
|
23
23
|
children,
|
|
24
|
-
}:
|
|
24
|
+
}: MultiSelectProps) {
|
|
25
25
|
const {
|
|
26
26
|
name,
|
|
27
27
|
selected,
|
|
@@ -29,12 +29,12 @@ export default function Select({
|
|
|
29
29
|
readonly,
|
|
30
30
|
hasError,
|
|
31
31
|
onChange: parentOnChange,
|
|
32
|
-
} = useContext(
|
|
32
|
+
} = useContext(MultiSelectGroupContext)
|
|
33
33
|
|
|
34
34
|
warning(
|
|
35
35
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
36
36
|
name !== undefined,
|
|
37
|
-
`"name" is not Provided for <
|
|
37
|
+
`"name" is not Provided for <MultiSelect>. Perhaps you forgot to wrap with <MultiSelectGroup> ?`
|
|
38
38
|
)
|
|
39
39
|
|
|
40
40
|
const isSelected = selected.includes(value) || forceChecked
|
|
@@ -52,8 +52,8 @@ export default function Select({
|
|
|
52
52
|
)
|
|
53
53
|
|
|
54
54
|
return (
|
|
55
|
-
<
|
|
56
|
-
<
|
|
55
|
+
<MultiSelectRoot aria-disabled={isDisabled}>
|
|
56
|
+
<MultiSelectInput
|
|
57
57
|
{...{
|
|
58
58
|
name,
|
|
59
59
|
value,
|
|
@@ -65,19 +65,19 @@ export default function Select({
|
|
|
65
65
|
overlay={variant === 'overlay'}
|
|
66
66
|
aria-invalid={hasError}
|
|
67
67
|
/>
|
|
68
|
-
<
|
|
68
|
+
<MultiSelectInputOverlay
|
|
69
69
|
overlay={variant === 'overlay'}
|
|
70
70
|
hasError={hasError}
|
|
71
71
|
aria-hidden={true}
|
|
72
72
|
>
|
|
73
73
|
<pixiv-icon name="24/Check" unsafe-non-guideline-scale={16 / 24} />
|
|
74
|
-
</
|
|
75
|
-
{Boolean(children) && <
|
|
76
|
-
</
|
|
74
|
+
</MultiSelectInputOverlay>
|
|
75
|
+
{Boolean(children) && <MultiSelectLabel>{children}</MultiSelectLabel>}
|
|
76
|
+
</MultiSelectRoot>
|
|
77
77
|
)
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
const
|
|
80
|
+
const MultiSelectRoot = styled.label`
|
|
81
81
|
display: grid;
|
|
82
82
|
grid-template-columns: auto 1fr;
|
|
83
83
|
align-items: center;
|
|
@@ -90,13 +90,13 @@ const SelectRoot = styled.label`
|
|
|
90
90
|
${theme((o) => o.disabled)}
|
|
91
91
|
`
|
|
92
92
|
|
|
93
|
-
const
|
|
93
|
+
const MultiSelectLabel = styled.div`
|
|
94
94
|
display: flex;
|
|
95
95
|
align-items: center;
|
|
96
96
|
${theme((o) => [o.typography(14), o.font.text1])}
|
|
97
97
|
`
|
|
98
98
|
|
|
99
|
-
const
|
|
99
|
+
const MultiSelectInput = styled.input.attrs({ type: 'checkbox' })<{
|
|
100
100
|
hasError: boolean
|
|
101
101
|
overlay: boolean
|
|
102
102
|
}>`
|
|
@@ -121,7 +121,10 @@ const SelectInput = styled.input.attrs({ type: 'checkbox' })<{
|
|
|
121
121
|
}
|
|
122
122
|
`
|
|
123
123
|
|
|
124
|
-
const
|
|
124
|
+
const MultiSelectInputOverlay = styled.div<{
|
|
125
|
+
overlay: boolean
|
|
126
|
+
hasError: boolean
|
|
127
|
+
}>`
|
|
125
128
|
position: absolute;
|
|
126
129
|
top: -2px;
|
|
127
130
|
left: -2px;
|
|
@@ -148,7 +151,7 @@ const SelectInputOverlay = styled.div<{ overlay: boolean; hasError: boolean }>`
|
|
|
148
151
|
`}
|
|
149
152
|
`
|
|
150
153
|
|
|
151
|
-
export type
|
|
154
|
+
export type MultiSelectGroupProps = React.PropsWithChildren<{
|
|
152
155
|
className?: string
|
|
153
156
|
name: string
|
|
154
157
|
ariaLabel: string
|
|
@@ -159,7 +162,7 @@ export type SelectGroupProps = React.PropsWithChildren<{
|
|
|
159
162
|
hasError?: boolean
|
|
160
163
|
}>
|
|
161
164
|
|
|
162
|
-
export function
|
|
165
|
+
export function MultiSelectGroup({
|
|
163
166
|
className,
|
|
164
167
|
name,
|
|
165
168
|
ariaLabel,
|
|
@@ -169,7 +172,7 @@ export function SelectGroup({
|
|
|
169
172
|
readonly = false,
|
|
170
173
|
hasError = false,
|
|
171
174
|
children,
|
|
172
|
-
}:
|
|
175
|
+
}: MultiSelectGroupProps) {
|
|
173
176
|
const handleChange = useCallback(
|
|
174
177
|
(payload: { value: string; selected: boolean }) => {
|
|
175
178
|
const index = selected.indexOf(payload.value)
|
|
@@ -188,7 +191,7 @@ export function SelectGroup({
|
|
|
188
191
|
)
|
|
189
192
|
|
|
190
193
|
return (
|
|
191
|
-
<
|
|
194
|
+
<MultiSelectGroupContext.Provider
|
|
192
195
|
value={{
|
|
193
196
|
name,
|
|
194
197
|
selected: Array.from(new Set(selected)),
|
|
@@ -205,6 +208,6 @@ export function SelectGroup({
|
|
|
205
208
|
>
|
|
206
209
|
{children}
|
|
207
210
|
</div>
|
|
208
|
-
</
|
|
211
|
+
</MultiSelectGroupContext.Provider>
|
|
209
212
|
)
|
|
210
213
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react'
|
|
2
|
+
import { RadioGroupState } from 'react-stately'
|
|
3
|
+
|
|
4
|
+
const RadioContext = createContext<RadioGroupState | null>(null)
|
|
5
|
+
|
|
6
|
+
type RadioProviderProps = React.PropsWithChildren<{
|
|
7
|
+
value: RadioGroupState
|
|
8
|
+
}>
|
|
9
|
+
export const RadioProvider: React.FC<RadioProviderProps> = ({
|
|
10
|
+
value,
|
|
11
|
+
children,
|
|
12
|
+
}) => {
|
|
13
|
+
return <RadioContext.Provider value={value}>{children}</RadioContext.Provider>
|
|
14
|
+
}
|
|
15
|
+
export const useRadioContext = () => {
|
|
16
|
+
const state = useContext(RadioContext)
|
|
17
|
+
|
|
18
|
+
if (state === null)
|
|
19
|
+
throw new Error('`<RadioProvider>` is not likely mounted.')
|
|
20
|
+
|
|
21
|
+
return state
|
|
22
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { action } from '@storybook/addon-actions'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import SegmentedControl, { SegmentedControlProps } from '.'
|
|
4
|
+
import { Story } from '../../_lib/compat'
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: 'SegmentedControl',
|
|
8
|
+
component: SegmentedControl,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const StringSegments: Story<SegmentedControlProps> = (props) => {
|
|
12
|
+
return <SegmentedControl {...props} onChange={action('change')} />
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
StringSegments.args = {
|
|
16
|
+
data: ['option1', 'option2', 'option3'],
|
|
17
|
+
disabled: false,
|
|
18
|
+
readonly: false,
|
|
19
|
+
required: false,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const ObjectSegments: Story<SegmentedControlProps> = (props) => {
|
|
23
|
+
return <SegmentedControl {...props} onChange={action('change')} />
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ObjectSegments.args = {
|
|
27
|
+
data: [
|
|
28
|
+
{ label: '選択肢1', value: 'option1' },
|
|
29
|
+
{ label: '選択肢2', value: 'option2' },
|
|
30
|
+
{ label: '選択肢3', value: 'option3' },
|
|
31
|
+
{ label: '選択肢4', value: 'option4', disabled: true },
|
|
32
|
+
],
|
|
33
|
+
disabled: false,
|
|
34
|
+
readonly: false,
|
|
35
|
+
required: false,
|
|
36
|
+
}
|