@fpkit/acss 6.0.0 → 6.2.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/libs/{chunk-DIJBIOFE.js → chunk-2ZJV6KQ3.js} +3 -3
- package/libs/{chunk-LXODKKA3.cjs → chunk-3D6WUYSL.cjs} +4 -4
- package/libs/{chunk-2JCDEC32.js → chunk-3ENBUQIB.js} +3 -3
- package/libs/{chunk-NCGVF2QS.cjs → chunk-3RVZZZWX.cjs} +4 -4
- package/libs/{chunk-M7JLT62Q.js → chunk-4F6SI5V5.js} +2 -2
- package/libs/chunk-4KJP3L35.js +7 -0
- package/libs/chunk-4KJP3L35.js.map +1 -0
- package/libs/chunk-66C2J4IX.cjs +13 -0
- package/libs/chunk-66C2J4IX.cjs.map +1 -0
- package/libs/{chunk-3XJC4XUG.js → chunk-6ADHES7B.js} +2 -2
- package/libs/{chunk-AOFQDQVS.cjs → chunk-6WMLG4O5.cjs} +3 -3
- package/libs/{chunk-Q7OAQLUT.js → chunk-AQAI6COH.js} +2 -2
- package/libs/{chunk-6BUJZ4DJ.cjs → chunk-BVPUT2PP.cjs} +3 -3
- package/libs/{chunk-F64GE6RG.cjs → chunk-GVVCXXKI.cjs} +4 -4
- package/libs/{chunk-75YQDONV.cjs → chunk-H4JRUNKU.cjs} +6 -6
- package/libs/{chunk-G3TFKMWB.js → chunk-H6A2CUWA.js} +5 -5
- package/libs/{chunk-2GJHKWEK.cjs → chunk-LQPWXSCK.cjs} +3 -3
- package/libs/{chunk-IBUTNPTQ.js → chunk-M5ES7OWP.js} +2 -2
- package/libs/{chunk-AWZLSWDO.js → chunk-MAG46S3P.js} +2 -2
- package/libs/{chunk-KAR3HDXK.js → chunk-MJJKNHVH.js} +2 -2
- package/libs/{chunk-5CJPTDK3.cjs → chunk-OZM455LO.cjs} +3 -3
- package/libs/{chunk-NPWHQVYB.cjs → chunk-QU5AQQ4S.cjs} +3 -3
- package/libs/{chunk-U5VA34SU.js → chunk-RQSMWB3J.js} +2 -2
- package/libs/{chunk-5QSNJQVH.cjs → chunk-S7NIA6PI.cjs} +3 -3
- package/libs/{chunk-MBWI67UT.js → chunk-SPESKPUA.js} +2 -2
- package/libs/{chunk-PMWL5XZ4.js → chunk-SQ44OCJ2.js} +3 -3
- package/libs/{chunk-EKJYOCLY.cjs → chunk-VISQ434C.cjs} +3 -3
- package/libs/{chunk-AFINOD2L.cjs → chunk-VN2CVD4H.cjs} +3 -3
- package/libs/{chunk-M5JARVJD.cjs → chunk-WTWGTWVI.cjs} +3 -3
- package/libs/{chunk-TF3GQKOY.js → chunk-X2RDXWH5.js} +2 -2
- package/libs/components/breadcrumbs/breadcrumb.cjs +6 -6
- package/libs/components/breadcrumbs/breadcrumb.d.cts +1 -1
- package/libs/components/breadcrumbs/breadcrumb.d.ts +1 -1
- package/libs/components/breadcrumbs/breadcrumb.js +3 -3
- package/libs/components/button.cjs +4 -4
- package/libs/components/button.d.cts +1 -1
- package/libs/components/button.d.ts +1 -1
- package/libs/components/button.js +2 -2
- package/libs/components/card.cjs +7 -7
- package/libs/components/card.d.cts +1 -1
- package/libs/components/card.d.ts +1 -1
- package/libs/components/card.js +2 -2
- package/libs/components/dialog/dialog.cjs +7 -7
- package/libs/components/dialog/dialog.js +5 -5
- package/libs/components/form/fields.cjs +4 -4
- package/libs/components/form/fields.d.cts +1 -1
- package/libs/components/form/fields.d.ts +1 -1
- package/libs/components/form/fields.js +2 -2
- package/libs/components/form/form.css +1 -1
- package/libs/components/form/form.css.map +1 -1
- package/libs/components/form/form.min.css +2 -2
- package/libs/components/form/select.css +1 -0
- package/libs/components/form/select.css.map +1 -0
- package/libs/components/form/select.min.css +3 -0
- package/libs/components/form/textarea.cjs +4 -4
- package/libs/components/form/textarea.js +2 -2
- package/libs/components/heading/heading.cjs +3 -3
- package/libs/components/heading/heading.d.cts +2 -2
- package/libs/components/heading/heading.d.ts +2 -2
- package/libs/components/heading/heading.js +2 -2
- package/libs/components/icons/icon.cjs +4 -4
- package/libs/components/icons/icon.d.cts +2 -2
- package/libs/components/icons/icon.d.ts +2 -2
- package/libs/components/icons/icon.js +2 -2
- package/libs/components/layout/landmarks.css +1 -1
- package/libs/components/layout/landmarks.css.map +1 -1
- package/libs/components/layout/landmarks.min.css +2 -2
- package/libs/components/link/link.cjs +6 -6
- package/libs/components/link/link.js +2 -2
- package/libs/components/list/list.cjs +5 -5
- package/libs/components/list/list.d.cts +2 -2
- package/libs/components/list/list.d.ts +2 -2
- package/libs/components/list/list.js +2 -2
- package/libs/components/modal.cjs +4 -4
- package/libs/components/modal.js +3 -3
- package/libs/components/nav/nav.cjs +7 -7
- package/libs/components/nav/nav.d.cts +2 -2
- package/libs/components/nav/nav.d.ts +2 -2
- package/libs/components/nav/nav.js +3 -3
- package/libs/components/text/text.cjs +5 -5
- package/libs/components/text/text.d.cts +1 -1
- package/libs/components/text/text.d.ts +1 -1
- package/libs/components/text/text.js +2 -2
- package/libs/{heading-81eef89a.d.ts → heading-064675b6.d.ts} +1 -1
- package/libs/hooks.cjs +4 -4
- package/libs/hooks.d.cts +1 -1
- package/libs/hooks.d.ts +1 -1
- package/libs/hooks.js +3 -3
- package/libs/{icons-df8e744f.d.ts → icons-48788561.d.ts} +1 -1
- package/libs/icons.cjs +3 -3
- package/libs/icons.d.cts +2 -2
- package/libs/icons.d.ts +2 -2
- package/libs/icons.js +2 -2
- package/libs/index.cjs +56 -55
- package/libs/index.cjs.map +1 -1
- package/libs/index.css +1 -1
- package/libs/index.css.map +1 -1
- package/libs/index.d.cts +43 -6
- package/libs/index.d.ts +43 -6
- package/libs/index.js +27 -27
- package/libs/index.js.map +1 -1
- package/libs/{list.types-d26de310.d.ts → list.types-bf2c44c1.d.ts} +1 -1
- package/libs/{ui-d01b50d4.d.ts → ui-993fc2e2.d.ts} +5 -2
- package/package.json +4 -7
- package/src/components/alert/alert.stories.tsx +1 -1
- package/src/components/alert/elements/dismiss-button.stories.tsx +1 -2
- package/src/components/badge/badge.stories.tsx +1 -1
- package/src/components/breadcrumbs/breadcrumb.stories.tsx +1 -2
- package/src/components/buttons/button.stories.tsx +1 -3
- package/src/components/cards/card.stories.tsx +1 -1
- package/src/components/cards/card.test.tsx +1 -1
- package/src/components/details/details.stories.tsx +1 -2
- package/src/components/dialog/dialog-modal.stories.tsx +1 -2
- package/src/components/dialog/dialog.stories.tsx +1 -1
- package/src/components/dialog/views/dialog-header.stories.tsx +1 -2
- package/src/components/form/CHECKBOX-STYLES.mdx +1 -1
- package/src/components/form/CHECKBOX.mdx +1 -1
- package/src/components/form/form.scss +4 -14
- package/src/components/form/form.stories.tsx +1 -1
- package/src/components/form/input.stories.tsx +1 -1
- package/src/components/form/select.scss +97 -0
- package/src/components/form/select.stories.tsx +225 -9
- package/src/components/form/select.test.tsx +541 -0
- package/src/components/form/select.tsx +133 -16
- package/src/components/heading/heading.stories.tsx +1 -2
- package/src/components/images/figure.stories.tsx +1 -2
- package/src/components/images/img.stories.tsx +1 -1
- package/src/components/layout/README.mdx +1117 -0
- package/src/components/layout/STYLES.mdx +159 -4
- package/src/components/layout/fieldset.stories.tsx +387 -0
- package/src/components/layout/landmarks.scss +115 -2
- package/src/components/layout/landmarks.stories.tsx +2 -6
- package/src/components/layout/landmarks.tsx +96 -27
- package/src/components/nav/nav.stories.tsx +1 -2
- package/src/components/text/text.stories.tsx +1 -1
- package/src/components/text-to-speech/TextToSpeech.stories.tsx +1 -1
- package/src/components/title/title.stories.tsx +1 -2
- package/src/components/ui.tsx +11 -4
- package/src/styles/form/form.css +75 -14
- package/src/styles/form/form.css.map +1 -1
- package/src/styles/form/select.css +75 -0
- package/src/styles/form/select.css.map +1 -0
- package/src/styles/index.css +157 -14
- package/src/styles/index.css.map +1 -1
- package/src/styles/layout/landmarks.css +83 -0
- package/src/styles/layout/landmarks.css.map +1 -1
- package/libs/chunk-DDSXKOUB.js +0 -7
- package/libs/chunk-DDSXKOUB.js.map +0 -1
- package/libs/chunk-EJ6KYBFE.cjs +0 -13
- package/libs/chunk-EJ6KYBFE.cjs.map +0 -1
- /package/libs/{chunk-DIJBIOFE.js.map → chunk-2ZJV6KQ3.js.map} +0 -0
- /package/libs/{chunk-LXODKKA3.cjs.map → chunk-3D6WUYSL.cjs.map} +0 -0
- /package/libs/{chunk-2JCDEC32.js.map → chunk-3ENBUQIB.js.map} +0 -0
- /package/libs/{chunk-NCGVF2QS.cjs.map → chunk-3RVZZZWX.cjs.map} +0 -0
- /package/libs/{chunk-M7JLT62Q.js.map → chunk-4F6SI5V5.js.map} +0 -0
- /package/libs/{chunk-3XJC4XUG.js.map → chunk-6ADHES7B.js.map} +0 -0
- /package/libs/{chunk-AOFQDQVS.cjs.map → chunk-6WMLG4O5.cjs.map} +0 -0
- /package/libs/{chunk-Q7OAQLUT.js.map → chunk-AQAI6COH.js.map} +0 -0
- /package/libs/{chunk-6BUJZ4DJ.cjs.map → chunk-BVPUT2PP.cjs.map} +0 -0
- /package/libs/{chunk-F64GE6RG.cjs.map → chunk-GVVCXXKI.cjs.map} +0 -0
- /package/libs/{chunk-75YQDONV.cjs.map → chunk-H4JRUNKU.cjs.map} +0 -0
- /package/libs/{chunk-G3TFKMWB.js.map → chunk-H6A2CUWA.js.map} +0 -0
- /package/libs/{chunk-2GJHKWEK.cjs.map → chunk-LQPWXSCK.cjs.map} +0 -0
- /package/libs/{chunk-IBUTNPTQ.js.map → chunk-M5ES7OWP.js.map} +0 -0
- /package/libs/{chunk-AWZLSWDO.js.map → chunk-MAG46S3P.js.map} +0 -0
- /package/libs/{chunk-KAR3HDXK.js.map → chunk-MJJKNHVH.js.map} +0 -0
- /package/libs/{chunk-5CJPTDK3.cjs.map → chunk-OZM455LO.cjs.map} +0 -0
- /package/libs/{chunk-NPWHQVYB.cjs.map → chunk-QU5AQQ4S.cjs.map} +0 -0
- /package/libs/{chunk-U5VA34SU.js.map → chunk-RQSMWB3J.js.map} +0 -0
- /package/libs/{chunk-5QSNJQVH.cjs.map → chunk-S7NIA6PI.cjs.map} +0 -0
- /package/libs/{chunk-MBWI67UT.js.map → chunk-SPESKPUA.js.map} +0 -0
- /package/libs/{chunk-PMWL5XZ4.js.map → chunk-SQ44OCJ2.js.map} +0 -0
- /package/libs/{chunk-EKJYOCLY.cjs.map → chunk-VISQ434C.cjs.map} +0 -0
- /package/libs/{chunk-AFINOD2L.cjs.map → chunk-VN2CVD4H.cjs.map} +0 -0
- /package/libs/{chunk-M5JARVJD.cjs.map → chunk-WTWGTWVI.cjs.map} +0 -0
- /package/libs/{chunk-TF3GQKOY.js.map → chunk-X2RDXWH5.js.map} +0 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import userEvent from '@testing-library/user-event'
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
5
|
+
import Select, { Option } from './select'
|
|
6
|
+
|
|
7
|
+
describe('Select', () => {
|
|
8
|
+
describe('rendering', () => {
|
|
9
|
+
it('renders select element', () => {
|
|
10
|
+
render(
|
|
11
|
+
<Select id="country" name="country">
|
|
12
|
+
<option value="us">United States</option>
|
|
13
|
+
</Select>
|
|
14
|
+
)
|
|
15
|
+
expect(screen.getByRole('combobox')).toBeInTheDocument()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('renders with provided id and name', () => {
|
|
19
|
+
render(
|
|
20
|
+
<Select id="country" name="country">
|
|
21
|
+
<option value="us">United States</option>
|
|
22
|
+
</Select>
|
|
23
|
+
)
|
|
24
|
+
const select = screen.getByRole('combobox')
|
|
25
|
+
expect(select).toHaveAttribute('id', 'country')
|
|
26
|
+
expect(select).toHaveAttribute('name', 'country')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('renders children options', () => {
|
|
30
|
+
render(
|
|
31
|
+
<Select id="country">
|
|
32
|
+
<option value="us">United States</option>
|
|
33
|
+
<option value="uk">United Kingdom</option>
|
|
34
|
+
<option value="ca">Canada</option>
|
|
35
|
+
</Select>
|
|
36
|
+
)
|
|
37
|
+
expect(screen.getByText('United States')).toBeInTheDocument()
|
|
38
|
+
expect(screen.getByText('United Kingdom')).toBeInTheDocument()
|
|
39
|
+
expect(screen.getByText('Canada')).toBeInTheDocument()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('renders empty option when no children provided', () => {
|
|
43
|
+
render(<Select id="empty" />)
|
|
44
|
+
const select = screen.getByRole('combobox')
|
|
45
|
+
expect(select.children).toHaveLength(1)
|
|
46
|
+
expect(select.children[0]).toHaveAttribute('value', '')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('accepts classes prop without error', () => {
|
|
50
|
+
// Test that the component accepts the classes prop
|
|
51
|
+
// The actual className rendering is handled by the UI component
|
|
52
|
+
expect(() => {
|
|
53
|
+
render(
|
|
54
|
+
<Select id="test" classes="custom-class">
|
|
55
|
+
<option value="1">Option 1</option>
|
|
56
|
+
</Select>
|
|
57
|
+
)
|
|
58
|
+
}).not.toThrow()
|
|
59
|
+
expect(screen.getByRole('combobox')).toBeInTheDocument()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('applies custom styles', () => {
|
|
63
|
+
render(
|
|
64
|
+
<Select id="test" styles={{ fontSize: '1.5rem' }}>
|
|
65
|
+
<option value="1">Option 1</option>
|
|
66
|
+
</Select>
|
|
67
|
+
)
|
|
68
|
+
expect(screen.getByRole('combobox')).toHaveStyle({ fontSize: '1.5rem' })
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('disabled state', () => {
|
|
73
|
+
it('applies disabled attribute when disabled', () => {
|
|
74
|
+
render(
|
|
75
|
+
<Select id="test" disabled>
|
|
76
|
+
<option value="1">Option 1</option>
|
|
77
|
+
</Select>
|
|
78
|
+
)
|
|
79
|
+
const select = screen.getByRole('combobox')
|
|
80
|
+
// useDisabledState hook manages disabled state via aria-disabled
|
|
81
|
+
// The actual disabled behavior is handled by the hook
|
|
82
|
+
expect(select).toHaveAttribute('aria-disabled', 'true')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('applies aria-disabled when disabled', () => {
|
|
86
|
+
render(
|
|
87
|
+
<Select id="test" disabled>
|
|
88
|
+
<option value="1">Option 1</option>
|
|
89
|
+
</Select>
|
|
90
|
+
)
|
|
91
|
+
expect(screen.getByRole('combobox')).toHaveAttribute('aria-disabled', 'true')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('does not call onSelectionChange when disabled', async () => {
|
|
95
|
+
const user = userEvent.setup()
|
|
96
|
+
const handleChange = vi.fn()
|
|
97
|
+
|
|
98
|
+
render(
|
|
99
|
+
<Select id="test" disabled onSelectionChange={handleChange}>
|
|
100
|
+
<option value="1">Option 1</option>
|
|
101
|
+
<option value="2">Option 2</option>
|
|
102
|
+
</Select>
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const select = screen.getByRole('combobox')
|
|
106
|
+
await user.selectOptions(select, '2')
|
|
107
|
+
|
|
108
|
+
expect(handleChange).not.toHaveBeenCalled()
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('required state', () => {
|
|
113
|
+
it('applies required attribute when required', () => {
|
|
114
|
+
render(
|
|
115
|
+
<Select id="test" required>
|
|
116
|
+
<option value="1">Option 1</option>
|
|
117
|
+
</Select>
|
|
118
|
+
)
|
|
119
|
+
expect(screen.getByRole('combobox')).toBeRequired()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('applies aria-required when required', () => {
|
|
123
|
+
render(
|
|
124
|
+
<Select id="test" required>
|
|
125
|
+
<option value="1">Option 1</option>
|
|
126
|
+
</Select>
|
|
127
|
+
)
|
|
128
|
+
expect(screen.getByRole('combobox')).toHaveAttribute('aria-required', 'true')
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('validation states', () => {
|
|
133
|
+
it('applies aria-invalid when validation state is invalid', () => {
|
|
134
|
+
render(
|
|
135
|
+
<Select id="test" validationState="invalid">
|
|
136
|
+
<option value="1">Option 1</option>
|
|
137
|
+
</Select>
|
|
138
|
+
)
|
|
139
|
+
expect(screen.getByRole('combobox')).toHaveAttribute('aria-invalid', 'true')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('does not apply aria-invalid when validation state is valid', () => {
|
|
143
|
+
render(
|
|
144
|
+
<Select id="test" validationState="valid">
|
|
145
|
+
<option value="1">Option 1</option>
|
|
146
|
+
</Select>
|
|
147
|
+
)
|
|
148
|
+
expect(screen.getByRole('combobox')).toHaveAttribute('aria-invalid', 'false')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('does not apply aria-invalid when validation state is none', () => {
|
|
152
|
+
render(
|
|
153
|
+
<Select id="test" validationState="none">
|
|
154
|
+
<option value="1">Option 1</option>
|
|
155
|
+
</Select>
|
|
156
|
+
)
|
|
157
|
+
expect(screen.getByRole('combobox')).toHaveAttribute('aria-invalid', 'false')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('associates error message with aria-describedby', () => {
|
|
161
|
+
render(
|
|
162
|
+
<Select id="test" validationState="invalid" errorMessage="This field is required">
|
|
163
|
+
<option value="1">Option 1</option>
|
|
164
|
+
</Select>
|
|
165
|
+
)
|
|
166
|
+
expect(screen.getByRole('combobox')).toHaveAttribute('aria-describedby', 'test-error')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('associates hint text with aria-describedby', () => {
|
|
170
|
+
render(
|
|
171
|
+
<Select id="test" hintText="Choose your country">
|
|
172
|
+
<option value="1">Option 1</option>
|
|
173
|
+
</Select>
|
|
174
|
+
)
|
|
175
|
+
expect(screen.getByRole('combobox')).toHaveAttribute('aria-describedby', 'test-hint')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('associates both error and hint with aria-describedby', () => {
|
|
179
|
+
render(
|
|
180
|
+
<Select
|
|
181
|
+
id="test"
|
|
182
|
+
validationState="invalid"
|
|
183
|
+
errorMessage="Invalid selection"
|
|
184
|
+
hintText="Choose carefully"
|
|
185
|
+
>
|
|
186
|
+
<option value="1">Option 1</option>
|
|
187
|
+
</Select>
|
|
188
|
+
)
|
|
189
|
+
expect(screen.getByRole('combobox')).toHaveAttribute('aria-describedby', 'test-error test-hint')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('does not set aria-describedby when no error or hint provided', () => {
|
|
193
|
+
render(
|
|
194
|
+
<Select id="test">
|
|
195
|
+
<option value="1">Option 1</option>
|
|
196
|
+
</Select>
|
|
197
|
+
)
|
|
198
|
+
expect(screen.getByRole('combobox')).not.toHaveAttribute('aria-describedby')
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('selection', () => {
|
|
203
|
+
it('sets default selected value', () => {
|
|
204
|
+
render(
|
|
205
|
+
<Select id="test" selected="2">
|
|
206
|
+
<option value="1">Option 1</option>
|
|
207
|
+
<option value="2">Option 2</option>
|
|
208
|
+
</Select>
|
|
209
|
+
)
|
|
210
|
+
expect(screen.getByRole('combobox')).toHaveValue('2')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('calls onSelectionChange when selection changes', async () => {
|
|
214
|
+
const user = userEvent.setup()
|
|
215
|
+
const handleChange = vi.fn()
|
|
216
|
+
|
|
217
|
+
render(
|
|
218
|
+
<Select id="test" onSelectionChange={handleChange}>
|
|
219
|
+
<option value="1">Option 1</option>
|
|
220
|
+
<option value="2">Option 2</option>
|
|
221
|
+
</Select>
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
const select = screen.getByRole('combobox')
|
|
225
|
+
await user.selectOptions(select, '2')
|
|
226
|
+
|
|
227
|
+
expect(handleChange).toHaveBeenCalledOnce()
|
|
228
|
+
expect(handleChange).toHaveBeenCalledWith(
|
|
229
|
+
expect.objectContaining({
|
|
230
|
+
target: expect.objectContaining({ value: '2' })
|
|
231
|
+
})
|
|
232
|
+
)
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
describe('keyboard interactions', () => {
|
|
237
|
+
it('calls onEnter when Enter key is pressed', async () => {
|
|
238
|
+
const user = userEvent.setup()
|
|
239
|
+
const handleEnter = vi.fn()
|
|
240
|
+
|
|
241
|
+
render(
|
|
242
|
+
<Select id="test" onEnter={handleEnter}>
|
|
243
|
+
<option value="1">Option 1</option>
|
|
244
|
+
</Select>
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
const select = screen.getByRole('combobox')
|
|
248
|
+
select.focus()
|
|
249
|
+
await user.keyboard('{Enter}')
|
|
250
|
+
|
|
251
|
+
expect(handleEnter).toHaveBeenCalledOnce()
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('calls both onEnter and onKeyDown when Enter is pressed', async () => {
|
|
255
|
+
const user = userEvent.setup()
|
|
256
|
+
const handleEnter = vi.fn()
|
|
257
|
+
const handleKeyDown = vi.fn()
|
|
258
|
+
|
|
259
|
+
render(
|
|
260
|
+
<Select id="test" onEnter={handleEnter} onKeyDown={handleKeyDown}>
|
|
261
|
+
<option value="1">Option 1</option>
|
|
262
|
+
</Select>
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
const select = screen.getByRole('combobox')
|
|
266
|
+
select.focus()
|
|
267
|
+
await user.keyboard('{Enter}')
|
|
268
|
+
|
|
269
|
+
expect(handleEnter).toHaveBeenCalledOnce()
|
|
270
|
+
expect(handleKeyDown).toHaveBeenCalledOnce()
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('calls only onKeyDown for non-Enter keys', async () => {
|
|
274
|
+
const user = userEvent.setup()
|
|
275
|
+
const handleEnter = vi.fn()
|
|
276
|
+
const handleKeyDown = vi.fn()
|
|
277
|
+
|
|
278
|
+
render(
|
|
279
|
+
<Select id="test" onEnter={handleEnter} onKeyDown={handleKeyDown}>
|
|
280
|
+
<option value="1">Option 1</option>
|
|
281
|
+
</Select>
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
const select = screen.getByRole('combobox')
|
|
285
|
+
select.focus()
|
|
286
|
+
await user.keyboard('{ArrowDown}')
|
|
287
|
+
|
|
288
|
+
expect(handleKeyDown).toHaveBeenCalledOnce()
|
|
289
|
+
expect(handleEnter).not.toHaveBeenCalled()
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('does not call onEnter when disabled', async () => {
|
|
293
|
+
const user = userEvent.setup()
|
|
294
|
+
const handleEnter = vi.fn()
|
|
295
|
+
|
|
296
|
+
render(
|
|
297
|
+
<Select id="test" disabled onEnter={handleEnter}>
|
|
298
|
+
<option value="1">Option 1</option>
|
|
299
|
+
</Select>
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
const select = screen.getByRole('combobox')
|
|
303
|
+
select.focus()
|
|
304
|
+
await user.keyboard('{Enter}')
|
|
305
|
+
|
|
306
|
+
expect(handleEnter).not.toHaveBeenCalled()
|
|
307
|
+
})
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
describe('event handlers', () => {
|
|
311
|
+
it('calls onBlur when select loses focus', async () => {
|
|
312
|
+
const user = userEvent.setup()
|
|
313
|
+
const handleBlur = vi.fn()
|
|
314
|
+
|
|
315
|
+
render(
|
|
316
|
+
<Select id="test" onBlur={handleBlur}>
|
|
317
|
+
<option value="1">Option 1</option>
|
|
318
|
+
</Select>
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
const select = screen.getByRole('combobox')
|
|
322
|
+
select.focus()
|
|
323
|
+
await user.tab()
|
|
324
|
+
|
|
325
|
+
expect(handleBlur).toHaveBeenCalledOnce()
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('calls onPointerDown on pointer interaction', async () => {
|
|
329
|
+
const user = userEvent.setup()
|
|
330
|
+
const handlePointerDown = vi.fn()
|
|
331
|
+
|
|
332
|
+
render(
|
|
333
|
+
<Select id="test" onPointerDown={handlePointerDown}>
|
|
334
|
+
<option value="1">Option 1</option>
|
|
335
|
+
</Select>
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
const select = screen.getByRole('combobox')
|
|
339
|
+
await user.pointer({ target: select, keys: '[MouseLeft>]' })
|
|
340
|
+
|
|
341
|
+
expect(handlePointerDown).toHaveBeenCalled()
|
|
342
|
+
})
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
describe('ref forwarding', () => {
|
|
346
|
+
it('forwards ref to select element', () => {
|
|
347
|
+
const ref = vi.fn()
|
|
348
|
+
|
|
349
|
+
render(
|
|
350
|
+
<Select id="test" ref={ref}>
|
|
351
|
+
<option value="1">Option 1</option>
|
|
352
|
+
</Select>
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
expect(ref).toHaveBeenCalledWith(expect.any(HTMLSelectElement))
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
describe('Option', () => {
|
|
361
|
+
describe('rendering', () => {
|
|
362
|
+
it('renders option element', () => {
|
|
363
|
+
render(
|
|
364
|
+
<Select id="test">
|
|
365
|
+
<Option value="us" label="United States" />
|
|
366
|
+
</Select>
|
|
367
|
+
)
|
|
368
|
+
expect(screen.getByText('United States')).toBeInTheDocument()
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('renders with value attribute', () => {
|
|
372
|
+
render(
|
|
373
|
+
<Select id="test">
|
|
374
|
+
<Option value="us" label="United States" />
|
|
375
|
+
</Select>
|
|
376
|
+
)
|
|
377
|
+
const option = screen.getByText('United States')
|
|
378
|
+
expect(option).toHaveAttribute('value', 'us')
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it('uses label as display text', () => {
|
|
382
|
+
render(
|
|
383
|
+
<Select id="test">
|
|
384
|
+
<Option value="us" label="United States" />
|
|
385
|
+
</Select>
|
|
386
|
+
)
|
|
387
|
+
expect(screen.getByText('United States')).toBeInTheDocument()
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
it('uses children over label when provided', () => {
|
|
391
|
+
render(
|
|
392
|
+
<Select id="test">
|
|
393
|
+
<Option value="us" label="Ignored">
|
|
394
|
+
United States
|
|
395
|
+
</Option>
|
|
396
|
+
</Select>
|
|
397
|
+
)
|
|
398
|
+
expect(screen.getByText('United States')).toBeInTheDocument()
|
|
399
|
+
expect(screen.queryByText('Ignored')).not.toBeInTheDocument()
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('uses value as display when label not provided', () => {
|
|
403
|
+
render(
|
|
404
|
+
<Select id="test">
|
|
405
|
+
<Option value="United States" />
|
|
406
|
+
</Select>
|
|
407
|
+
)
|
|
408
|
+
expect(screen.getByText('United States')).toBeInTheDocument()
|
|
409
|
+
})
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
describe('disabled state', () => {
|
|
413
|
+
it('applies disabled attribute when disabled', () => {
|
|
414
|
+
render(
|
|
415
|
+
<Select id="test">
|
|
416
|
+
<Option value="us" label="United States" disabled />
|
|
417
|
+
</Select>
|
|
418
|
+
)
|
|
419
|
+
expect(screen.getByText('United States')).toBeDisabled()
|
|
420
|
+
})
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
describe('styling variants', () => {
|
|
424
|
+
it('applies variant data attribute', () => {
|
|
425
|
+
render(
|
|
426
|
+
<Select id="test">
|
|
427
|
+
<Option value="us" label="United States" variant="primary" />
|
|
428
|
+
</Select>
|
|
429
|
+
)
|
|
430
|
+
expect(screen.getByText('United States')).toHaveAttribute('data-option', 'primary')
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
it('applies size data attribute', () => {
|
|
434
|
+
render(
|
|
435
|
+
<Select id="test">
|
|
436
|
+
<Option value="us" label="United States" size="lg" />
|
|
437
|
+
</Select>
|
|
438
|
+
)
|
|
439
|
+
expect(screen.getByText('United States')).toHaveAttribute('data-size', 'lg')
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('applies custom data attributes', () => {
|
|
443
|
+
render(
|
|
444
|
+
<Select id="test">
|
|
445
|
+
<Option
|
|
446
|
+
value="us"
|
|
447
|
+
label="United States"
|
|
448
|
+
dataAttributes={{ 'data-highlighted': true, 'data-category': 'premium' }}
|
|
449
|
+
/>
|
|
450
|
+
</Select>
|
|
451
|
+
)
|
|
452
|
+
const option = screen.getByText('United States')
|
|
453
|
+
expect(option).toHaveAttribute('data-highlighted', 'true')
|
|
454
|
+
expect(option).toHaveAttribute('data-category', 'premium')
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('accepts classes prop without error', () => {
|
|
458
|
+
// Test that the Option component accepts the classes prop
|
|
459
|
+
// The actual className rendering is handled by the UI component
|
|
460
|
+
expect(() => {
|
|
461
|
+
render(
|
|
462
|
+
<Select id="test">
|
|
463
|
+
<Option value="us" label="United States" classes="custom-option" />
|
|
464
|
+
</Select>
|
|
465
|
+
)
|
|
466
|
+
}).not.toThrow()
|
|
467
|
+
expect(screen.getByText('United States')).toBeInTheDocument()
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
it('applies custom styles', () => {
|
|
471
|
+
render(
|
|
472
|
+
<Select id="test">
|
|
473
|
+
<Option value="us" label="United States" styles={{ color: 'red' }} />
|
|
474
|
+
</Select>
|
|
475
|
+
)
|
|
476
|
+
// Browsers convert color values to rgb format
|
|
477
|
+
expect(screen.getByText('United States')).toHaveStyle({ color: 'rgb(255, 0, 0)' })
|
|
478
|
+
})
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
describe('legacy props support', () => {
|
|
482
|
+
it('supports selectValue prop for backwards compatibility', () => {
|
|
483
|
+
render(
|
|
484
|
+
<Select id="test">
|
|
485
|
+
<Option selectValue="us" selectLabel="United States" />
|
|
486
|
+
</Select>
|
|
487
|
+
)
|
|
488
|
+
const option = screen.getByText('United States')
|
|
489
|
+
expect(option).toHaveAttribute('value', 'us')
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it('prefers value over selectValue', () => {
|
|
493
|
+
render(
|
|
494
|
+
<Select id="test">
|
|
495
|
+
<Option value="uk" selectValue="us" label="United Kingdom" />
|
|
496
|
+
</Select>
|
|
497
|
+
)
|
|
498
|
+
const option = screen.getByText('United Kingdom')
|
|
499
|
+
expect(option).toHaveAttribute('value', 'uk')
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
it('prefers label over selectLabel', () => {
|
|
503
|
+
render(
|
|
504
|
+
<Select id="test">
|
|
505
|
+
<Option value="uk" label="United Kingdom" selectLabel="Ignored" />
|
|
506
|
+
</Select>
|
|
507
|
+
)
|
|
508
|
+
expect(screen.getByText('United Kingdom')).toBeInTheDocument()
|
|
509
|
+
expect(screen.queryByText('Ignored')).not.toBeInTheDocument()
|
|
510
|
+
})
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
describe('ref forwarding', () => {
|
|
514
|
+
it('forwards ref to option element', () => {
|
|
515
|
+
const ref = vi.fn()
|
|
516
|
+
|
|
517
|
+
render(
|
|
518
|
+
<Select id="test">
|
|
519
|
+
<Option ref={ref} value="us" label="United States" />
|
|
520
|
+
</Select>
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
expect(ref).toHaveBeenCalledWith(expect.any(HTMLOptionElement))
|
|
524
|
+
})
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
describe('compound component', () => {
|
|
528
|
+
it('is accessible via Select.Option', () => {
|
|
529
|
+
render(
|
|
530
|
+
<Select id="test">
|
|
531
|
+
<Select.Option value="us" label="United States" />
|
|
532
|
+
</Select>
|
|
533
|
+
)
|
|
534
|
+
expect(screen.getByText('United States')).toBeInTheDocument()
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
it('has correct display name', () => {
|
|
538
|
+
expect(Option.displayName).toBe('Select.Option')
|
|
539
|
+
})
|
|
540
|
+
})
|
|
541
|
+
})
|