@teammodlr/ui-kit 3.0.0-beta.24 → 3.0.0-beta.28

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.
@@ -16,7 +16,7 @@
16
16
  <div :class="selectComputedClasses">
17
17
  <template v-if="multiple && Array.isArray(selected) && selected.length > 0">
18
18
  <MTag v-for="value in selectedMax" :key="value" :closeable="true"
19
- @close="event => removeSelected(event, value)" :size="size">
19
+ @close="() => removeSelected(value)" :size="size">
20
20
  {{ getSelectedLabel(value) }}
21
21
  </MTag>
22
22
  <template v-if="selectedCountNotShown > 0">
@@ -71,24 +71,30 @@
71
71
  <input v-if="searchable" type="text" ref="searchRef" v-model="search" placeholder="Search"
72
72
  :class="style.searchInput" :data-input="`select-menu-search-${id}`" />
73
73
  </div>
74
- <div :class="style.options">
75
- <div v-for="(option, index) in computedOptions" :key="index" ref="optionsRef" :class="{
76
- [style.option]: true,
77
- [style.optionSelected]: option.active,
78
- [style.optionFocused]: selectedIndex === index
79
- }" @click.prevent="option.onClick" @mouseenter="selectedIndex = index"
80
- :data-element="`select-menu-option-${id}-${option.value}`">
81
- <span :class="style.optionIcon">
82
- <svg v-if="option.active" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
83
- <path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd"></path>
84
- </svg>
85
- </span>
86
- <span :class="style.optionLabel">{{ option.label }}</span>
87
- </div>
88
- <template v-if="computedOptions.length === 0">
89
- <div :class="style.noResults">No results</div>
74
+ <div v-if="computedOptions.length === 0" :class="style.noResults">No results</div>
75
+ <VList
76
+ v-else
77
+ ref="virtualListRef"
78
+ :data="computedOptions"
79
+ :item-size="optionItemHeight"
80
+ :style="{ height: optionsHeight + 'px', padding: '4px' }"
81
+ >
82
+ <template #default="{ item, index }">
83
+ <div :class="[optionSizeClass, {
84
+ [style.option]: true,
85
+ [style.optionSelected]: item.active,
86
+ [style.optionFocused]: selectedIndex === index
87
+ }]" @click.prevent="item.onClick" @mouseenter="selectedIndex = index"
88
+ :data-element="`select-menu-option-${id}-${item.value}`">
89
+ <span :class="style.optionIcon">
90
+ <svg v-if="item.active" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
91
+ <path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd"></path>
92
+ </svg>
93
+ </span>
94
+ <span :class="style.optionLabel">{{ item.label }}</span>
95
+ </div>
90
96
  </template>
91
- </div>
97
+ </VList>
92
98
  </PopoverContent>
93
99
  </Transition>
94
100
  </PopoverPortal>
@@ -115,7 +121,12 @@ import {
115
121
  type PropType
116
122
  } from 'vue';
117
123
  import MTag from '../tag/Tag.vue';
124
+ import { VList } from 'virtua/vue';
118
125
  import * as style from './style';
126
+
127
+ interface VListHandle {
128
+ scrollToIndex(index: number, opts?: { align?: 'start' | 'center' | 'end' | 'nearest' }): void;
129
+ }
119
130
  import { fade } from '../../styles/transitions.css';
120
131
  import { useInteractive } from '../../composables/useInteractive';
121
132
  import type { OnUpdateValue, OnUpdateValueImpl } from './types';
@@ -127,7 +138,9 @@ let shouldOpenNextFocusedSelect = false;
127
138
  export default defineComponent({
128
139
  name: 'MSelect',
129
140
  emits: {
130
- ...useInput.emits()
141
+ ...useInput.emits(),
142
+ 'on-open': () => true,
143
+ 'on-close': () => true
131
144
  },
132
145
  props: {
133
146
  ...useInput.props(),
@@ -222,6 +235,7 @@ export default defineComponent({
222
235
  },
223
236
  components: {
224
237
  MTag,
238
+ VList,
225
239
  PopoverContent,
226
240
  PopoverPortal,
227
241
  PopoverRoot,
@@ -229,7 +243,7 @@ export default defineComponent({
229
243
  },
230
244
  setup(props, { emit, expose }) {
231
245
  const container = ref<HTMLElement | null>(null);
232
- const optionsRef = ref<HTMLElement[]>([]);
246
+ const virtualListRef = ref<VListHandle | null>(null);
233
247
  const searchRef = ref<HTMLInputElement>();
234
248
  const selectRef = ref<HTMLElement>();
235
249
  const popper = ref<HTMLElement>();
@@ -601,9 +615,7 @@ export default defineComponent({
601
615
  }
602
616
  }
603
617
 
604
- function removeSelected(event: MouseEvent, value: string | number) {
605
- event.preventDefault();
606
- event.stopPropagation();
618
+ function removeSelected(value: string | number) {
607
619
  if (!props.multiple) {
608
620
  return;
609
621
  }
@@ -749,6 +761,13 @@ export default defineComponent({
749
761
  ]
750
762
  })
751
763
 
764
+ const optionItemHeightMap: Record<string, number> = { xs: 30, sm: 32, md: 38, lg: 42, xl: 46 };
765
+ const optionItemHeight = computed(() => optionItemHeightMap[props.size] ?? 38);
766
+ const optionsHeight = computed(() => {
767
+ const total = computedOptions.value.length * optionItemHeight.value + 8; // 8px = paddingTop+paddingBottom
768
+ return Math.min(total, 222);
769
+ });
770
+ const optionSizeClass = computed(() => style.optionSizes[props.size]);
752
771
  const fillRootClass = computed(() => props.fill ? style.fillRoot : undefined);
753
772
  const fillLabelClass = computed(() => props.fill ? style.fillLabel : undefined);
754
773
 
@@ -800,21 +819,20 @@ export default defineComponent({
800
819
  if (!keyboardNavigation.value) {
801
820
  return;
802
821
  }
803
- if (typeof index !== 'undefined' && optionsRef.value) {
804
- optionsRef.value[index].scrollIntoView({
805
- block: 'nearest',
806
- inline: 'nearest'
807
- });
822
+ if (typeof index !== 'undefined') {
823
+ virtualListRef.value?.scrollToIndex(index, { align: 'nearest' });
808
824
  }
809
825
  });
810
826
 
811
827
  watch(open, (value: boolean) => {
812
828
  nextTick(() => {
813
829
  if (!value) {
830
+ emit('on-close');
814
831
  search.value = '';
815
832
  }
816
833
 
817
834
  if (value) {
835
+ emit('on-open');
818
836
  zIndex.value = nextZIndex();
819
837
  }
820
838
 
@@ -832,7 +850,7 @@ export default defineComponent({
832
850
  });
833
851
  }
834
852
 
835
- if (value && optionsRef.value) {
853
+ if (value) {
836
854
  const selectedValue = Array.isArray(selected.value)
837
855
  ? selected.value[0]
838
856
  : selected.value;
@@ -840,9 +858,7 @@ export default defineComponent({
840
858
  i => i.value === selectedValue
841
859
  );
842
860
  if (index !== -1) {
843
- optionsRef.value[index].scrollIntoView({
844
- block: 'nearest'
845
- });
861
+ nextTick(() => virtualListRef.value?.scrollToIndex(index, { align: 'nearest' }));
846
862
  selectedIndex.value = index;
847
863
  } else if (computedOptions.value.length > 0) {
848
864
  selectedIndex.value = 0;
@@ -910,7 +926,7 @@ export default defineComponent({
910
926
 
911
927
  return {
912
928
  container,
913
- optionsRef,
929
+ virtualListRef,
914
930
  searchRef,
915
931
  selectRef,
916
932
  popper,
@@ -945,6 +961,9 @@ export default defineComponent({
945
961
  focused,
946
962
  fade,
947
963
  toggleAllItems,
964
+ optionItemHeight,
965
+ optionsHeight,
966
+ optionSizeClass,
948
967
  fillRootClass,
949
968
  fillLabelClass
950
969
  };
@@ -189,8 +189,7 @@ export const menu = style({
189
189
  borderRadius: '4px',
190
190
  border: `1px solid ${themeVars.color.gray[200]}`,
191
191
  boxShadow: themeVars.shadow.md,
192
- maxHeight: '230px',
193
- overflowY: 'auto'
192
+ overflow: 'hidden'
194
193
  });
195
194
 
196
195
  export const placeholder = style({
@@ -199,11 +198,7 @@ export const placeholder = style({
199
198
  });
200
199
 
201
200
  export const search = style({
202
- // padding: `${themeVars.space[1]} ${themeVars.space[1]} 0`,
203
201
  backgroundColor: '#FFFFFF',
204
- position: 'sticky',
205
- top: 0,
206
- zIndex: 1,
207
202
  borderBottom: `1px solid ${themeVars.color.gray[200]}`,
208
203
  display: 'flex'
209
204
  });
@@ -219,10 +214,7 @@ export const searchInput = style({
219
214
  });
220
215
 
221
216
  export const options = style({
222
- padding: themeVars.space[1],
223
- display: 'flex',
224
- flexDirection: 'column',
225
- gap: themeVars.space[0.5]
217
+ overflow: 'hidden'
226
218
  });
227
219
 
228
220
  export const option = style({
@@ -234,10 +226,7 @@ export const option = style({
234
226
  display: 'flex',
235
227
  alignItems: 'center',
236
228
  cursor: 'pointer',
237
- minHeight: '36px',
238
- fontSize: themeVars.fontSize.base,
239
- paddingBlock: themeVars.space['1.5'],
240
- paddingInline: themeVars.space['3'],
229
+ marginBottom: '2px',
241
230
  gap: themeVars.space[2],
242
231
  color: vars.optionColor,
243
232
  transition: 'background-color 0.05s ease-in-out',
@@ -280,6 +269,39 @@ export const optionSelected = style({
280
269
 
281
270
  export const optionLabel = style({});
282
271
 
272
+ export const optionSizes = styleVariants({
273
+ xs: {
274
+ minHeight: '28px',
275
+ fontSize: themeVars.fontSize.small,
276
+ paddingBlock: themeVars.space['1'],
277
+ paddingInline: themeVars.space['2']
278
+ },
279
+ sm: {
280
+ minHeight: '30px',
281
+ fontSize: themeVars.fontSize.base,
282
+ paddingBlock: themeVars.space['1'],
283
+ paddingInline: themeVars.space['2.5']
284
+ },
285
+ md: {
286
+ minHeight: '36px',
287
+ fontSize: themeVars.fontSize.base,
288
+ paddingBlock: themeVars.space['1.5'],
289
+ paddingInline: themeVars.space['3']
290
+ },
291
+ lg: {
292
+ minHeight: '40px',
293
+ fontSize: themeVars.fontSize.large,
294
+ paddingBlock: themeVars.space['2'],
295
+ paddingInline: themeVars.space['3']
296
+ },
297
+ xl: {
298
+ minHeight: '44px',
299
+ fontSize: themeVars.fontSize.large,
300
+ paddingBlock: themeVars.space['2.5'],
301
+ paddingInline: themeVars.space['3']
302
+ }
303
+ });
304
+
283
305
  export const checkAll = style({
284
306
  display: 'flex',
285
307
  alignItems: 'center',