@saooti/octopus-sdk 41.12.0-beta2 → 41.12.0-beta3
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/.claude/scheduled_tasks.lock +1 -0
- package/CHANGELOG.md +2 -0
- package/package.json +3 -3
- package/src/components/composable/form/useOctopusDropdown.ts +5 -2
- package/src/components/form/OctopusMultiselect.vue +86 -9
- package/src/components/form/OctopusSelect.vue +29 -2
- package/src/locale/de.json +1 -0
- package/src/locale/en.json +1 -0
- package/src/locale/es.json +1 -0
- package/src/locale/fr.json +1 -0
- package/src/locale/it.json +1 -0
- package/src/locale/sl.json +1 -0
- package/src/stores/class/general/organisation.ts +6 -0
- package/src/style/_variables.scss +2 -0
- package/src/style/bootstrap.scss +3 -2
- package/tests/components/form/OctopusMultiselect.spec.ts +298 -0
- package/tests/components/form/OctopusSelect.spec.ts +71 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"sessionId":"92d5c13f-8d38-4dc6-a66d-c7256640dc6f","pid":10553,"procStart":"32347","acquiredAt":1783406560303}
|
package/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
- Mise en commun de code entre `OctopusSelect` & `OctopusMultiselect`
|
|
16
16
|
- Ajustement de `OctopusMultiselect` pour fonctionner correctement avec des
|
|
17
17
|
objets identiques avec références différentes
|
|
18
|
+
- Ajout de nouvelles options à `OctopusMultiselect` pour permettre la saisie
|
|
19
|
+
de données et avoir une meilleure UX en cas de longue liste
|
|
18
20
|
- Ajout de nouvelles classes utilitaires dans le css
|
|
19
21
|
- Ajout de nouvelles options d'apparence pour `ClassicButton`
|
|
20
22
|
- Changement propriété principale de `ClassicButtonGroup` pour plus de cohérence
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saooti/octopus-sdk",
|
|
3
|
-
"version": "41.12.0-
|
|
3
|
+
"version": "41.12.0-beta3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Javascript SDK for using octopus",
|
|
6
6
|
"author": "Saooti",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"typescript-eslint": "^8.47.0",
|
|
52
52
|
"video.js": "^8.23.6",
|
|
53
53
|
"videojs-quality-selector-hls": "^1.1.1",
|
|
54
|
-
"vite": "^
|
|
54
|
+
"vite": "^8.0.16",
|
|
55
55
|
"vite-bundle-visualizer": "^1.2.1",
|
|
56
56
|
"vue-material-design-icons": "^5.3.1",
|
|
57
57
|
"vue-recaptcha": "^2.0.3",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"@stylistic/eslint-plugin": "*",
|
|
65
65
|
"@types/sockjs-client": "^1.5.4",
|
|
66
66
|
"@types/webpack-env": "^1.18.8",
|
|
67
|
-
"@vitejs/plugin-vue": "^
|
|
67
|
+
"@vitejs/plugin-vue": "^6.0.7",
|
|
68
68
|
"@vue/test-utils": "^2.4.6",
|
|
69
69
|
"eslint": "^9.39.1",
|
|
70
70
|
"eslint-plugin-vue": "10.9.1",
|
|
@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
|
|
|
4
4
|
|
|
5
5
|
export interface OctopusDropdownProps<T> {
|
|
6
6
|
options: T[];
|
|
7
|
-
optionLabel
|
|
7
|
+
optionLabel?: keyof T & string;
|
|
8
8
|
optionKey?: keyof T;
|
|
9
9
|
isDisabled?: boolean;
|
|
10
10
|
placeholder?: string;
|
|
@@ -51,7 +51,10 @@ export function useOctopusDropdown<T>(
|
|
|
51
51
|
const inputPlaceholder = computed(() => props.placeholder ?? t('Search'));
|
|
52
52
|
|
|
53
53
|
function getLabel(option: T): string {
|
|
54
|
-
|
|
54
|
+
if (props.optionLabel) {
|
|
55
|
+
return option[props.optionLabel] as string;
|
|
56
|
+
}
|
|
57
|
+
return option as unknown as string;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
function openDropdown(): void {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
:disabled="isDisabled"
|
|
42
42
|
@focus="openDropdown"
|
|
43
43
|
@input="handleInput"
|
|
44
|
+
@keydown.enter="handleCustomValueEnter"
|
|
44
45
|
>
|
|
45
46
|
<button
|
|
46
47
|
class="btn-transparent octopus-multiselect-chevron"
|
|
@@ -74,14 +75,20 @@
|
|
|
74
75
|
|
|
75
76
|
<div class="octopus-multiselect-options">
|
|
76
77
|
<ClassicCheckbox
|
|
77
|
-
v-for="(option, index) in
|
|
78
|
+
v-for="(option, index) in visibleOptions"
|
|
78
79
|
:key="index"
|
|
79
80
|
:text-init="isSelected(option)"
|
|
80
81
|
:label="getLabel(option)"
|
|
81
82
|
:is-disabled="isDisabled"
|
|
82
83
|
@update:text-init="toggleOption(option)"
|
|
83
84
|
/>
|
|
84
|
-
<
|
|
85
|
+
<template v-if="allowCustomValue && searchQuery.trim()">
|
|
86
|
+
<hr v-if="visibleOptions.length > 0">
|
|
87
|
+
<span class="text-indic px-2">
|
|
88
|
+
{{ t('Press Enter to add this value') }}
|
|
89
|
+
</span>
|
|
90
|
+
</template>
|
|
91
|
+
<span v-else-if="visibleOptions.length === 0" class="text-indic px-2">
|
|
85
92
|
{{ t('No elements found. Consider changing the search query.') }}
|
|
86
93
|
</span>
|
|
87
94
|
</div>
|
|
@@ -106,8 +113,16 @@ const props = defineProps<{
|
|
|
106
113
|
options: T[];
|
|
107
114
|
/** Key of each option object to use as the ID */
|
|
108
115
|
optionKey?: keyof T;
|
|
109
|
-
/** Key of each option object to use as the display label.
|
|
110
|
-
|
|
116
|
+
/** Key of each option object to use as the display label. Omit when `options` is a
|
|
117
|
+
* plain `string[]` — each string is used as its own label. Note: omitting this for
|
|
118
|
+
* an object array is a runtime mistake (not caught at compile time) and will render
|
|
119
|
+
* "[object Object]". */
|
|
120
|
+
optionLabel?: keyof T & string;
|
|
121
|
+
/** When true, pressing Enter in the search input adds the typed text as a new
|
|
122
|
+
* selected value, even if it doesn't match any option. Intended for use when
|
|
123
|
+
* options are plain strings (`optionLabel` omitted) — casting arbitrary typed
|
|
124
|
+
* text into an object-shaped T would not produce a valid option. */
|
|
125
|
+
allowCustomValue?: boolean;
|
|
111
126
|
/** Disables the field and all checkboxes when true. */
|
|
112
127
|
isDisabled?: boolean;
|
|
113
128
|
/** Placeholder shown in the input when no items are selected. Defaults to the translated "Search" string. */
|
|
@@ -118,6 +133,10 @@ const props = defineProps<{
|
|
|
118
133
|
noBorder?: boolean;
|
|
119
134
|
/** When true, hovering the closed field with overflow shows a tooltip listing all selected items. */
|
|
120
135
|
expandOnHover?: boolean;
|
|
136
|
+
/** When true, options selected at the moment the dropdown opens are moved to the
|
|
137
|
+
* top of the list. This is a snapshot taken at open time — it does not live-reorder
|
|
138
|
+
* while the dropdown stays open, only on the next closed→open transition. */
|
|
139
|
+
pullSelectedToTop?: boolean;
|
|
121
140
|
}>();
|
|
122
141
|
|
|
123
142
|
const emit = defineEmits<{
|
|
@@ -166,11 +185,35 @@ function updateDropdownPosition(): void {
|
|
|
166
185
|
}
|
|
167
186
|
const visibleCount = ref(2);
|
|
168
187
|
|
|
188
|
+
// Selection snapshot captured the instant the dropdown opens, used only to freeze the
|
|
189
|
+
// sort order when pullSelectedToTop is set — does not react to later `selected` changes
|
|
190
|
+
// while the dropdown stays open.
|
|
191
|
+
const pinnedSnapshot = ref<T[]>([]);
|
|
192
|
+
|
|
193
|
+
// Selected values not present in `options` — only populated when allowCustomValue is set,
|
|
194
|
+
// so a custom value typed via handleCustomValueEnter still shows (checked) in the dropdown.
|
|
195
|
+
const customSelectedOptions = computed<T[]>(() => {
|
|
196
|
+
if (!props.allowCustomValue) { return []; }
|
|
197
|
+
return (props.selected ?? []).filter((item) => !isInOptions(item));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const visibleOptions = computed<T[]>(() => {
|
|
201
|
+
const query = searchQuery.value.toLowerCase();
|
|
202
|
+
const filteredCustom = query
|
|
203
|
+
? customSelectedOptions.value.filter((option) => getLabel(option).toLowerCase().includes(query))
|
|
204
|
+
: customSelectedOptions.value;
|
|
205
|
+
const combined = [...displayedOptions.value, ...filteredCustom];
|
|
206
|
+
if (!props.pullSelectedToTop) {
|
|
207
|
+
return combined;
|
|
208
|
+
}
|
|
209
|
+
return [...combined.filter(isPinned), ...combined.filter((option) => !isPinned(option))];
|
|
210
|
+
});
|
|
211
|
+
|
|
169
212
|
const allSelected = computed(() => {
|
|
170
|
-
if (
|
|
213
|
+
if (visibleOptions.value.length === 0) {
|
|
171
214
|
return false;
|
|
172
215
|
}
|
|
173
|
-
return
|
|
216
|
+
return visibleOptions.value.every((option) => isSelected(option));
|
|
174
217
|
});
|
|
175
218
|
|
|
176
219
|
const hasSelected = computed(() => (props.selected?.length ?? 0) > 0);
|
|
@@ -195,6 +238,22 @@ function isSelected(option: T): boolean {
|
|
|
195
238
|
}
|
|
196
239
|
}
|
|
197
240
|
|
|
241
|
+
function isInOptions(option: T): boolean {
|
|
242
|
+
if (props.optionKey) {
|
|
243
|
+
return props.options.some((opt) => opt[props.optionKey] === option[props.optionKey]);
|
|
244
|
+
} else {
|
|
245
|
+
return props.options.includes(option);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isPinned(option: T): boolean {
|
|
250
|
+
if (props.optionKey) {
|
|
251
|
+
return pinnedSnapshot.value.some((s) => s[props.optionKey] === option[props.optionKey]);
|
|
252
|
+
} else {
|
|
253
|
+
return pinnedSnapshot.value.includes(option);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
198
257
|
function toggleOption(option: T): void {
|
|
199
258
|
const current = props.selected ?? [];
|
|
200
259
|
if (isSelected(option)) {
|
|
@@ -208,16 +267,27 @@ function toggleOption(option: T): void {
|
|
|
208
267
|
}
|
|
209
268
|
}
|
|
210
269
|
|
|
270
|
+
function handleCustomValueEnter(): void {
|
|
271
|
+
if (!props.allowCustomValue) { return; }
|
|
272
|
+
const value = searchQuery.value.trim();
|
|
273
|
+
if (!value) { return; }
|
|
274
|
+
const customOption = value as unknown as T;
|
|
275
|
+
if (!isSelected(customOption)) {
|
|
276
|
+
emit('update:selected', [...(props.selected ?? []), customOption]);
|
|
277
|
+
}
|
|
278
|
+
searchQuery.value = '';
|
|
279
|
+
}
|
|
280
|
+
|
|
211
281
|
function toggleAll(val: boolean): void {
|
|
212
282
|
const current = props.selected ?? [];
|
|
213
283
|
if (val) {
|
|
214
|
-
const toAdd =
|
|
284
|
+
const toAdd = visibleOptions.value.filter((option: T) => !isSelected(option));
|
|
215
285
|
emit('update:selected', [...current, ...toAdd]);
|
|
216
286
|
} else {
|
|
217
287
|
const key = props.optionKey;
|
|
218
288
|
emit('update:selected', current.filter((item: T) => key
|
|
219
|
-
? !
|
|
220
|
-
: !
|
|
289
|
+
? !visibleOptions.value.some((opt) => opt[key] === item[key])
|
|
290
|
+
: !visibleOptions.value.includes(item)
|
|
221
291
|
));
|
|
222
292
|
}
|
|
223
293
|
}
|
|
@@ -287,6 +357,7 @@ watch(() => props.selected, updateVisibleCount);
|
|
|
287
357
|
|
|
288
358
|
watch(isOpen, (val) => {
|
|
289
359
|
if (val) {
|
|
360
|
+
pinnedSnapshot.value = [...(props.selected ?? [])];
|
|
290
361
|
nextTick(updateDropdownPosition);
|
|
291
362
|
} else {
|
|
292
363
|
nextTick(updateVisibleCount);
|
|
@@ -403,4 +474,10 @@ watch(isOpen, (val) => {
|
|
|
403
474
|
padding: 0.25rem 0.5rem;
|
|
404
475
|
}
|
|
405
476
|
}
|
|
477
|
+
|
|
478
|
+
hr {
|
|
479
|
+
border-top: 1px solid var(--octopus-secondary);
|
|
480
|
+
border-bottom: none;
|
|
481
|
+
margin: 0;
|
|
482
|
+
}
|
|
406
483
|
</style>
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
>
|
|
52
52
|
<div class="octopus-select-options">
|
|
53
53
|
<button
|
|
54
|
-
v-for="(option, index) in
|
|
54
|
+
v-for="(option, index) in visibleOptions"
|
|
55
55
|
:key="index"
|
|
56
56
|
class="octopus-select-option"
|
|
57
57
|
:class="{ selected: isSelected(option) }"
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
>
|
|
60
60
|
{{ getLabel(option) }}
|
|
61
61
|
</button>
|
|
62
|
-
<span v-if="
|
|
62
|
+
<span v-if="visibleOptions.length === 0" class="text-indic px-2">
|
|
63
63
|
{{ t('No elements found. Consider changing the search query.') }}
|
|
64
64
|
</span>
|
|
65
65
|
</div>
|
|
@@ -93,6 +93,10 @@ const props = withDefaults(defineProps<{
|
|
|
93
93
|
noBorder?: boolean;
|
|
94
94
|
/** When true (default), clicking the already-selected option clears the selection. */
|
|
95
95
|
allowDeselect?: boolean;
|
|
96
|
+
/** When true, the option matching the current value is moved to the top of the list
|
|
97
|
+
* the moment the dropdown opens. This is a snapshot taken at open time — it does not
|
|
98
|
+
* live-reorder while the dropdown stays open, only on the next closed→open transition. */
|
|
99
|
+
pullSelectedToTop?: boolean;
|
|
96
100
|
}>(), {
|
|
97
101
|
label: undefined,
|
|
98
102
|
value: undefined,
|
|
@@ -144,8 +148,14 @@ function updateDropdownPosition(): void {
|
|
|
144
148
|
};
|
|
145
149
|
}
|
|
146
150
|
|
|
151
|
+
// Value snapshot captured the instant the dropdown opens, used only to freeze the
|
|
152
|
+
// sort order when pullSelectedToTop is set — does not react to later `value` changes
|
|
153
|
+
// while the dropdown stays open.
|
|
154
|
+
const pinnedValue = ref<T | undefined>(undefined);
|
|
155
|
+
|
|
147
156
|
watch(isOpen, (val) => {
|
|
148
157
|
if (val) {
|
|
158
|
+
pinnedValue.value = props.value;
|
|
149
159
|
nextTick(updateDropdownPosition);
|
|
150
160
|
}
|
|
151
161
|
});
|
|
@@ -175,6 +185,23 @@ function isSelected(option: T): boolean {
|
|
|
175
185
|
return toRaw(props.value as object) === toRaw(option as object);
|
|
176
186
|
}
|
|
177
187
|
|
|
188
|
+
function isPinned(option: T): boolean {
|
|
189
|
+
if (pinnedValue.value === undefined) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
if (props.optionKey) {
|
|
193
|
+
return pinnedValue.value[props.optionKey] === option[props.optionKey];
|
|
194
|
+
}
|
|
195
|
+
return toRaw(pinnedValue.value as object) === toRaw(option as object);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const visibleOptions = computed<T[]>(() => {
|
|
199
|
+
if (!props.pullSelectedToTop) {
|
|
200
|
+
return displayedOptions.value;
|
|
201
|
+
}
|
|
202
|
+
return [...displayedOptions.value.filter(isPinned), ...displayedOptions.value.filter((option) => !isPinned(option))];
|
|
203
|
+
});
|
|
204
|
+
|
|
178
205
|
function selectOption(option: T): void {
|
|
179
206
|
if (isSelected(option) && props.allowDeselect) {
|
|
180
207
|
emit('update:value', undefined);
|
package/src/locale/de.json
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"Emission image": "Bild zur Reihe",
|
|
32
32
|
"Emission name": "Titel der Reihe",
|
|
33
33
|
"No elements found. Consider changing the search query.": "Keine Ergebnisse. Bitte ändern Sie die Suchanfrage.",
|
|
34
|
+
"Press Enter to add this value": "Drücken Sie Enter, um diesen Wert hinzuzufügen",
|
|
34
35
|
"Podcast is not visible for listeners": "Podcast ist für Hörer verborgen",
|
|
35
36
|
"Validate": "Bestätigen",
|
|
36
37
|
"Invalidate": "Invalidate",
|
package/src/locale/en.json
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"Emission image": "Series image",
|
|
32
32
|
"Emission name": "Series title",
|
|
33
33
|
"No elements found. Consider changing the search query.": "No elements found. Consider changing the search query.",
|
|
34
|
+
"Press Enter to add this value": "Press Enter to add this value",
|
|
34
35
|
"Podcast is not visible for listeners": "Podcast is not visible for listeners",
|
|
35
36
|
"Validate": "Validate",
|
|
36
37
|
"Invalidate": "Invalidate",
|
package/src/locale/es.json
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"Emission image": "Imagen del programa",
|
|
32
32
|
"Emission name": "Título del programa",
|
|
33
33
|
"No elements found. Consider changing the search query.": "No hay resultados. Prueba con otros criterios de búsqueda.",
|
|
34
|
+
"Press Enter to add this value": "Pulsa Intro para añadir este valor",
|
|
34
35
|
"Podcast is not visible for listeners": "El público no puede visualizar este pódcast",
|
|
35
36
|
"Validate": "Validar",
|
|
36
37
|
"Invalidate": "Invalidar",
|
package/src/locale/fr.json
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"Emission subtitle": "Sous-titre de l'émission",
|
|
38
38
|
"No elements found. Consider changing the search query.":
|
|
39
39
|
"Aucun élement ne correspond à votre recherche",
|
|
40
|
+
"Press Enter to add this value": "Appuyez sur Entrée pour ajouter cette valeur",
|
|
40
41
|
"Podcast is not visible for listeners":
|
|
41
42
|
"L'épisode n'est pas disponible pour les auditeurs",
|
|
42
43
|
"Validate": "Valider",
|
package/src/locale/it.json
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"Emission image": "Immagine della serie",
|
|
31
31
|
"Emission name": "Titolo della serie",
|
|
32
32
|
"No elements found. Consider changing the search query.": "Non è stato trovato alcun elemento. Prova a cambiare i parametri di ricerca",
|
|
33
|
+
"Press Enter to add this value": "Premi Invio per aggiungere questo valore",
|
|
33
34
|
"Podcast is not visible for listeners": "Il podcast non è visibile agli ascoltatori",
|
|
34
35
|
"Validate": "Convalidare",
|
|
35
36
|
"Invalidate": "Invalidate",
|
package/src/locale/sl.json
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"Emission image": "Slika oddaje",
|
|
32
32
|
"Emission name": "Naslov oddaje",
|
|
33
33
|
"No elements found. Consider changing the search query.": "Ni zadetkov. Spremenite poizvedbo",
|
|
34
|
+
"Press Enter to add this value": "Pritisnite Enter, da dodate to vrednost",
|
|
34
35
|
"Podcast is not visible for listeners": "Za poslušalce skrit podkast",
|
|
35
36
|
"Validate": "Potrdi",
|
|
36
37
|
"Invalidate": "Invalidate",
|
|
@@ -24,12 +24,18 @@ export type OrganisationAttributes = {
|
|
|
24
24
|
PRIVATE?: string;
|
|
25
25
|
/** Live recordings enabled */
|
|
26
26
|
'live.active'?: boolean;
|
|
27
|
+
/** Videos enabled */
|
|
28
|
+
'video.active'?: boolean;
|
|
27
29
|
/**
|
|
28
30
|
* List of rubriquage **names**.
|
|
29
31
|
* The rubriques from these rubriquage are stored in stats and can be queried
|
|
30
32
|
* upon.
|
|
31
33
|
*/
|
|
32
34
|
rubriquage4MMIdentifier?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Enable individualized stats
|
|
37
|
+
*/
|
|
38
|
+
nominal_analytic?: string;
|
|
33
39
|
};
|
|
34
40
|
|
|
35
41
|
export interface Organisation {
|
|
@@ -49,6 +49,8 @@
|
|
|
49
49
|
--octopus-btn-play-bg: var(--octopus-primary-less-transparent);
|
|
50
50
|
--octopus-btn-play-fg: white;
|
|
51
51
|
--octopus-btn-play-radius: var(--octopus-border-radius);
|
|
52
|
+
--octopus-btn-social-bg: var(--octopus-secondary);
|
|
53
|
+
--octopus-btn-social-fg: var(--octopus-primary);
|
|
52
54
|
|
|
53
55
|
// Player
|
|
54
56
|
// Color for the transcript background
|
package/src/style/bootstrap.scss
CHANGED
|
@@ -154,7 +154,7 @@ input:not([class^="vs__"]), button:not([class^="vs__"]), select:not([class^="vs_
|
|
|
154
154
|
align-items: center;
|
|
155
155
|
justify-content: center;
|
|
156
156
|
background: var(--octopus-btn-primary-bg);
|
|
157
|
-
border: 1px solid var(--octopus-primary);
|
|
157
|
+
border: 1px solid var(--octopus-btn-primary-bg);
|
|
158
158
|
border-radius: var(--octopus-border-radius) !important;
|
|
159
159
|
color: var(--octopus-btn-primary-fg) !important;
|
|
160
160
|
font-weight: 500;
|
|
@@ -234,7 +234,8 @@ input:not([class^="vs__"]), button:not([class^="vs__"]), select:not([class^="vs_
|
|
|
234
234
|
width: 2.5rem !important;
|
|
235
235
|
height: 2.5rem !important;
|
|
236
236
|
padding: 0.5rem;
|
|
237
|
-
color : var(--octopus-
|
|
237
|
+
color : var(--octopus-btn-social-fg);
|
|
238
|
+
background-color : var(--octopus-btn-social-bg);
|
|
238
239
|
flex-shrink: 0;
|
|
239
240
|
border-radius: 50% !important;
|
|
240
241
|
|
|
@@ -3,6 +3,7 @@ import '@tests/mocks/i18n';
|
|
|
3
3
|
import OctopusMultiselect from '@/components/form/OctopusMultiselect.vue';
|
|
4
4
|
import { DOMWrapper, type VueWrapper } from '@vue/test-utils';
|
|
5
5
|
import { mount } from '@tests/utils';
|
|
6
|
+
import { nextTick } from 'vue';
|
|
6
7
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
8
|
|
|
8
9
|
const options = [
|
|
@@ -16,6 +17,20 @@ function getDropdown(): Element | null {
|
|
|
16
17
|
return document.body.querySelector('.octopus-multiselect-dropdown');
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
function getOptionLabels(): (string | undefined)[] {
|
|
21
|
+
return Array.from(document.body.querySelectorAll('.octopus-multiselect-options label'))
|
|
22
|
+
.map((l) => l.textContent?.trim());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Simulates a click outside the component to trigger onClickOutside's closeDropdown.
|
|
26
|
+
async function clickOutside(): Promise<void> {
|
|
27
|
+
const outsideElement = document.createElement('div');
|
|
28
|
+
document.body.appendChild(outsideElement);
|
|
29
|
+
outsideElement.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
30
|
+
await nextTick();
|
|
31
|
+
document.body.removeChild(outsideElement);
|
|
32
|
+
}
|
|
33
|
+
|
|
19
34
|
describe('OctopusMultiselect', () => {
|
|
20
35
|
// The dropdown teleports to .octopus-app, which is normally the app root (see App.vue).
|
|
21
36
|
// It doesn't exist in the test DOM, so it must be created before mount.
|
|
@@ -204,4 +219,287 @@ describe('OctopusMultiselect', () => {
|
|
|
204
219
|
await new DOMWrapper(allCheckbox).trigger('input');
|
|
205
220
|
expect(wrapper.emitted('update:selected')?.[0]).toEqual([[]]);
|
|
206
221
|
});
|
|
222
|
+
|
|
223
|
+
describe('pullSelectedToTop', () => {
|
|
224
|
+
it('does not reorder options when pullSelectedToTop is not set', async () => {
|
|
225
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
226
|
+
props: { options, optionLabel: 'name', selected: [options[1]] },
|
|
227
|
+
});
|
|
228
|
+
await wrapper.find('input').trigger('focus');
|
|
229
|
+
expect(getOptionLabels()).toEqual(['Alpha', 'Beta', 'Gamma']);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('moves the selected option to the top when pullSelectedToTop is true', async () => {
|
|
233
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
234
|
+
props: { options, optionLabel: 'name', selected: [options[1]], pullSelectedToTop: true },
|
|
235
|
+
});
|
|
236
|
+
await wrapper.find('input').trigger('focus');
|
|
237
|
+
expect(getOptionLabels()).toEqual(['Beta', 'Alpha', 'Gamma']);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('preserves relative order within pinned and unpinned groups', async () => {
|
|
241
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
242
|
+
props: { options, optionLabel: 'name', selected: [options[2], options[0]], pullSelectedToTop: true },
|
|
243
|
+
});
|
|
244
|
+
await wrapper.find('input').trigger('focus');
|
|
245
|
+
expect(getOptionLabels()).toEqual(['Alpha', 'Gamma', 'Beta']);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('does not reorder while the dropdown stays open as selection changes', async () => {
|
|
249
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
250
|
+
props: { options, optionLabel: 'name', selected: [], pullSelectedToTop: true },
|
|
251
|
+
});
|
|
252
|
+
await wrapper.find('input').trigger('focus');
|
|
253
|
+
const optionCheckboxes = document.body.querySelectorAll('.octopus-multiselect-options input[type="checkbox"]');
|
|
254
|
+
await new DOMWrapper(optionCheckboxes[2] as Element).trigger('input');
|
|
255
|
+
await wrapper.setProps({ selected: [options[2]] });
|
|
256
|
+
expect(getOptionLabels()).toEqual(['Alpha', 'Beta', 'Gamma']);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('re-pins using the latest selection after closing and reopening', async () => {
|
|
260
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
261
|
+
props: { options, optionLabel: 'name', selected: [options[2]], pullSelectedToTop: true },
|
|
262
|
+
});
|
|
263
|
+
await wrapper.find('input').trigger('focus');
|
|
264
|
+
await clickOutside();
|
|
265
|
+
await wrapper.find('input').trigger('focus');
|
|
266
|
+
expect(getOptionLabels()).toEqual(['Gamma', 'Alpha', 'Beta']);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('pins by optionKey even when selected objects are different references', async () => {
|
|
270
|
+
const selected = [{ id: 2, name: 'Beta' }];
|
|
271
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
272
|
+
props: { options, optionLabel: 'name', optionKey: 'id', selected, pullSelectedToTop: true },
|
|
273
|
+
});
|
|
274
|
+
await wrapper.find('input').trigger('focus');
|
|
275
|
+
expect(getOptionLabels()).toEqual(['Beta', 'Alpha', 'Gamma']);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('still respects the active search filter when pinning', async () => {
|
|
279
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
280
|
+
props: { options, optionLabel: 'name', selected: [options[2]], pullSelectedToTop: true },
|
|
281
|
+
});
|
|
282
|
+
await wrapper.find('input').trigger('focus');
|
|
283
|
+
await wrapper.find('input').setValue('be');
|
|
284
|
+
await wrapper.find('input').trigger('input');
|
|
285
|
+
expect(getOptionLabels()).toEqual(['Beta']);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('keeps a selected custom value pinned alongside allowCustomValue', async () => {
|
|
289
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
290
|
+
props: {
|
|
291
|
+
options: ['Alpha', 'Beta', 'Gamma'],
|
|
292
|
+
selected: ['Delta'],
|
|
293
|
+
allowCustomValue: true,
|
|
294
|
+
pullSelectedToTop: true,
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
await wrapper.find('input').trigger('focus');
|
|
298
|
+
expect(getOptionLabels()).toEqual(['Delta', 'Alpha', 'Beta', 'Gamma']);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('with string options', () => {
|
|
303
|
+
const stringOptions = ['Alpha', 'Beta', 'Gamma'];
|
|
304
|
+
|
|
305
|
+
it('uses each string as its own label without optionLabel', async () => {
|
|
306
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
307
|
+
props: { options: stringOptions },
|
|
308
|
+
});
|
|
309
|
+
await wrapper.find('input').trigger('focus');
|
|
310
|
+
const labels = document.body.querySelectorAll('.octopus-multiselect-options label');
|
|
311
|
+
expect(Array.from(labels).map((l) => l.textContent?.trim())).toEqual(stringOptions);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('emits update:selected when toggling a string option', async () => {
|
|
315
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
316
|
+
props: { options: stringOptions, selected: [] },
|
|
317
|
+
});
|
|
318
|
+
await wrapper.find('input').trigger('focus');
|
|
319
|
+
const optionCheckboxes = document.body.querySelectorAll('.octopus-multiselect-options input[type="checkbox"]');
|
|
320
|
+
await new DOMWrapper(optionCheckboxes[0] as Element).trigger('input');
|
|
321
|
+
expect(wrapper.emitted('update:selected')?.[0]).toEqual([['Alpha']]);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('deselects an already-selected string option', async () => {
|
|
325
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
326
|
+
props: { options: stringOptions, selected: ['Alpha'] },
|
|
327
|
+
});
|
|
328
|
+
await wrapper.find('input').trigger('focus');
|
|
329
|
+
const optionCheckboxes = document.body.querySelectorAll('.octopus-multiselect-options input[type="checkbox"]');
|
|
330
|
+
await new DOMWrapper(optionCheckboxes[0] as Element).trigger('input');
|
|
331
|
+
expect(wrapper.emitted('update:selected')?.[0]).toEqual([[]]);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('filters string options locally by search query', async () => {
|
|
335
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
336
|
+
props: { options: stringOptions },
|
|
337
|
+
});
|
|
338
|
+
await wrapper.find('input').trigger('focus');
|
|
339
|
+
await wrapper.find('input').setValue('al');
|
|
340
|
+
await wrapper.find('input').trigger('input');
|
|
341
|
+
const checkboxes = document.body.querySelectorAll('.octopus-multiselect-options input[type="checkbox"]');
|
|
342
|
+
expect(checkboxes).toHaveLength(1);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('toggleAll selects all displayed string options', async () => {
|
|
346
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
347
|
+
props: { options: stringOptions, selected: [] },
|
|
348
|
+
});
|
|
349
|
+
await wrapper.find('input').trigger('focus');
|
|
350
|
+
const allCheckbox = document.body.querySelector('.octopus-multiselect-dropdown > .octopus-form-item input[type="checkbox"]')!;
|
|
351
|
+
await new DOMWrapper(allCheckbox).trigger('input');
|
|
352
|
+
expect(wrapper.emitted('update:selected')?.[0]).toEqual([stringOptions]);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('shows selected strings in the selection display', async () => {
|
|
356
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
357
|
+
props: { options: stringOptions, selected: ['Alpha', 'Beta'] },
|
|
358
|
+
});
|
|
359
|
+
expect(wrapper.find('.octopus-multiselect-selection-text').text()).toBe('Alpha, Beta');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('adds typed value as a new selection on Enter when allowCustomValue is true', async () => {
|
|
363
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
364
|
+
props: { options: stringOptions, selected: [], allowCustomValue: true },
|
|
365
|
+
});
|
|
366
|
+
await wrapper.find('input').trigger('focus');
|
|
367
|
+
await wrapper.find('input').setValue('Delta');
|
|
368
|
+
await wrapper.find('input').trigger('keydown.enter');
|
|
369
|
+
expect(wrapper.emitted('update:selected')?.[0]).toEqual([['Delta']]);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('does not add typed value on Enter when allowCustomValue is not set', async () => {
|
|
373
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
374
|
+
props: { options: stringOptions, selected: [] },
|
|
375
|
+
});
|
|
376
|
+
await wrapper.find('input').trigger('focus');
|
|
377
|
+
await wrapper.find('input').setValue('Delta');
|
|
378
|
+
await wrapper.find('input').trigger('keydown.enter');
|
|
379
|
+
expect(wrapper.emitted('update:selected')).toBeUndefined();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('does not add an empty or whitespace-only value on Enter', async () => {
|
|
383
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
384
|
+
props: { options: stringOptions, selected: [], allowCustomValue: true },
|
|
385
|
+
});
|
|
386
|
+
await wrapper.find('input').trigger('focus');
|
|
387
|
+
await wrapper.find('input').setValue(' ');
|
|
388
|
+
await wrapper.find('input').trigger('keydown.enter');
|
|
389
|
+
expect(wrapper.emitted('update:selected')).toBeUndefined();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('does not duplicate an already-selected value on Enter', async () => {
|
|
393
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
394
|
+
props: { options: stringOptions, selected: ['Alpha'], allowCustomValue: true },
|
|
395
|
+
});
|
|
396
|
+
await wrapper.find('input').trigger('focus');
|
|
397
|
+
await wrapper.find('input').setValue('Alpha');
|
|
398
|
+
await wrapper.find('input').trigger('keydown.enter');
|
|
399
|
+
expect(wrapper.emitted('update:selected')).toBeUndefined();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('clears the search input after adding a custom value on Enter', async () => {
|
|
403
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
404
|
+
props: { options: stringOptions, selected: [], allowCustomValue: true },
|
|
405
|
+
});
|
|
406
|
+
await wrapper.find('input').trigger('focus');
|
|
407
|
+
await wrapper.find('input').setValue('Delta');
|
|
408
|
+
await wrapper.find('input').trigger('keydown.enter');
|
|
409
|
+
expect((wrapper.find('input').element as HTMLInputElement).value).toBe('');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('shows a previously added custom value as a checked option in the dropdown', async () => {
|
|
413
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
414
|
+
props: { options: stringOptions, selected: ['Delta'], allowCustomValue: true },
|
|
415
|
+
});
|
|
416
|
+
await wrapper.find('input').trigger('focus');
|
|
417
|
+
const labels = Array.from(document.body.querySelectorAll('.octopus-multiselect-options label'))
|
|
418
|
+
.map((l) => l.textContent?.trim());
|
|
419
|
+
expect(labels).toContain('Delta');
|
|
420
|
+
const checkboxes = document.body.querySelectorAll('.octopus-multiselect-options input[type="checkbox"]');
|
|
421
|
+
expect((checkboxes[labels.indexOf('Delta')] as HTMLInputElement).checked).toBe(true);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('shows the checkbox for a value right after it is added via Enter', async () => {
|
|
425
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
426
|
+
props: { options: stringOptions, selected: [], allowCustomValue: true },
|
|
427
|
+
});
|
|
428
|
+
await wrapper.find('input').trigger('focus');
|
|
429
|
+
await wrapper.find('input').setValue('Delta');
|
|
430
|
+
await wrapper.find('input').trigger('keydown.enter');
|
|
431
|
+
const emittedSelected = wrapper.emitted('update:selected')?.[0]?.[0] as string[];
|
|
432
|
+
await wrapper.setProps({ selected: emittedSelected });
|
|
433
|
+
const labels = Array.from(document.body.querySelectorAll('.octopus-multiselect-options label'))
|
|
434
|
+
.map((l) => l.textContent?.trim());
|
|
435
|
+
expect(labels).toContain('Delta');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('does not show out-of-options selected values in the dropdown when allowCustomValue is not set', async () => {
|
|
439
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
440
|
+
props: { options: stringOptions, selected: ['Delta'] },
|
|
441
|
+
});
|
|
442
|
+
await wrapper.find('input').trigger('focus');
|
|
443
|
+
const labels = Array.from(document.body.querySelectorAll('.octopus-multiselect-options label'))
|
|
444
|
+
.map((l) => l.textContent?.trim());
|
|
445
|
+
expect(labels).not.toContain('Delta');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('deselects a custom value when its checkbox in the dropdown is toggled off', async () => {
|
|
449
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
450
|
+
props: { options: stringOptions, selected: ['Alpha', 'Delta'], allowCustomValue: true },
|
|
451
|
+
});
|
|
452
|
+
await wrapper.find('input').trigger('focus');
|
|
453
|
+
const checkboxes = document.body.querySelectorAll('.octopus-multiselect-options input[type="checkbox"]');
|
|
454
|
+
// visibleOptions = [Alpha, Beta, Gamma, Delta] — Delta is the appended custom entry
|
|
455
|
+
await new DOMWrapper(checkboxes[3] as Element).trigger('input');
|
|
456
|
+
expect(wrapper.emitted('update:selected')?.[0]).toEqual([['Alpha']]);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('toggleAll includes an already-selected custom value shown in the dropdown', async () => {
|
|
460
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
461
|
+
props: { options: stringOptions, selected: ['Delta'], allowCustomValue: true },
|
|
462
|
+
});
|
|
463
|
+
await wrapper.find('input').trigger('focus');
|
|
464
|
+
const allCheckbox = document.body.querySelector('.octopus-multiselect-dropdown > .octopus-form-item input[type="checkbox"]')!;
|
|
465
|
+
await new DOMWrapper(allCheckbox).trigger('input');
|
|
466
|
+
const emitted = wrapper.emitted('update:selected')?.[0]?.[0] as string[];
|
|
467
|
+
expect(emitted).toHaveLength(4);
|
|
468
|
+
expect(emitted).toEqual(expect.arrayContaining(['Alpha', 'Beta', 'Gamma', 'Delta']));
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('shows a hint to press Enter when allowCustomValue is true and text is typed', async () => {
|
|
472
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
473
|
+
props: { options: stringOptions, allowCustomValue: true },
|
|
474
|
+
});
|
|
475
|
+
await wrapper.find('input').trigger('focus');
|
|
476
|
+
await wrapper.find('input').setValue('Delta');
|
|
477
|
+
await wrapper.find('input').trigger('input');
|
|
478
|
+
const hints = Array.from(document.body.querySelectorAll('.octopus-multiselect-options .text-indic'))
|
|
479
|
+
.map((el) => el.textContent?.trim());
|
|
480
|
+
expect(hints).toContain('Press Enter to add this value');
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('does not show the Enter hint when allowCustomValue is not set', async () => {
|
|
484
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
485
|
+
props: { options: stringOptions },
|
|
486
|
+
});
|
|
487
|
+
await wrapper.find('input').trigger('focus');
|
|
488
|
+
await wrapper.find('input').setValue('Delta');
|
|
489
|
+
await wrapper.find('input').trigger('input');
|
|
490
|
+
const hints = Array.from(document.body.querySelectorAll('.octopus-multiselect-options .text-indic'))
|
|
491
|
+
.map((el) => el.textContent?.trim());
|
|
492
|
+
expect(hints).not.toContain('Press Enter to add this value');
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('does not show the Enter hint when the search input is empty', async () => {
|
|
496
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
497
|
+
props: { options: stringOptions, allowCustomValue: true },
|
|
498
|
+
});
|
|
499
|
+
await wrapper.find('input').trigger('focus');
|
|
500
|
+
const hints = Array.from(document.body.querySelectorAll('.octopus-multiselect-options .text-indic'))
|
|
501
|
+
.map((el) => el.textContent?.trim());
|
|
502
|
+
expect(hints).not.toContain('Press Enter to add this value');
|
|
503
|
+
});
|
|
504
|
+
});
|
|
207
505
|
});
|
|
@@ -3,6 +3,7 @@ import '@tests/mocks/i18n';
|
|
|
3
3
|
import OctopusSelect from '@/components/form/OctopusSelect.vue';
|
|
4
4
|
import { DOMWrapper, type VueWrapper } from '@vue/test-utils';
|
|
5
5
|
import { mount } from '@tests/utils';
|
|
6
|
+
import { nextTick } from 'vue';
|
|
6
7
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
7
8
|
|
|
8
9
|
const options = [
|
|
@@ -16,6 +17,20 @@ function getDropdown(): Element | null {
|
|
|
16
17
|
return document.body.querySelector('.octopus-select-dropdown');
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
function getOptionLabels(): string[] {
|
|
21
|
+
return Array.from(document.body.querySelectorAll('.octopus-select-option'))
|
|
22
|
+
.map((el) => el.textContent?.trim() ?? '');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Simulates a click outside the component to trigger onClickOutside's closeDropdown.
|
|
26
|
+
async function clickOutside(): Promise<void> {
|
|
27
|
+
const outsideElement = document.createElement('div');
|
|
28
|
+
document.body.appendChild(outsideElement);
|
|
29
|
+
outsideElement.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
30
|
+
await nextTick();
|
|
31
|
+
document.body.removeChild(outsideElement);
|
|
32
|
+
}
|
|
33
|
+
|
|
19
34
|
describe('OctopusSelect', () => {
|
|
20
35
|
// The dropdown teleports to .octopus-app, which is normally the app root (see App.vue).
|
|
21
36
|
// It doesn't exist in the test DOM, so it must be created before mount.
|
|
@@ -165,4 +180,60 @@ describe('OctopusSelect', () => {
|
|
|
165
180
|
const optionButtons = document.body.querySelectorAll('.octopus-select-option');
|
|
166
181
|
expect(optionButtons[0].classList).toContain('selected');
|
|
167
182
|
});
|
|
183
|
+
|
|
184
|
+
describe('pullSelectedToTop', () => {
|
|
185
|
+
it('does not reorder options when pullSelectedToTop is not set', async () => {
|
|
186
|
+
wrapper = await mount(OctopusSelect, {
|
|
187
|
+
props: { options, optionLabel: 'name', value: options[1] },
|
|
188
|
+
});
|
|
189
|
+
await wrapper.find('input').trigger('focus');
|
|
190
|
+
expect(getOptionLabels()).toEqual(['Alpha', 'Beta', 'Gamma']);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('moves the selected option to the top when pullSelectedToTop is true', async () => {
|
|
194
|
+
wrapper = await mount(OctopusSelect, {
|
|
195
|
+
props: { options, optionLabel: 'name', value: options[1], pullSelectedToTop: true },
|
|
196
|
+
});
|
|
197
|
+
await wrapper.find('input').trigger('focus');
|
|
198
|
+
expect(getOptionLabels()).toEqual(['Beta', 'Alpha', 'Gamma']);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('does not reorder while the dropdown stays open as the value changes', async () => {
|
|
202
|
+
wrapper = await mount(OctopusSelect, {
|
|
203
|
+
props: { options, optionLabel: 'name', pullSelectedToTop: true },
|
|
204
|
+
});
|
|
205
|
+
await wrapper.find('input').trigger('focus');
|
|
206
|
+
await wrapper.setProps({ value: options[2] });
|
|
207
|
+
expect(getOptionLabels()).toEqual(['Alpha', 'Beta', 'Gamma']);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('re-pins using the latest value after closing and reopening', async () => {
|
|
211
|
+
wrapper = await mount(OctopusSelect, {
|
|
212
|
+
props: { options, optionLabel: 'name', value: options[2], pullSelectedToTop: true },
|
|
213
|
+
});
|
|
214
|
+
await wrapper.find('input').trigger('focus');
|
|
215
|
+
await clickOutside();
|
|
216
|
+
await wrapper.find('input').trigger('focus');
|
|
217
|
+
expect(getOptionLabels()).toEqual(['Gamma', 'Alpha', 'Beta']);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('pins by optionKey even when the value is a different reference', async () => {
|
|
221
|
+
const value = { id: 2, name: 'Beta' };
|
|
222
|
+
wrapper = await mount(OctopusSelect, {
|
|
223
|
+
props: { options, optionLabel: 'name', optionKey: 'id', value, pullSelectedToTop: true },
|
|
224
|
+
});
|
|
225
|
+
await wrapper.find('input').trigger('focus');
|
|
226
|
+
expect(getOptionLabels()).toEqual(['Beta', 'Alpha', 'Gamma']);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('still respects the active search filter when pinning', async () => {
|
|
230
|
+
wrapper = await mount(OctopusSelect, {
|
|
231
|
+
props: { options, optionLabel: 'name', value: options[2], pullSelectedToTop: true },
|
|
232
|
+
});
|
|
233
|
+
await wrapper.find('input').trigger('focus');
|
|
234
|
+
await wrapper.find('input').setValue('be');
|
|
235
|
+
await wrapper.find('input').trigger('input');
|
|
236
|
+
expect(getOptionLabels()).toEqual(['Beta']);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
168
239
|
});
|