@charcoal-ui/react 4.3.0 → 4.4.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/dist/components/MultiSelect/context.d.ts +14 -0
- package/dist/components/MultiSelect/context.d.ts.map +1 -0
- package/dist/components/MultiSelect/index.d.ts +38 -0
- package/dist/components/MultiSelect/index.d.ts.map +1 -0
- package/dist/index.cjs.js +318 -216
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.css +111 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +281 -181
- package/dist/index.esm.js.map +1 -1
- package/dist/layered.css +111 -0
- package/dist/layered.css.map +1 -1
- package/package.json +6 -7
- package/src/components/Button/__snapshots__/index.story.storyshot +89 -71
- package/src/components/Checkbox/CheckboxInput/__snapshots__/index.story.storyshot +50 -53
- package/src/components/Checkbox/__snapshots__/index.story.storyshot +108 -102
- package/src/components/Clickable/__snapshots__/index.story.storyshot +19 -17
- package/src/components/DropdownSelector/ListItem/__snapshots__/index.story.storyshot +45 -54
- package/src/components/DropdownSelector/MenuList/__snapshots__/index.story.storyshot +238 -275
- package/src/components/DropdownSelector/Popover/__snapshots__/index.story.storyshot +28 -50
- package/src/components/DropdownSelector/__snapshots__/index.story.storyshot +780 -1158
- package/src/components/Icon/__snapshots__/index.story.storyshot +9 -7
- package/src/components/IconButton/__snapshots__/index.story.storyshot +43 -37
- package/src/components/LoadingSpinner/__snapshots__/index.story.storyshot +52 -64
- package/src/components/Modal/__snapshots__/index.story.storyshot +568 -716
- package/src/components/MultiSelect/__snapshots__/index.story.storyshot +531 -0
- package/src/components/MultiSelect/context.ts +23 -0
- package/src/components/MultiSelect/index.css +139 -0
- package/src/components/MultiSelect/index.story.tsx +118 -0
- package/src/components/MultiSelect/index.test.tsx +255 -0
- package/src/components/MultiSelect/index.tsx +153 -0
- package/src/components/Radio/__snapshots__/index.story.storyshot +313 -367
- package/src/components/SegmentedControl/__snapshots__/index.story.storyshot +116 -228
- package/src/components/Switch/__snapshots__/index.story.storyshot +74 -73
- package/src/components/TagItem/__snapshots__/index.story.storyshot +177 -193
- package/src/components/TextArea/__snapshots__/TextArea.story.storyshot +372 -533
- package/src/components/TextField/__snapshots__/TextField.story.storyshot +444 -583
- package/src/index.ts +6 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
MultiSelectGroup,
|
|
4
|
+
default as MultiSelect,
|
|
5
|
+
MultiSelectGroupProps,
|
|
6
|
+
} from '.'
|
|
7
|
+
import { Meta, StoryObj } from '@storybook/react'
|
|
8
|
+
import { action } from '@storybook/addon-actions'
|
|
9
|
+
|
|
10
|
+
const StyledMultiSelectGroup = (props: MultiSelectGroupProps) => {
|
|
11
|
+
return (
|
|
12
|
+
<MultiSelectGroup
|
|
13
|
+
style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '8px' }}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
title: 'react/MultiSelect',
|
|
21
|
+
component: MultiSelect,
|
|
22
|
+
argTypes: {
|
|
23
|
+
variant: {
|
|
24
|
+
control: {
|
|
25
|
+
type: 'inline-radio',
|
|
26
|
+
options: ['default', 'overlay'],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
args: {
|
|
31
|
+
variant: 'default',
|
|
32
|
+
},
|
|
33
|
+
} as Meta<typeof MultiSelect>
|
|
34
|
+
|
|
35
|
+
export const Basic: StoryObj<typeof MultiSelect> = {
|
|
36
|
+
render: function Render(args) {
|
|
37
|
+
const options = ['選択肢1', '選択肢2', '選択肢3', '選択肢4']
|
|
38
|
+
return (
|
|
39
|
+
<StyledMultiSelectGroup
|
|
40
|
+
name="name"
|
|
41
|
+
label="label"
|
|
42
|
+
onChange={action('click')}
|
|
43
|
+
selected={['選択肢1', '選択肢3']}
|
|
44
|
+
>
|
|
45
|
+
{options.map((option) => (
|
|
46
|
+
<MultiSelect {...args} value={option} key={option}>
|
|
47
|
+
{option}
|
|
48
|
+
</MultiSelect>
|
|
49
|
+
))}
|
|
50
|
+
</StyledMultiSelectGroup>
|
|
51
|
+
)
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const Invalid: StoryObj<typeof MultiSelect> = {
|
|
56
|
+
render: function Render(args) {
|
|
57
|
+
const options = ['選択肢1', '選択肢2', '選択肢3', '選択肢4']
|
|
58
|
+
return (
|
|
59
|
+
<StyledMultiSelectGroup
|
|
60
|
+
name="name"
|
|
61
|
+
label="label"
|
|
62
|
+
onChange={action('click')}
|
|
63
|
+
selected={[]}
|
|
64
|
+
invalid
|
|
65
|
+
>
|
|
66
|
+
{options.map((option) => (
|
|
67
|
+
<MultiSelect {...args} value={option} key={option}>
|
|
68
|
+
{option}
|
|
69
|
+
</MultiSelect>
|
|
70
|
+
))}
|
|
71
|
+
</StyledMultiSelectGroup>
|
|
72
|
+
)
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const Overlay: StoryObj<typeof MultiSelect> = {
|
|
77
|
+
render: function Render(args) {
|
|
78
|
+
const options = ['選択肢1', '選択肢2', '選択肢3', '選択肢4']
|
|
79
|
+
return (
|
|
80
|
+
<StyledMultiSelectGroup
|
|
81
|
+
name="name"
|
|
82
|
+
label="label"
|
|
83
|
+
onChange={action('click')}
|
|
84
|
+
selected={[]}
|
|
85
|
+
>
|
|
86
|
+
{options.map((option) => (
|
|
87
|
+
<MultiSelect {...args} value={option} key={option}>
|
|
88
|
+
{option}
|
|
89
|
+
</MultiSelect>
|
|
90
|
+
))}
|
|
91
|
+
</StyledMultiSelectGroup>
|
|
92
|
+
)
|
|
93
|
+
},
|
|
94
|
+
args: {
|
|
95
|
+
variant: 'overlay',
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const Playground: StoryObj<typeof MultiSelect> = {
|
|
100
|
+
render: function Render(args) {
|
|
101
|
+
const [selected, setSelected] = useState<string[]>([])
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<StyledMultiSelectGroup
|
|
105
|
+
name=""
|
|
106
|
+
label=""
|
|
107
|
+
onChange={setSelected}
|
|
108
|
+
selected={selected}
|
|
109
|
+
>
|
|
110
|
+
{[1, 2, 3, 4].map((idx) => (
|
|
111
|
+
<MultiSelect {...args} value={`選択肢${idx}`} key={idx}>
|
|
112
|
+
選択肢{idx}
|
|
113
|
+
</MultiSelect>
|
|
114
|
+
))}
|
|
115
|
+
</StyledMultiSelectGroup>
|
|
116
|
+
)
|
|
117
|
+
},
|
|
118
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
2
|
+
import { default as MultiSelect, MultiSelectGroup } from '.'
|
|
3
|
+
|
|
4
|
+
describe('MultiSelect', () => {
|
|
5
|
+
describe('in development mode', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
process.env.NODE_ENV = 'development'
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
describe('when `<MultiSelect />` is used without `<MultiSelectGroup />`', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// eslint-disable-next-line no-console
|
|
13
|
+
console.error = vi.fn()
|
|
14
|
+
|
|
15
|
+
render(<MultiSelect value="a" />)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('emits error message', () => {
|
|
19
|
+
// eslint-disable-next-line no-console
|
|
20
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
21
|
+
expect.stringMatching(
|
|
22
|
+
/Perhaps you forgot to wrap with <MultiSelectGroup>/u
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('none of the options selected', () => {
|
|
30
|
+
let option1: HTMLInputElement
|
|
31
|
+
let option2: HTMLInputElement
|
|
32
|
+
let option3: HTMLInputElement
|
|
33
|
+
let allOptions: HTMLInputElement[]
|
|
34
|
+
let parent: HTMLDivElement
|
|
35
|
+
const childOnChange = vi.fn()
|
|
36
|
+
const parentOnChange = vi.fn()
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
render(
|
|
40
|
+
<TestComponent
|
|
41
|
+
selected={[]}
|
|
42
|
+
childOnChange={childOnChange}
|
|
43
|
+
parentOnChange={parentOnChange}
|
|
44
|
+
/>
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
option1 = screen.getByDisplayValue('option1')
|
|
48
|
+
option2 = screen.getByDisplayValue('option2')
|
|
49
|
+
option3 = screen.getByDisplayValue('option3')
|
|
50
|
+
allOptions = [option1, option2, option3]
|
|
51
|
+
parent = screen.getByTestId('SelectGroup')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('options have correct name', () => {
|
|
55
|
+
allOptions.forEach((element) => expect(element.name).toBe('defaultName'))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('parent have correct aria-label', () => {
|
|
59
|
+
expect(parent.getAttribute('aria-label')).toBe('defaultAriaLabel')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('none of the options are selected', () => {
|
|
63
|
+
allOptions.forEach((element) => expect(element.checked).toBeFalsy())
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('selecting option1', () => {
|
|
67
|
+
it('childOnChange is called', () => {
|
|
68
|
+
fireEvent.click(option1)
|
|
69
|
+
expect(childOnChange).toHaveBeenCalledWith({
|
|
70
|
+
value: 'option1',
|
|
71
|
+
selected: true,
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('parentOnChange is called', () => {
|
|
76
|
+
fireEvent.click(option1)
|
|
77
|
+
expect(parentOnChange).toHaveBeenCalledWith(['option1'])
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('option2 is selected', () => {
|
|
83
|
+
let option1: HTMLInputElement
|
|
84
|
+
let option2: HTMLInputElement
|
|
85
|
+
let option3: HTMLInputElement
|
|
86
|
+
const childOnChange = vi.fn()
|
|
87
|
+
const parentOnChange = vi.fn()
|
|
88
|
+
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
render(
|
|
91
|
+
<TestComponent
|
|
92
|
+
selected={['option2']}
|
|
93
|
+
childOnChange={childOnChange}
|
|
94
|
+
parentOnChange={parentOnChange}
|
|
95
|
+
/>
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
option1 = screen.getByDisplayValue('option1')
|
|
99
|
+
option2 = screen.getByDisplayValue('option2')
|
|
100
|
+
option3 = screen.getByDisplayValue('option3')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('only option2 is selected', () => {
|
|
104
|
+
expect(option2.checked).toBeTruthy()
|
|
105
|
+
;[option1, option3].forEach((element) =>
|
|
106
|
+
expect(element.checked).toBeFalsy()
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('selecting option1', () => {
|
|
111
|
+
it('parentOnChange is called', () => {
|
|
112
|
+
fireEvent.click(option1)
|
|
113
|
+
expect(parentOnChange).toHaveBeenCalledWith(['option2', 'option1'])
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('de-selecting option2', () => {
|
|
118
|
+
it('childOnChange is called', () => {
|
|
119
|
+
fireEvent.click(option2)
|
|
120
|
+
expect(childOnChange).toHaveBeenCalledWith({
|
|
121
|
+
value: 'option2',
|
|
122
|
+
selected: false,
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('parentOnChange is called', () => {
|
|
127
|
+
fireEvent.click(option2)
|
|
128
|
+
expect(parentOnChange).toHaveBeenCalledWith([])
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe('the group is disabled', () => {
|
|
134
|
+
let option1: HTMLInputElement
|
|
135
|
+
let option2: HTMLInputElement
|
|
136
|
+
let option3: HTMLInputElement
|
|
137
|
+
let allOptions: HTMLInputElement[]
|
|
138
|
+
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
render(<TestComponent selected={['option1']} parentDisabled={true} />)
|
|
141
|
+
|
|
142
|
+
option1 = screen.getByDisplayValue('option1')
|
|
143
|
+
option2 = screen.getByDisplayValue('option2')
|
|
144
|
+
option3 = screen.getByDisplayValue('option3')
|
|
145
|
+
allOptions = [option1, option2, option3]
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('all the options are disabled', () => {
|
|
149
|
+
allOptions.forEach((element) => expect(element.disabled).toBeTruthy())
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('the group is readonly', () => {
|
|
154
|
+
let option1: HTMLInputElement
|
|
155
|
+
let option2: HTMLInputElement
|
|
156
|
+
let option3: HTMLInputElement
|
|
157
|
+
let allOptions: HTMLInputElement[]
|
|
158
|
+
|
|
159
|
+
beforeEach(() => {
|
|
160
|
+
render(<TestComponent selected={['option1']} readonly={true} />)
|
|
161
|
+
|
|
162
|
+
option1 = screen.getByDisplayValue('option1')
|
|
163
|
+
option2 = screen.getByDisplayValue('option2')
|
|
164
|
+
option3 = screen.getByDisplayValue('option3')
|
|
165
|
+
allOptions = [option1, option2, option3]
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('all the options are disabled', () => {
|
|
169
|
+
allOptions.forEach((element) => expect(element.disabled).toBeTruthy())
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
describe('the group has error', () => {
|
|
174
|
+
let option1: HTMLInputElement
|
|
175
|
+
let option2: HTMLInputElement
|
|
176
|
+
let option3: HTMLInputElement
|
|
177
|
+
let allOptions: HTMLInputElement[]
|
|
178
|
+
|
|
179
|
+
beforeEach(() => {
|
|
180
|
+
render(<TestComponent selected={['option1']} invalid={true} />)
|
|
181
|
+
|
|
182
|
+
option1 = screen.getByDisplayValue('option1')
|
|
183
|
+
option2 = screen.getByDisplayValue('option2')
|
|
184
|
+
option3 = screen.getByDisplayValue('option3')
|
|
185
|
+
allOptions = [option1, option2, option3]
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('all the options have `aria-invalid="true"`', () => {
|
|
189
|
+
allOptions.forEach((element) =>
|
|
190
|
+
expect(element.getAttribute('aria-invalid')).toBeTruthy()
|
|
191
|
+
)
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe('option1 is disabled', () => {
|
|
196
|
+
let option1: HTMLInputElement
|
|
197
|
+
let option2: HTMLInputElement
|
|
198
|
+
|
|
199
|
+
beforeEach(() => {
|
|
200
|
+
render(<TestComponent selected={[]} firstOptionDisabled={true} />)
|
|
201
|
+
|
|
202
|
+
option1 = screen.getByDisplayValue('option1')
|
|
203
|
+
option2 = screen.getByDisplayValue('option2')
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('only option1 is disabled', () => {
|
|
207
|
+
expect(option1.disabled).toBeTruthy()
|
|
208
|
+
expect(option2.disabled).toBeFalsy()
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const TestComponent = ({
|
|
214
|
+
selected,
|
|
215
|
+
parentOnChange = () => {
|
|
216
|
+
return
|
|
217
|
+
},
|
|
218
|
+
childOnChange,
|
|
219
|
+
parentDisabled = false,
|
|
220
|
+
readonly = false,
|
|
221
|
+
invalid = false,
|
|
222
|
+
firstOptionDisabled = false,
|
|
223
|
+
}: {
|
|
224
|
+
selected: string[]
|
|
225
|
+
parentOnChange?: (selected: string[]) => void
|
|
226
|
+
childOnChange?: (payload: { value: string; selected: boolean }) => void
|
|
227
|
+
parentDisabled?: boolean
|
|
228
|
+
readonly?: boolean
|
|
229
|
+
invalid?: boolean
|
|
230
|
+
firstOptionDisabled?: boolean
|
|
231
|
+
}) => {
|
|
232
|
+
return (
|
|
233
|
+
<MultiSelectGroup
|
|
234
|
+
name="defaultName"
|
|
235
|
+
label="defaultAriaLabel"
|
|
236
|
+
disabled={parentDisabled}
|
|
237
|
+
onChange={parentOnChange}
|
|
238
|
+
{...{ selected, readonly, invalid }}
|
|
239
|
+
>
|
|
240
|
+
<MultiSelect
|
|
241
|
+
value="option1"
|
|
242
|
+
disabled={firstOptionDisabled}
|
|
243
|
+
onChange={childOnChange}
|
|
244
|
+
>
|
|
245
|
+
Option 1
|
|
246
|
+
</MultiSelect>
|
|
247
|
+
<MultiSelect value="option2" onChange={childOnChange}>
|
|
248
|
+
Option 2
|
|
249
|
+
</MultiSelect>
|
|
250
|
+
<MultiSelect value="option3" onChange={childOnChange}>
|
|
251
|
+
Option 3
|
|
252
|
+
</MultiSelect>
|
|
253
|
+
</MultiSelectGroup>
|
|
254
|
+
)
|
|
255
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { ChangeEvent, useCallback, useContext, forwardRef, memo } from 'react'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import warning from 'warning'
|
|
4
|
+
|
|
5
|
+
import { MultiSelectGroupContext } from './context'
|
|
6
|
+
import Icon from '../Icon'
|
|
7
|
+
import { useClassNames } from '../../_lib/useClassNames'
|
|
8
|
+
import './index.css'
|
|
9
|
+
|
|
10
|
+
export type MultiSelectProps = React.PropsWithChildren<{
|
|
11
|
+
value: string
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
variant?: 'default' | 'overlay'
|
|
14
|
+
className?: string
|
|
15
|
+
onChange?: (payload: { value: string; selected: boolean }) => void
|
|
16
|
+
}>
|
|
17
|
+
|
|
18
|
+
const MultiSelect = forwardRef<HTMLInputElement, MultiSelectProps>(
|
|
19
|
+
function MultiSelectInner(
|
|
20
|
+
{
|
|
21
|
+
value,
|
|
22
|
+
disabled = false,
|
|
23
|
+
onChange,
|
|
24
|
+
variant = 'default',
|
|
25
|
+
className,
|
|
26
|
+
children,
|
|
27
|
+
},
|
|
28
|
+
ref
|
|
29
|
+
) {
|
|
30
|
+
const {
|
|
31
|
+
name,
|
|
32
|
+
selected,
|
|
33
|
+
disabled: parentDisabled,
|
|
34
|
+
readonly,
|
|
35
|
+
invalid,
|
|
36
|
+
onChange: parentOnChange,
|
|
37
|
+
} = useContext(MultiSelectGroupContext)
|
|
38
|
+
|
|
39
|
+
warning(
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
41
|
+
name !== undefined,
|
|
42
|
+
`"name" is not Provided for <MultiSelect>. Perhaps you forgot to wrap with <MultiSelectGroup> ?`
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const isSelected = selected.includes(value)
|
|
46
|
+
const isDisabled = disabled || parentDisabled || readonly
|
|
47
|
+
|
|
48
|
+
const handleChange = useCallback(
|
|
49
|
+
(event: ChangeEvent<HTMLInputElement>) => {
|
|
50
|
+
if (!(event.currentTarget instanceof HTMLInputElement)) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
if (onChange) onChange({ value, selected: event.currentTarget.checked })
|
|
54
|
+
parentOnChange({ value, selected: event.currentTarget.checked })
|
|
55
|
+
},
|
|
56
|
+
[onChange, parentOnChange, value]
|
|
57
|
+
)
|
|
58
|
+
const classNames = useClassNames('charcoal-multi-select', className)
|
|
59
|
+
return (
|
|
60
|
+
<label aria-disabled={isDisabled} className={classNames}>
|
|
61
|
+
<input
|
|
62
|
+
className="charcoal-multi-select-input"
|
|
63
|
+
name={name}
|
|
64
|
+
value={value}
|
|
65
|
+
type="checkbox"
|
|
66
|
+
checked={isSelected}
|
|
67
|
+
disabled={isDisabled}
|
|
68
|
+
onChange={handleChange}
|
|
69
|
+
data-overlay={variant === 'overlay'}
|
|
70
|
+
aria-invalid={invalid}
|
|
71
|
+
ref={ref}
|
|
72
|
+
/>
|
|
73
|
+
<div
|
|
74
|
+
className="charcoal-multi-select-overlay"
|
|
75
|
+
data-overlay={variant === 'overlay'}
|
|
76
|
+
aria-invalid={invalid}
|
|
77
|
+
aria-hidden={true}
|
|
78
|
+
>
|
|
79
|
+
<Icon name="24/Check" unsafe-non-guideline-scale={16 / 24} />
|
|
80
|
+
</div>
|
|
81
|
+
{Boolean(children) && (
|
|
82
|
+
<div className="charcoal-multi-select-label">{children}</div>
|
|
83
|
+
)}
|
|
84
|
+
</label>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
export default memo(MultiSelect)
|
|
90
|
+
|
|
91
|
+
export type MultiSelectGroupProps = React.PropsWithChildren<{
|
|
92
|
+
className?: string
|
|
93
|
+
style?: React.CSSProperties
|
|
94
|
+
name: string
|
|
95
|
+
label: string
|
|
96
|
+
selected: string[]
|
|
97
|
+
onChange: (selected: string[]) => void
|
|
98
|
+
disabled?: boolean
|
|
99
|
+
readonly?: boolean
|
|
100
|
+
invalid?: boolean
|
|
101
|
+
}>
|
|
102
|
+
|
|
103
|
+
export function MultiSelectGroup({
|
|
104
|
+
className,
|
|
105
|
+
style,
|
|
106
|
+
name,
|
|
107
|
+
label,
|
|
108
|
+
selected,
|
|
109
|
+
onChange,
|
|
110
|
+
disabled = false,
|
|
111
|
+
readonly = false,
|
|
112
|
+
invalid = false,
|
|
113
|
+
children,
|
|
114
|
+
}: MultiSelectGroupProps) {
|
|
115
|
+
const handleChange = useCallback(
|
|
116
|
+
(payload: { value: string; selected: boolean }) => {
|
|
117
|
+
const index = selected.indexOf(payload.value)
|
|
118
|
+
|
|
119
|
+
if (payload.selected) {
|
|
120
|
+
if (index < 0) {
|
|
121
|
+
onChange([...selected, payload.value])
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
if (index >= 0) {
|
|
125
|
+
onChange([...selected.slice(0, index), ...selected.slice(index + 1)])
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
[onChange, selected]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<MultiSelectGroupContext.Provider
|
|
134
|
+
value={{
|
|
135
|
+
name,
|
|
136
|
+
selected: Array.from(new Set(selected)),
|
|
137
|
+
disabled,
|
|
138
|
+
readonly,
|
|
139
|
+
invalid,
|
|
140
|
+
onChange: handleChange,
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
<div
|
|
144
|
+
className={className}
|
|
145
|
+
style={style}
|
|
146
|
+
aria-label={label}
|
|
147
|
+
data-testid="SelectGroup"
|
|
148
|
+
>
|
|
149
|
+
{children}
|
|
150
|
+
</div>
|
|
151
|
+
</MultiSelectGroupContext.Provider>
|
|
152
|
+
)
|
|
153
|
+
}
|