@teammodlr/ui-kit 3.0.0 → 3.0.1

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 (45) hide show
  1. package/lib/components/cardcontextmenu/CardContextMenu.vue.d.ts +4 -2
  2. package/lib/components/clearbutton/ClearButton.vue.d.ts +4 -2
  3. package/lib/components/colorpicker/ColorPicker.vue.d.ts +6 -3
  4. package/lib/components/dialog/Dialog.vue.d.ts +14 -6
  5. package/lib/components/drawer/Drawer.vue.d.ts +8 -4
  6. package/lib/components/dropdown/Dropdown.vue.d.ts +4 -2
  7. package/lib/components/form/Form.vue.d.ts +4 -2
  8. package/lib/components/hovercard/HoverCard.vue.d.ts +4 -2
  9. package/lib/components/modal/Modal.vue.d.ts +11 -4
  10. package/lib/components/pagination/Pagination.vue.d.ts +6 -3
  11. package/lib/components/paneldialog/PanelDialog.vue.d.ts +14 -6
  12. package/lib/components/popover/Popover.vue.d.ts +4 -2
  13. package/lib/components/radiobuttongroup/RadioButtonGroup.vue.d.ts +4 -2
  14. package/lib/components/select/Select.vue.d.ts +22 -0
  15. package/lib/components/slider/Slider.vue.d.ts +6 -3
  16. package/lib/components/tab/Tab.vue.d.ts +10 -2
  17. package/lib/components/tab/TabGroup.vue.d.ts +6 -3
  18. package/lib/components/table/Table.vue.d.ts +4 -2
  19. package/lib/components/tree/Tree.vue.d.ts +6 -3
  20. package/lib/index.cjs.js +3 -3
  21. package/lib/index.es.js +4416 -4343
  22. package/lib/tsconfig.vitest.tsbuildinfo +1 -0
  23. package/package.json +1 -1
  24. package/src/components/cardcontextmenu/CardContextMenu.vue +3 -1
  25. package/src/components/clearbutton/ClearButton.vue +3 -1
  26. package/src/components/colorpicker/ColorPicker.vue +4 -1
  27. package/src/components/dialog/Dialog.vue +10 -1
  28. package/src/components/drawer/Drawer.vue +5 -1
  29. package/src/components/dropdown/Dropdown.vue +3 -1
  30. package/src/components/form/Form.vue +3 -3
  31. package/src/components/hovercard/HoverCard.vue +3 -1
  32. package/src/components/modal/Modal.vue +28 -11
  33. package/src/components/pagination/Pagination.vue +4 -1
  34. package/src/components/paneldialog/PanelDialog.vue +10 -1
  35. package/src/components/popover/Popover.vue +3 -1
  36. package/src/components/radiobuttongroup/RadioButtonGroup.vue +3 -1
  37. package/src/components/select/Select.vue +93 -73
  38. package/src/components/select/examples/index.stories.ts +2 -1
  39. package/src/components/select/examples/index.ts +1 -0
  40. package/src/components/select/examples/remote.vue +54 -0
  41. package/src/components/slider/Slider.vue +4 -1
  42. package/src/components/tab/Tab.vue +3 -1
  43. package/src/components/tab/TabGroup.vue +4 -4
  44. package/src/components/table/Table.vue +3 -3
  45. package/src/components/tree/Tree.vue +4 -1
@@ -8,7 +8,7 @@
8
8
  {{ description }}
9
9
  </div>
10
10
 
11
- <PopoverTrigger as-child :disabled="disabled || loading">
11
+ <PopoverTrigger as-child :disabled="blocked">
12
12
  <div :class="computedWrapperClasses" v-on="inputListeners" :id="id" ref="container"
13
13
  :aria-disabled="disabled ? 'true' : 'false'" :aria-labelledby="`${id}-label`" tabindex="0"
14
14
  data-m-select-trigger="true"
@@ -72,7 +72,8 @@
72
72
  <input v-if="searchable" type="text" ref="searchRef" v-model="search" placeholder="Search"
73
73
  :class="style.searchInput" :data-input="`select-menu-search-${id}`" />
74
74
  </div>
75
- <div v-if="computedOptions.length === 0" :class="style.noResults">No results</div>
75
+ <div v-if="showRemoteLoading" :class="style.noResults">Loading...</div>
76
+ <div v-else-if="computedOptions.length === 0" :class="style.noResults">No results</div>
76
77
  <VList
77
78
  v-else
78
79
  ref="virtualListRef"
@@ -116,6 +117,7 @@ import {
116
117
  computed,
117
118
  defineComponent,
118
119
  nextTick,
120
+ onBeforeUnmount,
119
121
  ref,
120
122
  watch,
121
123
  watchEffect,
@@ -143,7 +145,9 @@ export default defineComponent({
143
145
  ...(useInput.emits() as unknown as InputEmitsMap),
144
146
  'on-open': () => true,
145
147
  'on-close': () => true,
146
- clear: () => true
148
+ clear: () => true,
149
+ // Emitted when the search text changes (debounced when `remote`). Emits '' when the menu closes.
150
+ search: (_query: string) => true
147
151
  },
148
152
  props: {
149
153
  ...useInput.props(),
@@ -167,6 +171,17 @@ export default defineComponent({
167
171
  type: Boolean,
168
172
  default: false
169
173
  },
174
+ // Options are loaded from a server: search no longer filters locally,
175
+ // the consumer listens to `search` and updates `options`.
176
+ remote: {
177
+ type: Boolean,
178
+ default: false
179
+ },
180
+ // Debounce (ms) applied to the `search` event when `remote`
181
+ searchDebounce: {
182
+ type: Number,
183
+ default: 300
184
+ },
170
185
  size: {
171
186
  type: String as PropType<Size>,
172
187
  default: 'md'
@@ -302,6 +317,23 @@ export default defineComponent({
302
317
 
303
318
  const disabled = computed(() => formDisabled || props.disabled);
304
319
  const loading = computed(() => formLoading || props.loading);
320
+ // In remote mode `loading` is shown inside the menu (so the user can keep
321
+ // typing while a search is in flight) rather than locking the trigger.
322
+ const blocked = computed(() => disabled.value || (loading.value && !props.remote));
323
+
324
+ // Remote options come and go between searches, so remember every option we've
325
+ // seen to keep labels for selected values that are no longer in `options`.
326
+ const knownOptions = new Map<any, any>();
327
+ watch(
328
+ () => props.options,
329
+ (options) => {
330
+ if (!props.remote || !options) {
331
+ return;
332
+ }
333
+ options.forEach(option => knownOptions.set(option[props.optionValue], option));
334
+ },
335
+ { immediate: true }
336
+ );
305
337
 
306
338
  const selected = computed<any | any[]>({
307
339
  get() {
@@ -366,80 +398,44 @@ export default defineComponent({
366
398
  };
367
399
  });
368
400
 
401
+ const showRemoteLoading = computed(() => props.remote && loading.value);
402
+
369
403
  const computedOptions = computed(() => {
370
404
  if (!props.options || props.options.length == 0) {
371
405
  return [];
372
406
  }
373
407
 
374
- // Filter options by search
375
- if (props.searchable && search.value) {
376
- let options = props.options
377
- .filter(option => {
378
- return option[props.optionLabel]
379
- .toLowerCase()
380
- .includes(search.value.toLowerCase());
381
- })
382
- .map(option => {
383
- if (option[props.optionValue] === undefined) {
384
- throw new Error('Option value is undefined');
385
- }
386
-
387
- if (option[props.optionLabel] === undefined) {
388
- throw new Error('Option label is undefined');
389
- }
390
-
391
- let value = option[props.optionValue];
392
- let active: boolean;
393
- if (props.multiple && Array.isArray(selected.value)) {
394
- active = selected.value.includes(value);
395
- } else {
396
- active = selected.value === value;
397
- }
398
-
399
- return {
400
- value: value,
401
- label: option[props.optionLabel],
402
- active: active,
403
- onClick: () => handleOptionClick(value)
404
- };
405
- });
406
-
407
- if (props.allowBlank) {
408
- options.unshift({
409
- value: '',
410
- label: '',
411
- active: false,
412
- onClick: () => handleOptionClick('')
413
- });
414
- }
415
-
416
- return options;
417
- }
408
+ // Remote options are already filtered by the server
409
+ const query = props.searchable && !props.remote
410
+ ? search.value.toLowerCase()
411
+ : '';
418
412
 
419
- let options = props.options.map(option => {
420
- if (option[props.optionValue] === undefined) {
421
- throw new Error('Option value is undefined');
422
- }
413
+ const options = props.options
414
+ .filter(option => !query || option[props.optionLabel].toLowerCase().includes(query))
415
+ .map(option => {
416
+ if (option[props.optionValue] === undefined) {
417
+ throw new Error('Option value is undefined');
418
+ }
423
419
 
424
- if (option[props.optionLabel] === undefined) {
425
- throw new Error('Option label is undefined');
426
- }
420
+ if (option[props.optionLabel] === undefined) {
421
+ throw new Error('Option label is undefined');
422
+ }
427
423
 
428
- let value = option[props.optionValue];
429
- let active: boolean;
430
- if (props.multiple && Array.isArray(selected.value)) {
431
- active = selected.value.includes(value);
432
- } else {
433
- active = selected.value === value;
434
- }
424
+ const value = option[props.optionValue];
425
+ let active: boolean;
426
+ if (props.multiple && Array.isArray(selected.value)) {
427
+ active = selected.value.includes(value);
428
+ } else {
429
+ active = selected.value === value;
430
+ }
435
431
 
436
- return {
437
- value: value,
438
- label: option[props.optionLabel],
439
- active: active,
440
- onClick: () => handleOptionClick(value)
441
- };
442
- });
432
+ return {
433
+ value: value,
434
+ label: option[props.optionLabel],
435
+ active: active,
436
+ onClick: () => handleOptionClick(value)
437
+ };
438
+ });
443
439
 
444
440
  if (props.allowBlank) {
445
441
  options.unshift({
@@ -476,7 +472,7 @@ export default defineComponent({
476
472
  }
477
473
 
478
474
  function openSelect(forceOpen = false) {
479
- if (disabled.value || loading.value) {
475
+ if (blocked.value) {
480
476
  return;
481
477
  }
482
478
 
@@ -643,8 +639,7 @@ export default defineComponent({
643
639
  const showClear = computed(() => {
644
640
  return props.clearable
645
641
  && hasValue.value
646
- && !disabled.value
647
- && !loading.value;
642
+ && !blocked.value;
648
643
  });
649
644
 
650
645
  function clearSelected() {
@@ -831,7 +826,7 @@ export default defineComponent({
831
826
  [style.focused]: !props.noBorder && (open.value || focused.value),
832
827
  [style.fillBase]: props.fill,
833
828
  [style.noBorder]: props.noBorder,
834
- 'm-select--disabled': disabled.value || loading.value,
829
+ 'm-select--disabled': blocked.value,
835
830
  'm-select--focused': open.value || focused.value,
836
831
  'm-select--multiple': props.multiple,
837
832
  'm-select--error': formError.value
@@ -842,7 +837,7 @@ export default defineComponent({
842
837
  function getSelectedLabel(value: string | number | []) {
843
838
  const option = props.options.find(
844
839
  i => i[props.optionValue] === value
845
- );
840
+ ) ?? (props.remote ? knownOptions.get(value) : undefined);
846
841
  if (!option) {
847
842
  return null;
848
843
  }
@@ -865,6 +860,28 @@ export default defineComponent({
865
860
  }
866
861
  });
867
862
 
863
+ // Emit search changes. Remote searches are debounced so we don't hit the
864
+ // server on every keystroke; clearing the query (e.g. on close) is immediate.
865
+ let searchTimer: ReturnType<typeof setTimeout> | undefined;
866
+ watch(search, (query: string) => {
867
+ clearTimeout(searchTimer);
868
+
869
+ if (props.remote && props.searchDebounce > 0 && query !== '') {
870
+ searchTimer = setTimeout(() => emit('search', query), props.searchDebounce);
871
+ return;
872
+ }
873
+
874
+ emit('search', query);
875
+ });
876
+ onBeforeUnmount(() => clearTimeout(searchTimer));
877
+
878
+ // Remote options replace the list under the cursor, keep the highlight in range
879
+ watch(computedOptions, (options) => {
880
+ if (selectedIndex.value !== undefined && selectedIndex.value >= options.length) {
881
+ selectedIndex.value = 0;
882
+ }
883
+ });
884
+
868
885
  watch(open, (value: boolean) => {
869
886
  nextTick(() => {
870
887
  if (!value) {
@@ -925,6 +942,7 @@ export default defineComponent({
925
942
  const isOpen = computed(() => {
926
943
  if (
927
944
  props.multiple &&
945
+ !props.remote &&
928
946
  computedOptions.value.length == 0 &&
929
947
  !search.value
930
948
  ) {
@@ -1002,6 +1020,8 @@ export default defineComponent({
1002
1020
  inputListeners,
1003
1021
  style,
1004
1022
  focused,
1023
+ blocked,
1024
+ showRemoteLoading,
1005
1025
  fade,
1006
1026
  toggleAllItems,
1007
1027
  optionItemHeight,
@@ -1,5 +1,5 @@
1
1
  import MSelect from '../Select.vue';
2
- import { MSelectBasic, MSelectSearchable, MSelectMultiple, MSelectFill, MSelectClearable } from './index';
2
+ import { MSelectBasic, MSelectSearchable, MSelectMultiple, MSelectFill, MSelectClearable, MSelectRemote } from './index';
3
3
 
4
4
  import { createStory, sizeArgType } from '../../../__storybook__';
5
5
 
@@ -16,3 +16,4 @@ export const Searchable = createStory(MSelectSearchable);
16
16
  export const Multiple = createStory(MSelectMultiple);
17
17
  export const Fill = createStory(MSelectFill);
18
18
  export const Clearable = createStory(MSelectClearable);
19
+ export const Remote = createStory(MSelectRemote);
@@ -3,3 +3,4 @@ export { default as MSelectSearchable } from './searchable.vue';
3
3
  export { default as MSelectMultiple } from './multiple.vue';
4
4
  export { default as MSelectFill } from './fill.vue';
5
5
  export { default as MSelectClearable } from './clearable.vue';
6
+ export { default as MSelectRemote } from './remote.vue';
@@ -0,0 +1,54 @@
1
+ <template>
2
+ <MSelect
3
+ v-model="value"
4
+ :options="options"
5
+ :option-value="'id'"
6
+ :option-label="'name'"
7
+ :loading="loading"
8
+ searchable
9
+ remote
10
+ @search="onSearch"
11
+ @on-open="onSearch('')"
12
+ />
13
+ </template>
14
+
15
+ <script lang="ts" setup>
16
+ import { ref } from "vue";
17
+
18
+ const everything = Array.from({ length: 500 }, (_, i) => ({
19
+ id: i,
20
+ name: `Option ${i + 1}`
21
+ }));
22
+
23
+ // Stand-in for a server call
24
+ function fetchOptions(query: string): Promise<typeof everything> {
25
+ return new Promise(resolve => {
26
+ setTimeout(() => {
27
+ resolve(
28
+ everything
29
+ .filter(o => o.name.toLowerCase().includes(query.toLowerCase()))
30
+ .slice(0, 20)
31
+ );
32
+ }, 500);
33
+ });
34
+ }
35
+
36
+ const value = ref();
37
+ const options = ref<typeof everything>([]);
38
+ const loading = ref(false);
39
+ let latest = 0;
40
+
41
+ async function onSearch(query: string) {
42
+ const request = ++latest;
43
+ loading.value = true;
44
+ const result = await fetchOptions(query);
45
+
46
+ // Ignore stale responses
47
+ if (request !== latest) {
48
+ return;
49
+ }
50
+
51
+ options.value = result;
52
+ loading.value = false;
53
+ }
54
+ </script>
@@ -29,7 +29,10 @@ export default defineComponent({
29
29
  default: '',
30
30
  },
31
31
  },
32
- emits: ['update:modelValue', 'change'],
32
+ emits: {
33
+ 'update:modelValue': (_value: number) => true,
34
+ change: (_value: number) => true
35
+ },
33
36
  setup(props, { emit }) {
34
37
  const slider = ref<HTMLElement | null>(null);
35
38
  const dragging = ref(false);
@@ -48,7 +48,9 @@ export default defineComponent({
48
48
  default: false
49
49
  }
50
50
  },
51
- emits: ['onClose'],
51
+ emits: {
52
+ onClose: (_tab: { value: any; label: any }) => true
53
+ },
52
54
  setup(props, { attrs, slots }) {
53
55
  const computedValue = computed(() => props.value);
54
56
  const computedLabel = computed(() => props.label || props.value);
@@ -53,10 +53,10 @@ export default defineComponent({
53
53
  validator: (value: string) => ['base', 'card'].includes(value)
54
54
  }
55
55
  },
56
- emits: [
57
- 'update:modelValue',
58
- 'update:selected'
59
- ],
56
+ emits: {
57
+ 'update:modelValue': (_tab: any) => true,
58
+ 'update:selected': (_tab: any) => true
59
+ },
60
60
  setup(props, { emit }) {
61
61
  const wrapperRef = ref<HTMLElement>();
62
62
  const tabsRef = ref<HTMLElement>();
@@ -88,9 +88,9 @@ export default defineComponent({
88
88
  TableCell,
89
89
  MCheckbox
90
90
  },
91
- emits: [
92
- 'update:modelValue',
93
- ],
91
+ emits: {
92
+ 'update:modelValue': (_value: any) => true
93
+ },
94
94
  props: {
95
95
  modelValue: {
96
96
  type: Array,
@@ -22,7 +22,10 @@ export default defineComponent({
22
22
  components: { TreeNode },
23
23
  name: 'Tree',
24
24
  props: treeProps,
25
- emits: ['update:expandedKeys', 'dblclick'],
25
+ emits: {
26
+ 'update:expandedKeys': (_keys: any) => true,
27
+ dblclick: (_node: any) => true
28
+ },
26
29
  setup(props, { emit }) {
27
30
  const selfElRef = ref<HTMLDivElement | null>(null);
28
31
  const controlledExpandedKeysRef = toRef(props, 'expandedKeys');