@saooti/octopus-sdk 41.12.0-beta → 41.12.0-beta2
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/CHANGELOG.md +1 -0
- package/package.json +1 -1
- package/src/components/composable/form/useOctopusDropdown.ts +8 -3
- package/src/components/display/podcasts/PodcastPlayButton.vue +3 -3
- package/src/components/form/OctopusMultiselect.vue +73 -42
- package/src/components/form/OctopusSelect.vue +89 -48
- package/src/components/misc/modal/ClassicModal.vue +2 -0
- package/src/components/misc/player/elements/PlayerPlayButton.vue +22 -18
- package/src/stores/ParamSdkStore.ts +1 -0
- package/src/stores/class/general/organisation.ts +6 -0
- package/src/stores/class/rubrique/rubrique.ts +1 -1
- package/src/style/_variables.scss +11 -0
- package/src/style/bootstrap.scss +2 -2
- package/tests/components/form/OctopusMultiselect.spec.ts +63 -39
- package/tests/components/form/OctopusSelect.spec.ts +57 -32
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
- Ajout de nouvelles classes utilitaires dans le css
|
|
19
19
|
- Ajout de nouvelles options d'apparence pour `ClassicButton`
|
|
20
20
|
- Changement propriété principale de `ClassicButtonGroup` pour plus de cohérence
|
|
21
|
+
- Ajout de variables CSS pour configurer l'aspect de divers boutons
|
|
21
22
|
|
|
22
23
|
**Fixes**
|
|
23
24
|
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { computed, getCurrentInstance, nextTick, ref } from 'vue';
|
|
2
|
-
import { onClickOutside } from '@vueuse/core';
|
|
2
|
+
import { type MaybeElementRef, onClickOutside } from '@vueuse/core';
|
|
3
3
|
import { useI18n } from 'vue-i18n';
|
|
4
4
|
|
|
5
5
|
export interface OctopusDropdownProps<T> {
|
|
@@ -13,7 +13,12 @@ export interface OctopusDropdownProps<T> {
|
|
|
13
13
|
export function useOctopusDropdown<T>(
|
|
14
14
|
props: OctopusDropdownProps<T>,
|
|
15
15
|
emitSearch: (query: string) => void,
|
|
16
|
-
idPrefix: string = 'octopus-dropdown'
|
|
16
|
+
idPrefix: string = 'octopus-dropdown',
|
|
17
|
+
// Extra elements to exclude from the click-outside check.
|
|
18
|
+
// Needed when the dropdown is teleported outside containerRef (e.g. to body):
|
|
19
|
+
// without this, clicking inside the teleported dropdown triggers closeDropdown
|
|
20
|
+
// because it is no longer a DOM descendant of containerRef.
|
|
21
|
+
ignore?: MaybeElementRef[]
|
|
17
22
|
) {
|
|
18
23
|
const { t } = useI18n();
|
|
19
24
|
const instance = getCurrentInstance();
|
|
@@ -78,7 +83,7 @@ export function useOctopusDropdown<T>(
|
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
onClickOutside(containerRef, closeDropdown);
|
|
86
|
+
onClickOutside(containerRef, closeDropdown, ignore?.length ? { ignore } : undefined);
|
|
82
87
|
|
|
83
88
|
return {
|
|
84
89
|
searchQuery,
|
|
@@ -359,15 +359,15 @@ async function play(isVideo: boolean): Promise<void> {
|
|
|
359
359
|
left: 0;
|
|
360
360
|
font-size: 1rem;
|
|
361
361
|
color: white;
|
|
362
|
-
background-color: var(--octopus-
|
|
363
|
-
border-radius: var(--octopus-
|
|
362
|
+
background-color: var(--octopus-btn-play-bg);
|
|
363
|
+
border-radius: var(--octopus-btn-play-radius);
|
|
364
364
|
|
|
365
365
|
@media (width <= 960px) {
|
|
366
366
|
font-size: 0.8rem;
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
button{
|
|
370
|
-
color:
|
|
370
|
+
color: var(--octopus-btn-play-fg);
|
|
371
371
|
background-color: transparent;
|
|
372
372
|
border: 0;
|
|
373
373
|
display: flex;
|
|
@@ -58,33 +58,40 @@
|
|
|
58
58
|
{{ allLabelsText }}
|
|
59
59
|
</div>
|
|
60
60
|
|
|
61
|
-
<
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<div class="octopus-multiselect-options">
|
|
61
|
+
<Teleport to=".octopus-app">
|
|
62
|
+
<div
|
|
63
|
+
v-if="isOpen"
|
|
64
|
+
ref="dropdownRef"
|
|
65
|
+
class="octopus-multiselect-dropdown"
|
|
66
|
+
:style="dropdownStyle"
|
|
67
|
+
>
|
|
70
68
|
<ClassicCheckbox
|
|
71
|
-
|
|
72
|
-
:
|
|
73
|
-
:text-init="isSelected(option)"
|
|
74
|
-
:label="getLabel(option)"
|
|
69
|
+
:text-init="allSelected"
|
|
70
|
+
:label="selectAllText ?? t('All')"
|
|
75
71
|
:is-disabled="isDisabled"
|
|
76
|
-
@update:text-init="
|
|
72
|
+
@update:text-init="toggleAll"
|
|
77
73
|
/>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
|
|
75
|
+
<div class="octopus-multiselect-options">
|
|
76
|
+
<ClassicCheckbox
|
|
77
|
+
v-for="(option, index) in displayedOptions"
|
|
78
|
+
:key="index"
|
|
79
|
+
:text-init="isSelected(option)"
|
|
80
|
+
:label="getLabel(option)"
|
|
81
|
+
:is-disabled="isDisabled"
|
|
82
|
+
@update:text-init="toggleOption(option)"
|
|
83
|
+
/>
|
|
84
|
+
<span v-if="displayedOptions.length === 0" class="text-indic px-2">
|
|
85
|
+
{{ t('No elements found. Consider changing the search query.') }}
|
|
86
|
+
</span>
|
|
87
|
+
</div>
|
|
81
88
|
</div>
|
|
82
|
-
</
|
|
89
|
+
</Teleport>
|
|
83
90
|
</div>
|
|
84
91
|
</template>
|
|
85
92
|
|
|
86
93
|
<script setup lang="ts" generic="T">
|
|
87
|
-
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
|
94
|
+
import { type CSSProperties, computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
|
88
95
|
import { useI18n } from 'vue-i18n';
|
|
89
96
|
import ChevronDownIcon from 'vue-material-design-icons/ChevronDown.vue';
|
|
90
97
|
import ClassicCheckbox from './ClassicCheckbox.vue';
|
|
@@ -123,6 +130,10 @@ const emit = defineEmits<{
|
|
|
123
130
|
|
|
124
131
|
const { t } = useI18n();
|
|
125
132
|
|
|
133
|
+
// Ref on the teleported dropdown div — passed as ignored element to useOctopusDropdown
|
|
134
|
+
// so clicks inside the dropdown don't trigger the click-outside handler.
|
|
135
|
+
const dropdownRef = ref<HTMLElement | null>(null);
|
|
136
|
+
|
|
126
137
|
const {
|
|
127
138
|
searchQuery,
|
|
128
139
|
isOpen,
|
|
@@ -136,9 +147,23 @@ const {
|
|
|
136
147
|
openDropdown,
|
|
137
148
|
toggleDropdown,
|
|
138
149
|
handleInput,
|
|
139
|
-
} = useOctopusDropdown(props, (query) => emit('search', query), 'multiselect');
|
|
150
|
+
} = useOctopusDropdown(props, (query) => emit('search', query), 'multiselect', [dropdownRef]);
|
|
140
151
|
|
|
141
152
|
const selectionRef = ref<HTMLElement | null>(null);
|
|
153
|
+
|
|
154
|
+
// Position of the teleported dropdown (position: fixed, anchored below the trigger field)
|
|
155
|
+
const dropdownStyle = ref<CSSProperties>({});
|
|
156
|
+
|
|
157
|
+
function updateDropdownPosition(): void {
|
|
158
|
+
if (!containerRef.value) { return; }
|
|
159
|
+
const rect = containerRef.value.getBoundingClientRect();
|
|
160
|
+
dropdownStyle.value = {
|
|
161
|
+
position: 'fixed',
|
|
162
|
+
top: `${rect.bottom + 2}px`,
|
|
163
|
+
left: `${rect.left}px`,
|
|
164
|
+
width: `${rect.width}px`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
142
167
|
const visibleCount = ref(2);
|
|
143
168
|
|
|
144
169
|
const allSelected = computed(() => {
|
|
@@ -247,16 +272,23 @@ onMounted(() => {
|
|
|
247
272
|
resizeObserver.observe(selectionRef.value);
|
|
248
273
|
}
|
|
249
274
|
updateVisibleCount();
|
|
275
|
+
// Keep the teleported dropdown aligned when the page scrolls or the viewport resizes
|
|
276
|
+
window.addEventListener('scroll', updateDropdownPosition, true);
|
|
277
|
+
window.addEventListener('resize', updateDropdownPosition);
|
|
250
278
|
});
|
|
251
279
|
|
|
252
280
|
onUnmounted(() => {
|
|
253
281
|
resizeObserver?.disconnect();
|
|
282
|
+
window.removeEventListener('scroll', updateDropdownPosition, true);
|
|
283
|
+
window.removeEventListener('resize', updateDropdownPosition);
|
|
254
284
|
});
|
|
255
285
|
|
|
256
286
|
watch(() => props.selected, updateVisibleCount);
|
|
257
287
|
|
|
258
288
|
watch(isOpen, (val) => {
|
|
259
|
-
if (
|
|
289
|
+
if (val) {
|
|
290
|
+
nextTick(updateDropdownPosition);
|
|
291
|
+
} else {
|
|
260
292
|
nextTick(updateVisibleCount);
|
|
261
293
|
}
|
|
262
294
|
});
|
|
@@ -345,31 +377,30 @@ watch(isOpen, (val) => {
|
|
|
345
377
|
align-items: center;
|
|
346
378
|
}
|
|
347
379
|
|
|
348
|
-
|
|
349
|
-
position: absolute;
|
|
350
|
-
top: calc(100% + 2px);
|
|
351
|
-
left: 0;
|
|
352
|
-
right: 0;
|
|
353
|
-
z-index: 100;
|
|
354
|
-
background: white;
|
|
355
|
-
border: 1px solid var(--octopus-border-default);
|
|
356
|
-
border-radius: var(--octopus-border-radius);
|
|
357
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
358
|
-
padding: 0.25rem 0;
|
|
380
|
+
}
|
|
359
381
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
382
|
+
// Dropdown is teleported to body — scoped rules must be top-level so that [data-v-xxxx]
|
|
383
|
+
// is matched directly on the element rather than via a descendant-of-.octopus-multiselect selector.
|
|
384
|
+
.octopus-multiselect-dropdown {
|
|
385
|
+
z-index: 100;
|
|
386
|
+
background: white;
|
|
387
|
+
border: 1px solid var(--octopus-border-default);
|
|
388
|
+
border-radius: var(--octopus-border-radius);
|
|
389
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
390
|
+
padding: 0.25rem 0;
|
|
391
|
+
|
|
392
|
+
> .octopus-form-item {
|
|
393
|
+
padding: 0.25rem 0.5rem;
|
|
394
|
+
border-bottom: 1px solid var(--octopus-secondary);
|
|
364
395
|
}
|
|
396
|
+
}
|
|
365
397
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
398
|
+
.octopus-multiselect-options {
|
|
399
|
+
max-height: 14rem;
|
|
400
|
+
overflow-y: auto;
|
|
369
401
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
402
|
+
.octopus-form-item {
|
|
403
|
+
padding: 0.25rem 0.5rem;
|
|
373
404
|
}
|
|
374
405
|
}
|
|
375
406
|
</style>
|
|
@@ -42,27 +42,34 @@
|
|
|
42
42
|
</button>
|
|
43
43
|
</div>
|
|
44
44
|
|
|
45
|
-
<
|
|
46
|
-
<div
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
45
|
+
<Teleport to=".octopus-app">
|
|
46
|
+
<div
|
|
47
|
+
v-if="isOpen"
|
|
48
|
+
ref="dropdownRef"
|
|
49
|
+
class="octopus-select-dropdown"
|
|
50
|
+
:style="dropdownStyle"
|
|
51
|
+
>
|
|
52
|
+
<div class="octopus-select-options">
|
|
53
|
+
<button
|
|
54
|
+
v-for="(option, index) in displayedOptions"
|
|
55
|
+
:key="index"
|
|
56
|
+
class="octopus-select-option"
|
|
57
|
+
:class="{ selected: isSelected(option) }"
|
|
58
|
+
@click="selectOption(option)"
|
|
59
|
+
>
|
|
60
|
+
{{ getLabel(option) }}
|
|
61
|
+
</button>
|
|
62
|
+
<span v-if="displayedOptions.length === 0" class="text-indic px-2">
|
|
63
|
+
{{ t('No elements found. Consider changing the search query.') }}
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
59
66
|
</div>
|
|
60
|
-
</
|
|
67
|
+
</Teleport>
|
|
61
68
|
</div>
|
|
62
69
|
</template>
|
|
63
70
|
|
|
64
71
|
<script setup lang="ts" generic="T">
|
|
65
|
-
import { computed, toRaw } from 'vue';
|
|
72
|
+
import { type CSSProperties, computed, nextTick, onMounted, onUnmounted, ref, toRaw, watch } from 'vue';
|
|
66
73
|
import { useI18n } from 'vue-i18n';
|
|
67
74
|
import ChevronDownIcon from 'vue-material-design-icons/ChevronDown.vue';
|
|
68
75
|
import { useOctopusDropdown } from '../composable/form/useOctopusDropdown';
|
|
@@ -104,6 +111,10 @@ const emit = defineEmits<{
|
|
|
104
111
|
|
|
105
112
|
const { t } = useI18n();
|
|
106
113
|
|
|
114
|
+
// Ref on the teleported dropdown div — passed as ignored element to useOctopusDropdown
|
|
115
|
+
// so clicks inside the dropdown don't trigger the click-outside handler.
|
|
116
|
+
const dropdownRef = ref<HTMLElement | null>(null);
|
|
117
|
+
|
|
107
118
|
const {
|
|
108
119
|
searchQuery,
|
|
109
120
|
isOpen,
|
|
@@ -117,7 +128,38 @@ const {
|
|
|
117
128
|
closeDropdown,
|
|
118
129
|
toggleDropdown,
|
|
119
130
|
handleInput,
|
|
120
|
-
} = useOctopusDropdown(props, (query) => emit('search', query), 'select');
|
|
131
|
+
} = useOctopusDropdown(props, (query) => emit('search', query), 'select', [dropdownRef]);
|
|
132
|
+
|
|
133
|
+
// Position of the teleported dropdown (position: fixed, anchored below the trigger field)
|
|
134
|
+
const dropdownStyle = ref<CSSProperties>({});
|
|
135
|
+
|
|
136
|
+
function updateDropdownPosition(): void {
|
|
137
|
+
if (!containerRef.value) { return; }
|
|
138
|
+
const rect = containerRef.value.getBoundingClientRect();
|
|
139
|
+
dropdownStyle.value = {
|
|
140
|
+
position: 'fixed',
|
|
141
|
+
top: `${rect.bottom + 2}px`,
|
|
142
|
+
left: `${rect.left}px`,
|
|
143
|
+
width: `${rect.width}px`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
watch(isOpen, (val) => {
|
|
148
|
+
if (val) {
|
|
149
|
+
nextTick(updateDropdownPosition);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
onMounted(() => {
|
|
154
|
+
// Keep the teleported dropdown aligned when the page scrolls or the viewport resizes
|
|
155
|
+
window.addEventListener('scroll', updateDropdownPosition, true);
|
|
156
|
+
window.addEventListener('resize', updateDropdownPosition);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
onUnmounted(() => {
|
|
160
|
+
window.removeEventListener('scroll', updateDropdownPosition, true);
|
|
161
|
+
window.removeEventListener('resize', updateDropdownPosition);
|
|
162
|
+
});
|
|
121
163
|
|
|
122
164
|
const selectedLabel = computed(() =>
|
|
123
165
|
props.value !== undefined ? getLabel(props.value) : undefined
|
|
@@ -197,41 +239,40 @@ function selectOption(option: T): void {
|
|
|
197
239
|
align-items: center;
|
|
198
240
|
}
|
|
199
241
|
|
|
200
|
-
|
|
201
|
-
position: absolute;
|
|
202
|
-
top: calc(100% + 2px);
|
|
203
|
-
left: 0;
|
|
204
|
-
right: 0;
|
|
205
|
-
z-index: 100;
|
|
206
|
-
background: white;
|
|
207
|
-
border: 1px solid var(--octopus-border-default);
|
|
208
|
-
border-radius: var(--octopus-border-radius);
|
|
209
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
210
|
-
padding: 0.25rem 0;
|
|
211
|
-
}
|
|
242
|
+
}
|
|
212
243
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
244
|
+
// Dropdown is teleported to body — scoped rules must be top-level so that [data-v-xxxx]
|
|
245
|
+
// is matched directly on the element rather than via a descendant-of-.octopus-select selector.
|
|
246
|
+
.octopus-select-dropdown {
|
|
247
|
+
z-index: 100;
|
|
248
|
+
background: white;
|
|
249
|
+
border: 1px solid var(--octopus-border-default);
|
|
250
|
+
border-radius: var(--octopus-border-radius);
|
|
251
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
252
|
+
padding: 0.25rem 0;
|
|
253
|
+
}
|
|
217
254
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
padding: 0.25rem 0.5rem;
|
|
223
|
-
border: none;
|
|
224
|
-
background: transparent;
|
|
225
|
-
cursor: pointer;
|
|
255
|
+
.octopus-select-options {
|
|
256
|
+
max-height: 14rem;
|
|
257
|
+
overflow-y: auto;
|
|
258
|
+
}
|
|
226
259
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
260
|
+
.octopus-select-option {
|
|
261
|
+
display: block;
|
|
262
|
+
width: 100%;
|
|
263
|
+
text-align: left;
|
|
264
|
+
padding: 0.25rem 0.5rem;
|
|
265
|
+
border: none;
|
|
266
|
+
background: transparent;
|
|
267
|
+
cursor: pointer;
|
|
230
268
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
269
|
+
&:hover {
|
|
270
|
+
background: var(--octopus-secondary-lighter);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
&.selected {
|
|
274
|
+
font-weight: 600;
|
|
275
|
+
color: var(--octopus-primary);
|
|
235
276
|
}
|
|
236
277
|
}
|
|
237
278
|
</style>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
'play-button-box': !isBigButton,
|
|
8
8
|
'play-big-button-box': isBigButton,
|
|
9
9
|
}"
|
|
10
|
-
class="btn text-light
|
|
10
|
+
class="btn text-light"
|
|
11
11
|
@click="switchPausePlay"
|
|
12
12
|
>
|
|
13
13
|
<PlayIcon v-if="displayIsPaused" :size="isBigButton ? 60 : 30" />
|
|
@@ -107,23 +107,27 @@ function switchPausePlay(): void {
|
|
|
107
107
|
}
|
|
108
108
|
</script>
|
|
109
109
|
|
|
110
|
-
<style lang="scss">
|
|
110
|
+
<style scoped lang="scss">
|
|
111
111
|
@use "../../../../style/playButton";
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
112
|
+
|
|
113
|
+
.btn {
|
|
114
|
+
color: var(--octopus-player-btn-fg);
|
|
115
|
+
background-color: var(--octopus-player-btn-bg);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.play-button-box:not(.small-font) {
|
|
119
|
+
font-size: 1rem !important;
|
|
120
|
+
}
|
|
121
|
+
.play-big-button-box {
|
|
122
|
+
height: 5rem;
|
|
123
|
+
width: 5rem;
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
justify-content: center;
|
|
127
|
+
margin: 0 0.5rem;
|
|
128
|
+
border-radius: 50% !important;
|
|
129
|
+
font-size: 2.5rem !important;
|
|
130
|
+
flex-shrink: 0;
|
|
131
|
+
cursor: pointer;
|
|
128
132
|
}
|
|
129
133
|
</style>
|
|
@@ -48,6 +48,7 @@ export interface ParamStore {
|
|
|
48
48
|
/** Show time with the dates on podcasts */
|
|
49
49
|
showTimeWithDates?: boolean;
|
|
50
50
|
buttonPlus?: boolean;
|
|
51
|
+
/** Show the "Radio & Live" tab in the navigation; driven by additionalConfiguration from the backend */
|
|
51
52
|
isLiveTab?: boolean;
|
|
52
53
|
isCaptchaTest?: boolean;
|
|
53
54
|
podcastItem?: number;
|
|
@@ -24,6 +24,12 @@ export type OrganisationAttributes = {
|
|
|
24
24
|
PRIVATE?: string;
|
|
25
25
|
/** Live recordings enabled */
|
|
26
26
|
'live.active'?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* List of rubriquage **names**.
|
|
29
|
+
* The rubriques from these rubriquage are stored in stats and can be queried
|
|
30
|
+
* upon.
|
|
31
|
+
*/
|
|
32
|
+
rubriquage4MMIdentifier?: string;
|
|
27
33
|
};
|
|
28
34
|
|
|
29
35
|
export interface Organisation {
|
|
@@ -43,6 +43,13 @@
|
|
|
43
43
|
// ClassicDataTable
|
|
44
44
|
--table-line-height: 48px;
|
|
45
45
|
|
|
46
|
+
// Buttons
|
|
47
|
+
--octopus-btn-primary-bg: var(--octopus-primary);
|
|
48
|
+
--octopus-btn-primary-fg: var(--octopus-color-on-primary);
|
|
49
|
+
--octopus-btn-play-bg: var(--octopus-primary-less-transparent);
|
|
50
|
+
--octopus-btn-play-fg: white;
|
|
51
|
+
--octopus-btn-play-radius: var(--octopus-border-radius);
|
|
52
|
+
|
|
46
53
|
// Player
|
|
47
54
|
// Color for the transcript background
|
|
48
55
|
--octopus-player-transcript-bg-color: oklch(from var(--octopus-player-color) calc(l + 0.1) c h);
|
|
@@ -52,6 +59,10 @@
|
|
|
52
59
|
--octopus-player-progress-color-current: var(--octopus-primary);
|
|
53
60
|
// Color for the progress bar background
|
|
54
61
|
--octopus-player-progress-background-color: var(--octopus-secondary-lighter);
|
|
62
|
+
// Color of the player's button
|
|
63
|
+
--octopus-player-btn-bg: var(--octopus-btn-primary-bg);
|
|
64
|
+
// Color of the player's button
|
|
65
|
+
--octopus-player-btn-fg: var(--octopus-btn-primary-fg);
|
|
55
66
|
|
|
56
67
|
// Smartlink
|
|
57
68
|
--octopus-smartlink-title-color: var(--octopus-gray-text);
|
package/src/style/bootstrap.scss
CHANGED
|
@@ -153,10 +153,10 @@ input:not([class^="vs__"]), button:not([class^="vs__"]), select:not([class^="vs_
|
|
|
153
153
|
display: flex;
|
|
154
154
|
align-items: center;
|
|
155
155
|
justify-content: center;
|
|
156
|
-
background: var(--octopus-primary);
|
|
156
|
+
background: var(--octopus-btn-primary-bg);
|
|
157
157
|
border: 1px solid var(--octopus-primary);
|
|
158
158
|
border-radius: var(--octopus-border-radius) !important;
|
|
159
|
-
color: var(--octopus-
|
|
159
|
+
color: var(--octopus-btn-primary-fg) !important;
|
|
160
160
|
font-weight: 500;
|
|
161
161
|
|
|
162
162
|
&:not(.btn-on-dark):is(:focus, :hover, :active, .active){
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import '@tests/mocks/i18n';
|
|
2
2
|
|
|
3
3
|
import OctopusMultiselect from '@/components/form/OctopusMultiselect.vue';
|
|
4
|
+
import { DOMWrapper, type VueWrapper } from '@vue/test-utils';
|
|
4
5
|
import { mount } from '@tests/utils';
|
|
5
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
7
|
|
|
7
8
|
const options = [
|
|
8
9
|
{ id: 1, name: 'Alpha' },
|
|
@@ -10,66 +11,89 @@ const options = [
|
|
|
10
11
|
{ id: 3, name: 'Gamma' },
|
|
11
12
|
];
|
|
12
13
|
|
|
14
|
+
// The dropdown is teleported to .octopus-app — helper to query it from the document.
|
|
15
|
+
function getDropdown(): Element | null {
|
|
16
|
+
return document.body.querySelector('.octopus-multiselect-dropdown');
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
describe('OctopusMultiselect', () => {
|
|
20
|
+
// The dropdown teleports to .octopus-app, which is normally the app root (see App.vue).
|
|
21
|
+
// It doesn't exist in the test DOM, so it must be created before mount.
|
|
22
|
+
let appElement: HTMLElement;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
appElement = document.createElement('div');
|
|
26
|
+
appElement.className = 'octopus-app';
|
|
27
|
+
document.body.appendChild(appElement);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Shared wrapper ref so afterEach can properly unmount it.
|
|
31
|
+
// Proper unmount lets Vue clean up the teleport target before the next test starts.
|
|
32
|
+
let wrapper: VueWrapper;
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
wrapper?.unmount();
|
|
35
|
+
appElement.remove();
|
|
36
|
+
});
|
|
37
|
+
|
|
14
38
|
it('renders label when provided', async () => {
|
|
15
|
-
|
|
39
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
16
40
|
props: { options, optionLabel: 'name', label: 'My label' },
|
|
17
41
|
});
|
|
18
42
|
expect(wrapper.find('label.form-label').text()).toBe('My label');
|
|
19
43
|
});
|
|
20
44
|
|
|
21
45
|
it('does not render label when not provided', async () => {
|
|
22
|
-
|
|
46
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
23
47
|
props: { options, optionLabel: 'name' },
|
|
24
48
|
});
|
|
25
49
|
expect(wrapper.find('label.form-label').exists()).toBe(false);
|
|
26
50
|
});
|
|
27
51
|
|
|
28
52
|
it('opens dropdown on input focus', async () => {
|
|
29
|
-
|
|
53
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
30
54
|
props: { options, optionLabel: 'name' },
|
|
31
55
|
});
|
|
32
|
-
expect(
|
|
56
|
+
expect(getDropdown()).toBeNull();
|
|
33
57
|
await wrapper.find('input').trigger('focus');
|
|
34
|
-
expect(
|
|
58
|
+
expect(getDropdown()).not.toBeNull();
|
|
35
59
|
});
|
|
36
60
|
|
|
37
61
|
it('shows "All" checkbox and one checkbox per option when open', async () => {
|
|
38
|
-
|
|
62
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
39
63
|
props: { options, optionLabel: 'name' },
|
|
40
64
|
});
|
|
41
65
|
await wrapper.find('input').trigger('focus');
|
|
42
|
-
const checkboxes =
|
|
66
|
+
const checkboxes = document.body.querySelectorAll('input[type="checkbox"]');
|
|
43
67
|
// 1 for "All" + 3 for options
|
|
44
68
|
expect(checkboxes).toHaveLength(4);
|
|
45
69
|
});
|
|
46
70
|
|
|
47
71
|
it('filters options locally by search query', async () => {
|
|
48
|
-
|
|
72
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
49
73
|
props: { options, optionLabel: 'name' },
|
|
50
74
|
});
|
|
51
75
|
await wrapper.find('input').trigger('focus');
|
|
52
76
|
await wrapper.find('input').setValue('al');
|
|
53
77
|
await wrapper.find('input').trigger('input');
|
|
54
|
-
const checkboxes =
|
|
78
|
+
const checkboxes = document.body.querySelectorAll('input[type="checkbox"]');
|
|
55
79
|
// 1 for "All" + 1 matching "Alpha"
|
|
56
80
|
expect(checkboxes).toHaveLength(2);
|
|
57
81
|
});
|
|
58
82
|
|
|
59
83
|
it('shows no-results message when filter matches nothing', async () => {
|
|
60
|
-
|
|
84
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
61
85
|
props: { options, optionLabel: 'name' },
|
|
62
86
|
});
|
|
63
87
|
await wrapper.find('input').trigger('focus');
|
|
64
88
|
await wrapper.find('input').setValue('zzz');
|
|
65
89
|
await wrapper.find('input').trigger('input');
|
|
66
|
-
expect(
|
|
90
|
+
expect(document.body.querySelector('.text-indic')).not.toBeNull();
|
|
67
91
|
});
|
|
68
92
|
|
|
69
93
|
it('calls onSearch with current query and updates displayed options', async () => {
|
|
70
94
|
const searchResult = [{ id: 4, name: 'Delta' }];
|
|
71
95
|
const onSearch = vi.fn().mockResolvedValue(searchResult);
|
|
72
|
-
|
|
96
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
73
97
|
props: { options, optionLabel: 'name', onSearch },
|
|
74
98
|
});
|
|
75
99
|
await wrapper.find('input').trigger('focus');
|
|
@@ -80,48 +104,48 @@ describe('OctopusMultiselect', () => {
|
|
|
80
104
|
});
|
|
81
105
|
|
|
82
106
|
it('emits update:selected when toggling a single option', async () => {
|
|
83
|
-
|
|
107
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
84
108
|
props: { options, optionLabel: 'name', selected: [] },
|
|
85
109
|
});
|
|
86
110
|
await wrapper.find('input').trigger('focus');
|
|
87
|
-
const optionCheckboxes =
|
|
88
|
-
await optionCheckboxes[0].trigger('input');
|
|
111
|
+
const optionCheckboxes = document.body.querySelectorAll('.octopus-multiselect-options input[type="checkbox"]');
|
|
112
|
+
await new DOMWrapper(optionCheckboxes[0] as Element).trigger('input');
|
|
89
113
|
expect(wrapper.emitted('update:selected')?.[0]).toEqual([[options[0]]]);
|
|
90
114
|
});
|
|
91
115
|
|
|
92
116
|
it('deselects an already-selected option', async () => {
|
|
93
|
-
|
|
117
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
94
118
|
props: { options, optionLabel: 'name', selected: [options[0]] },
|
|
95
119
|
});
|
|
96
120
|
await wrapper.find('input').trigger('focus');
|
|
97
|
-
const optionCheckboxes =
|
|
98
|
-
await optionCheckboxes[0].trigger('input');
|
|
121
|
+
const optionCheckboxes = document.body.querySelectorAll('.octopus-multiselect-options input[type="checkbox"]');
|
|
122
|
+
await new DOMWrapper(optionCheckboxes[0] as Element).trigger('input');
|
|
99
123
|
expect(wrapper.emitted('update:selected')?.[0]).toEqual([[]]);
|
|
100
124
|
});
|
|
101
125
|
|
|
102
126
|
it('toggleAll selects all displayed options', async () => {
|
|
103
|
-
|
|
127
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
104
128
|
props: { options, optionLabel: 'name', selected: [] },
|
|
105
129
|
});
|
|
106
130
|
await wrapper.find('input').trigger('focus');
|
|
107
|
-
const allCheckbox =
|
|
108
|
-
await allCheckbox.trigger('input');
|
|
131
|
+
const allCheckbox = document.body.querySelector('.octopus-multiselect-dropdown > .octopus-form-item input[type="checkbox"]')!;
|
|
132
|
+
await new DOMWrapper(allCheckbox).trigger('input');
|
|
109
133
|
const emitted = wrapper.emitted('update:selected')?.[0]?.[0] as unknown[];
|
|
110
134
|
expect(emitted).toHaveLength(3);
|
|
111
135
|
});
|
|
112
136
|
|
|
113
137
|
it('toggleAll deselects all displayed options when all are selected', async () => {
|
|
114
|
-
|
|
138
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
115
139
|
props: { options, optionLabel: 'name', selected: [...options] },
|
|
116
140
|
});
|
|
117
141
|
await wrapper.find('input').trigger('focus');
|
|
118
|
-
const allCheckbox =
|
|
119
|
-
await allCheckbox.trigger('input');
|
|
142
|
+
const allCheckbox = document.body.querySelector('.octopus-multiselect-dropdown > .octopus-form-item input[type="checkbox"]')!;
|
|
143
|
+
await new DOMWrapper(allCheckbox).trigger('input');
|
|
120
144
|
expect(wrapper.emitted('update:selected')?.[0]).toEqual([[]]);
|
|
121
145
|
});
|
|
122
146
|
|
|
123
147
|
it('shows selected items in the selection display', async () => {
|
|
124
|
-
|
|
148
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
125
149
|
props: { options, optionLabel: 'name', selected: [options[0], options[1]] },
|
|
126
150
|
});
|
|
127
151
|
expect(wrapper.find('.octopus-multiselect-selection-text').text()).toBe('Alpha, Beta');
|
|
@@ -129,55 +153,55 @@ describe('OctopusMultiselect', () => {
|
|
|
129
153
|
});
|
|
130
154
|
|
|
131
155
|
it('shows overflow count badge when more than visible items are selected', async () => {
|
|
132
|
-
|
|
156
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
133
157
|
props: { options, optionLabel: 'name', selected: [...options] },
|
|
134
158
|
});
|
|
135
159
|
expect(wrapper.find('.octopus-multiselect-selection-count').exists()).toBe(true);
|
|
136
160
|
});
|
|
137
161
|
|
|
138
162
|
it('disables input when isDisabled is true', async () => {
|
|
139
|
-
|
|
163
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
140
164
|
props: { options, optionLabel: 'name', isDisabled: true },
|
|
141
165
|
});
|
|
142
166
|
expect(wrapper.find('input').attributes('disabled')).toBeDefined();
|
|
143
167
|
});
|
|
144
168
|
|
|
145
169
|
it('does not open dropdown when disabled', async () => {
|
|
146
|
-
|
|
170
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
147
171
|
props: { options, optionLabel: 'name', isDisabled: true },
|
|
148
172
|
});
|
|
149
173
|
await wrapper.find('input').trigger('focus');
|
|
150
|
-
expect(
|
|
174
|
+
expect(getDropdown()).toBeNull();
|
|
151
175
|
});
|
|
152
176
|
|
|
153
177
|
it('uses selectAllText as the "All" checkbox label when provided', async () => {
|
|
154
|
-
|
|
178
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
155
179
|
props: { options, optionLabel: 'name', selectAllText: 'Tout' },
|
|
156
180
|
});
|
|
157
181
|
await wrapper.find('input').trigger('focus');
|
|
158
|
-
const allCheckboxLabel =
|
|
159
|
-
expect(allCheckboxLabel.
|
|
182
|
+
const allCheckboxLabel = document.body.querySelector('.octopus-multiselect-dropdown > .octopus-form-item label')!;
|
|
183
|
+
expect(allCheckboxLabel.textContent?.trim()).toBe('Tout');
|
|
160
184
|
});
|
|
161
185
|
|
|
162
186
|
it('deselects by optionKey even when selected objects are different references', async () => {
|
|
163
187
|
const selected = [{ id: 1, name: 'Alpha' }];
|
|
164
|
-
|
|
188
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
165
189
|
props: { options, optionLabel: 'name', optionKey: 'id', selected },
|
|
166
190
|
});
|
|
167
191
|
await wrapper.find('input').trigger('focus');
|
|
168
|
-
const optionCheckboxes =
|
|
169
|
-
await optionCheckboxes[0].trigger('input');
|
|
192
|
+
const optionCheckboxes = document.body.querySelectorAll('.octopus-multiselect-options input[type="checkbox"]');
|
|
193
|
+
await new DOMWrapper(optionCheckboxes[0] as Element).trigger('input');
|
|
170
194
|
expect(wrapper.emitted('update:selected')?.[0]).toEqual([[]]);
|
|
171
195
|
});
|
|
172
196
|
|
|
173
197
|
it('toggleAll deselects by optionKey even when selected objects are different references', async () => {
|
|
174
198
|
const selected = options.map((o) => ({ ...o }));
|
|
175
|
-
|
|
199
|
+
wrapper = await mount(OctopusMultiselect, {
|
|
176
200
|
props: { options, optionLabel: 'name', optionKey: 'id', selected },
|
|
177
201
|
});
|
|
178
202
|
await wrapper.find('input').trigger('focus');
|
|
179
|
-
const allCheckbox =
|
|
180
|
-
await allCheckbox.trigger('input');
|
|
203
|
+
const allCheckbox = document.body.querySelector('.octopus-multiselect-dropdown > .octopus-form-item input[type="checkbox"]')!;
|
|
204
|
+
await new DOMWrapper(allCheckbox).trigger('input');
|
|
181
205
|
expect(wrapper.emitted('update:selected')?.[0]).toEqual([[]]);
|
|
182
206
|
});
|
|
183
207
|
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import '@tests/mocks/i18n';
|
|
2
2
|
|
|
3
3
|
import OctopusSelect from '@/components/form/OctopusSelect.vue';
|
|
4
|
+
import { DOMWrapper, type VueWrapper } from '@vue/test-utils';
|
|
4
5
|
import { mount } from '@tests/utils';
|
|
5
|
-
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
7
|
|
|
7
8
|
const options = [
|
|
8
9
|
{ id: 1, name: 'Alpha' },
|
|
@@ -10,123 +11,146 @@ const options = [
|
|
|
10
11
|
{ id: 3, name: 'Gamma' },
|
|
11
12
|
];
|
|
12
13
|
|
|
14
|
+
// The dropdown is teleported to .octopus-app — helper to query it from the document.
|
|
15
|
+
function getDropdown(): Element | null {
|
|
16
|
+
return document.body.querySelector('.octopus-select-dropdown');
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
describe('OctopusSelect', () => {
|
|
20
|
+
// The dropdown teleports to .octopus-app, which is normally the app root (see App.vue).
|
|
21
|
+
// It doesn't exist in the test DOM, so it must be created before mount.
|
|
22
|
+
let appElement: HTMLElement;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
appElement = document.createElement('div');
|
|
26
|
+
appElement.className = 'octopus-app';
|
|
27
|
+
document.body.appendChild(appElement);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Shared wrapper ref so afterEach can properly unmount it.
|
|
31
|
+
// Proper unmount lets Vue clean up the teleport target before the next test starts.
|
|
32
|
+
let wrapper: VueWrapper;
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
wrapper?.unmount();
|
|
35
|
+
appElement.remove();
|
|
36
|
+
});
|
|
37
|
+
|
|
14
38
|
it('renders label when provided', async () => {
|
|
15
|
-
|
|
39
|
+
wrapper = await mount(OctopusSelect, {
|
|
16
40
|
props: { options, optionLabel: 'name', label: 'My label' },
|
|
17
41
|
});
|
|
18
42
|
expect(wrapper.find('label.form-label').text()).toBe('My label');
|
|
19
43
|
});
|
|
20
44
|
|
|
21
45
|
it('does not render label when not provided', async () => {
|
|
22
|
-
|
|
46
|
+
wrapper = await mount(OctopusSelect, {
|
|
23
47
|
props: { options, optionLabel: 'name' },
|
|
24
48
|
});
|
|
25
49
|
expect(wrapper.find('label.form-label').exists()).toBe(false);
|
|
26
50
|
});
|
|
27
51
|
|
|
28
52
|
it('opens dropdown on input focus', async () => {
|
|
29
|
-
|
|
53
|
+
wrapper = await mount(OctopusSelect, {
|
|
30
54
|
props: { options, optionLabel: 'name' },
|
|
31
55
|
});
|
|
32
|
-
expect(
|
|
56
|
+
expect(getDropdown()).toBeNull();
|
|
33
57
|
await wrapper.find('input').trigger('focus');
|
|
34
|
-
expect(
|
|
58
|
+
expect(getDropdown()).not.toBeNull();
|
|
35
59
|
});
|
|
36
60
|
|
|
37
61
|
it('shows one button per option when open', async () => {
|
|
38
|
-
|
|
62
|
+
wrapper = await mount(OctopusSelect, {
|
|
39
63
|
props: { options, optionLabel: 'name' },
|
|
40
64
|
});
|
|
41
65
|
await wrapper.find('input').trigger('focus');
|
|
42
|
-
const optionButtons =
|
|
66
|
+
const optionButtons = document.body.querySelectorAll('.octopus-select-option');
|
|
43
67
|
expect(optionButtons).toHaveLength(3);
|
|
44
|
-
expect(optionButtons[0].
|
|
68
|
+
expect(optionButtons[0].textContent?.trim()).toBe('Alpha');
|
|
45
69
|
});
|
|
46
70
|
|
|
47
71
|
it('filters options locally by search query', async () => {
|
|
48
|
-
|
|
72
|
+
wrapper = await mount(OctopusSelect, {
|
|
49
73
|
props: { options, optionLabel: 'name' },
|
|
50
74
|
});
|
|
51
75
|
await wrapper.find('input').trigger('focus');
|
|
52
76
|
await wrapper.find('input').setValue('al');
|
|
53
77
|
await wrapper.find('input').trigger('input');
|
|
54
|
-
expect(
|
|
78
|
+
expect(document.body.querySelectorAll('.octopus-select-option')).toHaveLength(1);
|
|
55
79
|
});
|
|
56
80
|
|
|
57
81
|
it('shows no-results message when filter matches nothing', async () => {
|
|
58
|
-
|
|
82
|
+
wrapper = await mount(OctopusSelect, {
|
|
59
83
|
props: { options, optionLabel: 'name' },
|
|
60
84
|
});
|
|
61
85
|
await wrapper.find('input').trigger('focus');
|
|
62
86
|
await wrapper.find('input').setValue('zzz');
|
|
63
87
|
await wrapper.find('input').trigger('input');
|
|
64
|
-
expect(
|
|
88
|
+
expect(document.body.querySelector('.text-indic')).not.toBeNull();
|
|
65
89
|
});
|
|
66
90
|
|
|
67
91
|
it('emits update:value with the clicked option', async () => {
|
|
68
|
-
|
|
92
|
+
wrapper = await mount(OctopusSelect, {
|
|
69
93
|
props: { options, optionLabel: 'name' },
|
|
70
94
|
});
|
|
71
95
|
await wrapper.find('input').trigger('focus');
|
|
72
|
-
await
|
|
96
|
+
await new DOMWrapper(document.body.querySelectorAll('.octopus-select-option')[1]).trigger('click');
|
|
73
97
|
expect(wrapper.emitted('update:value')?.[0]).toEqual([options[1]]);
|
|
74
98
|
});
|
|
75
99
|
|
|
76
100
|
it('closes dropdown after selecting an option', async () => {
|
|
77
|
-
|
|
101
|
+
wrapper = await mount(OctopusSelect, {
|
|
78
102
|
props: { options, optionLabel: 'name' },
|
|
79
103
|
});
|
|
80
104
|
await wrapper.find('input').trigger('focus');
|
|
81
|
-
await
|
|
82
|
-
expect(
|
|
105
|
+
await new DOMWrapper(document.body.querySelectorAll('.octopus-select-option')[0]).trigger('click');
|
|
106
|
+
expect(getDropdown()).toBeNull();
|
|
83
107
|
});
|
|
84
108
|
|
|
85
109
|
it('shows selected item label in the field when closed', async () => {
|
|
86
|
-
|
|
110
|
+
wrapper = await mount(OctopusSelect, {
|
|
87
111
|
props: { options, optionLabel: 'name', value: options[0] },
|
|
88
112
|
});
|
|
89
113
|
expect(wrapper.find('.octopus-select-value').text()).toBe('Alpha');
|
|
90
114
|
});
|
|
91
115
|
|
|
92
116
|
it('marks the selected option with the selected class', async () => {
|
|
93
|
-
|
|
117
|
+
wrapper = await mount(OctopusSelect, {
|
|
94
118
|
props: { options, optionLabel: 'name', value: options[0] },
|
|
95
119
|
});
|
|
96
120
|
await wrapper.find('.octopus-select-field').trigger('click');
|
|
97
|
-
const optionButtons =
|
|
98
|
-
expect(optionButtons[0].
|
|
99
|
-
expect(optionButtons[1].
|
|
121
|
+
const optionButtons = document.body.querySelectorAll('.octopus-select-option');
|
|
122
|
+
expect(optionButtons[0].classList).toContain('selected');
|
|
123
|
+
expect(optionButtons[1].classList).not.toContain('selected');
|
|
100
124
|
});
|
|
101
125
|
|
|
102
126
|
it('deselects the current option by default (allowDeselect defaults to true)', async () => {
|
|
103
|
-
|
|
127
|
+
wrapper = await mount(OctopusSelect, {
|
|
104
128
|
props: { options, optionLabel: 'name', optionKey: 'id', value: options[0] },
|
|
105
129
|
});
|
|
106
130
|
await wrapper.find('.octopus-select-field').trigger('click');
|
|
107
|
-
await
|
|
131
|
+
await new DOMWrapper(document.body.querySelectorAll('.octopus-select-option')[0]).trigger('click');
|
|
108
132
|
expect(wrapper.emitted('update:value')?.[0]).toEqual([undefined]);
|
|
109
133
|
});
|
|
110
134
|
|
|
111
135
|
it('does not deselect when allowDeselect is false', async () => {
|
|
112
|
-
|
|
136
|
+
wrapper = await mount(OctopusSelect, {
|
|
113
137
|
props: { options, optionLabel: 'name', optionKey: 'id', value: options[0], allowDeselect: false as boolean },
|
|
114
138
|
});
|
|
115
139
|
await wrapper.find('.octopus-select-field').trigger('click');
|
|
116
|
-
await
|
|
140
|
+
await new DOMWrapper(document.body.querySelectorAll('.octopus-select-option')[0]).trigger('click');
|
|
117
141
|
expect(wrapper.emitted('update:value')?.[0]).toEqual([options[0]]);
|
|
118
142
|
});
|
|
119
143
|
|
|
120
144
|
it('does not open dropdown when disabled', async () => {
|
|
121
|
-
|
|
145
|
+
wrapper = await mount(OctopusSelect, {
|
|
122
146
|
props: { options, optionLabel: 'name', isDisabled: true },
|
|
123
147
|
});
|
|
124
148
|
await wrapper.find('input').trigger('focus');
|
|
125
|
-
expect(
|
|
149
|
+
expect(getDropdown()).toBeNull();
|
|
126
150
|
});
|
|
127
151
|
|
|
128
152
|
it('disables input when isDisabled is true', async () => {
|
|
129
|
-
|
|
153
|
+
wrapper = await mount(OctopusSelect, {
|
|
130
154
|
props: { options, optionLabel: 'name', isDisabled: true },
|
|
131
155
|
});
|
|
132
156
|
expect(wrapper.find('input').attributes('disabled')).toBeDefined();
|
|
@@ -134,10 +158,11 @@ describe('OctopusSelect', () => {
|
|
|
134
158
|
|
|
135
159
|
it('matches by optionKey when checking selection', async () => {
|
|
136
160
|
const value = { id: 1, name: 'Alpha' };
|
|
137
|
-
|
|
161
|
+
wrapper = await mount(OctopusSelect, {
|
|
138
162
|
props: { options, optionLabel: 'name', optionKey: 'id', value },
|
|
139
163
|
});
|
|
140
164
|
await wrapper.find('.octopus-select-field').trigger('click');
|
|
141
|
-
|
|
165
|
+
const optionButtons = document.body.querySelectorAll('.octopus-select-option');
|
|
166
|
+
expect(optionButtons[0].classList).toContain('selected');
|
|
142
167
|
});
|
|
143
168
|
});
|