feat(components): [splitter] add lazy prop (#21730)

* feat(components): [splitter] add lazy prop

* chore: cleanup

* fix: lazy change should end dragging
This commit is contained in:
Zhong
2025-08-16 17:22:54 +08:00
committed by GitHub
parent 9ac2da6c07
commit 67ce064086
10 changed files with 206 additions and 28 deletions

View File

@@ -57,13 +57,24 @@ splitter/size
:::
## Lazy ^(2.11.0)
When `lazy` is enabled, the panel size will not update in real time during dragging, but only after the drag ends.
:::demo
splitter/lazy
:::
## Splitter API
### Splitter Attributes
| Name | Description | Type | Default |
| ------ | -------------------------------- | ----------------------------------- | ---------- |
| layout | Layout direction of the splitter | ^[enum]`'horizontal' \| 'vertical'` | horizontal |
| Name | Description | Type | Default |
| -------------- | -------------------------------- | ----------------------------------- | ---------- |
| layout | Layout direction of the splitter | ^[enum]`'horizontal' \| 'vertical'` | horizontal |
| lazy ^(2.11.0) | Whether to enable lazy mode | ^[boolean] | false |
### Splitter Events

View File

@@ -0,0 +1,26 @@
<template>
<div
style="height: 250px; box-shadow: var(--el-border-color-light) 0px 0px 10px"
>
<el-splitter lazy>
<el-splitter-panel collapsible min="50">
<div class="demo-panel">1</div>
</el-splitter-panel>
<el-splitter-panel collapsible>
<div class="demo-panel">2</div>
</el-splitter-panel>
<el-splitter-panel collapsible>
<div class="demo-panel">3</div>
</el-splitter-panel>
</el-splitter>
</div>
</template>
<style scoped>
.demo-panel {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
</style>

View File

@@ -269,4 +269,64 @@ describe('Splitter', () => {
// Panel should be collapsed (size = 400)
expect(panels[0].attributes('style')).toContain('flex-basis: 400px;')
})
it('should not update panel size until drag ends when lazy is true', async () => {
const wrapper = mount(() => (
<div style={{ width: '400px', height: '400px' }}>
<ElSplitter lazy>
<ElSplitterPanel>Left Panel</ElSplitterPanel>
<ElSplitterPanel>Right Panel</ElSplitterPanel>
</ElSplitter>
</div>
))
await nextTick()
const panels = wrapper.findAll('.el-splitter-panel')
const splitBar = wrapper.find('.el-splitter-bar__dragger')
const mousedown = new MouseEvent('mousedown', { bubbles: true })
Object.defineProperty(mousedown, 'pageX', { value: 200 })
splitBar.element.dispatchEvent(mousedown)
const mousemove = new MouseEvent('mousemove', { bubbles: true })
Object.defineProperty(mousemove, 'pageX', { value: 100 })
window.dispatchEvent(mousemove)
await nextTick()
expect(panels[0].attributes('style')).toContain('flex-basis: 200px;')
const mouseup = new MouseEvent('mouseup', { bubbles: true })
Object.defineProperty(mouseup, 'pageX', { value: 100 })
window.dispatchEvent(mouseup)
await nextTick()
expect(panels[0].attributes('style')).toContain('flex-basis: 100px;')
})
it('should update panel size immediately when lazy is false', async () => {
const wrapper = mount(() => (
<div style={{ width: '400px', height: '400px' }}>
<ElSplitter>
<ElSplitterPanel>Left Panel</ElSplitterPanel>
<ElSplitterPanel>Right Panel</ElSplitterPanel>
</ElSplitter>
</div>
))
await nextTick()
const panels = wrapper.findAll('.el-splitter-panel')
const splitBar = wrapper.find('.el-splitter-bar__dragger')
const mousedown = new MouseEvent('mousedown', { bubbles: true })
Object.defineProperty(mousedown, 'pageX', { value: 200 })
splitBar.element.dispatchEvent(mousedown)
const mousemove = new MouseEvent('mousemove', { bubbles: true })
Object.defineProperty(mousemove, 'pageX', { value: 100 })
window.dispatchEvent(mousemove)
await nextTick()
expect(panels[0].attributes('style')).toContain('flex-basis: 100px;')
const mouseup = new MouseEvent('mouseup', { bubbles: true })
Object.defineProperty(mouseup, 'pageX', { value: 100 })
window.dispatchEvent(mouseup)
await nextTick()
expect(panels[0].attributes('style')).toContain('flex-basis: 100px;')
})
})

View File

@@ -1,5 +1,6 @@
import { computed, ref } from 'vue'
import { computed, ref, watch } from 'vue'
import { getPct, getPx, isPct, isPx } from './useSize'
import { NOOP } from '@element-plus/utils'
import type { ComputedRef, Ref } from 'vue'
import type { PanelItemState } from '../type'
@@ -7,7 +8,8 @@ import type { PanelItemState } from '../type'
export function useResize(
panels: Ref<PanelItemState[]>,
containerSize: ComputedRef<number>,
pxSizes: ComputedRef<number[]>
pxSizes: ComputedRef<number[]>,
lazy: Ref<boolean>
) {
const ptg2px = (ptg: number) => ptg * containerSize.value || 0
@@ -23,18 +25,28 @@ export function useResize(
return str ?? defaultLimit
}
const lazyOffset = ref(0)
const movingIndex = ref<{
index: number
confirmed: boolean
} | null>(null)
let cachePxSizes: number[] = []
let updatePanelSizes = NOOP
const limitSizes = computed(() =>
panels.value.map((item) => [item.min, item.max])
)
watch(lazy, () => {
if (lazyOffset.value) {
const mouseup = new MouseEvent('mouseup', { bubbles: true })
window.dispatchEvent(mouseup)
}
})
const onMoveStart = (index: number) => {
lazyOffset.value = 0
movingIndex.value = { index, confirmed: false }
cachePxSizes = pxSizes.value
}
@@ -91,13 +103,26 @@ export function useResize(
numSizes[mergedIndex]! += mergedOffset
numSizes[nextIndex]! -= mergedOffset
lazyOffset.value = mergedOffset
panels.value.forEach((panel, index) => {
panel.size = numSizes[index]
})
updatePanelSizes = () => {
panels.value.forEach((panel, index) => {
panel.size = numSizes[index]
})
updatePanelSizes = NOOP
}
if (!lazy.value) {
updatePanelSizes()
}
}
const onMoveEnd = () => {
if (lazy.value) {
updatePanelSizes()
}
lazyOffset.value = 0
movingIndex.value = null
cachePxSizes = []
}
@@ -135,5 +160,12 @@ export function useResize(
})
}
return { onMoveStart, onMoving, onMoveEnd, movingIndex, onCollapse }
return {
lazyOffset,
onMoveStart,
onMoving,
onMoveEnd,
movingIndex,
onCollapse,
}
}

View File

@@ -28,12 +28,9 @@ const props = defineProps({
type: Boolean,
default: true,
},
startCollapsible: {
type: Boolean,
},
endCollapsible: {
type: Boolean,
},
lazy: Boolean,
startCollapsible: Boolean,
endCollapsible: Boolean,
})
const emit = defineEmits(['moveStart', 'moving', 'moveEnd', 'collapse'])
@@ -143,6 +140,7 @@ const EndIcon = computed(() => (isHorizontal.value ? ArrowRight : ArrowDown))
ns.e('dragger'),
draggerPseudoClass,
resizable ? '' : ns.e('disable'),
ns.is('lazy', resizable && lazy),
]"
:style="draggerStyles"
@mousedown="onMousedown"

View File

@@ -37,7 +37,7 @@ if (!splitterContext)
'usage: <el-splitter><el-splitter-panel /></el-splitter/>'
)
const { panels, layout, containerSize, pxSizes } = toRefs(splitterContext)
const { panels, layout, lazy, containerSize, pxSizes } = toRefs(splitterContext)
const {
registerPanel,
@@ -179,6 +179,7 @@ onBeforeUnmount(() => unregisterPanel(_panel))
v-if="isShowBar"
:index="index"
:layout="layout"
:lazy="lazy"
:resizable="isResizable"
:start-collapsible="startCollapsible"
:end-collapsible="endCollapsible"

View File

@@ -9,6 +9,7 @@ export const splitterProps = buildProps({
default: 'horizontal',
values: ['horizontal', 'vertical'] as const,
},
lazy: Boolean,
} as const)
export type SplitterProps = ExtractPropTypes<typeof splitterProps>

View File

@@ -1,5 +1,12 @@
<script setup lang="ts">
import { getCurrentInstance, provide, reactive, toRef, watch } from 'vue'
import {
computed,
getCurrentInstance,
provide,
reactive,
toRef,
watch,
} from 'vue'
import { useNamespace, useOrderedChildren } from '@element-plus/hooks'
import { useContainer, useResize, useSize } from './hooks'
import { splitterProps } from './splitter'
@@ -20,6 +27,7 @@ const emits = defineEmits<{
const props = defineProps(splitterProps)
const layout = toRef(props, 'layout')
const lazy = toRef(props, 'lazy')
const { containerEl, containerSize } = useContainer(layout)
@@ -38,11 +46,22 @@ watch(panels, () => {
const { percentSizes, pxSizes } = useSize(panels, containerSize)
const { onMoveStart, onMoving, onMoveEnd, onCollapse, movingIndex } = useResize(
panels,
containerSize,
pxSizes
)
const {
lazyOffset,
movingIndex,
onMoveStart,
onMoving,
onMoveEnd,
onCollapse,
} = useResize(panels, containerSize, pxSizes, lazy)
const splitterStyles = computed(() => {
return {
[`--${ns.b()}-bar-offset`]: lazy.value
? `${lazyOffset.value}px`
: undefined,
}
})
const onResizeStart = (index: number) => {
onMoveStart(index)
@@ -51,7 +70,10 @@ const onResizeStart = (index: number) => {
const onResize = (index: number, offset: number) => {
onMoving(index, offset)
emits('resize', index, pxSizes.value)
if (!lazy.value) {
emits('resize', index, pxSizes.value)
}
}
const onResizeEnd = (index: number) => {
@@ -71,6 +93,7 @@ provide(
percentSizes,
pxSizes,
layout,
lazy,
movingIndex,
containerSize,
onMoveStart: onResizeStart,
@@ -84,7 +107,11 @@ provide(
</script>
<template>
<div ref="containerEl" :class="[ns.b(), ns.e(layout)]">
<div
ref="containerEl"
:class="[ns.b(), ns.e(layout)]"
:style="splitterStyles"
>
<slot />
<panels-sorter />
<!-- Prevent iframe touch events from breaking -->

View File

@@ -17,6 +17,7 @@ export type PanelItemState = UnwrapRef<{
export interface SplitterRootContext {
panels: PanelItemState[]
layout: Layout
lazy: boolean
containerSize: number
movingIndex: { index: number; confirmed: boolean } | null
percentSizes: number[]

View File

@@ -50,7 +50,8 @@
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
&::before {
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
@@ -58,6 +59,15 @@
transform: translate(-50%, -50%);
background-color: getCssVar('border-color-light');
}
&:not(.is-lazy)::after {
display: none;
}
&::after {
opacity: 0.4;
}
&:hover {
&::before {
background-color: getCssVar('color-primary-light-5');
@@ -65,21 +75,32 @@
}
}
@include e(dragger-horizontal) {
&::before {
&::before,
&::after {
height: 100%;
width: 2px;
}
}
@include e(dragger-vertical) {
&::before {
&::before,
&::after {
height: 2px;
width: 100%;
}
}
@include e(dragger-active) {
&::before {
&::before,
&::after {
background-color: getCssVar('color-primary-light-3');
}
&.#{$namespace}-splitter-bar__dragger-horizontal::after {
transform: translate(calc(-50% + getCssVar('splitter-bar-offset')), -50%);
}
&.#{$namespace}-splitter-bar__dragger-vertical::after {
transform: translate(-50%, calc(-50% + getCssVar('splitter-bar-offset')));
}
}
&:hover {
@include e(collapse-icon) {