@mozaic-ds/web-components 1.6.0 → 1.8.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/dist/Condition20.js +1 -1
- package/dist/attributes.js +1 -1
- package/dist/attributes.js.map +1 -1
- package/dist/branches.js +1 -1
- package/dist/branches.js.map +1 -1
- package/dist/bundle.d.ts +2 -0
- package/dist/bundle.d.ts.map +1 -1
- package/dist/bundle.js +2 -0
- package/dist/components/accordionlist/AccordionList.js +2 -2
- package/dist/components/accordionlistItem/AccordionListItem.js +2 -2
- package/dist/components/actionbottombar/ActionBottomBar.js +2 -2
- package/dist/components/actionlistbox/ActionListbox.js +3 -3
- package/dist/components/actionlistbox/ActionListbox.js.map +1 -1
- package/dist/components/actionlistbox/ActionListbox.svelte +1 -1
- package/dist/components/actionlistboxitem/ActionListboxItem.js +2 -2
- package/dist/components/avatar/Avatar.js +2 -2
- package/dist/components/breadcrumb/Breadcrumb.js +2 -2
- package/dist/components/builtinmenu/BuiltInMenu.js +2 -2
- package/dist/components/builtinmenuitem/BuiltInMenuItem.js +2 -2
- package/dist/components/button/Button.js +1 -1
- package/dist/components/callout/Callout.js +2 -2
- package/dist/components/carousel/Carousel.js +2 -2
- package/dist/components/checkbox/Checkbox.js +2 -2
- package/dist/components/checkboxgroup/CheckboxGroup.js +1 -1
- package/dist/components/checklistmenu/CheckListMenu.js +1 -1
- package/dist/components/circularprogressbar/CircularProgressbar.js +2 -2
- package/dist/components/combobox/Combobox.js +4 -0
- package/dist/components/combobox/Combobox.js.map +1 -0
- package/dist/components/combobox/Combobox.spec.js +186 -0
- package/dist/components/combobox/Combobox.stories.d.ts +17 -0
- package/dist/components/combobox/Combobox.stories.d.ts.map +1 -0
- package/dist/components/combobox/Combobox.stories.js +126 -0
- package/dist/components/combobox/Combobox.svelte +421 -0
- package/dist/components/combobox/Combobox.svelte.d.ts +104 -0
- package/dist/components/combobox/Combobox.svelte.d.ts.map +1 -0
- package/dist/components/combobox/README.md +38 -0
- package/dist/components/container/Container.js +2 -2
- package/dist/components/datepicker/Datepicker.js +3 -3
- package/dist/components/datepicker/Datepicker.js.map +1 -1
- package/dist/components/datepicker/Datepicker.svelte +2 -1
- package/dist/components/divider/Divider.js +2 -2
- package/dist/components/drawer/Drawer.js +4 -4
- package/dist/components/drawer/Drawer.svelte +2 -1
- package/dist/components/field/Field.js +2 -2
- package/dist/components/fileuploader/FileUploader.js +2 -2
- package/dist/components/fileuploader/FileUploader.js.map +1 -1
- package/dist/components/fileuploader/FileUploader.svelte +4 -4
- package/dist/components/fileuploader/FileUploader.svelte.d.ts +1 -0
- package/dist/components/fileuploader/FileUploader.svelte.d.ts.map +1 -1
- package/dist/components/fileuploaderitem/FileUploaderItem.js +3 -3
- package/dist/components/fileuploaderitem/FileUploaderItem.svelte +1 -4
- package/dist/components/flag/Flag.js +2 -2
- package/dist/components/iconbutton/IconButton.js +2 -2
- package/dist/components/kpiitem/KpiItem.js +2 -2
- package/dist/components/linearprogressbarbuffer/LinearProgressbarBuffer.js +2 -2
- package/dist/components/linearprogressbarpercentage/LinearProgressbarPercentage.js +2 -2
- package/dist/components/link/Link.js +1 -1
- package/dist/components/loader/Loader.js +2 -2
- package/dist/components/loadingoverlay/LoadingOverlay.js +2 -2
- package/dist/components/loadingoverlay/LoadingOverlay.svelte +1 -1
- package/dist/components/modal/Modal.js +4 -4
- package/dist/components/modal/Modal.js.map +1 -1
- package/dist/components/modal/Modal.spec.js +3 -1
- package/dist/components/modal/Modal.svelte +9 -3
- package/dist/components/modal/Modal.svelte.d.ts +4 -0
- package/dist/components/modal/Modal.svelte.d.ts.map +1 -1
- package/dist/components/modal/README.md +1 -0
- package/dist/components/navigationindicator/NavigationIndicator.js +2 -2
- package/dist/components/numberbadge/NumberBadge.js +2 -2
- package/dist/components/optionlistbox/OptionListbox.js +23 -0
- package/dist/components/optionlistbox/OptionListbox.js.map +1 -0
- package/dist/components/optionlistbox/OptionListbox.spec.js +350 -0
- package/dist/components/optionlistbox/OptionListbox.svelte +566 -0
- package/dist/components/optionlistbox/OptionListbox.svelte.d.ts +92 -0
- package/dist/components/optionlistbox/OptionListbox.svelte.d.ts.map +1 -0
- package/dist/components/optionlistbox/README.md +38 -0
- package/dist/components/overlay/Overlay.js +2 -2
- package/dist/components/overlay/Overlay.svelte +2 -2
- package/dist/components/pageheader/PageHeader.js +2 -2
- package/dist/components/pagination/Pagination.js +4 -4
- package/dist/components/passwordinput/PasswordInput.js +3 -3
- package/dist/components/passwordinput/PasswordInput.js.map +1 -1
- package/dist/components/passwordinput/PasswordInput.svelte +2 -1
- package/dist/components/phonenumber/PhoneNumber.js +4 -4
- package/dist/components/phonenumber/PhoneNumber.js.map +1 -1
- package/dist/components/phonenumber/PhoneNumber.svelte +3 -2
- package/dist/components/pincode/Pincode.js +2 -2
- package/dist/components/pincode/Pincode.js.map +1 -1
- package/dist/components/pincode/Pincode.svelte +3 -2
- package/dist/components/pincode/Pincode.svelte.d.ts +2 -1
- package/dist/components/pincode/Pincode.svelte.d.ts.map +1 -1
- package/dist/components/pincode/README.md +1 -1
- package/dist/components/popover/Popover.js +2 -2
- package/dist/components/quantityselector/QuantitySelector.js +3 -3
- package/dist/components/quantityselector/QuantitySelector.svelte +1 -1
- package/dist/components/radio/Radio.js +2 -2
- package/dist/components/radiogroup/RadioGroup.js +3 -3
- package/dist/components/radiogroup/RadioGroup.js.map +1 -1
- package/dist/components/radiogroup/RadioGroup.svelte +3 -2
- package/dist/components/radiogroup/RadioGroup.svelte.d.ts +1 -0
- package/dist/components/radiogroup/RadioGroup.svelte.d.ts.map +1 -1
- package/dist/components/segmentedcontrol/README.md +6 -3
- package/dist/components/segmentedcontrol/SegmentedControl.js +2 -2
- package/dist/components/segmentedcontrol/SegmentedControl.js.map +1 -1
- package/dist/components/segmentedcontrol/SegmentedControl.spec.js +60 -23
- package/dist/components/segmentedcontrol/SegmentedControl.stories.d.ts.map +1 -1
- package/dist/components/segmentedcontrol/SegmentedControl.stories.js +6 -1
- package/dist/components/segmentedcontrol/SegmentedControl.svelte +23 -10
- package/dist/components/segmentedcontrol/SegmentedControl.svelte.d.ts +10 -3
- package/dist/components/segmentedcontrol/SegmentedControl.svelte.d.ts.map +1 -1
- package/dist/components/select/Select.js +2 -2
- package/dist/components/sidebar/Sidebar.js +2 -2
- package/dist/components/sidebarexpandableitem/SidebarExpandableItem.js +2 -2
- package/dist/components/sidebarfooter/SidebarFooter.js +2 -2
- package/dist/components/sidebarfooter/_SidebarFooterMenu.js +2 -2
- package/dist/components/sidebarheader/SidebarHeader.js +2 -2
- package/dist/components/sidebarnavitem/SidebarNavItem.js +2 -2
- package/dist/components/sidebarshortcutitem/SidebarShortcutItem.js +2 -2
- package/dist/components/sidebarshortcuts/SidebarShortcuts.js +2 -2
- package/dist/components/starrating/StarRating.js +2 -2
- package/dist/components/statusbadge/StatusBadge.js +2 -2
- package/dist/components/statusdot/StatusDot.js +2 -2
- package/dist/components/statusmessage/StatusMessage.js +2 -2
- package/dist/components/statusmessage/StatusMessage.js.map +1 -1
- package/dist/components/statusmessage/StatusMessage.svelte +5 -0
- package/dist/components/statusnotification/StatusNotification.js +2 -2
- package/dist/components/statusnotification/StatusNotification.js.map +1 -1
- package/dist/components/statusnotification/StatusNotification.svelte +5 -0
- package/dist/components/stepperbottombar/StepperBottomBar.js +1 -1
- package/dist/components/steppercompact/StepperCompact.js +2 -2
- package/dist/components/stepperinline/README.md +6 -2
- package/dist/components/stepperinline/StepperInline.js +2 -2
- package/dist/components/stepperinline/StepperInline.js.map +1 -1
- package/dist/components/stepperinline/StepperInline.spec.js +57 -23
- package/dist/components/stepperinline/StepperInline.stories.d.ts.map +1 -1
- package/dist/components/stepperinline/StepperInline.stories.js +6 -11
- package/dist/components/stepperinline/StepperInline.svelte +23 -10
- package/dist/components/stepperinline/StepperInline.svelte.d.ts +10 -2
- package/dist/components/stepperinline/StepperInline.svelte.d.ts.map +1 -1
- package/dist/components/stepperstacked/README.md +15 -0
- package/dist/components/stepperstacked/StepperStacked.js +18 -0
- package/dist/components/stepperstacked/StepperStacked.js.map +1 -0
- package/dist/components/stepperstacked/StepperStacked.spec.js +138 -0
- package/dist/components/stepperstacked/StepperStacked.stories.d.ts +8 -0
- package/dist/components/stepperstacked/StepperStacked.stories.d.ts.map +1 -0
- package/dist/components/stepperstacked/StepperStacked.stories.js +33 -0
- package/dist/components/stepperstacked/StepperStacked.svelte +214 -0
- package/dist/components/stepperstacked/StepperStacked.svelte.d.ts +35 -0
- package/dist/components/stepperstacked/StepperStacked.svelte.d.ts.map +1 -0
- package/dist/components/tab/README.md +1 -0
- package/dist/components/tab/Tab.js +2 -2
- package/dist/components/tab/Tab.js.map +1 -1
- package/dist/components/tab/Tab.svelte +17 -1
- package/dist/components/tab/Tab.svelte.d.ts +4 -0
- package/dist/components/tab/Tab.svelte.d.ts.map +1 -1
- package/dist/components/tabs/Tabs.js +2 -2
- package/dist/components/tabs/Tabs.stories.d.ts +1 -0
- package/dist/components/tabs/Tabs.stories.d.ts.map +1 -1
- package/dist/components/tabs/Tabs.stories.js +10 -0
- package/dist/components/tag/README.md +1 -0
- package/dist/components/tag/Tag.js +2 -2
- package/dist/components/tag/Tag.js.map +1 -1
- package/dist/components/tag/Tag.svelte +7 -0
- package/dist/components/tag/Tag.svelte.d.ts +4 -0
- package/dist/components/tag/Tag.svelte.d.ts.map +1 -1
- package/dist/components/textarea/README.md +1 -1
- package/dist/components/textarea/Textarea.js +2 -2
- package/dist/components/textarea/Textarea.js.map +1 -1
- package/dist/components/textarea/Textarea.svelte +2 -1
- package/dist/components/textarea/Textarea.svelte.d.ts +1 -1
- package/dist/components/textarea/Textarea.svelte.d.ts.map +1 -1
- package/dist/components/textinput/README.md +1 -0
- package/dist/components/textinput/Textinput.js +4 -4
- package/dist/components/textinput/Textinput.js.map +1 -1
- package/dist/components/textinput/Textinput.stories.d.ts.map +1 -1
- package/dist/components/textinput/Textinput.stories.js +1 -0
- package/dist/components/textinput/Textinput.svelte +5 -1
- package/dist/components/textinput/Textinput.svelte.d.ts +2 -1
- package/dist/components/textinput/Textinput.svelte.d.ts.map +1 -1
- package/dist/components/tile/Tile.js +2 -2
- package/dist/components/tileclickable/TileClickable.js +1 -1
- package/dist/components/tileexpandable/TileExpandable.js +2 -2
- package/dist/components/tileexpandable/TileExpandable.js.map +1 -1
- package/dist/components/tileselectable/TileSelectable.js +2 -2
- package/dist/components/toaster/Toaster.js +3 -3
- package/dist/components/toaster/Toaster.js.map +1 -1
- package/dist/components/toaster/Toaster.svelte +6 -1
- package/dist/components/toggle/Toggle.js +2 -2
- package/dist/components/togglegroup/ToggleGroup.js +3 -3
- package/dist/components/togglegroup/ToggleGroup.js.map +1 -1
- package/dist/components/togglegroup/ToggleGroup.svelte +3 -2
- package/dist/components/togglegroup/ToggleGroup.svelte.d.ts +1 -0
- package/dist/components/togglegroup/ToggleGroup.svelte.d.ts.map +1 -1
- package/dist/components/tooltip/Tooltip.js +2 -2
- package/dist/custom-element.js +3 -3
- package/dist/custom-element.js.map +1 -1
- package/dist/documentation/DarkMode.mdx +115 -0
- package/dist/each.js +1 -1
- package/dist/each.js.map +1 -1
- package/dist/floating-item.svelte.js +1 -1
- package/dist/if.js +1 -1
- package/dist/if.js.map +1 -1
- package/dist/index-client.js +1 -1
- package/dist/input.js +1 -1
- package/dist/input.js.map +1 -1
- package/dist/main.d.ts +3 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +3 -1
- package/dist/svelte-component.js +1 -1
- package/dist/svelte-component.js.map +1 -1
- package/dist/svelte-element.js +1 -1
- package/dist/svelte-element.js.map +1 -1
- package/dist/this.js +1 -1
- package/dist/this.js.map +1 -1
- package/package.json +7 -5
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { render, fireEvent, screen, waitFor } from '@testing-library/svelte';
|
|
3
|
+
import { tick } from 'svelte';
|
|
4
|
+
import OptionListbox from './OptionListbox.svelte';
|
|
5
|
+
const baseOptions = [
|
|
6
|
+
{ label: 'Option A', value: 'a' },
|
|
7
|
+
{ label: 'Option B', value: 'b' },
|
|
8
|
+
{ label: 'Option C', value: 'c', disabled: true },
|
|
9
|
+
];
|
|
10
|
+
const sectionOptions = [
|
|
11
|
+
{ label: 'Group 1', value: 'group1', type: 'section' },
|
|
12
|
+
{ label: 'Option A', value: 'a' },
|
|
13
|
+
{ label: 'Option B', value: 'b' },
|
|
14
|
+
{ label: 'Group 2', value: 'group2', type: 'section' },
|
|
15
|
+
{ label: 'Option C', value: 'c' },
|
|
16
|
+
];
|
|
17
|
+
function renderListbox(props = {}) {
|
|
18
|
+
return render(OptionListbox, {
|
|
19
|
+
props: {
|
|
20
|
+
id: 'test-listbox',
|
|
21
|
+
options: baseOptions,
|
|
22
|
+
value: null,
|
|
23
|
+
activeindex: -1,
|
|
24
|
+
open: true,
|
|
25
|
+
...props,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
describe('Rendering', () => {
|
|
30
|
+
it('renders all options', () => {
|
|
31
|
+
renderListbox();
|
|
32
|
+
expect(screen.getByText('Option A')).toBeTruthy();
|
|
33
|
+
expect(screen.getByText('Option B')).toBeTruthy();
|
|
34
|
+
expect(screen.getByText('Option C')).toBeTruthy();
|
|
35
|
+
});
|
|
36
|
+
it('renders the search input when search=true', () => {
|
|
37
|
+
renderListbox({ search: true });
|
|
38
|
+
expect(screen.getByPlaceholderText('Find an option...')).toBeTruthy();
|
|
39
|
+
});
|
|
40
|
+
it('does not render the search input when search=false', () => {
|
|
41
|
+
renderListbox({ search: false });
|
|
42
|
+
expect(screen.queryByPlaceholderText('Find an option...')).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
it('renders Select all / Clear buttons when multiple + actions', () => {
|
|
45
|
+
renderListbox({ multiple: true, actions: true, value: [] });
|
|
46
|
+
expect(screen.getByText('Select all')).toBeTruthy();
|
|
47
|
+
expect(screen.getByText('Clear')).toBeTruthy();
|
|
48
|
+
});
|
|
49
|
+
it('does not render action buttons when actions=false', () => {
|
|
50
|
+
renderListbox({ multiple: true, actions: false, value: [] });
|
|
51
|
+
expect(screen.queryByText('Select all')).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
it('uses custom labels for search placeholder, select and clear', () => {
|
|
54
|
+
renderListbox({
|
|
55
|
+
multiple: true,
|
|
56
|
+
actions: true,
|
|
57
|
+
value: [],
|
|
58
|
+
search: true,
|
|
59
|
+
searchplaceholder: 'Rechercher...',
|
|
60
|
+
selectlabel: 'Tout sélectionner',
|
|
61
|
+
clearlabel: 'Effacer',
|
|
62
|
+
});
|
|
63
|
+
expect(screen.getByPlaceholderText('Rechercher...')).toBeTruthy();
|
|
64
|
+
expect(screen.getByText('Tout sélectionner')).toBeTruthy();
|
|
65
|
+
expect(screen.getByText('Effacer')).toBeTruthy();
|
|
66
|
+
});
|
|
67
|
+
it('renders section headers with role="presentation" when checkablesections=false', () => {
|
|
68
|
+
const { container } = renderListbox({ options: sectionOptions });
|
|
69
|
+
const sections = container.querySelectorAll('[role="presentation"]');
|
|
70
|
+
expect(sections.length).toBeGreaterThan(0);
|
|
71
|
+
});
|
|
72
|
+
it('renders section headers with role="option" when checkablesections=true and multiple=true', () => {
|
|
73
|
+
const { container } = renderListbox({
|
|
74
|
+
options: sectionOptions,
|
|
75
|
+
checkablesections: true,
|
|
76
|
+
multiple: true,
|
|
77
|
+
value: [],
|
|
78
|
+
});
|
|
79
|
+
const listItems = container.querySelectorAll('[role="option"]');
|
|
80
|
+
expect(listItems.length).toBe(sectionOptions.length);
|
|
81
|
+
});
|
|
82
|
+
it('marks disabled options with aria-disabled', () => {
|
|
83
|
+
const { container } = renderListbox();
|
|
84
|
+
const disabledOption = container.querySelector('[aria-disabled="true"]');
|
|
85
|
+
expect(disabledOption).toBeTruthy();
|
|
86
|
+
});
|
|
87
|
+
it('renders the listbox with role="listbox"', () => {
|
|
88
|
+
renderListbox();
|
|
89
|
+
expect(screen.getByRole('listbox')).toBeTruthy();
|
|
90
|
+
});
|
|
91
|
+
it('sets aria-multiselectable when multiple=true', () => {
|
|
92
|
+
renderListbox({ multiple: true, value: [] });
|
|
93
|
+
const listbox = screen.getByRole('listbox');
|
|
94
|
+
expect(listbox.getAttribute('aria-multiselectable')).toBe('true');
|
|
95
|
+
});
|
|
96
|
+
it('renders item content (secondary text) when provided', () => {
|
|
97
|
+
const options = [{ label: 'Option A', value: 'a', content: 'Extra info' }];
|
|
98
|
+
renderListbox({ options });
|
|
99
|
+
expect(screen.getByText('Extra info')).toBeTruthy();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('Single value', () => {
|
|
103
|
+
it('selects an option on click', async () => {
|
|
104
|
+
renderListbox({ value: null });
|
|
105
|
+
const optionA = screen.getByText('Option A').closest('li');
|
|
106
|
+
await fireEvent.click(optionA);
|
|
107
|
+
expect(optionA.classList.contains('mc-option-listbox__item--selected')).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
it('deselects an already selected option on click', async () => {
|
|
110
|
+
renderListbox({ value: 'a' });
|
|
111
|
+
const optionA = screen.getByText('Option A').closest('li');
|
|
112
|
+
expect(optionA.classList.contains('mc-option-listbox__item--selected')).toBe(true);
|
|
113
|
+
await fireEvent.click(optionA);
|
|
114
|
+
expect(optionA.classList.contains('mc-option-listbox__item--selected')).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
it('does not select disabled options', async () => {
|
|
117
|
+
renderListbox({ value: null });
|
|
118
|
+
const optionC = screen.getByText('Option C').closest('li');
|
|
119
|
+
await fireEvent.click(optionC);
|
|
120
|
+
expect(optionC.classList.contains('mc-option-listbox__item--selected')).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('Multiple values', () => {
|
|
124
|
+
it('selects multiple options by clicking', async () => {
|
|
125
|
+
renderListbox({ multiple: true, value: [] });
|
|
126
|
+
const optionA = screen.getByText('Option A').closest('li');
|
|
127
|
+
const optionB = screen.getByText('Option B').closest('li');
|
|
128
|
+
await fireEvent.click(optionA);
|
|
129
|
+
await fireEvent.click(optionB);
|
|
130
|
+
expect(optionA.classList.contains('mc-option-listbox__item--selected')).toBe(true);
|
|
131
|
+
expect(optionB.classList.contains('mc-option-listbox__item--selected')).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
it('deselects an option in multiple mode', async () => {
|
|
134
|
+
renderListbox({ multiple: true, value: ['a', 'b'] });
|
|
135
|
+
const optionA = screen.getByText('Option A').closest('li');
|
|
136
|
+
expect(optionA.classList.contains('mc-option-listbox__item--selected')).toBe(true);
|
|
137
|
+
await fireEvent.click(optionA);
|
|
138
|
+
expect(optionA.classList.contains('mc-option-listbox__item--selected')).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
it('selects all options when "Select all" is clicked', async () => {
|
|
141
|
+
renderListbox({ multiple: true, actions: true, value: [] });
|
|
142
|
+
await fireEvent.click(screen.getByText('Select all'));
|
|
143
|
+
const items = screen.getAllByRole('option').filter((el) => !el.getAttribute('aria-disabled'));
|
|
144
|
+
items.forEach((item) => {
|
|
145
|
+
expect(item.classList.contains('mc-option-listbox__item--selected')).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
it('clears all selections when "Clear" is clicked', async () => {
|
|
149
|
+
renderListbox({ multiple: true, actions: true, value: ['a', 'b'] });
|
|
150
|
+
await fireEvent.click(screen.getByText('Clear'));
|
|
151
|
+
const selected = screen
|
|
152
|
+
.getAllByRole('option')
|
|
153
|
+
.filter((el) => el.classList.contains('mc-option-listbox__item--selected'));
|
|
154
|
+
expect(selected.length).toBe(0);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe('Checkable sections', () => {
|
|
158
|
+
function renderWithSections(extra = {}) {
|
|
159
|
+
return renderListbox({
|
|
160
|
+
options: sectionOptions,
|
|
161
|
+
checkablesections: true,
|
|
162
|
+
multiple: true,
|
|
163
|
+
value: [],
|
|
164
|
+
...extra,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
it('selects all items in a section when the section header is clicked', async () => {
|
|
168
|
+
renderWithSections();
|
|
169
|
+
const sectionHeader = screen.getByText('Group 1').closest('li');
|
|
170
|
+
await fireEvent.click(sectionHeader);
|
|
171
|
+
const optionA = screen.getByText('Option A').closest('li');
|
|
172
|
+
const optionB = screen.getByText('Option B').closest('li');
|
|
173
|
+
expect(optionA.classList.contains('mc-option-listbox__item--selected')).toBe(true);
|
|
174
|
+
expect(optionB.classList.contains('mc-option-listbox__item--selected')).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
it('deselects all items in a section when the fully-selected header is clicked again', async () => {
|
|
177
|
+
renderWithSections({ value: ['a', 'b'] });
|
|
178
|
+
const sectionHeader = screen.getByText('Group 1').closest('li');
|
|
179
|
+
expect(sectionHeader.classList.contains('mc-option-listbox__item--selected')).toBe(true);
|
|
180
|
+
await fireEvent.click(sectionHeader);
|
|
181
|
+
const optionA = screen.getByText('Option A').closest('li');
|
|
182
|
+
const optionB = screen.getByText('Option B').closest('li');
|
|
183
|
+
expect(optionA.classList.contains('mc-option-listbox__item--selected')).toBe(false);
|
|
184
|
+
expect(optionB.classList.contains('mc-option-listbox__item--selected')).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
it('shows indeterminate state when only some items in a section are selected', async () => {
|
|
187
|
+
renderWithSections({ value: ['a'] });
|
|
188
|
+
const sectionHeader = screen.getByText('Group 1').closest('li');
|
|
189
|
+
expect(sectionHeader.classList.contains('mc-option-listbox__item--selected')).toBe(true);
|
|
190
|
+
const optionB = screen.getByText('Option B').closest('li');
|
|
191
|
+
expect(optionB.classList.contains('mc-option-listbox__item--selected')).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
it('is not indeterminate when no items in the section are selected', () => {
|
|
194
|
+
renderWithSections({ value: [] });
|
|
195
|
+
const sectionHeader = screen.getByText('Group 1').closest('li');
|
|
196
|
+
expect(sectionHeader.classList.contains('mc-option-listbox__item--selected')).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
it('is not indeterminate when all items in the section are selected', () => {
|
|
199
|
+
renderWithSections({ value: ['a', 'b'] });
|
|
200
|
+
const sectionHeader = screen.getByText('Group 1').closest('li');
|
|
201
|
+
expect(sectionHeader.classList.contains('mc-option-listbox__item--selected')).toBe(true);
|
|
202
|
+
const optionB = screen.getByText('Option B').closest('li');
|
|
203
|
+
expect(optionB.classList.contains('mc-option-listbox__item--selected')).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
it('clicking an indeterminate section header selects all its items', async () => {
|
|
206
|
+
renderWithSections({ value: ['a'] });
|
|
207
|
+
const sectionHeader = screen.getByText('Group 1').closest('li');
|
|
208
|
+
await fireEvent.click(sectionHeader);
|
|
209
|
+
const optionA = screen.getByText('Option A').closest('li');
|
|
210
|
+
const optionB = screen.getByText('Option B').closest('li');
|
|
211
|
+
expect(optionA.classList.contains('mc-option-listbox__item--selected')).toBe(true);
|
|
212
|
+
expect(optionB.classList.contains('mc-option-listbox__item--selected')).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
describe('Search', () => {
|
|
216
|
+
it('filters options as the user types', async () => {
|
|
217
|
+
renderListbox({ search: true });
|
|
218
|
+
const input = screen.getByPlaceholderText('Find an option...');
|
|
219
|
+
await fireEvent.input(input, { target: { value: 'Option A' } });
|
|
220
|
+
await waitFor(() => {
|
|
221
|
+
expect(screen.queryByText('Option B')).toBeNull();
|
|
222
|
+
expect(screen.getByText('Option A')).toBeTruthy();
|
|
223
|
+
}, { timeout: 500 });
|
|
224
|
+
});
|
|
225
|
+
it('shows all options when the search is cleared', async () => {
|
|
226
|
+
renderListbox({ search: true });
|
|
227
|
+
const input = screen.getByPlaceholderText('Find an option...');
|
|
228
|
+
await fireEvent.input(input, { target: { value: 'Option A' } });
|
|
229
|
+
await fireEvent.input(input, { target: { value: '' } });
|
|
230
|
+
await waitFor(() => {
|
|
231
|
+
expect(screen.getByText('Option A')).toBeTruthy();
|
|
232
|
+
expect(screen.getByText('Option B')).toBeTruthy();
|
|
233
|
+
}, { timeout: 500 });
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
async function fireOnInput(key, opts = {}) {
|
|
237
|
+
const input = screen.getByRole('combobox');
|
|
238
|
+
await fireEvent.keyDown(input, { key, ...opts });
|
|
239
|
+
await tick();
|
|
240
|
+
}
|
|
241
|
+
async function callKeydown(component, key, opts = {}) {
|
|
242
|
+
const event = new KeyboardEvent('keydown', { key, cancelable: true, bubbles: true, ...opts });
|
|
243
|
+
component.handleKeydown(event);
|
|
244
|
+
await tick();
|
|
245
|
+
}
|
|
246
|
+
describe('Keyboard navigation', () => {
|
|
247
|
+
it('moves active index forward with ArrowDown', async () => {
|
|
248
|
+
const { component } = renderListbox({ open: true, activeindex: 0 });
|
|
249
|
+
await callKeydown(component, 'ArrowDown');
|
|
250
|
+
const items = screen.getByRole('listbox').querySelectorAll('li');
|
|
251
|
+
expect(items[1].classList.contains('mc-option-listbox__item--active')).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
it('moves active index forward with ArrowDown', async () => {
|
|
254
|
+
renderListbox({ open: true, activeindex: 0, search: true });
|
|
255
|
+
await fireOnInput('ArrowDown');
|
|
256
|
+
const items = screen.getByRole('listbox').querySelectorAll('li');
|
|
257
|
+
expect(items[1].classList.contains('mc-option-listbox__item--active')).toBe(true);
|
|
258
|
+
});
|
|
259
|
+
it('moves active index backward with ArrowUp (via exported method)', async () => {
|
|
260
|
+
const { component } = renderListbox({ open: true, activeindex: 1 });
|
|
261
|
+
await callKeydown(component, 'ArrowUp');
|
|
262
|
+
const items = screen.getByRole('listbox').querySelectorAll('li');
|
|
263
|
+
expect(items[0].classList.contains('mc-option-listbox__item--active')).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
it('wraps to last selectable item when ArrowUp is pressed from index 0', async () => {
|
|
266
|
+
const { component } = renderListbox({ open: true, activeindex: 0 });
|
|
267
|
+
await callKeydown(component, 'ArrowUp');
|
|
268
|
+
const items = screen.getByRole('listbox').querySelectorAll('li');
|
|
269
|
+
expect(items[1].classList.contains('mc-option-listbox__item--active')).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
it('wraps to first selectable item when ArrowDown is pressed from last selectable', async () => {
|
|
272
|
+
const { component } = renderListbox({ open: true, activeindex: 1 });
|
|
273
|
+
await callKeydown(component, 'ArrowDown');
|
|
274
|
+
const items = screen.getByRole('listbox').querySelectorAll('li');
|
|
275
|
+
expect(items[0].classList.contains('mc-option-listbox__item--active')).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
it('selects the active item when Enter is pressed', async () => {
|
|
278
|
+
const { component } = renderListbox({ open: true, activeindex: 0 });
|
|
279
|
+
await callKeydown(component, 'Enter');
|
|
280
|
+
const items = screen.getByRole('listbox').querySelectorAll('li');
|
|
281
|
+
expect(items[0].classList.contains('mc-option-listbox__item--selected')).toBe(true);
|
|
282
|
+
});
|
|
283
|
+
it('does not select a disabled item when Enter is pressed on it', async () => {
|
|
284
|
+
const { component } = renderListbox({ open: true, activeindex: 2 });
|
|
285
|
+
await callKeydown(component, 'Enter');
|
|
286
|
+
const items = screen.getByRole('listbox').querySelectorAll('li');
|
|
287
|
+
expect(items[2].classList.contains('mc-option-listbox__item--selected')).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
it('calls onclose when Escape is pressed (via exported method)', async () => {
|
|
290
|
+
const onclose = vi.fn();
|
|
291
|
+
const { component } = renderListbox({ open: true, onclose });
|
|
292
|
+
await callKeydown(component, 'Escape');
|
|
293
|
+
expect(onclose).toHaveBeenCalledOnce();
|
|
294
|
+
});
|
|
295
|
+
it('calls onclose when Escape is pressed (via search input)', async () => {
|
|
296
|
+
const onclose = vi.fn();
|
|
297
|
+
renderListbox({ open: true, search: true, onclose });
|
|
298
|
+
await fireOnInput('Escape');
|
|
299
|
+
expect(onclose).toHaveBeenCalledOnce();
|
|
300
|
+
});
|
|
301
|
+
it('calls onopen when ArrowDown is pressed while the listbox is closed', async () => {
|
|
302
|
+
const onopen = vi.fn();
|
|
303
|
+
const { component } = renderListbox({ open: false, onopen, activeindex: -1 });
|
|
304
|
+
await callKeydown(component, 'ArrowDown');
|
|
305
|
+
expect(onopen).toHaveBeenCalledOnce();
|
|
306
|
+
});
|
|
307
|
+
it('calls onopen when ArrowUp is pressed while the listbox is closed', async () => {
|
|
308
|
+
const onopen = vi.fn();
|
|
309
|
+
const { component } = renderListbox({ open: false, onopen, activeindex: -1 });
|
|
310
|
+
await callKeydown(component, 'ArrowUp');
|
|
311
|
+
expect(onopen).toHaveBeenCalledOnce();
|
|
312
|
+
});
|
|
313
|
+
it('calls onopen when Enter is pressed while the listbox is closed', async () => {
|
|
314
|
+
const onopen = vi.fn();
|
|
315
|
+
const { component } = renderListbox({ open: false, onopen, activeindex: -1 });
|
|
316
|
+
await callKeydown(component, 'Enter');
|
|
317
|
+
expect(onopen).toHaveBeenCalledOnce();
|
|
318
|
+
});
|
|
319
|
+
it('skips disabled options during downward keyboard navigation', async () => {
|
|
320
|
+
const { component } = renderListbox({ open: true, activeindex: 1 });
|
|
321
|
+
await callKeydown(component, 'ArrowDown');
|
|
322
|
+
const items = screen.getByRole('listbox').querySelectorAll('li');
|
|
323
|
+
expect(items[0].classList.contains('mc-option-listbox__item--active')).toBe(true);
|
|
324
|
+
expect(items[2].classList.contains('mc-option-listbox__item--active')).toBe(false);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
describe('Readonly', () => {
|
|
328
|
+
it('applies readonly class to all items when readonly=true', () => {
|
|
329
|
+
const { container } = renderListbox({ readonly: true });
|
|
330
|
+
const items = container.querySelectorAll('.mc-option-listbox__item--readonly');
|
|
331
|
+
expect(items.length).toBe(baseOptions.length);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
describe('ARIA', () => {
|
|
335
|
+
it('sets aria-activedescendant on the search input when open', () => {
|
|
336
|
+
renderListbox({ search: true, activeindex: 0 });
|
|
337
|
+
const input = screen.getByRole('combobox');
|
|
338
|
+
expect(input.getAttribute('aria-activedescendant')).toBe('option-test-listbox-0');
|
|
339
|
+
});
|
|
340
|
+
it('sets aria-expanded on the search combobox', () => {
|
|
341
|
+
renderListbox({ search: true, open: true });
|
|
342
|
+
const input = screen.getByRole('combobox');
|
|
343
|
+
expect(input.getAttribute('aria-expanded')).toBe('true');
|
|
344
|
+
});
|
|
345
|
+
it('sets correct id on each option element', () => {
|
|
346
|
+
const { container } = renderListbox();
|
|
347
|
+
const firstOption = container.querySelector('#option-test-listbox-0');
|
|
348
|
+
expect(firstOption).toBeTruthy();
|
|
349
|
+
});
|
|
350
|
+
});
|