shadcn-rails 0.2.0 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +66 -2
- data/README.md +21 -8
- data/__mocks__/@floating-ui/dom.js +67 -0
- data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +23 -2
- data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +4 -31
- data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +32 -41
- data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +29 -55
- data/app/assets/javascripts/shadcn/controllers/popover_controller.js +29 -54
- data/app/assets/javascripts/shadcn/controllers/select_controller.js +26 -8
- data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +28 -59
- data/app/assets/javascripts/shadcn/index.js +7 -1
- data/app/assets/javascripts/shadcn/utils/floating.js +179 -0
- data/app/assets/stylesheets/shadcn/base.css +32 -0
- data/app/components/shadcn/accordion_component.html.erb +8 -0
- data/app/components/shadcn/accordion_component.rb +6 -15
- data/app/components/shadcn/alert_component.html.erb +6 -0
- data/app/components/shadcn/alert_component.rb +0 -18
- data/app/components/shadcn/alert_dialog_component.html.erb +12 -0
- data/app/components/shadcn/alert_dialog_component.rb +7 -27
- data/app/components/shadcn/aspect_ratio_component.html.erb +7 -0
- data/app/components/shadcn/aspect_ratio_component.rb +4 -19
- data/app/components/shadcn/avatar_component.html.erb +20 -0
- data/app/components/shadcn/avatar_component.rb +8 -36
- data/app/components/shadcn/badge_component.html.erb +1 -0
- data/app/components/shadcn/badge_component.rb +0 -11
- data/app/components/shadcn/base_component.rb +15 -2
- data/app/components/shadcn/breadcrumb_component.html.erb +5 -0
- data/app/components/shadcn/breadcrumb_component.rb +6 -16
- data/app/components/shadcn/button_component.html.erb +18 -0
- data/app/components/shadcn/button_component.rb +1 -41
- data/app/components/shadcn/card_component.html.erb +8 -0
- data/app/components/shadcn/card_component.rb +2 -6
- data/app/components/shadcn/checkbox_component.html.erb +32 -0
- data/app/components/shadcn/checkbox_component.rb +4 -43
- data/app/components/shadcn/collapsible_component.html.erb +8 -0
- data/app/components/shadcn/collapsible_component.rb +6 -15
- data/app/components/shadcn/context_menu_component.html.erb +11 -0
- data/app/components/shadcn/context_menu_component.rb +6 -26
- data/app/components/shadcn/dialog_component.html.erb +14 -0
- data/app/components/shadcn/dialog_component.rb +8 -29
- data/app/components/shadcn/drawer_component.html.erb +12 -0
- data/app/components/shadcn/drawer_component.rb +7 -27
- data/app/components/shadcn/dropdown_menu_component.html.erb +14 -0
- data/app/components/shadcn/dropdown_menu_component.rb +9 -29
- data/app/components/shadcn/field_component.rb +7 -8
- data/app/components/shadcn/hover_card_component.html.erb +12 -0
- data/app/components/shadcn/hover_card_component.rb +7 -26
- data/app/components/shadcn/input_component.html.erb +18 -0
- data/app/components/shadcn/input_component.rb +2 -27
- data/app/components/shadcn/input_otp_component.rb +3 -3
- data/app/components/shadcn/kbd_component.html.erb +1 -0
- data/app/components/shadcn/kbd_component.rb +3 -10
- data/app/components/shadcn/label_component.html.erb +3 -0
- data/app/components/shadcn/label_component.rb +2 -18
- data/app/components/shadcn/menubar_component.html.erb +6 -0
- data/app/components/shadcn/menubar_component.rb +4 -15
- data/app/components/shadcn/native_select_component.html.erb +22 -0
- data/app/components/shadcn/native_select_component.rb +9 -39
- data/app/components/shadcn/navigation_menu_component.html.erb +6 -0
- data/app/components/shadcn/navigation_menu_component.rb +4 -15
- data/app/components/shadcn/pagination_component.html.erb +5 -0
- data/app/components/shadcn/pagination_component.rb +11 -15
- data/app/components/shadcn/popover_component.html.erb +15 -0
- data/app/components/shadcn/popover_component.rb +10 -30
- data/app/components/shadcn/progress_component.html.erb +13 -0
- data/app/components/shadcn/progress_component.rb +6 -26
- data/app/components/shadcn/radio_group_component.html.erb +8 -0
- data/app/components/shadcn/radio_group_component.rb +12 -26
- data/app/components/shadcn/scroll_area_component.html.erb +7 -0
- data/app/components/shadcn/scroll_area_component.rb +4 -16
- data/app/components/shadcn/select_component.html.erb +46 -0
- data/app/components/shadcn/select_component.rb +6 -80
- data/app/components/shadcn/separator_component.html.erb +5 -0
- data/app/components/shadcn/separator_component.rb +6 -14
- data/app/components/shadcn/sheet_component.html.erb +12 -0
- data/app/components/shadcn/sheet_component.rb +7 -27
- data/app/components/shadcn/sidebar_component.rb +2 -2
- data/app/components/shadcn/skeleton_component.html.erb +1 -0
- data/app/components/shadcn/skeleton_component.rb +4 -2
- data/app/components/shadcn/slider_component.html.erb +12 -0
- data/app/components/shadcn/slider_component.rb +2 -21
- data/app/components/shadcn/spinner_component.html.erb +18 -0
- data/app/components/shadcn/spinner_component.rb +2 -30
- data/app/components/shadcn/switch_component.html.erb +72 -0
- data/app/components/shadcn/switch_component.rb +4 -82
- data/app/components/shadcn/table_component.html.erb +9 -0
- data/app/components/shadcn/table_component.rb +2 -10
- data/app/components/shadcn/tabs_component.html.erb +8 -0
- data/app/components/shadcn/tabs_component.rb +4 -17
- data/app/components/shadcn/textarea_component.html.erb +13 -0
- data/app/components/shadcn/textarea_component.rb +6 -22
- data/app/components/shadcn/toast_component.html.erb +36 -0
- data/app/components/shadcn/toast_component.rb +6 -54
- data/app/components/shadcn/toggle_component.html.erb +12 -0
- data/app/components/shadcn/toggle_component.rb +6 -21
- data/app/components/shadcn/toggle_group_component.html.erb +14 -0
- data/app/components/shadcn/toggle_group_component.rb +6 -29
- data/app/components/shadcn/tooltip_component.html.erb +20 -0
- data/app/components/shadcn/tooltip_component.rb +13 -38
- data/lib/generators/shadcn/add/USAGE +24 -0
- data/lib/generators/shadcn/add/add_generator.rb +279 -0
- data/lib/generators/shadcn/install/USAGE +22 -0
- data/lib/generators/shadcn/install/install_generator.rb +8 -3
- data/lib/generators/shadcn/install/templates/initializer.rb.tt +7 -27
- data/lib/generators/shadcn/install/templates/shadcn.yml.tt +15 -31
- data/lib/shadcn/rails/version.rb +1 -1
- metadata +47 -45
- data/.dockerignore +0 -40
- data/CLAUDE.md +0 -612
- data/PROGRESS.md +0 -495
- data/Rakefile +0 -95
- data/__tests__/controllers/__snapshots__/calendar_controller.test.js.snap +0 -13
- data/__tests__/controllers/__snapshots__/popover_controller.test.js.snap +0 -46
- data/__tests__/controllers/__snapshots__/sheet_controller.test.js.snap +0 -111
- data/__tests__/controllers/__snapshots__/tabs_controller.test.js.snap +0 -27
- data/__tests__/controllers/accordion_controller.test.js +0 -904
- data/__tests__/controllers/calendar_controller.test.js +0 -1370
- data/__tests__/controllers/carousel_controller.test.js +0 -912
- data/__tests__/controllers/checkbox_controller.test.js +0 -454
- data/__tests__/controllers/collapsible_controller.test.js +0 -407
- data/__tests__/controllers/combobox_controller.test.js +0 -971
- data/__tests__/controllers/context_menu_controller.test.js +0 -905
- data/__tests__/controllers/date_picker_controller.test.js +0 -636
- data/__tests__/controllers/dialog_controller.test.js +0 -878
- data/__tests__/controllers/drawer_controller.test.js +0 -995
- data/__tests__/controllers/menubar_controller.test.js +0 -737
- data/__tests__/controllers/navigation_menu_controller.test.js +0 -599
- data/__tests__/controllers/popover_controller.test.js +0 -982
- data/__tests__/controllers/radio_group_controller.test.js +0 -640
- data/__tests__/controllers/resizable_controller.test.js +0 -680
- data/__tests__/controllers/select_controller.test.js +0 -678
- data/__tests__/controllers/sheet_controller.test.js +0 -986
- data/__tests__/controllers/slider_controller.test.js +0 -1036
- data/__tests__/controllers/switch_controller.test.js +0 -424
- data/__tests__/controllers/tabs_controller.test.js +0 -907
- data/__tests__/controllers/toggle_group_controller.test.js +0 -839
- data/__tests__/controllers/tooltip_controller.test.js +0 -808
- data/__tests__/helpers/stimulus-test-helper.js +0 -203
- data/babel.config.cjs +0 -5
- data/bin/bump +0 -321
- data/bin/console +0 -11
- data/bin/release +0 -205
- data/bin/setup +0 -8
- data/bin/test +0 -75
- data/jest.config.js +0 -19
- data/jest.setup.js +0 -8
- data/lib/generators/shadcn/component/component_generator.rb +0 -188
- data/lib/generators/shadcn/theme/theme_generator.rb +0 -128
- data/package-lock.json +0 -7438
- data/package.json +0 -71
- data/rollup.config.js +0 -29
|
@@ -1,640 +0,0 @@
|
|
|
1
|
-
import { Application } from "@hotwired/stimulus"
|
|
2
|
-
import RadioGroupController from "../../app/assets/javascripts/shadcn/controllers/radio_group_controller.js"
|
|
3
|
-
import { setupController, cleanupController, click, nextFrame, keydown } from '../helpers/stimulus-test-helper.js'
|
|
4
|
-
|
|
5
|
-
describe("RadioGroupController", () => {
|
|
6
|
-
let application
|
|
7
|
-
let element
|
|
8
|
-
let controller
|
|
9
|
-
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
cleanupController(application)
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
describe("basic rendering and initialization", () => {
|
|
15
|
-
const basicHTML = `
|
|
16
|
-
<div data-controller="shadcn--radio-group"
|
|
17
|
-
data-shadcn--radio-group-name-value="size"
|
|
18
|
-
data-shadcn--radio-group-value-value=""
|
|
19
|
-
role="radiogroup">
|
|
20
|
-
<button data-shadcn--radio-group-target="item"
|
|
21
|
-
data-value="small"
|
|
22
|
-
role="radio"
|
|
23
|
-
type="button"
|
|
24
|
-
data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">
|
|
25
|
-
Small
|
|
26
|
-
<span data-shadcn--radio-group-target="indicator" class="opacity-0"></span>
|
|
27
|
-
</button>
|
|
28
|
-
<button data-shadcn--radio-group-target="item"
|
|
29
|
-
data-value="medium"
|
|
30
|
-
role="radio"
|
|
31
|
-
type="button"
|
|
32
|
-
data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">
|
|
33
|
-
Medium
|
|
34
|
-
<span data-shadcn--radio-group-target="indicator" class="opacity-0"></span>
|
|
35
|
-
</button>
|
|
36
|
-
<button data-shadcn--radio-group-target="item"
|
|
37
|
-
data-value="large"
|
|
38
|
-
role="radio"
|
|
39
|
-
type="button"
|
|
40
|
-
data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">
|
|
41
|
-
Large
|
|
42
|
-
<span data-shadcn--radio-group-target="indicator" class="opacity-0"></span>
|
|
43
|
-
</button>
|
|
44
|
-
</div>
|
|
45
|
-
`
|
|
46
|
-
|
|
47
|
-
beforeEach(async () => {
|
|
48
|
-
const setup = await setupController(RadioGroupController, basicHTML, 'shadcn--radio-group')
|
|
49
|
-
application = setup.application
|
|
50
|
-
element = setup.element
|
|
51
|
-
controller = setup.controller
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test("initializes with empty value", () => {
|
|
55
|
-
expect(controller.valueValue).toBe("")
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
test("initializes with name value", () => {
|
|
59
|
-
expect(controller.nameValue).toBe("size")
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
test("initializes all items with aria-checked false", () => {
|
|
63
|
-
controller.itemTargets.forEach(item => {
|
|
64
|
-
expect(item.getAttribute("aria-checked")).toBe("false")
|
|
65
|
-
})
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
test("initializes all items with data-state unchecked", () => {
|
|
69
|
-
controller.itemTargets.forEach(item => {
|
|
70
|
-
expect(item.dataset.state).toBe("unchecked")
|
|
71
|
-
})
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
test("first enabled item is focusable when no value selected", () => {
|
|
75
|
-
const firstItem = controller.itemTargets[0]
|
|
76
|
-
expect(firstItem.getAttribute("tabindex")).toBe("0")
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
test("other items are not focusable initially", () => {
|
|
80
|
-
const secondItem = controller.itemTargets[1]
|
|
81
|
-
const thirdItem = controller.itemTargets[2]
|
|
82
|
-
expect(secondItem.getAttribute("tabindex")).toBe("-1")
|
|
83
|
-
expect(thirdItem.getAttribute("tabindex")).toBe("-1")
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
describe("selection", () => {
|
|
88
|
-
const selectionHTML = `
|
|
89
|
-
<div data-controller="shadcn--radio-group"
|
|
90
|
-
data-shadcn--radio-group-name-value="option"
|
|
91
|
-
data-shadcn--radio-group-value-value="">
|
|
92
|
-
<button data-shadcn--radio-group-target="item"
|
|
93
|
-
data-value="one"
|
|
94
|
-
role="radio"
|
|
95
|
-
data-action="click->shadcn--radio-group#select">
|
|
96
|
-
One
|
|
97
|
-
<span data-shadcn--radio-group-target="indicator" class="opacity-0"></span>
|
|
98
|
-
</button>
|
|
99
|
-
<button data-shadcn--radio-group-target="item"
|
|
100
|
-
data-value="two"
|
|
101
|
-
role="radio"
|
|
102
|
-
data-action="click->shadcn--radio-group#select">
|
|
103
|
-
Two
|
|
104
|
-
<span data-shadcn--radio-group-target="indicator" class="opacity-0"></span>
|
|
105
|
-
</button>
|
|
106
|
-
</div>
|
|
107
|
-
`
|
|
108
|
-
|
|
109
|
-
beforeEach(async () => {
|
|
110
|
-
const setup = await setupController(RadioGroupController, selectionHTML, 'shadcn--radio-group')
|
|
111
|
-
application = setup.application
|
|
112
|
-
element = setup.element
|
|
113
|
-
controller = setup.controller
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
test("selects item when clicked", async () => {
|
|
117
|
-
const firstItem = controller.itemTargets[0]
|
|
118
|
-
click(firstItem)
|
|
119
|
-
await nextFrame()
|
|
120
|
-
|
|
121
|
-
expect(controller.valueValue).toBe("one")
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
test("updates aria-checked on selected item", async () => {
|
|
125
|
-
const firstItem = controller.itemTargets[0]
|
|
126
|
-
click(firstItem)
|
|
127
|
-
await nextFrame()
|
|
128
|
-
|
|
129
|
-
expect(firstItem.getAttribute("aria-checked")).toBe("true")
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
test("updates data-state on selected item", async () => {
|
|
133
|
-
const firstItem = controller.itemTargets[0]
|
|
134
|
-
click(firstItem)
|
|
135
|
-
await nextFrame()
|
|
136
|
-
|
|
137
|
-
expect(firstItem.dataset.state).toBe("checked")
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
test("makes selected item focusable", async () => {
|
|
141
|
-
const secondItem = controller.itemTargets[1]
|
|
142
|
-
click(secondItem)
|
|
143
|
-
await nextFrame()
|
|
144
|
-
|
|
145
|
-
expect(secondItem.getAttribute("tabindex")).toBe("0")
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
test("makes non-selected items non-focusable", async () => {
|
|
149
|
-
const firstItem = controller.itemTargets[0]
|
|
150
|
-
const secondItem = controller.itemTargets[1]
|
|
151
|
-
click(secondItem)
|
|
152
|
-
await nextFrame()
|
|
153
|
-
|
|
154
|
-
expect(firstItem.getAttribute("tabindex")).toBe("-1")
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
test("deselects previous selection when new item selected", async () => {
|
|
158
|
-
const firstItem = controller.itemTargets[0]
|
|
159
|
-
const secondItem = controller.itemTargets[1]
|
|
160
|
-
|
|
161
|
-
click(firstItem)
|
|
162
|
-
await nextFrame()
|
|
163
|
-
expect(firstItem.getAttribute("aria-checked")).toBe("true")
|
|
164
|
-
|
|
165
|
-
click(secondItem)
|
|
166
|
-
await nextFrame()
|
|
167
|
-
expect(firstItem.getAttribute("aria-checked")).toBe("false")
|
|
168
|
-
expect(secondItem.getAttribute("aria-checked")).toBe("true")
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
test("dispatches change event on selection", async () => {
|
|
172
|
-
let eventDetail = null
|
|
173
|
-
element.addEventListener("shadcn--radio-group:change", (e) => {
|
|
174
|
-
eventDetail = e.detail
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
const firstItem = controller.itemTargets[0]
|
|
178
|
-
click(firstItem)
|
|
179
|
-
await nextFrame()
|
|
180
|
-
|
|
181
|
-
expect(eventDetail).not.toBeNull()
|
|
182
|
-
expect(eventDetail.value).toBe("one")
|
|
183
|
-
expect(eventDetail.name).toBe("option")
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
test("dispatches native input event on selection", async () => {
|
|
187
|
-
let inputEventFired = false
|
|
188
|
-
element.addEventListener("input", () => {
|
|
189
|
-
inputEventFired = true
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
const firstItem = controller.itemTargets[0]
|
|
193
|
-
click(firstItem)
|
|
194
|
-
await nextFrame()
|
|
195
|
-
|
|
196
|
-
expect(inputEventFired).toBe(true)
|
|
197
|
-
})
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
describe("indicator visibility", () => {
|
|
201
|
-
const indicatorHTML = `
|
|
202
|
-
<div data-controller="shadcn--radio-group"
|
|
203
|
-
data-shadcn--radio-group-value-value="">
|
|
204
|
-
<button data-shadcn--radio-group-target="item"
|
|
205
|
-
data-value="a"
|
|
206
|
-
data-action="click->shadcn--radio-group#select">
|
|
207
|
-
A
|
|
208
|
-
<span data-shadcn--radio-group-target="indicator" class="opacity-0"></span>
|
|
209
|
-
</button>
|
|
210
|
-
<button data-shadcn--radio-group-target="item"
|
|
211
|
-
data-value="b"
|
|
212
|
-
data-action="click->shadcn--radio-group#select">
|
|
213
|
-
B
|
|
214
|
-
<span data-shadcn--radio-group-target="indicator" class="opacity-0"></span>
|
|
215
|
-
</button>
|
|
216
|
-
</div>
|
|
217
|
-
`
|
|
218
|
-
|
|
219
|
-
beforeEach(async () => {
|
|
220
|
-
const setup = await setupController(RadioGroupController, indicatorHTML, 'shadcn--radio-group')
|
|
221
|
-
application = setup.application
|
|
222
|
-
element = setup.element
|
|
223
|
-
controller = setup.controller
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
test("shows indicator for selected item", async () => {
|
|
227
|
-
const firstItem = controller.itemTargets[0]
|
|
228
|
-
click(firstItem)
|
|
229
|
-
await nextFrame()
|
|
230
|
-
|
|
231
|
-
const indicator = firstItem.querySelector('[data-shadcn--radio-group-target="indicator"]')
|
|
232
|
-
expect(indicator.classList.contains("opacity-0")).toBe(false)
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
test("hides indicator for non-selected items", async () => {
|
|
236
|
-
const firstItem = controller.itemTargets[0]
|
|
237
|
-
const secondItem = controller.itemTargets[1]
|
|
238
|
-
click(firstItem)
|
|
239
|
-
await nextFrame()
|
|
240
|
-
|
|
241
|
-
const indicator = secondItem.querySelector('[data-shadcn--radio-group-target="indicator"]')
|
|
242
|
-
expect(indicator.classList.contains("opacity-0")).toBe(true)
|
|
243
|
-
})
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
describe("keyboard navigation", () => {
|
|
247
|
-
const keyboardHTML = `
|
|
248
|
-
<div data-controller="shadcn--radio-group"
|
|
249
|
-
data-shadcn--radio-group-value-value="">
|
|
250
|
-
<button data-shadcn--radio-group-target="item"
|
|
251
|
-
data-value="first"
|
|
252
|
-
data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">
|
|
253
|
-
First
|
|
254
|
-
</button>
|
|
255
|
-
<button data-shadcn--radio-group-target="item"
|
|
256
|
-
data-value="second"
|
|
257
|
-
data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">
|
|
258
|
-
Second
|
|
259
|
-
</button>
|
|
260
|
-
<button data-shadcn--radio-group-target="item"
|
|
261
|
-
data-value="third"
|
|
262
|
-
data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">
|
|
263
|
-
Third
|
|
264
|
-
</button>
|
|
265
|
-
</div>
|
|
266
|
-
`
|
|
267
|
-
|
|
268
|
-
beforeEach(async () => {
|
|
269
|
-
const setup = await setupController(RadioGroupController, keyboardHTML, 'shadcn--radio-group')
|
|
270
|
-
application = setup.application
|
|
271
|
-
element = setup.element
|
|
272
|
-
controller = setup.controller
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
test("navigates forward with ArrowDown", async () => {
|
|
276
|
-
const firstItem = controller.itemTargets[0]
|
|
277
|
-
const secondItem = controller.itemTargets[1]
|
|
278
|
-
const focusSpy = jest.spyOn(secondItem, 'focus')
|
|
279
|
-
|
|
280
|
-
controller.handleKeydown({
|
|
281
|
-
key: "ArrowDown",
|
|
282
|
-
preventDefault: jest.fn(),
|
|
283
|
-
currentTarget: firstItem
|
|
284
|
-
})
|
|
285
|
-
await nextFrame()
|
|
286
|
-
|
|
287
|
-
expect(focusSpy).toHaveBeenCalled()
|
|
288
|
-
expect(controller.valueValue).toBe("second")
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
test("navigates forward with ArrowRight", async () => {
|
|
292
|
-
const firstItem = controller.itemTargets[0]
|
|
293
|
-
const secondItem = controller.itemTargets[1]
|
|
294
|
-
const focusSpy = jest.spyOn(secondItem, 'focus')
|
|
295
|
-
|
|
296
|
-
controller.handleKeydown({
|
|
297
|
-
key: "ArrowRight",
|
|
298
|
-
preventDefault: jest.fn(),
|
|
299
|
-
currentTarget: firstItem
|
|
300
|
-
})
|
|
301
|
-
await nextFrame()
|
|
302
|
-
|
|
303
|
-
expect(focusSpy).toHaveBeenCalled()
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
test("navigates backward with ArrowUp", async () => {
|
|
307
|
-
const firstItem = controller.itemTargets[0]
|
|
308
|
-
const secondItem = controller.itemTargets[1]
|
|
309
|
-
const focusSpy = jest.spyOn(firstItem, 'focus')
|
|
310
|
-
|
|
311
|
-
controller.handleKeydown({
|
|
312
|
-
key: "ArrowUp",
|
|
313
|
-
preventDefault: jest.fn(),
|
|
314
|
-
currentTarget: secondItem
|
|
315
|
-
})
|
|
316
|
-
await nextFrame()
|
|
317
|
-
|
|
318
|
-
expect(focusSpy).toHaveBeenCalled()
|
|
319
|
-
expect(controller.valueValue).toBe("first")
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
test("navigates backward with ArrowLeft", async () => {
|
|
323
|
-
const firstItem = controller.itemTargets[0]
|
|
324
|
-
const secondItem = controller.itemTargets[1]
|
|
325
|
-
const focusSpy = jest.spyOn(firstItem, 'focus')
|
|
326
|
-
|
|
327
|
-
controller.handleKeydown({
|
|
328
|
-
key: "ArrowLeft",
|
|
329
|
-
preventDefault: jest.fn(),
|
|
330
|
-
currentTarget: secondItem
|
|
331
|
-
})
|
|
332
|
-
await nextFrame()
|
|
333
|
-
|
|
334
|
-
expect(focusSpy).toHaveBeenCalled()
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
test("wraps from last to first with ArrowDown", async () => {
|
|
338
|
-
const firstItem = controller.itemTargets[0]
|
|
339
|
-
const thirdItem = controller.itemTargets[2]
|
|
340
|
-
const focusSpy = jest.spyOn(firstItem, 'focus')
|
|
341
|
-
|
|
342
|
-
controller.handleKeydown({
|
|
343
|
-
key: "ArrowDown",
|
|
344
|
-
preventDefault: jest.fn(),
|
|
345
|
-
currentTarget: thirdItem
|
|
346
|
-
})
|
|
347
|
-
await nextFrame()
|
|
348
|
-
|
|
349
|
-
expect(focusSpy).toHaveBeenCalled()
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
test("wraps from first to last with ArrowUp", async () => {
|
|
353
|
-
const firstItem = controller.itemTargets[0]
|
|
354
|
-
const thirdItem = controller.itemTargets[2]
|
|
355
|
-
const focusSpy = jest.spyOn(thirdItem, 'focus')
|
|
356
|
-
|
|
357
|
-
controller.handleKeydown({
|
|
358
|
-
key: "ArrowUp",
|
|
359
|
-
preventDefault: jest.fn(),
|
|
360
|
-
currentTarget: firstItem
|
|
361
|
-
})
|
|
362
|
-
await nextFrame()
|
|
363
|
-
|
|
364
|
-
expect(focusSpy).toHaveBeenCalled()
|
|
365
|
-
})
|
|
366
|
-
|
|
367
|
-
test("selects current item with Space", async () => {
|
|
368
|
-
const secondItem = controller.itemTargets[1]
|
|
369
|
-
|
|
370
|
-
controller.handleKeydown({
|
|
371
|
-
key: " ",
|
|
372
|
-
preventDefault: jest.fn(),
|
|
373
|
-
currentTarget: secondItem
|
|
374
|
-
})
|
|
375
|
-
await nextFrame()
|
|
376
|
-
|
|
377
|
-
expect(controller.valueValue).toBe("second")
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
test("selects current item with Enter", async () => {
|
|
381
|
-
const secondItem = controller.itemTargets[1]
|
|
382
|
-
|
|
383
|
-
controller.handleKeydown({
|
|
384
|
-
key: "Enter",
|
|
385
|
-
preventDefault: jest.fn(),
|
|
386
|
-
currentTarget: secondItem
|
|
387
|
-
})
|
|
388
|
-
await nextFrame()
|
|
389
|
-
|
|
390
|
-
expect(controller.valueValue).toBe("second")
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
test("dispatches change event on keyboard navigation", async () => {
|
|
394
|
-
let eventDetail = null
|
|
395
|
-
element.addEventListener("shadcn--radio-group:change", (e) => {
|
|
396
|
-
eventDetail = e.detail
|
|
397
|
-
})
|
|
398
|
-
|
|
399
|
-
const firstItem = controller.itemTargets[0]
|
|
400
|
-
controller.handleKeydown({
|
|
401
|
-
key: "ArrowDown",
|
|
402
|
-
preventDefault: jest.fn(),
|
|
403
|
-
currentTarget: firstItem
|
|
404
|
-
})
|
|
405
|
-
await nextFrame()
|
|
406
|
-
|
|
407
|
-
expect(eventDetail).not.toBeNull()
|
|
408
|
-
expect(eventDetail.value).toBe("second")
|
|
409
|
-
})
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
describe("disabled items", () => {
|
|
413
|
-
const disabledHTML = `
|
|
414
|
-
<div data-controller="shadcn--radio-group"
|
|
415
|
-
data-shadcn--radio-group-value-value="">
|
|
416
|
-
<button data-shadcn--radio-group-target="item"
|
|
417
|
-
data-value="enabled"
|
|
418
|
-
data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">
|
|
419
|
-
Enabled
|
|
420
|
-
</button>
|
|
421
|
-
<button data-shadcn--radio-group-target="item"
|
|
422
|
-
data-value="disabled"
|
|
423
|
-
disabled
|
|
424
|
-
data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">
|
|
425
|
-
Disabled
|
|
426
|
-
</button>
|
|
427
|
-
<button data-shadcn--radio-group-target="item"
|
|
428
|
-
data-value="also-enabled"
|
|
429
|
-
data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">
|
|
430
|
-
Also Enabled
|
|
431
|
-
</button>
|
|
432
|
-
</div>
|
|
433
|
-
`
|
|
434
|
-
|
|
435
|
-
beforeEach(async () => {
|
|
436
|
-
const setup = await setupController(RadioGroupController, disabledHTML, 'shadcn--radio-group')
|
|
437
|
-
application = setup.application
|
|
438
|
-
element = setup.element
|
|
439
|
-
controller = setup.controller
|
|
440
|
-
})
|
|
441
|
-
|
|
442
|
-
test("does not select disabled item on click", async () => {
|
|
443
|
-
const disabledItem = controller.itemTargets[1]
|
|
444
|
-
click(disabledItem)
|
|
445
|
-
await nextFrame()
|
|
446
|
-
|
|
447
|
-
expect(controller.valueValue).toBe("")
|
|
448
|
-
})
|
|
449
|
-
|
|
450
|
-
test("enabledItems excludes disabled items", () => {
|
|
451
|
-
const enabled = controller.enabledItems
|
|
452
|
-
expect(enabled.length).toBe(2)
|
|
453
|
-
expect(enabled.map(item => item.dataset.value)).toEqual(["enabled", "also-enabled"])
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
test("keyboard navigation skips disabled items", async () => {
|
|
457
|
-
const firstItem = controller.itemTargets[0]
|
|
458
|
-
const thirdItem = controller.itemTargets[2]
|
|
459
|
-
const focusSpy = jest.spyOn(thirdItem, 'focus')
|
|
460
|
-
|
|
461
|
-
controller.handleKeydown({
|
|
462
|
-
key: "ArrowDown",
|
|
463
|
-
preventDefault: jest.fn(),
|
|
464
|
-
currentTarget: firstItem
|
|
465
|
-
})
|
|
466
|
-
await nextFrame()
|
|
467
|
-
|
|
468
|
-
expect(focusSpy).toHaveBeenCalled()
|
|
469
|
-
expect(controller.valueValue).toBe("also-enabled")
|
|
470
|
-
})
|
|
471
|
-
})
|
|
472
|
-
|
|
473
|
-
describe("initial value", () => {
|
|
474
|
-
const initialValueHTML = `
|
|
475
|
-
<div data-controller="shadcn--radio-group"
|
|
476
|
-
data-shadcn--radio-group-value-value="medium">
|
|
477
|
-
<button data-shadcn--radio-group-target="item"
|
|
478
|
-
data-value="small">Small</button>
|
|
479
|
-
<button data-shadcn--radio-group-target="item"
|
|
480
|
-
data-value="medium">Medium</button>
|
|
481
|
-
<button data-shadcn--radio-group-target="item"
|
|
482
|
-
data-value="large">Large</button>
|
|
483
|
-
</div>
|
|
484
|
-
`
|
|
485
|
-
|
|
486
|
-
beforeEach(async () => {
|
|
487
|
-
const setup = await setupController(RadioGroupController, initialValueHTML, 'shadcn--radio-group')
|
|
488
|
-
application = setup.application
|
|
489
|
-
element = setup.element
|
|
490
|
-
controller = setup.controller
|
|
491
|
-
})
|
|
492
|
-
|
|
493
|
-
test("initializes with pre-set value", () => {
|
|
494
|
-
expect(controller.valueValue).toBe("medium")
|
|
495
|
-
})
|
|
496
|
-
|
|
497
|
-
test("marks correct item as checked on init", () => {
|
|
498
|
-
const mediumItem = controller.itemTargets[1]
|
|
499
|
-
expect(mediumItem.getAttribute("aria-checked")).toBe("true")
|
|
500
|
-
expect(mediumItem.dataset.state).toBe("checked")
|
|
501
|
-
})
|
|
502
|
-
|
|
503
|
-
test("selected item is focusable on init", () => {
|
|
504
|
-
const mediumItem = controller.itemTargets[1]
|
|
505
|
-
expect(mediumItem.getAttribute("tabindex")).toBe("0")
|
|
506
|
-
})
|
|
507
|
-
|
|
508
|
-
test("non-selected items are not focusable on init", () => {
|
|
509
|
-
const smallItem = controller.itemTargets[0]
|
|
510
|
-
const largeItem = controller.itemTargets[2]
|
|
511
|
-
expect(smallItem.getAttribute("tabindex")).toBe("-1")
|
|
512
|
-
expect(largeItem.getAttribute("tabindex")).toBe("-1")
|
|
513
|
-
})
|
|
514
|
-
})
|
|
515
|
-
|
|
516
|
-
describe("programmatic value change", () => {
|
|
517
|
-
const programmaticHTML = `
|
|
518
|
-
<div data-controller="shadcn--radio-group"
|
|
519
|
-
data-shadcn--radio-group-value-value="">
|
|
520
|
-
<button data-shadcn--radio-group-target="item" data-value="x">X</button>
|
|
521
|
-
<button data-shadcn--radio-group-target="item" data-value="y">Y</button>
|
|
522
|
-
</div>
|
|
523
|
-
`
|
|
524
|
-
|
|
525
|
-
beforeEach(async () => {
|
|
526
|
-
const setup = await setupController(RadioGroupController, programmaticHTML, 'shadcn--radio-group')
|
|
527
|
-
application = setup.application
|
|
528
|
-
element = setup.element
|
|
529
|
-
controller = setup.controller
|
|
530
|
-
})
|
|
531
|
-
|
|
532
|
-
test("updates selection when valueValue changes", async () => {
|
|
533
|
-
controller.valueValue = "y"
|
|
534
|
-
await nextFrame()
|
|
535
|
-
|
|
536
|
-
const yItem = controller.itemTargets[1]
|
|
537
|
-
expect(yItem.getAttribute("aria-checked")).toBe("true")
|
|
538
|
-
})
|
|
539
|
-
|
|
540
|
-
test("valueValueChanged callback updates UI", async () => {
|
|
541
|
-
controller.valueValue = "x"
|
|
542
|
-
await nextFrame()
|
|
543
|
-
|
|
544
|
-
const xItem = controller.itemTargets[0]
|
|
545
|
-
const yItem = controller.itemTargets[1]
|
|
546
|
-
expect(xItem.getAttribute("aria-checked")).toBe("true")
|
|
547
|
-
expect(yItem.getAttribute("aria-checked")).toBe("false")
|
|
548
|
-
})
|
|
549
|
-
})
|
|
550
|
-
|
|
551
|
-
describe("edge cases", () => {
|
|
552
|
-
const edgeCaseHTML = `
|
|
553
|
-
<div data-controller="shadcn--radio-group"
|
|
554
|
-
data-shadcn--radio-group-value-value="">
|
|
555
|
-
<button data-shadcn--radio-group-target="item"
|
|
556
|
-
data-value="only"
|
|
557
|
-
data-action="click->shadcn--radio-group#select keydown->shadcn--radio-group#handleKeydown">Only Option</button>
|
|
558
|
-
</div>
|
|
559
|
-
`
|
|
560
|
-
|
|
561
|
-
beforeEach(async () => {
|
|
562
|
-
const setup = await setupController(RadioGroupController, edgeCaseHTML, 'shadcn--radio-group')
|
|
563
|
-
application = setup.application
|
|
564
|
-
element = setup.element
|
|
565
|
-
controller = setup.controller
|
|
566
|
-
})
|
|
567
|
-
|
|
568
|
-
test("handles single item gracefully", async () => {
|
|
569
|
-
const onlyItem = controller.itemTargets[0]
|
|
570
|
-
click(onlyItem)
|
|
571
|
-
await nextFrame()
|
|
572
|
-
|
|
573
|
-
expect(controller.valueValue).toBe("only")
|
|
574
|
-
})
|
|
575
|
-
|
|
576
|
-
test("keyboard navigation with single item stays on that item", async () => {
|
|
577
|
-
const onlyItem = controller.itemTargets[0]
|
|
578
|
-
const focusSpy = jest.spyOn(onlyItem, 'focus')
|
|
579
|
-
|
|
580
|
-
controller.handleKeydown({
|
|
581
|
-
key: "ArrowDown",
|
|
582
|
-
preventDefault: jest.fn(),
|
|
583
|
-
currentTarget: onlyItem
|
|
584
|
-
})
|
|
585
|
-
await nextFrame()
|
|
586
|
-
|
|
587
|
-
expect(focusSpy).toHaveBeenCalled()
|
|
588
|
-
})
|
|
589
|
-
})
|
|
590
|
-
|
|
591
|
-
describe("non-matching key handling", () => {
|
|
592
|
-
const keyHandlingHTML = `
|
|
593
|
-
<div data-controller="shadcn--radio-group"
|
|
594
|
-
data-shadcn--radio-group-value-value="">
|
|
595
|
-
<button data-shadcn--radio-group-target="item"
|
|
596
|
-
data-value="a"
|
|
597
|
-
data-action="keydown->shadcn--radio-group#handleKeydown">A</button>
|
|
598
|
-
<button data-shadcn--radio-group-target="item"
|
|
599
|
-
data-value="b"
|
|
600
|
-
data-action="keydown->shadcn--radio-group#handleKeydown">B</button>
|
|
601
|
-
</div>
|
|
602
|
-
`
|
|
603
|
-
|
|
604
|
-
beforeEach(async () => {
|
|
605
|
-
const setup = await setupController(RadioGroupController, keyHandlingHTML, 'shadcn--radio-group')
|
|
606
|
-
application = setup.application
|
|
607
|
-
element = setup.element
|
|
608
|
-
controller = setup.controller
|
|
609
|
-
})
|
|
610
|
-
|
|
611
|
-
test("ignores non-navigation keys", async () => {
|
|
612
|
-
const firstItem = controller.itemTargets[0]
|
|
613
|
-
const preventDefault = jest.fn()
|
|
614
|
-
|
|
615
|
-
controller.handleKeydown({
|
|
616
|
-
key: "Tab",
|
|
617
|
-
preventDefault,
|
|
618
|
-
currentTarget: firstItem
|
|
619
|
-
})
|
|
620
|
-
await nextFrame()
|
|
621
|
-
|
|
622
|
-
expect(preventDefault).not.toHaveBeenCalled()
|
|
623
|
-
expect(controller.valueValue).toBe("")
|
|
624
|
-
})
|
|
625
|
-
|
|
626
|
-
test("ignores letter keys", async () => {
|
|
627
|
-
const firstItem = controller.itemTargets[0]
|
|
628
|
-
const preventDefault = jest.fn()
|
|
629
|
-
|
|
630
|
-
controller.handleKeydown({
|
|
631
|
-
key: "a",
|
|
632
|
-
preventDefault,
|
|
633
|
-
currentTarget: firstItem
|
|
634
|
-
})
|
|
635
|
-
await nextFrame()
|
|
636
|
-
|
|
637
|
-
expect(preventDefault).not.toHaveBeenCalled()
|
|
638
|
-
})
|
|
639
|
-
})
|
|
640
|
-
})
|