@weni/unnnic-system 3.34.0 → 3.34.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weni/unnnic-system",
3
- "version": "3.34.0",
3
+ "version": "3.34.2",
4
4
  "type": "commonjs",
5
5
  "files": [
6
6
  "dist",
@@ -879,7 +879,10 @@ function getStartAndEndDateByPeriod(period: string) {
879
879
  const daysMatch = period.match(/^last-(\d+)-days$/);
880
880
  const monthsMatch = period.match(/^last-(\d+)-months$/);
881
881
 
882
- if (daysMatch) {
882
+ if (period === 'today') {
883
+ periodStartDate = dateToString(todayClone);
884
+ periodEndDate = dateToString(todayClone);
885
+ } else if (daysMatch) {
883
886
  const howMuch = Number(daysMatch[1]);
884
887
 
885
888
  periodEndDate = dateToString(todayClone);
@@ -95,6 +95,28 @@ describe('DatePicker.vue', () => {
95
95
  expect(updateEquivalent[0][0]).toBe('Last 7 days');
96
96
  });
97
97
 
98
+ it('submits with today period and emits equivalent option name', async () => {
99
+ wrapper = factory({
100
+ options: [
101
+ { name: 'Today', id: 'today' },
102
+ { name: 'Custom', id: 'custom' },
103
+ ],
104
+ });
105
+
106
+ await wrapper.vm.autoSelect('today');
107
+ await wrapper.find('[data-testid="date-picker-submit"]').trigger('click');
108
+
109
+ const submit = wrapper.emitted('submit');
110
+ const updateEquivalent = wrapper.emitted('update:equivalentOption');
111
+
112
+ expect(submit).toBeTruthy();
113
+ expect(submit[0][0]).toHaveProperty('startDate');
114
+ expect(submit[0][0]).toHaveProperty('endDate');
115
+ expect(submit[0][0].startDate).toBe(submit[0][0].endDate);
116
+
117
+ expect(updateEquivalent[0][0]).toBe('Today');
118
+ });
119
+
98
120
  it('submits with custom selection and clears equivalent option', async () => {
99
121
  wrapper.vm.optionSelected = 'custom';
100
122
  await wrapper.find('[data-testid="date-picker-submit"]').trigger('click');
@@ -28,6 +28,7 @@ const englishMonths = [
28
28
  const englishDays = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
29
29
 
30
30
  const englishPeriods: PeriodOption[] = [
31
+ { name: 'Today', id: 'today' },
31
32
  { name: 'Last 7 days', id: 'last-7-days' },
32
33
  { name: 'Last 14 days', id: 'last-14-days' },
33
34
  { name: 'Last 30 days', id: 'last-30-days' },
@@ -96,6 +97,10 @@ buttons.en = buttons['en-us'];
96
97
 
97
98
  export const periods: Record<string, PeriodOption[]> = {
98
99
  'pt-br': [
100
+ {
101
+ name: 'Hoje',
102
+ id: 'today',
103
+ },
99
104
  {
100
105
  name: 'Últimos 7 dias',
101
106
  id: 'last-7-days',
@@ -128,6 +133,10 @@ export const periods: Record<string, PeriodOption[]> = {
128
133
  en: englishPeriods,
129
134
  'en-us': englishPeriods,
130
135
  es: [
136
+ {
137
+ name: 'Hoy',
138
+ id: 'today',
139
+ },
131
140
  {
132
141
  name: 'Últimos 7 días',
133
142
  id: 'last-7-days',
@@ -1,8 +1,36 @@
1
1
  import { mount, flushPromises } from '@vue/test-utils';
2
- import { beforeEach, describe, expect, afterEach, test } from 'vitest';
2
+ import { beforeEach, describe, expect, afterEach, test, vi } from 'vitest';
3
3
  import UnnnicMultiSelect from '../index.vue';
4
4
  import i18n from '@/utils/plugins/i18n';
5
5
 
6
+ const { infiniteScrollResetMock, useInfiniteScrollMock } = vi.hoisted(() => {
7
+ const infiniteScrollResetMock = vi.fn();
8
+ const useInfiniteScrollMock = vi.fn((_element, _onLoadMore, _options) => ({
9
+ reset: infiniteScrollResetMock,
10
+ isLoading: { value: false },
11
+ }));
12
+ return { infiniteScrollResetMock, useInfiniteScrollMock };
13
+ });
14
+
15
+ vi.mock('@vueuse/core', async (importOriginal) => {
16
+ const actual = await importOriginal();
17
+ return {
18
+ ...actual,
19
+ useInfiniteScroll: (...args) => useInfiniteScrollMock(...args),
20
+ };
21
+ });
22
+
23
+ function getInfiniteScrollCallbacks() {
24
+ const lastCall = useInfiniteScrollMock.mock.calls.at(-1);
25
+ return {
26
+ onLoadMore: lastCall?.[1],
27
+ canLoadMore: lastCall?.[2]?.canLoadMore,
28
+ };
29
+ }
30
+
31
+ const visibleScrollEl = { clientHeight: 200 };
32
+ const overflowingScrollEl = { clientHeight: 200, scrollHeight: 800 };
33
+
6
34
  describe('UnnnicMultiSelect.vue', () => {
7
35
  let wrapper;
8
36
 
@@ -32,6 +60,8 @@ describe('UnnnicMultiSelect.vue', () => {
32
60
  };
33
61
 
34
62
  beforeEach(() => {
63
+ infiniteScrollResetMock.mockClear();
64
+ useInfiniteScrollMock.mockClear();
35
65
  wrapper = mountWrapper();
36
66
  });
37
67
 
@@ -627,6 +657,176 @@ describe('UnnnicMultiSelect.vue', () => {
627
657
  });
628
658
  });
629
659
 
660
+ describe('infinite scroll functionality', () => {
661
+ test('infinite scroll is disabled by default', () => {
662
+ expect(wrapper.vm.infiniteScroll).toBe(false);
663
+ });
664
+
665
+ test('applies infinite scroll props correctly', async () => {
666
+ await wrapper.setProps({
667
+ infiniteScroll: true,
668
+ infiniteScrollDistance: 20,
669
+ infiniteScrollCanLoadMore: () => false,
670
+ });
671
+
672
+ expect(wrapper.vm.infiniteScroll).toBe(true);
673
+ expect(wrapper.vm.infiniteScrollDistance).toBe(20);
674
+ expect(wrapper.vm.infiniteScrollCanLoadMore()).toBe(false);
675
+ });
676
+
677
+ test('does not render loading when infiniteScrollLoading is false', async () => {
678
+ await wrapper.setProps({ infiniteScroll: true });
679
+ wrapper.vm.setOpenPopover(true);
680
+ await wrapper.vm.$nextTick();
681
+
682
+ const loading = wrapper.find('.unnnic-multi-select__infinite-loading');
683
+ expect(loading.exists()).toBe(false);
684
+ });
685
+
686
+ test('sets infiniteScrollLoading to true and verifies state', async () => {
687
+ await wrapper.setProps({
688
+ infiniteScroll: true,
689
+ options: [
690
+ { label: 'Option 1', value: 'option1' },
691
+ { label: 'Option 2', value: 'option2' },
692
+ ],
693
+ });
694
+
695
+ wrapper.vm.setOpenPopover(true);
696
+ await wrapper.vm.$nextTick();
697
+
698
+ expect(wrapper.vm.infiniteScrollLoading).toBe(false);
699
+
700
+ wrapper.vm.infiniteScrollLoading = true;
701
+ await wrapper.vm.$nextTick();
702
+
703
+ expect(wrapper.vm.infiniteScrollLoading).toBe(true);
704
+ expect(wrapper.vm.infiniteScroll).toBe(true);
705
+ });
706
+
707
+ test('finishInfiniteScroll sets loading to false', async () => {
708
+ await wrapper.setProps({ infiniteScroll: true });
709
+ wrapper.vm.infiniteScrollLoading = true;
710
+ expect(wrapper.vm.infiniteScrollLoading).toBe(true);
711
+
712
+ wrapper.vm.finishInfiniteScroll();
713
+ expect(wrapper.vm.infiniteScrollLoading).toBe(false);
714
+ });
715
+
716
+ test('resetInfiniteScroll sets loading to false', async () => {
717
+ await wrapper.setProps({ infiniteScroll: true });
718
+ wrapper.vm.infiniteScrollLoading = true;
719
+ expect(wrapper.vm.infiniteScrollLoading).toBe(true);
720
+
721
+ wrapper.vm.resetInfiniteScroll();
722
+ expect(wrapper.vm.infiniteScrollLoading).toBe(false);
723
+ });
724
+
725
+ test('calls useInfiniteScroll once and does not recreate it on finish', async () => {
726
+ wrapper.unmount();
727
+ useInfiniteScrollMock.mockClear();
728
+ infiniteScrollResetMock.mockClear();
729
+
730
+ const scrollWrapper = mountWrapper({ infiniteScroll: true });
731
+ expect(useInfiniteScrollMock).toHaveBeenCalledTimes(1);
732
+
733
+ scrollWrapper.vm.setOpenPopover(true);
734
+ await scrollWrapper.vm.$nextTick();
735
+ scrollWrapper.vm.infiniteScrollLoading = true;
736
+ scrollWrapper.vm.finishInfiniteScroll();
737
+ await scrollWrapper.vm.$nextTick();
738
+
739
+ expect(useInfiniteScrollMock).toHaveBeenCalledTimes(1);
740
+ expect(infiniteScrollResetMock).toHaveBeenCalled();
741
+
742
+ scrollWrapper.unmount();
743
+ });
744
+
745
+ test('emits scroll-end once when the list does not fill the popover', async () => {
746
+ const fewOptions = [
747
+ { label: 'Option 1', value: 'option1' },
748
+ { label: 'Option 2', value: 'option2' },
749
+ ];
750
+ const scrollWrapper = mountWrapper({
751
+ infiniteScroll: true,
752
+ options: fewOptions,
753
+ });
754
+
755
+ scrollWrapper.vm.setOpenPopover(true);
756
+ await scrollWrapper.vm.$nextTick();
757
+
758
+ const { onLoadMore, canLoadMore } = getInfiniteScrollCallbacks();
759
+ expect(canLoadMore(visibleScrollEl)).toBe(true);
760
+ expect(scrollWrapper.emitted('scroll-end')).toBeFalsy();
761
+
762
+ onLoadMore();
763
+ expect(scrollWrapper.emitted('scroll-end')).toHaveLength(1);
764
+ expect(scrollWrapper.vm.infiniteScrollLoading).toBe(true);
765
+ expect(canLoadMore(visibleScrollEl)).toBe(false);
766
+
767
+ onLoadMore();
768
+ expect(scrollWrapper.emitted('scroll-end')).toHaveLength(1);
769
+
770
+ scrollWrapper.unmount();
771
+ });
772
+
773
+ test('does not emit scroll-end on open when the list already overflows', async () => {
774
+ const manyOptions = Array.from({ length: 20 }, (_, i) => ({
775
+ label: `Option ${i + 1}`,
776
+ value: `option${i + 1}`,
777
+ }));
778
+ const scrollWrapper = mountWrapper({
779
+ infiniteScroll: true,
780
+ options: manyOptions,
781
+ optionsLines: 5,
782
+ });
783
+
784
+ scrollWrapper.vm.setOpenPopover(true);
785
+ await scrollWrapper.vm.$nextTick();
786
+
787
+ expect(scrollWrapper.emitted('scroll-end')).toBeFalsy();
788
+
789
+ const { canLoadMore } = getInfiniteScrollCallbacks();
790
+ expect(canLoadMore(overflowingScrollEl)).toBe(true);
791
+ expect(canLoadMore({ clientHeight: 0 })).toBe(false);
792
+
793
+ scrollWrapper.unmount();
794
+ });
795
+
796
+ test('canLoadMore is false when infinite scroll is disabled or the popover is closed', () => {
797
+ const { canLoadMore } = getInfiniteScrollCallbacks();
798
+ expect(canLoadMore(visibleScrollEl)).toBe(false);
799
+
800
+ wrapper.vm.setOpenPopover(true);
801
+ expect(canLoadMore(visibleScrollEl)).toBe(false);
802
+ });
803
+
804
+ test('displays loading indicator when infiniteScrollLoading is true', async () => {
805
+ await wrapper.setProps({ infiniteScroll: true });
806
+ wrapper.vm.setOpenPopover(true);
807
+ await wrapper.vm.$nextTick();
808
+
809
+ const { onLoadMore } = getInfiniteScrollCallbacks();
810
+ onLoadMore();
811
+ await wrapper.vm.$nextTick();
812
+
813
+ expect(wrapper.vm.infiniteScrollLoading).toBe(true);
814
+ expect(
815
+ document.querySelector('.unnnic-multi-select__infinite-loading'),
816
+ ).not.toBeNull();
817
+ });
818
+
819
+ test('does not display infinite scroll loading when infiniteScroll is false', async () => {
820
+ await wrapper.setProps({ infiniteScroll: false });
821
+ wrapper.vm.setOpenPopover(true);
822
+ await wrapper.vm.$nextTick();
823
+
824
+ expect(
825
+ document.querySelector('.unnnic-multi-select__infinite-loading'),
826
+ ).toBeNull();
827
+ });
828
+ });
829
+
630
830
  describe('snapshot testing', () => {
631
831
  test('matches snapshot with default props', () => {
632
832
  expect(wrapper.html()).toMatchSnapshot();
@@ -651,5 +851,17 @@ describe('UnnnicMultiSelect.vue', () => {
651
851
  await wrapper.setProps({ disabled: true });
652
852
  expect(wrapper.html()).toMatchSnapshot();
653
853
  });
854
+
855
+ test('matches snapshot with infinite scroll enabled', async () => {
856
+ await wrapper.setProps({ infiniteScroll: true });
857
+ wrapper.vm.setOpenPopover(true);
858
+ await wrapper.vm.$nextTick();
859
+
860
+ const { onLoadMore } = getInfiniteScrollCallbacks();
861
+ onLoadMore();
862
+ await wrapper.vm.$nextTick();
863
+
864
+ expect(wrapper.html()).toMatchSnapshot();
865
+ });
654
866
  });
655
867
  });
@@ -36,6 +36,24 @@ exports[`UnnnicMultiSelect.vue > snapshot testing > matches snapshot with disabl
36
36
  </div>"
37
37
  `;
38
38
 
39
+ exports[`UnnnicMultiSelect.vue > snapshot testing > matches snapshot with infinite scroll enabled 1`] = `
40
+ "<div data-v-03c7fb50="" class="unnnic-multi-select"><button data-v-9d52eef8="" data-v-03c7fb50="" class="unnnic-popover-trigger w-full" id="reka-popover-trigger-v-0" type="button" aria-haspopup="dialog" aria-expanded="true" aria-controls="reka-popover-content-v-1" data-state="open">
41
+ <section data-v-9f8d6c86="" data-v-d890ad85="" data-v-03c7fb50="" class="unnnic-form-element unnnic-form md unnnic-multi-select__input" data-testid="form-element">
42
+ <!--v-if-->
43
+ <div data-v-a0d36167="" data-v-d890ad85="" class="text-input size--md unnnic-multi-select__input unnnic-form-input" hascloudycolor="false" mask=""><input data-v-86533b41="" data-v-a0d36167="" class="unnnic-multi-select__input unnnic-form-input input-itself input size-md normal input--has-icon-right focus use-focus-prop unnnic-multi-select__input unnnic-form-input input-itself" hascloudycolor="false" placeholder="" iconleft="" iconright="keyboard_arrow_up" iconleftclickable="false" iconrightclickable="false" showclear="false" type="text" readonly="" value="">
44
+ <!--v-if-->
45
+ <section data-v-a0d36167="" class="icon-right-container">
46
+ <!--v-if--><span data-v-26446d8e="" data-v-a0d36167="" class="unnnic-icon material-symbols-rounded unnnic-icon-scheme--fg-base unnnic-icon-size--ant unnnic-icon__size--ant icon-right" data-testid="material-icon" translate="no">keyboard_arrow_up</span>
47
+ </section>
48
+ </div>
49
+ <!--v-if-->
50
+ </section>
51
+ </button>
52
+ <!--teleport start-->
53
+ <!--teleport end-->
54
+ </div>"
55
+ `;
56
+
39
57
  exports[`UnnnicMultiSelect.vue > snapshot testing > matches snapshot with multiple selected values 1`] = `
40
58
  "<div data-v-03c7fb50="" class="unnnic-multi-select"><button data-v-9d52eef8="" data-v-03c7fb50="" class="unnnic-popover-trigger w-full" id="reka-popover-trigger-v-0" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="" data-state="closed">
41
59
  <section data-v-9f8d6c86="" data-v-d890ad85="" data-v-03c7fb50="" class="unnnic-form-element unnnic-form md unnnic-multi-select__input" data-testid="form-element">
@@ -47,21 +47,29 @@
47
47
  >
48
48
  {{ $t('without_results') }}
49
49
  </p>
50
- <div
51
- v-else
52
- class="unnnic-multi-select__options"
53
- >
54
- <UnnnicMultiSelectOption
55
- v-for="(option, index) in filteredOptions"
56
- :key="String(option[props.itemValue])"
57
- :data-option-index="index"
58
- :label="String(option[props.itemLabel] ?? '')"
59
- :active="getActivatedOptionStatus(option)"
60
- :focused="focusedOptionIndex === index"
61
- :disabled="option.disabled"
62
- @update:model-value="handleSelectOption(option, $event)"
63
- />
64
- </div>
50
+ <template v-else>
51
+ <div class="unnnic-multi-select__options">
52
+ <UnnnicMultiSelectOption
53
+ v-for="(option, index) in filteredOptions"
54
+ :key="String(option[props.itemValue])"
55
+ :data-option-index="index"
56
+ :label="String(option[props.itemLabel] ?? '')"
57
+ :active="getActivatedOptionStatus(option)"
58
+ :focused="focusedOptionIndex === index"
59
+ :disabled="option.disabled"
60
+ @update:model-value="handleSelectOption(option, $event)"
61
+ />
62
+ </div>
63
+ <div
64
+ v-if="props.infiniteScroll && infiniteScrollLoading"
65
+ class="unnnic-multi-select__infinite-loading"
66
+ >
67
+ <UnnnicIconLoading
68
+ scheme="fg-base"
69
+ size="sm"
70
+ />
71
+ </div>
72
+ </template>
65
73
  </div>
66
74
  </PopoverContent>
67
75
  </Popover>
@@ -69,10 +77,13 @@
69
77
  </template>
70
78
 
71
79
  <script setup lang="ts">
72
- import { computed, watch, useTemplateRef } from 'vue';
80
+ import { computed, ref, watch, nextTick, useTemplateRef } from 'vue';
81
+
82
+ import { useInfiniteScroll } from '@vueuse/core';
73
83
 
74
84
  import { Popover, PopoverTrigger, PopoverContent } from '../ui/popover';
75
85
  import UnnnicInput from '../Input/Input.vue';
86
+ import UnnnicIconLoading from '../IconLoading/IconLoading.vue';
76
87
  import UnnnicMultiSelectOption from './MultiSelectOption.vue';
77
88
 
78
89
  import { useSelectBase } from '../../composables/useSelectBase';
@@ -85,6 +96,9 @@ defineOptions({
85
96
 
86
97
  interface MultiSelectProps extends SelectBaseProps {
87
98
  modelValue: (SelectOption | unknown)[];
99
+ infiniteScroll?: boolean;
100
+ infiniteScrollDistance?: number;
101
+ infiniteScrollCanLoadMore?: () => boolean;
88
102
  }
89
103
 
90
104
  const props = withDefaults(defineProps<MultiSelectProps>(), {
@@ -103,15 +117,19 @@ const props = withDefaults(defineProps<MultiSelectProps>(), {
103
117
  message: '',
104
118
  search: '',
105
119
  clearable: false,
120
+ infiniteScroll: false,
121
+ infiniteScrollDistance: 10,
122
+ infiniteScrollCanLoadMore: () => true,
106
123
  });
107
124
 
108
125
  const emit = defineEmits<{
109
126
  'update:modelValue': [value: (SelectOption | unknown)[]];
110
127
  'update:search': [value: string];
128
+ 'scroll-end': [];
111
129
  }>();
112
130
 
113
131
  const multiSelectInputRef = useTemplateRef<HTMLElement>('multiSelectInputRef');
114
- const contentRef = useTemplateRef<HTMLElement>('contentRef');
132
+ const contentRef = useTemplateRef<HTMLDivElement>('contentRef');
115
133
 
116
134
  const base = useSelectBase(props, 'multi', multiSelectInputRef, contentRef);
117
135
 
@@ -184,14 +202,66 @@ keyboard.setupKeydownBinding();
184
202
 
185
203
  const focusedOptionIndex = keyboard.focusedOptionIndex;
186
204
 
187
- watch(openPopover, () => {
188
- if (!base.openPopover.value) {
205
+ const infiniteScrollLoading = ref(false);
206
+
207
+ function canLoadMoreInfiniteScroll(el?: unknown) {
208
+ if (
209
+ !props.infiniteScroll ||
210
+ !base.openPopover.value ||
211
+ infiniteScrollLoading.value ||
212
+ !props.infiniteScrollCanLoadMore()
213
+ ) {
214
+ return false;
215
+ }
216
+
217
+ if (!el || typeof el !== 'object' || !('clientHeight' in el)) return false;
218
+ return (el as HTMLElement).clientHeight > 0;
219
+ }
220
+
221
+ const scrollElement = computed(
222
+ () =>
223
+ (contentRef.value?.closest('.unnnic-popover') as HTMLElement | null) ??
224
+ contentRef.value,
225
+ );
226
+
227
+ const { reset: resetInfiniteScrollObserver } = useInfiniteScroll(
228
+ scrollElement,
229
+ () => {
230
+ if (infiniteScrollLoading.value) return;
231
+ infiniteScrollLoading.value = true;
232
+ emit('scroll-end');
233
+ },
234
+ {
235
+ distance: props.infiniteScrollDistance,
236
+ canLoadMore: canLoadMoreInfiniteScroll,
237
+ },
238
+ );
239
+
240
+ watch(base.openPopover, (isOpen) => {
241
+ if (!isOpen) {
189
242
  handleSearch('');
190
- } else {
191
- keyboard.focusedOptionIndex.value = -1;
243
+ infiniteScrollLoading.value = false;
244
+ return;
245
+ }
246
+
247
+ keyboard.focusedOptionIndex.value = -1;
248
+ if (props.infiniteScroll) {
249
+ nextTick(() => resetInfiniteScrollObserver());
192
250
  }
193
251
  });
194
252
 
253
+ function finishInfiniteScroll() {
254
+ infiniteScrollLoading.value = false;
255
+ if (base.openPopover.value && props.infiniteScroll) {
256
+ nextTick(() => resetInfiniteScrollObserver());
257
+ }
258
+ }
259
+
260
+ function resetInfiniteScroll() {
261
+ infiniteScrollLoading.value = false;
262
+ nextTick(() => resetInfiniteScrollObserver());
263
+ }
264
+
195
265
  defineExpose({
196
266
  openPopover,
197
267
  setOpenPopover,
@@ -199,6 +269,9 @@ defineExpose({
199
269
  calculatedPopoverHeight: base.calculatedPopoverHeight,
200
270
  selectedItems,
201
271
  inputValue,
272
+ infiniteScrollLoading,
273
+ finishInfiniteScroll,
274
+ resetInfiniteScroll,
202
275
  });
203
276
  </script>
204
277
 
@@ -236,5 +309,13 @@ defineExpose({
236
309
  flex-direction: column;
237
310
  gap: $unnnic-space-6;
238
311
  }
312
+
313
+ &__infinite-loading {
314
+ display: flex;
315
+ justify-content: center;
316
+ align-items: center;
317
+ padding: $unnnic-space-2 0;
318
+ min-height: $unnnic-space-10;
319
+ }
239
320
  }
240
321
  </style>
@@ -82,6 +82,18 @@ export default {
82
82
  disabled: {
83
83
  description: 'Disable the select.',
84
84
  },
85
+ infiniteScroll: {
86
+ description:
87
+ 'Enable infinite scroll functionality. When enabled, the component will emit a `scroll-end` event when the user scrolls near the bottom of the options list.',
88
+ },
89
+ infiniteScrollDistance: {
90
+ description:
91
+ 'Distance in pixels from the bottom of the scroll area to trigger the `scroll-end` event. Default is 10.',
92
+ },
93
+ infiniteScrollCanLoadMore: {
94
+ description:
95
+ 'Function that returns a boolean indicating whether more items can be loaded. Used to prevent unnecessary scroll-end events.',
96
+ },
85
97
  },
86
98
  render: (args) => ({
87
99
  components: { UnnnicMultiSelect },
@@ -167,3 +179,77 @@ export const WithSearch = {
167
179
  search: '',
168
180
  },
169
181
  };
182
+
183
+ export const WithInfiniteScroll = {
184
+ render: () => ({
185
+ components: { UnnnicMultiSelect },
186
+ data() {
187
+ return {
188
+ selectedValue: [],
189
+ loadedOptions: [],
190
+ currentPage: 1,
191
+ totalPages: 10,
192
+ isLoading: false,
193
+ };
194
+ },
195
+ mounted() {
196
+ this.loadInitialOptions();
197
+ },
198
+ methods: {
199
+ loadInitialOptions() {
200
+ this.loadedOptions = this.generateOptions(1);
201
+ },
202
+ generateOptions(page) {
203
+ const startIndex = (page - 1) * 10 + 1;
204
+ return Array.from({ length: 10 }, (_, i) => ({
205
+ label: `Option ${startIndex + i}`,
206
+ value: `option${startIndex + i}`,
207
+ }));
208
+ },
209
+ async handleScrollEnd() {
210
+ if (this.currentPage >= this.totalPages || this.isLoading) {
211
+ return;
212
+ }
213
+
214
+ this.isLoading = true;
215
+
216
+ await new Promise((resolve) => setTimeout(resolve, 1000));
217
+
218
+ this.currentPage++;
219
+ const newOptions = this.generateOptions(this.currentPage);
220
+ this.loadedOptions = [...this.loadedOptions, ...newOptions];
221
+
222
+ this.isLoading = false;
223
+
224
+ this.$refs.multiSelectRef.finishInfiniteScroll();
225
+ },
226
+ canLoadMore() {
227
+ return this.currentPage < this.totalPages && !this.isLoading;
228
+ },
229
+ },
230
+ template: `
231
+ <div style="width: 300px;">
232
+ <h3>Infinite Scroll Example</h3>
233
+ <p style="color: #666; font-size: 14px;">
234
+ Scroll down in the options list to load more items.
235
+ <br />
236
+ Page: {{ currentPage }} / {{ totalPages }}
237
+ <br />
238
+ Total options: {{ loadedOptions.length }}
239
+ </p>
240
+ <p>Selected: {{ selectedValue }}</p>
241
+ <unnnic-multi-select
242
+ ref="multiSelectRef"
243
+ v-model="selectedValue"
244
+ :options="loadedOptions"
245
+ placeholder="Select options"
246
+ label="Infinite Scroll MultiSelect"
247
+ :infinite-scroll="true"
248
+ :infinite-scroll-distance="10"
249
+ :infinite-scroll-can-load-more="canLoadMore"
250
+ @scroll-end="handleScrollEnd"
251
+ />
252
+ </div>
253
+ `,
254
+ }),
255
+ };