@indielayer/ui 1.17.0 → 1.18.0

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 (47) hide show
  1. package/README.md +2 -2
  2. package/docs/assets/css/tailwind.css +6 -0
  3. package/docs/components/common/CodePreview.vue +14 -9
  4. package/docs/components/common/DocsFeatures.vue +41 -0
  5. package/docs/components/common/DocsHero.vue +216 -0
  6. package/docs/components/common/DocumentPage.vue +99 -112
  7. package/docs/components/common/ExampleBlocks.vue +157 -0
  8. package/docs/components/toolbar/Toolbar.vue +11 -2
  9. package/docs/components/toolbar/ToolbarColorToggle.vue +4 -4
  10. package/docs/components/toolbar/ToolbarSearch.vue +59 -62
  11. package/docs/composables/useDocMeta.ts +47 -0
  12. package/docs/icons.ts +28 -0
  13. package/docs/layouts/default.vue +1 -3
  14. package/docs/layouts/simple.vue +3 -1
  15. package/docs/main.ts +5 -0
  16. package/docs/pages/colors.vue +56 -47
  17. package/docs/pages/component/select/size.vue +1 -1
  18. package/docs/pages/component/select/usage.vue +14 -7
  19. package/docs/pages/error.vue +5 -3
  20. package/docs/pages/icons.vue +64 -54
  21. package/docs/pages/index.vue +93 -82
  22. package/docs/pages/typography.vue +38 -28
  23. package/docs/router/index.ts +31 -3
  24. package/docs/search/components.json +1 -1
  25. package/docs/search/index.json +1 -0
  26. package/lib/components/container/theme/Container.base.theme.js +1 -1
  27. package/lib/components/divider/theme/Divider.base.theme.js +1 -1
  28. package/lib/components/input/Input.vue.js +23 -24
  29. package/lib/components/select/Select.vue.d.ts +16 -27
  30. package/lib/components/select/Select.vue.js +451 -344
  31. package/lib/index.js +1 -1
  32. package/lib/index.umd.js +4 -4
  33. package/lib/version.d.ts +1 -1
  34. package/lib/version.js +1 -1
  35. package/lib/virtual/components/virtualList/VirtualList.vue.js +33 -31
  36. package/lib/virtual/components/virtualList/useDynamicRowHeight.js +18 -19
  37. package/package.json +8 -3
  38. package/src/components/container/theme/Container.base.theme.ts +1 -1
  39. package/src/components/divider/theme/Divider.base.theme.ts +1 -1
  40. package/src/components/input/Input.vue +1 -2
  41. package/src/components/select/Select.vue +94 -18
  42. package/src/version.ts +1 -1
  43. package/src/virtual/components/virtualList/VirtualList.test.ts +143 -26
  44. package/src/virtual/components/virtualList/VirtualList.vue +12 -18
  45. package/src/virtual/components/virtualList/useDynamicRowHeight.test.ts +22 -8
  46. package/src/virtual/components/virtualList/useDynamicRowHeight.ts +4 -2
  47. package/src/virtual/utils/parseNumericStyleValue.ts +2 -0
package/lib/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- declare const _default: "1.17.0";
1
+ declare const _default: "1.18.0";
2
2
  export default _default;
package/lib/version.js CHANGED
@@ -1,4 +1,4 @@
1
- const e = "1.17.0";
1
+ const e = "1.18.0";
2
2
  export {
3
3
  e as default
4
4
  };
@@ -1,11 +1,11 @@
1
- import { defineComponent as _, ref as $, computed as i, watch as h, openBlock as g, createBlock as k, resolveDynamicComponent as E, normalizeClass as B, normalizeStyle as f, withCtx as D, createElementBlock as V, Fragment as O, renderList as P, renderSlot as w, createElementVNode as L, unref as N } from "vue";
2
- import { useVirtualizer as X } from "../../core/useVirtualizer.js";
3
- import { isDynamicRowHeight as Y } from "./isDynamicRowHeight.js";
4
- import { DATA_ATTRIBUTE_LIST_INDEX as F } from "./useDynamicRowHeight.js";
5
- const G = {
1
+ import { defineComponent as k, ref as E, computed as i, watch as h, openBlock as w, createBlock as B, resolveDynamicComponent as D, normalizeClass as V, normalizeStyle as f, withCtx as P, createElementBlock as L, Fragment as N, renderList as O, renderSlot as g, createElementVNode as X, unref as Y } from "vue";
2
+ import { useVirtualizer as F } from "../../core/useVirtualizer.js";
3
+ import { isDynamicRowHeight as G } from "./isDynamicRowHeight.js";
4
+ import { DATA_ATTRIBUTE_LIST_INDEX as U } from "./useDynamicRowHeight.js";
5
+ const j = {
6
6
  name: "XVirtualList"
7
- }, M = /* @__PURE__ */ _({
8
- ...G,
7
+ }, Q = /* @__PURE__ */ k({
8
+ ...j,
9
9
  props: {
10
10
  class: {},
11
11
  defaultHeight: { default: 0 },
@@ -19,7 +19,7 @@ const G = {
19
19
  tag: { default: "div" }
20
20
  },
21
21
  setup(v, { expose: y }) {
22
- const t = v, n = $(null), p = i(() => t.rowProps || {}), H = i(() => t.rowCount), a = i(() => Y(t.rowHeight)), x = i(() => {
22
+ const t = v, n = E(null), p = i(() => t.rowProps || {}), x = i(() => t.rowCount), a = i(() => G(t.rowHeight)), H = i(() => {
23
23
  if (a.value) {
24
24
  const e = t.rowHeight, o = e.getAverageRowHeight();
25
25
  return (r) => e.getRowHeight(r) ?? o;
@@ -28,19 +28,19 @@ const G = {
28
28
  }), {
29
29
  getCellBounds: R,
30
30
  getEstimatedSize: C,
31
- scrollToIndex: b,
31
+ scrollToIndex: I,
32
32
  startIndexOverscan: u,
33
- startIndexVisible: I,
33
+ startIndexVisible: z,
34
34
  stopIndexOverscan: c,
35
- stopIndexVisible: z
36
- } = X({
35
+ stopIndexVisible: b
36
+ } = F({
37
37
  containerElement: n,
38
38
  containerStyle: t.style,
39
39
  defaultContainerSize: t.defaultHeight,
40
40
  direction: "vertical",
41
- itemCount: H,
41
+ itemCount: x,
42
42
  itemProps: p,
43
- itemSize: x,
43
+ itemSize: H,
44
44
  onResize: t.onResize,
45
45
  overscanCount: t.overscanCount
46
46
  });
@@ -54,7 +54,7 @@ const G = {
54
54
  index: r
55
55
  }) {
56
56
  var d, l;
57
- const s = b({
57
+ const s = I({
58
58
  align: e,
59
59
  containerScrollOffset: ((d = n.value) == null ? void 0 : d.scrollTop) ?? 0,
60
60
  index: r
@@ -66,19 +66,21 @@ const G = {
66
66
  }
67
67
  }), h(
68
68
  [n, u, c, a, () => t.rowHeight],
69
- ([e, o, r, s]) => !e || !s ? void 0 : (() => {
70
- const l = Array.from(e.children).filter((m, T) => {
69
+ ([e, o, r, s], d, l) => {
70
+ if (!e || !s)
71
+ return;
72
+ const T = Array.from(e.children).filter((m, _) => {
71
73
  if (m.hasAttribute("aria-hidden"))
72
74
  return !1;
73
- const S = `${o + T}`;
74
- return m.setAttribute(F, S), !0;
75
- });
76
- return t.rowHeight.observeRowElements(l);
77
- })(),
75
+ const $ = `${o + _}`;
76
+ return m.setAttribute(U, $), !0;
77
+ }), S = t.rowHeight;
78
+ l(S.observeRowElements(T));
79
+ },
78
80
  { flush: "post" }
79
81
  // Run after DOM updates
80
82
  ), h(
81
- [u, I, c, z],
83
+ [u, z, c, b],
82
84
  ([e, o, r, s]) => {
83
85
  e >= 0 && r >= 0 && t.onRowsRendered && t.onRowsRendered(
84
86
  {
@@ -117,10 +119,10 @@ const G = {
117
119
  }
118
120
  return e;
119
121
  });
120
- return (e, o) => (g(), k(E(e.tag), {
122
+ return (e, o) => (w(), B(D(e.tag), {
121
123
  ref_key: "element",
122
124
  ref: n,
123
- class: B(e.$props.class),
125
+ class: V(e.$props.class),
124
126
  style: f({
125
127
  position: "relative",
126
128
  maxHeight: "100%",
@@ -130,19 +132,19 @@ const G = {
130
132
  }),
131
133
  role: "list"
132
134
  }, {
133
- default: D(() => [
134
- (g(!0), V(O, null, P(A.value, (r) => w(e.$slots, "row", {
135
+ default: P(() => [
136
+ (w(!0), L(N, null, O(A.value, (r) => g(e.$slots, "row", {
135
137
  key: r.key,
136
138
  index: r.index,
137
139
  style: f(r.style),
138
140
  ariaAttributes: r.ariaAttributes,
139
141
  props: p.value
140
142
  })), 128)),
141
- w(e.$slots, "default"),
142
- L("div", {
143
+ g(e.$slots, "default"),
144
+ X("div", {
143
145
  "aria-hidden": "",
144
146
  style: f({
145
- height: `${N(C)}px`,
147
+ height: `${Y(C)}px`,
146
148
  width: "100%",
147
149
  zIndex: -1
148
150
  })
@@ -153,5 +155,5 @@ const G = {
153
155
  }
154
156
  });
155
157
  export {
156
- M as default
158
+ Q as default
157
159
  };
@@ -1,56 +1,55 @@
1
- import { ref as p, watch as d, onBeforeUnmount as H } from "vue";
2
- import { assert as R } from "../../utils/assert.js";
1
+ import { ref as p, watch as d, getCurrentInstance as H, onBeforeUnmount as R } from "vue";
2
+ import { assert as I } from "../../utils/assert.js";
3
3
  const b = "data-virtual-index";
4
- function x({
4
+ function y({
5
5
  defaultRowHeight: v,
6
- key: a
6
+ key: o
7
7
  }) {
8
- const r = p(/* @__PURE__ */ new Map()), o = p(0);
9
- a !== void 0 && typeof a == "object" && "value" in a && d(a, () => {
10
- r.value = /* @__PURE__ */ new Map(), o.value++;
8
+ const r = p(/* @__PURE__ */ new Map()), a = p(0);
9
+ o !== void 0 && typeof o == "object" && "value" in o && d(o, () => {
10
+ r.value = /* @__PURE__ */ new Map(), a.value++;
11
11
  });
12
12
  const w = () => {
13
- o.value;
13
+ a.value;
14
14
  let e = 0;
15
15
  return r.value.forEach((t) => {
16
16
  e += t;
17
17
  }), e === 0 ? v : e / r.value.size;
18
18
  }, m = (e) => {
19
- o.value;
19
+ a.value;
20
20
  const t = r.value.get(e);
21
21
  return t !== void 0 ? t : v;
22
22
  }, A = (e, t) => {
23
23
  if (r.value.get(e) === t)
24
24
  return;
25
25
  const n = new Map(r.value);
26
- n.set(e, t), r.value = n, o.value++;
26
+ n.set(e, t), r.value = n, a.value++;
27
27
  }, E = (e) => {
28
28
  if (e.length === 0)
29
29
  return;
30
30
  let t = !1;
31
31
  const n = [];
32
32
  if (e.forEach((s) => {
33
- const { borderBoxSize: i, target: c } = s, h = c.getAttribute(b);
34
- R(
35
- h !== null,
33
+ const { borderBoxSize: i, target: c } = s, g = c.getAttribute(b);
34
+ I(
35
+ g !== null,
36
36
  `Invalid ${b} attribute value`
37
37
  );
38
- const g = parseInt(h), { blockSize: l } = i[0];
38
+ const h = parseInt(g), { blockSize: l } = i[0];
39
39
  if (!l)
40
40
  return;
41
- r.value.get(g) !== l && (n.push({ index: g, height: l }), t = !0);
41
+ r.value.get(h) !== l && (n.push({ index: h, height: l }), t = !0);
42
42
  }), t) {
43
43
  const s = new Map(r.value);
44
44
  n.forEach(({ index: i, height: c }) => {
45
45
  s.set(i, c);
46
- }), r.value = s, o.value++;
46
+ }), r.value = s, a.value++;
47
47
  }
48
48
  }, u = new ResizeObserver(E);
49
- H(f);
50
49
  function f() {
51
50
  u.disconnect();
52
51
  }
53
- return {
52
+ return H() && R(f), {
54
53
  getAverageRowHeight: w,
55
54
  getRowHeight: m,
56
55
  setRowHeight: A,
@@ -65,5 +64,5 @@ function x({
65
64
  }
66
65
  export {
67
66
  b as DATA_ATTRIBUTE_LIST_INDEX,
68
- x as useDynamicRowHeight
67
+ y as useDynamicRowHeight
69
68
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indielayer/ui",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "description": "Indielayer UI Components with Tailwind CSS build for Vue 3",
5
5
  "author": {
6
6
  "name": "João Teixeira",
@@ -50,12 +50,13 @@
50
50
  "devDependencies": {
51
51
  "@indielayer/stylelint-config": "^1.0.0",
52
52
  "@rushstack/eslint-patch": "^1.3.2",
53
+ "@unhead/vue": "^2.0.19",
53
54
  "@tsconfig/node18": "^2.0.1",
54
55
  "@types/jsdom": "^21.1.1",
55
56
  "@types/node": "^18.16.18",
56
57
  "@vitejs/plugin-vue": "^4.2.3",
57
58
  "@vitejs/plugin-vue-jsx": "^3.0.1",
58
- "@vue/test-utils": "^2.4.0",
59
+ "@vue/test-utils": "^2.4.6",
59
60
  "@vue/tsconfig": "^0.4.0",
60
61
  "@vuepic/vue-datepicker": "^11.0.2",
61
62
  "@vueuse/core": "^11.1.0",
@@ -107,9 +108,13 @@
107
108
  "gen:types": "vue-tsc --declaration --emitDeclarationOnly -p tsconfig.vitest.json --composite false",
108
109
  "gen:version": "node .scripts/gen-version.cjs",
109
110
  "gen:search": "node .scripts/gen-search.cjs",
111
+ "gen:llms": "node .scripts/gen-llms.cjs",
112
+ "gen:sitemap": "node .scripts/gen-sitemap.cjs",
110
113
  "test": "pnpm test:unit",
114
+ "test:ci": "vitest run --environment jsdom",
111
115
  "test:unit": "vitest --environment jsdom",
112
116
  "typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
113
- "stylelint": "stylelint \"**/*.{css,vue,postcss,scss,sass}\" --ignore-path .gitignore"
117
+ "stylelint": "stylelint \"**/*.{css,vue,postcss,scss,sass}\" --ignore-path .gitignore",
118
+ "stylelint:fix": "stylelint \"**/*.{css,vue,postcss,scss,sass}\" --ignore-path .gitignore --fix"
114
119
  }
115
120
  }
@@ -5,7 +5,7 @@ const theme: ContainerTheme = {
5
5
  wrapper: ({ props }) => {
6
6
  const classes = ['px-4 md:px-6']
7
7
 
8
- if (!props.fluid) classes.push('max-w-screen-lg mx-auto')
8
+ if (!props.fluid) classes.push('max-w-screen-xl mx-auto')
9
9
 
10
10
  return classes
11
11
  },
@@ -4,7 +4,7 @@ const theme: DividerTheme = {
4
4
  classes: {
5
5
  wrapper: ({ props }) => `flex justify-center items-center ${props.vertical ? 'h-full flex-col' : 'w-full'}`,
6
6
 
7
- label: 'font-medium text-sm text-secondary-600 dark:text-secondary-300',
7
+ label: 'font-medium text-xs text-slate-400 dark:text-secondary-300',
8
8
 
9
9
  line: 'bg-secondary-200 dark:bg-secondary-700 flex-grow',
10
10
  },
@@ -152,14 +152,13 @@ defineExpose({ focus, blur, reset, validate, setError })
152
152
  ref="elRef"
153
153
  :class="[
154
154
  classes.input,
155
- type === 'password' ? 'pr-10' : '',
156
155
  // error
157
156
  errorInternal
158
157
  ? 'border-error-500 dark:border-error-400 focus:outline-error-500'
159
158
  : 'focus:outline-[color:var(--x-input-border)]',
160
159
  {
161
160
  '!pl-10': iconLeft || icon,
162
- '!pr-10': iconRight,
161
+ '!pr-10': iconRight || showPasswordToggle || showClearIcon,
163
162
  },
164
163
  ]"
165
164
  :disabled="disabled"
@@ -17,6 +17,9 @@ const selectProps = {
17
17
  type: String,
18
18
  default: 'Filter by...',
19
19
  },
20
+ filterablePrefix: Boolean,
21
+ filterableSuffix: Boolean,
22
+ hideSelectedOptionSlots: Boolean,
20
23
  virtualList: Boolean,
21
24
  virtualListOffsetTop: Number,
22
25
  virtualListOffsetBottom: Number,
@@ -110,12 +113,35 @@ const selected = computed<any | any[]>({
110
113
  },
111
114
  })
112
115
 
113
- const labelCache = computed(() => {
114
- if (!props.options) return new Map<SelectOption, string>()
116
+ const optionsByValue = computed(() => {
117
+ if (!props.options) return new Map<string | number, SelectOption>()
115
118
 
116
- return new Map(props.options.map((option) => [option, option.label.toLowerCase()]))
119
+ return new Map(props.options.map((option) => [option.value, option]))
117
120
  })
118
121
 
122
+ const filterCache = computed(() => {
123
+ if (!props.options) return new Map<SelectOption, { label: string; prefix?: string; suffix?: string; }>()
124
+
125
+ return new Map(props.options.map((option) => [
126
+ option,
127
+ {
128
+ label: option.label.toLowerCase(),
129
+ prefix: props.filterablePrefix && option.prefix ? option.prefix.toLowerCase() : undefined,
130
+ suffix: props.filterableSuffix && option.suffix ? option.suffix.toLowerCase() : undefined,
131
+ },
132
+ ]))
133
+ })
134
+
135
+ function matchesFilter(option: SelectOption, filterLower: string) {
136
+ const cached = filterCache.value.get(option)
137
+
138
+ if (!cached) return false
139
+
140
+ return cached.label.includes(filterLower)
141
+ || cached.prefix?.includes(filterLower)
142
+ || cached.suffix?.includes(filterLower)
143
+ }
144
+
119
145
  const internalOptions = computed(() => {
120
146
  if (!props.options || props.options.length === 0) return []
121
147
 
@@ -128,10 +154,9 @@ const internalOptions = computed(() => {
128
154
  : [],
129
155
  )
130
156
  const singleSelectedValue = !internalMultiple.value ? selected.value : null
131
- const cache = labelCache.value
132
157
 
133
158
  return props.options
134
- .filter((option) => !hasFilter || cache.get(option)?.includes(filterLower))
159
+ .filter((option) => !hasFilter || matchesFilter(option, filterLower))
135
160
  .map((option) => {
136
161
  const isActive = internalMultiple.value
137
162
  ? selectedSet.has(option.value)
@@ -265,7 +290,7 @@ function findSelectableIndex(start: number | undefined, direction = 'down') {
265
290
  }
266
291
 
267
292
  function handleOptionClick(value: string | number) {
268
- const option = props.options?.find((i) => i.value === value)
293
+ const option = getItem(value)
269
294
 
270
295
  if (!option || option.disabled) return
271
296
 
@@ -326,8 +351,14 @@ function handleRemove(e: Event, value: string) {
326
351
  }
327
352
  }
328
353
 
354
+ function getItem(value: string | number | []) {
355
+ if (Array.isArray(value)) return undefined
356
+
357
+ return optionsByValue.value.get(value)
358
+ }
359
+
329
360
  function getLabel(value: string | number | []) {
330
- const option = props.options?.find((i) => i.value === value)
361
+ const option = getItem(value)
331
362
 
332
363
  if (option) return option.label
333
364
 
@@ -542,15 +573,29 @@ defineExpose({ focus, blur, reset, validate, setError, filterRef })
542
573
  :key="value"
543
574
  size="xs"
544
575
  removable
545
- :outlined="!(isDisabled || options?.find((i) => i.value === value)?.disabled)"
546
- :disabled="isDisabled || options?.find((i) => i.value === value)?.disabled"
576
+ :outlined="!(isDisabled || getItem(value)?.disabled)"
577
+ :disabled="isDisabled || getItem(value)?.disabled"
547
578
  :style="{ 'max-width': valueIndex === 0 && hiddenTagsCounterRef ? `calc(100% - ${hiddenTagsCounterRef.offsetWidth + 6 + 'px'})` : undefined }"
548
579
  @remove="(e: Event) => { handleRemove(e, value) }"
549
580
  >
550
- <template #prefix>
551
- <slot name="tag-prefix" :item="options?.find((i) => i.value === value)"></slot>
581
+ <template v-if="!hideSelectedOptionSlots">
582
+ <div class="flex items-center">
583
+ <span v-if="$slots.prefix || getItem(value)?.prefix" class="mr-2 shrink-0">
584
+ <slot name="prefix" :item="getItem(value)">{{ getItem(value)?.prefix }}</slot>
585
+ </span>
586
+
587
+ <span class="flex-1 truncate">
588
+ {{ getLabel(value) }}
589
+ </span>
590
+
591
+ <span v-if="$slots.suffix || getItem(value)?.suffix" class="ml-1 shrink-0">
592
+ <slot name="suffix" :item="getItem(value)">{{ getItem(value)?.suffix }}</slot>
593
+ </span>
594
+ </div>
595
+ </template>
596
+ <template v-else>
597
+ {{ getLabel(value) }}
552
598
  </template>
553
- {{ getLabel(value) }}
554
599
  </x-tag>
555
600
 
556
601
  <div
@@ -562,7 +607,24 @@ defineExpose({ focus, blur, reset, validate, setError, filterRef })
562
607
  </div>
563
608
  </template>
564
609
  <template v-else-if="!internalMultiple && !isEmpty(selected) && getLabel(selected) !== ''">
565
- {{ getLabel(selected) }}
610
+ <template v-if="!hideSelectedOptionSlots">
611
+ <div class="flex items-center">
612
+ <span v-if="$slots.prefix || getItem(selected)?.prefix" class="mr-2 shrink-0">
613
+ <slot name="prefix" :item="getItem(selected)">{{ getItem(selected)?.prefix }}</slot>
614
+ </span>
615
+
616
+ <span class="flex-1 truncate">
617
+ {{ getLabel(selected) }}
618
+ </span>
619
+
620
+ <span v-if="$slots.suffix || getItem(selected)?.suffix" class="ml-1 shrink-0">
621
+ <slot name="suffix" :item="getItem(selected)">{{ getItem(selected)?.suffix }}</slot>
622
+ </span>
623
+ </div>
624
+ </template>
625
+ <template v-else>
626
+ {{ getLabel(selected) }}
627
+ </template>
566
628
  </template>
567
629
 
568
630
  <template v-else>
@@ -636,14 +698,28 @@ defineExpose({ focus, blur, reset, validate, setError, filterRef })
636
698
  :key="value"
637
699
  size="xs"
638
700
  removable
639
- :outlined="!(isDisabled || options?.find((i) => i.value === value)?.disabled)"
640
- :disabled="isDisabled || options?.find((i) => i.value === value)?.disabled"
701
+ :outlined="!(isDisabled || getItem(value)?.disabled)"
702
+ :disabled="isDisabled || getItem(value)?.disabled"
641
703
  @remove="(e: Event) => { handleRemove(e, value) }"
642
704
  >
643
- <template #prefix>
644
- <slot name="tag-prefix" :item="options?.find((i) => i.value === value)"></slot>
705
+ <template v-if="!hideSelectedOptionSlots">
706
+ <div class="flex items-center">
707
+ <span v-if="$slots.prefix || getItem(value)?.prefix" class="mr-2 shrink-0">
708
+ <slot name="prefix" :item="getItem(value)">{{ getItem(value)?.prefix }}</slot>
709
+ </span>
710
+
711
+ <span class="flex-1 truncate">
712
+ {{ getLabel(value) }}
713
+ </span>
714
+
715
+ <span v-if="$slots.suffix || getItem(value)?.suffix" class="ml-1 shrink-0">
716
+ <slot name="suffix" :item="getItem(value)">{{ getItem(value)?.suffix }}</slot>
717
+ </span>
718
+ </div>
719
+ </template>
720
+ <template v-else>
721
+ {{ getLabel(value) }}
645
722
  </template>
646
- {{ getLabel(value) }}
647
723
  </x-tag>
648
724
  </x-popover-container>
649
725
  </template>
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export default '1.17.0'
1
+ export default '1.18.0'
@@ -1,47 +1,164 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { mount } from '@vue/test-utils'
3
- import { h, type CSSProperties } from 'vue'
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { mount, flushPromises } from '@vue/test-utils'
3
+ import { h, nextTick, type CSSProperties } from 'vue'
4
4
  import VirtualList from './VirtualList.vue'
5
+ import { useDynamicRowHeight, DATA_ATTRIBUTE_LIST_INDEX } from './useDynamicRowHeight'
6
+ import { mockResizeObserver, setElementSize } from '../../test-utils/mockResizeObserver'
7
+ import type { DynamicRowHeight } from './types'
5
8
 
6
9
  interface RowSlotProps {
7
10
  index: number;
8
11
  style: CSSProperties;
9
12
  }
10
13
 
14
+ const listStyle = { height: '400px' } as const
15
+
16
+ function mountVirtualList(
17
+ props: Record<string, unknown>,
18
+ dynamicRowHeight?: DynamicRowHeight,
19
+ ) {
20
+ return mount(VirtualList, {
21
+ props: {
22
+ rowCount: 100,
23
+ rowHeight: dynamicRowHeight ?? 50,
24
+ style: listStyle,
25
+ ...props,
26
+ },
27
+ slots: {
28
+ row: ({ index, style }: RowSlotProps) =>
29
+ h('div', { style, class: 'test-row' }, `Row ${index}`),
30
+ },
31
+ })
32
+ }
33
+
34
+ function createMockDynamicRowHeight(defaultRowHeight = 50) {
35
+ const unobserve = vi.fn<[], void>()
36
+
37
+ const dynamicRowHeight: DynamicRowHeight = {
38
+ getAverageRowHeight: vi.fn(() => defaultRowHeight),
39
+ getRowHeight: vi.fn(() => undefined),
40
+ setRowHeight: vi.fn(),
41
+ observeRowElements: vi.fn(() => unobserve),
42
+ cleanup: vi.fn(),
43
+ }
44
+
45
+ return { dynamicRowHeight, unobserve }
46
+ }
47
+
11
48
  describe('VirtualList', () => {
12
49
  it('renders correctly', () => {
13
- const wrapper = mount(VirtualList, {
14
- props: {
15
- rowCount: 100,
16
- rowHeight: 50,
17
- style: { height: '400px' },
18
- },
19
- slots: {
20
- row: ({ index, style }: RowSlotProps) =>
21
- h('div', { style }, `Row ${index}`),
22
- },
23
- })
50
+ const wrapper = mountVirtualList({})
24
51
 
25
52
  expect(wrapper.exists()).toBe(true)
26
53
  expect(wrapper.attributes('role')).toBe('list')
27
54
  })
28
55
 
29
56
  it('renders visible rows', () => {
30
- const wrapper = mount(VirtualList, {
31
- props: {
32
- rowCount: 100,
33
- rowHeight: 50,
34
- style: { height: '400px' },
35
- },
36
- slots: {
37
- row: ({ index, style }: RowSlotProps) =>
38
- h('div', { style, class: 'test-row' }, `Row ${index}`),
39
- },
40
- })
57
+ const wrapper = mountVirtualList({})
41
58
 
42
- // Should render some rows (visible + overscan)
43
59
  const rows = wrapper.findAll('.test-row')
44
60
 
45
61
  expect(rows.length).toBeGreaterThan(0)
46
62
  })
63
+
64
+ describe('dynamic row height watch', () => {
65
+ it('does not observe row elements when rowHeight is fixed', async () => {
66
+ const { dynamicRowHeight } = createMockDynamicRowHeight()
67
+
68
+ mountVirtualList({ rowHeight: 50 })
69
+
70
+ await flushPromises()
71
+ await nextTick()
72
+
73
+ expect(dynamicRowHeight.observeRowElements).not.toHaveBeenCalled()
74
+ })
75
+
76
+ it('observes visible row elements and sets data-virtual-index', async () => {
77
+ const { dynamicRowHeight } = createMockDynamicRowHeight()
78
+
79
+ const wrapper = mountVirtualList({ rowHeight: dynamicRowHeight })
80
+
81
+ await flushPromises()
82
+ await nextTick()
83
+
84
+ expect(dynamicRowHeight.observeRowElements).toHaveBeenCalled()
85
+
86
+ const observedElements = vi.mocked(dynamicRowHeight.observeRowElements).mock
87
+ .calls[0]?.[0] as Element[]
88
+
89
+ expect(observedElements.length).toBeGreaterThan(0)
90
+ expect(
91
+ observedElements.every((element) => !element.hasAttribute('aria-hidden')),
92
+ ).toBe(true)
93
+
94
+ const listElement = wrapper.element as HTMLElement
95
+ const rowElements = Array.from(listElement.children).filter(
96
+ (child) => !child.hasAttribute('aria-hidden'),
97
+ )
98
+
99
+ expect(rowElements.length).toBe(observedElements.length)
100
+ rowElements.forEach((element) => {
101
+ expect(element.hasAttribute(DATA_ATTRIBUTE_LIST_INDEX)).toBe(true)
102
+ })
103
+ })
104
+
105
+ it('runs previous unobserve when the observed range changes', async () => {
106
+ const { dynamicRowHeight, unobserve } = createMockDynamicRowHeight()
107
+
108
+ const wrapper = mountVirtualList({ rowHeight: dynamicRowHeight })
109
+
110
+ await flushPromises()
111
+ await nextTick()
112
+
113
+ expect(dynamicRowHeight.observeRowElements).toHaveBeenCalledTimes(1)
114
+ expect(unobserve).not.toHaveBeenCalled()
115
+
116
+ const listElement = wrapper.element as HTMLDivElement
117
+
118
+ listElement.scrollTop = 2500
119
+ listElement.dispatchEvent(new Event('scroll'))
120
+ await flushPromises()
121
+ await nextTick()
122
+
123
+ expect(unobserve).toHaveBeenCalledTimes(1)
124
+ expect(dynamicRowHeight.observeRowElements).toHaveBeenCalledTimes(2)
125
+ })
126
+ })
127
+
128
+ describe('dynamic row height integration', () => {
129
+ let unmockResizeObserver: (() => void) | undefined
130
+ let dynamicRowHeight: DynamicRowHeight | undefined
131
+
132
+ beforeEach(() => {
133
+ unmockResizeObserver = mockResizeObserver()
134
+ dynamicRowHeight = useDynamicRowHeight({ defaultRowHeight: 50 })
135
+ })
136
+
137
+ afterEach(() => {
138
+ dynamicRowHeight?.cleanup()
139
+ unmockResizeObserver?.()
140
+ })
141
+
142
+ it('updates measured heights when observed rows resize', async () => {
143
+ const wrapper = mountVirtualList({ rowHeight: dynamicRowHeight })
144
+
145
+ await flushPromises()
146
+ await nextTick()
147
+
148
+ const rowElement = wrapper.find('.test-row').element as HTMLElement
149
+
150
+ setElementSize({
151
+ element: rowElement,
152
+ width: 100,
153
+ height: 72,
154
+ })
155
+
156
+ await flushPromises()
157
+ await nextTick()
158
+
159
+ const index = Number(rowElement.getAttribute(DATA_ATTRIBUTE_LIST_INDEX))
160
+
161
+ expect(dynamicRowHeight?.getRowHeight(index)).toBe(72)
162
+ })
163
+ })
47
164
  })