vueless 1.4.12-beta.14 → 1.4.12-beta.16
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/package.json +1 -1
- package/ui.form-date-picker/UDatePicker.vue +11 -1
- package/ui.form-date-picker/tests/UDatePicker.test.ts +47 -0
- package/ui.form-listbox/UListbox.vue +1 -0
- package/ui.form-select/USelect.vue +1 -1
- package/ui.form-select/tests/USelect.test.ts +57 -0
- package/ui.form-switch/USwitch.vue +13 -12
- package/ui.form-switch/tests/USwitch.test.ts +151 -0
- package/v.tooltip/vTooltip.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vueless",
|
|
3
|
-
"version": "1.4.12-beta.
|
|
3
|
+
"version": "1.4.12-beta.16",
|
|
4
4
|
"description": "Vue Styleless UI Component Library, powered by Tailwind CSS.",
|
|
5
5
|
"author": "Johnny Grid <hello@vueless.com> (https://vueless.com)",
|
|
6
6
|
"homepage": "https://vueless.com",
|
|
@@ -119,6 +119,16 @@ function deactivate() {
|
|
|
119
119
|
customView.value = View.Day;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
function deactivateOnBlur(event: FocusEvent) {
|
|
123
|
+
const related = event.relatedTarget as HTMLElement | null;
|
|
124
|
+
|
|
125
|
+
if (related && wrapperRef.value?.contains(related)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
deactivate();
|
|
130
|
+
}
|
|
131
|
+
|
|
122
132
|
function onUserFormatDateChange(value: string) {
|
|
123
133
|
userFormatDate.value = value.trim();
|
|
124
134
|
}
|
|
@@ -279,7 +289,7 @@ watchEffect(() => {
|
|
|
279
289
|
</script>
|
|
280
290
|
|
|
281
291
|
<template>
|
|
282
|
-
<div v-bind="wrapperAttrs" ref="wrapper" :data-test="getDataTest()">
|
|
292
|
+
<div v-bind="wrapperAttrs" ref="wrapper" :data-test="getDataTest()" @focusout="deactivateOnBlur">
|
|
283
293
|
<UInput
|
|
284
294
|
:id="elementId"
|
|
285
295
|
:key="String(userFormatDate)"
|
|
@@ -356,6 +356,53 @@ describe("UDatePicker.vue", () => {
|
|
|
356
356
|
expect(calendar.isVisible()).toBe(true);
|
|
357
357
|
});
|
|
358
358
|
|
|
359
|
+
it("Calendar – hides when focus leaves the whole component", async () => {
|
|
360
|
+
const component = mount(UDatePicker, {
|
|
361
|
+
props: {
|
|
362
|
+
modelValue: new Date(),
|
|
363
|
+
},
|
|
364
|
+
attachTo: document.body,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const input = component.getComponent(UInput).find("input");
|
|
368
|
+
const calendar = component.findComponent({ name: "UCalendar" });
|
|
369
|
+
|
|
370
|
+
await input.trigger("focus");
|
|
371
|
+
expect(calendar.isVisible()).toBe(true);
|
|
372
|
+
|
|
373
|
+
const outsideElement = document.createElement("button");
|
|
374
|
+
|
|
375
|
+
document.body.appendChild(outsideElement);
|
|
376
|
+
|
|
377
|
+
await component.trigger("focusout", { relatedTarget: outsideElement });
|
|
378
|
+
|
|
379
|
+
expect(calendar.isVisible()).toBe(false);
|
|
380
|
+
|
|
381
|
+
outsideElement.remove();
|
|
382
|
+
component.unmount();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("Calendar – stays open when focus moves inside the component", async () => {
|
|
386
|
+
const component = mount(UDatePicker, {
|
|
387
|
+
props: {
|
|
388
|
+
modelValue: new Date(),
|
|
389
|
+
},
|
|
390
|
+
attachTo: document.body,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const input = component.getComponent(UInput).find("input");
|
|
394
|
+
const calendar = component.findComponent({ name: "UCalendar" });
|
|
395
|
+
|
|
396
|
+
await input.trigger("focus");
|
|
397
|
+
expect(calendar.isVisible()).toBe(true);
|
|
398
|
+
|
|
399
|
+
await component.trigger("focusout", { relatedTarget: calendar.element });
|
|
400
|
+
|
|
401
|
+
expect(calendar.isVisible()).toBe(true);
|
|
402
|
+
|
|
403
|
+
component.unmount();
|
|
404
|
+
});
|
|
405
|
+
|
|
359
406
|
it("Calendar – hides when escape key is pressed on calendar", async () => {
|
|
360
407
|
const component = mount(UDatePicker, {
|
|
361
408
|
props: {
|
|
@@ -255,6 +255,63 @@ describe("USelect.vue", () => {
|
|
|
255
255
|
expect(component.getComponent(UListbox).props("searchable")).toBe(true);
|
|
256
256
|
});
|
|
257
257
|
|
|
258
|
+
it("Searchable – closes when the search input blurs to an outside element", async () => {
|
|
259
|
+
const component = mount(USelect, {
|
|
260
|
+
props: {
|
|
261
|
+
searchable: true,
|
|
262
|
+
options: defaultOptions,
|
|
263
|
+
},
|
|
264
|
+
attachTo: document.body,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
await component.get("[role='combobox']").trigger("focus");
|
|
268
|
+
await flushPromises();
|
|
269
|
+
|
|
270
|
+
expect(component.emitted("open")).toBeTruthy();
|
|
271
|
+
|
|
272
|
+
const outside = document.createElement("button");
|
|
273
|
+
|
|
274
|
+
document.body.appendChild(outside);
|
|
275
|
+
|
|
276
|
+
await component
|
|
277
|
+
.getComponent(UListbox)
|
|
278
|
+
.get("input")
|
|
279
|
+
.trigger("blur", { relatedTarget: outside });
|
|
280
|
+
await flushPromises();
|
|
281
|
+
|
|
282
|
+
expect(component.emitted("close")).toBeTruthy();
|
|
283
|
+
expect(component.findComponent(UListbox).exists()).toBe(false);
|
|
284
|
+
|
|
285
|
+
outside.remove();
|
|
286
|
+
component.unmount();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("Searchable – stays open when focus moves within its own listbox", async () => {
|
|
290
|
+
const component = mount(USelect, {
|
|
291
|
+
props: {
|
|
292
|
+
searchable: true,
|
|
293
|
+
options: defaultOptions,
|
|
294
|
+
},
|
|
295
|
+
attachTo: document.body,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await component.get("[role='combobox']").trigger("focus");
|
|
299
|
+
await flushPromises();
|
|
300
|
+
|
|
301
|
+
const listboxEl = component.getComponent(UListbox).element as HTMLElement;
|
|
302
|
+
|
|
303
|
+
await component
|
|
304
|
+
.getComponent(UListbox)
|
|
305
|
+
.get("input")
|
|
306
|
+
.trigger("blur", { relatedTarget: listboxEl });
|
|
307
|
+
await flushPromises();
|
|
308
|
+
|
|
309
|
+
expect(component.emitted("close")).toBeFalsy();
|
|
310
|
+
expect(component.findComponent(UListbox).exists()).toBe(true);
|
|
311
|
+
|
|
312
|
+
component.unmount();
|
|
313
|
+
});
|
|
314
|
+
|
|
258
315
|
it("Search v-model – passes search to UListbox and filters", async () => {
|
|
259
316
|
const component = mount(USelect, {
|
|
260
317
|
props: {
|
|
@@ -68,16 +68,19 @@ function toggle() {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
function
|
|
77
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Toggles when the visible track is clicked. Clicks forwarded from the label
|
|
73
|
+
* arrive on the hidden checkbox (which already toggles via v-model), so skip
|
|
74
|
+
* those to avoid a double toggle.
|
|
75
|
+
*/
|
|
76
|
+
function onClickWrapper(event: MouseEvent) {
|
|
77
|
+
if ((event.target as HTMLElement)?.tagName !== "INPUT") {
|
|
78
|
+
toggle();
|
|
79
|
+
}
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
function
|
|
82
|
+
function onKeydownToggle(event: Event) {
|
|
83
|
+
event.preventDefault();
|
|
81
84
|
toggle();
|
|
82
85
|
}
|
|
83
86
|
|
|
@@ -119,7 +122,6 @@ const {
|
|
|
119
122
|
:disabled="disabled"
|
|
120
123
|
v-bind="switchLabelAttrs"
|
|
121
124
|
:data-test="getDataTest()"
|
|
122
|
-
@click="onClickToggle"
|
|
123
125
|
>
|
|
124
126
|
<template #label>
|
|
125
127
|
<!--
|
|
@@ -141,9 +143,9 @@ const {
|
|
|
141
143
|
ref="wrapper"
|
|
142
144
|
tabindex="0"
|
|
143
145
|
v-bind="wrapperAttrs"
|
|
144
|
-
@keydown.enter="onKeydownSpace"
|
|
145
|
-
@keydown.space.prevent="onKeydownSpace"
|
|
146
146
|
@click="onClickWrapper"
|
|
147
|
+
@keydown.enter="onKeydownToggle"
|
|
148
|
+
@keydown.space="onKeydownToggle"
|
|
147
149
|
>
|
|
148
150
|
<input
|
|
149
151
|
:id="elementId"
|
|
@@ -154,7 +156,6 @@ const {
|
|
|
154
156
|
:aria-labelledby="inputAriaLabelledBy"
|
|
155
157
|
:aria-label="!hasLabel ? localeMessages.switch : undefined"
|
|
156
158
|
v-bind="inputAttrs"
|
|
157
|
-
@click="onClickToggle"
|
|
158
159
|
/>
|
|
159
160
|
|
|
160
161
|
<span v-bind="circleAttrs">
|
|
@@ -13,6 +13,7 @@ describe("USwitch.vue", () => {
|
|
|
13
13
|
const modelValue = true;
|
|
14
14
|
|
|
15
15
|
const component = mount(USwitch, {
|
|
16
|
+
attachTo: document.body,
|
|
16
17
|
props: {
|
|
17
18
|
modelValue,
|
|
18
19
|
"onUpdate:modelValue": (e) => component.setProps({ modelValue: e }),
|
|
@@ -28,6 +29,8 @@ describe("USwitch.vue", () => {
|
|
|
28
29
|
await inputElement.trigger("click");
|
|
29
30
|
|
|
30
31
|
expect(component.emitted("update:modelValue")![1][0]).toBe(true);
|
|
32
|
+
|
|
33
|
+
component.unmount();
|
|
31
34
|
});
|
|
32
35
|
|
|
33
36
|
it("Label – passes label to ULabel component", () => {
|
|
@@ -199,6 +202,154 @@ describe("USwitch.vue", () => {
|
|
|
199
202
|
});
|
|
200
203
|
});
|
|
201
204
|
|
|
205
|
+
describe("Toggle behavior", () => {
|
|
206
|
+
it("Label click – plain-text label toggles exactly once", async () => {
|
|
207
|
+
const component = mount(USwitch, {
|
|
208
|
+
attachTo: document.body,
|
|
209
|
+
props: {
|
|
210
|
+
modelValue: false,
|
|
211
|
+
label: "Test Label",
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await component.find("label").trigger("click");
|
|
216
|
+
|
|
217
|
+
const emitted = component.emitted("update:modelValue");
|
|
218
|
+
|
|
219
|
+
expect(emitted).toHaveLength(1);
|
|
220
|
+
expect(emitted![0][0]).toBe(true);
|
|
221
|
+
|
|
222
|
+
component.unmount();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("Label slot click – #label slot content toggles exactly once", async () => {
|
|
226
|
+
const component = mount(USwitch, {
|
|
227
|
+
attachTo: document.body,
|
|
228
|
+
props: {
|
|
229
|
+
modelValue: false,
|
|
230
|
+
},
|
|
231
|
+
slots: {
|
|
232
|
+
label: "Custom Label Content",
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await component.find("label").trigger("click");
|
|
237
|
+
|
|
238
|
+
const emitted = component.emitted("update:modelValue");
|
|
239
|
+
|
|
240
|
+
expect(emitted).toHaveLength(1);
|
|
241
|
+
expect(emitted![0][0]).toBe(true);
|
|
242
|
+
|
|
243
|
+
component.unmount();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// The track (wrapper) is a sibling of the <label>, outside its `for` forwarding,
|
|
247
|
+
// so its own @click="toggle" is the only path — clicking it must toggle exactly once.
|
|
248
|
+
it("Track click – wrapper toggles exactly once", async () => {
|
|
249
|
+
const component = mount(USwitch, {
|
|
250
|
+
attachTo: document.body,
|
|
251
|
+
props: {
|
|
252
|
+
modelValue: false,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await component.get("[vl-key='wrapper']").trigger("click");
|
|
257
|
+
|
|
258
|
+
const emitted = component.emitted("update:modelValue");
|
|
259
|
+
|
|
260
|
+
expect(emitted).toHaveLength(1);
|
|
261
|
+
expect(emitted![0][0]).toBe(true);
|
|
262
|
+
|
|
263
|
+
component.unmount();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("Track click – disabled wrapper emits nothing", async () => {
|
|
267
|
+
const component = mount(USwitch, {
|
|
268
|
+
attachTo: document.body,
|
|
269
|
+
props: {
|
|
270
|
+
modelValue: false,
|
|
271
|
+
disabled: true,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await component.get("[vl-key='wrapper']").trigger("click");
|
|
276
|
+
|
|
277
|
+
expect(component.emitted("update:modelValue")).toBeUndefined();
|
|
278
|
+
|
|
279
|
+
component.unmount();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("Keyboard – Enter toggles exactly once", async () => {
|
|
283
|
+
const component = mount(USwitch, {
|
|
284
|
+
attachTo: document.body,
|
|
285
|
+
props: {
|
|
286
|
+
modelValue: false,
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
await component.get("[vl-key='wrapper']").trigger("keydown.enter");
|
|
291
|
+
|
|
292
|
+
const emitted = component.emitted("update:modelValue");
|
|
293
|
+
|
|
294
|
+
expect(emitted).toHaveLength(1);
|
|
295
|
+
expect(emitted![0][0]).toBe(true);
|
|
296
|
+
|
|
297
|
+
component.unmount();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("Keyboard – Space toggles exactly once", async () => {
|
|
301
|
+
const component = mount(USwitch, {
|
|
302
|
+
attachTo: document.body,
|
|
303
|
+
props: {
|
|
304
|
+
modelValue: false,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
await component.get("[vl-key='wrapper']").trigger("keydown.space");
|
|
309
|
+
|
|
310
|
+
const emitted = component.emitted("update:modelValue");
|
|
311
|
+
|
|
312
|
+
expect(emitted).toHaveLength(1);
|
|
313
|
+
expect(emitted![0][0]).toBe(true);
|
|
314
|
+
|
|
315
|
+
component.unmount();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("Disabled – label click emits nothing", async () => {
|
|
319
|
+
const component = mount(USwitch, {
|
|
320
|
+
attachTo: document.body,
|
|
321
|
+
props: {
|
|
322
|
+
modelValue: false,
|
|
323
|
+
label: "Test Label",
|
|
324
|
+
disabled: true,
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
await component.find("label").trigger("click");
|
|
329
|
+
|
|
330
|
+
expect(component.emitted("update:modelValue")).toBeUndefined();
|
|
331
|
+
|
|
332
|
+
component.unmount();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("Disabled – keyboard emits nothing", async () => {
|
|
336
|
+
const component = mount(USwitch, {
|
|
337
|
+
attachTo: document.body,
|
|
338
|
+
props: {
|
|
339
|
+
modelValue: false,
|
|
340
|
+
disabled: true,
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
await component.get("[vl-key='wrapper']").trigger("keydown.enter");
|
|
345
|
+
await component.get("[vl-key='wrapper']").trigger("keydown.space");
|
|
346
|
+
|
|
347
|
+
expect(component.emitted("update:modelValue")).toBeUndefined();
|
|
348
|
+
|
|
349
|
+
component.unmount();
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
202
353
|
describe("Slots", () => {
|
|
203
354
|
it("Label – renders custom content from label slot", () => {
|
|
204
355
|
const customLabelContent = "Custom Label Content";
|
package/v.tooltip/vTooltip.ts
CHANGED
|
@@ -64,7 +64,7 @@ function setUpTippy(el: HTMLElement, props: string | TippyProps) {
|
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
if (typeof props !== "string" && props.content && String(props.content).length) {
|
|
67
|
+
if (typeof props !== "string" && props && props.content && String(props.content).length) {
|
|
68
68
|
tippy(el, merge({}, settings, props || {}));
|
|
69
69
|
}
|
|
70
70
|
}
|