vueless 1.4.9-beta.2 → 1.4.9
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/components.d.ts +1 -0
- package/components.ts +1 -0
- package/constants.d.ts +1 -0
- package/constants.js +1 -0
- package/icons/storybook/drag_handle.svg +1 -0
- package/package.json +1 -1
- package/ui.container-splitter/USplitter.vue +390 -0
- package/ui.container-splitter/config.ts +35 -0
- package/ui.container-splitter/constants.ts +1 -0
- package/ui.container-splitter/storybook/docs.mdx +17 -0
- package/ui.container-splitter/storybook/stories.ts +171 -0
- package/ui.container-splitter/tests/USplitter.test.ts +347 -0
- package/ui.container-splitter/types.ts +62 -0
package/components.d.ts
CHANGED
|
@@ -54,6 +54,7 @@ export { default as UCard } from "./ui.container-card/UCard.vue";
|
|
|
54
54
|
export { default as UModal } from "./ui.container-modal/UModal.vue";
|
|
55
55
|
export { default as UModalConfirm } from "./ui.container-modal-confirm/UModalConfirm.vue";
|
|
56
56
|
export { default as UDrawer } from "./ui.container-drawer/UDrawer.vue";
|
|
57
|
+
export { default as USplitter } from "./ui.container-splitter/USplitter.vue";
|
|
57
58
|
export { default as UPage } from "./ui.container-page/UPage.vue";
|
|
58
59
|
/* Images and Icons */
|
|
59
60
|
export { default as UIcon } from "./ui.image-icon/UIcon.vue";
|
package/components.ts
CHANGED
|
@@ -54,6 +54,7 @@ export { default as UCard } from "./ui.container-card/UCard.vue";
|
|
|
54
54
|
export { default as UModal } from "./ui.container-modal/UModal.vue";
|
|
55
55
|
export { default as UModalConfirm } from "./ui.container-modal-confirm/UModalConfirm.vue";
|
|
56
56
|
export { default as UDrawer } from "./ui.container-drawer/UDrawer.vue";
|
|
57
|
+
export { default as USplitter } from "./ui.container-splitter/USplitter.vue";
|
|
57
58
|
export { default as UPage } from "./ui.container-page/UPage.vue";
|
|
58
59
|
/* Images and Icons */
|
|
59
60
|
export { default as UIcon } from "./ui.image-icon/UIcon.vue";
|
package/constants.d.ts
CHANGED
package/constants.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 -960 960 960"><path d="M154.02-381.87V-450h652.2v68.13h-652.2Zm0-128.13v-68.37h652.2V-510h-652.2Z"/></svg>
|
package/package.json
CHANGED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, useSlots, useTemplateRef, ref, watch, onMounted, onBeforeUnmount } from "vue";
|
|
3
|
+
|
|
4
|
+
import { useUI } from "../composables/useUI";
|
|
5
|
+
import { getDefaults } from "../utils/ui";
|
|
6
|
+
|
|
7
|
+
import { COMPONENT_NAME } from "./constants";
|
|
8
|
+
import defaultConfig from "./config";
|
|
9
|
+
|
|
10
|
+
import type { Props, Config } from "./types";
|
|
11
|
+
|
|
12
|
+
defineOptions({ inheritAttrs: false });
|
|
13
|
+
|
|
14
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
15
|
+
...getDefaults<Props, Config>(defaultConfig, COMPONENT_NAME),
|
|
16
|
+
modelValue: () => [],
|
|
17
|
+
minSizes: () => [],
|
|
18
|
+
maxSizes: () => [],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const emit = defineEmits([
|
|
22
|
+
/**
|
|
23
|
+
* Triggers when panel sizes change.
|
|
24
|
+
* @property {number[]} modelValue
|
|
25
|
+
*/
|
|
26
|
+
"update:modelValue",
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Triggers when resize starts.
|
|
30
|
+
*/
|
|
31
|
+
"resize-start",
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Triggers when resize ends.
|
|
35
|
+
* @property {number[]} sizes
|
|
36
|
+
*/
|
|
37
|
+
"resize-end",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const slots = useSlots();
|
|
41
|
+
const wrapperRef = useTemplateRef<HTMLDivElement>("wrapper");
|
|
42
|
+
|
|
43
|
+
const panelSlots = computed(() => {
|
|
44
|
+
return Object.keys(slots).filter((name) => name.startsWith("panel-"));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const panelCount = computed(() => panelSlots.value.length);
|
|
48
|
+
|
|
49
|
+
const isDragging = ref(false);
|
|
50
|
+
const activeGutterIndex = ref<number | null>(null);
|
|
51
|
+
const dragStartPosition = ref(0);
|
|
52
|
+
const dragCurrentPosition = ref(0);
|
|
53
|
+
const containerSize = ref(0);
|
|
54
|
+
const panelSizes = ref<number[]>([]);
|
|
55
|
+
|
|
56
|
+
const isHorizontal = computed(() => !props.vertical);
|
|
57
|
+
|
|
58
|
+
function parseSize(size: number | string, containerSize: number): number {
|
|
59
|
+
if (typeof size === "string") {
|
|
60
|
+
if (size.endsWith("%")) {
|
|
61
|
+
return (parseFloat(size) / 100) * containerSize;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (size.endsWith("px")) {
|
|
65
|
+
return parseFloat(size);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return parseFloat(size);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return size;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function initializeSizes() {
|
|
75
|
+
if (props.stateKey) {
|
|
76
|
+
const storage = props.stateStorage === "local" ? localStorage : sessionStorage;
|
|
77
|
+
const stored = storage.getItem(props.stateKey);
|
|
78
|
+
|
|
79
|
+
if (stored) {
|
|
80
|
+
const parsedSizes = JSON.parse(stored);
|
|
81
|
+
|
|
82
|
+
if (Array.isArray(parsedSizes) && parsedSizes.length === panelCount.value) {
|
|
83
|
+
panelSizes.value = parsedSizes;
|
|
84
|
+
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (props.modelValue && props.modelValue.length === panelCount.value) {
|
|
91
|
+
panelSizes.value = [...props.modelValue];
|
|
92
|
+
} else {
|
|
93
|
+
const equalSize = 100 / panelCount.value;
|
|
94
|
+
|
|
95
|
+
panelSizes.value = Array(panelCount.value).fill(equalSize);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function updateContainerSize() {
|
|
100
|
+
if (!wrapperRef.value) return;
|
|
101
|
+
|
|
102
|
+
const rect = wrapperRef.value.getBoundingClientRect();
|
|
103
|
+
|
|
104
|
+
containerSize.value = isHorizontal.value ? rect.width : rect.height;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getMinSize(index: number): number {
|
|
108
|
+
if (!props.minSizes) return 0;
|
|
109
|
+
|
|
110
|
+
const minSize = Array.isArray(props.minSizes) ? props.minSizes[index] : props.minSizes;
|
|
111
|
+
|
|
112
|
+
if (minSize === undefined) return 0;
|
|
113
|
+
|
|
114
|
+
return parseSize(minSize, containerSize.value);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getMaxSize(index: number): number {
|
|
118
|
+
if (!props.maxSizes) return containerSize.value;
|
|
119
|
+
|
|
120
|
+
const maxSize = Array.isArray(props.maxSizes) ? props.maxSizes[index] : props.maxSizes;
|
|
121
|
+
|
|
122
|
+
if (maxSize === undefined) return containerSize.value;
|
|
123
|
+
|
|
124
|
+
return parseSize(maxSize, containerSize.value);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function clampSize(size: number, index: number): number {
|
|
128
|
+
const minSize = getMinSize(index);
|
|
129
|
+
const maxSize = getMaxSize(index);
|
|
130
|
+
|
|
131
|
+
return Math.max(minSize, Math.min(maxSize, size));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function onPointerDown(event: PointerEvent, gutterIndex: number) {
|
|
135
|
+
if (props.disabled) return;
|
|
136
|
+
|
|
137
|
+
event.preventDefault();
|
|
138
|
+
|
|
139
|
+
isDragging.value = true;
|
|
140
|
+
activeGutterIndex.value = gutterIndex;
|
|
141
|
+
|
|
142
|
+
const clientPos = isHorizontal.value ? event.clientX : event.clientY;
|
|
143
|
+
|
|
144
|
+
dragStartPosition.value = clientPos;
|
|
145
|
+
dragCurrentPosition.value = clientPos;
|
|
146
|
+
|
|
147
|
+
updateContainerSize();
|
|
148
|
+
|
|
149
|
+
document.addEventListener("pointermove", onPointerMove);
|
|
150
|
+
document.addEventListener("pointerup", onPointerUp);
|
|
151
|
+
document.body.style.cursor = isHorizontal.value ? "col-resize" : "row-resize";
|
|
152
|
+
document.body.style.userSelect = "none";
|
|
153
|
+
|
|
154
|
+
emit("resize-start");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function onPointerMove(event: PointerEvent) {
|
|
158
|
+
if (!isDragging.value || activeGutterIndex.value === null) return;
|
|
159
|
+
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
|
|
162
|
+
const clientPos = isHorizontal.value ? event.clientX : event.clientY;
|
|
163
|
+
|
|
164
|
+
dragCurrentPosition.value = clientPos;
|
|
165
|
+
|
|
166
|
+
const delta = dragCurrentPosition.value - dragStartPosition.value;
|
|
167
|
+
const deltaPercent = (delta / containerSize.value) * 100;
|
|
168
|
+
|
|
169
|
+
const leftIndex = activeGutterIndex.value;
|
|
170
|
+
const rightIndex = activeGutterIndex.value + 1;
|
|
171
|
+
|
|
172
|
+
const newLeftSize = panelSizes.value[leftIndex] + deltaPercent;
|
|
173
|
+
const newRightSize = panelSizes.value[rightIndex] - deltaPercent;
|
|
174
|
+
|
|
175
|
+
const leftSizePx = (newLeftSize / 100) * containerSize.value;
|
|
176
|
+
const rightSizePx = (newRightSize / 100) * containerSize.value;
|
|
177
|
+
|
|
178
|
+
const clampedLeftPx = clampSize(leftSizePx, leftIndex);
|
|
179
|
+
const clampedRightPx = clampSize(rightSizePx, rightIndex);
|
|
180
|
+
|
|
181
|
+
const clampedLeftPercent = (clampedLeftPx / containerSize.value) * 100;
|
|
182
|
+
const clampedRightPercent = (clampedRightPx / containerSize.value) * 100;
|
|
183
|
+
|
|
184
|
+
const totalClamped = clampedLeftPercent + clampedRightPercent;
|
|
185
|
+
const originalTotal = panelSizes.value[leftIndex] + panelSizes.value[rightIndex];
|
|
186
|
+
|
|
187
|
+
if (Math.abs(totalClamped - originalTotal) < 0.1) {
|
|
188
|
+
const newSizes = [...panelSizes.value];
|
|
189
|
+
|
|
190
|
+
newSizes[leftIndex] = clampedLeftPercent;
|
|
191
|
+
newSizes[rightIndex] = clampedRightPercent;
|
|
192
|
+
|
|
193
|
+
panelSizes.value = newSizes;
|
|
194
|
+
dragStartPosition.value = dragCurrentPosition.value;
|
|
195
|
+
|
|
196
|
+
emit("update:modelValue", newSizes);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function onPointerUp() {
|
|
201
|
+
if (!isDragging.value) return;
|
|
202
|
+
|
|
203
|
+
document.removeEventListener("pointermove", onPointerMove);
|
|
204
|
+
document.removeEventListener("pointerup", onPointerUp);
|
|
205
|
+
document.body.style.cursor = "";
|
|
206
|
+
document.body.style.userSelect = "";
|
|
207
|
+
|
|
208
|
+
isDragging.value = false;
|
|
209
|
+
|
|
210
|
+
if (props.stateKey) {
|
|
211
|
+
const storage = props.stateStorage === "local" ? localStorage : sessionStorage;
|
|
212
|
+
|
|
213
|
+
storage.setItem(props.stateKey, JSON.stringify(panelSizes.value));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
emit("resize-end", panelSizes.value);
|
|
217
|
+
|
|
218
|
+
activeGutterIndex.value = null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function onKeyDown(event: KeyboardEvent, gutterIndex: number) {
|
|
222
|
+
if (props.disabled) return;
|
|
223
|
+
|
|
224
|
+
const stepValue = event.shiftKey ? props.resizeStep : 1;
|
|
225
|
+
let delta = 0;
|
|
226
|
+
|
|
227
|
+
if (isHorizontal.value) {
|
|
228
|
+
if (event.key === "ArrowLeft") delta = -stepValue;
|
|
229
|
+
if (event.key === "ArrowRight") delta = stepValue;
|
|
230
|
+
} else {
|
|
231
|
+
if (event.key === "ArrowUp") delta = -stepValue;
|
|
232
|
+
if (event.key === "ArrowDown") delta = stepValue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (delta === 0) return;
|
|
236
|
+
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
|
|
239
|
+
updateContainerSize();
|
|
240
|
+
|
|
241
|
+
const leftIndex = gutterIndex;
|
|
242
|
+
const rightIndex = gutterIndex + 1;
|
|
243
|
+
|
|
244
|
+
const newLeftSize = panelSizes.value[leftIndex] + delta;
|
|
245
|
+
const newRightSize = panelSizes.value[rightIndex] - delta;
|
|
246
|
+
|
|
247
|
+
const leftSizePx = (newLeftSize / 100) * containerSize.value;
|
|
248
|
+
const rightSizePx = (newRightSize / 100) * containerSize.value;
|
|
249
|
+
|
|
250
|
+
const clampedLeftPx = clampSize(leftSizePx, leftIndex);
|
|
251
|
+
const clampedRightPx = clampSize(rightSizePx, rightIndex);
|
|
252
|
+
|
|
253
|
+
const clampedLeftPercent = (clampedLeftPx / containerSize.value) * 100;
|
|
254
|
+
const clampedRightPercent = (clampedRightPx / containerSize.value) * 100;
|
|
255
|
+
|
|
256
|
+
const newSizes = [...panelSizes.value];
|
|
257
|
+
|
|
258
|
+
newSizes[leftIndex] = clampedLeftPercent;
|
|
259
|
+
newSizes[rightIndex] = clampedRightPercent;
|
|
260
|
+
|
|
261
|
+
panelSizes.value = newSizes;
|
|
262
|
+
|
|
263
|
+
emit("update:modelValue", newSizes);
|
|
264
|
+
|
|
265
|
+
if (props.stateKey) {
|
|
266
|
+
const storage = props.stateStorage === "local" ? localStorage : sessionStorage;
|
|
267
|
+
|
|
268
|
+
storage.setItem(props.stateKey, JSON.stringify(newSizes));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function onDoubleClick(gutterIndex: number) {
|
|
273
|
+
if (props.disabled) return;
|
|
274
|
+
|
|
275
|
+
const leftIndex = gutterIndex;
|
|
276
|
+
const rightIndex = gutterIndex + 1;
|
|
277
|
+
|
|
278
|
+
const totalSize = panelSizes.value[leftIndex] + panelSizes.value[rightIndex];
|
|
279
|
+
const equalSize = totalSize / 2;
|
|
280
|
+
|
|
281
|
+
const newSizes = [...panelSizes.value];
|
|
282
|
+
|
|
283
|
+
newSizes[leftIndex] = equalSize;
|
|
284
|
+
newSizes[rightIndex] = equalSize;
|
|
285
|
+
|
|
286
|
+
panelSizes.value = newSizes;
|
|
287
|
+
|
|
288
|
+
emit("update:modelValue", newSizes);
|
|
289
|
+
|
|
290
|
+
if (props.stateKey) {
|
|
291
|
+
const storage = props.stateStorage === "local" ? localStorage : sessionStorage;
|
|
292
|
+
|
|
293
|
+
storage.setItem(props.stateKey, JSON.stringify(newSizes));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function getPanelStyle(index: number) {
|
|
298
|
+
const size = panelSizes.value[index] || 0;
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
flexBasis: `${size}%`,
|
|
302
|
+
flexGrow: 0,
|
|
303
|
+
flexShrink: 0,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function getGutterAriaValue(gutterIndex: number) {
|
|
308
|
+
return Math.round(panelSizes.value[gutterIndex] || 0);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
watch(
|
|
312
|
+
() => props.modelValue,
|
|
313
|
+
(newValue) => {
|
|
314
|
+
if (newValue && newValue.length === panelCount.value && !isDragging.value) {
|
|
315
|
+
panelSizes.value = [...newValue];
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
initializeSizes();
|
|
321
|
+
|
|
322
|
+
onMounted(() => {
|
|
323
|
+
updateContainerSize();
|
|
324
|
+
|
|
325
|
+
window.addEventListener("resize", updateContainerSize);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
onBeforeUnmount(() => {
|
|
329
|
+
window.removeEventListener("resize", updateContainerSize);
|
|
330
|
+
document.removeEventListener("pointermove", onPointerMove);
|
|
331
|
+
document.removeEventListener("pointerup", onPointerUp);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
defineExpose({
|
|
335
|
+
/**
|
|
336
|
+
* A reference to the component's wrapper element for direct DOM manipulation.
|
|
337
|
+
* @property {HTMLDivElement}
|
|
338
|
+
*/
|
|
339
|
+
wrapperRef,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const gutterStyle = computed(() => ({
|
|
343
|
+
"--gutter-size": `${props.gutterSize}px`,
|
|
344
|
+
}));
|
|
345
|
+
|
|
346
|
+
const { getDataTest, wrapperAttrs, panelAttrs, gutterAttrs } = useUI<Config>(defaultConfig);
|
|
347
|
+
</script>
|
|
348
|
+
|
|
349
|
+
<template>
|
|
350
|
+
<div ref="wrapper" v-bind="wrapperAttrs" :data-test="getDataTest()">
|
|
351
|
+
<template v-for="(slotName, index) in panelSlots" :key="slotName">
|
|
352
|
+
<div
|
|
353
|
+
v-bind="panelAttrs"
|
|
354
|
+
:style="getPanelStyle(index)"
|
|
355
|
+
:data-test="getDataTest(`panel-${index + 1}`)"
|
|
356
|
+
>
|
|
357
|
+
<!--
|
|
358
|
+
@slot Use it to add panel content.
|
|
359
|
+
@binding {number} index
|
|
360
|
+
@binding {number} size
|
|
361
|
+
-->
|
|
362
|
+
<slot :name="slotName" :index="index" :size="panelSizes[index]" />
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
<div
|
|
366
|
+
v-if="index < panelSlots.length - 1"
|
|
367
|
+
role="separator"
|
|
368
|
+
tabindex="0"
|
|
369
|
+
:aria-orientation="vertical ? 'vertical' : 'horizontal'"
|
|
370
|
+
:aria-valuenow="getGutterAriaValue(index)"
|
|
371
|
+
:aria-valuemin="0"
|
|
372
|
+
:aria-valuemax="100"
|
|
373
|
+
:aria-label="`Resize panels ${index + 1} and ${index + 2}`"
|
|
374
|
+
:style="gutterStyle"
|
|
375
|
+
v-bind="gutterAttrs"
|
|
376
|
+
:data-test="getDataTest(`gutter-${index + 1}`)"
|
|
377
|
+
@pointerdown="onPointerDown($event, index)"
|
|
378
|
+
@keydown="onKeyDown($event, index)"
|
|
379
|
+
@dblclick="onDoubleClick(index)"
|
|
380
|
+
>
|
|
381
|
+
<!--
|
|
382
|
+
@slot Use it to add custom handle inside the divider.
|
|
383
|
+
@binding {boolean} is-dragging
|
|
384
|
+
@binding {number} index
|
|
385
|
+
-->
|
|
386
|
+
<slot name="handle" :is-dragging="isDragging" :index="index" />
|
|
387
|
+
</div>
|
|
388
|
+
</template>
|
|
389
|
+
</div>
|
|
390
|
+
</template>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export default /*tw*/ {
|
|
2
|
+
wrapper: {
|
|
3
|
+
base: "flex w-full h-full",
|
|
4
|
+
variants: {
|
|
5
|
+
vertical: {
|
|
6
|
+
false: "flex-row",
|
|
7
|
+
true: "flex-col",
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
panel: "",
|
|
12
|
+
gutter: {
|
|
13
|
+
base: `
|
|
14
|
+
flex items-center justify-center shrink-0 select-none rounded-medium
|
|
15
|
+
focus-visible:outline-2 focus-visible:outline-grayscale-accented
|
|
16
|
+
`,
|
|
17
|
+
variants: {
|
|
18
|
+
vertical: {
|
|
19
|
+
false: "cursor-col-resize w-[var(--gutter-size)] h-full",
|
|
20
|
+
true: "cursor-row-resize h-[var(--gutter-size)] w-full",
|
|
21
|
+
},
|
|
22
|
+
disabled: {
|
|
23
|
+
true: "cursor-not-allowed opacity-50",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
defaults: {
|
|
28
|
+
vertical: false,
|
|
29
|
+
gutterSize: 8,
|
|
30
|
+
disabled: false,
|
|
31
|
+
stateKey: null,
|
|
32
|
+
stateStorage: "session",
|
|
33
|
+
resizeStep: 5,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const COMPONENT_NAME = "USplitter";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Meta, Title, Subtitle, Description, Primary, Controls, Stories, Source } from "@storybook/addon-docs/blocks";
|
|
2
|
+
import { getSource } from "../../utils/storybook.ts";
|
|
3
|
+
|
|
4
|
+
import * as stories from "./stories.ts";
|
|
5
|
+
import defaultConfig from "../config.ts?raw"
|
|
6
|
+
|
|
7
|
+
<Meta of={stories} />
|
|
8
|
+
<Title of={stories} />
|
|
9
|
+
<Subtitle of={stories} />
|
|
10
|
+
<Description of={stories} />
|
|
11
|
+
<Primary of={stories} />
|
|
12
|
+
<Controls of={stories.Default} />
|
|
13
|
+
<Stories of={stories} />
|
|
14
|
+
|
|
15
|
+
## Default config
|
|
16
|
+
<Source code={getSource(defaultConfig)} language="jsx" dark />
|
|
17
|
+
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { getArgTypes, getDocsDescription } from "../../utils/storybook";
|
|
2
|
+
|
|
3
|
+
import USplitter from "../USplitter.vue";
|
|
4
|
+
import UPlaceholder from "../../ui.container-placeholder/UPlaceholder.vue";
|
|
5
|
+
import UIcon from "../../ui.image-icon/UIcon.vue";
|
|
6
|
+
|
|
7
|
+
import type { Meta, StoryFn } from "@storybook/vue3-vite";
|
|
8
|
+
import type { Props } from "../types.ts";
|
|
9
|
+
|
|
10
|
+
interface USplitterArgs extends Props {
|
|
11
|
+
slotTemplate?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
id: "5085",
|
|
16
|
+
title: "Containers / Splitter",
|
|
17
|
+
component: USplitter,
|
|
18
|
+
argTypes: {
|
|
19
|
+
...getArgTypes(USplitter.__name),
|
|
20
|
+
},
|
|
21
|
+
parameters: {
|
|
22
|
+
docs: {
|
|
23
|
+
...getDocsDescription(USplitter.__name),
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
} as Meta;
|
|
27
|
+
|
|
28
|
+
const DefaultTemplate: StoryFn<USplitterArgs> = (args: USplitterArgs) => ({
|
|
29
|
+
components: { USplitter, UPlaceholder },
|
|
30
|
+
setup: () => {
|
|
31
|
+
const sizes = args.modelValue || [40, 60];
|
|
32
|
+
|
|
33
|
+
return { args, sizes };
|
|
34
|
+
},
|
|
35
|
+
template: `
|
|
36
|
+
<div class="h-80">
|
|
37
|
+
<USplitter v-bind="args" v-model="sizes">
|
|
38
|
+
<template #panel-1>
|
|
39
|
+
<UPlaceholder label="Panel 1" />
|
|
40
|
+
</template>
|
|
41
|
+
<template #panel-2>
|
|
42
|
+
<UPlaceholder label="Panel 2" />
|
|
43
|
+
</template>
|
|
44
|
+
</USplitter>
|
|
45
|
+
</div>
|
|
46
|
+
`,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const Default = DefaultTemplate.bind({});
|
|
50
|
+
Default.args = {};
|
|
51
|
+
|
|
52
|
+
export const Disabled = DefaultTemplate.bind({});
|
|
53
|
+
Disabled.args = { disabled: true };
|
|
54
|
+
|
|
55
|
+
export const VerticalOrientation = DefaultTemplate.bind({});
|
|
56
|
+
VerticalOrientation.args = { vertical: true };
|
|
57
|
+
|
|
58
|
+
export const GutterSize = DefaultTemplate.bind({});
|
|
59
|
+
GutterSize.args = { gutterSize: 16 };
|
|
60
|
+
|
|
61
|
+
export const MinSizes = DefaultTemplate.bind({});
|
|
62
|
+
MinSizes.args = { minSizes: "30%", modelValue: [50, 50] };
|
|
63
|
+
MinSizes.parameters = {
|
|
64
|
+
docs: {
|
|
65
|
+
description: {
|
|
66
|
+
story: [
|
|
67
|
+
"Caps how small each panel can get while dragging.",
|
|
68
|
+
"Use one value for every panel or an array for per-panel limits.",
|
|
69
|
+
"Numbers are treated as pixels; strings can use `%` of the splitter axis or a `px` length.",
|
|
70
|
+
"Here each panel stays at least 30% wide.",
|
|
71
|
+
].join(" "),
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const MaxSizes = DefaultTemplate.bind({});
|
|
77
|
+
MaxSizes.args = { maxSizes: "65%", modelValue: [50, 50] };
|
|
78
|
+
MaxSizes.parameters = {
|
|
79
|
+
docs: {
|
|
80
|
+
description: {
|
|
81
|
+
story: [
|
|
82
|
+
"Caps how large each panel can grow. Omitted means a panel may use the full container.",
|
|
83
|
+
"Here neither panel can exceed 65% width.",
|
|
84
|
+
].join(" "),
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const ResizeStep = DefaultTemplate.bind({});
|
|
90
|
+
ResizeStep.args = { resizeStep: 10 };
|
|
91
|
+
ResizeStep.parameters = {
|
|
92
|
+
docs: {
|
|
93
|
+
description: {
|
|
94
|
+
story: "Increments/decrements the size of the panels while pressing the arrow keys.",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const StateKeyAndStorage = DefaultTemplate.bind({});
|
|
100
|
+
StateKeyAndStorage.args = { stateKey: "splitter-state", stateStorage: "local" };
|
|
101
|
+
StateKeyAndStorage.parameters = {
|
|
102
|
+
docs: {
|
|
103
|
+
description: {
|
|
104
|
+
story: [
|
|
105
|
+
"`stateKey` is the storage entry name: when set, the splitter saves panel sizes after each resize",
|
|
106
|
+
"and restores them on the next visit.",
|
|
107
|
+
"`stateStorage` chooses where that data lives—`session` (sessionStorage, cleared when the tab closes)",
|
|
108
|
+
"or `local` (localStorage, persists across sessions).",
|
|
109
|
+
"This story uses local storage so you can reload the page and keep your layout.",
|
|
110
|
+
].join(" "),
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const Nested: StoryFn<USplitterArgs> = (args: USplitterArgs) => ({
|
|
116
|
+
components: { USplitter, UPlaceholder },
|
|
117
|
+
setup: () => {
|
|
118
|
+
const sizes = args.modelValue || [35, 65];
|
|
119
|
+
|
|
120
|
+
return { args, sizes };
|
|
121
|
+
},
|
|
122
|
+
template: `
|
|
123
|
+
<div class="h-80">
|
|
124
|
+
<USplitter v-bind="args" v-model="sizes">
|
|
125
|
+
<template #panel-1>
|
|
126
|
+
<UPlaceholder label="Panel 1" />
|
|
127
|
+
</template>
|
|
128
|
+
<template #panel-2>
|
|
129
|
+
<USplitter v-bind="args" v-model="sizes" vertical>
|
|
130
|
+
<template #panel-1>
|
|
131
|
+
<UPlaceholder label="Panel 2" />
|
|
132
|
+
</template>
|
|
133
|
+
<template #panel-2>
|
|
134
|
+
<UPlaceholder label="Panel 3" />
|
|
135
|
+
</template>
|
|
136
|
+
</USplitter>
|
|
137
|
+
</template>
|
|
138
|
+
</USplitter>
|
|
139
|
+
</div>
|
|
140
|
+
`,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
export const HandleSlot: StoryFn<USplitterArgs> = (args: USplitterArgs) => ({
|
|
144
|
+
components: { USplitter, UPlaceholder, UIcon },
|
|
145
|
+
setup: () => {
|
|
146
|
+
const sizes = args.modelValue || [40, 60];
|
|
147
|
+
|
|
148
|
+
return { args, sizes };
|
|
149
|
+
},
|
|
150
|
+
template: `
|
|
151
|
+
<div class="h-80">
|
|
152
|
+
<USplitter v-bind="args" v-model="sizes">
|
|
153
|
+
<template #panel-1>
|
|
154
|
+
<UPlaceholder label="Panel 1" />
|
|
155
|
+
</template>
|
|
156
|
+
<template #panel-2>
|
|
157
|
+
<UPlaceholder label="Panel 2" />
|
|
158
|
+
</template>
|
|
159
|
+
<template #handle>
|
|
160
|
+
<UIcon
|
|
161
|
+
name="drag_handle"
|
|
162
|
+
size="sm"
|
|
163
|
+
color="neutral"
|
|
164
|
+
interactive
|
|
165
|
+
class="rotate-90 cursor-col-resize text-muted"
|
|
166
|
+
/>
|
|
167
|
+
</template>
|
|
168
|
+
</USplitter>
|
|
169
|
+
</div>
|
|
170
|
+
`,
|
|
171
|
+
});
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { mount } from "@vue/test-utils";
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
|
+
|
|
4
|
+
import USplitter from "../USplitter.vue";
|
|
5
|
+
|
|
6
|
+
function dispatchPointerDown(target: Element, clientX: number, clientY: number) {
|
|
7
|
+
target.dispatchEvent(
|
|
8
|
+
new PointerEvent("pointerdown", {
|
|
9
|
+
bubbles: true,
|
|
10
|
+
cancelable: true,
|
|
11
|
+
clientX,
|
|
12
|
+
clientY,
|
|
13
|
+
pointerId: 1,
|
|
14
|
+
pointerType: "mouse",
|
|
15
|
+
}),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("USplitter", () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.useFakeTimers();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
localStorage.clear();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("Props", () => {
|
|
30
|
+
it("ModelValue – initializes with provided sizes", () => {
|
|
31
|
+
const modelValue = [30, 70];
|
|
32
|
+
|
|
33
|
+
const component = mount(USplitter, {
|
|
34
|
+
props: {
|
|
35
|
+
modelValue,
|
|
36
|
+
},
|
|
37
|
+
slots: {
|
|
38
|
+
"panel-1": "<div>Panel 1</div>",
|
|
39
|
+
"panel-2": "<div>Panel 2</div>",
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const panels = component.findAll("[vl-key='panel']");
|
|
44
|
+
|
|
45
|
+
expect(panels).toHaveLength(2);
|
|
46
|
+
expect(panels[0].attributes("style")).toMatch(/(^|;)\s*flex(-basis)?:[^;]*30%/);
|
|
47
|
+
expect(panels[1].attributes("style")).toMatch(/(^|;)\s*flex(-basis)?:[^;]*70%/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("Vertical – applies horizontal layout by default", () => {
|
|
51
|
+
const component = mount(USplitter, {
|
|
52
|
+
slots: {
|
|
53
|
+
"panel-1": "<div>Panel 1</div>",
|
|
54
|
+
"panel-2": "<div>Panel 2</div>",
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const wrapper = component.find("[vl-key='wrapper']");
|
|
59
|
+
|
|
60
|
+
expect(wrapper.attributes("class")).toContain("flex-row");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("Vertical – applies vertical layout when true", () => {
|
|
64
|
+
const component = mount(USplitter, {
|
|
65
|
+
props: {
|
|
66
|
+
vertical: true,
|
|
67
|
+
},
|
|
68
|
+
slots: {
|
|
69
|
+
"panel-1": "<div>Panel 1</div>",
|
|
70
|
+
"panel-2": "<div>Panel 2</div>",
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const wrapper = component.find("[vl-key='wrapper']");
|
|
75
|
+
|
|
76
|
+
expect(wrapper.attributes("class")).toContain("flex-col");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("GutterSize – applies custom gutter size", () => {
|
|
80
|
+
const gutterSize = 10;
|
|
81
|
+
|
|
82
|
+
const component = mount(USplitter, {
|
|
83
|
+
props: {
|
|
84
|
+
gutterSize,
|
|
85
|
+
},
|
|
86
|
+
slots: {
|
|
87
|
+
"panel-1": "<div>Panel 1</div>",
|
|
88
|
+
"panel-2": "<div>Panel 2</div>",
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const gutter = component.find("[vl-key='gutter']");
|
|
93
|
+
|
|
94
|
+
expect(gutter.attributes("style")).toContain("--gutter-size: 10px");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("Disabled – applies disabled state", () => {
|
|
98
|
+
const component = mount(USplitter, {
|
|
99
|
+
props: {
|
|
100
|
+
disabled: true,
|
|
101
|
+
},
|
|
102
|
+
slots: {
|
|
103
|
+
"panel-1": "<div>Panel 1</div>",
|
|
104
|
+
"panel-2": "<div>Panel 2</div>",
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const gutter = component.find("[vl-key='gutter']");
|
|
109
|
+
|
|
110
|
+
expect(gutter.attributes("class")).toContain("cursor-not-allowed");
|
|
111
|
+
expect(gutter.attributes("class")).toContain("opacity-50");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("Slots", () => {
|
|
116
|
+
it("Panel slots – renders multiple panels", () => {
|
|
117
|
+
const component = mount(USplitter, {
|
|
118
|
+
slots: {
|
|
119
|
+
"panel-1": "<div>Panel 1</div>",
|
|
120
|
+
"panel-2": "<div>Panel 2</div>",
|
|
121
|
+
"panel-3": "<div>Panel 3</div>",
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const panels = component.findAll("[vl-key='panel']");
|
|
126
|
+
|
|
127
|
+
expect(panels).toHaveLength(3);
|
|
128
|
+
expect(component.text()).toContain("Panel 1");
|
|
129
|
+
expect(component.text()).toContain("Panel 2");
|
|
130
|
+
expect(component.text()).toContain("Panel 3");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("Handle slot – renders custom handle", () => {
|
|
134
|
+
const component = mount(USplitter, {
|
|
135
|
+
slots: {
|
|
136
|
+
"panel-1": "<div>Panel 1</div>",
|
|
137
|
+
"panel-2": "<div>Panel 2</div>",
|
|
138
|
+
handle: "<div class='custom-handle'>Custom</div>",
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(component.find(".custom-handle").exists()).toBe(true);
|
|
143
|
+
expect(component.text()).toContain("Custom");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("Events", () => {
|
|
148
|
+
it("Update:modelValue – emits when resizing", async () => {
|
|
149
|
+
const component = mount(USplitter, {
|
|
150
|
+
props: {
|
|
151
|
+
modelValue: [50, 50],
|
|
152
|
+
},
|
|
153
|
+
slots: {
|
|
154
|
+
"panel-1": "<div>Panel 1</div>",
|
|
155
|
+
"panel-2": "<div>Panel 2</div>",
|
|
156
|
+
},
|
|
157
|
+
attachTo: document.body,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const gutter = component.find("[vl-key='gutter']");
|
|
161
|
+
const wrapper = component.find("[vl-key='wrapper']");
|
|
162
|
+
|
|
163
|
+
vi.spyOn(wrapper.element, "getBoundingClientRect").mockReturnValue({
|
|
164
|
+
width: 1000,
|
|
165
|
+
height: 500,
|
|
166
|
+
top: 0,
|
|
167
|
+
left: 0,
|
|
168
|
+
right: 1000,
|
|
169
|
+
bottom: 500,
|
|
170
|
+
} as DOMRect);
|
|
171
|
+
|
|
172
|
+
dispatchPointerDown(gutter.element, 500, 250);
|
|
173
|
+
|
|
174
|
+
const pointerMoveEvent = new PointerEvent("pointermove", {
|
|
175
|
+
clientX: 600,
|
|
176
|
+
clientY: 250,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
document.dispatchEvent(pointerMoveEvent);
|
|
180
|
+
|
|
181
|
+
await component.vm.$nextTick();
|
|
182
|
+
|
|
183
|
+
expect(component.emitted("update:modelValue")).toBeTruthy();
|
|
184
|
+
|
|
185
|
+
component.unmount();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("Resize-start – emits when drag starts", async () => {
|
|
189
|
+
const component = mount(USplitter, {
|
|
190
|
+
slots: {
|
|
191
|
+
"panel-1": "<div>Panel 1</div>",
|
|
192
|
+
"panel-2": "<div>Panel 2</div>",
|
|
193
|
+
},
|
|
194
|
+
attachTo: document.body,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const gutter = component.find("[vl-key='gutter']");
|
|
198
|
+
|
|
199
|
+
dispatchPointerDown(gutter.element, 500, 250);
|
|
200
|
+
|
|
201
|
+
expect(component.emitted("resize-start")).toBeTruthy();
|
|
202
|
+
|
|
203
|
+
component.unmount();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("Resize-end – emits when drag ends", async () => {
|
|
207
|
+
const component = mount(USplitter, {
|
|
208
|
+
slots: {
|
|
209
|
+
"panel-1": "<div>Panel 1</div>",
|
|
210
|
+
"panel-2": "<div>Panel 2</div>",
|
|
211
|
+
},
|
|
212
|
+
attachTo: document.body,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const gutter = component.find("[vl-key='gutter']");
|
|
216
|
+
|
|
217
|
+
dispatchPointerDown(gutter.element, 500, 250);
|
|
218
|
+
|
|
219
|
+
const pointerUpEvent = new PointerEvent("pointerup");
|
|
220
|
+
|
|
221
|
+
document.dispatchEvent(pointerUpEvent);
|
|
222
|
+
|
|
223
|
+
await component.vm.$nextTick();
|
|
224
|
+
|
|
225
|
+
expect(component.emitted("resize-end")).toBeTruthy();
|
|
226
|
+
|
|
227
|
+
component.unmount();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe("Interaction", () => {
|
|
232
|
+
it("Gutter – renders between panels", () => {
|
|
233
|
+
const component = mount(USplitter, {
|
|
234
|
+
slots: {
|
|
235
|
+
"panel-1": "<div>Panel 1</div>",
|
|
236
|
+
"panel-2": "<div>Panel 2</div>",
|
|
237
|
+
"panel-3": "<div>Panel 3</div>",
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const gutters = component.findAll("[vl-key='gutter']");
|
|
242
|
+
|
|
243
|
+
expect(gutters).toHaveLength(2);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("Keyboard – handles arrow key navigation", async () => {
|
|
247
|
+
const component = mount(USplitter, {
|
|
248
|
+
props: {
|
|
249
|
+
modelValue: [50, 50],
|
|
250
|
+
},
|
|
251
|
+
slots: {
|
|
252
|
+
"panel-1": "<div>Panel 1</div>",
|
|
253
|
+
"panel-2": "<div>Panel 2</div>",
|
|
254
|
+
},
|
|
255
|
+
attachTo: document.body,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const gutter = component.find("[vl-key='gutter']");
|
|
259
|
+
const wrapper = component.find("[vl-key='wrapper']");
|
|
260
|
+
|
|
261
|
+
vi.spyOn(wrapper.element, "getBoundingClientRect").mockReturnValue({
|
|
262
|
+
width: 1000,
|
|
263
|
+
height: 500,
|
|
264
|
+
top: 0,
|
|
265
|
+
left: 0,
|
|
266
|
+
right: 1000,
|
|
267
|
+
bottom: 500,
|
|
268
|
+
} as DOMRect);
|
|
269
|
+
|
|
270
|
+
await gutter.trigger("keydown", {
|
|
271
|
+
key: "ArrowRight",
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await component.vm.$nextTick();
|
|
275
|
+
|
|
276
|
+
expect(component.emitted("update:modelValue")).toBeTruthy();
|
|
277
|
+
|
|
278
|
+
component.unmount();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("Double click – resets to equal sizes", async () => {
|
|
282
|
+
const component = mount(USplitter, {
|
|
283
|
+
props: {
|
|
284
|
+
modelValue: [30, 70],
|
|
285
|
+
},
|
|
286
|
+
slots: {
|
|
287
|
+
"panel-1": "<div>Panel 1</div>",
|
|
288
|
+
"panel-2": "<div>Panel 2</div>",
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const gutter = component.find("[vl-key='gutter']");
|
|
293
|
+
|
|
294
|
+
await gutter.trigger("dblclick");
|
|
295
|
+
|
|
296
|
+
const emitted = component.emitted("update:modelValue");
|
|
297
|
+
|
|
298
|
+
expect(emitted).toBeTruthy();
|
|
299
|
+
|
|
300
|
+
if (!emitted) {
|
|
301
|
+
throw new Error("expected update:modelValue");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const lastEmit = emitted[emitted.length - 1][0] as number[];
|
|
305
|
+
|
|
306
|
+
expect(lastEmit[0]).toBeCloseTo(50, 0);
|
|
307
|
+
expect(lastEmit[1]).toBeCloseTo(50, 0);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe("Accessibility", () => {
|
|
312
|
+
it("ARIA – gutter has proper ARIA attributes", () => {
|
|
313
|
+
const component = mount(USplitter, {
|
|
314
|
+
props: {
|
|
315
|
+
modelValue: [40, 60],
|
|
316
|
+
},
|
|
317
|
+
slots: {
|
|
318
|
+
"panel-1": "<div>Panel 1</div>",
|
|
319
|
+
"panel-2": "<div>Panel 2</div>",
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const gutter = component.find("[vl-key='gutter']");
|
|
324
|
+
|
|
325
|
+
expect(gutter.attributes("role")).toBe("separator");
|
|
326
|
+
expect(gutter.attributes("aria-orientation")).toBe("horizontal");
|
|
327
|
+
expect(gutter.attributes("aria-valuenow")).toBe("40");
|
|
328
|
+
expect(gutter.attributes("aria-valuemin")).toBe("0");
|
|
329
|
+
expect(gutter.attributes("aria-valuemax")).toBe("100");
|
|
330
|
+
expect(gutter.attributes("tabindex")).toBe("0");
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe("Exposed refs", () => {
|
|
335
|
+
it("WrapperRef – exposes wrapper element ref", () => {
|
|
336
|
+
const component = mount(USplitter, {
|
|
337
|
+
slots: {
|
|
338
|
+
"panel-1": "<div>Panel 1</div>",
|
|
339
|
+
"panel-2": "<div>Panel 2</div>",
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
expect(component.vm.wrapperRef).toBeDefined();
|
|
344
|
+
expect(component.vm.wrapperRef instanceof HTMLDivElement).toBe(true);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import defaultConfig from "./config";
|
|
2
|
+
|
|
3
|
+
import type { ComponentConfig } from "../types";
|
|
4
|
+
|
|
5
|
+
export type Config = typeof defaultConfig;
|
|
6
|
+
|
|
7
|
+
export interface Props {
|
|
8
|
+
/**
|
|
9
|
+
* Panel sizes (percentage or px).
|
|
10
|
+
*/
|
|
11
|
+
modelValue?: number[];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Makes the splitter vertical (panels stacked top-to-bottom).
|
|
15
|
+
*/
|
|
16
|
+
vertical?: boolean;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Minimum sizes per panel (percentage or px). Can be a single value for all panels or an array of values per panel.
|
|
20
|
+
*/
|
|
21
|
+
minSizes?: number | string | (number | string)[];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Maximum sizes per panel (percentage or px). Can be a single value for all panels or an array of values per panel.
|
|
25
|
+
*/
|
|
26
|
+
maxSizes?: number | string | (number | string)[];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Gutter (divider) size in pixels.
|
|
30
|
+
*/
|
|
31
|
+
gutterSize?: number;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Disable resizing.
|
|
35
|
+
*/
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Storage identifier of a stateful Splitter.
|
|
40
|
+
*/
|
|
41
|
+
stateKey?: string | null;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Defines where a stateful splitter keeps its state, valid values are 'session' for sessionStorage and 'local' for localStorage.
|
|
45
|
+
*/
|
|
46
|
+
stateStorage?: "session" | "local";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Step factor to increment/decrement the size of the panels while pressing the arrow keys.
|
|
50
|
+
*/
|
|
51
|
+
resizeStep?: number;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Component config object.
|
|
55
|
+
*/
|
|
56
|
+
config?: ComponentConfig<Config>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Data-test attribute for automated testing.
|
|
60
|
+
*/
|
|
61
|
+
dataTest?: string | null;
|
|
62
|
+
}
|