Skip to content

Commit 4e057e9

Browse files
refactor: move collapsing logic to composable (#43)
1 parent ad7145c commit 4e057e9

File tree

3 files changed

+346
-25
lines changed

3 files changed

+346
-25
lines changed

packages/vue-split-panel/src/SplitPanel.vue

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts" setup>
22
import type { SplitPanelProps } from './types';
3-
import { ref, useTemplateRef, watch } from 'vue';
3+
import { useTemplateRef } from 'vue';
4+
import { useCollapse } from './composables/use-collapse';
45
import { useGridTemplate } from './composables/use-grid-template';
56
import { useKeyboard } from './composables/use-keyboard';
67
import { usePointer } from './composables/use-pointer';
@@ -35,10 +36,6 @@ const collapsed = defineModel<boolean>('collapsed', { default: false });
3536
const panelEl = useTemplateRef('split-panel');
3637
const dividerEl = useTemplateRef('divider');
3738
38-
let expandedSizePercentage = 0;
39-
40-
const collapseTransitionState = ref<null | 'expanding' | 'collapsing'>(null);
41-
4239
const {
4340
sizePercentage,
4441
sizePixels,
@@ -103,26 +100,7 @@ useResize(sizePercentage, {
103100
primary: () => props.primary,
104101
});
105102
106-
const onTransitionEnd = (event: TransitionEvent) => {
107-
collapseTransitionState.value = null;
108-
emits('transitionend', event);
109-
};
110-
111-
watch(collapsed, (newCollapsed) => {
112-
if (newCollapsed === true) {
113-
expandedSizePercentage = sizePercentage.value;
114-
sizePercentage.value = 0;
115-
collapseTransitionState.value = 'collapsing';
116-
}
117-
else {
118-
sizePercentage.value = expandedSizePercentage;
119-
collapseTransitionState.value = 'expanding';
120-
}
121-
});
122-
123-
const collapse = () => collapsed.value = true;
124-
const expand = () => collapsed.value = false;
125-
const toggle = (val: boolean) => collapsed.value = val;
103+
const { onTransitionEnd, collapseTransitionState, toggle, expand, collapse } = useCollapse(collapsed, sizePercentage, emits);
126104
127105
defineExpose({ collapse, expand, toggle });
128106
</script>
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
import { nextTick, ref } from 'vue';
3+
import { useCollapse } from './use-collapse';
4+
5+
describe('useCollapse', () => {
6+
it('should return expected methods and properties', () => {
7+
const collapsed = ref(false);
8+
const sizePercentage = ref(50);
9+
const emits = vi.fn();
10+
11+
const result = useCollapse(collapsed, sizePercentage, emits);
12+
13+
expect(result).toHaveProperty('onTransitionEnd');
14+
expect(result).toHaveProperty('collapse');
15+
expect(result).toHaveProperty('expand');
16+
expect(result).toHaveProperty('toggle');
17+
expect(result).toHaveProperty('collapseTransitionState');
18+
expect(typeof result.onTransitionEnd).toBe('function');
19+
expect(typeof result.collapse).toBe('function');
20+
expect(typeof result.expand).toBe('function');
21+
expect(typeof result.toggle).toBe('function');
22+
expect(result.collapseTransitionState.value).toBeNull();
23+
});
24+
25+
describe('collapse method', () => {
26+
it('should set collapsed to true', () => {
27+
const collapsed = ref(false);
28+
const sizePercentage = ref(50);
29+
const emits = vi.fn();
30+
31+
const { collapse } = useCollapse(collapsed, sizePercentage, emits);
32+
33+
collapse();
34+
35+
expect(collapsed.value).toBe(true);
36+
});
37+
});
38+
39+
describe('expand method', () => {
40+
it('should set collapsed to false', () => {
41+
const collapsed = ref(true);
42+
const sizePercentage = ref(0);
43+
const emits = vi.fn();
44+
45+
const { expand } = useCollapse(collapsed, sizePercentage, emits);
46+
47+
expand();
48+
49+
expect(collapsed.value).toBe(false);
50+
});
51+
});
52+
53+
describe('toggle method', () => {
54+
it('should set collapsed to the provided value', () => {
55+
const collapsed = ref(false);
56+
const sizePercentage = ref(50);
57+
const emits = vi.fn();
58+
59+
const { toggle } = useCollapse(collapsed, sizePercentage, emits);
60+
61+
toggle(true);
62+
expect(collapsed.value).toBe(true);
63+
64+
toggle(false);
65+
expect(collapsed.value).toBe(false);
66+
});
67+
});
68+
69+
describe('collapsed watcher behavior', () => {
70+
it('should store size and set to 0 when collapsing', async () => {
71+
const collapsed = ref(false);
72+
const sizePercentage = ref(75);
73+
const emits = vi.fn();
74+
75+
const { collapseTransitionState } = useCollapse(collapsed, sizePercentage, emits);
76+
77+
collapsed.value = true;
78+
await nextTick();
79+
80+
expect(sizePercentage.value).toBe(0);
81+
expect(collapseTransitionState.value).toBe('collapsing');
82+
});
83+
84+
it('should restore size when expanding', async () => {
85+
const collapsed = ref(false);
86+
const sizePercentage = ref(60);
87+
const emits = vi.fn();
88+
89+
const { collapseTransitionState } = useCollapse(collapsed, sizePercentage, emits);
90+
91+
// First collapse to store the size
92+
collapsed.value = true;
93+
await nextTick();
94+
expect(sizePercentage.value).toBe(0);
95+
96+
// Then expand to restore
97+
collapsed.value = false;
98+
await nextTick();
99+
100+
expect(sizePercentage.value).toBe(60);
101+
expect(collapseTransitionState.value).toBe('expanding');
102+
});
103+
104+
it('should preserve original size through multiple collapse/expand cycles', async () => {
105+
const collapsed = ref(false);
106+
const sizePercentage = ref(42);
107+
const emits = vi.fn();
108+
109+
useCollapse(collapsed, sizePercentage, emits);
110+
111+
// First cycle
112+
collapsed.value = true;
113+
await nextTick();
114+
expect(sizePercentage.value).toBe(0);
115+
116+
collapsed.value = false;
117+
await nextTick();
118+
expect(sizePercentage.value).toBe(42);
119+
120+
// Second cycle
121+
collapsed.value = true;
122+
await nextTick();
123+
expect(sizePercentage.value).toBe(0);
124+
125+
collapsed.value = false;
126+
await nextTick();
127+
expect(sizePercentage.value).toBe(42);
128+
});
129+
130+
it('should handle size changes between collapse cycles', async () => {
131+
const collapsed = ref(false);
132+
const sizePercentage = ref(30);
133+
const emits = vi.fn();
134+
135+
useCollapse(collapsed, sizePercentage, emits);
136+
137+
// First collapse
138+
collapsed.value = true;
139+
await nextTick();
140+
expect(sizePercentage.value).toBe(0);
141+
142+
// Expand and change size
143+
collapsed.value = false;
144+
await nextTick();
145+
expect(sizePercentage.value).toBe(30);
146+
147+
// Manually change size while expanded
148+
sizePercentage.value = 80;
149+
150+
// Collapse again - should store new size
151+
collapsed.value = true;
152+
await nextTick();
153+
expect(sizePercentage.value).toBe(0);
154+
155+
// Expand - should restore new size
156+
collapsed.value = false;
157+
await nextTick();
158+
expect(sizePercentage.value).toBe(80);
159+
});
160+
});
161+
162+
describe('transition state management', () => {
163+
it('should start with null transition state', () => {
164+
const collapsed = ref(false);
165+
const sizePercentage = ref(50);
166+
const emits = vi.fn();
167+
168+
const { collapseTransitionState } = useCollapse(collapsed, sizePercentage, emits);
169+
170+
expect(collapseTransitionState.value).toBeNull();
171+
});
172+
173+
it('should set collapsing state when collapsed becomes true', async () => {
174+
const collapsed = ref(false);
175+
const sizePercentage = ref(50);
176+
const emits = vi.fn();
177+
178+
const { collapseTransitionState } = useCollapse(collapsed, sizePercentage, emits);
179+
180+
collapsed.value = true;
181+
await nextTick();
182+
183+
expect(collapseTransitionState.value).toBe('collapsing');
184+
});
185+
186+
it('should set expanding state when collapsed becomes false', async () => {
187+
const collapsed = ref(true);
188+
const sizePercentage = ref(0);
189+
const emits = vi.fn();
190+
191+
const { collapseTransitionState } = useCollapse(collapsed, sizePercentage, emits);
192+
193+
collapsed.value = false;
194+
await nextTick();
195+
196+
expect(collapseTransitionState.value).toBe('expanding');
197+
});
198+
});
199+
200+
describe('onTransitionEnd', () => {
201+
it('should clear transition state and emit event', () => {
202+
const collapsed = ref(false);
203+
const sizePercentage = ref(50);
204+
const emits = vi.fn();
205+
206+
const { onTransitionEnd, collapseTransitionState } = useCollapse(collapsed, sizePercentage, emits);
207+
208+
// Set a transition state first
209+
collapseTransitionState.value = 'collapsing';
210+
211+
const mockEvent = new TransitionEvent('transitionend', {
212+
propertyName: 'grid-template-columns',
213+
elapsedTime: 0.3,
214+
});
215+
216+
onTransitionEnd(mockEvent);
217+
218+
expect(collapseTransitionState.value).toBeNull();
219+
expect(emits).toHaveBeenCalledWith('transitionend', mockEvent);
220+
});
221+
222+
it('should work with expanding state', () => {
223+
const collapsed = ref(true);
224+
const sizePercentage = ref(0);
225+
const emits = vi.fn();
226+
227+
const { onTransitionEnd, collapseTransitionState } = useCollapse(collapsed, sizePercentage, emits);
228+
229+
collapseTransitionState.value = 'expanding';
230+
231+
const mockEvent = new TransitionEvent('transitionend');
232+
onTransitionEnd(mockEvent);
233+
234+
expect(collapseTransitionState.value).toBeNull();
235+
expect(emits).toHaveBeenCalledWith('transitionend', mockEvent);
236+
});
237+
});
238+
239+
describe('integration scenarios', () => {
240+
it('should handle rapid collapse/expand operations', async () => {
241+
const collapsed = ref(false);
242+
const sizePercentage = ref(65);
243+
const emits = vi.fn();
244+
245+
const { collapseTransitionState, onTransitionEnd } = useCollapse(collapsed, sizePercentage, emits);
246+
247+
// Rapid collapse
248+
collapsed.value = true;
249+
await nextTick();
250+
expect(collapseTransitionState.value).toBe('collapsing');
251+
expect(sizePercentage.value).toBe(0);
252+
253+
// Immediate expand before transition ends
254+
collapsed.value = false;
255+
await nextTick();
256+
expect(collapseTransitionState.value).toBe('expanding');
257+
expect(sizePercentage.value).toBe(65);
258+
259+
// Simulate transition end
260+
const mockEvent = new TransitionEvent('transitionend');
261+
onTransitionEnd(mockEvent);
262+
expect(collapseTransitionState.value).toBeNull();
263+
});
264+
265+
it('should work with methods triggering state changes', async () => {
266+
const collapsed = ref(false);
267+
const sizePercentage = ref(45);
268+
const emits = vi.fn();
269+
270+
const { collapse, expand, toggle, collapseTransitionState } = useCollapse(collapsed, sizePercentage, emits);
271+
272+
// Use collapse method
273+
collapse();
274+
await nextTick();
275+
expect(collapsed.value).toBe(true);
276+
expect(sizePercentage.value).toBe(0);
277+
expect(collapseTransitionState.value).toBe('collapsing');
278+
279+
// Use expand method
280+
expand();
281+
await nextTick();
282+
expect(collapsed.value).toBe(false);
283+
expect(sizePercentage.value).toBe(45);
284+
expect(collapseTransitionState.value).toBe('expanding');
285+
286+
// Use toggle method
287+
toggle(true);
288+
await nextTick();
289+
expect(collapsed.value).toBe(true);
290+
expect(sizePercentage.value).toBe(0);
291+
expect(collapseTransitionState.value).toBe('collapsing');
292+
});
293+
294+
it('should work with zero initial size', async () => {
295+
const collapsed = ref(false);
296+
const sizePercentage = ref(0);
297+
const emits = vi.fn();
298+
299+
useCollapse(collapsed, sizePercentage, emits);
300+
301+
// Collapse when already at 0
302+
collapsed.value = true;
303+
await nextTick();
304+
expect(sizePercentage.value).toBe(0);
305+
306+
// Expand - should restore to 0
307+
collapsed.value = false;
308+
await nextTick();
309+
expect(sizePercentage.value).toBe(0);
310+
});
311+
});
312+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { Ref } from 'vue';
2+
import { ref, watch } from 'vue';
3+
4+
export const useCollapse = (collapsed: Ref<boolean>, sizePercentage: Ref<number>, emits: (evt: 'transitionend', event: TransitionEvent) => void) => {
5+
let expandedSizePercentage = 0;
6+
7+
const collapseTransitionState = ref<null | 'expanding' | 'collapsing'>(null);
8+
9+
const onTransitionEnd = (event: TransitionEvent) => {
10+
collapseTransitionState.value = null;
11+
emits('transitionend', event);
12+
};
13+
14+
watch(collapsed, (newCollapsed) => {
15+
if (newCollapsed === true) {
16+
expandedSizePercentage = sizePercentage.value;
17+
sizePercentage.value = 0;
18+
collapseTransitionState.value = 'collapsing';
19+
}
20+
else {
21+
sizePercentage.value = expandedSizePercentage;
22+
collapseTransitionState.value = 'expanding';
23+
}
24+
});
25+
26+
const collapse = () => collapsed.value = true;
27+
const expand = () => collapsed.value = false;
28+
const toggle = (val: boolean) => collapsed.value = val;
29+
30+
return { onTransitionEnd, collapse, expand, toggle, collapseTransitionState };
31+
};

0 commit comments

Comments
 (0)