@coreui/react 5.9.2 → 5.11.0
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/LICENSE +1 -1
- package/README.md +2 -2
- package/dist/cjs/components/chip/CChip.d.ts +76 -0
- package/dist/cjs/components/chip/CChip.js +178 -0
- package/dist/cjs/components/chip/CChip.js.map +1 -0
- package/dist/cjs/components/chip/index.d.ts +2 -0
- package/dist/cjs/components/dropdown/CDropdown.js +1 -1
- package/dist/cjs/components/dropdown/CDropdown.js.map +1 -1
- package/dist/cjs/components/form/CChipInput.d.ts +92 -0
- package/dist/cjs/components/form/CChipInput.js +253 -0
- package/dist/cjs/components/form/CChipInput.js.map +1 -0
- package/dist/cjs/components/form/index.d.ts +2 -1
- package/dist/cjs/components/index.d.ts +2 -0
- package/dist/cjs/components/nav/CNavGroup.d.ts +3 -1
- package/dist/cjs/components/nav/CNavGroup.js +9 -5
- package/dist/cjs/components/nav/CNavGroup.js.map +1 -1
- package/dist/cjs/components/popover/CPopover.js +14 -17
- package/dist/cjs/components/popover/CPopover.js.map +1 -1
- package/dist/cjs/components/search-button/CSearchButton.d.ts +32 -0
- package/dist/cjs/components/search-button/CSearchButton.js +77 -0
- package/dist/cjs/components/search-button/CSearchButton.js.map +1 -0
- package/dist/cjs/components/search-button/index.d.ts +2 -0
- package/dist/cjs/components/search-button/types.d.ts +10 -0
- package/dist/cjs/components/search-button/utils.d.ts +11 -0
- package/dist/cjs/components/search-button/utils.js +115 -0
- package/dist/cjs/components/search-button/utils.js.map +1 -0
- package/dist/cjs/components/sidebar/CSidebarNav.d.ts +12 -0
- package/dist/cjs/components/sidebar/CSidebarNav.js +7 -2
- package/dist/cjs/components/sidebar/CSidebarNav.js.map +1 -1
- package/dist/cjs/components/tooltip/CTooltip.js +14 -17
- package/dist/cjs/components/tooltip/CTooltip.js.map +1 -1
- package/dist/cjs/index.js +6 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/components/chip/CChip.d.ts +76 -0
- package/dist/esm/components/chip/CChip.js +176 -0
- package/dist/esm/components/chip/CChip.js.map +1 -0
- package/dist/esm/components/chip/index.d.ts +2 -0
- package/dist/esm/components/dropdown/CDropdown.js +1 -1
- package/dist/esm/components/dropdown/CDropdown.js.map +1 -1
- package/dist/esm/components/form/CChipInput.d.ts +92 -0
- package/dist/esm/components/form/CChipInput.js +251 -0
- package/dist/esm/components/form/CChipInput.js.map +1 -0
- package/dist/esm/components/form/index.d.ts +2 -1
- package/dist/esm/components/index.d.ts +2 -0
- package/dist/esm/components/nav/CNavGroup.d.ts +3 -1
- package/dist/esm/components/nav/CNavGroup.js +9 -5
- package/dist/esm/components/nav/CNavGroup.js.map +1 -1
- package/dist/esm/components/popover/CPopover.js +14 -17
- package/dist/esm/components/popover/CPopover.js.map +1 -1
- package/dist/esm/components/search-button/CSearchButton.d.ts +32 -0
- package/dist/esm/components/search-button/CSearchButton.js +75 -0
- package/dist/esm/components/search-button/CSearchButton.js.map +1 -0
- package/dist/esm/components/search-button/index.d.ts +2 -0
- package/dist/esm/components/search-button/types.d.ts +10 -0
- package/dist/esm/components/search-button/utils.d.ts +11 -0
- package/dist/esm/components/search-button/utils.js +104 -0
- package/dist/esm/components/search-button/utils.js.map +1 -0
- package/dist/esm/components/sidebar/CSidebarNav.d.ts +12 -0
- package/dist/esm/components/sidebar/CSidebarNav.js +7 -2
- package/dist/esm/components/sidebar/CSidebarNav.js.map +1 -1
- package/dist/esm/components/tooltip/CTooltip.js +14 -17
- package/dist/esm/components/tooltip/CTooltip.js.map +1 -1
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/chip/CChip.tsx +372 -0
- package/src/components/chip/__tests__/CChip.spec.tsx +113 -0
- package/src/components/chip/__tests__/__snapshots__/CChip.spec.tsx.snap +65 -0
- package/src/components/chip/index.ts +3 -0
- package/src/components/dropdown/CDropdown.tsx +1 -1
- package/src/components/dropdown/__tests__/CDropdown.spec.tsx +1 -1
- package/src/components/dropdown/__tests__/__snapshots__/CDropdown.spec.tsx.snap +2 -2
- package/src/components/dropdown/__tests__/__snapshots__/CDropdownMenu.spec.tsx.snap +1 -1
- package/src/components/form/CChipInput.tsx +477 -0
- package/src/components/form/__tests__/CChipInput.spec.tsx +62 -0
- package/src/components/form/__tests__/__snapshots__/CChipInput.spec.tsx.snap +91 -0
- package/src/components/form/index.ts +2 -0
- package/src/components/index.ts +2 -0
- package/src/components/nav/CNavGroup.tsx +11 -6
- package/src/components/nav/__tests__/CNavGroup.spec.tsx +29 -1
- package/src/components/nav/__tests__/__snapshots__/CNavGroup.spec.tsx.snap +1 -1
- package/src/components/popover/CPopover.tsx +15 -20
- package/src/components/popover/__tests__/CPopover.spec.tsx +10 -4
- package/src/components/search-button/CSearchButton.tsx +195 -0
- package/src/components/search-button/__tests__/CSearchButton.spec.tsx +95 -0
- package/src/components/search-button/__tests__/__snapshots__/CSearchButton.spec.tsx.snap +87 -0
- package/src/components/search-button/index.ts +3 -0
- package/src/components/search-button/types.ts +10 -0
- package/src/components/search-button/utils.ts +140 -0
- package/src/components/sidebar/CSidebarNav.tsx +27 -2
- package/src/components/tooltip/CTooltip.tsx +15 -20
- package/src/components/tooltip/__tests__/CTooltip.spec.tsx +6 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
ClipboardEvent,
|
|
3
|
+
FocusEvent,
|
|
4
|
+
HTMLAttributes,
|
|
5
|
+
KeyboardEvent,
|
|
6
|
+
ReactNode,
|
|
7
|
+
forwardRef,
|
|
8
|
+
useEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
} from 'react'
|
|
13
|
+
import PropTypes from 'prop-types'
|
|
14
|
+
import classNames from 'classnames'
|
|
15
|
+
|
|
16
|
+
import { CChip } from '../chip/CChip'
|
|
17
|
+
import { useForkedRef } from '../../hooks'
|
|
18
|
+
|
|
19
|
+
type ChipClassName = string | ((value: string) => string)
|
|
20
|
+
|
|
21
|
+
export interface CChipInputProps extends Omit<
|
|
22
|
+
HTMLAttributes<HTMLDivElement>,
|
|
23
|
+
'onChange' | 'onInput' | 'onSelect'
|
|
24
|
+
> {
|
|
25
|
+
/**
|
|
26
|
+
* Adds custom classes to the React Chip Input component root element.
|
|
27
|
+
*/
|
|
28
|
+
className?: string
|
|
29
|
+
/**
|
|
30
|
+
* Adds custom classes to chips rendered by the React Chip Input component. Accepts a static className or a resolver function based on chip value.
|
|
31
|
+
*/
|
|
32
|
+
chipClassName?: ChipClassName
|
|
33
|
+
/**
|
|
34
|
+
* Creates a new chip when the React Chip Input component loses focus with a pending value.
|
|
35
|
+
*/
|
|
36
|
+
createOnBlur?: boolean
|
|
37
|
+
/**
|
|
38
|
+
* Sets the initial uncontrolled values rendered by the React Chip Input component.
|
|
39
|
+
*/
|
|
40
|
+
defaultValue?: string[]
|
|
41
|
+
/**
|
|
42
|
+
* Disables the React Chip Input component and prevents adding, removing, or selecting chips.
|
|
43
|
+
*/
|
|
44
|
+
disabled?: boolean
|
|
45
|
+
/**
|
|
46
|
+
* Sets the `id` of the internal text input rendered by the React Chip Input component.
|
|
47
|
+
*/
|
|
48
|
+
id?: string
|
|
49
|
+
/**
|
|
50
|
+
* Renders an inline label inside the React Chip Input component container.
|
|
51
|
+
*/
|
|
52
|
+
label?: ReactNode
|
|
53
|
+
/**
|
|
54
|
+
* Sets the maximum number of chips that can be created in the React Chip Input component.
|
|
55
|
+
*/
|
|
56
|
+
maxChips?: number | null
|
|
57
|
+
/**
|
|
58
|
+
* Sets the name of the hidden input used by the React Chip Input component for form submission.
|
|
59
|
+
*/
|
|
60
|
+
name?: string
|
|
61
|
+
/**
|
|
62
|
+
* Callback fired when the React Chip Input component adds a new chip.
|
|
63
|
+
*/
|
|
64
|
+
onAdd?: (value: string) => void
|
|
65
|
+
/**
|
|
66
|
+
* Callback fired when the value list of the React Chip Input component changes.
|
|
67
|
+
*/
|
|
68
|
+
onChange?: (values: string[]) => void
|
|
69
|
+
/**
|
|
70
|
+
* Callback fired when the internal text input value changes in the React Chip Input component.
|
|
71
|
+
*/
|
|
72
|
+
onInput?: (value: string) => void
|
|
73
|
+
/**
|
|
74
|
+
* Callback fired when the React Chip Input component removes a chip.
|
|
75
|
+
*/
|
|
76
|
+
onRemove?: (value: string) => void
|
|
77
|
+
/**
|
|
78
|
+
* Callback fired when the selected chip values change in the React Chip Input component.
|
|
79
|
+
*/
|
|
80
|
+
onSelect?: (selected: string[]) => void
|
|
81
|
+
/**
|
|
82
|
+
* Sets placeholder text for the internal input of the React Chip Input component.
|
|
83
|
+
*/
|
|
84
|
+
placeholder?: string
|
|
85
|
+
/**
|
|
86
|
+
* Makes the React Chip Input component readonly while keeping values visible.
|
|
87
|
+
*/
|
|
88
|
+
readOnly?: boolean
|
|
89
|
+
/**
|
|
90
|
+
* Displays remove buttons on chips managed by the React Chip Input component.
|
|
91
|
+
*/
|
|
92
|
+
removable?: boolean
|
|
93
|
+
/**
|
|
94
|
+
* Enables chip selection behavior in the React Chip Input component.
|
|
95
|
+
*/
|
|
96
|
+
selectable?: boolean
|
|
97
|
+
/**
|
|
98
|
+
* Sets the separator character used to create chips while typing or pasting in the React Chip Input component.
|
|
99
|
+
*/
|
|
100
|
+
separator?: string | null
|
|
101
|
+
/**
|
|
102
|
+
* Sets the size of the React Chip Input component to small or large.
|
|
103
|
+
*/
|
|
104
|
+
size?: 'sm' | 'lg'
|
|
105
|
+
/**
|
|
106
|
+
* Controls the values rendered by the React Chip Input component.
|
|
107
|
+
*
|
|
108
|
+
* @controllable onChange
|
|
109
|
+
*/
|
|
110
|
+
value?: string[]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const resolveChipClassName = (chipClassName: ChipClassName | undefined, value: string) => {
|
|
114
|
+
if (!chipClassName) {
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (typeof chipClassName === 'function') {
|
|
119
|
+
const resolvedClassName = chipClassName(value)
|
|
120
|
+
return typeof resolvedClassName === 'string' ? resolvedClassName : undefined
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return chipClassName
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const uniqueValues = (values: string[]) => [
|
|
127
|
+
...new Set(values.map((value) => value.trim()).filter(Boolean)),
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
export const CChipInput = forwardRef<HTMLDivElement, CChipInputProps>(
|
|
131
|
+
(
|
|
132
|
+
{
|
|
133
|
+
className,
|
|
134
|
+
chipClassName,
|
|
135
|
+
createOnBlur = true,
|
|
136
|
+
defaultValue = [],
|
|
137
|
+
disabled,
|
|
138
|
+
id,
|
|
139
|
+
label,
|
|
140
|
+
maxChips = null,
|
|
141
|
+
name,
|
|
142
|
+
onAdd,
|
|
143
|
+
onChange,
|
|
144
|
+
onInput,
|
|
145
|
+
onRemove,
|
|
146
|
+
onSelect,
|
|
147
|
+
placeholder = '',
|
|
148
|
+
readOnly,
|
|
149
|
+
removable = true,
|
|
150
|
+
selectable,
|
|
151
|
+
separator = ',',
|
|
152
|
+
size,
|
|
153
|
+
value,
|
|
154
|
+
...rest
|
|
155
|
+
},
|
|
156
|
+
ref
|
|
157
|
+
) => {
|
|
158
|
+
const isControlled = value !== undefined
|
|
159
|
+
const [_values, setValues] = useState<string[]>(() => uniqueValues(defaultValue))
|
|
160
|
+
const [inputValue, setInputValue] = useState('')
|
|
161
|
+
const [selectedValues, setSelectedValues] = useState<string[]>([])
|
|
162
|
+
const rootRef = useRef<HTMLDivElement>(null)
|
|
163
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
164
|
+
const forkedRef = useForkedRef(ref, rootRef)
|
|
165
|
+
|
|
166
|
+
const values = useMemo(
|
|
167
|
+
() => uniqueValues(isControlled ? (value as string[]) : _values),
|
|
168
|
+
[isControlled, value, _values]
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
setSelectedValues((prev) => prev.filter((item) => values.includes(item)))
|
|
173
|
+
}, [values])
|
|
174
|
+
|
|
175
|
+
const emitValuesChange = (nextValues: string[]) => {
|
|
176
|
+
if (!isControlled) {
|
|
177
|
+
setValues(nextValues)
|
|
178
|
+
}
|
|
179
|
+
onChange?.(nextValues)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const canAddMore = maxChips === null || values.length < maxChips
|
|
183
|
+
|
|
184
|
+
const add = (rawValue: string) => {
|
|
185
|
+
if (disabled || readOnly) {
|
|
186
|
+
return false
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const normalizedValue = String(rawValue).trim()
|
|
190
|
+
if (!normalizedValue || values.includes(normalizedValue) || !canAddMore) {
|
|
191
|
+
return false
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const nextValues = [...values, normalizedValue]
|
|
195
|
+
emitValuesChange(nextValues)
|
|
196
|
+
onAdd?.(normalizedValue)
|
|
197
|
+
return true
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const remove = (valueToRemove: string) => {
|
|
201
|
+
if (disabled || readOnly) {
|
|
202
|
+
return false
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!values.includes(valueToRemove)) {
|
|
206
|
+
return false
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const nextValues = values.filter((item) => item !== valueToRemove)
|
|
210
|
+
emitValuesChange(nextValues)
|
|
211
|
+
setSelectedValues((prev) => {
|
|
212
|
+
const nextSelected = prev.filter((item) => item !== valueToRemove)
|
|
213
|
+
if (nextSelected.length !== prev.length) {
|
|
214
|
+
onSelect?.(nextSelected)
|
|
215
|
+
}
|
|
216
|
+
return nextSelected
|
|
217
|
+
})
|
|
218
|
+
onRemove?.(valueToRemove)
|
|
219
|
+
return true
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const createFromInput = () => {
|
|
223
|
+
if (add(inputValue)) {
|
|
224
|
+
setInputValue('')
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const focusLastChip = () => {
|
|
229
|
+
if (!rootRef.current) {
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const focusableChips = [
|
|
234
|
+
...rootRef.current.querySelectorAll<HTMLElement>(
|
|
235
|
+
'[data-coreui-chip-focusable="true"]:not(.disabled)'
|
|
236
|
+
),
|
|
237
|
+
]
|
|
238
|
+
if (focusableChips.length === 0) {
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
focusableChips[focusableChips.length - 1].focus()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const handleInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
245
|
+
switch (event.key) {
|
|
246
|
+
case 'Enter': {
|
|
247
|
+
event.preventDefault()
|
|
248
|
+
createFromInput()
|
|
249
|
+
break
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
case 'Backspace':
|
|
253
|
+
case 'Delete': {
|
|
254
|
+
if (inputValue === '') {
|
|
255
|
+
event.preventDefault()
|
|
256
|
+
focusLastChip()
|
|
257
|
+
}
|
|
258
|
+
break
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
case 'ArrowLeft': {
|
|
262
|
+
if (event.currentTarget.selectionStart === 0 && event.currentTarget.selectionEnd === 0) {
|
|
263
|
+
event.preventDefault()
|
|
264
|
+
focusLastChip()
|
|
265
|
+
}
|
|
266
|
+
break
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
case 'Escape': {
|
|
270
|
+
setInputValue('')
|
|
271
|
+
event.currentTarget.blur()
|
|
272
|
+
break
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// No default
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const handleInputChange = (value: string) => {
|
|
280
|
+
if (disabled || readOnly) {
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (separator && value.includes(separator)) {
|
|
285
|
+
const parts = value.split(separator)
|
|
286
|
+
const chipsToAdd = uniqueValues(parts.slice(0, -1))
|
|
287
|
+
const nextValues = [...values]
|
|
288
|
+
|
|
289
|
+
for (const chipValue of chipsToAdd) {
|
|
290
|
+
if (maxChips !== null && nextValues.length >= maxChips) {
|
|
291
|
+
break
|
|
292
|
+
}
|
|
293
|
+
if (!nextValues.includes(chipValue)) {
|
|
294
|
+
nextValues.push(chipValue)
|
|
295
|
+
onAdd?.(chipValue)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (nextValues.length !== values.length) {
|
|
300
|
+
emitValuesChange(nextValues)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const tail = parts[parts.length - 1] || ''
|
|
304
|
+
setInputValue(tail)
|
|
305
|
+
onInput?.(tail)
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
setInputValue(value)
|
|
310
|
+
onInput?.(value)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const handlePaste = (event: ClipboardEvent<HTMLInputElement>) => {
|
|
314
|
+
if (disabled || readOnly || !separator) {
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const pastedData = event.clipboardData.getData('text')
|
|
319
|
+
if (!pastedData.includes(separator)) {
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
event.preventDefault()
|
|
324
|
+
const chipsToAdd = uniqueValues(pastedData.split(separator))
|
|
325
|
+
const nextValues = [...values]
|
|
326
|
+
|
|
327
|
+
for (const chipValue of chipsToAdd) {
|
|
328
|
+
if (maxChips !== null && nextValues.length >= maxChips) {
|
|
329
|
+
break
|
|
330
|
+
}
|
|
331
|
+
if (!nextValues.includes(chipValue)) {
|
|
332
|
+
nextValues.push(chipValue)
|
|
333
|
+
onAdd?.(chipValue)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (nextValues.length !== values.length) {
|
|
338
|
+
emitValuesChange(nextValues)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
setInputValue('')
|
|
342
|
+
onInput?.('')
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const handleInputBlur = (event: FocusEvent<HTMLInputElement>) => {
|
|
346
|
+
if (!createOnBlur) {
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if ((event.relatedTarget as HTMLElement | null)?.closest('.chip')) {
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
createFromInput()
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const handleContainerKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
358
|
+
if (event.target === inputRef.current) {
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (event.key.length === 1) {
|
|
363
|
+
inputRef.current?.focus()
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const handleContainerClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
368
|
+
if (event.target === rootRef.current) {
|
|
369
|
+
inputRef.current?.focus()
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const handleSelectedChange = (chipValue: string, selected: boolean) => {
|
|
374
|
+
setSelectedValues((prev) => {
|
|
375
|
+
const nextSelected = selected
|
|
376
|
+
? uniqueValues([...prev, chipValue])
|
|
377
|
+
: prev.filter((value) => value !== chipValue)
|
|
378
|
+
onSelect?.(nextSelected)
|
|
379
|
+
return nextSelected
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const inputSize = Math.max(placeholder.length, inputValue.length, 1)
|
|
384
|
+
|
|
385
|
+
return (
|
|
386
|
+
<div
|
|
387
|
+
className={classNames(
|
|
388
|
+
'chip-input',
|
|
389
|
+
{
|
|
390
|
+
'chip-input-sm': size === 'sm',
|
|
391
|
+
'chip-input-lg': size === 'lg',
|
|
392
|
+
disabled,
|
|
393
|
+
},
|
|
394
|
+
className
|
|
395
|
+
)}
|
|
396
|
+
aria-disabled={disabled ? true : undefined}
|
|
397
|
+
aria-readonly={readOnly ? true : undefined}
|
|
398
|
+
onClick={handleContainerClick}
|
|
399
|
+
onKeyDown={handleContainerKeyDown}
|
|
400
|
+
{...rest}
|
|
401
|
+
ref={forkedRef}
|
|
402
|
+
>
|
|
403
|
+
{label && (
|
|
404
|
+
<label className="chip-input-label" htmlFor={id}>
|
|
405
|
+
{label}
|
|
406
|
+
</label>
|
|
407
|
+
)}
|
|
408
|
+
|
|
409
|
+
{values.map((chipValue) => (
|
|
410
|
+
<CChip
|
|
411
|
+
key={chipValue}
|
|
412
|
+
className={resolveChipClassName(chipClassName, chipValue)}
|
|
413
|
+
removable={Boolean(removable && !disabled && !readOnly)}
|
|
414
|
+
ariaRemoveLabel={`Remove ${chipValue}`}
|
|
415
|
+
disabled={disabled}
|
|
416
|
+
onRemove={() => remove(chipValue)}
|
|
417
|
+
selectable={selectable}
|
|
418
|
+
selected={selectedValues.includes(chipValue)}
|
|
419
|
+
onSelectedChange={(selected) => handleSelectedChange(chipValue, selected)}
|
|
420
|
+
>
|
|
421
|
+
{chipValue}
|
|
422
|
+
</CChip>
|
|
423
|
+
))}
|
|
424
|
+
|
|
425
|
+
<input
|
|
426
|
+
type="text"
|
|
427
|
+
id={id}
|
|
428
|
+
className="chip-input-field"
|
|
429
|
+
disabled={disabled}
|
|
430
|
+
readOnly={Boolean(!disabled && readOnly)}
|
|
431
|
+
placeholder={placeholder}
|
|
432
|
+
size={inputSize}
|
|
433
|
+
value={inputValue}
|
|
434
|
+
onBlur={handleInputBlur}
|
|
435
|
+
onChange={(event) => handleInputChange(event.target.value)}
|
|
436
|
+
onKeyDown={handleInputKeyDown}
|
|
437
|
+
onPaste={handlePaste}
|
|
438
|
+
onFocus={() => {
|
|
439
|
+
if (selectedValues.length > 0) {
|
|
440
|
+
setSelectedValues([])
|
|
441
|
+
onSelect?.([])
|
|
442
|
+
}
|
|
443
|
+
}}
|
|
444
|
+
ref={inputRef}
|
|
445
|
+
/>
|
|
446
|
+
|
|
447
|
+
{name && <input type="hidden" name={name} value={values.join(',')} />}
|
|
448
|
+
</div>
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
CChipInput.propTypes = {
|
|
454
|
+
chipClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
|
455
|
+
className: PropTypes.string,
|
|
456
|
+
createOnBlur: PropTypes.bool,
|
|
457
|
+
defaultValue: PropTypes.array,
|
|
458
|
+
disabled: PropTypes.bool,
|
|
459
|
+
id: PropTypes.string,
|
|
460
|
+
label: PropTypes.node,
|
|
461
|
+
maxChips: PropTypes.number,
|
|
462
|
+
name: PropTypes.string,
|
|
463
|
+
onAdd: PropTypes.func,
|
|
464
|
+
onChange: PropTypes.func,
|
|
465
|
+
onInput: PropTypes.func,
|
|
466
|
+
onRemove: PropTypes.func,
|
|
467
|
+
onSelect: PropTypes.func,
|
|
468
|
+
placeholder: PropTypes.string,
|
|
469
|
+
readOnly: PropTypes.bool,
|
|
470
|
+
removable: PropTypes.bool,
|
|
471
|
+
selectable: PropTypes.bool,
|
|
472
|
+
separator: PropTypes.string,
|
|
473
|
+
size: PropTypes.oneOf(['sm', 'lg']),
|
|
474
|
+
value: PropTypes.array,
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
CChipInput.displayName = 'CChipInput'
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { fireEvent, render } from '@testing-library/react'
|
|
3
|
+
import '@testing-library/jest-dom'
|
|
4
|
+
import { CChipInput } from '../index'
|
|
5
|
+
|
|
6
|
+
test('loads and displays CChipInput component', async () => {
|
|
7
|
+
const { container } = render(<CChipInput defaultValue={['React', 'TypeScript']} />)
|
|
8
|
+
expect(container).toMatchSnapshot()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('CChipInput renders internal label', async () => {
|
|
12
|
+
const { getByText } = render(<CChipInput id="skillsInput" label="Skills:" />)
|
|
13
|
+
|
|
14
|
+
const label = getByText('Skills:')
|
|
15
|
+
expect(label).toHaveClass('chip-input-label')
|
|
16
|
+
expect(label).toHaveAttribute('for', 'skillsInput')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('CChipInput adds chip on enter', async () => {
|
|
20
|
+
const onChange = jest.fn()
|
|
21
|
+
const { container } = render(<CChipInput onChange={onChange} />)
|
|
22
|
+
|
|
23
|
+
const input = container.querySelector('.chip-input-field') as HTMLInputElement
|
|
24
|
+
fireEvent.change(input, { target: { value: 'CoreUI' } })
|
|
25
|
+
fireEvent.keyDown(input, { key: 'Enter' })
|
|
26
|
+
|
|
27
|
+
expect(container.querySelectorAll('.chip')).toHaveLength(1)
|
|
28
|
+
expect(container.querySelector('.chip')?.textContent).toContain('CoreUI')
|
|
29
|
+
expect(onChange).toHaveBeenCalledWith(['CoreUI'])
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('CChipInput handles separators', async () => {
|
|
33
|
+
const { container } = render(<CChipInput separator="," />)
|
|
34
|
+
const input = container.querySelector('.chip-input-field') as HTMLInputElement
|
|
35
|
+
|
|
36
|
+
fireEvent.change(input, { target: { value: 'React,TypeScript,' } })
|
|
37
|
+
|
|
38
|
+
expect(container.querySelectorAll('.chip')).toHaveLength(2)
|
|
39
|
+
expect(input.value).toBe('')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('CChipInput removes chip via remove button', async () => {
|
|
43
|
+
const onRemove = jest.fn()
|
|
44
|
+
const { container } = render(<CChipInput defaultValue={['React']} onRemove={onRemove} />)
|
|
45
|
+
|
|
46
|
+
const removeButton = container.querySelector('.chip-remove') as HTMLButtonElement
|
|
47
|
+
fireEvent.click(removeButton)
|
|
48
|
+
|
|
49
|
+
expect(container.querySelectorAll('.chip')).toHaveLength(0)
|
|
50
|
+
expect(onRemove).toHaveBeenCalledWith('React')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('CChipInput selectable chips', async () => {
|
|
54
|
+
const onSelect = jest.fn()
|
|
55
|
+
const { container } = render(<CChipInput defaultValue={['React']} selectable onSelect={onSelect} />)
|
|
56
|
+
|
|
57
|
+
const chip = container.querySelector('.chip') as HTMLElement
|
|
58
|
+
fireEvent.click(chip)
|
|
59
|
+
|
|
60
|
+
expect(chip).toHaveClass('active')
|
|
61
|
+
expect(onSelect).toHaveBeenCalledWith(['React'])
|
|
62
|
+
})
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`loads and displays CChipInput component 1`] = `
|
|
4
|
+
<div>
|
|
5
|
+
<div
|
|
6
|
+
class="chip-input"
|
|
7
|
+
>
|
|
8
|
+
<span
|
|
9
|
+
class="chip"
|
|
10
|
+
data-coreui-chip-focusable="true"
|
|
11
|
+
tabindex="0"
|
|
12
|
+
>
|
|
13
|
+
React
|
|
14
|
+
<button
|
|
15
|
+
aria-label="Remove React"
|
|
16
|
+
class="chip-remove"
|
|
17
|
+
tabindex="-1"
|
|
18
|
+
type="button"
|
|
19
|
+
>
|
|
20
|
+
<svg
|
|
21
|
+
fill="none"
|
|
22
|
+
height="16"
|
|
23
|
+
stroke="currentColor"
|
|
24
|
+
stroke-linecap="round"
|
|
25
|
+
stroke-width="2"
|
|
26
|
+
viewBox="0 0 16 16"
|
|
27
|
+
width="16"
|
|
28
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
29
|
+
>
|
|
30
|
+
<line
|
|
31
|
+
x1="4"
|
|
32
|
+
x2="12"
|
|
33
|
+
y1="4"
|
|
34
|
+
y2="12"
|
|
35
|
+
/>
|
|
36
|
+
<line
|
|
37
|
+
x1="12"
|
|
38
|
+
x2="4"
|
|
39
|
+
y1="4"
|
|
40
|
+
y2="12"
|
|
41
|
+
/>
|
|
42
|
+
</svg>
|
|
43
|
+
</button>
|
|
44
|
+
</span>
|
|
45
|
+
<span
|
|
46
|
+
class="chip"
|
|
47
|
+
data-coreui-chip-focusable="true"
|
|
48
|
+
tabindex="0"
|
|
49
|
+
>
|
|
50
|
+
TypeScript
|
|
51
|
+
<button
|
|
52
|
+
aria-label="Remove TypeScript"
|
|
53
|
+
class="chip-remove"
|
|
54
|
+
tabindex="-1"
|
|
55
|
+
type="button"
|
|
56
|
+
>
|
|
57
|
+
<svg
|
|
58
|
+
fill="none"
|
|
59
|
+
height="16"
|
|
60
|
+
stroke="currentColor"
|
|
61
|
+
stroke-linecap="round"
|
|
62
|
+
stroke-width="2"
|
|
63
|
+
viewBox="0 0 16 16"
|
|
64
|
+
width="16"
|
|
65
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
66
|
+
>
|
|
67
|
+
<line
|
|
68
|
+
x1="4"
|
|
69
|
+
x2="12"
|
|
70
|
+
y1="4"
|
|
71
|
+
y2="12"
|
|
72
|
+
/>
|
|
73
|
+
<line
|
|
74
|
+
x1="12"
|
|
75
|
+
x2="4"
|
|
76
|
+
y1="4"
|
|
77
|
+
y2="12"
|
|
78
|
+
/>
|
|
79
|
+
</svg>
|
|
80
|
+
</button>
|
|
81
|
+
</span>
|
|
82
|
+
<input
|
|
83
|
+
class="chip-input-field"
|
|
84
|
+
placeholder=""
|
|
85
|
+
size="1"
|
|
86
|
+
type="text"
|
|
87
|
+
value=""
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CForm } from './CForm'
|
|
2
2
|
import { CFormCheck } from './CFormCheck'
|
|
3
|
+
import { CChipInput } from './CChipInput'
|
|
3
4
|
import { CFormControlValidation } from './CFormControlValidation'
|
|
4
5
|
import { CFormControlWrapper } from './CFormControlWrapper'
|
|
5
6
|
import { CFormFeedback } from './CFormFeedback'
|
|
@@ -17,6 +18,7 @@ import { CInputGroupText } from './CInputGroupText'
|
|
|
17
18
|
export {
|
|
18
19
|
CForm,
|
|
19
20
|
CFormCheck,
|
|
21
|
+
CChipInput,
|
|
20
22
|
CFormControlValidation,
|
|
21
23
|
CFormControlWrapper,
|
|
22
24
|
CFormFeedback,
|
package/src/components/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './button-group'
|
|
|
9
9
|
export * from './callout'
|
|
10
10
|
export * from './card'
|
|
11
11
|
export * from './carousel'
|
|
12
|
+
export * from './chip'
|
|
12
13
|
export * from './close-button'
|
|
13
14
|
export * from './collapse'
|
|
14
15
|
export * from './conditional-portal'
|
|
@@ -29,6 +30,7 @@ export * from './pagination'
|
|
|
29
30
|
export * from './placeholder'
|
|
30
31
|
export * from './progress'
|
|
31
32
|
export * from './popover'
|
|
33
|
+
export * from './search-button'
|
|
32
34
|
export * from './sidebar'
|
|
33
35
|
export * from './spinner'
|
|
34
36
|
export * from './table'
|