@saooti/octopus-sdk 41.11.1 → 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.
Files changed (33) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/eslint-config.mjs +10 -2
  3. package/index.ts +5 -2
  4. package/package.json +4 -1
  5. package/src/api/radioApi.ts +14 -1
  6. package/src/components/buttons/ClassicButton.vue +18 -1
  7. package/src/components/composable/form/useOctopusDropdown.ts +104 -0
  8. package/src/components/composable/useSticky.ts +45 -0
  9. package/src/components/display/emission/EmissionGroupChooser.vue +22 -24
  10. package/src/components/display/podcasts/PodcastPlayButton.vue +3 -3
  11. package/src/components/form/ClassicButtonGroup.vue +15 -13
  12. package/src/components/form/ClassicSelect.vue +11 -2
  13. package/src/components/form/OctopusMultiselect.vue +236 -109
  14. package/src/components/form/OctopusSelect.vue +278 -0
  15. package/src/components/misc/ClassicPopover.vue +9 -2
  16. package/src/components/misc/modal/ClassicModal.vue +3 -0
  17. package/src/components/misc/player/elements/PlayerPlayButton.vue +22 -18
  18. package/src/components/pages/PlaylistsPage.vue +1 -0
  19. package/src/helper/colorFromString.ts +12 -5
  20. package/src/helper/equals.ts +21 -9
  21. package/src/stores/ParamSdkStore.ts +1 -0
  22. package/src/stores/class/general/organisation.ts +6 -0
  23. package/src/stores/class/general/podcast.ts +8 -0
  24. package/src/stores/class/rubrique/rubrique.ts +1 -1
  25. package/src/style/_utilities.scss +1 -0
  26. package/src/style/_variables.scss +11 -0
  27. package/src/style/bootstrap.scss +9 -2
  28. package/tests/components/form/ClassicButtonGroup.spec.ts +10 -10
  29. package/tests/components/form/OctopusMultiselect.spec.ts +84 -39
  30. package/tests/components/form/OctopusSelect.spec.ts +168 -0
  31. package/tsconfig.json +3 -7
  32. package/tsconfig.test.json +13 -0
  33. package/vitest.config.js +2 -1
@@ -0,0 +1,278 @@
1
+ <template>
2
+ <div
3
+ ref="containerRef"
4
+ class="octopus-select"
5
+ :class="{ 'form-margin': label }"
6
+ >
7
+ <label
8
+ v-if="label"
9
+ :for="computedId"
10
+ class="form-label"
11
+ >
12
+ {{ label }}
13
+ </label>
14
+
15
+ <div
16
+ class="octopus-select-field"
17
+ :class="{ disabled: isDisabled, open: isOpen, noBorder }"
18
+ @click="openDropdown"
19
+ >
20
+ <span
21
+ v-show="selectedLabel && !isOpen"
22
+ class="octopus-select-value"
23
+ >{{ selectedLabel }}</span>
24
+ <input
25
+ v-show="!selectedLabel || isOpen"
26
+ :id="computedId"
27
+ ref="inputRef"
28
+ v-model="searchQuery"
29
+ type="text"
30
+ class="octopus-select-input"
31
+ :placeholder="inputPlaceholder"
32
+ :disabled="isDisabled"
33
+ @focus="openDropdown"
34
+ @input="handleInput"
35
+ >
36
+ <button
37
+ class="btn-transparent octopus-select-chevron"
38
+ :disabled="isDisabled"
39
+ @click.stop="toggleDropdown"
40
+ >
41
+ <ChevronDownIcon />
42
+ </button>
43
+ </div>
44
+
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>
66
+ </div>
67
+ </Teleport>
68
+ </div>
69
+ </template>
70
+
71
+ <script setup lang="ts" generic="T">
72
+ import { type CSSProperties, computed, nextTick, onMounted, onUnmounted, ref, toRaw, watch } from 'vue';
73
+ import { useI18n } from 'vue-i18n';
74
+ import ChevronDownIcon from 'vue-material-design-icons/ChevronDown.vue';
75
+ import { useOctopusDropdown } from '../composable/form/useOctopusDropdown';
76
+
77
+ const props = withDefaults(defineProps<{
78
+ /** Optional label displayed above the field. */
79
+ label?: string;
80
+ /** Currently selected item. Bind with `v-model:value`. */
81
+ value?: T;
82
+ /** Full list of options to display or filter. */
83
+ options: T[];
84
+ /** Key of each option object to use as the ID. */
85
+ optionKey?: keyof T;
86
+ /** Key of each option object to use as the display label. */
87
+ optionLabel: keyof T & string;
88
+ /** Disables the field when true. */
89
+ isDisabled?: boolean;
90
+ /** Placeholder shown in the input when no item is selected. Defaults to the translated "Search" string. */
91
+ placeholder?: string;
92
+ /** Disable the border around the input. */
93
+ noBorder?: boolean;
94
+ /** When true (default), clicking the already-selected option clears the selection. */
95
+ allowDeselect?: boolean;
96
+ }>(), {
97
+ label: undefined,
98
+ value: undefined,
99
+ optionKey: undefined,
100
+ placeholder: undefined,
101
+ allowDeselect: true,
102
+ });
103
+
104
+ const emit = defineEmits<{
105
+ /** Emitted when the selection changes. */
106
+ (e: 'update:value', value: T | undefined): void;
107
+ /** Emitted on every input change. When listened to, the parent is responsible for
108
+ * updating `options`; otherwise the component filters `options` client-side. */
109
+ (e: 'search', query: string): void;
110
+ }>();
111
+
112
+ const { t } = useI18n();
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
+
118
+ const {
119
+ searchQuery,
120
+ isOpen,
121
+ containerRef,
122
+ inputRef,
123
+ computedId,
124
+ displayedOptions,
125
+ inputPlaceholder,
126
+ getLabel,
127
+ openDropdown,
128
+ closeDropdown,
129
+ toggleDropdown,
130
+ handleInput,
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
+ });
163
+
164
+ const selectedLabel = computed(() =>
165
+ props.value !== undefined ? getLabel(props.value) : undefined
166
+ );
167
+
168
+ function isSelected(option: T): boolean {
169
+ if (props.value === undefined) {
170
+ return false;
171
+ }
172
+ if (props.optionKey) {
173
+ return props.value[props.optionKey] === option[props.optionKey];
174
+ }
175
+ return toRaw(props.value as object) === toRaw(option as object);
176
+ }
177
+
178
+ function selectOption(option: T): void {
179
+ if (isSelected(option) && props.allowDeselect) {
180
+ emit('update:value', undefined);
181
+ } else {
182
+ emit('update:value', option);
183
+ }
184
+ closeDropdown();
185
+ }
186
+ </script>
187
+
188
+ <style scoped lang="scss">
189
+ .octopus-select {
190
+ position: relative;
191
+
192
+ .octopus-select-field {
193
+ display: flex;
194
+ align-items: center;
195
+ border: 1px solid var(--octopus-border-default);
196
+ border-radius: var(--octopus-border-radius);
197
+ background: white;
198
+ cursor: pointer;
199
+
200
+ &.open {
201
+ border-color: var(--octopus-primary);
202
+ }
203
+
204
+ &.disabled {
205
+ background: var(--octopus-secondary-lighter);
206
+ cursor: default;
207
+ }
208
+
209
+ &.noBorder {
210
+ border: none;
211
+ }
212
+ }
213
+
214
+ .octopus-select-value {
215
+ flex: 1;
216
+ min-width: 0;
217
+ overflow: hidden;
218
+ text-overflow: ellipsis;
219
+ white-space: nowrap;
220
+ padding: 0.4rem 0.5rem;
221
+ padding-right: 0;
222
+ height: 2rem;
223
+ }
224
+
225
+ .octopus-select-input {
226
+ flex: 1;
227
+ border: none;
228
+ background: transparent;
229
+ padding: 0.4rem 0.5rem;
230
+ height: 2rem;
231
+ outline: none;
232
+ cursor: inherit;
233
+ min-width: 0;
234
+ }
235
+
236
+ .octopus-select-chevron {
237
+ padding: 0.25rem 0.5rem;
238
+ display: flex;
239
+ align-items: center;
240
+ }
241
+
242
+ }
243
+
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
+ }
254
+
255
+ .octopus-select-options {
256
+ max-height: 14rem;
257
+ overflow-y: auto;
258
+ }
259
+
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;
268
+
269
+ &:hover {
270
+ background: var(--octopus-secondary-lighter);
271
+ }
272
+
273
+ &.selected {
274
+ font-weight: 600;
275
+ color: var(--octopus-primary);
276
+ }
277
+ }
278
+ </style>
@@ -48,7 +48,9 @@ const props = defineProps({
48
48
  popoverClass: { type: String, default: undefined },
49
49
  isTopLayer: { type: Boolean, default: false },
50
50
  /** @deprecated No longer needed. If set to true, max height of popover will not overflow from parent */
51
- constrainHeight: { type: Boolean, default: true }
51
+ constrainHeight: { type: Boolean, default: true },
52
+ /** Force z-index */
53
+ zIndex: { type: Number, default: undefined }
52
54
  })
53
55
 
54
56
  //Emits
@@ -74,7 +76,12 @@ const router = useRouter();
74
76
 
75
77
  //Computed
76
78
  const popoverId = computed(() => "popover" + props.target);
77
- const positionInlineStyle = computed(() => `left: ${posX.value}px; top: ${posY.value}px;max-height:${maxHeight.value}`);
79
+ const positionInlineStyle = computed(() => ({
80
+ left: `${posX.value}px`,
81
+ top: `${posY.value}px`,
82
+ 'max-height': maxHeight.value,
83
+ 'z-index': props.zIndex ?? 10
84
+ }));
78
85
  const displayPopover = computed(() => show.value && !props.disable);
79
86
  const isTopLayerPopover = computed(() => (props.isTopLayer || "octopus-modal"===props.relativeClass) && Object.hasOwn(HTMLElement.prototype, "popover"));
80
87
 
@@ -31,6 +31,7 @@
31
31
  </div>
32
32
  </div>
33
33
  <div v-show="!onlyHeader" class="octopus-modal-body">
34
+ <slot />
34
35
  <slot name="body" />
35
36
  </div>
36
37
  <div v-show="!onlyHeader" class="octopus-modal-footer">
@@ -109,6 +110,8 @@ function closePopup(): void {
109
110
 
110
111
  @media (width <= 500px) {
111
112
  width: 95vw;
113
+ margin-left: 2.5vw;
114
+ margin-right: 2.5vw;
112
115
  }
113
116
 
114
117
  .octopus-modal-body {
@@ -7,7 +7,7 @@
7
7
  'play-button-box': !isBigButton,
8
8
  'play-big-button-box': isBigButton,
9
9
  }"
10
- class="btn text-light bg-primary"
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
- .octopus-app {
113
- .play-button-box:not(.small-font) {
114
- font-size: 1rem !important;
115
- }
116
- .play-big-button-box {
117
- height: 5rem;
118
- width: 5rem;
119
- display: flex;
120
- align-items: center;
121
- justify-content: center;
122
- margin: 0 0.5rem;
123
- border-radius: 50% !important;
124
- font-size: 2.5rem !important;
125
- flex-shrink: 0;
126
- cursor: pointer;
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>
@@ -1,5 +1,6 @@
1
1
  <template>
2
2
  <section v-if="isInit" class="page-box">
3
+ <!-- TODO à intégrer dans frontoffice -->
3
4
  <router-link
4
5
  v-if="isRolePlaylists && !isPodcastmaker"
5
6
  to="/main/priv/edit/playlist"
@@ -1,15 +1,22 @@
1
1
  /**
2
2
  * Get a color from a string.
3
3
  * Using the same string, the color will always be the same.
4
+ *
5
+ * Uses FNV-1a hashing (XOR-then-multiply with the FNV prime 16777619) for strong
6
+ * bit avalanche: a single character difference propagates through all hash bits,
7
+ * so similar strings produce very different hues.
8
+ *
4
9
  * @param str The string used for the color
5
10
  * @param saturation Saturation of the color
6
11
  * @param lightness Lightness of the color
7
12
  * @returns A color
8
13
  */
9
14
  export function colorFromString(str: string, saturation = 80, lightness = 70) {
10
- let hash = 0;
11
- str.split('').forEach(char => {
12
- hash = char.charCodeAt(0) + ((hash << 5) - hash);
13
- });
14
- return `hsl(${(hash + 360) % 360}, ${saturation}%, ${lightness}%)`;
15
+ let hash = 2166136261;
16
+ for (const char of str) {
17
+ hash ^= char.charCodeAt(0);
18
+ hash = Math.imul(hash, 16777619) >>> 0;
19
+ }
20
+ const hue = Math.round((hash / 0xFFFFFFFF) * 360);
21
+ return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
15
22
  }
@@ -1,20 +1,32 @@
1
1
  export function deepEqual(obj1: any, obj2: any) {
2
+ // it's just the same object. No need to compare.
3
+ if(obj1 === obj2) {
4
+ return true;
5
+ }
2
6
 
3
- if(obj1 === obj2) // it's just the same object. No need to compare.
4
- {return true;}
7
+ // compare primitives
8
+ if(isPrimitive(obj1) && isPrimitive(obj2)) {
9
+ return obj1 === obj2;
10
+ }
5
11
 
6
- if(isPrimitive(obj1) && isPrimitive(obj2)) // compare primitives
7
- {return obj1 === obj2;}
12
+ // one is a primitive but not the other (for example undefined vs array)
13
+ if (isPrimitive(obj1) || isPrimitive(obj2)) {
14
+ return false;
15
+ }
8
16
 
9
17
  if(Object.keys(obj1).length !== Object.keys(obj2).length) {
10
18
  return false;
11
19
  }
12
20
 
13
21
  // compare objects with same number of keys
14
- for(const key in obj1)
15
- {
16
- if(!(key in obj2)) {return false;} //other object doesn't have this prop
17
- if(!deepEqual(obj1[key], obj2[key])) {return false;}
22
+ for(const key in obj1) {
23
+ //other object doesn't have this prop
24
+ if(!(key in obj2)) {
25
+ return false;
26
+ }
27
+ if(!deepEqual(obj1[key], obj2[key])) {
28
+ return false;
29
+ }
18
30
  }
19
31
 
20
32
  return true;
@@ -23,4 +35,4 @@ export function deepEqual(obj1: any, obj2: any) {
23
35
  //check if value is primitive
24
36
  function isPrimitive(obj: unknown) {
25
37
  return (obj !== Object(obj));
26
- }
38
+ }
@@ -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 {
@@ -129,6 +129,14 @@ export function simplifiedToFull(simplified: SimplifiedPodcast, organisation: Or
129
129
  };
130
130
  }
131
131
 
132
+ export function podcastToSimplified(full: Podcast): SimplifiedPodcast {
133
+ return {
134
+ ...full,
135
+ emissionId: full.emission.emissionId,
136
+ organisationId: full.organisation.id
137
+ };
138
+ }
139
+
132
140
  export function emptyPodcastData(): Podcast {
133
141
  return {
134
142
  podcastId: 0,
@@ -3,7 +3,7 @@ export interface Rubrique {
3
3
  name: string;
4
4
  podcastCount?: number;
5
5
  rubriquageId?: number;
6
- rubriqueId?: number;
6
+ rubriqueId: number;
7
7
  score?: number;
8
8
  organisationPrivacy?: string;
9
9
  }
@@ -297,6 +297,7 @@ $utilities: map.merge(
297
297
  class: fw,
298
298
  values: (
299
299
  normal: 400,
300
+ 600: 600,
300
301
  bold: 700,
301
302
  )
302
303
  ),
@@ -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);
@@ -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-color-on-primary) !important;
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){
@@ -219,6 +219,13 @@ input:not([class^="vs__"]), button:not([class^="vs__"]), select:not([class^="vs_
219
219
  border: 0;
220
220
  }
221
221
 
222
+ .btn-icon {
223
+ background: transparent;
224
+ border: 0;
225
+ border-radius: 100%;
226
+ height: fit-content;
227
+ }
228
+
222
229
  .share-btn {
223
230
  display: inline-flex;
224
231
  align-items: center;
@@ -11,14 +11,14 @@ const options = [
11
11
  describe('ClassicButtonGroup', () => {
12
12
  it('renders one button per option', async () => {
13
13
  const wrapper = await mount(ClassicButtonGroup, {
14
- props: { options, modelValue: ['PODCAST'] },
14
+ props: { options, value: ['PODCAST'] },
15
15
  });
16
16
  expect(wrapper.findAll('button')).toHaveLength(3);
17
17
  });
18
18
 
19
19
  it('applies active class to selected options only', async () => {
20
20
  const wrapper = await mount(ClassicButtonGroup, {
21
- props: { options, modelValue: ['PODCAST', 'LIVE'] },
21
+ props: { options, value: ['PODCAST', 'LIVE'] },
22
22
  });
23
23
  const buttons = wrapper.findAll('button');
24
24
  expect(buttons[0].classes()).toContain('active');
@@ -26,27 +26,27 @@ describe('ClassicButtonGroup', () => {
26
26
  expect(buttons[2].classes()).toContain('active');
27
27
  });
28
28
 
29
- it('emits update:modelValue with added value when clicking inactive button', async () => {
29
+ it('emits update:value with added value when clicking inactive button', async () => {
30
30
  const wrapper = await mount(ClassicButtonGroup, {
31
- props: { options, modelValue: ['PODCAST'] },
31
+ props: { options, value: ['PODCAST'] },
32
32
  });
33
33
  await wrapper.findAll('button')[1].trigger('click');
34
- expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([['PODCAST', 'VIDEO']]);
34
+ expect(wrapper.emitted('update:value')?.[0]).toEqual([['PODCAST', 'VIDEO']]);
35
35
  });
36
36
 
37
- it('emits update:modelValue without removed value when clicking active button (not last)', async () => {
37
+ it('emits update:value without removed value when clicking active button (not last)', async () => {
38
38
  const wrapper = await mount(ClassicButtonGroup, {
39
- props: { options, modelValue: ['PODCAST', 'VIDEO'] },
39
+ props: { options, value: ['PODCAST', 'VIDEO'] },
40
40
  });
41
41
  await wrapper.findAll('button')[0].trigger('click');
42
- expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([['VIDEO']]);
42
+ expect(wrapper.emitted('update:value')?.[0]).toEqual([['VIDEO']]);
43
43
  });
44
44
 
45
45
  it('does not emit when clicking the only active button', async () => {
46
46
  const wrapper = await mount(ClassicButtonGroup, {
47
- props: { options, modelValue: ['PODCAST'] },
47
+ props: { options, value: ['PODCAST'] },
48
48
  });
49
49
  await wrapper.findAll('button')[0].trigger('click');
50
- expect(wrapper.emitted('update:modelValue')).toBeUndefined();
50
+ expect(wrapper.emitted('update:value')).toBeUndefined();
51
51
  });
52
52
  });