@pequity/squirrel 6.0.6 → 6.0.8
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/README.md +2 -2
- package/dist/cjs/chunks/p-dropdown-select.js +27 -5
- package/dist/cjs/useSelectList.js +10 -5
- package/dist/es/chunks/p-dropdown-select.js +28 -6
- package/dist/es/useSelectList.js +10 -5
- package/dist/squirrel/components/p-dropdown-select/p-dropdown-select.vue.d.ts +21 -4
- package/package.json +24 -24
- package/squirrel/components/p-dropdown-select/p-dropdown-select.spec.js +145 -0
- package/squirrel/components/p-dropdown-select/p-dropdown-select.stories.js +36 -0
- package/squirrel/components/p-dropdown-select/p-dropdown-select.vue +29 -6
- package/squirrel/components/p-select-list/useSelectList.ts +12 -8
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ Import and use the components you need in your Vue 3 project:
|
|
|
76
76
|
</template>
|
|
77
77
|
|
|
78
78
|
<script setup lang="ts">
|
|
79
|
-
import { PBtn
|
|
79
|
+
import { PBtn } from '@pequity/squirrel';
|
|
80
80
|
</script>
|
|
81
81
|
```
|
|
82
82
|
|
|
@@ -111,7 +111,7 @@ Then, in your consumer project's `.env.local` file, add an `VUE_APP_SQUIRREL_LOC
|
|
|
111
111
|
|
|
112
112
|
Finally, in your project's `vite.config` file, add the following:
|
|
113
113
|
|
|
114
|
-
> Heads up! The `vite.config.
|
|
114
|
+
> Heads up! The `vite.config.ts` file of the `pequity/frontendv2` already includes the following configuration.
|
|
115
115
|
|
|
116
116
|
```js
|
|
117
117
|
import { defineConfig, searchForWorkspaceRoot } from 'vite';
|
|
@@ -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;
|
|
@@ -264,6 +271,10 @@ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
|
|
|
264
271
|
destroyNavigationSvc();
|
|
265
272
|
(_b = (_a = formControl.value) == null ? void 0 : _a.querySelector("button")) == null ? void 0 : _b.focus();
|
|
266
273
|
};
|
|
274
|
+
const handleCreate = () => {
|
|
275
|
+
emit("create", search.value);
|
|
276
|
+
dropdownShow.value = false;
|
|
277
|
+
};
|
|
267
278
|
return (_ctx, _cache) => {
|
|
268
279
|
const _directive_close_popper = vue.resolveDirective("close-popper");
|
|
269
280
|
return vue.openBlock(), vue.createElementBlock("div", {
|
|
@@ -398,7 +409,18 @@ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
|
|
|
398
409
|
!vue.unref(computedItems).length ? vue.renderSlot(_ctx.$slots, "no-items", { key: 0 }, () => [
|
|
399
410
|
vue.createElementVNode("div", {
|
|
400
411
|
class: vue.normalizeClass(["flex items-center justify-center", vue.unref(pSelectList.SIZES)[__props.size]])
|
|
401
|
-
},
|
|
412
|
+
}, [
|
|
413
|
+
__props.creatable && vue.unref(search) ? (vue.openBlock(), vue.createElementBlock("button", {
|
|
414
|
+
key: 0,
|
|
415
|
+
class: "hover:text-primary-hover flex items-center gap-2 font-medium text-p-blue-40",
|
|
416
|
+
onClick: handleCreate
|
|
417
|
+
}, [
|
|
418
|
+
vue.createVNode(pIcon_vue_vue_type_script_setup_true_lang._sfc_main, { icon: "fe:plus-circle" }),
|
|
419
|
+
vue.createTextVNode(" Add '" + vue.toDisplayString(vue.unref(search)) + "' ", 1)
|
|
420
|
+
])) : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [
|
|
421
|
+
vue.createTextVNode("No items found")
|
|
422
|
+
], 64))
|
|
423
|
+
], 2)
|
|
402
424
|
]) : vue.createCommentVNode("", true)
|
|
403
425
|
], 6)
|
|
404
426
|
], 16)
|
|
@@ -420,7 +442,7 @@ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
|
|
|
420
442
|
}, () => [
|
|
421
443
|
vue.createElementVNode("div", _hoisted_3, vue.toDisplayString(__props.multiple && vue.unref(selectedItems).length > 1 ? `${vue.unref(selectedItems).length} option${vue.unref(selectedItems).length > 1 ? "s" : ""} selected` : vue.unref(selectedItems)[0][__props.itemText]), 1)
|
|
422
444
|
]),
|
|
423
|
-
__props.clearable && vue.unref(
|
|
445
|
+
__props.clearable && vue.unref(selectedItems).length ? (vue.openBlock(), vue.createElementBlock("button", {
|
|
424
446
|
key: 2,
|
|
425
447
|
class: vue.normalizeClass(["absolute top-1/2 flex -translate-y-1/2 items-center justify-center text-p-gray-40 hover:text-p-gray-60", [vue.unref(pSelectList.SIZES)[__props.size], CLEAR_BUTTON_SPACING[__props.size]]]),
|
|
426
448
|
"aria-label": "Clear selection",
|
|
@@ -217,12 +217,17 @@ const useSelectList = (props, inputSearch, virtualizerRef, emit) => {
|
|
|
217
217
|
emit("update:modelValue", toArrOfObjIfNeeded(toEmit));
|
|
218
218
|
};
|
|
219
219
|
const clearAll = () => {
|
|
220
|
-
if (!props.multiple) return;
|
|
221
220
|
search.value = "";
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
221
|
+
if (props.multiple) {
|
|
222
|
+
const disabledItemsValues = internalItems.value.filter((item) => isDisabled(item)).map((item) => item[props.itemValue]);
|
|
223
|
+
const selectedItemsValues = internalValue.value;
|
|
224
|
+
const selectedDisabledItems = lodashEs.intersection(disabledItemsValues, selectedItemsValues);
|
|
225
|
+
emit("update:modelValue", toArrOfObjIfNeeded(selectedDisabledItems));
|
|
226
|
+
} else {
|
|
227
|
+
if (!isDisabled(selectedItems.value[0])) {
|
|
228
|
+
emit("update:modelValue", null);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
226
231
|
};
|
|
227
232
|
vue.watch(
|
|
228
233
|
() => props.items,
|
|
@@ -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;
|
|
@@ -263,6 +270,10 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
263
270
|
destroyNavigationSvc();
|
|
264
271
|
(_b = (_a = formControl.value) == null ? void 0 : _a.querySelector("button")) == null ? void 0 : _b.focus();
|
|
265
272
|
};
|
|
273
|
+
const handleCreate = () => {
|
|
274
|
+
emit("create", search.value);
|
|
275
|
+
dropdownShow.value = false;
|
|
276
|
+
};
|
|
266
277
|
return (_ctx, _cache) => {
|
|
267
278
|
const _directive_close_popper = resolveDirective("close-popper");
|
|
268
279
|
return openBlock(), createElementBlock("div", {
|
|
@@ -397,7 +408,18 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
397
408
|
!unref(computedItems).length ? renderSlot(_ctx.$slots, "no-items", { key: 0 }, () => [
|
|
398
409
|
createElementVNode("div", {
|
|
399
410
|
class: normalizeClass(["flex items-center justify-center", unref(SIZES)[__props.size]])
|
|
400
|
-
},
|
|
411
|
+
}, [
|
|
412
|
+
__props.creatable && unref(search) ? (openBlock(), createElementBlock("button", {
|
|
413
|
+
key: 0,
|
|
414
|
+
class: "hover:text-primary-hover flex items-center gap-2 font-medium text-p-blue-40",
|
|
415
|
+
onClick: handleCreate
|
|
416
|
+
}, [
|
|
417
|
+
createVNode(_sfc_main$1, { icon: "fe:plus-circle" }),
|
|
418
|
+
createTextVNode(" Add '" + toDisplayString(unref(search)) + "' ", 1)
|
|
419
|
+
])) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
|
|
420
|
+
createTextVNode("No items found")
|
|
421
|
+
], 64))
|
|
422
|
+
], 2)
|
|
401
423
|
]) : createCommentVNode("", true)
|
|
402
424
|
], 6)
|
|
403
425
|
], 16)
|
|
@@ -419,7 +441,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
419
441
|
}, () => [
|
|
420
442
|
createElementVNode("div", _hoisted_3, toDisplayString(__props.multiple && unref(selectedItems).length > 1 ? `${unref(selectedItems).length} option${unref(selectedItems).length > 1 ? "s" : ""} selected` : unref(selectedItems)[0][__props.itemText]), 1)
|
|
421
443
|
]),
|
|
422
|
-
__props.clearable && unref(
|
|
444
|
+
__props.clearable && unref(selectedItems).length ? (openBlock(), createElementBlock("button", {
|
|
423
445
|
key: 2,
|
|
424
446
|
class: normalizeClass(["absolute top-1/2 flex -translate-y-1/2 items-center justify-center text-p-gray-40 hover:text-p-gray-60", [unref(SIZES)[__props.size], CLEAR_BUTTON_SPACING[__props.size]]]),
|
|
425
447
|
"aria-label": "Clear selection",
|
package/dist/es/useSelectList.js
CHANGED
|
@@ -215,12 +215,17 @@ const useSelectList = (props, inputSearch, virtualizerRef, emit) => {
|
|
|
215
215
|
emit("update:modelValue", toArrOfObjIfNeeded(toEmit));
|
|
216
216
|
};
|
|
217
217
|
const clearAll = () => {
|
|
218
|
-
if (!props.multiple) return;
|
|
219
218
|
search.value = "";
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
if (props.multiple) {
|
|
220
|
+
const disabledItemsValues = internalItems.value.filter((item) => isDisabled(item)).map((item) => item[props.itemValue]);
|
|
221
|
+
const selectedItemsValues = internalValue.value;
|
|
222
|
+
const selectedDisabledItems = intersection(disabledItemsValues, selectedItemsValues);
|
|
223
|
+
emit("update:modelValue", toArrOfObjIfNeeded(selectedDisabledItems));
|
|
224
|
+
} else {
|
|
225
|
+
if (!isDisabled(selectedItems.value[0])) {
|
|
226
|
+
emit("update:modelValue", null);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
224
229
|
};
|
|
225
230
|
watch(
|
|
226
231
|
() => props.items,
|
|
@@ -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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pequity/squirrel",
|
|
3
3
|
"description": "Squirrel component library",
|
|
4
|
-
"version": "6.0.
|
|
5
|
-
"packageManager": "pnpm@9.15.
|
|
4
|
+
"version": "6.0.8",
|
|
5
|
+
"packageManager": "pnpm@9.15.4",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"preinstall": "npx only-allow pnpm",
|
|
@@ -53,54 +53,54 @@
|
|
|
53
53
|
"@playwright/test": "^1.49.1",
|
|
54
54
|
"@semantic-release/changelog": "^6.0.3",
|
|
55
55
|
"@semantic-release/git": "^10.0.1",
|
|
56
|
-
"@storybook/addon-a11y": "^8.
|
|
57
|
-
"@storybook/addon-actions": "^8.
|
|
58
|
-
"@storybook/addon-essentials": "^8.
|
|
59
|
-
"@storybook/addon-interactions": "^8.
|
|
60
|
-
"@storybook/addon-links": "^8.
|
|
61
|
-
"@storybook/blocks": "^8.
|
|
62
|
-
"@storybook/manager-api": "^8.
|
|
63
|
-
"@storybook/test": "^8.
|
|
56
|
+
"@storybook/addon-a11y": "^8.5.0",
|
|
57
|
+
"@storybook/addon-actions": "^8.5.0",
|
|
58
|
+
"@storybook/addon-essentials": "^8.5.0",
|
|
59
|
+
"@storybook/addon-interactions": "^8.5.0",
|
|
60
|
+
"@storybook/addon-links": "^8.5.0",
|
|
61
|
+
"@storybook/blocks": "^8.5.0",
|
|
62
|
+
"@storybook/manager-api": "^8.5.0",
|
|
63
|
+
"@storybook/test": "^8.5.0",
|
|
64
64
|
"@storybook/test-runner": "^0.21.0",
|
|
65
|
-
"@storybook/theming": "^8.
|
|
66
|
-
"@storybook/vue3": "^8.
|
|
67
|
-
"@storybook/vue3-vite": "^8.
|
|
65
|
+
"@storybook/theming": "^8.5.0",
|
|
66
|
+
"@storybook/vue3": "^8.5.0",
|
|
67
|
+
"@storybook/vue3-vite": "^8.5.0",
|
|
68
68
|
"@tanstack/vue-virtual": "3.11.2",
|
|
69
69
|
"@types/jsdom": "^21.1.7",
|
|
70
70
|
"@types/lodash-es": "^4.17.12",
|
|
71
|
-
"@types/node": "^22.10.
|
|
71
|
+
"@types/node": "^22.10.7",
|
|
72
72
|
"@vitejs/plugin-vue": "^5.2.1",
|
|
73
|
-
"@vitest/coverage-v8": "^
|
|
73
|
+
"@vitest/coverage-v8": "^3.0.3",
|
|
74
74
|
"@vue/compiler-sfc": "3.5.13",
|
|
75
75
|
"@vue/test-utils": "^2.4.6",
|
|
76
76
|
"@vuepic/vue-datepicker": "11.0.1",
|
|
77
77
|
"autoprefixer": "^10.4.20",
|
|
78
78
|
"dayjs": "1.11.13",
|
|
79
|
-
"eslint": "^9.
|
|
79
|
+
"eslint": "^9.18.0",
|
|
80
80
|
"eslint-plugin-storybook": "^0.11.2",
|
|
81
81
|
"floating-vue": "5.2.2",
|
|
82
82
|
"glob": "^11.0.1",
|
|
83
83
|
"husky": "^9.1.7",
|
|
84
84
|
"iconify-icon": "^2.3.0",
|
|
85
85
|
"jsdom": "^26.0.0",
|
|
86
|
-
"lint-staged": "^15.
|
|
86
|
+
"lint-staged": "^15.4.1",
|
|
87
87
|
"lodash-es": "4.17.21",
|
|
88
88
|
"make-coverage-badge": "^1.2.0",
|
|
89
|
-
"postcss": "^8.
|
|
89
|
+
"postcss": "^8.5.1",
|
|
90
90
|
"prettier": "^3.4.2",
|
|
91
|
-
"prettier-plugin-tailwindcss": "^0.6.
|
|
91
|
+
"prettier-plugin-tailwindcss": "^0.6.10",
|
|
92
92
|
"resolve-tspaths": "^0.8.23",
|
|
93
93
|
"rimraf": "^6.0.1",
|
|
94
|
-
"sass": "^1.83.
|
|
94
|
+
"sass": "^1.83.4",
|
|
95
95
|
"semantic-release": "^24.2.1",
|
|
96
|
-
"storybook": "^8.
|
|
96
|
+
"storybook": "^8.5.0",
|
|
97
97
|
"svgo": "^3.3.2",
|
|
98
98
|
"tailwindcss": "^3.4.17",
|
|
99
99
|
"typescript": "5.7.3",
|
|
100
|
-
"vite": "^6.0.
|
|
101
|
-
"vitest": "^
|
|
100
|
+
"vite": "^6.0.11",
|
|
101
|
+
"vitest": "^3.0.3",
|
|
102
102
|
"vue": "3.5.13",
|
|
103
|
-
"vue-currency-input": "3.1
|
|
103
|
+
"vue-currency-input": "3.2.1",
|
|
104
104
|
"vue-router": "4.5.0",
|
|
105
105
|
"vue-toastification": "2.0.0-rc.5",
|
|
106
106
|
"vue-tsc": "2.2.0"
|
|
@@ -653,6 +653,17 @@ describe('PDropdownSelect.vue', () => {
|
|
|
653
653
|
cleanup(wrapper);
|
|
654
654
|
});
|
|
655
655
|
|
|
656
|
+
it('does not render clear button when there is no value selected', async () => {
|
|
657
|
+
useVirtualizer.mockImplementation(() => createMockedVirtualizer(20));
|
|
658
|
+
|
|
659
|
+
const wrapper = createWrapper({ selected: null }, { clearable: true });
|
|
660
|
+
|
|
661
|
+
const clearButton = wrapper.find('button[aria-label="Clear selection"]');
|
|
662
|
+
expect(clearButton.exists()).toBe(false);
|
|
663
|
+
|
|
664
|
+
cleanup(wrapper);
|
|
665
|
+
});
|
|
666
|
+
|
|
656
667
|
it('clears multiple selections when clearable is true', async () => {
|
|
657
668
|
useVirtualizer.mockImplementation(() => createMockedVirtualizer(20));
|
|
658
669
|
|
|
@@ -666,4 +677,138 @@ describe('PDropdownSelect.vue', () => {
|
|
|
666
677
|
|
|
667
678
|
cleanup(wrapper);
|
|
668
679
|
});
|
|
680
|
+
|
|
681
|
+
it('clears a single selection select when clearable is true', async () => {
|
|
682
|
+
useVirtualizer.mockImplementation(() => createMockedVirtualizer(10));
|
|
683
|
+
|
|
684
|
+
const items = cloneDeep(filterListItems).slice(0, 10);
|
|
685
|
+
items[0].disabled = false;
|
|
686
|
+
const wrapper = createWrapper({ selected: 1, items }, { multiple: false, clearable: true });
|
|
687
|
+
|
|
688
|
+
expect(wrapper.vm.$data.selected).toEqual(1);
|
|
689
|
+
|
|
690
|
+
const clearButton = wrapper.find('button[aria-label="Clear selection"]');
|
|
691
|
+
expect(clearButton.exists()).toBe(true);
|
|
692
|
+
|
|
693
|
+
await clearButton.trigger('click');
|
|
694
|
+
expect(wrapper.vm.$data.selected).toEqual(null);
|
|
695
|
+
|
|
696
|
+
cleanup(wrapper);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it('does not clear a single selection select when the item is disabled', async () => {
|
|
700
|
+
useVirtualizer.mockImplementation(() => createMockedVirtualizer(10));
|
|
701
|
+
|
|
702
|
+
const items = cloneDeep(filterListItems).slice(0, 10);
|
|
703
|
+
items[0].disabled = true;
|
|
704
|
+
const wrapper = createWrapper({ selected: 1, items }, { multiple: false, clearable: true });
|
|
705
|
+
|
|
706
|
+
expect(wrapper.vm.$data.selected).toEqual(1);
|
|
707
|
+
|
|
708
|
+
const clearButton = wrapper.find('button[aria-label="Clear selection"]');
|
|
709
|
+
expect(clearButton.exists()).toBe(true);
|
|
710
|
+
|
|
711
|
+
await clearButton.trigger('click');
|
|
712
|
+
expect(wrapper.vm.$data.selected).toEqual(1);
|
|
713
|
+
|
|
714
|
+
cleanup(wrapper);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
describe('creatable functionality', () => {
|
|
718
|
+
it('shows create option when no items match search and creatable is true', async () => {
|
|
719
|
+
useVirtualizer.mockImplementation(() => createMockedVirtualizer(20));
|
|
720
|
+
const wrapper = createWrapper({ selected: null }, { searchable: true, creatable: true });
|
|
721
|
+
await wrapper.find('button').trigger('click');
|
|
722
|
+
await sleep(200);
|
|
723
|
+
const searchInput = wrapper.find('input.text-night');
|
|
724
|
+
await searchInput.setValue('New Item');
|
|
725
|
+
const createButton = wrapper.find('button.hover\\:text-primary-hover');
|
|
726
|
+
expect(createButton.exists()).toBe(true);
|
|
727
|
+
expect(createButton.text()).toBe("Add 'New Item'");
|
|
728
|
+
cleanup(wrapper);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it('does not show create option when creatable is false', async () => {
|
|
732
|
+
useVirtualizer.mockImplementation(() => createMockedVirtualizer(20));
|
|
733
|
+
const wrapper = createWrapper({ selected: null }, { searchable: true, creatable: false });
|
|
734
|
+
await wrapper.find('button').trigger('click');
|
|
735
|
+
await sleep(200);
|
|
736
|
+
const searchInput = wrapper.find('input.text-night');
|
|
737
|
+
await searchInput.setValue('New Item');
|
|
738
|
+
const createButton = wrapper.find('button.hover\\:text-primary-hover');
|
|
739
|
+
expect(createButton.exists()).toBe(false);
|
|
740
|
+
expect(wrapper.text()).toContain('No items found');
|
|
741
|
+
cleanup(wrapper);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it('emits create event when clicking create option', async () => {
|
|
745
|
+
useVirtualizer.mockImplementation(() => createMockedVirtualizer(20));
|
|
746
|
+
const wrapper = createWrapper({ selected: null }, { searchable: true, creatable: true });
|
|
747
|
+
await wrapper.find('button').trigger('click');
|
|
748
|
+
await sleep(200);
|
|
749
|
+
const searchInput = wrapper.find('input.text-night');
|
|
750
|
+
await searchInput.setValue('New Item');
|
|
751
|
+
const createButton = wrapper.find('button.hover\\:text-primary-hover');
|
|
752
|
+
await createButton.trigger('click');
|
|
753
|
+
const pDropdownSelectCmp = wrapper.findComponent(PDropdownSelect);
|
|
754
|
+
expect(pDropdownSelectCmp.emitted().create[0]).toEqual(['New Item']);
|
|
755
|
+
expect(wrapper.find('.pdropdown-stub-popper').exists()).toBe(false);
|
|
756
|
+
cleanup(wrapper);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it('does not update model value when creating new item', async () => {
|
|
760
|
+
useVirtualizer.mockImplementation(() => createMockedVirtualizer(20));
|
|
761
|
+
const wrapper = createWrapper({ selected: null }, { searchable: true, creatable: true });
|
|
762
|
+
await wrapper.find('button').trigger('click');
|
|
763
|
+
await sleep(200);
|
|
764
|
+
const searchInput = wrapper.find('input.text-night');
|
|
765
|
+
await searchInput.setValue('New Item');
|
|
766
|
+
const createButton = wrapper.find('button.hover\\:text-primary-hover');
|
|
767
|
+
await createButton.trigger('click');
|
|
768
|
+
const pDropdownSelectCmp = wrapper.findComponent(PDropdownSelect);
|
|
769
|
+
expect(pDropdownSelectCmp.emitted()['update:modelValue']).toBeFalsy();
|
|
770
|
+
expect(wrapper.vm.$data.selected).toBe(null);
|
|
771
|
+
cleanup(wrapper);
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it('does not create object value when valueIsObject is true', async () => {
|
|
775
|
+
useVirtualizer.mockImplementation(() => createMockedVirtualizer(20));
|
|
776
|
+
const wrapper = createWrapper({ selected: null }, { searchable: true, creatable: true, valueIsObject: true });
|
|
777
|
+
await wrapper.find('button').trigger('click');
|
|
778
|
+
await sleep(200);
|
|
779
|
+
const searchInput = wrapper.find('input.text-night');
|
|
780
|
+
await searchInput.setValue('New Item');
|
|
781
|
+
const createButton = wrapper.find('button.hover\\:text-primary-hover');
|
|
782
|
+
await createButton.trigger('click');
|
|
783
|
+
const pDropdownSelectCmp = wrapper.findComponent(PDropdownSelect);
|
|
784
|
+
expect(pDropdownSelectCmp.emitted().create[0]).toEqual(['New Item']);
|
|
785
|
+
expect(pDropdownSelectCmp.emitted()['update:modelValue']).toBeFalsy();
|
|
786
|
+
expect(wrapper.vm.$data.selected).toBe(null);
|
|
787
|
+
cleanup(wrapper);
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it('emits create event with search value when using custom itemValue and itemText', async () => {
|
|
791
|
+
useVirtualizer.mockImplementation(() => createMockedVirtualizer(20));
|
|
792
|
+
const wrapper = createWrapper(
|
|
793
|
+
{ selected: null },
|
|
794
|
+
{
|
|
795
|
+
searchable: true,
|
|
796
|
+
creatable: true,
|
|
797
|
+
valueIsObject: true,
|
|
798
|
+
itemValue: 'customValue',
|
|
799
|
+
itemText: 'customText',
|
|
800
|
+
}
|
|
801
|
+
);
|
|
802
|
+
await wrapper.find('button').trigger('click');
|
|
803
|
+
await sleep(200);
|
|
804
|
+
const searchInput = wrapper.find('input.text-night');
|
|
805
|
+
await searchInput.setValue('New Item');
|
|
806
|
+
const createButton = wrapper.find('button.hover\\:text-primary-hover');
|
|
807
|
+
await createButton.trigger('click');
|
|
808
|
+
const pDropdownSelectCmp = wrapper.findComponent(PDropdownSelect);
|
|
809
|
+
expect(pDropdownSelectCmp.emitted().create[0]).toEqual(['New Item']);
|
|
810
|
+
expect(pDropdownSelectCmp.emitted()['update:modelValue']).toBeFalsy();
|
|
811
|
+
cleanup(wrapper);
|
|
812
|
+
});
|
|
813
|
+
});
|
|
669
814
|
});
|
|
@@ -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
|
+
};
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
}}
|
|
39
39
|
</div>
|
|
40
40
|
</slot>
|
|
41
|
-
<!--
|
|
41
|
+
<!-- Clear selection button -->
|
|
42
42
|
<button
|
|
43
|
-
v-if="clearable &&
|
|
43
|
+
v-if="clearable && selectedItems.length"
|
|
44
44
|
class="absolute top-1/2 flex -translate-y-1/2 items-center justify-center text-p-gray-40 hover:text-p-gray-60"
|
|
45
45
|
:class="[SIZES[size], CLEAR_BUTTON_SPACING[size]]"
|
|
46
46
|
aria-label="Clear selection"
|
|
@@ -148,7 +148,18 @@
|
|
|
148
148
|
</div>
|
|
149
149
|
</div>
|
|
150
150
|
<slot v-if="!computedItems.length" name="no-items">
|
|
151
|
-
<div :class="['flex items-center justify-center', SIZES[size]]">
|
|
151
|
+
<div :class="['flex items-center justify-center', SIZES[size]]">
|
|
152
|
+
<template v-if="creatable && search">
|
|
153
|
+
<button
|
|
154
|
+
class="hover:text-primary-hover flex items-center gap-2 font-medium text-p-blue-40"
|
|
155
|
+
@click="handleCreate"
|
|
156
|
+
>
|
|
157
|
+
<PIcon icon="fe:plus-circle" />
|
|
158
|
+
Add '{{ search }}'
|
|
159
|
+
</button>
|
|
160
|
+
</template>
|
|
161
|
+
<template v-else>No items found</template>
|
|
162
|
+
</div>
|
|
152
163
|
</slot>
|
|
153
164
|
</div>
|
|
154
165
|
</div>
|
|
@@ -197,7 +208,7 @@ defineSlots<{
|
|
|
197
208
|
item(props: { item: any; isItemSelected: boolean; itemTextSplit: string[] }): unknown;
|
|
198
209
|
}>();
|
|
199
210
|
|
|
200
|
-
const emit = defineEmits(['update:modelValue', 'select']);
|
|
211
|
+
const emit = defineEmits(['update:modelValue', 'select', 'create']);
|
|
201
212
|
|
|
202
213
|
const props = defineProps({
|
|
203
214
|
modelValue: {
|
|
@@ -241,14 +252,14 @@ const props = defineProps({
|
|
|
241
252
|
},
|
|
242
253
|
},
|
|
243
254
|
/**
|
|
244
|
-
* Set property of **items
|
|
255
|
+
* Set property of **items**'s text value
|
|
245
256
|
*/
|
|
246
257
|
itemText: {
|
|
247
258
|
type: String,
|
|
248
259
|
default: 'text',
|
|
249
260
|
},
|
|
250
261
|
/**
|
|
251
|
-
* Set property of **items
|
|
262
|
+
* Set property of **items**'s value - must be primitive.
|
|
252
263
|
*/
|
|
253
264
|
itemValue: {
|
|
254
265
|
type: [String, Number],
|
|
@@ -328,6 +339,13 @@ const props = defineProps({
|
|
|
328
339
|
type: String as PropType<string>,
|
|
329
340
|
default: 'disabled',
|
|
330
341
|
},
|
|
342
|
+
/**
|
|
343
|
+
* Enables the ability to create new items when no search results are found
|
|
344
|
+
*/
|
|
345
|
+
creatable: {
|
|
346
|
+
type: Boolean,
|
|
347
|
+
default: false,
|
|
348
|
+
},
|
|
331
349
|
});
|
|
332
350
|
|
|
333
351
|
// Async helpers
|
|
@@ -453,4 +471,9 @@ const onHide = () => {
|
|
|
453
471
|
destroyNavigationSvc();
|
|
454
472
|
(formControl.value?.querySelector('button') as HTMLElement)?.focus();
|
|
455
473
|
};
|
|
474
|
+
|
|
475
|
+
const handleCreate = () => {
|
|
476
|
+
emit('create', search.value);
|
|
477
|
+
dropdownShow.value = false;
|
|
478
|
+
};
|
|
456
479
|
</script>
|
|
@@ -299,20 +299,24 @@ export const useSelectList = (props: Props, inputSearch: InputSearch, virtualize
|
|
|
299
299
|
};
|
|
300
300
|
|
|
301
301
|
const clearAll = () => {
|
|
302
|
-
if (!props.multiple) return;
|
|
303
|
-
|
|
304
302
|
search.value = '';
|
|
305
303
|
|
|
306
304
|
// We cannot clear disabled items that are selected
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
305
|
+
if (props.multiple) {
|
|
306
|
+
const disabledItemsValues = internalItems.value
|
|
307
|
+
.filter((item) => isDisabled(item))
|
|
308
|
+
.map((item) => item[props.itemValue]);
|
|
310
309
|
|
|
311
|
-
|
|
310
|
+
const selectedItemsValues = internalValue.value;
|
|
312
311
|
|
|
313
|
-
|
|
312
|
+
const selectedDisabledItems = intersection(disabledItemsValues, selectedItemsValues);
|
|
314
313
|
|
|
315
|
-
|
|
314
|
+
emit('update:modelValue', toArrOfObjIfNeeded(selectedDisabledItems));
|
|
315
|
+
} else {
|
|
316
|
+
if (!isDisabled(selectedItems.value[0])) {
|
|
317
|
+
emit('update:modelValue', null);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
316
320
|
};
|
|
317
321
|
|
|
318
322
|
// Watch
|