@pequity/squirrel 6.0.7 → 6.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/chunks/p-dropdown-select.js +30 -5
- package/dist/es/chunks/p-dropdown-select.js +31 -6
- package/dist/squirrel/components/p-dropdown-select/p-dropdown-select.vue.d.ts +21 -4
- package/package.json +1 -1
- package/squirrel/components/p-dropdown-select/p-dropdown-select.spec.js +134 -0
- package/squirrel/components/p-dropdown-select/p-dropdown-select.stories.js +36 -0
- package/squirrel/components/p-dropdown-select/p-dropdown-select.vue +35 -6
|
@@ -74,14 +74,14 @@ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
|
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
76
|
/**
|
|
77
|
-
* Set property of **items
|
|
77
|
+
* Set property of **items**'s text value
|
|
78
78
|
*/
|
|
79
79
|
itemText: {
|
|
80
80
|
type: String,
|
|
81
81
|
default: "text"
|
|
82
82
|
},
|
|
83
83
|
/**
|
|
84
|
-
* Set property of **items
|
|
84
|
+
* Set property of **items**'s value - must be primitive.
|
|
85
85
|
*/
|
|
86
86
|
itemValue: {
|
|
87
87
|
type: [String, Number],
|
|
@@ -160,9 +160,16 @@ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
|
|
|
160
160
|
disabledBy: {
|
|
161
161
|
type: String,
|
|
162
162
|
default: "disabled"
|
|
163
|
+
},
|
|
164
|
+
/**
|
|
165
|
+
* Enables the ability to create new items when no search results are found
|
|
166
|
+
*/
|
|
167
|
+
creatable: {
|
|
168
|
+
type: Boolean,
|
|
169
|
+
default: false
|
|
163
170
|
}
|
|
164
171
|
},
|
|
165
|
-
emits: ["update:modelValue", "select"],
|
|
172
|
+
emits: ["update:modelValue", "select", "create"],
|
|
166
173
|
setup(__props, { emit: __emit }) {
|
|
167
174
|
const emit = __emit;
|
|
168
175
|
const props = __props;
|
|
@@ -224,6 +231,9 @@ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
|
|
|
224
231
|
return rest;
|
|
225
232
|
});
|
|
226
233
|
const style = vue.computed(() => $attrs.style);
|
|
234
|
+
const selectableItemsCount = vue.computed(
|
|
235
|
+
() => internalItems.value.filter((item) => !isDisabled(item) || isSelected(item[props.itemValue])).length
|
|
236
|
+
);
|
|
227
237
|
vue.watch(
|
|
228
238
|
dropdownShow,
|
|
229
239
|
(nV) => {
|
|
@@ -264,6 +274,10 @@ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
|
|
|
264
274
|
destroyNavigationSvc();
|
|
265
275
|
(_b = (_a = formControl.value) == null ? void 0 : _a.querySelector("button")) == null ? void 0 : _b.focus();
|
|
266
276
|
};
|
|
277
|
+
const handleCreate = () => {
|
|
278
|
+
emit("create", search.value);
|
|
279
|
+
dropdownShow.value = false;
|
|
280
|
+
};
|
|
267
281
|
return (_ctx, _cache) => {
|
|
268
282
|
const _directive_close_popper = vue.resolveDirective("close-popper");
|
|
269
283
|
return vue.openBlock(), vue.createElementBlock("div", {
|
|
@@ -398,7 +412,18 @@ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
|
|
|
398
412
|
!vue.unref(computedItems).length ? vue.renderSlot(_ctx.$slots, "no-items", { key: 0 }, () => [
|
|
399
413
|
vue.createElementVNode("div", {
|
|
400
414
|
class: vue.normalizeClass(["flex items-center justify-center", vue.unref(pSelectList.SIZES)[__props.size]])
|
|
401
|
-
},
|
|
415
|
+
}, [
|
|
416
|
+
__props.creatable && vue.unref(search) ? (vue.openBlock(), vue.createElementBlock("button", {
|
|
417
|
+
key: 0,
|
|
418
|
+
class: "hover:text-primary-hover flex items-center gap-2 font-medium text-p-blue-40",
|
|
419
|
+
onClick: handleCreate
|
|
420
|
+
}, [
|
|
421
|
+
vue.createVNode(pIcon_vue_vue_type_script_setup_true_lang._sfc_main, { icon: "fe:plus-circle" }),
|
|
422
|
+
vue.createTextVNode(" Add '" + vue.toDisplayString(vue.unref(search)) + "' ", 1)
|
|
423
|
+
])) : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [
|
|
424
|
+
vue.createTextVNode("No items found")
|
|
425
|
+
], 64))
|
|
426
|
+
], 2)
|
|
402
427
|
]) : vue.createCommentVNode("", true)
|
|
403
428
|
], 6)
|
|
404
429
|
], 16)
|
|
@@ -418,7 +443,7 @@ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
|
|
|
418
443
|
key: 1,
|
|
419
444
|
item: __props.multiple ? vue.unref(selectedItems) : vue.unref(selectedItems)[0]
|
|
420
445
|
}, () => [
|
|
421
|
-
vue.createElementVNode("div", _hoisted_3, vue.toDisplayString(__props.multiple
|
|
446
|
+
vue.createElementVNode("div", _hoisted_3, vue.toDisplayString(__props.multiple ? vue.unref(selectedItems).length === selectableItemsCount.value ? "All options selected" : `${vue.unref(selectedItems).length} option${vue.unref(selectedItems).length > 1 ? "s" : ""} selected` : vue.unref(selectedItems)[0][__props.itemText]), 1)
|
|
422
447
|
]),
|
|
423
448
|
__props.clearable && vue.unref(selectedItems).length ? (vue.openBlock(), vue.createElementBlock("button", {
|
|
424
449
|
key: 2,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineComponent, ref, useAttrs, computed, watch, onMounted, onUnmounted, resolveDirective, openBlock, createElementBlock, normalizeClass, unref, normalizeStyle, toDisplayString, createCommentVNode, createVNode, mergeProps, withCtx, createElementVNode, isRef, Fragment, renderList, withDirectives, renderSlot, withModifiers, vShow } from "vue";
|
|
1
|
+
import { defineComponent, ref, useAttrs, computed, watch, onMounted, onUnmounted, resolveDirective, openBlock, createElementBlock, normalizeClass, unref, normalizeStyle, toDisplayString, createCommentVNode, createVNode, mergeProps, withCtx, createElementVNode, isRef, Fragment, renderList, withDirectives, renderSlot, createTextVNode, withModifiers, vShow } from "vue";
|
|
2
2
|
import PDropdown from "../p-dropdown.js";
|
|
3
3
|
import { _ as _sfc_main$1 } from "./p-icon.js";
|
|
4
4
|
import PInputSearch from "../p-input-search.js";
|
|
@@ -73,14 +73,14 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
/**
|
|
76
|
-
* Set property of **items
|
|
76
|
+
* Set property of **items**'s text value
|
|
77
77
|
*/
|
|
78
78
|
itemText: {
|
|
79
79
|
type: String,
|
|
80
80
|
default: "text"
|
|
81
81
|
},
|
|
82
82
|
/**
|
|
83
|
-
* Set property of **items
|
|
83
|
+
* Set property of **items**'s value - must be primitive.
|
|
84
84
|
*/
|
|
85
85
|
itemValue: {
|
|
86
86
|
type: [String, Number],
|
|
@@ -159,9 +159,16 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
159
159
|
disabledBy: {
|
|
160
160
|
type: String,
|
|
161
161
|
default: "disabled"
|
|
162
|
+
},
|
|
163
|
+
/**
|
|
164
|
+
* Enables the ability to create new items when no search results are found
|
|
165
|
+
*/
|
|
166
|
+
creatable: {
|
|
167
|
+
type: Boolean,
|
|
168
|
+
default: false
|
|
162
169
|
}
|
|
163
170
|
},
|
|
164
|
-
emits: ["update:modelValue", "select"],
|
|
171
|
+
emits: ["update:modelValue", "select", "create"],
|
|
165
172
|
setup(__props, { emit: __emit }) {
|
|
166
173
|
const emit = __emit;
|
|
167
174
|
const props = __props;
|
|
@@ -223,6 +230,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
223
230
|
return rest;
|
|
224
231
|
});
|
|
225
232
|
const style = computed(() => $attrs.style);
|
|
233
|
+
const selectableItemsCount = computed(
|
|
234
|
+
() => internalItems.value.filter((item) => !isDisabled(item) || isSelected(item[props.itemValue])).length
|
|
235
|
+
);
|
|
226
236
|
watch(
|
|
227
237
|
dropdownShow,
|
|
228
238
|
(nV) => {
|
|
@@ -263,6 +273,10 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
263
273
|
destroyNavigationSvc();
|
|
264
274
|
(_b = (_a = formControl.value) == null ? void 0 : _a.querySelector("button")) == null ? void 0 : _b.focus();
|
|
265
275
|
};
|
|
276
|
+
const handleCreate = () => {
|
|
277
|
+
emit("create", search.value);
|
|
278
|
+
dropdownShow.value = false;
|
|
279
|
+
};
|
|
266
280
|
return (_ctx, _cache) => {
|
|
267
281
|
const _directive_close_popper = resolveDirective("close-popper");
|
|
268
282
|
return openBlock(), createElementBlock("div", {
|
|
@@ -397,7 +411,18 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
397
411
|
!unref(computedItems).length ? renderSlot(_ctx.$slots, "no-items", { key: 0 }, () => [
|
|
398
412
|
createElementVNode("div", {
|
|
399
413
|
class: normalizeClass(["flex items-center justify-center", unref(SIZES)[__props.size]])
|
|
400
|
-
},
|
|
414
|
+
}, [
|
|
415
|
+
__props.creatable && unref(search) ? (openBlock(), createElementBlock("button", {
|
|
416
|
+
key: 0,
|
|
417
|
+
class: "hover:text-primary-hover flex items-center gap-2 font-medium text-p-blue-40",
|
|
418
|
+
onClick: handleCreate
|
|
419
|
+
}, [
|
|
420
|
+
createVNode(_sfc_main$1, { icon: "fe:plus-circle" }),
|
|
421
|
+
createTextVNode(" Add '" + toDisplayString(unref(search)) + "' ", 1)
|
|
422
|
+
])) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
|
|
423
|
+
createTextVNode("No items found")
|
|
424
|
+
], 64))
|
|
425
|
+
], 2)
|
|
401
426
|
]) : createCommentVNode("", true)
|
|
402
427
|
], 6)
|
|
403
428
|
], 16)
|
|
@@ -417,7 +442,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
417
442
|
key: 1,
|
|
418
443
|
item: __props.multiple ? unref(selectedItems) : unref(selectedItems)[0]
|
|
419
444
|
}, () => [
|
|
420
|
-
createElementVNode("div", _hoisted_3, toDisplayString(__props.multiple
|
|
445
|
+
createElementVNode("div", _hoisted_3, toDisplayString(__props.multiple ? unref(selectedItems).length === selectableItemsCount.value ? "All options selected" : `${unref(selectedItems).length} option${unref(selectedItems).length > 1 ? "s" : ""} selected` : unref(selectedItems)[0][__props.itemText]), 1)
|
|
421
446
|
]),
|
|
422
447
|
__props.clearable && unref(selectedItems).length ? (openBlock(), createElementBlock("button", {
|
|
423
448
|
key: 2,
|
|
@@ -262,14 +262,14 @@ declare const __VLS_component: import("vue").DefineComponent<import("vue").Extra
|
|
|
262
262
|
validator(value: Size): boolean;
|
|
263
263
|
};
|
|
264
264
|
/**
|
|
265
|
-
* Set property of **items
|
|
265
|
+
* Set property of **items**'s text value
|
|
266
266
|
*/
|
|
267
267
|
itemText: {
|
|
268
268
|
type: StringConstructor;
|
|
269
269
|
default: string;
|
|
270
270
|
};
|
|
271
271
|
/**
|
|
272
|
-
* Set property of **items
|
|
272
|
+
* Set property of **items**'s value - must be primitive.
|
|
273
273
|
*/
|
|
274
274
|
itemValue: {
|
|
275
275
|
type: (StringConstructor | NumberConstructor)[];
|
|
@@ -349,9 +349,17 @@ declare const __VLS_component: import("vue").DefineComponent<import("vue").Extra
|
|
|
349
349
|
type: PropType<string>;
|
|
350
350
|
default: string;
|
|
351
351
|
};
|
|
352
|
+
/**
|
|
353
|
+
* Enables the ability to create new items when no search results are found
|
|
354
|
+
*/
|
|
355
|
+
creatable: {
|
|
356
|
+
type: BooleanConstructor;
|
|
357
|
+
default: boolean;
|
|
358
|
+
};
|
|
352
359
|
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
353
360
|
select: (...args: any[]) => void;
|
|
354
361
|
"update:modelValue": (...args: any[]) => void;
|
|
362
|
+
create: (...args: any[]) => void;
|
|
355
363
|
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
356
364
|
modelValue: {
|
|
357
365
|
type: PropType<ModelValue>;
|
|
@@ -392,14 +400,14 @@ declare const __VLS_component: import("vue").DefineComponent<import("vue").Extra
|
|
|
392
400
|
validator(value: Size): boolean;
|
|
393
401
|
};
|
|
394
402
|
/**
|
|
395
|
-
* Set property of **items
|
|
403
|
+
* Set property of **items**'s text value
|
|
396
404
|
*/
|
|
397
405
|
itemText: {
|
|
398
406
|
type: StringConstructor;
|
|
399
407
|
default: string;
|
|
400
408
|
};
|
|
401
409
|
/**
|
|
402
|
-
* Set property of **items
|
|
410
|
+
* Set property of **items**'s value - must be primitive.
|
|
403
411
|
*/
|
|
404
412
|
itemValue: {
|
|
405
413
|
type: (StringConstructor | NumberConstructor)[];
|
|
@@ -479,9 +487,17 @@ declare const __VLS_component: import("vue").DefineComponent<import("vue").Extra
|
|
|
479
487
|
type: PropType<string>;
|
|
480
488
|
default: string;
|
|
481
489
|
};
|
|
490
|
+
/**
|
|
491
|
+
* Enables the ability to create new items when no search results are found
|
|
492
|
+
*/
|
|
493
|
+
creatable: {
|
|
494
|
+
type: BooleanConstructor;
|
|
495
|
+
default: boolean;
|
|
496
|
+
};
|
|
482
497
|
}>> & Readonly<{
|
|
483
498
|
onSelect?: ((...args: any[]) => any) | undefined;
|
|
484
499
|
"onUpdate:modelValue"?: ((...args: any[]) => any) | undefined;
|
|
500
|
+
onCreate?: ((...args: any[]) => any) | undefined;
|
|
485
501
|
}>, {
|
|
486
502
|
size: "sm" | "md" | "lg";
|
|
487
503
|
label: string;
|
|
@@ -505,6 +521,7 @@ declare const __VLS_component: import("vue").DefineComponent<import("vue").Extra
|
|
|
505
521
|
placeholderSearch: string;
|
|
506
522
|
selectedTopShown: boolean;
|
|
507
523
|
disabledBy: string;
|
|
524
|
+
creatable: boolean;
|
|
508
525
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {
|
|
509
526
|
formControl: HTMLDivElement;
|
|
510
527
|
button: HTMLButtonElement;
|
package/package.json
CHANGED
|
@@ -193,6 +193,42 @@ describe('PDropdownSelect.vue', () => {
|
|
|
193
193
|
|
|
194
194
|
expect(selectedItems.length).toBe(selectedItemsOptions.length);
|
|
195
195
|
expect(wrapper.vm.$data.selected.length).toBe(items.length);
|
|
196
|
+
expect(wrapper.find('button').text()).toBe('All options selected');
|
|
197
|
+
|
|
198
|
+
cleanup(wrapper);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('shows "All options selected" when all selectable options and disabled-selected options are selected', async () => {
|
|
202
|
+
useVirtualizer.mockImplementation(() => createMockedVirtualizer(10));
|
|
203
|
+
|
|
204
|
+
const items = cloneDeep(filterListItems).slice(0, 10);
|
|
205
|
+
items[0].disabled = true;
|
|
206
|
+
items[1].disabled = true;
|
|
207
|
+
// Pre-select one disabled item
|
|
208
|
+
const wrapper = createWrapper({ selected: [1], items }, { multiple: true });
|
|
209
|
+
|
|
210
|
+
await wrapper.find('button').trigger('click');
|
|
211
|
+
await wrapper.findByText('Select all').trigger('click');
|
|
212
|
+
|
|
213
|
+
const selectedItemsOptions = wrapper.findAll('[p-select-list-option-item]');
|
|
214
|
+
const selectedItems = wrapper.findAll('[p-select-list-option-item].selected');
|
|
215
|
+
|
|
216
|
+
// Should have all non-disabled items (8) plus the pre-selected disabled item (1)
|
|
217
|
+
expect(selectedItems.length).toBe(selectedItemsOptions.length - 1);
|
|
218
|
+
expect(wrapper.vm.$data.selected.length).toBe(items.length - 1);
|
|
219
|
+
expect(wrapper.find('button').text()).toBe('All options selected');
|
|
220
|
+
|
|
221
|
+
cleanup(wrapper);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('shows number of selected options when not all non-disabled options are selected', async () => {
|
|
225
|
+
useVirtualizer.mockImplementation(() => createMockedVirtualizer(10));
|
|
226
|
+
|
|
227
|
+
const items = cloneDeep(filterListItems).slice(0, 10);
|
|
228
|
+
items[0].disabled = true;
|
|
229
|
+
const wrapper = createWrapper({ selected: [2, 3], items }, { multiple: true });
|
|
230
|
+
|
|
231
|
+
expect(wrapper.find('button').text()).toBe('2 options selected');
|
|
196
232
|
|
|
197
233
|
cleanup(wrapper);
|
|
198
234
|
});
|
|
@@ -713,4 +749,102 @@ describe('PDropdownSelect.vue', () => {
|
|
|
713
749
|
|
|
714
750
|
cleanup(wrapper);
|
|
715
751
|
});
|
|
752
|
+
|
|
753
|
+
describe('creatable functionality', () => {
|
|
754
|
+
it('shows create option when no items match search and creatable is true', async () => {
|
|
755
|
+
useVirtualizer.mockImplementation(() => createMockedVirtualizer(20));
|
|
756
|
+
const wrapper = createWrapper({ selected: null }, { searchable: true, creatable: true });
|
|
757
|
+
await wrapper.find('button').trigger('click');
|
|
758
|
+
await sleep(200);
|
|
759
|
+
const searchInput = wrapper.find('input.text-night');
|
|
760
|
+
await searchInput.setValue('New Item');
|
|
761
|
+
const createButton = wrapper.find('button.hover\\:text-primary-hover');
|
|
762
|
+
expect(createButton.exists()).toBe(true);
|
|
763
|
+
expect(createButton.text()).toBe("Add 'New Item'");
|
|
764
|
+
cleanup(wrapper);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
it('does not show create option when creatable is false', async () => {
|
|
768
|
+
useVirtualizer.mockImplementation(() => createMockedVirtualizer(20));
|
|
769
|
+
const wrapper = createWrapper({ selected: null }, { searchable: true, creatable: false });
|
|
770
|
+
await wrapper.find('button').trigger('click');
|
|
771
|
+
await sleep(200);
|
|
772
|
+
const searchInput = wrapper.find('input.text-night');
|
|
773
|
+
await searchInput.setValue('New Item');
|
|
774
|
+
const createButton = wrapper.find('button.hover\\:text-primary-hover');
|
|
775
|
+
expect(createButton.exists()).toBe(false);
|
|
776
|
+
expect(wrapper.text()).toContain('No items found');
|
|
777
|
+
cleanup(wrapper);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it('emits create event when clicking create option', async () => {
|
|
781
|
+
useVirtualizer.mockImplementation(() => createMockedVirtualizer(20));
|
|
782
|
+
const wrapper = createWrapper({ selected: null }, { searchable: true, creatable: true });
|
|
783
|
+
await wrapper.find('button').trigger('click');
|
|
784
|
+
await sleep(200);
|
|
785
|
+
const searchInput = wrapper.find('input.text-night');
|
|
786
|
+
await searchInput.setValue('New Item');
|
|
787
|
+
const createButton = wrapper.find('button.hover\\:text-primary-hover');
|
|
788
|
+
await createButton.trigger('click');
|
|
789
|
+
const pDropdownSelectCmp = wrapper.findComponent(PDropdownSelect);
|
|
790
|
+
expect(pDropdownSelectCmp.emitted().create[0]).toEqual(['New Item']);
|
|
791
|
+
expect(wrapper.find('.pdropdown-stub-popper').exists()).toBe(false);
|
|
792
|
+
cleanup(wrapper);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
it('does not update model value when creating new item', async () => {
|
|
796
|
+
useVirtualizer.mockImplementation(() => createMockedVirtualizer(20));
|
|
797
|
+
const wrapper = createWrapper({ selected: null }, { searchable: true, creatable: true });
|
|
798
|
+
await wrapper.find('button').trigger('click');
|
|
799
|
+
await sleep(200);
|
|
800
|
+
const searchInput = wrapper.find('input.text-night');
|
|
801
|
+
await searchInput.setValue('New Item');
|
|
802
|
+
const createButton = wrapper.find('button.hover\\:text-primary-hover');
|
|
803
|
+
await createButton.trigger('click');
|
|
804
|
+
const pDropdownSelectCmp = wrapper.findComponent(PDropdownSelect);
|
|
805
|
+
expect(pDropdownSelectCmp.emitted()['update:modelValue']).toBeFalsy();
|
|
806
|
+
expect(wrapper.vm.$data.selected).toBe(null);
|
|
807
|
+
cleanup(wrapper);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it('does not create object value when valueIsObject is true', async () => {
|
|
811
|
+
useVirtualizer.mockImplementation(() => createMockedVirtualizer(20));
|
|
812
|
+
const wrapper = createWrapper({ selected: null }, { searchable: true, creatable: true, valueIsObject: true });
|
|
813
|
+
await wrapper.find('button').trigger('click');
|
|
814
|
+
await sleep(200);
|
|
815
|
+
const searchInput = wrapper.find('input.text-night');
|
|
816
|
+
await searchInput.setValue('New Item');
|
|
817
|
+
const createButton = wrapper.find('button.hover\\:text-primary-hover');
|
|
818
|
+
await createButton.trigger('click');
|
|
819
|
+
const pDropdownSelectCmp = wrapper.findComponent(PDropdownSelect);
|
|
820
|
+
expect(pDropdownSelectCmp.emitted().create[0]).toEqual(['New Item']);
|
|
821
|
+
expect(pDropdownSelectCmp.emitted()['update:modelValue']).toBeFalsy();
|
|
822
|
+
expect(wrapper.vm.$data.selected).toBe(null);
|
|
823
|
+
cleanup(wrapper);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
it('emits create event with search value when using custom itemValue and itemText', async () => {
|
|
827
|
+
useVirtualizer.mockImplementation(() => createMockedVirtualizer(20));
|
|
828
|
+
const wrapper = createWrapper(
|
|
829
|
+
{ selected: null },
|
|
830
|
+
{
|
|
831
|
+
searchable: true,
|
|
832
|
+
creatable: true,
|
|
833
|
+
valueIsObject: true,
|
|
834
|
+
itemValue: 'customValue',
|
|
835
|
+
itemText: 'customText',
|
|
836
|
+
}
|
|
837
|
+
);
|
|
838
|
+
await wrapper.find('button').trigger('click');
|
|
839
|
+
await sleep(200);
|
|
840
|
+
const searchInput = wrapper.find('input.text-night');
|
|
841
|
+
await searchInput.setValue('New Item');
|
|
842
|
+
const createButton = wrapper.find('button.hover\\:text-primary-hover');
|
|
843
|
+
await createButton.trigger('click');
|
|
844
|
+
const pDropdownSelectCmp = wrapper.findComponent(PDropdownSelect);
|
|
845
|
+
expect(pDropdownSelectCmp.emitted().create[0]).toEqual(['New Item']);
|
|
846
|
+
expect(pDropdownSelectCmp.emitted()['update:modelValue']).toBeFalsy();
|
|
847
|
+
cleanup(wrapper);
|
|
848
|
+
});
|
|
849
|
+
});
|
|
716
850
|
});
|
|
@@ -312,3 +312,39 @@ export const ClearableMultiple = {
|
|
|
312
312
|
clearable: true,
|
|
313
313
|
},
|
|
314
314
|
};
|
|
315
|
+
|
|
316
|
+
export const Creatable = {
|
|
317
|
+
render: (args) => ({
|
|
318
|
+
components: { PDropdownSelect },
|
|
319
|
+
setup() {
|
|
320
|
+
const value = ref(null);
|
|
321
|
+
return { args, value };
|
|
322
|
+
},
|
|
323
|
+
template: `
|
|
324
|
+
<div>
|
|
325
|
+
<PDropdownSelect
|
|
326
|
+
v-model="value"
|
|
327
|
+
v-bind="args"
|
|
328
|
+
@create="(searchTerm) => alert('Create item: ' + searchTerm)"
|
|
329
|
+
/>
|
|
330
|
+
<div class="mt-4">Selected: {{ value }}</div>
|
|
331
|
+
</div>
|
|
332
|
+
`,
|
|
333
|
+
}),
|
|
334
|
+
parameters: {
|
|
335
|
+
docs: {
|
|
336
|
+
description: {
|
|
337
|
+
story:
|
|
338
|
+
'Example with creatable functionality enabled. When no search results are found, an option to create a new item is shown.',
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
args: {
|
|
343
|
+
...Default.args,
|
|
344
|
+
label: 'Creatable dropdown',
|
|
345
|
+
items: items2,
|
|
346
|
+
searchable: true,
|
|
347
|
+
creatable: true,
|
|
348
|
+
placeholder: 'Search or create an item...',
|
|
349
|
+
},
|
|
350
|
+
};
|
|
@@ -32,8 +32,10 @@
|
|
|
32
32
|
<slot v-else name="selected-item" :item="multiple ? selectedItems : selectedItems[0]">
|
|
33
33
|
<div class="truncate text-left">
|
|
34
34
|
{{
|
|
35
|
-
multiple
|
|
36
|
-
?
|
|
35
|
+
multiple
|
|
36
|
+
? selectedItems.length === selectableItemsCount
|
|
37
|
+
? 'All options selected'
|
|
38
|
+
: `${selectedItems.length} option${selectedItems.length > 1 ? 's' : ''} selected`
|
|
37
39
|
: selectedItems[0][itemText]
|
|
38
40
|
}}
|
|
39
41
|
</div>
|
|
@@ -148,7 +150,18 @@
|
|
|
148
150
|
</div>
|
|
149
151
|
</div>
|
|
150
152
|
<slot v-if="!computedItems.length" name="no-items">
|
|
151
|
-
<div :class="['flex items-center justify-center', SIZES[size]]">
|
|
153
|
+
<div :class="['flex items-center justify-center', SIZES[size]]">
|
|
154
|
+
<template v-if="creatable && search">
|
|
155
|
+
<button
|
|
156
|
+
class="hover:text-primary-hover flex items-center gap-2 font-medium text-p-blue-40"
|
|
157
|
+
@click="handleCreate"
|
|
158
|
+
>
|
|
159
|
+
<PIcon icon="fe:plus-circle" />
|
|
160
|
+
Add '{{ search }}'
|
|
161
|
+
</button>
|
|
162
|
+
</template>
|
|
163
|
+
<template v-else>No items found</template>
|
|
164
|
+
</div>
|
|
152
165
|
</slot>
|
|
153
166
|
</div>
|
|
154
167
|
</div>
|
|
@@ -197,7 +210,7 @@ defineSlots<{
|
|
|
197
210
|
item(props: { item: any; isItemSelected: boolean; itemTextSplit: string[] }): unknown;
|
|
198
211
|
}>();
|
|
199
212
|
|
|
200
|
-
const emit = defineEmits(['update:modelValue', 'select']);
|
|
213
|
+
const emit = defineEmits(['update:modelValue', 'select', 'create']);
|
|
201
214
|
|
|
202
215
|
const props = defineProps({
|
|
203
216
|
modelValue: {
|
|
@@ -241,14 +254,14 @@ const props = defineProps({
|
|
|
241
254
|
},
|
|
242
255
|
},
|
|
243
256
|
/**
|
|
244
|
-
* Set property of **items
|
|
257
|
+
* Set property of **items**'s text value
|
|
245
258
|
*/
|
|
246
259
|
itemText: {
|
|
247
260
|
type: String,
|
|
248
261
|
default: 'text',
|
|
249
262
|
},
|
|
250
263
|
/**
|
|
251
|
-
* Set property of **items
|
|
264
|
+
* Set property of **items**'s value - must be primitive.
|
|
252
265
|
*/
|
|
253
266
|
itemValue: {
|
|
254
267
|
type: [String, Number],
|
|
@@ -328,6 +341,13 @@ const props = defineProps({
|
|
|
328
341
|
type: String as PropType<string>,
|
|
329
342
|
default: 'disabled',
|
|
330
343
|
},
|
|
344
|
+
/**
|
|
345
|
+
* Enables the ability to create new items when no search results are found
|
|
346
|
+
*/
|
|
347
|
+
creatable: {
|
|
348
|
+
type: Boolean,
|
|
349
|
+
default: false,
|
|
350
|
+
},
|
|
331
351
|
});
|
|
332
352
|
|
|
333
353
|
// Async helpers
|
|
@@ -401,6 +421,10 @@ const attrs = computed(() => {
|
|
|
401
421
|
|
|
402
422
|
const style = computed(() => $attrs.style as StyleValue);
|
|
403
423
|
|
|
424
|
+
const selectableItemsCount = computed(
|
|
425
|
+
() => internalItems.value.filter((item) => !isDisabled(item) || isSelected(item[props.itemValue])).length
|
|
426
|
+
);
|
|
427
|
+
|
|
404
428
|
// Watch
|
|
405
429
|
// Sorts internalItems putting the selected ones first
|
|
406
430
|
watch(
|
|
@@ -453,4 +477,9 @@ const onHide = () => {
|
|
|
453
477
|
destroyNavigationSvc();
|
|
454
478
|
(formControl.value?.querySelector('button') as HTMLElement)?.focus();
|
|
455
479
|
};
|
|
480
|
+
|
|
481
|
+
const handleCreate = () => {
|
|
482
|
+
emit('create', search.value);
|
|
483
|
+
dropdownShow.value = false;
|
|
484
|
+
};
|
|
456
485
|
</script>
|