@coreui/react-pro 5.17.1 → 5.19.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/README.md +1 -1
- package/dist/cjs/components/autocomplete/CAutocomplete.d.ts +243 -0
- package/dist/cjs/components/autocomplete/CAutocomplete.js +306 -0
- package/dist/cjs/components/autocomplete/CAutocomplete.js.map +1 -0
- package/dist/cjs/components/autocomplete/CAutocompleteOptions.d.ts +17 -0
- package/dist/cjs/components/autocomplete/CAutocompleteOptions.js +72 -0
- package/dist/cjs/components/autocomplete/CAutocompleteOptions.js.map +1 -0
- package/dist/cjs/components/autocomplete/index.d.ts +2 -0
- package/dist/cjs/components/autocomplete/types.d.ts +18 -0
- package/dist/cjs/components/autocomplete/utils.d.ts +11 -0
- package/dist/cjs/components/autocomplete/utils.js +107 -0
- package/dist/cjs/components/autocomplete/utils.js.map +1 -0
- package/dist/cjs/components/calendar/CCalendarPanel.js +6 -7
- package/dist/cjs/components/calendar/CCalendarPanel.js.map +1 -1
- package/dist/cjs/components/calendar/utils.d.ts +19 -5
- package/dist/cjs/components/calendar/utils.js +20 -9
- package/dist/cjs/components/calendar/utils.js.map +1 -1
- package/dist/cjs/components/date-range-picker/CDateRangePicker.js +10 -8
- package/dist/cjs/components/date-range-picker/CDateRangePicker.js.map +1 -1
- package/dist/cjs/components/index.d.ts +1 -0
- package/dist/cjs/components/multi-select/CMultiSelect.d.ts +6 -0
- package/dist/cjs/components/multi-select/CMultiSelect.js +13 -12
- package/dist/cjs/components/multi-select/CMultiSelect.js.map +1 -1
- package/dist/cjs/components/multi-select/CMultiSelectOptions.js +6 -5
- package/dist/cjs/components/multi-select/CMultiSelectOptions.js.map +1 -1
- package/dist/cjs/components/multi-select/CMultiSelectSelection.js +1 -1
- package/dist/cjs/components/multi-select/CMultiSelectSelection.js.map +1 -1
- package/dist/cjs/components/multi-select/utils.d.ts +0 -2
- package/dist/cjs/components/multi-select/utils.js +0 -26
- package/dist/cjs/components/multi-select/utils.js.map +1 -1
- package/dist/cjs/components/time-picker/CTimePicker.js +15 -1
- package/dist/cjs/components/time-picker/CTimePicker.js.map +1 -1
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/utils/getNextSibling.d.ts +2 -0
- package/dist/cjs/utils/getNextSibling.js +23 -0
- package/dist/cjs/utils/getNextSibling.js.map +1 -0
- package/dist/cjs/utils/getPreviousSibling.d.ts +2 -0
- package/dist/cjs/utils/getPreviousSibling.js +22 -0
- package/dist/cjs/utils/getPreviousSibling.js.map +1 -0
- package/dist/cjs/utils/index.d.ts +3 -1
- package/dist/esm/components/autocomplete/CAutocomplete.d.ts +243 -0
- package/dist/esm/components/autocomplete/CAutocomplete.js +304 -0
- package/dist/esm/components/autocomplete/CAutocomplete.js.map +1 -0
- package/dist/esm/components/autocomplete/CAutocompleteOptions.d.ts +17 -0
- package/dist/esm/components/autocomplete/CAutocompleteOptions.js +70 -0
- package/dist/esm/components/autocomplete/CAutocompleteOptions.js.map +1 -0
- package/dist/esm/components/autocomplete/index.d.ts +2 -0
- package/dist/esm/components/autocomplete/types.d.ts +18 -0
- package/dist/esm/components/autocomplete/utils.d.ts +11 -0
- package/dist/esm/components/autocomplete/utils.js +96 -0
- package/dist/esm/components/autocomplete/utils.js.map +1 -0
- package/dist/esm/components/calendar/CCalendarPanel.js +7 -8
- package/dist/esm/components/calendar/CCalendarPanel.js.map +1 -1
- package/dist/esm/components/calendar/utils.d.ts +19 -5
- package/dist/esm/components/calendar/utils.js +20 -9
- package/dist/esm/components/calendar/utils.js.map +1 -1
- package/dist/esm/components/date-range-picker/CDateRangePicker.js +10 -8
- package/dist/esm/components/date-range-picker/CDateRangePicker.js.map +1 -1
- package/dist/esm/components/index.d.ts +1 -0
- package/dist/esm/components/multi-select/CMultiSelect.d.ts +6 -0
- package/dist/esm/components/multi-select/CMultiSelect.js +14 -13
- package/dist/esm/components/multi-select/CMultiSelect.js.map +1 -1
- package/dist/esm/components/multi-select/CMultiSelectOptions.js +4 -3
- package/dist/esm/components/multi-select/CMultiSelectOptions.js.map +1 -1
- package/dist/esm/components/multi-select/CMultiSelectSelection.js +1 -1
- package/dist/esm/components/multi-select/CMultiSelectSelection.js.map +1 -1
- package/dist/esm/components/multi-select/utils.d.ts +0 -2
- package/dist/esm/components/multi-select/utils.js +1 -25
- package/dist/esm/components/multi-select/utils.js.map +1 -1
- package/dist/esm/components/time-picker/CTimePicker.js +15 -1
- package/dist/esm/components/time-picker/CTimePicker.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/utils/getNextSibling.d.ts +2 -0
- package/dist/esm/utils/getNextSibling.js +18 -0
- package/dist/esm/utils/getNextSibling.js.map +1 -0
- package/dist/esm/utils/getPreviousSibling.d.ts +2 -0
- package/dist/esm/utils/getPreviousSibling.js +18 -0
- package/dist/esm/utils/getPreviousSibling.js.map +1 -0
- package/dist/esm/utils/index.d.ts +3 -1
- package/package.json +10 -10
- package/src/components/autocomplete/CAutocomplete.tsx +846 -0
- package/src/components/autocomplete/CAutocompleteOptions.tsx +165 -0
- package/src/components/autocomplete/__tests__/CAutocomplete.spec.tsx +244 -0
- package/src/components/autocomplete/__tests__/CAutocompleteOptions.spec.tsx +210 -0
- package/src/components/autocomplete/index.ts +3 -0
- package/src/components/autocomplete/types.ts +25 -0
- package/src/components/autocomplete/utils.ts +125 -0
- package/src/components/calendar/CCalendarPanel.tsx +5 -15
- package/src/components/calendar/utils.ts +24 -10
- package/src/components/date-range-picker/CDateRangePicker.tsx +11 -7
- package/src/components/index.ts +1 -0
- package/src/components/multi-select/CMultiSelect.tsx +23 -4
- package/src/components/multi-select/CMultiSelectOptions.tsx +8 -5
- package/src/components/multi-select/CMultiSelectSelection.tsx +3 -3
- package/src/components/multi-select/utils.ts +0 -34
- package/src/components/time-picker/CTimePicker.tsx +14 -4
- package/src/utils/getNextSibling.ts +18 -0
- package/src/utils/getPreviousSibling.ts +18 -0
- package/src/utils/index.ts +4 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import React, { forwardRef, HTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import classNames from 'classnames'
|
|
4
|
+
|
|
5
|
+
import { CElementCover } from '../element-cover'
|
|
6
|
+
import { CVirtualScroller } from '../virtual-scroller'
|
|
7
|
+
|
|
8
|
+
import { getNextSibling, getPreviousSibling } from '../../utils'
|
|
9
|
+
|
|
10
|
+
import type { Option, OptionsGroup } from './types'
|
|
11
|
+
import { getOptionLabel, highlightSubstring, isOptionDisabled, isOptionSelected } from './utils'
|
|
12
|
+
|
|
13
|
+
export interface CAutocompleteOptionsProps extends HTMLAttributes<HTMLDivElement> {
|
|
14
|
+
highlightOptionsOnSearch?: boolean
|
|
15
|
+
loading?: boolean
|
|
16
|
+
onOptionClick?: (option: Option) => void
|
|
17
|
+
options: (Option | OptionsGroup)[]
|
|
18
|
+
optionsMaxHeight?: number | string
|
|
19
|
+
optionsTemplate?: (option: Option) => ReactNode
|
|
20
|
+
optionsGroupsTemplate?: (option: OptionsGroup) => ReactNode
|
|
21
|
+
searchNoResultsLabel?: boolean | string | ReactNode
|
|
22
|
+
searchValue?: string
|
|
23
|
+
selected: Option | null
|
|
24
|
+
virtualScroller?: boolean
|
|
25
|
+
visibleItems?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const CAutocompleteOptions = forwardRef<HTMLDivElement, CAutocompleteOptionsProps>(
|
|
29
|
+
(
|
|
30
|
+
{
|
|
31
|
+
highlightOptionsOnSearch,
|
|
32
|
+
loading,
|
|
33
|
+
onOptionClick,
|
|
34
|
+
options,
|
|
35
|
+
optionsMaxHeight,
|
|
36
|
+
optionsTemplate,
|
|
37
|
+
optionsGroupsTemplate,
|
|
38
|
+
searchNoResultsLabel,
|
|
39
|
+
searchValue,
|
|
40
|
+
selected,
|
|
41
|
+
virtualScroller,
|
|
42
|
+
visibleItems = 10,
|
|
43
|
+
},
|
|
44
|
+
ref
|
|
45
|
+
) => {
|
|
46
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>, option: Option) => {
|
|
47
|
+
if (event.code === 'Space' || event.key === 'Enter') {
|
|
48
|
+
event.preventDefault()
|
|
49
|
+
onOptionClick?.(option)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (event.key === 'Down' || event.key === 'ArrowDown') {
|
|
53
|
+
event.preventDefault()
|
|
54
|
+
const target = event.target as HTMLElement
|
|
55
|
+
const next = getNextSibling(
|
|
56
|
+
target,
|
|
57
|
+
'.autocomplete-option:not(.disabled):not(:disabled)'
|
|
58
|
+
) as HTMLElement | null
|
|
59
|
+
|
|
60
|
+
if (next) {
|
|
61
|
+
next.focus()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (event.key === 'Up' || event.key === 'ArrowUp') {
|
|
66
|
+
event.preventDefault()
|
|
67
|
+
const target = event.target as HTMLElement
|
|
68
|
+
const prev = getPreviousSibling(
|
|
69
|
+
target,
|
|
70
|
+
'.autocomplete-option:not(.disabled):not(:disabled)'
|
|
71
|
+
) as HTMLElement | null
|
|
72
|
+
|
|
73
|
+
if (prev) {
|
|
74
|
+
prev.focus()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const createOption = (option: Option, index: number) => (
|
|
80
|
+
<div
|
|
81
|
+
className={classNames('autocomplete-option', {
|
|
82
|
+
disabled: isOptionDisabled(option),
|
|
83
|
+
selected: isOptionSelected(option, selected),
|
|
84
|
+
})}
|
|
85
|
+
key={index}
|
|
86
|
+
onClick={() => onOptionClick && onOptionClick(option as Option)}
|
|
87
|
+
onKeyDown={(event) => handleKeyDown(event, option as Option)}
|
|
88
|
+
tabIndex={0}
|
|
89
|
+
>
|
|
90
|
+
{optionsTemplate ? (
|
|
91
|
+
optionsTemplate(option as Option)
|
|
92
|
+
) : highlightOptionsOnSearch ? (
|
|
93
|
+
<div
|
|
94
|
+
dangerouslySetInnerHTML={{
|
|
95
|
+
__html: highlightSubstring(getOptionLabel(option), searchValue),
|
|
96
|
+
}}
|
|
97
|
+
></div>
|
|
98
|
+
) : (
|
|
99
|
+
getOptionLabel(option)
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const createOptions = (options: (Option | OptionsGroup)[]): JSX.Element | JSX.Element[] =>
|
|
105
|
+
options.length > 0 ? (
|
|
106
|
+
options.map((option: Option | OptionsGroup | string, index: number) =>
|
|
107
|
+
typeof option !== 'string' && 'options' in option ? (
|
|
108
|
+
<React.Fragment key={index}>
|
|
109
|
+
<div className="autocomplete-optgroup-label">
|
|
110
|
+
{optionsGroupsTemplate
|
|
111
|
+
? optionsGroupsTemplate(option as OptionsGroup)
|
|
112
|
+
: option.label}
|
|
113
|
+
</div>
|
|
114
|
+
{option.options.map((option: Option, index: number) => createOption(option, index))}
|
|
115
|
+
</React.Fragment>
|
|
116
|
+
) : (
|
|
117
|
+
createOption(option as Option, index)
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
) : (
|
|
121
|
+
<div className="autocomplete-options-empty">{searchNoResultsLabel}</div>
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<>
|
|
126
|
+
{virtualScroller ? (
|
|
127
|
+
<CVirtualScroller
|
|
128
|
+
className="autocomplete-options"
|
|
129
|
+
visibleItems={visibleItems}
|
|
130
|
+
role="listbox"
|
|
131
|
+
ref={ref}
|
|
132
|
+
>
|
|
133
|
+
{createOptions(options)}
|
|
134
|
+
</CVirtualScroller>
|
|
135
|
+
) : (
|
|
136
|
+
<div
|
|
137
|
+
className="autocomplete-options"
|
|
138
|
+
{...(optionsMaxHeight !== 'auto' && {
|
|
139
|
+
style: { maxHeight: optionsMaxHeight, overflow: 'scroll' },
|
|
140
|
+
})}
|
|
141
|
+
role="listbox"
|
|
142
|
+
ref={ref}
|
|
143
|
+
>
|
|
144
|
+
{createOptions(options)}
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
{loading && <CElementCover />}
|
|
148
|
+
</>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
CAutocompleteOptions.propTypes = {
|
|
154
|
+
loading: PropTypes.bool,
|
|
155
|
+
onOptionClick: PropTypes.func,
|
|
156
|
+
options: PropTypes.array.isRequired,
|
|
157
|
+
optionsMaxHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
158
|
+
optionsTemplate: PropTypes.func,
|
|
159
|
+
optionsGroupsTemplate: PropTypes.func,
|
|
160
|
+
searchNoResultsLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
|
161
|
+
virtualScroller: PropTypes.bool,
|
|
162
|
+
visibleItems: PropTypes.number,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
CAutocompleteOptions.displayName = 'CAutocompleteOptions'
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
3
|
+
import '@testing-library/jest-dom'
|
|
4
|
+
import { CAutocomplete } from '../CAutocomplete'
|
|
5
|
+
|
|
6
|
+
const mockOptions = [
|
|
7
|
+
{ label: 'Apple', value: 1 },
|
|
8
|
+
{ label: 'Banana', value: 2 },
|
|
9
|
+
{ label: 'Cherry', value: 3 },
|
|
10
|
+
{ label: 'Date', value: 4 },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
const mockOptionsWithGroups = [
|
|
14
|
+
{
|
|
15
|
+
label: 'Fruits',
|
|
16
|
+
options: [
|
|
17
|
+
{ label: 'Apple', value: 1 },
|
|
18
|
+
{ label: 'Banana', value: 2 },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
label: 'Vegetables',
|
|
23
|
+
options: [
|
|
24
|
+
{ label: 'Carrot', value: 3 },
|
|
25
|
+
{ label: 'Spinach', value: 4 },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
test('loads and displays CAutocomplete component', async () => {
|
|
31
|
+
const { container } = render(<CAutocomplete options={mockOptions} />)
|
|
32
|
+
expect(container).toMatchSnapshot()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('CAutocomplete customize', async () => {
|
|
36
|
+
const { container } = render(
|
|
37
|
+
<CAutocomplete
|
|
38
|
+
className="bazinga"
|
|
39
|
+
disabled={true}
|
|
40
|
+
size="lg"
|
|
41
|
+
placeholder="Select option"
|
|
42
|
+
options={mockOptions}
|
|
43
|
+
cleaner={true}
|
|
44
|
+
indicator={true}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
expect(container).toMatchSnapshot()
|
|
48
|
+
expect(container.querySelector('.autocomplete')).toHaveClass('bazinga')
|
|
49
|
+
expect(container.querySelector('.autocomplete')).toHaveClass('autocomplete-lg')
|
|
50
|
+
expect(container.querySelector('.autocomplete')).toHaveClass('disabled')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('CAutocomplete renders input with placeholder', async () => {
|
|
54
|
+
render(<CAutocomplete options={mockOptions} placeholder="Type to search..." />)
|
|
55
|
+
const input = screen.getByRole('combobox')
|
|
56
|
+
expect(input).toHaveAttribute('placeholder', 'Type to search...')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('CAutocomplete opens dropdown on input click', async () => {
|
|
60
|
+
render(<CAutocomplete options={mockOptions} />)
|
|
61
|
+
const input = screen.getByRole('combobox')
|
|
62
|
+
|
|
63
|
+
fireEvent.click(input)
|
|
64
|
+
|
|
65
|
+
await waitFor(() => {
|
|
66
|
+
const listbox = screen.getByRole('listbox')
|
|
67
|
+
expect(listbox).toBeVisible()
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('CAutocomplete filters options on input change', async () => {
|
|
72
|
+
render(<CAutocomplete options={mockOptions} />)
|
|
73
|
+
const input = screen.getByRole('combobox')
|
|
74
|
+
|
|
75
|
+
fireEvent.change(input, { target: { value: 'app' } })
|
|
76
|
+
|
|
77
|
+
await waitFor(() => {
|
|
78
|
+
const option = screen.getByText('Apple')
|
|
79
|
+
expect(option).toBeVisible()
|
|
80
|
+
expect(screen.queryByText('Banana')).not.toBeInTheDocument()
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('CAutocomplete selects option on click', async () => {
|
|
85
|
+
const onChange = jest.fn()
|
|
86
|
+
render(<CAutocomplete options={mockOptions} onChange={onChange} />)
|
|
87
|
+
|
|
88
|
+
const input = screen.getByRole('combobox')
|
|
89
|
+
fireEvent.click(input)
|
|
90
|
+
|
|
91
|
+
await waitFor(() => {
|
|
92
|
+
const option = screen.getByText('Apple')
|
|
93
|
+
fireEvent.click(option)
|
|
94
|
+
expect(onChange).toHaveBeenCalledWith(mockOptions[0])
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('CAutocomplete selects option on Enter key', async () => {
|
|
99
|
+
const onChange = jest.fn()
|
|
100
|
+
render(<CAutocomplete options={mockOptions} onChange={onChange} />)
|
|
101
|
+
|
|
102
|
+
const input = screen.getByRole('combobox')
|
|
103
|
+
fireEvent.change(input, { target: { value: 'Apple' } })
|
|
104
|
+
fireEvent.keyDown(input, { key: 'Enter' })
|
|
105
|
+
|
|
106
|
+
expect(onChange).toHaveBeenCalledWith(mockOptions[0])
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('CAutocomplete closes dropdown on Escape key', async () => {
|
|
110
|
+
render(<CAutocomplete options={mockOptions} />)
|
|
111
|
+
const input = screen.getByRole('combobox')
|
|
112
|
+
|
|
113
|
+
fireEvent.click(input)
|
|
114
|
+
|
|
115
|
+
await waitFor(() => {
|
|
116
|
+
expect(screen.getByRole('listbox')).toBeVisible()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
fireEvent.keyDown(input, { key: 'Escape' })
|
|
120
|
+
|
|
121
|
+
await waitFor(() => {
|
|
122
|
+
expect(input).toHaveAttribute('aria-expanded', 'false')
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('CAutocomplete navigates options with arrow keys', async () => {
|
|
127
|
+
render(<CAutocomplete options={mockOptions} />)
|
|
128
|
+
const input = screen.getByRole('combobox')
|
|
129
|
+
|
|
130
|
+
fireEvent.click(input)
|
|
131
|
+
|
|
132
|
+
await waitFor(() => {
|
|
133
|
+
expect(screen.getByRole('listbox')).toBeVisible()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
fireEvent.keyDown(input, { key: 'ArrowDown' })
|
|
137
|
+
|
|
138
|
+
const firstOption = screen.getByText('Apple')
|
|
139
|
+
expect(firstOption).toHaveFocus()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('CAutocomplete shows cleaner button when option selected', async () => {
|
|
143
|
+
render(<CAutocomplete options={mockOptions} cleaner={true} value={1} />)
|
|
144
|
+
|
|
145
|
+
const cleanerButton = screen.getByRole('button', { name: '' })
|
|
146
|
+
expect(cleanerButton).toHaveClass('autocomplete-cleaner')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('CAutocomplete clears selection when cleaner clicked', async () => {
|
|
150
|
+
const onChange = jest.fn()
|
|
151
|
+
render(<CAutocomplete options={mockOptions} cleaner={true} value={1} onChange={onChange} />)
|
|
152
|
+
|
|
153
|
+
const cleanerButton = screen.getByRole('button', { name: '' })
|
|
154
|
+
fireEvent.click(cleanerButton)
|
|
155
|
+
|
|
156
|
+
expect(onChange).toHaveBeenCalledWith(undefined)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('CAutocomplete shows no results message', async () => {
|
|
160
|
+
render(<CAutocomplete options={mockOptions} searchNoResultsLabel="No options found" />)
|
|
161
|
+
|
|
162
|
+
const input = screen.getByRole('combobox')
|
|
163
|
+
fireEvent.change(input, { target: { value: 'xyz' } })
|
|
164
|
+
|
|
165
|
+
await waitFor(() => {
|
|
166
|
+
expect(screen.getByText('No options found')).toBeVisible()
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('CAutocomplete renders option groups', async () => {
|
|
171
|
+
render(<CAutocomplete options={mockOptionsWithGroups} />)
|
|
172
|
+
|
|
173
|
+
const input = screen.getByRole('combobox')
|
|
174
|
+
fireEvent.click(input)
|
|
175
|
+
|
|
176
|
+
await waitFor(() => {
|
|
177
|
+
expect(screen.getByText('Fruits')).toBeVisible()
|
|
178
|
+
expect(screen.getByText('Vegetables')).toBeVisible()
|
|
179
|
+
expect(screen.getByText('Apple')).toBeVisible()
|
|
180
|
+
expect(screen.getByText('Carrot')).toBeVisible()
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('CAutocomplete shows hint when enabled', async () => {
|
|
185
|
+
render(<CAutocomplete options={mockOptions} showHints={true} />)
|
|
186
|
+
|
|
187
|
+
const input = screen.getByRole('combobox')
|
|
188
|
+
fireEvent.change(input, { target: { value: 'ap' } })
|
|
189
|
+
|
|
190
|
+
await waitFor(() => {
|
|
191
|
+
const hintInput = screen.getByDisplayValue('apple')
|
|
192
|
+
expect(hintInput).toHaveClass('autocomplete-selection-hint')
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('CAutocomplete allows custom values when allowOnlyDefinedOptions is false', async () => {
|
|
197
|
+
const onChange = jest.fn()
|
|
198
|
+
render(
|
|
199
|
+
<CAutocomplete options={mockOptions} onChange={onChange} allowOnlyDefinedOptions={false} />
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
const input = screen.getByRole('combobox')
|
|
203
|
+
fireEvent.change(input, { target: { value: 'Custom Value' } })
|
|
204
|
+
fireEvent.keyDown(input, { key: 'Enter' })
|
|
205
|
+
|
|
206
|
+
expect(onChange).toHaveBeenCalledWith('Custom Value')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('CAutocomplete prevents custom values when allowOnlyDefinedOptions is true', async () => {
|
|
210
|
+
const onChange = jest.fn()
|
|
211
|
+
render(<CAutocomplete options={mockOptions} onChange={onChange} allowOnlyDefinedOptions={true} />)
|
|
212
|
+
|
|
213
|
+
const input = screen.getByRole('combobox')
|
|
214
|
+
fireEvent.change(input, { target: { value: 'Custom Value' } })
|
|
215
|
+
fireEvent.keyDown(input, { key: 'Enter' })
|
|
216
|
+
|
|
217
|
+
expect(onChange).not.toHaveBeenCalled()
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
test('CAutocomplete highlights search matches', async () => {
|
|
221
|
+
render(<CAutocomplete options={mockOptions} highlightOptionsOnSearch={true} />)
|
|
222
|
+
|
|
223
|
+
const input = screen.getByRole('combobox')
|
|
224
|
+
fireEvent.change(input, { target: { value: 'app' } })
|
|
225
|
+
|
|
226
|
+
await waitFor(() => {
|
|
227
|
+
const option = screen.getByText('Apple')
|
|
228
|
+
expect(option.closest('.autocomplete-option')).toBeInTheDocument()
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('CAutocomplete is disabled when disabled prop is true', async () => {
|
|
233
|
+
render(<CAutocomplete options={mockOptions} disabled={true} />)
|
|
234
|
+
|
|
235
|
+
const input = screen.getByRole('combobox')
|
|
236
|
+
expect(input).toBeDisabled()
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('CAutocomplete is readonly when readOnly prop is true', async () => {
|
|
240
|
+
render(<CAutocomplete options={mockOptions} readOnly={true} />)
|
|
241
|
+
|
|
242
|
+
const input = screen.getByRole('combobox')
|
|
243
|
+
expect(input).toHaveAttribute('readonly')
|
|
244
|
+
})
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import '@testing-library/jest-dom'
|
|
4
|
+
import { CAutocompleteOptions } from '../CAutocompleteOptions'
|
|
5
|
+
|
|
6
|
+
const mockOptions = [
|
|
7
|
+
{ label: 'Apple', value: 1 },
|
|
8
|
+
{ label: 'Banana', value: 2 },
|
|
9
|
+
{ label: 'Cherry', value: 3 },
|
|
10
|
+
{ label: 'Date', value: 4 },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
const mockOptionsWithGroups = [
|
|
14
|
+
{
|
|
15
|
+
label: 'Fruits',
|
|
16
|
+
options: [
|
|
17
|
+
{ label: 'Apple', value: 1 },
|
|
18
|
+
{ label: 'Banana', value: 2 },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
label: 'Vegetables',
|
|
23
|
+
options: [
|
|
24
|
+
{ label: 'Carrot', value: 3 },
|
|
25
|
+
{ label: 'Spinach', value: 4 },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
test('loads and displays CAutocompleteOptions component', async () => {
|
|
31
|
+
const { container } = render(<CAutocompleteOptions options={mockOptions} selected={[]} />)
|
|
32
|
+
expect(container).toMatchSnapshot()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('CAutocompleteOptions renders all options', async () => {
|
|
36
|
+
render(<CAutocompleteOptions options={mockOptions} selected={[]} />)
|
|
37
|
+
|
|
38
|
+
expect(screen.getByText('Apple')).toBeVisible()
|
|
39
|
+
expect(screen.getByText('Banana')).toBeVisible()
|
|
40
|
+
expect(screen.getByText('Cherry')).toBeVisible()
|
|
41
|
+
expect(screen.getByText('Date')).toBeVisible()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('CAutocompleteOptions renders option groups', async () => {
|
|
45
|
+
render(<CAutocompleteOptions options={mockOptionsWithGroups} selected={[]} />)
|
|
46
|
+
|
|
47
|
+
expect(screen.getByText('Fruits')).toBeVisible()
|
|
48
|
+
expect(screen.getByText('Vegetables')).toBeVisible()
|
|
49
|
+
expect(screen.getByText('Apple')).toBeVisible()
|
|
50
|
+
expect(screen.getByText('Carrot')).toBeVisible()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('CAutocompleteOptions marks selected option', async () => {
|
|
54
|
+
render(<CAutocompleteOptions options={mockOptions} selected={[mockOptions[0]]} />)
|
|
55
|
+
|
|
56
|
+
const selectedOption = screen.getByText('Apple').closest('.autocomplete-option')
|
|
57
|
+
expect(selectedOption).toHaveClass('selected')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('CAutocompleteOptions calls onOptionClick when option clicked', async () => {
|
|
61
|
+
const onOptionClick = jest.fn()
|
|
62
|
+
render(<CAutocompleteOptions options={mockOptions} selected={[]} onOptionClick={onOptionClick} />)
|
|
63
|
+
|
|
64
|
+
const option = screen.getByText('Apple')
|
|
65
|
+
fireEvent.click(option)
|
|
66
|
+
|
|
67
|
+
expect(onOptionClick).toHaveBeenCalledWith(mockOptions[0])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('CAutocompleteOptions handles Enter key press', async () => {
|
|
71
|
+
const onOptionClick = jest.fn()
|
|
72
|
+
render(<CAutocompleteOptions options={mockOptions} selected={[]} onOptionClick={onOptionClick} />)
|
|
73
|
+
|
|
74
|
+
const option = screen.getByText('Apple')
|
|
75
|
+
fireEvent.keyDown(option, { key: 'Enter' })
|
|
76
|
+
|
|
77
|
+
expect(onOptionClick).toHaveBeenCalledWith(mockOptions[0])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('CAutocompleteOptions handles Space key press', async () => {
|
|
81
|
+
const onOptionClick = jest.fn()
|
|
82
|
+
render(<CAutocompleteOptions options={mockOptions} selected={[]} onOptionClick={onOptionClick} />)
|
|
83
|
+
|
|
84
|
+
const option = screen.getByText('Apple')
|
|
85
|
+
fireEvent.keyDown(option, { code: 'Space' })
|
|
86
|
+
|
|
87
|
+
expect(onOptionClick).toHaveBeenCalledWith(mockOptions[0])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('CAutocompleteOptions navigates with arrow keys', async () => {
|
|
91
|
+
render(<CAutocompleteOptions options={mockOptions} selected={[]} />)
|
|
92
|
+
|
|
93
|
+
const firstOption = screen.getByText('Apple')
|
|
94
|
+
const secondOption = screen.getByText('Banana')
|
|
95
|
+
|
|
96
|
+
firstOption.focus()
|
|
97
|
+
fireEvent.keyDown(firstOption, { key: 'ArrowDown' })
|
|
98
|
+
|
|
99
|
+
expect(document.activeElement).toBe(secondOption)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('CAutocompleteOptions shows no results message', async () => {
|
|
103
|
+
render(
|
|
104
|
+
<CAutocompleteOptions options={[]} selected={[]} searchNoResultsLabel="No options found" />
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
expect(screen.getByText('No options found')).toBeVisible()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('CAutocompleteOptions highlights search matches', async () => {
|
|
111
|
+
render(
|
|
112
|
+
<CAutocompleteOptions
|
|
113
|
+
options={mockOptions}
|
|
114
|
+
selected={[]}
|
|
115
|
+
highlightOptionsOnSearch={true}
|
|
116
|
+
searchValue="app"
|
|
117
|
+
/>
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
const option = screen.getByText('Apple').closest('.autocomplete-option')
|
|
121
|
+
expect(option).toContainHTML('dangerouslySetInnerHTML')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('CAutocompleteOptions uses custom option template', async () => {
|
|
125
|
+
const customTemplate = (option) => (
|
|
126
|
+
<div className="custom-option">
|
|
127
|
+
{option.label} - {option.value}
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
render(
|
|
132
|
+
<CAutocompleteOptions options={mockOptions} selected={[]} optionsTemplate={customTemplate} />
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
expect(screen.getByText('Apple - 1')).toBeVisible()
|
|
136
|
+
expect(screen.getByText('Banana - 2')).toBeVisible()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('CAutocompleteOptions uses custom group template', async () => {
|
|
140
|
+
const customGroupTemplate = (group) => (
|
|
141
|
+
<div className="custom-group">{group.label.toUpperCase()}</div>
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
render(
|
|
145
|
+
<CAutocompleteOptions
|
|
146
|
+
options={mockOptionsWithGroups}
|
|
147
|
+
selected={[]}
|
|
148
|
+
optionsGroupsTemplate={customGroupTemplate}
|
|
149
|
+
/>
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
expect(screen.getByText('FRUITS')).toBeVisible()
|
|
153
|
+
expect(screen.getByText('VEGETABLES')).toBeVisible()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('CAutocompleteOptions shows loading state', async () => {
|
|
157
|
+
const { container } = render(
|
|
158
|
+
<CAutocompleteOptions options={mockOptions} selected={[]} loading={true} />
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
expect(container.querySelector('.element-cover')).toBeInTheDocument()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('CAutocompleteOptions renders with virtual scroller', async () => {
|
|
165
|
+
const { container } = render(
|
|
166
|
+
<CAutocompleteOptions
|
|
167
|
+
options={mockOptions}
|
|
168
|
+
selected={[]}
|
|
169
|
+
virtualScroller={true}
|
|
170
|
+
visibleItems={2}
|
|
171
|
+
/>
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
expect(container.querySelector('.virtual-scroller')).toBeInTheDocument()
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('CAutocompleteOptions sets max height', async () => {
|
|
178
|
+
const { container } = render(
|
|
179
|
+
<CAutocompleteOptions options={mockOptions} selected={[]} optionsMaxHeight={200} />
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
const optionsContainer = container.querySelector('.autocomplete-options')
|
|
183
|
+
expect(optionsContainer).toHaveStyle('max-height: 200px')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('CAutocompleteOptions marks disabled options', async () => {
|
|
187
|
+
const optionsWithDisabled = [
|
|
188
|
+
{ label: 'Apple', value: 1 },
|
|
189
|
+
{ label: 'Banana', value: 2, disabled: true },
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
render(<CAutocompleteOptions options={optionsWithDisabled} selected={[]} />)
|
|
193
|
+
|
|
194
|
+
const disabledOption = screen.getByText('Banana').closest('.autocomplete-option')
|
|
195
|
+
expect(disabledOption).toHaveClass('disabled')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('CAutocompleteOptions has correct accessibility attributes', async () => {
|
|
199
|
+
render(<CAutocompleteOptions options={mockOptions} selected={[]} />)
|
|
200
|
+
|
|
201
|
+
const listbox = screen.getByRole('listbox')
|
|
202
|
+
expect(listbox).toBeInTheDocument()
|
|
203
|
+
|
|
204
|
+
const options = screen.getAllByRole('option')
|
|
205
|
+
expect(options).toHaveLength(4)
|
|
206
|
+
|
|
207
|
+
options.forEach((option) => {
|
|
208
|
+
expect(option).toHaveAttribute('tabIndex', '0')
|
|
209
|
+
})
|
|
210
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type Option =
|
|
2
|
+
| string
|
|
3
|
+
| {
|
|
4
|
+
disabled?: boolean
|
|
5
|
+
label: string
|
|
6
|
+
options?: Option[]
|
|
7
|
+
custom?: boolean
|
|
8
|
+
selected?: boolean
|
|
9
|
+
value?: number | string
|
|
10
|
+
[key: string]: unknown
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type OptionsGroup = {
|
|
14
|
+
label: string
|
|
15
|
+
options?: Option[]
|
|
16
|
+
[key: string]: unknown
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type Search =
|
|
20
|
+
| 'external'
|
|
21
|
+
| 'global'
|
|
22
|
+
| {
|
|
23
|
+
external?: boolean
|
|
24
|
+
global?: boolean
|
|
25
|
+
}
|