vueless 1.4.12-beta.13 → 1.4.12-beta.15
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.data-list/UDataList.vue +25 -7
- package/ui.data-list/tests/UDataList.test.ts +46 -0
- package/ui.data-list/types.ts +6 -0
- package/ui.form-switch/USwitch.vue +13 -12
- package/ui.form-switch/tests/USwitch.test.ts +151 -0
- package/utils/node/loaderIcon.js +1 -1
- 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.15",
|
|
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",
|
|
@@ -42,6 +42,10 @@ function isCrossed(element: DataListItem) {
|
|
|
42
42
|
return Boolean(element.crossed);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function hasNestedList(element: DataListItem) {
|
|
46
|
+
return Boolean(element.children?.length || (props.nestedKey && element[props.nestedKey]));
|
|
47
|
+
}
|
|
48
|
+
|
|
45
49
|
function onDragEnd() {
|
|
46
50
|
const sortData = prepareSortData(props.list);
|
|
47
51
|
|
|
@@ -181,14 +185,33 @@ const {
|
|
|
181
185
|
</div>
|
|
182
186
|
|
|
183
187
|
<UDataList
|
|
184
|
-
v-if="element
|
|
188
|
+
v-if="hasNestedList(element)"
|
|
185
189
|
hide-empty-state-for-nesting
|
|
186
190
|
:list="element.children"
|
|
187
191
|
:group="group"
|
|
192
|
+
:size="size"
|
|
193
|
+
:label-key="labelKey"
|
|
194
|
+
:value-key="valueKey"
|
|
195
|
+
:nested-key="nestedKey"
|
|
196
|
+
:animation-duration="animationDuration"
|
|
197
|
+
:force-fallback="forceFallback"
|
|
198
|
+
:fallback-on-body="fallbackOnBody"
|
|
199
|
+
:fallback-class="fallbackClass"
|
|
188
200
|
v-bind="nestedAttrs"
|
|
201
|
+
:class="!element.children?.length && 'min-h-4'"
|
|
189
202
|
:data-test="getDataTest('table')"
|
|
190
203
|
@drag-sort="onDragEnd"
|
|
191
204
|
>
|
|
205
|
+
<!-- @vue-ignore -->
|
|
206
|
+
<template #drag="slotProps: { item: DataListItem; iconName: string }">
|
|
207
|
+
<!--
|
|
208
|
+
@slot Use it to add something instead of the drag icon.
|
|
209
|
+
@binding {object} item
|
|
210
|
+
@binding {string} icon-name
|
|
211
|
+
-->
|
|
212
|
+
<slot name="drag" :item="slotProps.item" :icon-name="slotProps.iconName" />
|
|
213
|
+
</template>
|
|
214
|
+
|
|
192
215
|
<!-- @vue-ignore -->
|
|
193
216
|
<template #label="slotProps: { item: DataListItem; crossed: boolean }">
|
|
194
217
|
<!--
|
|
@@ -196,12 +219,7 @@ const {
|
|
|
196
219
|
@binding {object} item
|
|
197
220
|
@binding {boolean} crossed
|
|
198
221
|
-->
|
|
199
|
-
<slot name="label" :item="slotProps.item" :crossed="slotProps.crossed"
|
|
200
|
-
<div
|
|
201
|
-
v-bind="slotProps.crossed ? labelCrossedAttrs : labelAttrs"
|
|
202
|
-
v-text="slotProps.item[labelKey]"
|
|
203
|
-
/>
|
|
204
|
-
</slot>
|
|
222
|
+
<slot name="label" :item="slotProps.item" :crossed="slotProps.crossed" />
|
|
205
223
|
</template>
|
|
206
224
|
|
|
207
225
|
<!-- @vue-ignore -->
|
|
@@ -253,6 +253,52 @@ describe("UDataList.vue", () => {
|
|
|
253
253
|
expect(nestedComponent.props("list")).toEqual(nestedList[0].children);
|
|
254
254
|
});
|
|
255
255
|
|
|
256
|
+
it("Nesting – renders empty nested drop-zone for childless items when nestedKey is truthy", () => {
|
|
257
|
+
const component = mount(UDataList, {
|
|
258
|
+
props: {
|
|
259
|
+
list: [{ value: 1, label: "Leaf", canNest: true }],
|
|
260
|
+
nestedKey: "canNest",
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const nestedComponent = component.findComponent("[vl-key='nested']");
|
|
265
|
+
|
|
266
|
+
expect(nestedComponent.exists()).toBe(true);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("Nesting – does not render drop-zone for childless items when nestedKey value is falsy", () => {
|
|
270
|
+
const component = mount(UDataList, {
|
|
271
|
+
props: {
|
|
272
|
+
list: [{ value: 1, label: "Leaf", canNest: false }],
|
|
273
|
+
nestedKey: "canNest",
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const nestedComponent = component.findComponent("[vl-key='nested']");
|
|
278
|
+
|
|
279
|
+
expect(nestedComponent.exists()).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("Nesting – emits dragged item with parentValue of the leaf after drop into empty drop-zone", async () => {
|
|
283
|
+
const leaf: DataListItem = { value: 1, label: "Leaf", canNest: true, children: [] };
|
|
284
|
+
const list: DataListItem[] = [leaf, { value: 2, label: "Dragged", canNest: true }];
|
|
285
|
+
|
|
286
|
+
const component = mount(UDataList, {
|
|
287
|
+
props: { list, nestedKey: "canNest" },
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Simulate vuedraggable moving item 2 into the leaf's (previously empty) nested list.
|
|
291
|
+
list.splice(1, 1);
|
|
292
|
+
leaf.children!.push({ value: 2, label: "Dragged", canNest: true });
|
|
293
|
+
|
|
294
|
+
await component.findComponent(draggable).vm.$emit("end");
|
|
295
|
+
|
|
296
|
+
const sortData = component.emitted("dragSort")?.[0]?.[0] as DataListItem[];
|
|
297
|
+
const dragged = sortData.find((item) => item.value === 2);
|
|
298
|
+
|
|
299
|
+
expect(dragged?.parentValue).toBe(leaf.value);
|
|
300
|
+
});
|
|
301
|
+
|
|
256
302
|
it("Data Test – applies correct data-test attribute", () => {
|
|
257
303
|
const dataTest = "test-data-list";
|
|
258
304
|
|
package/ui.data-list/types.ts
CHANGED
|
@@ -70,6 +70,12 @@ export interface Props {
|
|
|
70
70
|
*/
|
|
71
71
|
hideEmptyStateForNesting?: boolean;
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Item key whose truthy value renders an empty nested drop-zone,
|
|
75
|
+
* so a deeper level can be created by drag even without children.
|
|
76
|
+
*/
|
|
77
|
+
nestedKey?: string;
|
|
78
|
+
|
|
73
79
|
/**
|
|
74
80
|
* Component config object.
|
|
75
81
|
*/
|
|
@@ -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/utils/node/loaderIcon.js
CHANGED
|
@@ -227,7 +227,7 @@ async function findAndCopyIcons(files, library, vuelessConfig, debug) {
|
|
|
227
227
|
const iconPropsPattern = `\\b\\w*(name|icon)\\w*\\s*=\\s*(['"])(.*?)\\2`;
|
|
228
228
|
const uComponentIconNamePattern =
|
|
229
229
|
// eslint-disable-next-line vue/max-len
|
|
230
|
-
/<UIcon\b[^>]*?\b(name)\s*[:=]\s*(['"])(.*?)\2[^>]*?>|<(?!UIcon\b)(
|
|
230
|
+
/<UIcon\b[^>]*?\b(name)\s*[:=]\s*(['"])(.*?)\2[^>]*?>|<(?!UIcon\b)([A-Z]\w+)\b[^>]*?\b(\w*icon\w*)\s*[:=]\s*(['"])(.*?)\6[^>]*?>/;
|
|
231
231
|
|
|
232
232
|
const uComponentIconNameArray = fileContents.match(new RegExp(uComponentIconNamePattern, "g"));
|
|
233
233
|
|
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
|
}
|