@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.
Files changed (40) hide show
  1. package/dist/components/MultiSelect/context.d.ts +14 -0
  2. package/dist/components/MultiSelect/context.d.ts.map +1 -0
  3. package/dist/components/MultiSelect/index.d.ts +38 -0
  4. package/dist/components/MultiSelect/index.d.ts.map +1 -0
  5. package/dist/index.cjs.js +318 -216
  6. package/dist/index.cjs.js.map +1 -1
  7. package/dist/index.css +111 -0
  8. package/dist/index.css.map +1 -1
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.esm.js +281 -181
  12. package/dist/index.esm.js.map +1 -1
  13. package/dist/layered.css +111 -0
  14. package/dist/layered.css.map +1 -1
  15. package/package.json +6 -7
  16. package/src/components/Button/__snapshots__/index.story.storyshot +89 -71
  17. package/src/components/Checkbox/CheckboxInput/__snapshots__/index.story.storyshot +50 -53
  18. package/src/components/Checkbox/__snapshots__/index.story.storyshot +108 -102
  19. package/src/components/Clickable/__snapshots__/index.story.storyshot +19 -17
  20. package/src/components/DropdownSelector/ListItem/__snapshots__/index.story.storyshot +45 -54
  21. package/src/components/DropdownSelector/MenuList/__snapshots__/index.story.storyshot +238 -275
  22. package/src/components/DropdownSelector/Popover/__snapshots__/index.story.storyshot +28 -50
  23. package/src/components/DropdownSelector/__snapshots__/index.story.storyshot +780 -1158
  24. package/src/components/Icon/__snapshots__/index.story.storyshot +9 -7
  25. package/src/components/IconButton/__snapshots__/index.story.storyshot +43 -37
  26. package/src/components/LoadingSpinner/__snapshots__/index.story.storyshot +52 -64
  27. package/src/components/Modal/__snapshots__/index.story.storyshot +568 -716
  28. package/src/components/MultiSelect/__snapshots__/index.story.storyshot +531 -0
  29. package/src/components/MultiSelect/context.ts +23 -0
  30. package/src/components/MultiSelect/index.css +139 -0
  31. package/src/components/MultiSelect/index.story.tsx +118 -0
  32. package/src/components/MultiSelect/index.test.tsx +255 -0
  33. package/src/components/MultiSelect/index.tsx +153 -0
  34. package/src/components/Radio/__snapshots__/index.story.storyshot +313 -367
  35. package/src/components/SegmentedControl/__snapshots__/index.story.storyshot +116 -228
  36. package/src/components/Switch/__snapshots__/index.story.storyshot +74 -73
  37. package/src/components/TagItem/__snapshots__/index.story.storyshot +177 -193
  38. package/src/components/TextArea/__snapshots__/TextArea.story.storyshot +372 -533
  39. package/src/components/TextField/__snapshots__/TextField.story.storyshot +444 -583
  40. 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
+ }