From 543a8c41d53a2ac1a9a039e95186caffd2d88ae3 Mon Sep 17 00:00:00 2001 From: bendera Date: Thu, 7 Nov 2024 19:45:31 +0100 Subject: [PATCH 1/6] Fix resize logic in fixed pane mode --- .../vscode-split-layout.styles.ts | 18 ++- .../vscode-split-layout.ts | 135 +++++++++++------- 2 files changed, 101 insertions(+), 52 deletions(-) diff --git a/src/vscode-split-layout/vscode-split-layout.styles.ts b/src/vscode-split-layout/vscode-split-layout.styles.ts index 5b187deab..24544dab7 100644 --- a/src/vscode-split-layout/vscode-split-layout.styles.ts +++ b/src/vscode-split-layout/vscode-split-layout.styles.ts @@ -34,8 +34,9 @@ const styles: CSSResultGroup = [ .start { box-sizing: border-box; - flex-shrink: 0; - width: 100%; + flex: 1; + min-height: 0; + min-width: 0; } :host([split='vertical']) .start { @@ -47,7 +48,18 @@ const styles: CSSResultGroup = [ } .end { - flex-shrink: 0; + flex: 1; + min-height: 0; + min-width: 0; + } + + :host([split='vertical']) .start, + :host([split='vertical']) .end { + height: 100%; + } + + :host([split='horizontal']) .start, + :host([split='horizontal']) .end { width: 100%; } diff --git a/src/vscode-split-layout/vscode-split-layout.ts b/src/vscode-split-layout/vscode-split-layout.ts index 9fcaf27e3..bcfbfcf86 100644 --- a/src/vscode-split-layout/vscode-split-layout.ts +++ b/src/vscode-split-layout/vscode-split-layout.ts @@ -16,6 +16,7 @@ const DEFAULT_HANDLE_SIZE = 4; type PositionUnit = 'pixel' | 'percent'; type Orientation = 'horizontal' | 'vertical'; +type FixedPane = 'start' | 'end' | 'none'; export const parseValue = ( raw: string @@ -117,7 +118,37 @@ export class VscodeSplitLayout extends VscElement { * The size of the fixed pane will not change when the component is resized. */ @property({attribute: 'fixed-pane'}) - fixedPane: 'none' | 'start' | 'end' = 'none'; + set fixedPane(newVal: FixedPane) { + this._fixedPane = newVal; + + if (!this._wrapperEl) { + return; + } + + if (newVal === 'none') { + if (this._wrapperObserved) { + this._resizeObserver.unobserve(this._wrapperEl); + this._wrapperObserved = false; + } + } else { + const {width, height} = this._boundRect; + const max = this.split === 'vertical' ? width : height; + + this._fixedPaneSize = + this.fixedPane === 'start' + ? this._handlePosition + : max - this._handlePosition; + + if (!this._wrapperObserved) { + this._resizeObserver.observe(this._wrapperEl); + this._wrapperObserved = true; + } + } + } + get fixedPane(): FixedPane { + return this._fixedPane; + } + private _fixedPane: FixedPane = 'none'; @state() private _handlePosition = 0; @@ -145,6 +176,15 @@ export class VscodeSplitLayout extends VscElement { private _boundRect: DOMRect = new DOMRect(); private _handleOffset = 0; + private _resizeObserver: ResizeObserver; + private _wrapperObserved: boolean = false; + private _fixedPaneSize: number = 0; + + constructor() { + super(); + + this._resizeObserver = new ResizeObserver(this._handleResize); + } /** * Sets the handle position to the value specified in the `initialHandlePosition` property. @@ -178,6 +218,11 @@ export class VscodeSplitLayout extends VscElement { } protected firstUpdated(_changedProperties: PropertyValues): void { + if (this.fixedPane !== 'none') { + this._resizeObserver.observe(this._wrapperEl); + this._wrapperObserved = true; + } + this._boundRect = this._wrapperEl.getBoundingClientRect(); const {value, unit} = this.handlePosition @@ -187,6 +232,21 @@ export class VscodeSplitLayout extends VscElement { this._setPosition(value, unit); } + private _handleResize = (entries: ResizeObserverEntry[]) => { + const rect = entries[0].contentRect; + const {width, height} = rect; + this._boundRect = rect; + const max = this.split === 'vertical' ? width : height; + + if (this.fixedPane === 'start') { + this._handlePosition = this._fixedPaneSize; + } + + if (this.fixedPane === 'end') { + this._handlePosition = max - this._fixedPaneSize; + } + }; + private _handlePositionChanged() { if (this.handlePosition && this._wrapperEl) { this._boundRect = this._wrapperEl.getBoundingClientRect(); @@ -263,21 +323,21 @@ export class VscodeSplitLayout extends VscElement { private _handleMouseMove = (event: MouseEvent) => { const {clientX, clientY} = event; const {left, top, height, width} = this._boundRect; + const vert = this.split === 'vertical'; + const maxPos = vert ? width : height; + const mousePos = vert ? clientX - left : clientY - top; - if (this._split === 'vertical') { - const mouseXLocal = clientX - left; - this._handlePosition = Math.max( - 0, - Math.min(mouseXLocal - this._handleOffset + this.handleSize / 2, width) - ); + this._handlePosition = Math.max( + 0, + Math.min(mousePos - this._handleOffset + this.handleSize / 2, maxPos) + ); + + if (this.fixedPane === 'start') { + this._fixedPaneSize = this._handlePosition; } - if (this._split === 'horizontal') { - const mouseYLocal = clientY - top; - this._handlePosition = Math.max( - 0, - Math.min(mouseYLocal - this._handleOffset + this.handleSize / 2, height) - ); + if (this.fixedPane === 'end') { + this._fixedPaneSize = maxPos - this._handlePosition; } }; @@ -304,49 +364,26 @@ export class VscodeSplitLayout extends VscElement { render(): TemplateResult { const {width, height} = this._boundRect; - const handlePosMax = this.split === 'vertical' ? width : height; + const maxPos = this.split === 'vertical' ? width : height; const handlePosCss = this.fixedPane !== 'none' ? `${this._handlePosition}px` - : `${pxToPercent(this._handlePosition, handlePosMax)}%`; - const startPaneStyles = {height: '100%', width: '100%'}; + : `${pxToPercent(this._handlePosition, maxPos)}%`; - if (this.split === 'vertical') { - if (this.fixedPane === 'none') { - startPaneStyles.width = `${pxToPercent(this._handlePosition, width)}%`; - } else if (this.fixedPane === 'start') { - startPaneStyles.width = `${this._handlePosition}px`; - } else { - startPaneStyles.width = '100%'; - } + let startPaneSize = ''; + + if (this.fixedPane === 'start') { + startPaneSize = `0 0 ${this._fixedPaneSize}px`; } else { - if (this.fixedPane === 'none') { - startPaneStyles.height = `${pxToPercent(this._handlePosition, height)}%`; - } else if (this.fixedPane === 'start') { - startPaneStyles.height = `${this._handlePosition}px`; - } else { - startPaneStyles.height = '100%'; - } + startPaneSize = `1 1 ${pxToPercent(this._handlePosition, maxPos)}%`; } - const endPaneStyles = {height: '100%', width: '100%'}; + let endPaneSize = ''; - if (this.split === 'vertical') { - if (this.fixedPane === 'none') { - endPaneStyles.width = `${pxToPercent(width - this._handlePosition, width)}%`; - } else if (this.fixedPane === 'end') { - endPaneStyles.width = `${width - this._handlePosition}px`; - } else { - endPaneStyles.width = '100%'; - } + if (this.fixedPane === 'end') { + endPaneSize = `0 0 ${this._fixedPaneSize}px`; } else { - if (this.fixedPane === 'none') { - endPaneStyles.height = `${pxToPercent(height - this._handlePosition, height)}%`; - } else if (this.fixedPane === 'end') { - endPaneStyles.height = `${height - this._handlePosition}px`; - } else { - endPaneStyles.height = '100%'; - } + endPaneSize = `1 1 ${pxToPercent(maxPos - this._handlePosition, maxPos)}%`; } const handleStylesPropObj: {[prop: string]: string} = { @@ -390,10 +427,10 @@ export class VscodeSplitLayout extends VscElement { return html`
-
+
-
+
From 8c27ada76c3c259d0abc6343a1e22839b54976db Mon Sep 17 00:00:00 2001 From: bendera Date: Thu, 7 Nov 2024 20:11:55 +0100 Subject: [PATCH 2/6] Refactor --- .../vscode-split-layout.ts | 77 ++++++++++--------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/src/vscode-split-layout/vscode-split-layout.ts b/src/vscode-split-layout/vscode-split-layout.ts index bcfbfcf86..a7c6f36ec 100644 --- a/src/vscode-split-layout/vscode-split-layout.ts +++ b/src/vscode-split-layout/vscode-split-layout.ts @@ -16,7 +16,7 @@ const DEFAULT_HANDLE_SIZE = 4; type PositionUnit = 'pixel' | 'percent'; type Orientation = 'horizontal' | 'vertical'; -type FixedPane = 'start' | 'end' | 'none'; +type FixedPaneType = 'start' | 'end' | 'none'; export const parseValue = ( raw: string @@ -107,7 +107,7 @@ export class VscodeSplitLayout extends VscElement { @property({attribute: 'handle-position'}) set handlePosition(newVal: string) { this._rawHandlePosition = newVal; - this._handlePositionChanged(); + this._handlePositionPropChanged(); } get handlePosition(): string | undefined { return this._rawHandlePosition; @@ -118,37 +118,14 @@ export class VscodeSplitLayout extends VscElement { * The size of the fixed pane will not change when the component is resized. */ @property({attribute: 'fixed-pane'}) - set fixedPane(newVal: FixedPane) { + set fixedPane(newVal: FixedPaneType) { this._fixedPane = newVal; - - if (!this._wrapperEl) { - return; - } - - if (newVal === 'none') { - if (this._wrapperObserved) { - this._resizeObserver.unobserve(this._wrapperEl); - this._wrapperObserved = false; - } - } else { - const {width, height} = this._boundRect; - const max = this.split === 'vertical' ? width : height; - - this._fixedPaneSize = - this.fixedPane === 'start' - ? this._handlePosition - : max - this._handlePosition; - - if (!this._wrapperObserved) { - this._resizeObserver.observe(this._wrapperEl); - this._wrapperObserved = true; - } - } + this._fixedPanePropChanged(newVal); } - get fixedPane(): FixedPane { + get fixedPane(): FixedPaneType { return this._fixedPane; } - private _fixedPane: FixedPane = 'none'; + private _fixedPane: FixedPaneType = 'none'; @state() private _handlePosition = 0; @@ -232,6 +209,40 @@ export class VscodeSplitLayout extends VscElement { this._setPosition(value, unit); } + private _handlePositionPropChanged() { + if (this.handlePosition && this._wrapperEl) { + this._boundRect = this._wrapperEl.getBoundingClientRect(); + const {value, unit} = parseValue(this.handlePosition); + this._setPosition(value, unit); + } + } + + private _fixedPanePropChanged(newVal: FixedPaneType) { + if (!this._wrapperEl) { + return; + } + + if (newVal === 'none') { + if (this._wrapperObserved) { + this._resizeObserver.unobserve(this._wrapperEl); + this._wrapperObserved = false; + } + } else { + const {width, height} = this._boundRect; + const max = this.split === 'vertical' ? width : height; + + this._fixedPaneSize = + this.fixedPane === 'start' + ? this._handlePosition + : max - this._handlePosition; + + if (!this._wrapperObserved) { + this._resizeObserver.observe(this._wrapperEl); + this._wrapperObserved = true; + } + } + } + private _handleResize = (entries: ResizeObserverEntry[]) => { const rect = entries[0].contentRect; const {width, height} = rect; @@ -247,14 +258,6 @@ export class VscodeSplitLayout extends VscElement { } }; - private _handlePositionChanged() { - if (this.handlePosition && this._wrapperEl) { - this._boundRect = this._wrapperEl.getBoundingClientRect(); - const {value, unit} = parseValue(this.handlePosition); - this._setPosition(value, unit); - } - } - private _setPosition(value: number, unit: PositionUnit) { const {width, height} = this._boundRect; const max = this.split === 'vertical' ? width : height; From d37f7bd27fe19bcacdd0b237d806001589163a79 Mon Sep 17 00:00:00 2001 From: bendera Date: Thu, 7 Nov 2024 21:14:55 +0100 Subject: [PATCH 3/6] Update tests --- .../vscode-split-layout.test.ts | 79 ++++++++++++++----- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/src/vscode-split-layout/vscode-split-layout.test.ts b/src/vscode-split-layout/vscode-split-layout.test.ts index 7a08d2b1e..4adf4afc2 100644 --- a/src/vscode-split-layout/vscode-split-layout.test.ts +++ b/src/vscode-split-layout/vscode-split-layout.test.ts @@ -108,13 +108,40 @@ describe('vscode-split-layout', () => { expect(handle.style.left).to.eq('0px'); expect(handle.style.top).to.eq('50%'); }); + + it('should set handle size in vertical split mode', async () => { + const el = await fixture( + html`` + ); + const handle = el.shadowRoot?.querySelector('.handle') as HTMLDivElement; + + expect(handle.offsetWidth).to.eq(20); + expect(handle.offsetLeft).to.eq(240); + }); + + it('should set handle size in horizontal split mode', async () => { + const el = await fixture( + html`` + ); + const handle = el.shadowRoot?.querySelector('.handle') as HTMLDivElement; + + expect(handle.offsetHeight).to.eq(20); + expect(handle.offsetTop).to.eq(240); + }); }); describe('user interactions', () => { it('should panes resize in vertical mode', async () => { const el = await fixture( html`` ); @@ -125,18 +152,17 @@ describe('vscode-split-layout', () => { ) as HTMLDivElement; const paneEnd = el.shadowRoot?.querySelector('.end') as HTMLDivElement; - expect(handle).not.to.be.null; - expect(handle.style.left).to.eq('20%'); - expect(handle.style.top).to.eq('0px'); - expect(paneStart.style.width).to.eq('20%'); - expect(paneEnd.style.width).to.eq('80%'); + expect(paneStart.offsetWidth, 'start pane width before resizing').to.eq( + 100 + ); + expect(paneEnd.offsetWidth, 'end pane width before resizing').to.eq(400); await dragElement(handle, 100); - expect(paneStart.style.width).to.eq('40%'); - expect(paneEnd.style.width).to.eq('60%'); - expect(handle.style.left).to.eq('40%'); - expect(handle.style.top).to.eq('0px'); + expect(paneStart.offsetWidth, 'start pane width after resizing').to.eq( + 200 + ); + expect(paneEnd.offsetWidth, 'end pane width after resizing').to.eq(300); }); it('should panes resize in horizontal mode', async () => { @@ -149,15 +175,24 @@ describe('vscode-split-layout', () => { ); const handle = el.shadowRoot?.querySelector('.handle') as HTMLDivElement; + const paneStart = el.shadowRoot?.querySelector( + '.start' + ) as HTMLDivElement; + const paneEnd = el.shadowRoot?.querySelector('.end') as HTMLDivElement; - expect(handle).not.to.be.null; - expect(handle.style.left).to.eq('0px'); - expect(handle.style.top).to.eq('20%'); + expect(paneStart.offsetHeight, 'start pane height before resizing').to.eq( + 100 + ); + expect(paneEnd.offsetHeight, 'end pane height before resizing').to.eq( + 400 + ); await dragElement(handle, 0, 100); - expect(handle.style.left).to.eq('0px'); - expect(handle.style.top).to.eq('40%'); + expect(paneStart.offsetHeight, 'start pane height after resizing').to.eq( + 200 + ); + expect(paneEnd.offsetHeight, 'end pane height after resizing').to.eq(300); }); it('should dispatch "vsc-split-layout-change" event when handle position is changed', async () => { @@ -207,17 +242,21 @@ describe('vscode-split-layout', () => { expect(revertedHandlePos).to.eq('20%'); }); - it('should not reset handle position on double click when "reset-on-dbl-click" is unset'); + it( + 'should not reset handle position on double click when "reset-on-dbl-click" is unset' + ); }); // TODO - it('should set vertical handle size'); - it('should set horizontal handle size'); it('should change divider orientation from vertical to horizontal'); it('should change divider orientation from horizontal to vertical'); it('should set fixed start panel'); it('should set fixed end panel'); - it('should set handle position programmatically when divider orientation is vertical'); - it('should set handle position programmatically when divider orientation is horizontal'); + it( + 'should set handle position programmatically when divider orientation is vertical' + ); + it( + 'should set handle position programmatically when divider orientation is horizontal' + ); it('should reset to the default position programmatically'); }); From f58b224ba8d64b9099d7643617d0d25f4574b063 Mon Sep 17 00:00:00 2001 From: bendera Date: Thu, 7 Nov 2024 22:55:33 +0100 Subject: [PATCH 4/6] Adjust handle css --- src/vscode-split-layout/vscode-split-layout.styles.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vscode-split-layout/vscode-split-layout.styles.ts b/src/vscode-split-layout/vscode-split-layout.styles.ts index 24544dab7..8c31ae40d 100644 --- a/src/vscode-split-layout/vscode-split-layout.styles.ts +++ b/src/vscode-split-layout/vscode-split-layout.styles.ts @@ -86,18 +86,19 @@ const styles: CSSResultGroup = [ } .handle { + background-color: transparent; position: absolute; z-index: 2; } .handle.hover { + transition: background-color 0.1s ease-out 0.3s; background-color: var(--vscode-sash-hoverBorder); - transition: background-color 100ms linear 300ms; } .handle.hide { background-color: transparent; - transition: background-color 100ms linear; + transition: background-color 0.1s ease-out; } .handle.split-vertical { From 931725003e7887533c354b9e8d1649d4bd5f8c32 Mon Sep 17 00:00:00 2001 From: bendera Date: Thu, 7 Nov 2024 23:25:49 +0100 Subject: [PATCH 5/6] Add tests --- .../vscode-split-layout.test.ts | 126 +++++++++++++++--- 1 file changed, 105 insertions(+), 21 deletions(-) diff --git a/src/vscode-split-layout/vscode-split-layout.test.ts b/src/vscode-split-layout/vscode-split-layout.test.ts index 4adf4afc2..17013b237 100644 --- a/src/vscode-split-layout/vscode-split-layout.test.ts +++ b/src/vscode-split-layout/vscode-split-layout.test.ts @@ -15,6 +15,18 @@ describe('vscode-split-layout', () => { expect(el).to.instanceOf(VscodeSplitLayout); }); + it('default values', async () => { + const el = await fixture( + html`` + ); + + expect(el.split).to.eq('vertical'); + expect(el.resetOnDblClick).to.be.false; + expect(el.handleSize).to.eq(4); + expect(el.initialHandlePosition).to.eq('50%'); + expect(el.fixedPane).to.eq('none'); + }); + describe('helper functions', () => { it('should parse percentage value correctly', () => { expect(parseValue('50%')).to.deep.equal({unit: 'percent', value: 50}); @@ -75,38 +87,62 @@ describe('vscode-split-layout', () => { }); describe('when provided parameters', () => { - it('should adjust the handle vertical', async () => { + it('should handle position set correctly when the divider orientation is vertical, and the position is specified in pixels', async () => { const el = await fixture( html`` + ); + + const handle = el.shadowRoot!.querySelector('.handle') as HTMLDivElement; + + expect(handle.offsetLeft).to.eq(98); + expect(handle.offsetTop).to.eq(0); + }); + + it('should handle position set correctly when the divider orientation is vertical, and the position is specified in percent', async () => { + const el = await fixture( + html`` + ); + + const handle = el.shadowRoot!.querySelector('.handle') as HTMLDivElement; + + expect(handle.offsetLeft).to.eq(98); + expect(handle.offsetTop).to.eq(0); + }); + + it('should handle position set correctly when the divider orientation is horizontal, and the position is specified in pixels', async () => { + const el = await fixture( + html`` ); const handle = el.shadowRoot!.querySelector('.handle') as HTMLDivElement; - expect(handle.style.left).to.eq('50%'); - expect(handle.style.top).to.eq('0px'); + expect(handle.offsetLeft).to.eq(0); + expect(handle.offsetTop).to.eq(98); }); - it('should adjust the handle horizontal', async () => { + it('should handle position set correctly when the divider orientation is horizontal, and the position is specified in percent', async () => { const el = await fixture( html`` ); const handle = el.shadowRoot!.querySelector('.handle') as HTMLDivElement; - expect(handle.style.left).to.eq('0px'); - expect(handle.style.top).to.eq('50%'); + expect(handle.offsetLeft).to.eq(0); + expect(handle.offsetTop).to.eq(98); }); it('should set handle size in vertical split mode', async () => { @@ -135,6 +171,61 @@ describe('vscode-split-layout', () => { expect(handle.offsetHeight).to.eq(20); expect(handle.offsetTop).to.eq(240); }); + + it('should change divider orientation from vertical to horizontal correctly', async () => { + const el = await fixture( + html`` + ); + + const startWidthBefore = + el.shadowRoot?.querySelector('.start')!.offsetWidth; + const endWidthBefore = + el.shadowRoot?.querySelector('.end')!.offsetWidth; + + el.split = 'horizontal'; + await el.updateComplete; + + const startHeightAfter = + el.shadowRoot?.querySelector('.start')!.offsetHeight; + const endHeightAfter = + el.shadowRoot?.querySelector('.end')!.offsetHeight; + + expect(startWidthBefore).to.eq(100); + expect(endWidthBefore).to.eq(400); + expect(startHeightAfter).to.eq(100); + expect(endHeightAfter).to.eq(400); + }); + + it('should change divider orientation from horizontal to vertical correctly', async () => { + const el = await fixture( + html`` + ); + + const startHeightBefore = + el.shadowRoot?.querySelector('.start')!.offsetHeight; + const endHeightBefore = + el.shadowRoot?.querySelector('.end')!.offsetHeight; + + el.split = 'vertical'; + await el.updateComplete; + + const startWidthAfter = + el.shadowRoot?.querySelector('.start')!.offsetWidth; + const endWidthAfter = + el.shadowRoot?.querySelector('.end')!.offsetWidth; + + expect(startHeightBefore).to.eq(100); + expect(endHeightBefore).to.eq(400); + expect(startWidthAfter).to.eq(100); + expect(endWidthAfter).to.eq(400); + }); }); describe('user interactions', () => { @@ -248,15 +339,8 @@ describe('vscode-split-layout', () => { }); // TODO - it('should change divider orientation from vertical to horizontal'); - it('should change divider orientation from horizontal to vertical'); it('should set fixed start panel'); it('should set fixed end panel'); - it( - 'should set handle position programmatically when divider orientation is vertical' - ); - it( - 'should set handle position programmatically when divider orientation is horizontal' - ); it('should reset to the default position programmatically'); + it('should nested instances reset when slotted content is changed'); }); From 171fb7c0dd5e17bcf438a104e565b13e326d3fa5 Mon Sep 17 00:00:00 2001 From: bendera Date: Fri, 8 Nov 2024 00:03:53 +0100 Subject: [PATCH 6/6] Update changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32517aaa4..c47abb8ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] +### Added + +- **SplitLayout**: Added `handlePosition` property to adjust the handle position programmatically. +- **SplitLayout**: Added `fixedPane` property. When the parent element is resized, the panes adjust proportionally. + This parameter allows you to set one of the panes to a fixed size so its dimensions won’t change during resizing. +- **SplitLayout**: Added `resetHandlePosition()` method to reset the handle position to the default value. +- **SplitLayout**: Dispatch `vsc-split-layout-change` event when a panel is resized. + ### Fixed - **SingleSelect**, **MultiSelect**: Fix the widget height when it is empty.