diff --git a/packages/vue-split-panel/src/SplitPanel.vue b/packages/vue-split-panel/src/SplitPanel.vue index b15560e..46d88d5 100644 --- a/packages/vue-split-panel/src/SplitPanel.vue +++ b/packages/vue-split-panel/src/SplitPanel.vue @@ -1,6 +1,7 @@ diff --git a/packages/vue-split-panel/src/composables/use-collapse.test.ts b/packages/vue-split-panel/src/composables/use-collapse.test.ts new file mode 100644 index 0000000..9c50f0a --- /dev/null +++ b/packages/vue-split-panel/src/composables/use-collapse.test.ts @@ -0,0 +1,312 @@ +import { describe, expect, it, vi } from 'vitest'; +import { nextTick, ref } from 'vue'; +import { useCollapse } from './use-collapse'; + +describe('useCollapse', () => { + it('should return expected methods and properties', () => { + const collapsed = ref(false); + const sizePercentage = ref(50); + const emits = vi.fn(); + + const result = useCollapse(collapsed, sizePercentage, emits); + + expect(result).toHaveProperty('onTransitionEnd'); + expect(result).toHaveProperty('collapse'); + expect(result).toHaveProperty('expand'); + expect(result).toHaveProperty('toggle'); + expect(result).toHaveProperty('collapseTransitionState'); + expect(typeof result.onTransitionEnd).toBe('function'); + expect(typeof result.collapse).toBe('function'); + expect(typeof result.expand).toBe('function'); + expect(typeof result.toggle).toBe('function'); + expect(result.collapseTransitionState.value).toBeNull(); + }); + + describe('collapse method', () => { + it('should set collapsed to true', () => { + const collapsed = ref(false); + const sizePercentage = ref(50); + const emits = vi.fn(); + + const { collapse } = useCollapse(collapsed, sizePercentage, emits); + + collapse(); + + expect(collapsed.value).toBe(true); + }); + }); + + describe('expand method', () => { + it('should set collapsed to false', () => { + const collapsed = ref(true); + const sizePercentage = ref(0); + const emits = vi.fn(); + + const { expand } = useCollapse(collapsed, sizePercentage, emits); + + expand(); + + expect(collapsed.value).toBe(false); + }); + }); + + describe('toggle method', () => { + it('should set collapsed to the provided value', () => { + const collapsed = ref(false); + const sizePercentage = ref(50); + const emits = vi.fn(); + + const { toggle } = useCollapse(collapsed, sizePercentage, emits); + + toggle(true); + expect(collapsed.value).toBe(true); + + toggle(false); + expect(collapsed.value).toBe(false); + }); + }); + + describe('collapsed watcher behavior', () => { + it('should store size and set to 0 when collapsing', async () => { + const collapsed = ref(false); + const sizePercentage = ref(75); + const emits = vi.fn(); + + const { collapseTransitionState } = useCollapse(collapsed, sizePercentage, emits); + + collapsed.value = true; + await nextTick(); + + expect(sizePercentage.value).toBe(0); + expect(collapseTransitionState.value).toBe('collapsing'); + }); + + it('should restore size when expanding', async () => { + const collapsed = ref(false); + const sizePercentage = ref(60); + const emits = vi.fn(); + + const { collapseTransitionState } = useCollapse(collapsed, sizePercentage, emits); + + // First collapse to store the size + collapsed.value = true; + await nextTick(); + expect(sizePercentage.value).toBe(0); + + // Then expand to restore + collapsed.value = false; + await nextTick(); + + expect(sizePercentage.value).toBe(60); + expect(collapseTransitionState.value).toBe('expanding'); + }); + + it('should preserve original size through multiple collapse/expand cycles', async () => { + const collapsed = ref(false); + const sizePercentage = ref(42); + const emits = vi.fn(); + + useCollapse(collapsed, sizePercentage, emits); + + // First cycle + collapsed.value = true; + await nextTick(); + expect(sizePercentage.value).toBe(0); + + collapsed.value = false; + await nextTick(); + expect(sizePercentage.value).toBe(42); + + // Second cycle + collapsed.value = true; + await nextTick(); + expect(sizePercentage.value).toBe(0); + + collapsed.value = false; + await nextTick(); + expect(sizePercentage.value).toBe(42); + }); + + it('should handle size changes between collapse cycles', async () => { + const collapsed = ref(false); + const sizePercentage = ref(30); + const emits = vi.fn(); + + useCollapse(collapsed, sizePercentage, emits); + + // First collapse + collapsed.value = true; + await nextTick(); + expect(sizePercentage.value).toBe(0); + + // Expand and change size + collapsed.value = false; + await nextTick(); + expect(sizePercentage.value).toBe(30); + + // Manually change size while expanded + sizePercentage.value = 80; + + // Collapse again - should store new size + collapsed.value = true; + await nextTick(); + expect(sizePercentage.value).toBe(0); + + // Expand - should restore new size + collapsed.value = false; + await nextTick(); + expect(sizePercentage.value).toBe(80); + }); + }); + + describe('transition state management', () => { + it('should start with null transition state', () => { + const collapsed = ref(false); + const sizePercentage = ref(50); + const emits = vi.fn(); + + const { collapseTransitionState } = useCollapse(collapsed, sizePercentage, emits); + + expect(collapseTransitionState.value).toBeNull(); + }); + + it('should set collapsing state when collapsed becomes true', async () => { + const collapsed = ref(false); + const sizePercentage = ref(50); + const emits = vi.fn(); + + const { collapseTransitionState } = useCollapse(collapsed, sizePercentage, emits); + + collapsed.value = true; + await nextTick(); + + expect(collapseTransitionState.value).toBe('collapsing'); + }); + + it('should set expanding state when collapsed becomes false', async () => { + const collapsed = ref(true); + const sizePercentage = ref(0); + const emits = vi.fn(); + + const { collapseTransitionState } = useCollapse(collapsed, sizePercentage, emits); + + collapsed.value = false; + await nextTick(); + + expect(collapseTransitionState.value).toBe('expanding'); + }); + }); + + describe('onTransitionEnd', () => { + it('should clear transition state and emit event', () => { + const collapsed = ref(false); + const sizePercentage = ref(50); + const emits = vi.fn(); + + const { onTransitionEnd, collapseTransitionState } = useCollapse(collapsed, sizePercentage, emits); + + // Set a transition state first + collapseTransitionState.value = 'collapsing'; + + const mockEvent = new TransitionEvent('transitionend', { + propertyName: 'grid-template-columns', + elapsedTime: 0.3, + }); + + onTransitionEnd(mockEvent); + + expect(collapseTransitionState.value).toBeNull(); + expect(emits).toHaveBeenCalledWith('transitionend', mockEvent); + }); + + it('should work with expanding state', () => { + const collapsed = ref(true); + const sizePercentage = ref(0); + const emits = vi.fn(); + + const { onTransitionEnd, collapseTransitionState } = useCollapse(collapsed, sizePercentage, emits); + + collapseTransitionState.value = 'expanding'; + + const mockEvent = new TransitionEvent('transitionend'); + onTransitionEnd(mockEvent); + + expect(collapseTransitionState.value).toBeNull(); + expect(emits).toHaveBeenCalledWith('transitionend', mockEvent); + }); + }); + + describe('integration scenarios', () => { + it('should handle rapid collapse/expand operations', async () => { + const collapsed = ref(false); + const sizePercentage = ref(65); + const emits = vi.fn(); + + const { collapseTransitionState, onTransitionEnd } = useCollapse(collapsed, sizePercentage, emits); + + // Rapid collapse + collapsed.value = true; + await nextTick(); + expect(collapseTransitionState.value).toBe('collapsing'); + expect(sizePercentage.value).toBe(0); + + // Immediate expand before transition ends + collapsed.value = false; + await nextTick(); + expect(collapseTransitionState.value).toBe('expanding'); + expect(sizePercentage.value).toBe(65); + + // Simulate transition end + const mockEvent = new TransitionEvent('transitionend'); + onTransitionEnd(mockEvent); + expect(collapseTransitionState.value).toBeNull(); + }); + + it('should work with methods triggering state changes', async () => { + const collapsed = ref(false); + const sizePercentage = ref(45); + const emits = vi.fn(); + + const { collapse, expand, toggle, collapseTransitionState } = useCollapse(collapsed, sizePercentage, emits); + + // Use collapse method + collapse(); + await nextTick(); + expect(collapsed.value).toBe(true); + expect(sizePercentage.value).toBe(0); + expect(collapseTransitionState.value).toBe('collapsing'); + + // Use expand method + expand(); + await nextTick(); + expect(collapsed.value).toBe(false); + expect(sizePercentage.value).toBe(45); + expect(collapseTransitionState.value).toBe('expanding'); + + // Use toggle method + toggle(true); + await nextTick(); + expect(collapsed.value).toBe(true); + expect(sizePercentage.value).toBe(0); + expect(collapseTransitionState.value).toBe('collapsing'); + }); + + it('should work with zero initial size', async () => { + const collapsed = ref(false); + const sizePercentage = ref(0); + const emits = vi.fn(); + + useCollapse(collapsed, sizePercentage, emits); + + // Collapse when already at 0 + collapsed.value = true; + await nextTick(); + expect(sizePercentage.value).toBe(0); + + // Expand - should restore to 0 + collapsed.value = false; + await nextTick(); + expect(sizePercentage.value).toBe(0); + }); + }); +}); diff --git a/packages/vue-split-panel/src/composables/use-collapse.ts b/packages/vue-split-panel/src/composables/use-collapse.ts new file mode 100644 index 0000000..f2d88f2 --- /dev/null +++ b/packages/vue-split-panel/src/composables/use-collapse.ts @@ -0,0 +1,31 @@ +import type { Ref } from 'vue'; +import { ref, watch } from 'vue'; + +export const useCollapse = (collapsed: Ref, sizePercentage: Ref, emits: (evt: 'transitionend', event: TransitionEvent) => void) => { + let expandedSizePercentage = 0; + + const collapseTransitionState = ref(null); + + const onTransitionEnd = (event: TransitionEvent) => { + collapseTransitionState.value = null; + emits('transitionend', event); + }; + + watch(collapsed, (newCollapsed) => { + if (newCollapsed === true) { + expandedSizePercentage = sizePercentage.value; + sizePercentage.value = 0; + collapseTransitionState.value = 'collapsing'; + } + else { + sizePercentage.value = expandedSizePercentage; + collapseTransitionState.value = 'expanding'; + } + }); + + const collapse = () => collapsed.value = true; + const expand = () => collapsed.value = false; + const toggle = (val: boolean) => collapsed.value = val; + + return { onTransitionEnd, collapse, expand, toggle, collapseTransitionState }; +};