@sabrenski/spire-ui 0.0.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/LICENSE +21 -0
- package/README.md +233 -0
- package/dist/index.d.ts +4981 -0
- package/dist/spire-ui.css +1 -0
- package/dist/spire-ui.es.js +18403 -0
- package/dist/spire-ui.umd.js +45 -0
- package/package.json +83 -0
- package/src/components/Accordion/Accordion.test.ts +218 -0
- package/src/components/Accordion/AccordionContent.vue +112 -0
- package/src/components/Accordion/AccordionItem.vue +87 -0
- package/src/components/Accordion/AccordionRoot.vue +111 -0
- package/src/components/Accordion/AccordionTrigger.vue +125 -0
- package/src/components/Accordion/index.ts +11 -0
- package/src/components/Accordion/keys.ts +23 -0
- package/src/components/Avatar/Avatar.test.ts +181 -0
- package/src/components/Avatar/Avatar.vue +150 -0
- package/src/components/Avatar/index.ts +2 -0
- package/src/components/Badge/Badge.test.ts +141 -0
- package/src/components/Badge/Badge.vue +133 -0
- package/src/components/Badge/index.ts +2 -0
- package/src/components/BadgeContainer/BadgeContainer.test.ts +150 -0
- package/src/components/BadgeContainer/BadgeContainer.vue +90 -0
- package/src/components/BadgeContainer/index.ts +2 -0
- package/src/components/Breadcrumb/Breadcrumb.test.ts +342 -0
- package/src/components/Breadcrumb/BreadcrumbEllipsis.vue +96 -0
- package/src/components/Breadcrumb/BreadcrumbItem.vue +16 -0
- package/src/components/Breadcrumb/BreadcrumbLink.vue +67 -0
- package/src/components/Breadcrumb/BreadcrumbList.vue +20 -0
- package/src/components/Breadcrumb/BreadcrumbPage.vue +25 -0
- package/src/components/Breadcrumb/BreadcrumbRoot.vue +41 -0
- package/src/components/Breadcrumb/BreadcrumbSeparator.vue +63 -0
- package/src/components/Breadcrumb/index.ts +13 -0
- package/src/components/Breadcrumb/keys.ts +7 -0
- package/src/components/Button/Button.test.ts +231 -0
- package/src/components/Button/Button.vue +349 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Callout/Callout.test.ts +260 -0
- package/src/components/Callout/Callout.vue +341 -0
- package/src/components/Callout/index.ts +2 -0
- package/src/components/Card/Card.test.ts +565 -0
- package/src/components/Card/Card.vue +209 -0
- package/src/components/Card/CardContent.vue +57 -0
- package/src/components/Card/CardFooter.vue +72 -0
- package/src/components/Card/CardHeader.vue +111 -0
- package/src/components/Card/CardImage.vue +124 -0
- package/src/components/Card/index.ts +14 -0
- package/src/components/Chart/BarChart.vue +208 -0
- package/src/components/Chart/BaseChart.vue +444 -0
- package/src/components/Chart/Chart.test.ts +359 -0
- package/src/components/Chart/DonutChart.vue +283 -0
- package/src/components/Chart/LineChart.vue +211 -0
- package/src/components/Chart/index.ts +20 -0
- package/src/components/Chart/useChartTheme.ts +192 -0
- package/src/components/Checkbox/Checkbox.test.ts +209 -0
- package/src/components/Checkbox/Checkbox.vue +285 -0
- package/src/components/Checkbox/index.ts +2 -0
- package/src/components/ChoiceChip/ChoiceChip.test.ts +142 -0
- package/src/components/ChoiceChip/ChoiceChip.vue +218 -0
- package/src/components/ChoiceChip/index.ts +2 -0
- package/src/components/ChoiceChipGroup/ChoiceChipGroup.test.ts +151 -0
- package/src/components/ChoiceChipGroup/ChoiceChipGroup.vue +70 -0
- package/src/components/ChoiceChipGroup/index.ts +2 -0
- package/src/components/ColorPicker/ColorArea.vue +159 -0
- package/src/components/ColorPicker/ColorPicker.test.ts +250 -0
- package/src/components/ColorPicker/ColorPicker.vue +339 -0
- package/src/components/ColorPicker/ColorSlider.vue +191 -0
- package/src/components/ColorPicker/index.ts +7 -0
- package/src/components/Combobox/Combobox.test.ts +891 -0
- package/src/components/Combobox/Combobox.vue +934 -0
- package/src/components/Combobox/index.ts +2 -0
- package/src/components/DataTable/DataTable.test.ts +1221 -0
- package/src/components/DataTable/DataTable.vue +1415 -0
- package/src/components/DataTable/index.ts +10 -0
- package/src/components/DatePicker/DatePicker.test.ts +625 -0
- package/src/components/DatePicker/DatePicker.vue +1586 -0
- package/src/components/DatePicker/index.ts +2 -0
- package/src/components/Drawer/Drawer.test.ts +336 -0
- package/src/components/Drawer/Drawer.vue +466 -0
- package/src/components/Drawer/index.ts +2 -0
- package/src/components/Dropdown/Dropdown.test.ts +607 -0
- package/src/components/Dropdown/Dropdown.vue +807 -0
- package/src/components/Dropdown/DropdownItem.vue +227 -0
- package/src/components/Dropdown/DropdownSeparator.vue +14 -0
- package/src/components/Dropdown/DropdownSub.vue +104 -0
- package/src/components/Dropdown/DropdownSubContent.vue +187 -0
- package/src/components/Dropdown/DropdownSubTrigger.vue +151 -0
- package/src/components/Dropdown/index.ts +14 -0
- package/src/components/EmptyState/EmptyState.test.ts +180 -0
- package/src/components/EmptyState/EmptyState.vue +137 -0
- package/src/components/EmptyState/index.ts +2 -0
- package/src/components/FileUpload/FileUpload.test.ts +1151 -0
- package/src/components/FileUpload/FileUpload.vue +1042 -0
- package/src/components/FileUpload/index.ts +2 -0
- package/src/components/Heading/Heading.test.ts +107 -0
- package/src/components/Heading/Heading.vue +67 -0
- package/src/components/Heading/index.ts +2 -0
- package/src/components/Icon/Icon.test.ts +157 -0
- package/src/components/Icon/Icon.vue +86 -0
- package/src/components/Icon/index.ts +2 -0
- package/src/components/Input/Input.test.ts +273 -0
- package/src/components/Input/Input.vue +388 -0
- package/src/components/Input/index.ts +2 -0
- package/src/components/Layout/Container.vue +67 -0
- package/src/components/Layout/Grid.vue +159 -0
- package/src/components/Layout/GridItem.vue +154 -0
- package/src/components/Layout/Layout.test.ts +202 -0
- package/src/components/Layout/Stack.vue +128 -0
- package/src/components/Layout/index.ts +9 -0
- package/src/components/Layout/keys.ts +7 -0
- package/src/components/Modal/Modal.test.ts +311 -0
- package/src/components/Modal/Modal.vue +336 -0
- package/src/components/Modal/index.ts +2 -0
- package/src/components/Pagination/Pagination.test.ts +303 -0
- package/src/components/Pagination/Pagination.vue +212 -0
- package/src/components/Pagination/index.ts +3 -0
- package/src/components/Pagination/utils.ts +86 -0
- package/src/components/Popover/Popover.test.ts +285 -0
- package/src/components/Popover/Popover.vue +441 -0
- package/src/components/Popover/index.ts +2 -0
- package/src/components/Progress/Progress.test.ts +361 -0
- package/src/components/Progress/Progress.vue +363 -0
- package/src/components/Progress/index.ts +7 -0
- package/src/components/Radio/Radio.test.ts +216 -0
- package/src/components/Radio/Radio.vue +214 -0
- package/src/components/Radio/index.ts +2 -0
- package/src/components/Rating/Rating.test.ts +319 -0
- package/src/components/Rating/Rating.vue +247 -0
- package/src/components/Rating/index.ts +2 -0
- package/src/components/SegmentedControl/SegmentedControl.test.ts +292 -0
- package/src/components/SegmentedControl/SegmentedControl.vue +288 -0
- package/src/components/SegmentedControl/index.ts +2 -0
- package/src/components/Select/Select.test.ts +589 -0
- package/src/components/Select/Select.vue +666 -0
- package/src/components/Select/index.ts +2 -0
- package/src/components/Sidebar/Sidebar.test.ts +301 -0
- package/src/components/Sidebar/SidebarGroup.vue +103 -0
- package/src/components/Sidebar/SidebarItem.vue +196 -0
- package/src/components/Sidebar/SidebarLayout.vue +42 -0
- package/src/components/Sidebar/SidebarRoot.vue +122 -0
- package/src/components/Sidebar/index.ts +11 -0
- package/src/components/Sidebar/keys.ts +14 -0
- package/src/components/Skeleton/Skeleton.test.ts +130 -0
- package/src/components/Skeleton/Skeleton.vue +104 -0
- package/src/components/Skeleton/index.ts +2 -0
- package/src/components/Slider/Slider.test.ts +416 -0
- package/src/components/Slider/Slider.vue +435 -0
- package/src/components/Slider/index.ts +2 -0
- package/src/components/Slider/utils.ts +91 -0
- package/src/components/Spinner/Spinner.test.ts +79 -0
- package/src/components/Spinner/Spinner.vue +159 -0
- package/src/components/Spinner/index.ts +2 -0
- package/src/components/SpireProvider/SpireProvider.vue +71 -0
- package/src/components/SpireProvider/index.ts +11 -0
- package/src/components/Stepper/Stepper.test.ts +221 -0
- package/src/components/Stepper/StepperContent.vue +51 -0
- package/src/components/Stepper/StepperItem.vue +89 -0
- package/src/components/Stepper/StepperRoot.vue +101 -0
- package/src/components/Stepper/StepperSeparator.vue +52 -0
- package/src/components/Stepper/StepperTrigger.vue +144 -0
- package/src/components/Stepper/index.ts +11 -0
- package/src/components/Stepper/keys.ts +27 -0
- package/src/components/Switch/Switch.test.ts +214 -0
- package/src/components/Switch/Switch.vue +235 -0
- package/src/components/Switch/index.ts +2 -0
- package/src/components/Tabs/Tabs.test.ts +363 -0
- package/src/components/Tabs/Tabs.vue +318 -0
- package/src/components/Tabs/index.ts +2 -0
- package/src/components/Text/Text.test.ts +154 -0
- package/src/components/Text/Text.vue +100 -0
- package/src/components/Text/index.ts +2 -0
- package/src/components/Textarea/Textarea.test.ts +432 -0
- package/src/components/Textarea/Textarea.vue +411 -0
- package/src/components/Textarea/index.ts +2 -0
- package/src/components/TimePicker/TimePicker.test.ts +352 -0
- package/src/components/TimePicker/TimePicker.vue +569 -0
- package/src/components/TimePicker/index.ts +2 -0
- package/src/components/Timeline/Timeline.test.ts +193 -0
- package/src/components/Timeline/Timeline.vue +111 -0
- package/src/components/Timeline/TimelineItem.vue +167 -0
- package/src/components/Timeline/index.ts +13 -0
- package/src/components/Timeline/keys.ts +21 -0
- package/src/components/Toast/ToastItem.test.ts +289 -0
- package/src/components/Toast/ToastItem.vue +370 -0
- package/src/components/Toast/ToastProvider.test.ts +158 -0
- package/src/components/Toast/ToastProvider.vue +181 -0
- package/src/components/Toast/index.ts +83 -0
- package/src/components/Toast/toastState.test.ts +165 -0
- package/src/components/Toast/toastState.ts +161 -0
- package/src/components/ToggleButton/ToggleButton.test.ts +166 -0
- package/src/components/ToggleButton/ToggleButton.vue +197 -0
- package/src/components/ToggleButton/index.ts +2 -0
- package/src/components/ToggleGroup/ToggleGroup.test.ts +181 -0
- package/src/components/ToggleGroup/ToggleGroup.vue +130 -0
- package/src/components/ToggleGroup/index.ts +2 -0
- package/src/components/Tooltip/Tooltip.test.ts +238 -0
- package/src/components/Tooltip/Tooltip.vue +217 -0
- package/src/components/Tooltip/index.ts +2 -0
- package/src/components/TreeView/TreeView.test.ts +357 -0
- package/src/components/TreeView/TreeView.vue +251 -0
- package/src/components/TreeView/TreeViewItem.vue +288 -0
- package/src/components/TreeView/index.ts +11 -0
- package/src/components/TreeView/keys.ts +35 -0
- package/src/composables/index.ts +12 -0
- package/src/composables/useClickOutside.ts +36 -0
- package/src/composables/useClipboard.ts +35 -0
- package/src/composables/useEventListener.ts +48 -0
- package/src/composables/useFocusTrap.ts +58 -0
- package/src/composables/useHoverReveal.ts +98 -0
- package/src/composables/useId.ts +10 -0
- package/src/composables/useMagnetic.ts +171 -0
- package/src/composables/useRelativePosition.ts +127 -0
- package/src/composables/useRipple.ts +146 -0
- package/src/composables/useScrollLock.ts +25 -0
- package/src/composables/useSpireConfig.ts +27 -0
- package/src/composables/useStagger.ts +224 -0
- package/src/config/icons.test.ts +115 -0
- package/src/config/icons.ts +170 -0
- package/src/index.ts +361 -0
- package/src/styles/depth.css +129 -0
- package/src/styles/effects.css +169 -0
- package/src/styles/fallback.css +152 -0
- package/src/styles/main.css +25 -0
- package/src/styles/mood.css +211 -0
- package/src/styles/motion.css +159 -0
- package/src/styles/reset.css +97 -0
- package/src/styles/theme.css +708 -0
- package/src/styles/tokens.css +183 -0
- package/src/utils/.gitkeep +0 -0
- package/src/utils/color.ts +277 -0
- package/src/utils/date.test.ts +522 -0
- package/src/utils/date.ts +380 -0
- package/src/utils/index.ts +23 -0
- package/src/utils/object.test.ts +80 -0
- package/src/utils/object.ts +25 -0
- package/src/utils/string.test.ts +64 -0
- package/src/utils/string.ts +32 -0
- package/src/utils/time.ts +156 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { mount, config } from '@vue/test-utils'
|
|
3
|
+
import { nextTick } from 'vue'
|
|
4
|
+
import Select from './Select.vue'
|
|
5
|
+
|
|
6
|
+
// Disable teleport for testing
|
|
7
|
+
config.global.stubs = {
|
|
8
|
+
teleport: true
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const defaultOptions = [
|
|
12
|
+
{ label: 'Apple', value: 'apple' },
|
|
13
|
+
{ label: 'Banana', value: 'banana' },
|
|
14
|
+
{ label: 'Cherry', value: 'cherry' }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
describe('Select', () => {
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.restoreAllMocks()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('Rendering', () => {
|
|
23
|
+
it('renders trigger button with combobox role', () => {
|
|
24
|
+
const wrapper = mount(Select, {
|
|
25
|
+
props: { options: defaultOptions }
|
|
26
|
+
})
|
|
27
|
+
expect(wrapper.find('[role="combobox"]').exists()).toBe(true)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('renders label when provided', () => {
|
|
31
|
+
const wrapper = mount(Select, {
|
|
32
|
+
props: { options: defaultOptions, label: 'Select Fruit' }
|
|
33
|
+
})
|
|
34
|
+
expect(wrapper.find('label').text()).toBe('Select Fruit')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('renders required indicator when required', () => {
|
|
38
|
+
const wrapper = mount(Select, {
|
|
39
|
+
props: { options: defaultOptions, label: 'Fruit', required: true }
|
|
40
|
+
})
|
|
41
|
+
expect(wrapper.find('.ui-select__required').exists()).toBe(true)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('renders placeholder when no selection', () => {
|
|
45
|
+
const wrapper = mount(Select, {
|
|
46
|
+
props: { options: defaultOptions, placeholder: 'Choose...' }
|
|
47
|
+
})
|
|
48
|
+
expect(wrapper.find('.ui-select__value').text()).toBe('Choose...')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('renders selected value label', () => {
|
|
52
|
+
const wrapper = mount(Select, {
|
|
53
|
+
props: { options: defaultOptions, modelValue: 'banana' }
|
|
54
|
+
})
|
|
55
|
+
expect(wrapper.find('.ui-select__value').text()).toBe('Banana')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('renders hint text when provided', () => {
|
|
59
|
+
const wrapper = mount(Select, {
|
|
60
|
+
props: { options: defaultOptions, hint: 'Pick your favorite' }
|
|
61
|
+
})
|
|
62
|
+
expect(wrapper.find('.ui-select__message--hint').text()).toBe('Pick your favorite')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('renders error text when provided', () => {
|
|
66
|
+
const wrapper = mount(Select, {
|
|
67
|
+
props: { options: defaultOptions, error: 'Selection required' }
|
|
68
|
+
})
|
|
69
|
+
expect(wrapper.find('.ui-select__message--error').text()).toBe('Selection required')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('renders chevron icon', () => {
|
|
73
|
+
const wrapper = mount(Select, {
|
|
74
|
+
props: { options: defaultOptions }
|
|
75
|
+
})
|
|
76
|
+
expect(wrapper.find('.ui-select__chevron').exists()).toBe(true)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('Open/Close behavior', () => {
|
|
81
|
+
it('opens dropdown on click', async () => {
|
|
82
|
+
const wrapper = mount(Select, {
|
|
83
|
+
props: { options: defaultOptions }
|
|
84
|
+
})
|
|
85
|
+
await wrapper.find('[role="combobox"]').trigger('click')
|
|
86
|
+
expect(wrapper.find('[role="listbox"]').exists()).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('closes dropdown on second click', async () => {
|
|
90
|
+
const wrapper = mount(Select, {
|
|
91
|
+
props: { options: defaultOptions }
|
|
92
|
+
})
|
|
93
|
+
const trigger = wrapper.find('[role="combobox"]')
|
|
94
|
+
await trigger.trigger('click')
|
|
95
|
+
expect(wrapper.find('[role="listbox"]').exists()).toBe(true)
|
|
96
|
+
await trigger.trigger('click')
|
|
97
|
+
expect(wrapper.find('[role="listbox"]').exists()).toBe(false)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('does not open when disabled', async () => {
|
|
101
|
+
const wrapper = mount(Select, {
|
|
102
|
+
props: { options: defaultOptions, disabled: true }
|
|
103
|
+
})
|
|
104
|
+
await wrapper.find('[role="combobox"]').trigger('click')
|
|
105
|
+
expect(wrapper.find('[role="listbox"]').exists()).toBe(false)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('renders all options when open', async () => {
|
|
109
|
+
const wrapper = mount(Select, {
|
|
110
|
+
props: { options: defaultOptions }
|
|
111
|
+
})
|
|
112
|
+
await wrapper.find('[role="combobox"]').trigger('click')
|
|
113
|
+
const options = wrapper.findAll('[role="option"]')
|
|
114
|
+
expect(options).toHaveLength(3)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('chevron rotates when open', async () => {
|
|
118
|
+
const wrapper = mount(Select, {
|
|
119
|
+
props: { options: defaultOptions }
|
|
120
|
+
})
|
|
121
|
+
await wrapper.find('[role="combobox"]').trigger('click')
|
|
122
|
+
expect(wrapper.find('.ui-select__chevron--open').exists()).toBe(true)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('Selection', () => {
|
|
127
|
+
it('emits update:modelValue on option click', async () => {
|
|
128
|
+
const wrapper = mount(Select, {
|
|
129
|
+
props: { options: defaultOptions }
|
|
130
|
+
})
|
|
131
|
+
await wrapper.find('[role="combobox"]').trigger('click')
|
|
132
|
+
const options = wrapper.findAll('[role="option"]')
|
|
133
|
+
await options[1].trigger('click')
|
|
134
|
+
expect(wrapper.emitted('update:modelValue')).toEqual([['banana']])
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('emits change event on selection', async () => {
|
|
138
|
+
const wrapper = mount(Select, {
|
|
139
|
+
props: { options: defaultOptions }
|
|
140
|
+
})
|
|
141
|
+
await wrapper.find('[role="combobox"]').trigger('click')
|
|
142
|
+
const options = wrapper.findAll('[role="option"]')
|
|
143
|
+
await options[1].trigger('click')
|
|
144
|
+
expect(wrapper.emitted('change')).toEqual([['banana']])
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('closes dropdown after selection', async () => {
|
|
148
|
+
const wrapper = mount(Select, {
|
|
149
|
+
props: { options: defaultOptions }
|
|
150
|
+
})
|
|
151
|
+
await wrapper.find('[role="combobox"]').trigger('click')
|
|
152
|
+
const options = wrapper.findAll('[role="option"]')
|
|
153
|
+
await options[1].trigger('click')
|
|
154
|
+
expect(wrapper.find('[role="listbox"]').exists()).toBe(false)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('does not emit when disabled option clicked', async () => {
|
|
158
|
+
const options = [
|
|
159
|
+
{ label: 'Apple', value: 'apple' },
|
|
160
|
+
{ label: 'Banana', value: 'banana', disabled: true }
|
|
161
|
+
]
|
|
162
|
+
const wrapper = mount(Select, {
|
|
163
|
+
props: { options }
|
|
164
|
+
})
|
|
165
|
+
await wrapper.find('[role="combobox"]').trigger('click')
|
|
166
|
+
const optionElements = wrapper.findAll('[role="option"]')
|
|
167
|
+
await optionElements[1].trigger('click')
|
|
168
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('shows checkmark for selected option', async () => {
|
|
172
|
+
const wrapper = mount(Select, {
|
|
173
|
+
props: { options: defaultOptions, modelValue: 'banana' }
|
|
174
|
+
})
|
|
175
|
+
await wrapper.find('[role="combobox"]').trigger('click')
|
|
176
|
+
const selectedOption = wrapper.findAll('[role="option"]')[1]
|
|
177
|
+
expect(selectedOption.find('.ui-select__check').exists()).toBe(true)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
describe('Keyboard navigation', () => {
|
|
182
|
+
it('opens with Enter key', async () => {
|
|
183
|
+
const wrapper = mount(Select, {
|
|
184
|
+
props: { options: defaultOptions },
|
|
185
|
+
attachTo: document.body
|
|
186
|
+
})
|
|
187
|
+
await wrapper.find('[role="combobox"]').trigger('keydown', { key: 'Enter' })
|
|
188
|
+
expect(wrapper.find('[role="listbox"]').exists()).toBe(true)
|
|
189
|
+
wrapper.unmount()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('opens with Space key', async () => {
|
|
193
|
+
const wrapper = mount(Select, {
|
|
194
|
+
props: { options: defaultOptions },
|
|
195
|
+
attachTo: document.body
|
|
196
|
+
})
|
|
197
|
+
await wrapper.find('[role="combobox"]').trigger('keydown', { key: ' ' })
|
|
198
|
+
expect(wrapper.find('[role="listbox"]').exists()).toBe(true)
|
|
199
|
+
wrapper.unmount()
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('opens with ArrowDown key', async () => {
|
|
203
|
+
const wrapper = mount(Select, {
|
|
204
|
+
props: { options: defaultOptions },
|
|
205
|
+
attachTo: document.body
|
|
206
|
+
})
|
|
207
|
+
await wrapper.find('[role="combobox"]').trigger('keydown', { key: 'ArrowDown' })
|
|
208
|
+
expect(wrapper.find('[role="listbox"]').exists()).toBe(true)
|
|
209
|
+
wrapper.unmount()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('opens with ArrowUp key', async () => {
|
|
213
|
+
const wrapper = mount(Select, {
|
|
214
|
+
props: { options: defaultOptions },
|
|
215
|
+
attachTo: document.body
|
|
216
|
+
})
|
|
217
|
+
await wrapper.find('[role="combobox"]').trigger('keydown', { key: 'ArrowUp' })
|
|
218
|
+
expect(wrapper.find('[role="listbox"]').exists()).toBe(true)
|
|
219
|
+
wrapper.unmount()
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('closes with Escape key', async () => {
|
|
223
|
+
const wrapper = mount(Select, {
|
|
224
|
+
props: { options: defaultOptions },
|
|
225
|
+
attachTo: document.body
|
|
226
|
+
})
|
|
227
|
+
const trigger = wrapper.find('[role="combobox"]')
|
|
228
|
+
await trigger.trigger('click')
|
|
229
|
+
await trigger.trigger('keydown', { key: 'Escape' })
|
|
230
|
+
expect(wrapper.find('[role="listbox"]').exists()).toBe(false)
|
|
231
|
+
wrapper.unmount()
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('navigates down with ArrowDown', async () => {
|
|
235
|
+
const wrapper = mount(Select, {
|
|
236
|
+
props: { options: defaultOptions },
|
|
237
|
+
attachTo: document.body
|
|
238
|
+
})
|
|
239
|
+
const trigger = wrapper.find('[role="combobox"]')
|
|
240
|
+
await trigger.trigger('click')
|
|
241
|
+
await trigger.trigger('keydown', { key: 'ArrowDown' })
|
|
242
|
+
await nextTick()
|
|
243
|
+
// Check highlighted class moved
|
|
244
|
+
const options = wrapper.findAll('[role="option"]')
|
|
245
|
+
expect(options[1].classes()).toContain('ui-select__option--highlighted')
|
|
246
|
+
wrapper.unmount()
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('navigates up with ArrowUp', async () => {
|
|
250
|
+
const wrapper = mount(Select, {
|
|
251
|
+
props: { options: defaultOptions, modelValue: 'cherry' },
|
|
252
|
+
attachTo: document.body
|
|
253
|
+
})
|
|
254
|
+
const trigger = wrapper.find('[role="combobox"]')
|
|
255
|
+
await trigger.trigger('click')
|
|
256
|
+
await trigger.trigger('keydown', { key: 'ArrowUp' })
|
|
257
|
+
await nextTick()
|
|
258
|
+
const options = wrapper.findAll('[role="option"]')
|
|
259
|
+
expect(options[1].classes()).toContain('ui-select__option--highlighted')
|
|
260
|
+
wrapper.unmount()
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('wraps around at end', async () => {
|
|
264
|
+
const wrapper = mount(Select, {
|
|
265
|
+
props: { options: defaultOptions, modelValue: 'cherry' },
|
|
266
|
+
attachTo: document.body
|
|
267
|
+
})
|
|
268
|
+
const trigger = wrapper.find('[role="combobox"]')
|
|
269
|
+
await trigger.trigger('click')
|
|
270
|
+
await trigger.trigger('keydown', { key: 'ArrowDown' })
|
|
271
|
+
await nextTick()
|
|
272
|
+
const options = wrapper.findAll('[role="option"]')
|
|
273
|
+
expect(options[0].classes()).toContain('ui-select__option--highlighted')
|
|
274
|
+
wrapper.unmount()
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('selects with Enter on highlighted option', async () => {
|
|
278
|
+
const wrapper = mount(Select, {
|
|
279
|
+
props: { options: defaultOptions },
|
|
280
|
+
attachTo: document.body
|
|
281
|
+
})
|
|
282
|
+
const trigger = wrapper.find('[role="combobox"]')
|
|
283
|
+
await trigger.trigger('click')
|
|
284
|
+
await trigger.trigger('keydown', { key: 'ArrowDown' })
|
|
285
|
+
await trigger.trigger('keydown', { key: 'Enter' })
|
|
286
|
+
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['banana'])
|
|
287
|
+
wrapper.unmount()
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('goes to first option with Home key', async () => {
|
|
291
|
+
const wrapper = mount(Select, {
|
|
292
|
+
props: { options: defaultOptions, modelValue: 'cherry' },
|
|
293
|
+
attachTo: document.body
|
|
294
|
+
})
|
|
295
|
+
const trigger = wrapper.find('[role="combobox"]')
|
|
296
|
+
await trigger.trigger('click')
|
|
297
|
+
await trigger.trigger('keydown', { key: 'Home' })
|
|
298
|
+
await nextTick()
|
|
299
|
+
const options = wrapper.findAll('[role="option"]')
|
|
300
|
+
expect(options[0].classes()).toContain('ui-select__option--highlighted')
|
|
301
|
+
wrapper.unmount()
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('goes to last option with End key', async () => {
|
|
305
|
+
const wrapper = mount(Select, {
|
|
306
|
+
props: { options: defaultOptions, modelValue: 'apple' },
|
|
307
|
+
attachTo: document.body
|
|
308
|
+
})
|
|
309
|
+
const trigger = wrapper.find('[role="combobox"]')
|
|
310
|
+
await trigger.trigger('click')
|
|
311
|
+
await trigger.trigger('keydown', { key: 'End' })
|
|
312
|
+
await nextTick()
|
|
313
|
+
const options = wrapper.findAll('[role="option"]')
|
|
314
|
+
expect(options[2].classes()).toContain('ui-select__option--highlighted')
|
|
315
|
+
wrapper.unmount()
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('skips disabled options', async () => {
|
|
319
|
+
const options = [
|
|
320
|
+
{ label: 'Apple', value: 'apple' },
|
|
321
|
+
{ label: 'Banana', value: 'banana', disabled: true },
|
|
322
|
+
{ label: 'Cherry', value: 'cherry' }
|
|
323
|
+
]
|
|
324
|
+
const wrapper = mount(Select, {
|
|
325
|
+
props: { options, modelValue: 'apple' },
|
|
326
|
+
attachTo: document.body
|
|
327
|
+
})
|
|
328
|
+
const trigger = wrapper.find('[role="combobox"]')
|
|
329
|
+
await trigger.trigger('click')
|
|
330
|
+
await trigger.trigger('keydown', { key: 'ArrowDown' })
|
|
331
|
+
await nextTick()
|
|
332
|
+
const optionElements = wrapper.findAll('[role="option"]')
|
|
333
|
+
expect(optionElements[2].classes()).toContain('ui-select__option--highlighted')
|
|
334
|
+
wrapper.unmount()
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
describe('Highlight on hover', () => {
|
|
339
|
+
it('highlights option on mouseenter', async () => {
|
|
340
|
+
const wrapper = mount(Select, {
|
|
341
|
+
props: { options: defaultOptions }
|
|
342
|
+
})
|
|
343
|
+
await wrapper.find('[role="combobox"]').trigger('click')
|
|
344
|
+
await wrapper.findAll('[role="option"]')[2].trigger('mouseenter')
|
|
345
|
+
await nextTick()
|
|
346
|
+
const options = wrapper.findAll('[role="option"]')
|
|
347
|
+
expect(options[2].classes()).toContain('ui-select__option--highlighted')
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('does not highlight disabled option on mouseenter', async () => {
|
|
351
|
+
const options = [
|
|
352
|
+
{ label: 'Apple', value: 'apple' },
|
|
353
|
+
{ label: 'Banana', value: 'banana', disabled: true }
|
|
354
|
+
]
|
|
355
|
+
const wrapper = mount(Select, {
|
|
356
|
+
props: { options }
|
|
357
|
+
})
|
|
358
|
+
await wrapper.find('[role="combobox"]').trigger('click')
|
|
359
|
+
const optionElements = wrapper.findAll('[role="option"]')
|
|
360
|
+
await optionElements[1].trigger('mouseenter')
|
|
361
|
+
expect(optionElements[1].classes()).not.toContain('ui-select__option--highlighted')
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
describe('Disabled state', () => {
|
|
366
|
+
it('applies disabled attribute to trigger', () => {
|
|
367
|
+
const wrapper = mount(Select, {
|
|
368
|
+
props: { options: defaultOptions, disabled: true }
|
|
369
|
+
})
|
|
370
|
+
expect(wrapper.find('[role="combobox"]').attributes('disabled')).toBeDefined()
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('applies disabled class to container', () => {
|
|
374
|
+
const wrapper = mount(Select, {
|
|
375
|
+
props: { options: defaultOptions, disabled: true }
|
|
376
|
+
})
|
|
377
|
+
expect(wrapper.find('.ui-select--disabled').exists()).toBe(true)
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it('applies disabled class to disabled options', async () => {
|
|
381
|
+
const options = [
|
|
382
|
+
{ label: 'Apple', value: 'apple' },
|
|
383
|
+
{ label: 'Banana', value: 'banana', disabled: true }
|
|
384
|
+
]
|
|
385
|
+
const wrapper = mount(Select, {
|
|
386
|
+
props: { options }
|
|
387
|
+
})
|
|
388
|
+
await wrapper.find('[role="combobox"]').trigger('click')
|
|
389
|
+
const optionElements = wrapper.findAll('[role="option"]')
|
|
390
|
+
expect(optionElements[1].classes()).toContain('ui-select__option--disabled')
|
|
391
|
+
})
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
describe('Error state', () => {
|
|
395
|
+
it('applies error class when error provided', () => {
|
|
396
|
+
const wrapper = mount(Select, {
|
|
397
|
+
props: { options: defaultOptions, error: 'Required' }
|
|
398
|
+
})
|
|
399
|
+
expect(wrapper.find('.ui-select--error').exists()).toBe(true)
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('applies error class to trigger', () => {
|
|
403
|
+
const wrapper = mount(Select, {
|
|
404
|
+
props: { options: defaultOptions, error: 'Required' }
|
|
405
|
+
})
|
|
406
|
+
expect(wrapper.find('.ui-select__trigger--error').exists()).toBe(true)
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('error has alert role', () => {
|
|
410
|
+
const wrapper = mount(Select, {
|
|
411
|
+
props: { options: defaultOptions, error: 'Required' }
|
|
412
|
+
})
|
|
413
|
+
expect(wrapper.find('.ui-select__message--error').attributes('role')).toBe('alert')
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
describe('Sizes', () => {
|
|
418
|
+
const sizes = ['xs', 'sm', 'md', 'lg', 'xl'] as const
|
|
419
|
+
|
|
420
|
+
sizes.forEach(size => {
|
|
421
|
+
it(`applies ${size} size class`, () => {
|
|
422
|
+
const wrapper = mount(Select, {
|
|
423
|
+
props: { options: defaultOptions, size }
|
|
424
|
+
})
|
|
425
|
+
expect(wrapper.find(`.ui-select--${size}`).exists()).toBe(true)
|
|
426
|
+
expect(wrapper.find(`.ui-select__trigger--${size}`).exists()).toBe(true)
|
|
427
|
+
})
|
|
428
|
+
})
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
describe('Block mode', () => {
|
|
432
|
+
it('applies block class when block prop is true', () => {
|
|
433
|
+
const wrapper = mount(Select, {
|
|
434
|
+
props: { options: defaultOptions, block: true }
|
|
435
|
+
})
|
|
436
|
+
expect(wrapper.find('.ui-select--block').exists()).toBe(true)
|
|
437
|
+
})
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
describe('Hidden native select', () => {
|
|
441
|
+
it('renders hidden native select when name provided', () => {
|
|
442
|
+
const wrapper = mount(Select, {
|
|
443
|
+
props: { options: defaultOptions, name: 'fruit' }
|
|
444
|
+
})
|
|
445
|
+
expect(wrapper.find('select').exists()).toBe(true)
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it('does not render hidden select without name', () => {
|
|
449
|
+
const wrapper = mount(Select, {
|
|
450
|
+
props: { options: defaultOptions }
|
|
451
|
+
})
|
|
452
|
+
expect(wrapper.find('select').exists()).toBe(false)
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
it('hidden select has correct name and value', () => {
|
|
456
|
+
const wrapper = mount(Select, {
|
|
457
|
+
props: { options: defaultOptions, name: 'fruit', modelValue: 'banana' }
|
|
458
|
+
})
|
|
459
|
+
const select = wrapper.find('select')
|
|
460
|
+
expect(select.attributes('name')).toBe('fruit')
|
|
461
|
+
expect((select.element as HTMLSelectElement).value).toBe('banana')
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
it('hidden select is aria-hidden', () => {
|
|
465
|
+
const wrapper = mount(Select, {
|
|
466
|
+
props: { options: defaultOptions, name: 'fruit' }
|
|
467
|
+
})
|
|
468
|
+
expect(wrapper.find('select').attributes('aria-hidden')).toBe('true')
|
|
469
|
+
})
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
describe('Accessibility', () => {
|
|
473
|
+
it('trigger has aria-haspopup="listbox"', () => {
|
|
474
|
+
const wrapper = mount(Select, {
|
|
475
|
+
props: { options: defaultOptions }
|
|
476
|
+
})
|
|
477
|
+
expect(wrapper.find('[role="combobox"]').attributes('aria-haspopup')).toBe('listbox')
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('trigger has aria-expanded', async () => {
|
|
481
|
+
const wrapper = mount(Select, {
|
|
482
|
+
props: { options: defaultOptions }
|
|
483
|
+
})
|
|
484
|
+
const trigger = wrapper.find('[role="combobox"]')
|
|
485
|
+
expect(trigger.attributes('aria-expanded')).toBe('false')
|
|
486
|
+
await trigger.trigger('click')
|
|
487
|
+
expect(trigger.attributes('aria-expanded')).toBe('true')
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it('trigger has aria-controls linking to listbox', async () => {
|
|
491
|
+
const wrapper = mount(Select, {
|
|
492
|
+
props: { options: defaultOptions, id: 'test-select' }
|
|
493
|
+
})
|
|
494
|
+
await wrapper.find('[role="combobox"]').trigger('click')
|
|
495
|
+
expect(wrapper.find('[role="combobox"]').attributes('aria-controls')).toBe('test-select-listbox')
|
|
496
|
+
expect(wrapper.find('[role="listbox"]').attributes('id')).toBe('test-select-listbox')
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
it('options have role="option"', async () => {
|
|
500
|
+
const wrapper = mount(Select, {
|
|
501
|
+
props: { options: defaultOptions }
|
|
502
|
+
})
|
|
503
|
+
await wrapper.find('[role="combobox"]').trigger('click')
|
|
504
|
+
wrapper.findAll('[role="option"]').forEach(option => {
|
|
505
|
+
expect(option.attributes('role')).toBe('option')
|
|
506
|
+
})
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
it('selected option has aria-selected="true"', async () => {
|
|
510
|
+
const wrapper = mount(Select, {
|
|
511
|
+
props: { options: defaultOptions, modelValue: 'banana' }
|
|
512
|
+
})
|
|
513
|
+
await wrapper.find('[role="combobox"]').trigger('click')
|
|
514
|
+
const options = wrapper.findAll('[role="option"]')
|
|
515
|
+
expect(options[0].attributes('aria-selected')).toBe('false')
|
|
516
|
+
expect(options[1].attributes('aria-selected')).toBe('true')
|
|
517
|
+
expect(options[2].attributes('aria-selected')).toBe('false')
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it('disabled options have aria-disabled', async () => {
|
|
521
|
+
const options = [
|
|
522
|
+
{ label: 'Apple', value: 'apple' },
|
|
523
|
+
{ label: 'Banana', value: 'banana', disabled: true }
|
|
524
|
+
]
|
|
525
|
+
const wrapper = mount(Select, {
|
|
526
|
+
props: { options }
|
|
527
|
+
})
|
|
528
|
+
await wrapper.find('[role="combobox"]').trigger('click')
|
|
529
|
+
const optionElements = wrapper.findAll('[role="option"]')
|
|
530
|
+
expect(optionElements[1].attributes('aria-disabled')).toBe('true')
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
it('trigger has aria-activedescendant when open', async () => {
|
|
534
|
+
const wrapper = mount(Select, {
|
|
535
|
+
props: { options: defaultOptions, id: 'test-select' }
|
|
536
|
+
})
|
|
537
|
+
const trigger = wrapper.find('[role="combobox"]')
|
|
538
|
+
await trigger.trigger('click')
|
|
539
|
+
// First non-disabled option is highlighted
|
|
540
|
+
expect(trigger.attributes('aria-activedescendant')).toBe('test-select-option-0')
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
it('aria-invalid set when error exists', () => {
|
|
544
|
+
const wrapper = mount(Select, {
|
|
545
|
+
props: { options: defaultOptions, error: 'Required' }
|
|
546
|
+
})
|
|
547
|
+
expect(wrapper.find('[role="combobox"]').attributes('aria-invalid')).toBe('true')
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
it('aria-required set when required', () => {
|
|
551
|
+
const wrapper = mount(Select, {
|
|
552
|
+
props: { options: defaultOptions, required: true }
|
|
553
|
+
})
|
|
554
|
+
expect(wrapper.find('[role="combobox"]').attributes('aria-required')).toBe('true')
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
it('aria-describedby links to hint', () => {
|
|
558
|
+
const wrapper = mount(Select, {
|
|
559
|
+
props: { options: defaultOptions, hint: 'Choose wisely', id: 'test-select' }
|
|
560
|
+
})
|
|
561
|
+
expect(wrapper.find('[role="combobox"]').attributes('aria-describedby')).toBe('test-select-hint')
|
|
562
|
+
expect(wrapper.find('.ui-select__message--hint').attributes('id')).toBe('test-select-hint')
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
it('aria-describedby links to error when present', () => {
|
|
566
|
+
const wrapper = mount(Select, {
|
|
567
|
+
props: { options: defaultOptions, hint: 'Choose wisely', error: 'Required', id: 'test-select' }
|
|
568
|
+
})
|
|
569
|
+
// Error takes precedence over hint
|
|
570
|
+
expect(wrapper.find('[role="combobox"]').attributes('aria-describedby')).toBe('test-select-error')
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
it('label links to trigger via for attribute', () => {
|
|
574
|
+
const wrapper = mount(Select, {
|
|
575
|
+
props: { options: defaultOptions, label: 'Fruit', id: 'test-select' }
|
|
576
|
+
})
|
|
577
|
+
expect(wrapper.find('label').attributes('for')).toBe('test-select')
|
|
578
|
+
expect(wrapper.find('[role="combobox"]').attributes('id')).toBe('test-select')
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
it('listbox has aria-labelledby', async () => {
|
|
582
|
+
const wrapper = mount(Select, {
|
|
583
|
+
props: { options: defaultOptions, id: 'test-select' }
|
|
584
|
+
})
|
|
585
|
+
await wrapper.find('[role="combobox"]').trigger('click')
|
|
586
|
+
expect(wrapper.find('[role="listbox"]').attributes('aria-labelledby')).toBe('test-select')
|
|
587
|
+
})
|
|
588
|
+
})
|
|
589
|
+
})
|