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. diff --git a/src/vscode-split-layout/vscode-split-layout.styles.ts b/src/vscode-split-layout/vscode-split-layout.styles.ts index 5b187deab..8c31ae40d 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%; } @@ -74,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 { diff --git a/src/vscode-split-layout/vscode-split-layout.test.ts b/src/vscode-split-layout/vscode-split-layout.test.ts index 7a08d2b1e..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,144 @@ 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.style.left).to.eq('50%'); - expect(handle.style.top).to.eq('0px'); + expect(handle.offsetLeft).to.eq(98); + expect(handle.offsetTop).to.eq(0); }); - 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 pixels', 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 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.offsetLeft).to.eq(0); + expect(handle.offsetTop).to.eq(98); + }); + + 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); + }); + + 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); }); }); @@ -114,7 +232,7 @@ describe('vscode-split-layout', () => { it('should panes resize in vertical mode', async () => { const el = await fixture( html`` ); @@ -125,18 +243,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 +266,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 +333,14 @@ 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 reset to the default position programmatically'); + it('should nested instances reset when slotted content is changed'); }); diff --git a/src/vscode-split-layout/vscode-split-layout.ts b/src/vscode-split-layout/vscode-split-layout.ts index 9fcaf27e3..a7c6f36ec 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 FixedPaneType = 'start' | 'end' | 'none'; export const parseValue = ( raw: string @@ -106,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; @@ -117,7 +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'}) - fixedPane: 'none' | 'start' | 'end' = 'none'; + set fixedPane(newVal: FixedPaneType) { + this._fixedPane = newVal; + this._fixedPanePropChanged(newVal); + } + get fixedPane(): FixedPaneType { + return this._fixedPane; + } + private _fixedPane: FixedPaneType = 'none'; @state() private _handlePosition = 0; @@ -145,6 +153,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 +195,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,7 +209,7 @@ export class VscodeSplitLayout extends VscElement { this._setPosition(value, unit); } - private _handlePositionChanged() { + private _handlePositionPropChanged() { if (this.handlePosition && this._wrapperEl) { this._boundRect = this._wrapperEl.getBoundingClientRect(); const {value, unit} = parseValue(this.handlePosition); @@ -195,6 +217,47 @@ export class VscodeSplitLayout extends VscElement { } } + 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; + 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 _setPosition(value: number, unit: PositionUnit) { const {width, height} = this._boundRect; const max = this.split === 'vertical' ? width : height; @@ -263,21 +326,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 +367,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 +430,10 @@ export class VscodeSplitLayout extends VscElement { return html`
-
+
-
+