From 30b6a1cd0f58d1bf467f1cd587e9bd9dcafecb5d Mon Sep 17 00:00:00 2001 From: MonikaKirkova Date: Tue, 4 Nov 2025 10:53:49 +0200 Subject: [PATCH 01/24] feat(splitter): initial structure implementation --- .../common/definitions/defineAllComponents.ts | 6 +++ src/components/splitter/splitter-bar.ts | 32 ++++++++++++ src/components/splitter/splitter-pane.ts | 30 +++++++++++ src/components/splitter/splitter.ts | 52 +++++++++++++++++++ src/components/types.ts | 1 + src/index.ts | 3 ++ stories/splitter.stories.ts | 47 +++++++++++++++++ 7 files changed, 171 insertions(+) create mode 100644 src/components/splitter/splitter-bar.ts create mode 100644 src/components/splitter/splitter-pane.ts create mode 100644 src/components/splitter/splitter.ts create mode 100644 stories/splitter.stories.ts diff --git a/src/components/common/definitions/defineAllComponents.ts b/src/components/common/definitions/defineAllComponents.ts index 4d3e03119..cbf01883c 100644 --- a/src/components/common/definitions/defineAllComponents.ts +++ b/src/components/common/definitions/defineAllComponents.ts @@ -56,6 +56,9 @@ import IgcRangeSliderComponent from '../../slider/range-slider.js'; import IgcSliderComponent from '../../slider/slider.js'; import IgcSliderLabelComponent from '../../slider/slider-label.js'; import IgcSnackbarComponent from '../../snackbar/snackbar.js'; +import IgcSplitterComponent from '../../splitter/splitter.js'; +import IgcSplitterBarComponent from '../../splitter/splitter-bar.js'; +import IgcSplitterPaneComponent from '../../splitter/splitter-pane.js'; import IgcStepComponent from '../../stepper/step.js'; import IgcStepperComponent from '../../stepper/stepper.js'; import IgcTabComponent from '../../tabs/tab.js'; @@ -134,6 +137,9 @@ const allComponents: IgniteComponent[] = [ IgcCircularGradientComponent, IgcSnackbarComponent, IgcDateTimeInputComponent, + IgcSplitterBarComponent, + IgcSplitterComponent, + IgcSplitterPaneComponent, IgcStepperComponent, IgcStepComponent, IgcTextareaComponent, diff --git a/src/components/splitter/splitter-bar.ts b/src/components/splitter/splitter-bar.ts new file mode 100644 index 000000000..79c63908d --- /dev/null +++ b/src/components/splitter/splitter-bar.ts @@ -0,0 +1,32 @@ +import { html, LitElement } from 'lit'; +import { registerComponent } from '../common/definitions/register.js'; + +export default class IgcSplitterBarComponent extends LitElement { + public static readonly tagName = 'igc-splitter-bar'; + + /* blazorSuppress */ + public static register() { + registerComponent(IgcSplitterBarComponent); + } + + // constructor() { + // super(); + // //addThemingController(this, all); + // } + + protected override render() { + return html` +
+
+
+
+
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'igc-splitter-bar': IgcSplitterBarComponent; + } +} diff --git a/src/components/splitter/splitter-pane.ts b/src/components/splitter/splitter-pane.ts new file mode 100644 index 000000000..c36b0e9fe --- /dev/null +++ b/src/components/splitter/splitter-pane.ts @@ -0,0 +1,30 @@ +import { html, LitElement } from 'lit'; +import { registerComponent } from '../common/definitions/register.js'; + +export default class IgcSplitterPaneComponent extends LitElement { + public static readonly tagName = 'igc-splitter-pane'; + + /* blazorSuppress */ + public static register() { + registerComponent(IgcSplitterPaneComponent); + } + + // constructor() { + // super(); + // //addThemingController(this, all); + // } + + protected override render() { + return html` +
+ +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'igc-splitter-pane': IgcSplitterPaneComponent; + } +} diff --git a/src/components/splitter/splitter.ts b/src/components/splitter/splitter.ts new file mode 100644 index 000000000..44a44bd10 --- /dev/null +++ b/src/components/splitter/splitter.ts @@ -0,0 +1,52 @@ +import { html, LitElement } from 'lit'; +import { property, queryAssignedElements } from 'lit/decorators.js'; +import { registerComponent } from '../common/definitions/register.js'; +import type { SplitterOrientation } from '../types.js'; +import IgcSplitterPaneComponent from './splitter-pane.js'; + +export default class IgcSplitterComponent extends LitElement { + public static readonly tagName = 'igc-splitter'; + + /* blazorSuppress */ + public static register() { + registerComponent(IgcSplitterComponent, IgcSplitterPaneComponent); + } + + /** Returns all of the splitter's panes. */ + @queryAssignedElements({ selector: 'igc-splitter-pane' }) + public panes!: Array; + + /** Gets/Sets the orientation of the stepper. + * + * @remarks + * Default value is `horizontal`. + */ + @property({ reflect: true }) + public orientation: SplitterOrientation = 'horizontal'; + + // constructor() { + // super(); + // } + + private _onSlotChange = () => { + // panes updates after slot distribution; trigger re-render + this.requestUpdate(); + }; + + private _renderBar() { + return html` `; + } + + protected override render() { + return html` + + ${this.panes.slice(0, -1).map(() => html` ${this._renderBar()} `)} + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'igc-splitter': IgcSplitterComponent; + } +} diff --git a/src/components/types.ts b/src/components/types.ts index ab829fcdd..8f213462c 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -47,6 +47,7 @@ export type MaskInputValueMode = 'raw' | 'withFormatting'; export type NavDrawerPosition = 'start' | 'end' | 'top' | 'bottom' | 'relative'; export type SliderTickLabelRotation = 0 | 90 | -90; export type SliderTickOrientation = 'end' | 'mirror' | 'start'; +export type SplitterOrientation = 'horizontal' | 'vertical'; export type StepperOrientation = 'horizontal' | 'vertical'; export type StepperStepType = 'full' | 'indicator' | 'title'; export type StepperTitlePosition = 'auto' | 'bottom' | 'top' | 'end' | 'start'; diff --git a/src/index.ts b/src/index.ts index 324c4660f..9a191cccf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,6 +66,9 @@ export { default as IgcSwitchComponent } from './components/checkbox/switch.js'; export { default as IgcTextareaComponent } from './components/textarea/textarea.js'; export { default as IgcTreeComponent } from './components/tree/tree.js'; export { default as IgcTreeItemComponent } from './components/tree/tree-item.js'; +export { default as IgcSplitterBarComponent } from './components/splitter/splitter-bar.js'; +export { default as IgcSpltterComponent } from './components/splitter/splitter.js'; +export { default as IgcSplitterPaneComponent } from './components/splitter/splitter-pane.js'; export { default as IgcStepperComponent } from './components/stepper/stepper.js'; export { default as IgcStepComponent } from './components/stepper/step.js'; export { default as IgcTooltipComponent } from './components/tooltip/tooltip.js'; diff --git a/stories/splitter.stories.ts b/stories/splitter.stories.ts new file mode 100644 index 000000000..f018e37ed --- /dev/null +++ b/stories/splitter.stories.ts @@ -0,0 +1,47 @@ +import type { Meta, StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; + +import { + IgcSplitterPaneComponent, + defineComponents, +} from 'igniteui-webcomponents'; +import IgcSplitterComponent from '../src/components/splitter/splitter.js'; + +defineComponents(IgcSplitterComponent, IgcSplitterPaneComponent); + +const metadata: Meta = { + title: 'Splitter', + component: 'igc-splitter', + parameters: { + docs: { + description: { + component: + 'The Splitter lays out panes with draggable bars rendered between each pair of panes.', + }, + }, + }, + argTypes: { + orientation: { + options: ['horizontal', 'vertical'], + control: { type: 'inline-radio' }, + description: 'Orientation of the splitter.', + table: { defaultValue: { summary: 'horizontal' } }, + }, + }, + args: { + orientation: 'horizontal', + }, +}; + +export default metadata; +type Story = StoryObj; + +export const Default: Story = { + render: () => html` + + Pane 1 + Pane 2 + Pane 3 + + `, +}; From 857ca99c3b4ae2825c5b654f6b8ae07c188d71f5 Mon Sep 17 00:00:00 2001 From: Bozhidara Pachilova Date: Tue, 4 Nov 2025 12:49:27 +0200 Subject: [PATCH 02/24] feat(splitter): add initial poc styles --- src/components/splitter/splitter-bar.ts | 22 ++++++++- src/components/splitter/splitter-pane.ts | 26 ++++++++-- src/components/splitter/splitter.ts | 44 ++++++++++++----- .../splitter/themes/splitter.base.scss | 47 +++++++++++++++++++ stories/splitter.stories.ts | 21 +++++++-- 5 files changed, 136 insertions(+), 24 deletions(-) create mode 100644 src/components/splitter/themes/splitter.base.scss diff --git a/src/components/splitter/splitter-bar.ts b/src/components/splitter/splitter-bar.ts index 79c63908d..f1df2544a 100644 --- a/src/components/splitter/splitter-bar.ts +++ b/src/components/splitter/splitter-bar.ts @@ -1,14 +1,34 @@ import { html, LitElement } from 'lit'; +import { property } from 'lit/decorators.js'; import { registerComponent } from '../common/definitions/register.js'; +import { asNumber } from '../common/util.js'; +import { styles } from './themes/splitter.base.css.js'; export default class IgcSplitterBarComponent extends LitElement { public static readonly tagName = 'igc-splitter-bar'; + public static override styles = [styles]; /* blazorSuppress */ public static register() { registerComponent(IgcSplitterBarComponent); } + private _order = -1; + + /** + * Gets/sets the bar's visual position in the layout. + * @hidden @internal + */ + @property({ type: Number }) + public set order(value: number) { + this._order = asNumber(value); + this.style.order = this._order.toString(); + } + + public get order(): number { + return this._order; + } + // constructor() { // super(); // //addThemingController(this, all); @@ -16,7 +36,7 @@ export default class IgcSplitterBarComponent extends LitElement { protected override render() { return html` -
+
diff --git a/src/components/splitter/splitter-pane.ts b/src/components/splitter/splitter-pane.ts index c36b0e9fe..a804e16d3 100644 --- a/src/components/splitter/splitter-pane.ts +++ b/src/components/splitter/splitter-pane.ts @@ -1,25 +1,41 @@ import { html, LitElement } from 'lit'; +import { property } from 'lit/decorators.js'; import { registerComponent } from '../common/definitions/register.js'; +import { asNumber } from '../common/util.js'; +import { styles } from './themes/splitter.base.css.js'; export default class IgcSplitterPaneComponent extends LitElement { public static readonly tagName = 'igc-splitter-pane'; + public static override styles = [styles]; /* blazorSuppress */ public static register() { registerComponent(IgcSplitterPaneComponent); } + private _order = -1; + + /** + * Gets/sets the pane's visual position in the layout. + * @hidden @internal + */ + @property({ type: Number }) + public set order(value: number) { + this._order = asNumber(value); + this.style.order = this._order.toString(); + } + + public get order(): number { + return this._order; + } + // constructor() { // super(); // //addThemingController(this, all); // } protected override render() { - return html` -
- -
- `; + return html` `; } } diff --git a/src/components/splitter/splitter.ts b/src/components/splitter/splitter.ts index 44a44bd10..8e777317a 100644 --- a/src/components/splitter/splitter.ts +++ b/src/components/splitter/splitter.ts @@ -1,22 +1,34 @@ import { html, LitElement } from 'lit'; import { property, queryAssignedElements } from 'lit/decorators.js'; +import { addSlotController } from '../common/controllers/slot.js'; import { registerComponent } from '../common/definitions/register.js'; import type { SplitterOrientation } from '../types.js'; +import IgcSplitterBarComponent from './splitter-bar.js'; import IgcSplitterPaneComponent from './splitter-pane.js'; +import { styles } from './themes/splitter.base.css.js'; export default class IgcSplitterComponent extends LitElement { public static readonly tagName = 'igc-splitter'; + public static override styles = [styles]; /* blazorSuppress */ public static register() { - registerComponent(IgcSplitterComponent, IgcSplitterPaneComponent); + registerComponent( + IgcSplitterComponent, + IgcSplitterPaneComponent, + IgcSplitterBarComponent + ); } + private readonly _slots = addSlotController(this, { + onChange: this._handleSlotChange, + }); + /** Returns all of the splitter's panes. */ @queryAssignedElements({ selector: 'igc-splitter-pane' }) public panes!: Array; - /** Gets/Sets the orientation of the stepper. + /** Gets/Sets the orientation of the splitter. * * @remarks * Default value is `horizontal`. @@ -24,23 +36,29 @@ export default class IgcSplitterComponent extends LitElement { @property({ reflect: true }) public orientation: SplitterOrientation = 'horizontal'; - // constructor() { - // super(); - // } + protected _handleSlotChange(): void { + this._assignFlexOrder(); + } - private _onSlotChange = () => { - // panes updates after slot distribution; trigger re-render - this.requestUpdate(); - }; + private _assignFlexOrder() { + let k = 0; + this.panes.forEach((pane) => { + pane.order = k; + k += 2; + }); + } - private _renderBar() { - return html` `; + private _renderBar(order: number) { + return html` `; } protected override render() { return html` - - ${this.panes.slice(0, -1).map(() => html` ${this._renderBar()} `)} + + ${this.panes.map((pane, i) => { + const isLast = i === this.panes.length - 1; + return html`${!isLast ? this._renderBar(pane.order + 1) : ''}`; + })} `; } } diff --git a/src/components/splitter/themes/splitter.base.scss b/src/components/splitter/themes/splitter.base.scss new file mode 100644 index 000000000..b037457e7 --- /dev/null +++ b/src/components/splitter/themes/splitter.base.scss @@ -0,0 +1,47 @@ +@use 'styles/common/component'; +@use 'styles/utilities' as *; + +:host { + display: flex; + width: 100%; + height: 100%; + color: var(--ig-gray-900); + background: var(--ig-gray-100); + border: 1px solid var(--ig-gray-200); +} + +:host([orientation='horizontal']) { + flex-direction: row; + + igc-splitter-bar { + width: 5px; + height: auto; + } + + igc-splitter-pane { + height: 100%; + } +} + +:host([orientation='vertical']) { + flex-direction: column; + + igc-splitter-bar { + height: 5px; + width: auto; + } + + igc-splitter-pane { + width: 100%; + } +} + +igc-splitter-bar { + background-color: var(--ig-gray-200); +} + +igc-splitter-pane { + background: var(--ig-gray-900-contrast); + border: 1px solid var(--ig-gray-300); + flex: 1 1 0; +} diff --git a/stories/splitter.stories.ts b/stories/splitter.stories.ts index f018e37ed..53e437c2c 100644 --- a/stories/splitter.stories.ts +++ b/stories/splitter.stories.ts @@ -37,11 +37,22 @@ export default metadata; type Story = StoryObj; export const Default: Story = { - render: () => html` - - Pane 1 - Pane 2 - Pane 3 + render: ({ orientation }) => html` + + + +
Pane 1
+
+ +
Pane 2
+
+ +
Pane 3
+
`, }; From f62528e374ff497c5cab1dbfeefa8c56f2ab9f74 Mon Sep 17 00:00:00 2001 From: Bozhidara Pachilova Date: Tue, 4 Nov 2025 17:15:23 +0200 Subject: [PATCH 03/24] chore: fix initial styles, add spec file --- src/components/splitter/splitter-bar.ts | 2 - src/components/splitter/splitter-pane.ts | 2 - src/components/splitter/splitter.spec.ts | 80 +++++++++++++++++++ .../splitter/themes/splitter.base.scss | 22 +++-- 4 files changed, 89 insertions(+), 17 deletions(-) create mode 100644 src/components/splitter/splitter.spec.ts diff --git a/src/components/splitter/splitter-bar.ts b/src/components/splitter/splitter-bar.ts index f1df2544a..9aa22d1d1 100644 --- a/src/components/splitter/splitter-bar.ts +++ b/src/components/splitter/splitter-bar.ts @@ -2,11 +2,9 @@ import { html, LitElement } from 'lit'; import { property } from 'lit/decorators.js'; import { registerComponent } from '../common/definitions/register.js'; import { asNumber } from '../common/util.js'; -import { styles } from './themes/splitter.base.css.js'; export default class IgcSplitterBarComponent extends LitElement { public static readonly tagName = 'igc-splitter-bar'; - public static override styles = [styles]; /* blazorSuppress */ public static register() { diff --git a/src/components/splitter/splitter-pane.ts b/src/components/splitter/splitter-pane.ts index a804e16d3..9450496f6 100644 --- a/src/components/splitter/splitter-pane.ts +++ b/src/components/splitter/splitter-pane.ts @@ -2,11 +2,9 @@ import { html, LitElement } from 'lit'; import { property } from 'lit/decorators.js'; import { registerComponent } from '../common/definitions/register.js'; import { asNumber } from '../common/util.js'; -import { styles } from './themes/splitter.base.css.js'; export default class IgcSplitterPaneComponent extends LitElement { public static readonly tagName = 'igc-splitter-pane'; - public static override styles = [styles]; /* blazorSuppress */ public static register() { diff --git a/src/components/splitter/splitter.spec.ts b/src/components/splitter/splitter.spec.ts new file mode 100644 index 000000000..076a47c95 --- /dev/null +++ b/src/components/splitter/splitter.spec.ts @@ -0,0 +1,80 @@ +import { elementUpdated, expect, fixture, html } from '@open-wc/testing'; + +import { defineComponents } from '../common/definitions/defineComponents.js'; +import IgcSplitterComponent from './splitter.js'; +import IgcSplitterBarComponent from './splitter-bar.js'; +import IgcSplitterPaneComponent from './splitter-pane.js'; + +describe('Splitter', () => { + before(() => { + defineComponents( + IgcSplitterComponent, + IgcSplitterPaneComponent, + IgcSplitterBarComponent + ); + }); + + let splitter: IgcSplitterComponent; + + describe('Rendering', () => { + beforeEach(async () => { + splitter = await fixture(createSplitter()); + }); + + it('should render', () => { + expect(splitter).to.exist; + expect(splitter).to.be.instanceOf(IgcSplitterComponent); + }); + + it('is accessible', async () => { + await expect(splitter).to.be.accessible(); + await expect(splitter).shadowDom.to.be.accessible(); + }); + + it('should render panes and assign correct flex order', async () => { + await elementUpdated(splitter); + + expect(splitter.panes).to.have.lengthOf(3); + + expect(splitter.panes[0].order).to.equal(0); + expect(splitter.panes[1].order).to.equal(2); + expect(splitter.panes[2].order).to.equal(4); + }); + + it('should render splitter bars and assign correct flex order', async () => { + await elementUpdated(splitter); + const bars = Array.from( + splitter.renderRoot.querySelectorAll(IgcSplitterBarComponent.tagName) + ); + + expect(bars).to.have.lengthOf(2); + + expect(bars[0].order).to.equal(1); + expect(bars[1].order).to.equal(3); + }); + + it('should have default horizontal orientation', () => { + expect(splitter.orientation).to.equal('horizontal'); + expect(splitter.hasAttribute('orientation')).to.be.true; + expect(splitter.getAttribute('orientation')).to.equal('horizontal'); + }); + + it('should change orientation to vertical', async () => { + splitter.orientation = 'vertical'; + await elementUpdated(splitter); + + expect(splitter.orientation).to.equal('vertical'); + expect(splitter.getAttribute('orientation')).to.equal('vertical'); + }); + }); +}); + +function createSplitter() { + return html` + + Pane 1 + Pane 2 + Pane 3 + + `; +} diff --git a/src/components/splitter/themes/splitter.base.scss b/src/components/splitter/themes/splitter.base.scss index b037457e7..8b47aa515 100644 --- a/src/components/splitter/themes/splitter.base.scss +++ b/src/components/splitter/themes/splitter.base.scss @@ -8,6 +8,10 @@ color: var(--ig-gray-900); background: var(--ig-gray-100); border: 1px solid var(--ig-gray-200); + + ::slotted(igc-splitter-pane) { + flex: 1 1 0; + } } :host([orientation='horizontal']) { @@ -16,10 +20,7 @@ igc-splitter-bar { width: 5px; height: auto; - } - - igc-splitter-pane { - height: 100%; + cursor: col-resize; } } @@ -29,19 +30,14 @@ igc-splitter-bar { height: 5px; width: auto; - } - - igc-splitter-pane { - width: 100%; + cursor: row-resize; } } igc-splitter-bar { background-color: var(--ig-gray-200); -} -igc-splitter-pane { - background: var(--ig-gray-900-contrast); - border: 1px solid var(--ig-gray-300); - flex: 1 1 0; + &:hover { + background-color: var(--ig-gray-400); + } } From fea9e07544c0cd13f443db195e7b5ecb99e52d7e Mon Sep 17 00:00:00 2001 From: MonikaKirkova Date: Tue, 4 Nov 2025 18:06:54 +0200 Subject: [PATCH 04/24] feat(splitter): add splitter-pane props --- src/components/splitter/splitter-pane.ts | 154 ++++++++++++++++++ src/components/splitter/splitter.ts | 59 ++++++- .../splitter/themes/splitter.base.scss | 8 + stories/splitter.stories.ts | 74 +++++++-- 4 files changed, 279 insertions(+), 16 deletions(-) diff --git a/src/components/splitter/splitter-pane.ts b/src/components/splitter/splitter-pane.ts index 9450496f6..c5440fc8a 100644 --- a/src/components/splitter/splitter-pane.ts +++ b/src/components/splitter/splitter-pane.ts @@ -2,6 +2,7 @@ import { html, LitElement } from 'lit'; import { property } from 'lit/decorators.js'; import { registerComponent } from '../common/definitions/register.js'; import { asNumber } from '../common/util.js'; +import type IgcSplitterComponent from './splitter.js'; export default class IgcSplitterPaneComponent extends LitElement { public static readonly tagName = 'igc-splitter-pane'; @@ -12,6 +13,17 @@ export default class IgcSplitterPaneComponent extends LitElement { } private _order = -1; + private _minSize?: string; + private _maxSize?: string; + private _size = 'auto'; + private _collapsed = false; + private _minWidth?: string; + private _minHeight?: string; + private _maxWidth?: string; + private _maxHeight?: string; + + /** @hidden @internal */ + public owner: IgcSplitterComponent | undefined; /** * Gets/sets the pane's visual position in the layout. @@ -27,11 +39,153 @@ export default class IgcSplitterPaneComponent extends LitElement { return this._order; } + /** + * The minimum size of the pane. + * @attr + */ + @property({ type: String, reflect: true }) + public set minSize(value: string) { + this._minSize = value; + this.dispatchEvent(new CustomEvent('sizeChanged', { bubbles: true })); + } + + public get minSize(): string | undefined { + return this._minSize; + } + + /** + * The maximum size of the pane. + * @attr + */ + @property({ type: String, reflect: true }) + public set maxSize(value: string) { + this._maxSize = value; + this.dispatchEvent(new CustomEvent('sizeChanged', { bubbles: true })); + } + + public get maxSize(): string | undefined { + return this._maxSize; + } + + /** + * Gets/sets the pane's minWidth. + * @hidden @internal + */ + @property({ type: String }) + public set minWidth(value: string) { + this._minWidth = value; + this.style.minWidth = this._minWidth; + } + + public get minWidth(): string | undefined { + return this._minWidth; + } + + /** + * Gets/sets the pane's maxWidth. + * @hidden @internal + */ + @property({ type: String }) + public set maxWidth(value: string) { + this._maxWidth = value; + this.style.maxWidth = this._maxWidth; + } + + public get maxWidth(): string | undefined { + return this._maxWidth; + } + + /** + * Gets/sets the pane's minHeight. + * @hidden @internal + */ + @property({ type: String }) + public set minHeight(value: string) { + this._minHeight = value; + this.style.minHeight = this._minHeight; + } + + public get minHeight(): string | undefined { + return this._minHeight; + } + + /** + * Gets/sets the pane's maxHeight. + * @hidden @internal + */ + @property({ type: String }) + public set maxHeight(value: string) { + this._maxHeight = value; + this.style.maxHeight = this._maxHeight; + } + + public get maxHeight(): string | undefined { + return this._maxHeight; + } + + /** + * Defines if the pane is resizable or not. + * @attr + */ + @property({ type: Boolean, reflect: true }) + public resizable = true; + + /** + * Gets/sets the pane's maxHeight. + * @hidden @internal + */ + @property({ type: String }) + public get flex() { + //const size = this.dragSize || this.size; + //const grow = this.isPercentageSize && !this.dragSize ? 1 : 0; + const grow = this.isPercentageSize ? 1 : 0; + return `${grow} ${grow} ${this.size}`; + //return `${0} ${0} ${this.size}`; + } + + /** + * The size of the pane. + * @attr + */ + @property({ type: String, reflect: true }) + public set size(value: string) { + this._size = value; + this.style.flex = this.flex; + } + + public get size(): string { + return this._size; + } + + /** @hidden @internal */ + public get isPercentageSize() { + return this.size === 'auto' || this.size.indexOf('%') !== -1; + } + + /** + * Collapsed state of the pane. + * @attr + */ + @property({ type: Boolean, reflect: true }) + public set collapsed(value: boolean) { + this._collapsed = value; + //this.requestUpdate(); + } + + public get collapsed(): boolean { + return this._collapsed; + } + // constructor() { // super(); // //addThemingController(this, all); // } + /** Toggles the collapsed state of the pane. */ + public toggle() { + this.collapsed = !this.collapsed; + } + protected override render() { return html` `; } diff --git a/src/components/splitter/splitter.ts b/src/components/splitter/splitter.ts index 8e777317a..9d6c0177e 100644 --- a/src/components/splitter/splitter.ts +++ b/src/components/splitter/splitter.ts @@ -1,6 +1,7 @@ import { html, LitElement } from 'lit'; import { property, queryAssignedElements } from 'lit/decorators.js'; import { addSlotController } from '../common/controllers/slot.js'; +import { watch } from '../common/decorators/watch.js'; import { registerComponent } from '../common/definitions/register.js'; import type { SplitterOrientation } from '../types.js'; import IgcSplitterBarComponent from './splitter-bar.js'; @@ -37,7 +38,7 @@ export default class IgcSplitterComponent extends LitElement { public orientation: SplitterOrientation = 'horizontal'; protected _handleSlotChange(): void { - this._assignFlexOrder(); + this.initPanes(); } private _assignFlexOrder() { @@ -48,6 +49,62 @@ export default class IgcSplitterComponent extends LitElement { }); } + /** + * @hidden @internal + * This method inits panes with properties. + */ + private initPanes() { + this.panes.forEach((pane) => { + pane.owner = this; + if (this.orientation === 'horizontal') { + pane.minWidth = pane.minSize ?? '0'; + pane.maxWidth = pane.maxSize ?? '100%'; + } else { + pane.minHeight = pane.minSize ?? '0'; + pane.maxHeight = pane.maxSize ?? '100%'; + } + }); + this._assignFlexOrder(); + //in igniteui-angular this is added as feature but i haven't checked why + // if (this.panes.filter(x => x.collapsed).length > 0) { + // // if any panes are collapsed, reset sizes. + // this.resetPaneSizes(); + // } + } + + /** + * @hidden @internal + * This method reset pane sizes. + */ + private resetPaneSizes() { + if (this.panes) { + // if type is changed runtime, should reset sizes. + this.panes.forEach((pane) => { + pane.size = 'auto'; + pane.minWidth = '0'; + pane.maxWidth = '100%'; + pane.minHeight = '0'; + pane.maxHeight = '100%'; + }); + } + } + + @watch('orientation', { waitUntilFirstUpdate: true }) + protected orientationChange(): void { + this.setAttribute('aria-orientation', this.orientation); + this.resetPaneSizes(); + this.initPanes(); + } + + constructor() { + super(); + + this.addEventListener('sizeChanged', (event: any) => { + event.stopPropagation(); + this.initPanes(); + }); + } + private _renderBar(order: number) { return html` `; } diff --git a/src/components/splitter/themes/splitter.base.scss b/src/components/splitter/themes/splitter.base.scss index 8b47aa515..583fc3fc8 100644 --- a/src/components/splitter/themes/splitter.base.scss +++ b/src/components/splitter/themes/splitter.base.scss @@ -22,6 +22,10 @@ height: auto; cursor: col-resize; } + + ::slotted(igc-splitter-pane[collapsed]) { + display: none; + } } :host([orientation='vertical']) { @@ -32,6 +36,10 @@ width: auto; cursor: row-resize; } + + ::slotted(igc-splitter-pane[collapsed]) { + display: none; + } } igc-splitter-bar { diff --git a/stories/splitter.stories.ts b/stories/splitter.stories.ts index 53e437c2c..b99a8e6dc 100644 --- a/stories/splitter.stories.ts +++ b/stories/splitter.stories.ts @@ -1,11 +1,9 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import { html } from 'lit'; -import { - IgcSplitterPaneComponent, - defineComponents, -} from 'igniteui-webcomponents'; +import { defineComponents } from 'igniteui-webcomponents'; import IgcSplitterComponent from '../src/components/splitter/splitter.js'; +import IgcSplitterPaneComponent from '../src/components/splitter/splitter-pane.js'; defineComponents(IgcSplitterComponent, IgcSplitterPaneComponent); @@ -36,23 +34,69 @@ const metadata: Meta = { export default metadata; type Story = StoryObj; +function changePaneMinMaxSizes() { + const panes = document.querySelectorAll('igc-splitter-pane'); + panes[0].minSize = '100px'; + panes[0].maxSize = '300px'; + panes[1].minSize = '50px'; + panes[1].maxSize = '200px'; + panes[2].minSize = '150px'; + panes[2].maxSize = '400px'; +} + +function changePaneSize() { + const panes = document.querySelectorAll('igc-splitter-pane'); + panes[1].size = '100px'; +} + export const Default: Story = { render: ({ orientation }) => html` - - -
Pane 1
-
- -
Pane 2
-
- -
Pane 3
-
-
+ + + + +
+ + +
Pane 1
+
+ +
Pane 2
+
+ +
Pane 3
+
+
+ + + +
Pane 1
+
+ +
Pane 2
+
+ +
Pane 3
+
+
+
`, }; From b1f82e4b96c8bab05821cb112a38280594becf1e Mon Sep 17 00:00:00 2001 From: Bozhidara Pachilova Date: Wed, 5 Nov 2025 12:44:00 +0200 Subject: [PATCH 05/24] chore: minor changes; add nested story; test nested --- src/components/splitter/splitter-pane.ts | 16 +++--- src/components/splitter/splitter.spec.ts | 70 ++++++++++++++++++++++++ src/components/splitter/splitter.ts | 53 ++++++++++++------ stories/splitter.stories.ts | 50 +++++++++++++++-- 4 files changed, 160 insertions(+), 29 deletions(-) diff --git a/src/components/splitter/splitter-pane.ts b/src/components/splitter/splitter-pane.ts index c5440fc8a..b651826e9 100644 --- a/src/components/splitter/splitter-pane.ts +++ b/src/components/splitter/splitter-pane.ts @@ -43,7 +43,7 @@ export default class IgcSplitterPaneComponent extends LitElement { * The minimum size of the pane. * @attr */ - @property({ type: String, reflect: true }) + @property({ reflect: true }) public set minSize(value: string) { this._minSize = value; this.dispatchEvent(new CustomEvent('sizeChanged', { bubbles: true })); @@ -57,7 +57,7 @@ export default class IgcSplitterPaneComponent extends LitElement { * The maximum size of the pane. * @attr */ - @property({ type: String, reflect: true }) + @property({ reflect: true }) public set maxSize(value: string) { this._maxSize = value; this.dispatchEvent(new CustomEvent('sizeChanged', { bubbles: true })); @@ -71,7 +71,7 @@ export default class IgcSplitterPaneComponent extends LitElement { * Gets/sets the pane's minWidth. * @hidden @internal */ - @property({ type: String }) + @property() public set minWidth(value: string) { this._minWidth = value; this.style.minWidth = this._minWidth; @@ -85,7 +85,7 @@ export default class IgcSplitterPaneComponent extends LitElement { * Gets/sets the pane's maxWidth. * @hidden @internal */ - @property({ type: String }) + @property() public set maxWidth(value: string) { this._maxWidth = value; this.style.maxWidth = this._maxWidth; @@ -99,7 +99,7 @@ export default class IgcSplitterPaneComponent extends LitElement { * Gets/sets the pane's minHeight. * @hidden @internal */ - @property({ type: String }) + @property() public set minHeight(value: string) { this._minHeight = value; this.style.minHeight = this._minHeight; @@ -113,7 +113,7 @@ export default class IgcSplitterPaneComponent extends LitElement { * Gets/sets the pane's maxHeight. * @hidden @internal */ - @property({ type: String }) + @property() public set maxHeight(value: string) { this._maxHeight = value; this.style.maxHeight = this._maxHeight; @@ -134,7 +134,7 @@ export default class IgcSplitterPaneComponent extends LitElement { * Gets/sets the pane's maxHeight. * @hidden @internal */ - @property({ type: String }) + @property() public get flex() { //const size = this.dragSize || this.size; //const grow = this.isPercentageSize && !this.dragSize ? 1 : 0; @@ -147,7 +147,7 @@ export default class IgcSplitterPaneComponent extends LitElement { * The size of the pane. * @attr */ - @property({ type: String, reflect: true }) + @property({ reflect: true }) public set size(value: string) { this._size = value; this.style.flex = this.flex; diff --git a/src/components/splitter/splitter.spec.ts b/src/components/splitter/splitter.spec.ts index 076a47c95..0a2037708 100644 --- a/src/components/splitter/splitter.spec.ts +++ b/src/components/splitter/splitter.spec.ts @@ -66,6 +66,57 @@ describe('Splitter', () => { expect(splitter.orientation).to.equal('vertical'); expect(splitter.getAttribute('orientation')).to.equal('vertical'); }); + + it('should render nested splitters correctly', async () => { + const nestedSplitter = await fixture( + createNestedSplitter() + ); + await elementUpdated(nestedSplitter); + + expect(nestedSplitter.panes).to.have.lengthOf(2); + expect(nestedSplitter.orientation).to.equal('horizontal'); + + const outerBars = Array.from( + nestedSplitter.renderRoot.querySelectorAll( + IgcSplitterBarComponent.tagName + ) + ); + expect(outerBars).to.have.lengthOf(1); + + const firstPane = nestedSplitter.panes[0]; + const leftSplitter = firstPane.querySelector( + IgcSplitterComponent.tagName + ) as IgcSplitterComponent; + + expect(leftSplitter).to.exist; + expect(leftSplitter.orientation).to.equal('vertical'); + + expect(leftSplitter.panes).to.have.lengthOf(2); + + const leftBars = Array.from( + leftSplitter.renderRoot.querySelectorAll( + IgcSplitterBarComponent.tagName + ) + ); + expect(leftBars).to.have.lengthOf(1); + + const secondPane = nestedSplitter.panes[1]; + const rightSplitter = secondPane.querySelector( + IgcSplitterComponent.tagName + ) as IgcSplitterComponent; + + expect(rightSplitter).to.exist; + expect(rightSplitter.orientation).to.equal('vertical'); + + expect(rightSplitter.panes).to.have.lengthOf(2); + + const rightBars = Array.from( + rightSplitter.renderRoot.querySelectorAll( + IgcSplitterBarComponent.tagName + ) + ); + expect(rightBars).to.have.lengthOf(1); + }); }); }); @@ -78,3 +129,22 @@ function createSplitter() {
`; } + +function createNestedSplitter() { + return html` + + + + Top Left Pane + Bottom Left Pane + + + + + Top Right Pane + Bottom Right Pane + + + + `; +} diff --git a/src/components/splitter/splitter.ts b/src/components/splitter/splitter.ts index 9d6c0177e..4e614d69b 100644 --- a/src/components/splitter/splitter.ts +++ b/src/components/splitter/splitter.ts @@ -1,5 +1,6 @@ import { html, LitElement } from 'lit'; import { property, queryAssignedElements } from 'lit/decorators.js'; +import { addInternalsController } from '../common/controllers/internals.js'; import { addSlotController } from '../common/controllers/slot.js'; import { watch } from '../common/decorators/watch.js'; import { registerComponent } from '../common/definitions/register.js'; @@ -21,10 +22,18 @@ export default class IgcSplitterComponent extends LitElement { ); } + //#region Properties + private readonly _slots = addSlotController(this, { onChange: this._handleSlotChange, }); + private readonly _internals = addInternalsController(this, { + initialARIA: { + ariaOrientation: 'horizontal', + }, + }); + /** Returns all of the splitter's panes. */ @queryAssignedElements({ selector: 'igc-splitter-pane' }) public panes!: Array; @@ -37,8 +46,28 @@ export default class IgcSplitterComponent extends LitElement { @property({ reflect: true }) public orientation: SplitterOrientation = 'horizontal'; + //#endregion + + //#region Internal API + + @watch('orientation', { waitUntilFirstUpdate: true }) + protected _orientationChange(): void { + this._internals.setARIA({ ariaOrientation: this.orientation }); + this._resetPaneSizes(); + this._initPanes(); + } + + constructor() { + super(); + + this.addEventListener('sizeChanged', (event: any) => { + event.stopPropagation(); + this._initPanes(); + }); + } + protected _handleSlotChange(): void { - this.initPanes(); + this._initPanes(); } private _assignFlexOrder() { @@ -53,7 +82,7 @@ export default class IgcSplitterComponent extends LitElement { * @hidden @internal * This method inits panes with properties. */ - private initPanes() { + private _initPanes() { this.panes.forEach((pane) => { pane.owner = this; if (this.orientation === 'horizontal') { @@ -76,7 +105,7 @@ export default class IgcSplitterComponent extends LitElement { * @hidden @internal * This method reset pane sizes. */ - private resetPaneSizes() { + private _resetPaneSizes() { if (this.panes) { // if type is changed runtime, should reset sizes. this.panes.forEach((pane) => { @@ -89,21 +118,9 @@ export default class IgcSplitterComponent extends LitElement { } } - @watch('orientation', { waitUntilFirstUpdate: true }) - protected orientationChange(): void { - this.setAttribute('aria-orientation', this.orientation); - this.resetPaneSizes(); - this.initPanes(); - } + //#endregion - constructor() { - super(); - - this.addEventListener('sizeChanged', (event: any) => { - event.stopPropagation(); - this.initPanes(); - }); - } + //#region Rendering private _renderBar(order: number) { return html` `; @@ -118,6 +135,8 @@ export default class IgcSplitterComponent extends LitElement { })} `; } + + //#endregion } declare global { diff --git a/stories/splitter.stories.ts b/stories/splitter.stories.ts index b99a8e6dc..323a32f0d 100644 --- a/stories/splitter.stories.ts +++ b/stories/splitter.stories.ts @@ -4,6 +4,7 @@ import { html } from 'lit'; import { defineComponents } from 'igniteui-webcomponents'; import IgcSplitterComponent from '../src/components/splitter/splitter.js'; import IgcSplitterPaneComponent from '../src/components/splitter/splitter-pane.js'; +import { disableStoryControls } from './story.js'; defineComponents(IgcSplitterComponent, IgcSplitterPaneComponent); @@ -37,11 +38,11 @@ type Story = StoryObj; function changePaneMinMaxSizes() { const panes = document.querySelectorAll('igc-splitter-pane'); panes[0].minSize = '100px'; - panes[0].maxSize = '300px'; + panes[0].maxSize = '200px'; panes[1].minSize = '50px'; - panes[1].maxSize = '200px'; + panes[1].maxSize = '100px'; panes[2].minSize = '150px'; - panes[2].maxSize = '400px'; + panes[2].maxSize = '100px'; } function changePaneSize() { @@ -74,7 +75,7 @@ export const Default: Story = {
- +
Pane 1
@@ -100,3 +101,44 @@ export const Default: Story = {
`, }; + +export const NestedSplitters: Story = { + argTypes: disableStoryControls(metadata), + render: () => html` + + + + + + +
Top Left Pane
+
+ + +
Bottom Left Pane
+
+
+
+ + + + +
Top Right Pane
+
+ + +
Bottom Right Pane
+
+
+
+
+ `, +}; From 5b3d2fe727b4bd8e2f4f65981cad90a7527243a7 Mon Sep 17 00:00:00 2001 From: Bozhidara Pachilova Date: Wed, 5 Nov 2025 16:30:25 +0200 Subject: [PATCH 06/24] feat(splitter): add args for each pane to default story --- src/components/splitter/splitter.ts | 10 + .../splitter/themes/splitter.base.scss | 1 + stories/splitter.stories.ts | 187 ++++++++++++++---- 3 files changed, 165 insertions(+), 33 deletions(-) diff --git a/src/components/splitter/splitter.ts b/src/components/splitter/splitter.ts index 4e614d69b..fbfc38b4d 100644 --- a/src/components/splitter/splitter.ts +++ b/src/components/splitter/splitter.ts @@ -9,6 +9,16 @@ import IgcSplitterBarComponent from './splitter-bar.js'; import IgcSplitterPaneComponent from './splitter-pane.js'; import { styles } from './themes/splitter.base.css.js'; +/** + * The Splitter component provides a framework for a simple layout, splitting the view horizontally or vertically + * into multiple smaller resizable and collapsible areas. + * + * @element igc-splitter + * * + * @fires igc... - Emitted when ... . + * + * @csspart ... - ... . + */ export default class IgcSplitterComponent extends LitElement { public static readonly tagName = 'igc-splitter'; public static override styles = [styles]; diff --git a/src/components/splitter/themes/splitter.base.scss b/src/components/splitter/themes/splitter.base.scss index 583fc3fc8..eabf2ed9b 100644 --- a/src/components/splitter/themes/splitter.base.scss +++ b/src/components/splitter/themes/splitter.base.scss @@ -11,6 +11,7 @@ ::slotted(igc-splitter-pane) { flex: 1 1 0; + overflow: auto; } } diff --git a/stories/splitter.stories.ts b/stories/splitter.stories.ts index 323a32f0d..895c3c8f0 100644 --- a/stories/splitter.stories.ts +++ b/stories/splitter.stories.ts @@ -8,7 +8,30 @@ import { disableStoryControls } from './story.js'; defineComponents(IgcSplitterComponent, IgcSplitterPaneComponent); -const metadata: Meta = { +type SplitterStoryArgs = IgcSplitterComponent & { + /* Pane 1 properties */ + pane1Size?: string; + pane1MinSize?: string; + pane1MaxSize?: string; + pane1Collapsed?: boolean; + pane1Resizable?: boolean; + + /* Pane 2 properties */ + pane2Size?: string; + pane2MinSize?: string; + pane2MaxSize?: string; + pane2Collapsed?: boolean; + pane2Resizable?: boolean; + + /* Pane 3 properties */ + pane3Size?: string; + pane3MinSize?: string; + pane3MaxSize?: string; + pane3Collapsed?: boolean; + pane3Resizable?: boolean; +}; + +const metadata: Meta = { title: 'Splitter', component: 'igc-splitter', parameters: { @@ -26,14 +49,98 @@ const metadata: Meta = { description: 'Orientation of the splitter.', table: { defaultValue: { summary: 'horizontal' } }, }, + pane1Size: { + control: 'text', + description: 'Size of the first pane (e.g., "auto", "100px", "30%")', + table: { category: 'Pane 1' }, + }, + pane1MinSize: { + control: 'text', + description: 'Minimum size of the first pane', + table: { category: 'Pane 1' }, + }, + pane1MaxSize: { + control: 'text', + description: 'Maximum size of the first pane', + table: { category: 'Pane 1' }, + }, + pane1Collapsed: { + control: 'boolean', + description: 'Collapsed state of the first pane', + table: { category: 'Pane 1' }, + }, + pane1Resizable: { + control: 'boolean', + description: 'Whether the first pane is resizable', + table: { category: 'Pane 1' }, + }, + pane2Size: { + control: 'text', + description: 'Size of the second pane (e.g., "auto", "100px", "30%")', + table: { category: 'Pane 2' }, + }, + pane2MinSize: { + control: 'text', + description: 'Minimum size of the second pane', + table: { category: 'Pane 2' }, + }, + pane2MaxSize: { + control: 'text', + description: 'Maximum size of the second pane', + table: { category: 'Pane 2' }, + }, + pane2Collapsed: { + control: 'boolean', + description: 'Collapsed state of the second pane', + table: { category: 'Pane 2' }, + }, + pane2Resizable: { + control: 'boolean', + description: 'Whether the second pane is resizable', + table: { category: 'Pane 2' }, + }, + pane3Size: { + control: 'text', + description: 'Size of the third pane (e.g., "auto", "100px", "30%")', + table: { category: 'Pane 3' }, + }, + pane3MinSize: { + control: 'text', + description: 'Minimum size of the third pane', + table: { category: 'Pane 3' }, + }, + pane3MaxSize: { + control: 'text', + description: 'Maximum size of the third pane', + table: { category: 'Pane 3' }, + }, + pane3Collapsed: { + control: 'boolean', + description: 'Collapsed state of the third pane', + table: { category: 'Pane 3' }, + }, + pane3Resizable: { + control: 'boolean', + description: 'Whether the third pane is resizable', + table: { category: 'Pane 3' }, + }, }, args: { orientation: 'horizontal', + pane1Size: 'auto', + pane1Resizable: true, + pane1Collapsed: false, + pane2Size: 'auto', + pane2Resizable: true, + pane2Collapsed: false, + pane3Size: 'auto', + pane3Resizable: true, + pane3Collapsed: false, }, }; export default metadata; -type Story = StoryObj; +type Story = StoryObj; function changePaneMinMaxSizes() { const panes = document.querySelectorAll('igc-splitter-pane'); @@ -45,13 +152,25 @@ function changePaneMinMaxSizes() { panes[2].maxSize = '100px'; } -function changePaneSize() { - const panes = document.querySelectorAll('igc-splitter-pane'); - panes[1].size = '100px'; -} - export const Default: Story = { - render: ({ orientation }) => html` + render: ({ + orientation, + pane1Size, + pane1MinSize, + pane1MaxSize, + pane1Collapsed, + pane1Resizable, + pane2Size, + pane2MinSize, + pane2MaxSize, + pane2Collapsed, + pane2Resizable, + pane3Size, + pane3MinSize, + pane3MaxSize, + pane3Collapsed, + pane3Resizable, + }) => html` - - -
- -
Pane 1
-
- -
Pane 2
-
- -
Pane 3
-
-
- - - +
Pane 1
- +
Pane 2
- +
Pane 3
+ Change All Panes Min/Max Sizes `, }; From 5d7907d7799ace4ea92dd0a51d1b19f2ccc4dddd Mon Sep 17 00:00:00 2001 From: MonikaKirkova Date: Wed, 5 Nov 2025 16:48:02 +0200 Subject: [PATCH 07/24] feat(splitter): add nonCollapsible prop --- src/components/splitter/splitter.ts | 9 +++++++++ stories/splitter.stories.ts | 8 +++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/splitter/splitter.ts b/src/components/splitter/splitter.ts index fbfc38b4d..6fca7d5a8 100644 --- a/src/components/splitter/splitter.ts +++ b/src/components/splitter/splitter.ts @@ -56,6 +56,15 @@ export default class IgcSplitterComponent extends LitElement { @property({ reflect: true }) public orientation: SplitterOrientation = 'horizontal'; + /** + * Sets the visibility of the handle and expanders in the splitter bar. + * @remarks + * Default value is `false`. + * @attr + */ + @property({ type: Boolean, reflect: true }) + public nonCollapsible = false; + //#endregion //#region Internal API diff --git a/stories/splitter.stories.ts b/stories/splitter.stories.ts index 895c3c8f0..02d2cf9fd 100644 --- a/stories/splitter.stories.ts +++ b/stories/splitter.stories.ts @@ -127,6 +127,7 @@ const metadata: Meta = { }, args: { orientation: 'horizontal', + nonCollapsible: false, pane1Size: 'auto', pane1Resizable: true, pane1Collapsed: false, @@ -155,6 +156,7 @@ function changePaneMinMaxSizes() { export const Default: Story = { render: ({ orientation, + nonCollapsible, pane1Size, pane1MinSize, pane1MaxSize, @@ -184,7 +186,11 @@ export const Default: Story = {
- + Date: Thu, 6 Nov 2025 19:38:09 +0200 Subject: [PATCH 08/24] feat(splitter): add initial resize logic --- src/components/splitter/splitter-bar.ts | 68 ++++++++++++- src/components/splitter/splitter.ts | 124 +++++++++++++++++++++++- 2 files changed, 184 insertions(+), 8 deletions(-) diff --git a/src/components/splitter/splitter-bar.ts b/src/components/splitter/splitter-bar.ts index 9aa22d1d1..1a6817a26 100644 --- a/src/components/splitter/splitter-bar.ts +++ b/src/components/splitter/splitter-bar.ts @@ -1,9 +1,22 @@ import { html, LitElement } from 'lit'; import { property } from 'lit/decorators.js'; import { registerComponent } from '../common/definitions/register.js'; +import type { Constructor } from '../common/mixins/constructor.js'; +import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; import { asNumber } from '../common/util.js'; +import { addResizeController } from '../resize-container/resize-controller.js'; +import type { SplitterOrientation } from '../types.js'; +import type IgcSplitterPaneComponent from './splitter-pane.js'; -export default class IgcSplitterBarComponent extends LitElement { +export interface IgcSplitterBarComponentEventMap { + igcMovingStart: CustomEvent; + igcMoving: CustomEvent; + igcMovingEnd: CustomEvent; +} +export default class IgcSplitterBarComponent extends EventEmitterMixin< + IgcSplitterBarComponentEventMap, + Constructor +>(LitElement) { public static readonly tagName = 'igc-splitter-bar'; /* blazorSuppress */ @@ -27,10 +40,55 @@ export default class IgcSplitterBarComponent extends LitElement { return this._order; } - // constructor() { - // super(); - // //addThemingController(this, all); - // } + /** Gets/Sets the orientation of the splitter. + * + * @remarks + * Default value is `horizontal`. + */ + @property({ reflect: true }) + public orientation: SplitterOrientation = 'horizontal'; + + @property({ attribute: false }) + public paneBefore?: IgcSplitterPaneComponent; + + @property({ attribute: false }) + public paneAfter?: IgcSplitterPaneComponent; + + constructor() { + super(); + addResizeController(this, { + mode: 'immediate', + resizeTarget: (): HTMLElement => this.paneBefore ?? this, // we don’t resize the bar, we just use the delta + start: () => { + if ( + !this.paneBefore?.resizable || + !this.paneAfter?.resizable || + this.paneBefore.collapsed + ) { + return false; + } + this.emitEvent('igcMovingStart', { detail: this.paneBefore }); + return true; + }, + resize: ({ state }) => { + const isHorizontal = this.orientation === 'horizontal'; + const delta = isHorizontal ? state.deltaX : state.deltaY; + + if (delta !== 0) { + this.emitEvent('igcMoving', { detail: delta }); + } + }, + end: ({ state }) => { + const isHorizontal = this.orientation === 'horizontal'; + const delta = isHorizontal ? state.deltaX : state.deltaY; + if (delta !== 0) { + this.emitEvent('igcMovingEnd', { detail: delta }); + } + }, + cancel: () => {}, + }); + //addThemingController(this, all); + } protected override render() { return html` diff --git a/src/components/splitter/splitter.ts b/src/components/splitter/splitter.ts index 6fca7d5a8..e0ab42a5f 100644 --- a/src/components/splitter/splitter.ts +++ b/src/components/splitter/splitter.ts @@ -137,12 +137,130 @@ export default class IgcSplitterComponent extends LitElement { } } + private paneBefore!: IgcSplitterPaneComponent; + private paneAfter!: IgcSplitterPaneComponent; + private initialPaneBeforeSize!: number; + private initialPaneAfterSize!: number; + + private _handleMovingStart(event: CustomEvent) { + // Handle the moving start event + const panes = this.panes; + this.paneBefore = event.detail; + this.paneAfter = panes[panes.indexOf(this.paneBefore) + 1]; + + const paneRect = this.paneBefore.getBoundingClientRect(); + this.initialPaneBeforeSize = + this.orientation === 'horizontal' ? paneRect.width : paneRect.height; + + const siblingRect = this.paneAfter.getBoundingClientRect(); + this.initialPaneAfterSize = + this.orientation === 'horizontal' + ? siblingRect.width + : siblingRect.height; + } + private _handleMoving(event: CustomEvent) { + const [paneSize, siblingSize] = this.calcNewSizes(event.detail); + + this.paneBefore.size = paneSize + 'px'; + this.paneAfter.size = siblingSize + 'px'; + } + + //I am not sure if this code changes anything, it looks like it works without it as well, + // however I found a bug, which I am not sure how to reproduce it and still haven't encaountered it with this code + private _handleMovingEnd(event: CustomEvent) { + const [paneSize, siblingSize] = this.calcNewSizes(event.detail); + + if (this.paneBefore.isPercentageSize) { + // handle % resizes + const totalSize = this.getTotalSize(); + const percentPaneSize = (paneSize / totalSize) * 100; + this.paneBefore.size = percentPaneSize + '%'; + } else { + // px resize + this.paneBefore.size = paneSize + 'px'; + } + + if (this.paneAfter.isPercentageSize) { + // handle % resizes + const totalSize = this.getTotalSize(); + const percentSiblingPaneSize = (siblingSize / totalSize) * 100; + this.paneAfter.size = percentSiblingPaneSize + '%'; + } else { + // px resize + this.paneAfter.size = siblingSize + 'px'; + } + } + + /** + * @hidden @internal + * Calculates new sizes for the panes based on move delta and initial sizes + */ + private calcNewSizes(delta: number): [number, number] { + let finalDelta: number; + const min = + Number.parseInt( + this.paneBefore.minSize ? this.paneBefore.minSize : '0', + 10 + ) || 0; + const minSibling = + Number.parseInt( + this.paneAfter.minSize ? this.paneAfter.minSize : '0', + 10 + ) || 0; + const max = + Number.parseInt( + this.paneBefore.maxSize ? this.paneBefore.maxSize : '0', + 10 + ) || this.initialPaneBeforeSize + this.initialPaneAfterSize - minSibling; + const maxSibling = + Number.parseInt( + this.paneAfter.maxSize ? this.paneAfter.maxSize : '0', + 10 + ) || this.initialPaneBeforeSize + this.initialPaneAfterSize - min; + + if (delta < 0) { + const maxPossibleDelta = Math.min( + this.initialPaneBeforeSize - min, + maxSibling - this.initialPaneAfterSize + ); + finalDelta = Math.min(maxPossibleDelta, Math.abs(delta)) * -1; + } else { + const maxPossibleDelta = Math.min( + max - this.initialPaneBeforeSize, + this.initialPaneAfterSize - minSibling + ); + finalDelta = Math.min(maxPossibleDelta, Math.abs(delta)); + } + return [ + this.initialPaneBeforeSize + finalDelta, + this.initialPaneAfterSize - finalDelta, + ]; + } + + private getTotalSize() { + const computed = document.defaultView?.getComputedStyle(this); + const totalSize = + this.orientation === 'horizontal' + ? computed?.getPropertyValue('width') + : computed?.getPropertyValue('height'); + return Number.parseFloat(totalSize ? totalSize : '0'); + } //#endregion //#region Rendering - private _renderBar(order: number) { - return html` `; + private _renderBar(order: number, i: number) { + return html` + + `; } protected override render() { @@ -150,7 +268,7 @@ export default class IgcSplitterComponent extends LitElement { ${this.panes.map((pane, i) => { const isLast = i === this.panes.length - 1; - return html`${!isLast ? this._renderBar(pane.order + 1) : ''}`; + return html`${!isLast ? this._renderBar(pane.order + 1, i) : ''}`; })} `; } From 96a7364215605f93fd5bd175b4654afe70c48b39 Mon Sep 17 00:00:00 2001 From: Bozhidara Pachilova Date: Fri, 7 Nov 2025 15:41:58 +0200 Subject: [PATCH 09/24] refactor(splitter): alternative approach to render bars and handle properties&styles --- src/components/common/context.ts | 6 + src/components/splitter/splitter-bar.ts | 162 ++++++-- src/components/splitter/splitter-pane.ts | 351 ++++++++++++------ src/components/splitter/splitter.spec.ts | 272 ++++++++++++-- src/components/splitter/splitter.ts | 225 +---------- .../splitter/themes/splitter-bar.base.scss | 19 + .../splitter/themes/splitter-pane.scss | 17 + .../splitter/themes/splitter.base.scss | 52 +-- src/index.ts | 2 +- stories/splitter.stories.ts | 15 +- 10 files changed, 691 insertions(+), 430 deletions(-) create mode 100644 src/components/splitter/themes/splitter-bar.base.scss create mode 100644 src/components/splitter/themes/splitter-pane.scss diff --git a/src/components/common/context.ts b/src/components/common/context.ts index b31c3bafb..f41f8ebe7 100644 --- a/src/components/common/context.ts +++ b/src/components/common/context.ts @@ -1,5 +1,6 @@ import { createContext } from '@lit/context'; import type { Ref } from 'lit/directives/ref.js'; +import type { IgcSplitterComponent } from '../../index.js'; import type IgcCarouselComponent from '../carousel/carousel.js'; import type { ChatState } from '../chat/chat-state.js'; import type IgcTileManagerComponent from '../tile-manager/tile-manager.js'; @@ -24,9 +25,14 @@ const chatUserInputContext = createContext( Symbol('chat-user-input-context') ); +const splitterContext = createContext( + Symbol('splitter-context') +); + export { carouselContext, tileManagerContext, chatContext, chatUserInputContext, + splitterContext, }; diff --git a/src/components/splitter/splitter-bar.ts b/src/components/splitter/splitter-bar.ts index 1a6817a26..be8efda0c 100644 --- a/src/components/splitter/splitter-bar.ts +++ b/src/components/splitter/splitter-bar.ts @@ -1,12 +1,17 @@ -import { html, LitElement } from 'lit'; -import { property } from 'lit/decorators.js'; +import { ContextConsumer } from '@lit/context'; +import { html, LitElement, nothing } from 'lit'; +import { type StyleInfo, styleMap } from 'lit/directives/style-map.js'; +import { splitterContext } from '../common/context.js'; +import { addInternalsController } from '../common/controllers/internals.js'; +import { createMutationController } from '../common/controllers/mutation-observer.js'; import { registerComponent } from '../common/definitions/register.js'; import type { Constructor } from '../common/mixins/constructor.js'; import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; -import { asNumber } from '../common/util.js'; import { addResizeController } from '../resize-container/resize-controller.js'; import type { SplitterOrientation } from '../types.js'; -import type IgcSplitterPaneComponent from './splitter-pane.js'; +import type IgcSplitterComponent from './splitter.js'; +import IgcSplitterPaneComponent from './splitter-pane.js'; +import { styles } from './themes/splitter-bar.base.css.js'; export interface IgcSplitterBarComponentEventMap { igcMovingStart: CustomEvent; @@ -18,60 +23,92 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< Constructor >(LitElement) { public static readonly tagName = 'igc-splitter-bar'; + public static styles = [styles]; /* blazorSuppress */ public static register() { registerComponent(IgcSplitterBarComponent); } - private _order = -1; + private readonly _internals = addInternalsController(this, { + initialARIA: { + ariaOrientation: 'horizontal', + }, + }); - /** - * Gets/sets the bar's visual position in the layout. - * @hidden @internal - */ - @property({ type: Number }) - public set order(value: number) { - this._order = asNumber(value); - this.style.order = this._order.toString(); - } + protected _contextConsumer = new ContextConsumer(this, { + context: splitterContext, + subscribe: true, + callback: (value) => { + this._handleContextChange(value); + }, + }); + + private _internalStyles: StyleInfo = {}; + private _orientation?: SplitterOrientation; + private _splitter?: IgcSplitterComponent; + + private get _siblingPanes(): Array { + if (!this._splitter || !this._splitter.panes) { + return []; + } + + const panes = this._splitter.panes; + const ownerPaneIndex = panes.findIndex((p) => p.shadowRoot?.contains(this)); + + if (ownerPaneIndex === -1) { + return []; + } - public get order(): number { - return this._order; + const currentPane = panes[ownerPaneIndex]; + const nextPane = panes[ownerPaneIndex + 1] || null; + return [currentPane, nextPane]; } - /** Gets/Sets the orientation of the splitter. - * - * @remarks - * Default value is `horizontal`. - */ - @property({ reflect: true }) - public orientation: SplitterOrientation = 'horizontal'; + private get _styles(): StyleInfo { + return { + display: 'flex', + flexDirection: this._orientation === 'horizontal' ? 'column' : 'row', + width: this._orientation === 'horizontal' ? '5px' : '100%', + height: this._orientation === 'horizontal' ? '100%' : '5px', + '--cursor': this._cursor, + }; + } - @property({ attribute: false }) - public paneBefore?: IgcSplitterPaneComponent; + private get _resizeDisallowed() { + return !!this._siblingPanes.find( + (x) => x && (x.resizable === false || x.collapsed === true) + ); + } - @property({ attribute: false }) - public paneAfter?: IgcSplitterPaneComponent; + /** + * Returns the appropriate cursor style based on orientation and resize state. + */ + private get _cursor(): string { + if (this._resizeDisallowed) { + return 'default'; + } + return this._orientation === 'horizontal' ? 'col-resize' : 'row-resize'; + } constructor() { super(); addResizeController(this, { mode: 'immediate', - resizeTarget: (): HTMLElement => this.paneBefore ?? this, // we don’t resize the bar, we just use the delta + resizeTarget: (): HTMLElement => this._siblingPanes[0] ?? this, // we don’t resize the bar, we just use the delta start: () => { if ( - !this.paneBefore?.resizable || - !this.paneAfter?.resizable || - this.paneBefore.collapsed + !this._siblingPanes[0]?.resizable || + !this._siblingPanes[1]?.resizable || + this._siblingPanes[0].collapsed ) { return false; } - this.emitEvent('igcMovingStart', { detail: this.paneBefore }); + this.emitEvent('igcMovingStart', { detail: this._siblingPanes[0] }); return true; }, resize: ({ state }) => { - const isHorizontal = this.orientation === 'horizontal'; + const isHorizontal = this._orientation === 'horizontal'; const delta = isHorizontal ? state.deltaX : state.deltaY; if (delta !== 0) { @@ -79,7 +116,7 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< } }, end: ({ state }) => { - const isHorizontal = this.orientation === 'horizontal'; + const isHorizontal = this._orientation === 'horizontal'; const delta = isHorizontal ? state.deltaX : state.deltaY; if (delta !== 0) { this.emitEvent('igcMovingEnd', { detail: delta }); @@ -90,12 +127,61 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< //addThemingController(this, all); } + public override connectedCallback(): void { + super.connectedCallback(); + this._siblingPanes?.forEach((pane) => { + this._createSiblingPaneMutationController(pane!); + }); + } + + private _createSiblingPaneMutationController(pane: IgcSplitterPaneComponent) { + createMutationController(pane, { + callback: () => { + this.requestUpdate(); + }, + filter: [IgcSplitterPaneComponent.tagName], + config: { + attributeFilter: ['collapsed', 'resizable'], + subtree: true, + }, + }); + } + + private _handleContextChange(splitter: IgcSplitterComponent) { + this._splitter = splitter; + if (this._orientation !== splitter.orientation) { + this._orientation = splitter.orientation; + this._internals.setARIA({ ariaOrientation: this._orientation }); + Object.assign(this._internalStyles, this._styles); + } + } + + private _renderBarControls() { + if (this._splitter?.nonCollapsible) { + return nothing; + } + const siblings = this._siblingPanes; + const prevButtonHidden = siblings[0]?.collapsed && !siblings[1]?.collapsed; + const nextButtonHidden = siblings[1]?.collapsed && !siblings[0]?.collapsed; + return html` +
+
+
+ `; + } + protected override render() { return html` -
-
-
-
+
+ ${this._renderBarControls()}
`; } diff --git a/src/components/splitter/splitter-pane.ts b/src/components/splitter/splitter-pane.ts index b651826e9..649f91816 100644 --- a/src/components/splitter/splitter-pane.ts +++ b/src/components/splitter/splitter-pane.ts @@ -1,52 +1,62 @@ -import { html, LitElement } from 'lit'; +import { ContextConsumer } from '@lit/context'; +import { html, LitElement, nothing } from 'lit'; import { property } from 'lit/decorators.js'; +import { type StyleInfo, styleMap } from 'lit/directives/style-map.js'; +import { splitterContext } from '../common/context.js'; import { registerComponent } from '../common/definitions/register.js'; -import { asNumber } from '../common/util.js'; +import type { SplitterOrientation } from '../types.js'; import type IgcSplitterComponent from './splitter.js'; +import IgcSplitterBarComponent from './splitter-bar.js'; +import { styles } from './themes/splitter-pane.css.js'; export default class IgcSplitterPaneComponent extends LitElement { public static readonly tagName = 'igc-splitter-pane'; + public static override styles = [styles]; /* blazorSuppress */ public static register() { - registerComponent(IgcSplitterPaneComponent); + registerComponent(IgcSplitterPaneComponent, IgcSplitterBarComponent); } - private _order = -1; + private _splitterContext = new ContextConsumer(this, { + context: splitterContext, + subscribe: true, + callback: (value) => { + this._handleContextChange(value); + }, + }); + + private _internalStyles: StyleInfo = {}; private _minSize?: string; private _maxSize?: string; private _size = 'auto'; private _collapsed = false; - private _minWidth?: string; - private _minHeight?: string; - private _maxWidth?: string; - private _maxHeight?: string; + private _orientation?: SplitterOrientation; - /** @hidden @internal */ - public owner: IgcSplitterComponent | undefined; + private get _isPercentageSize() { + return this._size === 'auto' || this._size.indexOf('%') !== -1; + } - /** - * Gets/sets the pane's visual position in the layout. - * @hidden @internal - */ - @property({ type: Number }) - public set order(value: number) { - this._order = asNumber(value); - this.style.order = this._order.toString(); + private get _splitter(): IgcSplitterComponent | undefined { + return this._splitterContext.value; } - public get order(): number { - return this._order; + private get _flex() { + //const size = this.dragSize || this.size; + //const grow = this.isPercentageSize && !this.dragSize ? 1 : 0; + const grow = this._isPercentageSize ? 1 : 0; + return `${grow} ${grow} ${this._size}`; + //return `${0} ${0} ${this.size}`; } /** * The minimum size of the pane. * @attr */ - @property({ reflect: true }) + @property({ attribute: 'min-size', reflect: true }) public set minSize(value: string) { this._minSize = value; - this.dispatchEvent(new CustomEvent('sizeChanged', { bubbles: true })); + this._initPane(); } public get minSize(): string | undefined { @@ -57,92 +67,16 @@ export default class IgcSplitterPaneComponent extends LitElement { * The maximum size of the pane. * @attr */ - @property({ reflect: true }) + @property({ attribute: 'max-size', reflect: true }) public set maxSize(value: string) { this._maxSize = value; - this.dispatchEvent(new CustomEvent('sizeChanged', { bubbles: true })); + this._initPane(); } public get maxSize(): string | undefined { return this._maxSize; } - /** - * Gets/sets the pane's minWidth. - * @hidden @internal - */ - @property() - public set minWidth(value: string) { - this._minWidth = value; - this.style.minWidth = this._minWidth; - } - - public get minWidth(): string | undefined { - return this._minWidth; - } - - /** - * Gets/sets the pane's maxWidth. - * @hidden @internal - */ - @property() - public set maxWidth(value: string) { - this._maxWidth = value; - this.style.maxWidth = this._maxWidth; - } - - public get maxWidth(): string | undefined { - return this._maxWidth; - } - - /** - * Gets/sets the pane's minHeight. - * @hidden @internal - */ - @property() - public set minHeight(value: string) { - this._minHeight = value; - this.style.minHeight = this._minHeight; - } - - public get minHeight(): string | undefined { - return this._minHeight; - } - - /** - * Gets/sets the pane's maxHeight. - * @hidden @internal - */ - @property() - public set maxHeight(value: string) { - this._maxHeight = value; - this.style.maxHeight = this._maxHeight; - } - - public get maxHeight(): string | undefined { - return this._maxHeight; - } - - /** - * Defines if the pane is resizable or not. - * @attr - */ - @property({ type: Boolean, reflect: true }) - public resizable = true; - - /** - * Gets/sets the pane's maxHeight. - * @hidden @internal - */ - @property() - public get flex() { - //const size = this.dragSize || this.size; - //const grow = this.isPercentageSize && !this.dragSize ? 1 : 0; - const grow = this.isPercentageSize ? 1 : 0; - return `${grow} ${grow} ${this.size}`; - //return `${0} ${0} ${this.size}`; - } - /** * The size of the pane. * @attr @@ -150,17 +84,21 @@ export default class IgcSplitterPaneComponent extends LitElement { @property({ reflect: true }) public set size(value: string) { this._size = value; - this.style.flex = this.flex; + Object.assign(this._internalStyles, { + flex: this._flex, + }); } public get size(): string { return this._size; } - /** @hidden @internal */ - public get isPercentageSize() { - return this.size === 'auto' || this.size.indexOf('%') !== -1; - } + /** + * Defines if the pane is resizable or not. + * @attr + */ + @property({ type: Boolean, reflect: true }) + public resizable = true; /** * Collapsed state of the pane. @@ -169,7 +107,6 @@ export default class IgcSplitterPaneComponent extends LitElement { @property({ type: Boolean, reflect: true }) public set collapsed(value: boolean) { this._collapsed = value; - //this.requestUpdate(); } public get collapsed(): boolean { @@ -181,13 +118,213 @@ export default class IgcSplitterPaneComponent extends LitElement { // //addThemingController(this, all); // } + protected override firstUpdated() { + this._initPane(); + } + + private _handleContextChange(splitter: IgcSplitterComponent) { + if (this._orientation && this._orientation !== splitter.orientation) { + this._resetPane(); + } + this._orientation = splitter.orientation; + this.requestUpdate(); + } + + private _resetPane() { + this.size = 'auto'; + Object.assign(this._internalStyles, { + minWidth: 0, + maxWidth: '100%', + minHeight: 0, + maxHeight: '100%', + flex: this._flex, + }); + } + + private _initPane() { + let sizes = {}; + if (this._orientation === 'horizontal') { + sizes = { + minWidth: this.minSize ?? 0, + maxWidth: this.maxSize ?? '100%', + }; + } else { + sizes = { + minHeight: this.minSize ?? 0, + maxHeight: this.maxSize ?? '100%', + }; + } + Object.assign(this._internalStyles, { ...sizes, flex: this._flex }); + this.requestUpdate(); + } + + private paneBefore!: IgcSplitterPaneComponent; + private paneAfter!: IgcSplitterPaneComponent; + private initialPaneBeforeSize!: number; + private initialPaneAfterSize!: number; + private isPaneBeforePercentage = false; + private isPaneAfterPercentage = false; + + private _handleMovingStart(event: CustomEvent) { + // Only handle if this is the pane that owns the bar + if (event.detail !== this) { + return; + } + + // Handle the moving start event + const panes = this._splitter!.panes; + this.paneBefore = this; + this.paneAfter = panes[panes.indexOf(this) + 1]; + + // Store original size types before we start changing them + this.isPaneBeforePercentage = this.paneBefore._isPercentageSize; + this.isPaneAfterPercentage = this.paneAfter._isPercentageSize; + + const paneBeforeBase = + this.paneBefore.shadowRoot?.querySelector('[part="base"]'); + const paneAfterBase = + this.paneAfter.shadowRoot?.querySelector('[part="base"]'); + + const paneRect = paneBeforeBase!.getBoundingClientRect(); + this.initialPaneBeforeSize = + this._orientation === 'horizontal' ? paneRect.width : paneRect.height; + + const siblingRect = paneAfterBase!.getBoundingClientRect(); + this.initialPaneAfterSize = + this._orientation === 'horizontal' + ? siblingRect.width + : siblingRect.height; + } + + private _handleMoving(event: CustomEvent) { + // Only handle if this pane owns the bar (is the one before the bar) + if (!this.paneBefore || this.paneBefore !== this) { + return; + } + + const [paneSize, siblingSize] = this._calcNewSizes(event.detail); + + this.paneBefore.size = `${paneSize}px`; + this.paneAfter.size = `${siblingSize}px`; + } + + //I am not sure if this code changes anything, it looks like it works without it as well, + // however I found a bug, which I am not sure how to reproduce it and still haven't encaountered it with this code + private _handleMovingEnd(event: CustomEvent) { + // Only handle if this pane owns the bar (is the one before the bar) + if (!this.paneBefore || this.paneBefore !== this) { + return; + } + + const [paneSize, siblingSize] = this._calcNewSizes(event.detail); + + if (this.isPaneBeforePercentage) { + // handle % resizes + const totalSize = this.getTotalSize(); + const percentPaneSize = (paneSize / totalSize) * 100; + this.paneBefore.size = `${percentPaneSize}%`; + } else { + // px resize + this.paneBefore.size = `${paneSize}px`; + } + + if (this.isPaneAfterPercentage) { + // handle % resizes + const totalSize = this.getTotalSize(); + const percentSiblingPaneSize = (siblingSize / totalSize) * 100; + this.paneAfter.size = `${percentSiblingPaneSize}%`; + } else { + // px resize + this.paneAfter.size = `${siblingSize}px`; + } + } + + private _calcNewSizes(delta: number): [number, number] { + let finalDelta: number; + const min = + Number.parseInt( + this.paneBefore.minSize ? this.paneBefore.minSize : '0', + 10 + ) || 0; + const minSibling = + Number.parseInt( + this.paneAfter.minSize ? this.paneAfter.minSize : '0', + 10 + ) || 0; + const max = + Number.parseInt( + this.paneBefore.maxSize ? this.paneBefore.maxSize : '0', + 10 + ) || this.initialPaneBeforeSize + this.initialPaneAfterSize - minSibling; + const maxSibling = + Number.parseInt( + this.paneAfter.maxSize ? this.paneAfter.maxSize : '0', + 10 + ) || this.initialPaneBeforeSize + this.initialPaneAfterSize - min; + + if (delta < 0) { + const maxPossibleDelta = Math.min( + this.initialPaneBeforeSize - min, + maxSibling - this.initialPaneAfterSize + ); + finalDelta = Math.min(maxPossibleDelta, Math.abs(delta)) * -1; + } else { + const maxPossibleDelta = Math.min( + max - this.initialPaneBeforeSize, + this.initialPaneAfterSize - minSibling + ); + finalDelta = Math.min(maxPossibleDelta, Math.abs(delta)); + } + return [ + this.initialPaneBeforeSize + finalDelta, + this.initialPaneAfterSize - finalDelta, + ]; + } + + private getTotalSize() { + if (!this._splitter) { + return 0; + } + // get the size of part base + const splitterBase = + this._splitter.shadowRoot?.querySelector('[part="base"]'); + if (!splitterBase) { + return 0; + } + const rect = splitterBase.getBoundingClientRect(); + return this._orientation === 'horizontal' ? rect.width : rect.height; + } + /** Toggles the collapsed state of the pane. */ public toggle() { this.collapsed = !this.collapsed; } + private get _isLastPane(): boolean { + if (!this._splitter || !this._splitter.panes) { + return false; + } + const panes = this._splitter.panes; + return panes.indexOf(this) === panes.length - 1; + } + + private _renderBar() { + return html` + + `; + } + protected override render() { - return html` `; + return html` +
+ +
+ ${this._isLastPane ? nothing : this._renderBar()} + `; } } diff --git a/src/components/splitter/splitter.spec.ts b/src/components/splitter/splitter.spec.ts index 0a2037708..8385decf8 100644 --- a/src/components/splitter/splitter.spec.ts +++ b/src/components/splitter/splitter.spec.ts @@ -1,6 +1,6 @@ import { elementUpdated, expect, fixture, html } from '@open-wc/testing'; - import { defineComponents } from '../common/definitions/defineComponents.js'; +import type { SplitterOrientation } from '../types.js'; import IgcSplitterComponent from './splitter.js'; import IgcSplitterBarComponent from './splitter-bar.js'; import IgcSplitterPaneComponent from './splitter-pane.js'; @@ -16,11 +16,11 @@ describe('Splitter', () => { let splitter: IgcSplitterComponent; - describe('Rendering', () => { - beforeEach(async () => { - splitter = await fixture(createSplitter()); - }); + beforeEach(async () => { + splitter = await fixture(createSplitter()); + }); + describe('Rendering', () => { it('should render', () => { expect(splitter).to.exist; expect(splitter).to.be.instanceOf(IgcSplitterComponent); @@ -31,26 +31,19 @@ describe('Splitter', () => { await expect(splitter).shadowDom.to.be.accessible(); }); - it('should render panes and assign correct flex order', async () => { + it('should render a split bar for each splitter pane except the last one', async () => { await elementUpdated(splitter); expect(splitter.panes).to.have.lengthOf(3); - expect(splitter.panes[0].order).to.equal(0); - expect(splitter.panes[1].order).to.equal(2); - expect(splitter.panes[2].order).to.equal(4); - }); - - it('should render splitter bars and assign correct flex order', async () => { - await elementUpdated(splitter); - const bars = Array.from( - splitter.renderRoot.querySelectorAll(IgcSplitterBarComponent.tagName) - ); - + const bars = getSplitterBars(splitter); expect(bars).to.have.lengthOf(2); - expect(bars[0].order).to.equal(1); - expect(bars[1].order).to.equal(3); + bars.forEach((bar, index) => { + const pane = splitter.panes[index]; + const paneBase = getSplitterPaneBase(pane) as HTMLElement; + expect(bar.previousElementSibling).to.equal(paneBase); + }); }); it('should have default horizontal orientation', () => { @@ -76,11 +69,7 @@ describe('Splitter', () => { expect(nestedSplitter.panes).to.have.lengthOf(2); expect(nestedSplitter.orientation).to.equal('horizontal'); - const outerBars = Array.from( - nestedSplitter.renderRoot.querySelectorAll( - IgcSplitterBarComponent.tagName - ) - ); + const outerBars = getSplitterBars(nestedSplitter); expect(outerBars).to.have.lengthOf(1); const firstPane = nestedSplitter.panes[0]; @@ -93,11 +82,7 @@ describe('Splitter', () => { expect(leftSplitter.panes).to.have.lengthOf(2); - const leftBars = Array.from( - leftSplitter.renderRoot.querySelectorAll( - IgcSplitterBarComponent.tagName - ) - ); + const leftBars = getSplitterBars(leftSplitter); expect(leftBars).to.have.lengthOf(1); const secondPane = nestedSplitter.panes[1]; @@ -110,14 +95,184 @@ describe('Splitter', () => { expect(rightSplitter.panes).to.have.lengthOf(2); - const rightBars = Array.from( - rightSplitter.renderRoot.querySelectorAll( - IgcSplitterBarComponent.tagName - ) - ); + const rightBars = getSplitterBars(rightSplitter); expect(rightBars).to.have.lengthOf(1); }); }); + + describe('Properties', () => { + it('should set nonCollapsible property', async () => { + splitter.nonCollapsible = true; + await elementUpdated(splitter); + + expect(splitter.nonCollapsible).to.be.true; + expect(splitter.hasAttribute('non-collapsible')).to.be.true; + }); + + it('should reset pane sizes when orientation changes', async () => { + const pane = splitter.panes[0]; + pane.size = '200px'; + await elementUpdated(splitter); + + const base = getSplitterPaneBase(pane) as HTMLElement; + const style = getComputedStyle(base); + expect(style.flex).to.equal('0 0 200px'); + + splitter.orientation = 'vertical'; + await elementUpdated(splitter); + + expect(pane.size).to.equal('auto'); + }); + + it('should use default min/max values when not specified', async () => { + await elementUpdated(splitter); + + const pane = splitter.panes[0]; + const base = getSplitterPaneBase(pane) as HTMLElement; + const style = getComputedStyle(base); + expect(style.flex).to.equal('1 1 auto'); + + expect(pane.size).to.equal('auto'); + + expect(style.minWidth).to.equal('0px'); + expect(style.maxWidth).to.equal('100%'); + + splitter.orientation = 'vertical'; + await elementUpdated(splitter); + + expect(style.minHeight).to.equal('0px'); + expect(style.maxHeight).to.equal('100%'); + }); + + it('should apply minSize and maxSize to panes for horizontal orientation', async () => { + splitter = await fixture( + createTwoPanesWithSizesAndConstraints({ + minSize1: '100px', + maxSize1: '500px', + }) + ); + + await elementUpdated(splitter); + + const pane = splitter.panes[0]; + const base = getSplitterPaneBase(pane) as HTMLElement; + const style = getComputedStyle(base); + expect(style.minWidth).to.equal('100px'); + expect(style.maxWidth).to.equal('500px'); + }); + + it('should apply minSize and maxSize to panes for vertical orientation', async () => { + splitter = await fixture( + createTwoPanesWithSizesAndConstraints({ + minSize1: '100px', + maxSize1: '500px', + orientation: 'vertical', + }) + ); + await elementUpdated(splitter); + + const pane = splitter.panes[0]; + const base = getSplitterPaneBase(pane) as HTMLElement; + const style = getComputedStyle(base); + expect(style.minHeight).to.equal('100px'); + expect(style.maxHeight).to.equal('500px'); + }); + + it('should handle percentage sizes', async () => { + const splitter = await fixture( + createTwoPanesWithSizesAndConstraints({ + size1: '30%', + size2: '70%', + minSize1: '20%', + maxSize1: '80%', + }) + ); + await elementUpdated(splitter); + + const pane1 = splitter.panes[0]; + const base1 = getSplitterPaneBase(pane1) as HTMLElement; + const style1 = getComputedStyle(base1); + + const pane2 = splitter.panes[1]; + const base2 = getSplitterPaneBase(pane2) as HTMLElement; + const style2 = getComputedStyle(base2); + + expect(splitter.panes[0].size).to.equal('30%'); + expect(splitter.panes[1].size).to.equal('70%'); + expect(style1.flex).to.equal('1 1 30%'); + expect(style2.flex).to.equal('1 1 70%'); + + expect(pane1.minSize).to.equal('20%'); + expect(pane1.maxSize).to.equal('80%'); + expect(style1.minWidth).to.equal('20%'); + expect(style1.maxWidth).to.equal('80%'); + + // TODO: test with drag; add constraints to second pane + }); + + it('should handle mixed px and % constraints', async () => { + const mixedConstraintSplitter = await fixture( + createTwoPanesWithSizesAndConstraints({ + minSize1: '100px', + maxSize1: '50%', + }) + ); + await elementUpdated(mixedConstraintSplitter); + + const pane = mixedConstraintSplitter.panes[0]; + const base1 = getSplitterPaneBase(pane) as HTMLElement; + const style = getComputedStyle(base1); + + expect(pane.minSize).to.equal('100px'); + expect(pane.maxSize).to.equal('50%'); + expect(style.minWidth).to.equal('100px'); + expect(style.maxWidth).to.equal('50%'); + + // TODO: test with drag + }); + + it('should dynamically update when panes are added', async () => { + expect(splitter.panes).to.have.lengthOf(3); + + const newPane = document.createElement( + 'igc-splitter-pane' + ) as IgcSplitterPaneComponent; + newPane.textContent = 'New Pane'; + splitter.appendChild(newPane); + + await elementUpdated(splitter); + + expect(splitter.panes).to.have.lengthOf(4); + }); + + it('should dynamically update when panes are removed', async () => { + expect(splitter.panes).to.have.lengthOf(3); + + const paneToRemove = splitter.panes[1]; + paneToRemove.remove(); + + await elementUpdated(splitter); + + expect(splitter.panes).to.have.lengthOf(2); + }); + }); + + describe('Methods & Events', () => { + it('should expand/collapse panes when toggle is invoked', async () => { + const pane = splitter.panes[0]; + expect(pane.collapsed).to.be.false; + + pane.toggle(); + await elementUpdated(splitter); + + expect(pane.collapsed).to.be.true; + + pane.toggle(); + await elementUpdated(splitter); + + expect(pane.collapsed).to.be.false; + }); + }); }); function createSplitter() { @@ -148,3 +303,52 @@ function createNestedSplitter() { `; } + +type SplitterTestSizesAndConstraints = { + size1?: string; + size2?: string; + minSize1?: string; + maxSize1?: string; + minSize2?: string; + maxSize2?: string; + orientation?: SplitterOrientation; +}; + +function createTwoPanesWithSizesAndConstraints( + config: SplitterTestSizesAndConstraints +) { + return html` + + + Pane 1 + + + Pane 2 + + + `; +} + +function getSplitterPaneBase(pane: IgcSplitterPaneComponent) { + return pane.shadowRoot!.querySelector('div[part~="base"]'); +} + +function getSplitterBars(splitter: IgcSplitterComponent) { + const bars: IgcSplitterBarComponent[] = []; + + splitter.panes.forEach((pane) => { + const bar = pane.shadowRoot!.querySelector(IgcSplitterBarComponent.tagName); + if (bar) { + bars.push(bar); + } + }); + return bars; +} diff --git a/src/components/splitter/splitter.ts b/src/components/splitter/splitter.ts index e0ab42a5f..8c9433e88 100644 --- a/src/components/splitter/splitter.ts +++ b/src/components/splitter/splitter.ts @@ -1,11 +1,11 @@ +import { ContextProvider } from '@lit/context'; import { html, LitElement } from 'lit'; import { property, queryAssignedElements } from 'lit/decorators.js'; +import { splitterContext } from '../common/context.js'; import { addInternalsController } from '../common/controllers/internals.js'; -import { addSlotController } from '../common/controllers/slot.js'; import { watch } from '../common/decorators/watch.js'; import { registerComponent } from '../common/definitions/register.js'; import type { SplitterOrientation } from '../types.js'; -import IgcSplitterBarComponent from './splitter-bar.js'; import IgcSplitterPaneComponent from './splitter-pane.js'; import { styles } from './themes/splitter.base.css.js'; @@ -25,29 +25,25 @@ export default class IgcSplitterComponent extends LitElement { /* blazorSuppress */ public static register() { - registerComponent( - IgcSplitterComponent, - IgcSplitterPaneComponent, - IgcSplitterBarComponent - ); + registerComponent(IgcSplitterComponent, IgcSplitterPaneComponent); } //#region Properties - private readonly _slots = addSlotController(this, { - onChange: this._handleSlotChange, - }); - private readonly _internals = addInternalsController(this, { initialARIA: { ariaOrientation: 'horizontal', }, }); - /** Returns all of the splitter's panes. */ @queryAssignedElements({ selector: 'igc-splitter-pane' }) public panes!: Array; + private readonly _context = new ContextProvider(this, { + context: splitterContext, + initialValue: this, + }); + /** Gets/Sets the orientation of the splitter. * * @remarks @@ -62,218 +58,35 @@ export default class IgcSplitterComponent extends LitElement { * Default value is `false`. * @attr */ - @property({ type: Boolean, reflect: true }) + @property({ type: Boolean, attribute: 'non-collapsible', reflect: true }) public nonCollapsible = false; //#endregion //#region Internal API - @watch('orientation', { waitUntilFirstUpdate: true }) + @watch('orientation') protected _orientationChange(): void { this._internals.setARIA({ ariaOrientation: this.orientation }); - this._resetPaneSizes(); - this._initPanes(); + this._updateContext(); } - constructor() { - super(); - - this.addEventListener('sizeChanged', (event: any) => { - event.stopPropagation(); - this._initPanes(); - }); + @watch('panes') + @watch('nonCollapsible') + private _updateContext(): void { + this._context.setValue(this, true); + this.requestUpdate(); } - protected _handleSlotChange(): void { - this._initPanes(); - } - - private _assignFlexOrder() { - let k = 0; - this.panes.forEach((pane) => { - pane.order = k; - k += 2; - }); - } - - /** - * @hidden @internal - * This method inits panes with properties. - */ - private _initPanes() { - this.panes.forEach((pane) => { - pane.owner = this; - if (this.orientation === 'horizontal') { - pane.minWidth = pane.minSize ?? '0'; - pane.maxWidth = pane.maxSize ?? '100%'; - } else { - pane.minHeight = pane.minSize ?? '0'; - pane.maxHeight = pane.maxSize ?? '100%'; - } - }); - this._assignFlexOrder(); - //in igniteui-angular this is added as feature but i haven't checked why - // if (this.panes.filter(x => x.collapsed).length > 0) { - // // if any panes are collapsed, reset sizes. - // this.resetPaneSizes(); - // } - } - - /** - * @hidden @internal - * This method reset pane sizes. - */ - private _resetPaneSizes() { - if (this.panes) { - // if type is changed runtime, should reset sizes. - this.panes.forEach((pane) => { - pane.size = 'auto'; - pane.minWidth = '0'; - pane.maxWidth = '100%'; - pane.minHeight = '0'; - pane.maxHeight = '100%'; - }); - } - } - - private paneBefore!: IgcSplitterPaneComponent; - private paneAfter!: IgcSplitterPaneComponent; - private initialPaneBeforeSize!: number; - private initialPaneAfterSize!: number; - - private _handleMovingStart(event: CustomEvent) { - // Handle the moving start event - const panes = this.panes; - this.paneBefore = event.detail; - this.paneAfter = panes[panes.indexOf(this.paneBefore) + 1]; - - const paneRect = this.paneBefore.getBoundingClientRect(); - this.initialPaneBeforeSize = - this.orientation === 'horizontal' ? paneRect.width : paneRect.height; - - const siblingRect = this.paneAfter.getBoundingClientRect(); - this.initialPaneAfterSize = - this.orientation === 'horizontal' - ? siblingRect.width - : siblingRect.height; - } - private _handleMoving(event: CustomEvent) { - const [paneSize, siblingSize] = this.calcNewSizes(event.detail); - - this.paneBefore.size = paneSize + 'px'; - this.paneAfter.size = siblingSize + 'px'; - } - - //I am not sure if this code changes anything, it looks like it works without it as well, - // however I found a bug, which I am not sure how to reproduce it and still haven't encaountered it with this code - private _handleMovingEnd(event: CustomEvent) { - const [paneSize, siblingSize] = this.calcNewSizes(event.detail); - - if (this.paneBefore.isPercentageSize) { - // handle % resizes - const totalSize = this.getTotalSize(); - const percentPaneSize = (paneSize / totalSize) * 100; - this.paneBefore.size = percentPaneSize + '%'; - } else { - // px resize - this.paneBefore.size = paneSize + 'px'; - } - - if (this.paneAfter.isPercentageSize) { - // handle % resizes - const totalSize = this.getTotalSize(); - const percentSiblingPaneSize = (siblingSize / totalSize) * 100; - this.paneAfter.size = percentSiblingPaneSize + '%'; - } else { - // px resize - this.paneAfter.size = siblingSize + 'px'; - } - } - - /** - * @hidden @internal - * Calculates new sizes for the panes based on move delta and initial sizes - */ - private calcNewSizes(delta: number): [number, number] { - let finalDelta: number; - const min = - Number.parseInt( - this.paneBefore.minSize ? this.paneBefore.minSize : '0', - 10 - ) || 0; - const minSibling = - Number.parseInt( - this.paneAfter.minSize ? this.paneAfter.minSize : '0', - 10 - ) || 0; - const max = - Number.parseInt( - this.paneBefore.maxSize ? this.paneBefore.maxSize : '0', - 10 - ) || this.initialPaneBeforeSize + this.initialPaneAfterSize - minSibling; - const maxSibling = - Number.parseInt( - this.paneAfter.maxSize ? this.paneAfter.maxSize : '0', - 10 - ) || this.initialPaneBeforeSize + this.initialPaneAfterSize - min; - - if (delta < 0) { - const maxPossibleDelta = Math.min( - this.initialPaneBeforeSize - min, - maxSibling - this.initialPaneAfterSize - ); - finalDelta = Math.min(maxPossibleDelta, Math.abs(delta)) * -1; - } else { - const maxPossibleDelta = Math.min( - max - this.initialPaneBeforeSize, - this.initialPaneAfterSize - minSibling - ); - finalDelta = Math.min(maxPossibleDelta, Math.abs(delta)); - } - return [ - this.initialPaneBeforeSize + finalDelta, - this.initialPaneAfterSize - finalDelta, - ]; - } - - private getTotalSize() { - const computed = document.defaultView?.getComputedStyle(this); - const totalSize = - this.orientation === 'horizontal' - ? computed?.getPropertyValue('width') - : computed?.getPropertyValue('height'); - return Number.parseFloat(totalSize ? totalSize : '0'); - } //#endregion - //#region Rendering - - private _renderBar(order: number, i: number) { - return html` - - `; - } - protected override render() { return html` - - ${this.panes.map((pane, i) => { - const isLast = i === this.panes.length - 1; - return html`${!isLast ? this._renderBar(pane.order + 1, i) : ''}`; - })} +
+ +
`; } - - //#endregion } declare global { diff --git a/src/components/splitter/themes/splitter-bar.base.scss b/src/components/splitter/themes/splitter-bar.base.scss new file mode 100644 index 000000000..a3a5b1ba6 --- /dev/null +++ b/src/components/splitter/themes/splitter-bar.base.scss @@ -0,0 +1,19 @@ +@use 'styles/common/component'; + +:host { + display: flex; + background-color: var(--ig-gray-200); + + &:hover { + background-color: var(--ig-gray-400); + } + + [part='base'] { + cursor: var(--cursor, default); + } +} + +[part='expander-start'], +[part='expander-end'] { + cursor: pointer; +} diff --git a/src/components/splitter/themes/splitter-pane.scss b/src/components/splitter/themes/splitter-pane.scss new file mode 100644 index 000000000..24abc1e48 --- /dev/null +++ b/src/components/splitter/themes/splitter-pane.scss @@ -0,0 +1,17 @@ +@use 'styles/common/component'; + +:host { + [part='base'] { + overflow: auto; + } + + display: flex; + flex: 1 1 auto; + width: 100%; + height: 100%; +} + + +:host([collapsed]) [part='base'] { + display: none; +} diff --git a/src/components/splitter/themes/splitter.base.scss b/src/components/splitter/themes/splitter.base.scss index eabf2ed9b..49a1397e0 100644 --- a/src/components/splitter/themes/splitter.base.scss +++ b/src/components/splitter/themes/splitter.base.scss @@ -2,51 +2,33 @@ @use 'styles/utilities' as *; :host { - display: flex; - width: 100%; - height: 100%; - color: var(--ig-gray-900); - background: var(--ig-gray-100); - border: 1px solid var(--ig-gray-200); - - ::slotted(igc-splitter-pane) { - flex: 1 1 0; - overflow: auto; + [part='base'] { + width: 100%; + height: 100%; + display: flex; + color: var(--ig-gray-900); + background: var(--ig-gray-100); + border: 1px solid var(--ig-gray-200); } -} -:host([orientation='horizontal']) { - flex-direction: row; - - igc-splitter-bar { - width: 5px; - height: auto; - cursor: col-resize; + ::slotted(igc-splitter-pane) { + width: 100%; + height: 100%; + display: contents; } ::slotted(igc-splitter-pane[collapsed]) { - display: none; + height: 0; + width: 0; } } :host([orientation='vertical']) { - flex-direction: column; - - igc-splitter-bar { - height: 5px; - width: auto; - cursor: row-resize; + [part='base'] { + flex-direction: column; } - ::slotted(igc-splitter-pane[collapsed]) { - display: none; - } -} - -igc-splitter-bar { - background-color: var(--ig-gray-200); - - &:hover { - background-color: var(--ig-gray-400); + ::slotted(igc-splitter-pane) { + flex-direction: column; } } diff --git a/src/index.ts b/src/index.ts index 9a191cccf..69494757e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -67,7 +67,7 @@ export { default as IgcTextareaComponent } from './components/textarea/textarea. export { default as IgcTreeComponent } from './components/tree/tree.js'; export { default as IgcTreeItemComponent } from './components/tree/tree-item.js'; export { default as IgcSplitterBarComponent } from './components/splitter/splitter-bar.js'; -export { default as IgcSpltterComponent } from './components/splitter/splitter.js'; +export { default as IgcSplitterComponent } from './components/splitter/splitter.js'; export { default as IgcSplitterPaneComponent } from './components/splitter/splitter-pane.js'; export { default as IgcStepperComponent } from './components/stepper/stepper.js'; export { default as IgcStepComponent } from './components/stepper/step.js'; diff --git a/stories/splitter.stories.ts b/stories/splitter.stories.ts index 02d2cf9fd..c1dc91b8e 100644 --- a/stories/splitter.stories.ts +++ b/stories/splitter.stories.ts @@ -144,7 +144,11 @@ export default metadata; type Story = StoryObj; function changePaneMinMaxSizes() { - const panes = document.querySelectorAll('igc-splitter-pane'); + const splitter = document.querySelector('igc-splitter'); + const panes = splitter?.panes; + if (!panes) { + return; + } panes[0].minSize = '100px'; panes[0].maxSize = '200px'; panes[1].minSize = '50px'; @@ -179,9 +183,7 @@ export const Default: Story = { } .splitters { - display: flex; - flex-direction: column; - gap: 40px; + height: 400px; } @@ -189,7 +191,6 @@ export const Default: Story = { From 33d0b788d0676cbec6b61329272dcb462d44c760 Mon Sep 17 00:00:00 2001 From: Bozhidara Pachilova Date: Mon, 10 Nov 2025 11:09:57 +0200 Subject: [PATCH 10/24] fix(splitter): make resize work after changes; add updateTarget option for resize controller --- .../resize-container/resize-controller.ts | 9 +++++++-- src/components/resize-container/types.ts | 1 + src/components/splitter/splitter-bar.ts | 10 +++++++++- src/components/splitter/themes/splitter.base.scss | 4 ++++ stories/splitter.stories.ts | 13 ++++++++----- 5 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/components/resize-container/resize-controller.ts b/src/components/resize-container/resize-controller.ts index dd5636347..9f030189d 100644 --- a/src/components/resize-container/resize-controller.ts +++ b/src/components/resize-container/resize-controller.ts @@ -21,6 +21,7 @@ class ResizeController implements ReactiveController { private readonly _options: ResizeControllerConfiguration = { enabled: true, + updateTarget: true, layer: getDefaultLayer, }; @@ -166,7 +167,9 @@ class ResizeController implements ReactiveController { const parameters = { event, state: this._stateParameters }; this._options.resize?.call(this._host, parameters); this._state.current = parameters.state.current; - this._updatePosition(this._isDeferred ? this._ghost : this._resizeTarget); + if (this._options.updateTarget) { + this._updatePosition(this._isDeferred ? this._ghost : this._resizeTarget); + } } private _handlePointerEnd(event: PointerEvent): void { @@ -175,7 +178,9 @@ class ResizeController implements ReactiveController { this._options.end?.call(this._host, parameters); this._state.current = parameters.state.current; - parameters.state.commit?.() ?? this._updatePosition(this._resizeTarget); + if (this._options.updateTarget) { + parameters.state.commit?.() ?? this._updatePosition(this._resizeTarget); + } this.dispose(); } diff --git a/src/components/resize-container/types.ts b/src/components/resize-container/types.ts index 29dc520d8..fefae198f 100644 --- a/src/components/resize-container/types.ts +++ b/src/components/resize-container/types.ts @@ -24,6 +24,7 @@ export type ResizeControllerConfiguration = { enabled?: boolean; ref?: Ref[]; mode?: ResizeMode; + updateTarget?: boolean; deferredFactory?: ResizeGhostFactory; layer?: () => HTMLElement; /** Callback invoked at the start of a resize operation. */ diff --git a/src/components/splitter/splitter-bar.ts b/src/components/splitter/splitter-bar.ts index be8efda0c..a5f1ca537 100644 --- a/src/components/splitter/splitter-bar.ts +++ b/src/components/splitter/splitter-bar.ts @@ -95,7 +95,15 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< super(); addResizeController(this, { mode: 'immediate', - resizeTarget: (): HTMLElement => this._siblingPanes[0] ?? this, // we don’t resize the bar, we just use the delta + updateTarget: false, + resizeTarget: () => { + // we don’t resize the bar, we just use the delta + const pane = this._siblingPanes[0]; + return ( + (pane?.shadowRoot?.querySelector('[part="base"]') as HTMLElement) ?? + this + ); + }, start: () => { if ( !this._siblingPanes[0]?.resizable || diff --git a/src/components/splitter/themes/splitter.base.scss b/src/components/splitter/themes/splitter.base.scss index 49a1397e0..15ae54ab2 100644 --- a/src/components/splitter/themes/splitter.base.scss +++ b/src/components/splitter/themes/splitter.base.scss @@ -2,6 +2,10 @@ @use 'styles/utilities' as *; :host { + display: flex; + width: 100%; + height: 100%; + [part='base'] { width: 100%; height: 100%; diff --git a/stories/splitter.stories.ts b/stories/splitter.stories.ts index c1dc91b8e..cd6774506 100644 --- a/stories/splitter.stories.ts +++ b/stories/splitter.stories.ts @@ -149,12 +149,12 @@ function changePaneMinMaxSizes() { if (!panes) { return; } - panes[0].minSize = '100px'; + panes[0].minSize = '50px'; panes[0].maxSize = '200px'; - panes[1].minSize = '50px'; - panes[1].maxSize = '100px'; + panes[1].minSize = '100px'; + panes[1].maxSize = '300px'; panes[2].minSize = '150px'; - panes[2].maxSize = '100px'; + panes[2].maxSize = '450px'; } export const Default: Story = { @@ -234,12 +234,15 @@ export const NestedSplitters: Story = { argTypes: disableStoryControls(metadata), render: () => html` - + From edee68fdac25b689e9f08fc3c10e7a623bf992dc Mon Sep 17 00:00:00 2001 From: Bozhidara Pachilova Date: Mon, 10 Nov 2025 12:28:07 +0200 Subject: [PATCH 11/24] chore: style splitter bar through horizontal/vertical part --- src/components/splitter/splitter-bar.ts | 44 +++++++++---------- .../splitter/themes/splitter-bar.base.scss | 37 +++++++++++++++- 2 files changed, 56 insertions(+), 25 deletions(-) diff --git a/src/components/splitter/splitter-bar.ts b/src/components/splitter/splitter-bar.ts index a5f1ca537..93f9f58cd 100644 --- a/src/components/splitter/splitter-bar.ts +++ b/src/components/splitter/splitter-bar.ts @@ -7,6 +7,7 @@ import { createMutationController } from '../common/controllers/mutation-observe import { registerComponent } from '../common/definitions/register.js'; import type { Constructor } from '../common/mixins/constructor.js'; import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; +import { partMap } from '../common/part-map.js'; import { addResizeController } from '../resize-container/resize-controller.js'; import type { SplitterOrientation } from '../types.js'; import type IgcSplitterComponent from './splitter.js'; @@ -44,8 +45,18 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< }, }); - private _internalStyles: StyleInfo = {}; - private _orientation?: SplitterOrientation; + protected _resolvePartNames() { + return { + base: true, + [this._orientation.toString()]: true, + }; + } + + private _internalStyles: StyleInfo = { + '--cursor': this._cursor, + }; + + private _orientation: SplitterOrientation = 'horizontal'; private _splitter?: IgcSplitterComponent; private get _siblingPanes(): Array { @@ -65,16 +76,6 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< return [currentPane, nextPane]; } - private get _styles(): StyleInfo { - return { - display: 'flex', - flexDirection: this._orientation === 'horizontal' ? 'column' : 'row', - width: this._orientation === 'horizontal' ? '5px' : '100%', - height: this._orientation === 'horizontal' ? '100%' : '5px', - '--cursor': this._cursor, - }; - } - private get _resizeDisallowed() { return !!this._siblingPanes.find( (x) => x && (x.resizable === false || x.collapsed === true) @@ -160,7 +161,7 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< if (this._orientation !== splitter.orientation) { this._orientation = splitter.orientation; this._internals.setARIA({ ariaOrientation: this._orientation }); - Object.assign(this._internalStyles, this._styles); + Object.assign(this._internalStyles, { cursor: this._cursor }); } } @@ -172,23 +173,18 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< const prevButtonHidden = siblings[0]?.collapsed && !siblings[1]?.collapsed; const nextButtonHidden = siblings[1]?.collapsed && !siblings[0]?.collapsed; return html` -
+
-
+
`; } protected override render() { return html` -
+
${this._renderBarControls()}
`; diff --git a/src/components/splitter/themes/splitter-bar.base.scss b/src/components/splitter/themes/splitter-bar.base.scss index a3a5b1ba6..655d30817 100644 --- a/src/components/splitter/themes/splitter-bar.base.scss +++ b/src/components/splitter/themes/splitter-bar.base.scss @@ -8,12 +8,47 @@ background-color: var(--ig-gray-400); } - [part='base'] { + [part~='base'] { + display: flex; cursor: var(--cursor, default); } + + [part='base horizontal'] { + [part='handle'] { + height: 50px; + } + + flex-direction: column; + width: 5px; + height: 100%; + } + + [part='base vertical'] { + [part='handle'] { + width: 50px; + } + + flex-direction: row; + width: 100%; + height: 5px; + } } [part='expander-start'], [part='expander-end'] { cursor: pointer; + width: 5px; + height: 5px; +} + +[part='expander-start'] { + background-color: red; +} + +[part='expander-end'] { + background-color: green; +} + +[part='handle'] { + background-color: yellow; } From b7b4e8d4cc562cd32c24d412d7badba4159167c2 Mon Sep 17 00:00:00 2001 From: MonikaKirkova Date: Wed, 12 Nov 2025 11:02:04 +0200 Subject: [PATCH 12/24] fix(splitter): modify flex prop to allow different sizes --- src/components/splitter/splitter-pane.ts | 112 +++++++++++++----- .../splitter/themes/splitter.base.scss | 1 + 2 files changed, 84 insertions(+), 29 deletions(-) diff --git a/src/components/splitter/splitter-pane.ts b/src/components/splitter/splitter-pane.ts index 649f91816..e08018aa5 100644 --- a/src/components/splitter/splitter-pane.ts +++ b/src/components/splitter/splitter-pane.ts @@ -44,9 +44,24 @@ export default class IgcSplitterPaneComponent extends LitElement { private get _flex() { //const size = this.dragSize || this.size; //const grow = this.isPercentageSize && !this.dragSize ? 1 : 0; - const grow = this._isPercentageSize ? 1 : 0; - return `${grow} ${grow} ${this._size}`; - //return `${0} ${0} ${this.size}`; + + //tova ne raboti ako setnem procent na nqkoi pane a ako e dolnoto se mesti malko po-malko nadqsno vupreki che go handelvam + // const grow = this._isPercentageSize ? 1 : 0; + // return `${grow} ${grow} ${this._size}`; + + // Flex rules: + // - Explicit percentage (e.g., 30%): fixed at that size => flex: 0 0 + // - Explicit px (e.g., 200px): fixed => flex: 0 0 + // - Auto: participates in remaining space => flex: 1 1 0px + if (this._size === 'auto') { + return '1 1 0px'; + // } if (this._isPercentageSize) { + // const basis = this._isLastPane + // ? this._size // last pane has no internal bar after it + // : `calc(${this._size} - 5px)`; + // return `0 0 ${`calc(${this._size} - 3px)`}`; + } + return `0 0 ${this._size}`; } /** @@ -106,6 +121,12 @@ export default class IgcSplitterPaneComponent extends LitElement { */ @property({ type: Boolean, reflect: true }) public set collapsed(value: boolean) { + if (this._splitter) { + // reset sibling sizes when pane collapse state changes. + this._splitter.panes.forEach((pane) => { + pane.size = 'auto'; + }); + } this._collapsed = value; } @@ -176,6 +197,10 @@ export default class IgcSplitterPaneComponent extends LitElement { this.paneBefore = this; this.paneAfter = panes[panes.indexOf(this) + 1]; + // Normalize any 'auto' pane sizes to explicit percentages so flex redistribution + // does not modify unaffected panes when only a subset gets pixel / percent updates. + //this._normalizeAutoPaneSizes(); + // Store original size types before we start changing them this.isPaneBeforePercentage = this.paneBefore._isPercentageSize; this.isPaneAfterPercentage = this.paneAfter._isPercentageSize; @@ -208,9 +233,8 @@ export default class IgcSplitterPaneComponent extends LitElement { this.paneAfter.size = `${siblingSize}px`; } - //I am not sure if this code changes anything, it looks like it works without it as well, - // however I found a bug, which I am not sure how to reproduce it and still haven't encaountered it with this code private _handleMovingEnd(event: CustomEvent) { + let last = false; // Only handle if this pane owns the bar (is the one before the bar) if (!this.paneBefore || this.paneBefore !== this) { return; @@ -220,9 +244,11 @@ export default class IgcSplitterPaneComponent extends LitElement { if (this.isPaneBeforePercentage) { // handle % resizes - const totalSize = this.getTotalSize(); - const percentPaneSize = (paneSize / totalSize) * 100; - this.paneBefore.size = `${percentPaneSize}%`; + last = this.paneBefore._isLastPane; + + this._convertSizeToPercentage(this.paneBefore, last); + + //this._convertSizeToPercentage(this.paneBefore, false); } else { // px resize this.paneBefore.size = `${paneSize}px`; @@ -230,9 +256,10 @@ export default class IgcSplitterPaneComponent extends LitElement { if (this.isPaneAfterPercentage) { // handle % resizes - const totalSize = this.getTotalSize(); - const percentSiblingPaneSize = (siblingSize / totalSize) * 100; - this.paneAfter.size = `${percentSiblingPaneSize}%`; + last = this.paneAfter._isLastPane; + this._convertSizeToPercentage(this.paneAfter, last); + + //this._convertSizeToPercentage(this.paneAfter, false); } else { // px resize this.paneAfter.size = `${siblingSize}px`; @@ -241,26 +268,14 @@ export default class IgcSplitterPaneComponent extends LitElement { private _calcNewSizes(delta: number): [number, number] { let finalDelta: number; - const min = - Number.parseInt( - this.paneBefore.minSize ? this.paneBefore.minSize : '0', - 10 - ) || 0; - const minSibling = - Number.parseInt( - this.paneAfter.minSize ? this.paneAfter.minSize : '0', - 10 - ) || 0; + const min = Number.parseInt(this.paneBefore.minSize ?? '0', 10) || 0; + const minSibling = Number.parseInt(this.paneAfter.minSize ?? '0', 10) || 0; const max = - Number.parseInt( - this.paneBefore.maxSize ? this.paneBefore.maxSize : '0', - 10 - ) || this.initialPaneBeforeSize + this.initialPaneAfterSize - minSibling; + Number.parseInt(this.paneBefore.maxSize ?? '0', 10) || + this.initialPaneBeforeSize + this.initialPaneAfterSize - minSibling; const maxSibling = - Number.parseInt( - this.paneAfter.maxSize ? this.paneAfter.maxSize : '0', - 10 - ) || this.initialPaneBeforeSize + this.initialPaneAfterSize - min; + Number.parseInt(this.paneAfter.maxSize ?? '0', 10) || + this.initialPaneBeforeSize + this.initialPaneAfterSize - min; if (delta < 0) { const maxPossibleDelta = Math.min( @@ -295,6 +310,45 @@ export default class IgcSplitterPaneComponent extends LitElement { return this._orientation === 'horizontal' ? rect.width : rect.height; } + /** Converts all panes with size 'auto' to explicit percentage sizes based on their current rendered size. */ + private _normalizeAutoPaneSizes() { + if (!this._splitter || !this._splitter.panes.length) { + return; + } + for (const pane of this._splitter.panes) { + if (pane.size === 'auto') { + if (this._isLastPane) { + this._convertSizeToPercentage(pane, true); + } else { + this._convertSizeToPercentage(pane, false); + } + } + } + } + + private _convertSizeToPercentage( + pane: IgcSplitterPaneComponent, + last: boolean + ) { + const base = pane.shadowRoot?.querySelector('[part="base"]'); + if (!base) { + return; + } + const rect = base.getBoundingClientRect(); + let currentSize = + this._orientation === 'horizontal' ? rect.width : rect.height; + if (!last) { + currentSize += 5; + } + const visual = last ? currentSize : currentSize; + const totalSize = this.getTotalSize(); + const percentSize = (visual / totalSize) * 100; + // if (!last) { + // percentSize += (5 / totalSize) * 100; + // } + pane.size = `${percentSize.toFixed(3)}%`; + } + /** Toggles the collapsed state of the pane. */ public toggle() { this.collapsed = !this.collapsed; diff --git a/src/components/splitter/themes/splitter.base.scss b/src/components/splitter/themes/splitter.base.scss index 15ae54ab2..6df7c0b01 100644 --- a/src/components/splitter/themes/splitter.base.scss +++ b/src/components/splitter/themes/splitter.base.scss @@ -13,6 +13,7 @@ color: var(--ig-gray-900); background: var(--ig-gray-100); border: 1px solid var(--ig-gray-200); + user-select: none; } ::slotted(igc-splitter-pane) { From a05d121028e4802dcdc0d81ed84a4122be343116 Mon Sep 17 00:00:00 2001 From: MonikaKirkova Date: Wed, 12 Nov 2025 11:23:36 +0200 Subject: [PATCH 13/24] fix(splitter): revert changes from previous commit --- src/components/splitter/splitter-pane.ts | 78 +++--------------------- 1 file changed, 8 insertions(+), 70 deletions(-) diff --git a/src/components/splitter/splitter-pane.ts b/src/components/splitter/splitter-pane.ts index e08018aa5..4015c71ea 100644 --- a/src/components/splitter/splitter-pane.ts +++ b/src/components/splitter/splitter-pane.ts @@ -45,23 +45,8 @@ export default class IgcSplitterPaneComponent extends LitElement { //const size = this.dragSize || this.size; //const grow = this.isPercentageSize && !this.dragSize ? 1 : 0; - //tova ne raboti ako setnem procent na nqkoi pane a ako e dolnoto se mesti malko po-malko nadqsno vupreki che go handelvam - // const grow = this._isPercentageSize ? 1 : 0; - // return `${grow} ${grow} ${this._size}`; - - // Flex rules: - // - Explicit percentage (e.g., 30%): fixed at that size => flex: 0 0 - // - Explicit px (e.g., 200px): fixed => flex: 0 0 - // - Auto: participates in remaining space => flex: 1 1 0px - if (this._size === 'auto') { - return '1 1 0px'; - // } if (this._isPercentageSize) { - // const basis = this._isLastPane - // ? this._size // last pane has no internal bar after it - // : `calc(${this._size} - 5px)`; - // return `0 0 ${`calc(${this._size} - 3px)`}`; - } - return `0 0 ${this._size}`; + const grow = this._isPercentageSize ? 1 : 0; + return `${grow} ${grow} ${this._size}`; } /** @@ -197,10 +182,6 @@ export default class IgcSplitterPaneComponent extends LitElement { this.paneBefore = this; this.paneAfter = panes[panes.indexOf(this) + 1]; - // Normalize any 'auto' pane sizes to explicit percentages so flex redistribution - // does not modify unaffected panes when only a subset gets pixel / percent updates. - //this._normalizeAutoPaneSizes(); - // Store original size types before we start changing them this.isPaneBeforePercentage = this.paneBefore._isPercentageSize; this.isPaneAfterPercentage = this.paneAfter._isPercentageSize; @@ -234,7 +215,6 @@ export default class IgcSplitterPaneComponent extends LitElement { } private _handleMovingEnd(event: CustomEvent) { - let last = false; // Only handle if this pane owns the bar (is the one before the bar) if (!this.paneBefore || this.paneBefore !== this) { return; @@ -244,11 +224,9 @@ export default class IgcSplitterPaneComponent extends LitElement { if (this.isPaneBeforePercentage) { // handle % resizes - last = this.paneBefore._isLastPane; - - this._convertSizeToPercentage(this.paneBefore, last); - - //this._convertSizeToPercentage(this.paneBefore, false); + const totalSize = this.getTotalSize(); + const percentPaneSize = (paneSize / totalSize) * 100; + this.paneBefore.size = `${percentPaneSize}%`; } else { // px resize this.paneBefore.size = `${paneSize}px`; @@ -256,10 +234,9 @@ export default class IgcSplitterPaneComponent extends LitElement { if (this.isPaneAfterPercentage) { // handle % resizes - last = this.paneAfter._isLastPane; - this._convertSizeToPercentage(this.paneAfter, last); - - //this._convertSizeToPercentage(this.paneAfter, false); + const totalSize = this.getTotalSize(); + const percentPaneSize = (paneSize / totalSize) * 100; + this.paneBefore.size = `${percentPaneSize}%`; } else { // px resize this.paneAfter.size = `${siblingSize}px`; @@ -310,45 +287,6 @@ export default class IgcSplitterPaneComponent extends LitElement { return this._orientation === 'horizontal' ? rect.width : rect.height; } - /** Converts all panes with size 'auto' to explicit percentage sizes based on their current rendered size. */ - private _normalizeAutoPaneSizes() { - if (!this._splitter || !this._splitter.panes.length) { - return; - } - for (const pane of this._splitter.panes) { - if (pane.size === 'auto') { - if (this._isLastPane) { - this._convertSizeToPercentage(pane, true); - } else { - this._convertSizeToPercentage(pane, false); - } - } - } - } - - private _convertSizeToPercentage( - pane: IgcSplitterPaneComponent, - last: boolean - ) { - const base = pane.shadowRoot?.querySelector('[part="base"]'); - if (!base) { - return; - } - const rect = base.getBoundingClientRect(); - let currentSize = - this._orientation === 'horizontal' ? rect.width : rect.height; - if (!last) { - currentSize += 5; - } - const visual = last ? currentSize : currentSize; - const totalSize = this.getTotalSize(); - const percentSize = (visual / totalSize) * 100; - // if (!last) { - // percentSize += (5 / totalSize) * 100; - // } - pane.size = `${percentSize.toFixed(3)}%`; - } - /** Toggles the collapsed state of the pane. */ public toggle() { this.collapsed = !this.collapsed; From 59b3285c45beca8edf64cfcb012965bb054a1f3f Mon Sep 17 00:00:00 2001 From: Bozhidara Pachilova Date: Wed, 12 Nov 2025 12:37:43 +0200 Subject: [PATCH 14/24] chore: style tweaks; more tests; resize fix --- src/components/splitter/splitter-bar.ts | 39 ++++- src/components/splitter/splitter-pane.ts | 158 ++++++++++-------- src/components/splitter/splitter.spec.ts | 128 +++++++++++++- .../splitter/themes/splitter-bar.base.scss | 6 +- .../splitter/themes/splitter-pane.scss | 1 + .../splitter/themes/splitter.base.scss | 3 - stories/splitter.stories.ts | 5 +- 7 files changed, 249 insertions(+), 91 deletions(-) diff --git a/src/components/splitter/splitter-bar.ts b/src/components/splitter/splitter-bar.ts index 93f9f58cd..43e32c61c 100644 --- a/src/components/splitter/splitter-bar.ts +++ b/src/components/splitter/splitter-bar.ts @@ -52,9 +52,7 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< }; } - private _internalStyles: StyleInfo = { - '--cursor': this._cursor, - }; + private _internalStyles: StyleInfo = {}; private _orientation: SplitterOrientation = 'horizontal'; private _splitter?: IgcSplitterComponent; @@ -94,6 +92,8 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< constructor() { super(); + this._internalStyles = { '--cursor': this._cursor }; + addResizeController(this, { mode: 'immediate', updateTarget: false, @@ -146,6 +146,7 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< private _createSiblingPaneMutationController(pane: IgcSplitterPaneComponent) { createMutationController(pane, { callback: () => { + Object.assign(this._internalStyles, { '--cursor': this._cursor }); this.requestUpdate(); }, filter: [IgcSplitterPaneComponent.tagName], @@ -161,8 +162,26 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< if (this._orientation !== splitter.orientation) { this._orientation = splitter.orientation; this._internals.setARIA({ ariaOrientation: this._orientation }); - Object.assign(this._internalStyles, { cursor: this._cursor }); + Object.assign(this._internalStyles, { '--cursor': this._cursor }); + } + this.requestUpdate(); + } + + private _handleExpanderClick(start: boolean, event: PointerEvent) { + // Prevent resize controller from starting + event.stopPropagation(); + + const prevSibling = this._siblingPanes[0]!; + const nextSibling = this._siblingPanes[1]!; + let target: IgcSplitterPaneComponent; + if (start) { + // if next is clicked when prev pane is hidden, show prev pane, else hide next pane. + target = prevSibling.collapsed ? prevSibling : nextSibling; + } else { + // if prev is clicked when next pane is hidden, show next pane, else hide prev pane. + target = nextSibling.collapsed ? nextSibling : prevSibling; } + target.toggle(); } private _renderBarControls() { @@ -173,9 +192,17 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< const prevButtonHidden = siblings[0]?.collapsed && !siblings[1]?.collapsed; const nextButtonHidden = siblings[1]?.collapsed && !siblings[0]?.collapsed; return html` -
+
this._handleExpanderClick(true, e)} + >
-
+
this._handleExpanderClick(false, e)} + >
`; } diff --git a/src/components/splitter/splitter-pane.ts b/src/components/splitter/splitter-pane.ts index 4015c71ea..f4e2f0623 100644 --- a/src/components/splitter/splitter-pane.ts +++ b/src/components/splitter/splitter-pane.ts @@ -1,6 +1,6 @@ import { ContextConsumer } from '@lit/context'; import { html, LitElement, nothing } from 'lit'; -import { property } from 'lit/decorators.js'; +import { property, query } from 'lit/decorators.js'; import { type StyleInfo, styleMap } from 'lit/directives/style-map.js'; import { splitterContext } from '../common/context.js'; import { registerComponent } from '../common/definitions/register.js'; @@ -33,6 +33,16 @@ export default class IgcSplitterPaneComponent extends LitElement { private _collapsed = false; private _orientation?: SplitterOrientation; + private _prevPane!: IgcSplitterPaneComponent; + private _nextPane!: IgcSplitterPaneComponent; + private _prevPaneInitialSize!: number; + private _nextPaneInitialSize!: number; + private _isPrevPanePercentage = false; + private _isNextPanePercentage = false; + + @query('[part~="base"]', true) + private readonly _base!: HTMLElement; + private get _isPercentageSize() { return this._size === 'auto' || this._size.indexOf('%') !== -1; } @@ -42,13 +52,17 @@ export default class IgcSplitterPaneComponent extends LitElement { } private get _flex() { - //const size = this.dragSize || this.size; - //const grow = this.isPercentageSize && !this.dragSize ? 1 : 0; - const grow = this._isPercentageSize ? 1 : 0; return `${grow} ${grow} ${this._size}`; } + private get _rectSize() { + const relevantDimension = + this._orientation === 'horizontal' ? 'width' : 'height'; + const paneRect = this._base.getBoundingClientRect(); + return paneRect[relevantDimension]; + } + /** * The minimum size of the pane. * @attr @@ -164,112 +178,97 @@ export default class IgcSplitterPaneComponent extends LitElement { this.requestUpdate(); } - private paneBefore!: IgcSplitterPaneComponent; - private paneAfter!: IgcSplitterPaneComponent; - private initialPaneBeforeSize!: number; - private initialPaneAfterSize!: number; - private isPaneBeforePercentage = false; - private isPaneAfterPercentage = false; - private _handleMovingStart(event: CustomEvent) { - // Only handle if this is the pane that owns the bar if (event.detail !== this) { return; } - // Handle the moving start event const panes = this._splitter!.panes; - this.paneBefore = this; - this.paneAfter = panes[panes.indexOf(this) + 1]; + this._prevPane = this; + this._nextPane = panes[panes.indexOf(this) + 1]; // Store original size types before we start changing them - this.isPaneBeforePercentage = this.paneBefore._isPercentageSize; - this.isPaneAfterPercentage = this.paneAfter._isPercentageSize; - - const paneBeforeBase = - this.paneBefore.shadowRoot?.querySelector('[part="base"]'); - const paneAfterBase = - this.paneAfter.shadowRoot?.querySelector('[part="base"]'); - - const paneRect = paneBeforeBase!.getBoundingClientRect(); - this.initialPaneBeforeSize = - this._orientation === 'horizontal' ? paneRect.width : paneRect.height; - - const siblingRect = paneAfterBase!.getBoundingClientRect(); - this.initialPaneAfterSize = - this._orientation === 'horizontal' - ? siblingRect.width - : siblingRect.height; + this._isPrevPanePercentage = this._prevPane._isPercentageSize; + this._isNextPanePercentage = this._nextPane._isPercentageSize; + + this._prevPaneInitialSize = this._rectSize; + this._nextPaneInitialSize = this._nextPane._rectSize; } private _handleMoving(event: CustomEvent) { - // Only handle if this pane owns the bar (is the one before the bar) - if (!this.paneBefore || this.paneBefore !== this) { - return; - } - const [paneSize, siblingSize] = this._calcNewSizes(event.detail); - this.paneBefore.size = `${paneSize}px`; - this.paneAfter.size = `${siblingSize}px`; + this._prevPane.size = `${paneSize}px`; + this._nextPane.size = `${siblingSize}px`; } private _handleMovingEnd(event: CustomEvent) { - // Only handle if this pane owns the bar (is the one before the bar) - if (!this.paneBefore || this.paneBefore !== this) { - return; - } - const [paneSize, siblingSize] = this._calcNewSizes(event.detail); - if (this.isPaneBeforePercentage) { - // handle % resizes - const totalSize = this.getTotalSize(); - const percentPaneSize = (paneSize / totalSize) * 100; - this.paneBefore.size = `${percentPaneSize}%`; - } else { - // px resize - this.paneBefore.size = `${paneSize}px`; - } + const totalSize = this.getTotalSize(); + + this._adjustPaneSize( + this._prevPane, + this._isPrevPanePercentage, + paneSize, + totalSize + ); + this._adjustPaneSize( + this._nextPane, + this._isNextPanePercentage, + siblingSize, + totalSize + ); + + this._splitter!.panes.filter( + (pane) => pane !== this && pane !== this._nextPane + ).forEach((pane) => { + const size = pane._rectSize; + this._adjustPaneSize(pane, pane._isPercentageSize, size, totalSize); + }); + } - if (this.isPaneAfterPercentage) { - // handle % resizes - const totalSize = this.getTotalSize(); - const percentPaneSize = (paneSize / totalSize) * 100; - this.paneBefore.size = `${percentPaneSize}%`; + private _adjustPaneSize( + pane: IgcSplitterPaneComponent, + isPercent: boolean, + size: number, + totalSize: number + ) { + if (isPercent) { + const percentPaneSize = (size / totalSize) * 100; + pane.size = `${percentPaneSize}%`; } else { - // px resize - this.paneAfter.size = `${siblingSize}px`; + pane.size = `${size}px`; } } private _calcNewSizes(delta: number): [number, number] { let finalDelta: number; - const min = Number.parseInt(this.paneBefore.minSize ?? '0', 10) || 0; - const minSibling = Number.parseInt(this.paneAfter.minSize ?? '0', 10) || 0; + const min = Number.parseInt(this._prevPane.minSize ?? '0', 10) || 0; + const minSibling = Number.parseInt(this._nextPane.minSize ?? '0', 10) || 0; const max = - Number.parseInt(this.paneBefore.maxSize ?? '0', 10) || - this.initialPaneBeforeSize + this.initialPaneAfterSize - minSibling; + Number.parseInt(this._prevPane.maxSize ?? '0', 10) || + this._prevPaneInitialSize + this._nextPaneInitialSize - minSibling; const maxSibling = - Number.parseInt(this.paneAfter.maxSize ?? '0', 10) || - this.initialPaneBeforeSize + this.initialPaneAfterSize - min; + Number.parseInt(this._nextPane.maxSize ?? '0', 10) || + this._prevPaneInitialSize + this._nextPaneInitialSize - min; if (delta < 0) { const maxPossibleDelta = Math.min( - this.initialPaneBeforeSize - min, - maxSibling - this.initialPaneAfterSize + this._prevPaneInitialSize - min, + maxSibling - this._nextPaneInitialSize ); finalDelta = Math.min(maxPossibleDelta, Math.abs(delta)) * -1; } else { const maxPossibleDelta = Math.min( - max - this.initialPaneBeforeSize, - this.initialPaneAfterSize - minSibling + max - this._prevPaneInitialSize, + this._nextPaneInitialSize - minSibling ); finalDelta = Math.min(maxPossibleDelta, Math.abs(delta)); } return [ - this.initialPaneBeforeSize + finalDelta, - this.initialPaneAfterSize - finalDelta, + this._prevPaneInitialSize + finalDelta, + this._nextPaneInitialSize - finalDelta, ]; } @@ -277,14 +276,25 @@ export default class IgcSplitterPaneComponent extends LitElement { if (!this._splitter) { return 0; } - // get the size of part base const splitterBase = this._splitter.shadowRoot?.querySelector('[part="base"]'); if (!splitterBase) { return 0; } + + const bar = this.shadowRoot?.querySelector('igc-splitter-bar'); + const barSize = bar + ? Number.parseInt( + getComputedStyle(bar).getPropertyValue('--bar-size').trim(), + 10 + ) || 0 + : 0; + const rect = splitterBase.getBoundingClientRect(); - return this._orientation === 'horizontal' ? rect.width : rect.height; + const barsLength = this._splitter.panes.length - 1; + const barsSize = barsLength * barSize; + const size = this._orientation === 'horizontal' ? rect.width : rect.height; + return size - barsSize; } /** Toggles the collapsed state of the pane. */ diff --git a/src/components/splitter/splitter.spec.ts b/src/components/splitter/splitter.spec.ts index 8385decf8..b107bb8e8 100644 --- a/src/components/splitter/splitter.spec.ts +++ b/src/components/splitter/splitter.spec.ts @@ -1,4 +1,10 @@ -import { elementUpdated, expect, fixture, html } from '@open-wc/testing'; +import { + elementUpdated, + expect, + fixture, + html, + nextFrame, +} from '@open-wc/testing'; import { defineComponents } from '../common/definitions/defineComponents.js'; import type { SplitterOrientation } from '../types.js'; import IgcSplitterComponent from './splitter.js'; @@ -98,6 +104,50 @@ describe('Splitter', () => { const rightBars = getSplitterBars(rightSplitter); expect(rightBars).to.have.lengthOf(1); }); + + it('should not display the bar elements if the splitter is nonCollapsible', async () => { + splitter.nonCollapsible = true; + await elementUpdated(splitter); + + const bars = getSplitterBars(splitter); + bars.forEach((bar) => { + const base = bar.shadowRoot!.querySelector( + '[part~="base"]' + ) as HTMLElement; + expect(base.children).to.have.lengthOf(0); + }); + }); + + it('should set a default cursor on the bar in case any of its siblings is not resizable or collapsed', async () => { + const firstPane = splitter.panes[0]; + const secondPane = splitter.panes[1]; + const bars = getSplitterBars(splitter); + const firstBar = bars[0].shadowRoot!.querySelector( + '[part~="base"]' + ) as HTMLElement; + + const style = getComputedStyle(firstBar); + expect(style.cursor).to.equal('col-resize'); + + firstPane.resizable = false; + await elementUpdated(splitter); + await nextFrame(); + + expect(style.cursor).to.equal('default'); + + firstPane.resizable = true; + secondPane.collapsed = true; + await elementUpdated(splitter); + await nextFrame(); + + expect(style.cursor).to.equal('default'); + + secondPane.collapsed = false; + await elementUpdated(splitter); + await nextFrame(); + + expect(style.cursor).to.equal('col-resize'); + }); }); describe('Properties', () => { @@ -257,7 +307,7 @@ describe('Splitter', () => { }); }); - describe('Methods & Events', () => { + describe('Methods, Events & Interactions', () => { it('should expand/collapse panes when toggle is invoked', async () => { const pane = splitter.panes[0]; expect(pane.collapsed).to.be.false; @@ -272,6 +322,74 @@ describe('Splitter', () => { expect(pane.collapsed).to.be.false; }); + + it('should toggle the previous pane when the bar expander-end is clicked', async () => { + const bars = getSplitterBars(splitter); + const firstBar = bars[0]; + const firstPane = splitter.panes[0]; + const secondPane = splitter.panes[1]; + + expect(firstPane.collapsed).to.be.false; + + const expanderStart = getExpander(firstBar, 'start'); + const expanderEnd = getExpander(firstBar, 'end'); + + expanderEnd.dispatchEvent( + new PointerEvent('pointerdown', { bubbles: true }) + ); + await elementUpdated(splitter); + await nextFrame(); + + expect(firstPane.collapsed).to.be.true; + expect(secondPane.collapsed).to.be.false; + expect(expanderStart.hidden).to.be.true; + expect(expanderEnd.hidden).to.be.false; + + expanderEnd.dispatchEvent( + new PointerEvent('pointerdown', { bubbles: true }) + ); + await elementUpdated(splitter); + await nextFrame(); + + expect(firstPane.collapsed).to.be.false; + expect(secondPane.collapsed).to.be.false; + expect(expanderStart.hidden).to.be.false; + expect(expanderEnd.hidden).to.be.false; + }); + + it('should toggle the next pane when the bar expander-start is clicked', async () => { + const bars = getSplitterBars(splitter); + const firstBar = bars[0]; + const firstPane = splitter.panes[0]; + const secondPane = splitter.panes[1]; + + expect(secondPane.collapsed).to.be.false; + + const expanderStart = getExpander(firstBar, 'start'); + const expanderEnd = getExpander(firstBar, 'end'); + + expanderStart.dispatchEvent( + new PointerEvent('pointerdown', { bubbles: true }) + ); + await elementUpdated(splitter); + await nextFrame(); + + expect(secondPane.collapsed).to.be.true; + expect(firstPane.collapsed).to.be.false; + expect(expanderStart.hidden).to.be.false; + expect(expanderEnd.hidden).to.be.true; + + expanderStart.dispatchEvent( + new PointerEvent('pointerdown', { bubbles: true }) + ); + await elementUpdated(splitter); + await nextFrame(); + + expect(firstPane.collapsed).to.be.false; + expect(secondPane.collapsed).to.be.false; + expect(expanderStart.hidden).to.be.false; + expect(expanderEnd.hidden).to.be.false; + }); }); }); @@ -352,3 +470,9 @@ function getSplitterBars(splitter: IgcSplitterComponent) { }); return bars; } + +function getExpander(bar: IgcSplitterBarComponent, which: 'start' | 'end') { + return bar.shadowRoot!.querySelector( + `[part="expander-${which}"]` + ) as HTMLElement; +} diff --git a/src/components/splitter/themes/splitter-bar.base.scss b/src/components/splitter/themes/splitter-bar.base.scss index 655d30817..21575d725 100644 --- a/src/components/splitter/themes/splitter-bar.base.scss +++ b/src/components/splitter/themes/splitter-bar.base.scss @@ -1,6 +1,7 @@ @use 'styles/common/component'; :host { + --bar-size: 5px; display: flex; background-color: var(--ig-gray-200); @@ -11,6 +12,7 @@ [part~='base'] { display: flex; cursor: var(--cursor, default); + justify-content: center; } [part='base horizontal'] { @@ -19,7 +21,7 @@ } flex-direction: column; - width: 5px; + width: var(--bar-size); height: 100%; } @@ -30,7 +32,7 @@ flex-direction: row; width: 100%; - height: 5px; + height: var(--bar-size); } } diff --git a/src/components/splitter/themes/splitter-pane.scss b/src/components/splitter/themes/splitter-pane.scss index 24abc1e48..9aa3b017a 100644 --- a/src/components/splitter/themes/splitter-pane.scss +++ b/src/components/splitter/themes/splitter-pane.scss @@ -3,6 +3,7 @@ :host { [part='base'] { overflow: auto; + box-sizing: border-box; } display: flex; diff --git a/src/components/splitter/themes/splitter.base.scss b/src/components/splitter/themes/splitter.base.scss index 6df7c0b01..6d219793a 100644 --- a/src/components/splitter/themes/splitter.base.scss +++ b/src/components/splitter/themes/splitter.base.scss @@ -33,7 +33,4 @@ flex-direction: column; } - ::slotted(igc-splitter-pane) { - flex-direction: column; - } } diff --git a/stories/splitter.stories.ts b/stories/splitter.stories.ts index cd6774506..71a5a38b7 100644 --- a/stories/splitter.stories.ts +++ b/stories/splitter.stories.ts @@ -234,15 +234,12 @@ export const NestedSplitters: Story = { argTypes: disableStoryControls(metadata), render: () => html` - + From da506d4c824d2361ef805978dcb475f3854b03e1 Mon Sep 17 00:00:00 2001 From: Bozhidara Pachilova Date: Wed, 12 Nov 2025 14:06:02 +0200 Subject: [PATCH 15/24] fix: handle shrink differently to reflect proper percentage sizes? --- src/components/splitter/splitter-pane.ts | 32 ++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/components/splitter/splitter-pane.ts b/src/components/splitter/splitter-pane.ts index f4e2f0623..4aad9acc3 100644 --- a/src/components/splitter/splitter-pane.ts +++ b/src/components/splitter/splitter-pane.ts @@ -43,17 +43,22 @@ export default class IgcSplitterPaneComponent extends LitElement { @query('[part~="base"]', true) private readonly _base!: HTMLElement; + private get _splitter(): IgcSplitterComponent | undefined { + return this._splitterContext.value; + } + private get _isPercentageSize() { - return this._size === 'auto' || this._size.indexOf('%') !== -1; + return this._size.indexOf('%') !== -1; } - private get _splitter(): IgcSplitterComponent | undefined { - return this._splitterContext.value; + private get _isAutoSize() { + return this._size === 'auto'; } private get _flex() { - const grow = this._isPercentageSize ? 1 : 0; - return `${grow} ${grow} ${this._size}`; + const grow = this._isAutoSize ? 1 : 0; + const shrink = this._isAutoSize || this._isPercentageSize ? 1 : 0; + return `${grow} ${shrink} ${this._size}`; } private get _rectSize() { @@ -188,8 +193,10 @@ export default class IgcSplitterPaneComponent extends LitElement { this._nextPane = panes[panes.indexOf(this) + 1]; // Store original size types before we start changing them - this._isPrevPanePercentage = this._prevPane._isPercentageSize; - this._isNextPanePercentage = this._nextPane._isPercentageSize; + this._isPrevPanePercentage = + this._prevPane._isPercentageSize || this._prevPane._isAutoSize; + this._isNextPanePercentage = + this._nextPane._isPercentageSize || this._nextPane._isAutoSize; this._prevPaneInitialSize = this._rectSize; this._nextPaneInitialSize = this._nextPane._rectSize; @@ -224,17 +231,22 @@ export default class IgcSplitterPaneComponent extends LitElement { (pane) => pane !== this && pane !== this._nextPane ).forEach((pane) => { const size = pane._rectSize; - this._adjustPaneSize(pane, pane._isPercentageSize, size, totalSize); + this._adjustPaneSize( + pane, + pane._isPercentageSize || pane._isAutoSize, + size, + totalSize + ); }); } private _adjustPaneSize( pane: IgcSplitterPaneComponent, - isPercent: boolean, + isPercentOrAuto: boolean, size: number, totalSize: number ) { - if (isPercent) { + if (isPercentOrAuto) { const percentPaneSize = (size / totalSize) * 100; pane.size = `${percentPaneSize}%`; } else { From 9184867c8f2889138b83e5ce2ec44e88075f795b Mon Sep 17 00:00:00 2001 From: MonikaKirkova Date: Wed, 12 Nov 2025 17:42:08 +0200 Subject: [PATCH 16/24] feat(splitter): implement keyboard navigation --- src/components/splitter/splitter-bar.ts | 65 ++++++++++++++++++++----- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/src/components/splitter/splitter-bar.ts b/src/components/splitter/splitter-bar.ts index 43e32c61c..02e655706 100644 --- a/src/components/splitter/splitter-bar.ts +++ b/src/components/splitter/splitter-bar.ts @@ -3,6 +3,14 @@ import { html, LitElement, nothing } from 'lit'; import { type StyleInfo, styleMap } from 'lit/directives/style-map.js'; import { splitterContext } from '../common/context.js'; import { addInternalsController } from '../common/controllers/internals.js'; +import { + addKeybindings, + arrowDown, + arrowLeft, + arrowRight, + arrowUp, + ctrlKey, +} from '../common/controllers/key-bindings.js'; import { createMutationController } from '../common/controllers/mutation-observer.js'; import { registerComponent } from '../common/definitions/register.js'; import type { Constructor } from '../common/mixins/constructor.js'; @@ -106,14 +114,10 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< ); }, start: () => { - if ( - !this._siblingPanes[0]?.resizable || - !this._siblingPanes[1]?.resizable || - this._siblingPanes[0].collapsed - ) { + if (this._resizeDisallowed) { return false; } - this.emitEvent('igcMovingStart', { detail: this._siblingPanes[0] }); + this.emitEvent('igcMovingStart', { detail: this._siblingPanes[0]! }); return true; }, resize: ({ state }) => { @@ -133,6 +137,16 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< }, cancel: () => {}, }); + + addKeybindings(this) + .set(arrowUp, this.resizePanes) + .set(arrowDown, this.resizePanes) + .set(arrowLeft, this.resizePanes) + .set(arrowRight, this.resizePanes) + .set([ctrlKey, arrowUp], () => this._handleExpanderClick(true)) + .set([ctrlKey, arrowDown], () => this._handleExpanderClick(false)) + .set([ctrlKey, arrowLeft], () => this._handleExpanderClick(true)) + .set([ctrlKey, arrowRight], () => this._handleExpanderClick(false)); //addThemingController(this, all); } @@ -143,6 +157,34 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< }); } + private resizePanes(event: KeyboardEvent) { + if (this._resizeDisallowed) { + return false; + } + if ( + (event.key === arrowUp || event.key === arrowDown) && + this._orientation === 'horizontal' + ) { + return false; + } + if ( + (event.key === arrowLeft || event.key === arrowRight) && + this._orientation === 'vertical' + ) { + return false; + } + let delta = 0; + if (event.key === arrowUp || event.key === arrowLeft) { + delta = -10; + } else { + delta = 10; + } + this.emitEvent('igcMovingStart', { detail: this._siblingPanes[0]! }); + this.emitEvent('igcMoving', { detail: delta }); + this.emitEvent('igcMovingEnd', { detail: delta }); + return true; + } + private _createSiblingPaneMutationController(pane: IgcSplitterPaneComponent) { createMutationController(pane, { callback: () => { @@ -167,19 +209,19 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< this.requestUpdate(); } - private _handleExpanderClick(start: boolean, event: PointerEvent) { + private _handleExpanderClick(start: boolean, event?: PointerEvent) { // Prevent resize controller from starting - event.stopPropagation(); + event?.stopPropagation(); const prevSibling = this._siblingPanes[0]!; const nextSibling = this._siblingPanes[1]!; let target: IgcSplitterPaneComponent; if (start) { - // if next is clicked when prev pane is hidden, show prev pane, else hide next pane. - target = prevSibling.collapsed ? prevSibling : nextSibling; - } else { // if prev is clicked when next pane is hidden, show next pane, else hide prev pane. target = nextSibling.collapsed ? nextSibling : prevSibling; + } else { + // if next is clicked when prev pane is hidden, show prev pane, else hide next pane. + target = prevSibling.collapsed ? prevSibling : nextSibling; } target.toggle(); } @@ -211,6 +253,7 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin<
${this._renderBarControls()}
From 65685bddbe955bcfdbe05cb77ddbd16b9bac5a6e Mon Sep 17 00:00:00 2001 From: MonikaKirkova Date: Thu, 13 Nov 2025 13:15:47 +0200 Subject: [PATCH 17/24] feat(splitter): implement splitter events --- src/components/splitter/splitter-bar.ts | 56 +++++++++++++++++++----- src/components/splitter/splitter-pane.ts | 24 +++++++++- src/components/splitter/splitter.ts | 20 ++++++++- 3 files changed, 84 insertions(+), 16 deletions(-) diff --git a/src/components/splitter/splitter-bar.ts b/src/components/splitter/splitter-bar.ts index 02e655706..3a7f43411 100644 --- a/src/components/splitter/splitter-bar.ts +++ b/src/components/splitter/splitter-bar.ts @@ -139,14 +139,14 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< }); addKeybindings(this) - .set(arrowUp, this.resizePanes) - .set(arrowDown, this.resizePanes) - .set(arrowLeft, this.resizePanes) - .set(arrowRight, this.resizePanes) - .set([ctrlKey, arrowUp], () => this._handleExpanderClick(true)) - .set([ctrlKey, arrowDown], () => this._handleExpanderClick(false)) - .set([ctrlKey, arrowLeft], () => this._handleExpanderClick(true)) - .set([ctrlKey, arrowRight], () => this._handleExpanderClick(false)); + .set(arrowUp, this._handleResizePanes) + .set(arrowDown, this._handleResizePanes) + .set(arrowLeft, this._handleResizePanes) + .set(arrowRight, this._handleResizePanes) + .set([ctrlKey, arrowUp], this._handleExpandPanes) + .set([ctrlKey, arrowDown], this._handleExpandPanes) + .set([ctrlKey, arrowLeft], this._handleExpandPanes) + .set([ctrlKey, arrowRight], this._handleExpandPanes); //addThemingController(this, all); } @@ -157,7 +157,7 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< }); } - private resizePanes(event: KeyboardEvent) { + private _handleResizePanes(event: KeyboardEvent) { if (this._resizeDisallowed) { return false; } @@ -185,6 +185,29 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< return true; } + private _handleExpandPanes(event: KeyboardEvent) { + if (this._splitter?.nonCollapsible) { + return; + } + const { prevButtonHidden, nextButtonHidden } = + this._getExpanderHiddenState(); + + if ( + ((event.key === arrowUp && this._orientation === 'vertical') || + (event.key === arrowLeft && this._orientation === 'horizontal')) && + !prevButtonHidden + ) { + this._handleExpanderClick(true); + } + if ( + ((event.key === arrowDown && this._orientation === 'vertical') || + (event.key === arrowRight && this._orientation === 'horizontal')) && + !nextButtonHidden + ) { + this._handleExpanderClick(false); + } + } + private _createSiblingPaneMutationController(pane: IgcSplitterPaneComponent) { createMutationController(pane, { callback: () => { @@ -215,6 +238,7 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< const prevSibling = this._siblingPanes[0]!; const nextSibling = this._siblingPanes[1]!; + let target: IgcSplitterPaneComponent; if (start) { // if prev is clicked when next pane is hidden, show next pane, else hide prev pane. @@ -224,15 +248,23 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< target = prevSibling.collapsed ? prevSibling : nextSibling; } target.toggle(); + target.emitEvent('igcToggle', { detail: target }); + } + + private _getExpanderHiddenState() { + const [prev, next] = this._siblingPanes; + return { + prevButtonHidden: !!(prev?.collapsed && !next?.collapsed), + nextButtonHidden: !!(next?.collapsed && !prev?.collapsed), + }; } private _renderBarControls() { if (this._splitter?.nonCollapsible) { return nothing; } - const siblings = this._siblingPanes; - const prevButtonHidden = siblings[0]?.collapsed && !siblings[1]?.collapsed; - const nextButtonHidden = siblings[1]?.collapsed && !siblings[0]?.collapsed; + const { prevButtonHidden, nextButtonHidden } = + this._getExpanderHiddenState(); return html`
; +} +export default class IgcSplitterPaneComponent extends EventEmitterMixin< + IgcSplitterPaneComponentEventMap, + Constructor +>(LitElement) { public static readonly tagName = 'igc-splitter-pane'; - public static override styles = [styles]; + public static styles = [styles]; /* blazorSuppress */ public static register() { @@ -200,6 +208,10 @@ export default class IgcSplitterPaneComponent extends LitElement { this._prevPaneInitialSize = this._rectSize; this._nextPaneInitialSize = this._nextPane._rectSize; + + this._splitter!.emitEvent('igcResizeStart', { + detail: { pane: this, sibling: this._nextPane }, + }); } private _handleMoving(event: CustomEvent) { @@ -207,6 +219,10 @@ export default class IgcSplitterPaneComponent extends LitElement { this._prevPane.size = `${paneSize}px`; this._nextPane.size = `${siblingSize}px`; + + this._splitter!.emitEvent('igcResizing', { + detail: { pane: this, sibling: this._nextPane }, + }); } private _handleMovingEnd(event: CustomEvent) { @@ -238,6 +254,10 @@ export default class IgcSplitterPaneComponent extends LitElement { totalSize ); }); + + this._splitter!.emitEvent('igcResizeEnd', { + detail: { pane: this, sibling: this._nextPane }, + }); } private _adjustPaneSize( diff --git a/src/components/splitter/splitter.ts b/src/components/splitter/splitter.ts index 8c9433e88..a05704707 100644 --- a/src/components/splitter/splitter.ts +++ b/src/components/splitter/splitter.ts @@ -5,10 +5,23 @@ import { splitterContext } from '../common/context.js'; import { addInternalsController } from '../common/controllers/internals.js'; import { watch } from '../common/decorators/watch.js'; import { registerComponent } from '../common/definitions/register.js'; +import type { Constructor } from '../common/mixins/constructor.js'; +import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; import type { SplitterOrientation } from '../types.js'; import IgcSplitterPaneComponent from './splitter-pane.js'; import { styles } from './themes/splitter.base.css.js'; +export interface IgcSplitterBarResizeEventArgs { + pane: IgcSplitterPaneComponent; + sibling: IgcSplitterPaneComponent; +} + +export interface IgcSplitterComponentEventMap { + igcResizeStart: CustomEvent; + igcResizing: CustomEvent; + igcResizeEnd: CustomEvent; +} + /** * The Splitter component provides a framework for a simple layout, splitting the view horizontally or vertically * into multiple smaller resizable and collapsible areas. @@ -19,9 +32,12 @@ import { styles } from './themes/splitter.base.css.js'; * * @csspart ... - ... . */ -export default class IgcSplitterComponent extends LitElement { +export default class IgcSplitterComponent extends EventEmitterMixin< + IgcSplitterComponentEventMap, + Constructor +>(LitElement) { public static readonly tagName = 'igc-splitter'; - public static override styles = [styles]; + public static styles = [styles]; /* blazorSuppress */ public static register() { From 2e6130d3c35355c9fdd28d0e6d33f256524e6837 Mon Sep 17 00:00:00 2001 From: Bozhidara Pachilova Date: Thu, 13 Nov 2025 14:20:21 +0200 Subject: [PATCH 18/24] refactor(splitter): rename to nonResizable; port skip fn for key bindings (wip) --- src/components/splitter/splitter-bar.ts | 38 +++++++++---------- src/components/splitter/splitter-pane.ts | 4 +- src/components/splitter/splitter.spec.ts | 28 +++++++------- .../splitter/themes/splitter-bar.base.scss | 1 + .../splitter/themes/splitter-pane.scss | 1 - .../splitter/themes/splitter.base.scss | 1 - stories/splitter.stories.ts | 30 +++++++-------- 7 files changed, 51 insertions(+), 52 deletions(-) diff --git a/src/components/splitter/splitter-bar.ts b/src/components/splitter/splitter-bar.ts index 3a7f43411..0411753fa 100644 --- a/src/components/splitter/splitter-bar.ts +++ b/src/components/splitter/splitter-bar.ts @@ -19,7 +19,7 @@ import { partMap } from '../common/part-map.js'; import { addResizeController } from '../resize-container/resize-controller.js'; import type { SplitterOrientation } from '../types.js'; import type IgcSplitterComponent from './splitter.js'; -import IgcSplitterPaneComponent from './splitter-pane.js'; +import type IgcSplitterPaneComponent from './splitter-pane.js'; import { styles } from './themes/splitter-bar.base.css.js'; export interface IgcSplitterBarComponentEventMap { @@ -84,7 +84,7 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< private get _resizeDisallowed() { return !!this._siblingPanes.find( - (x) => x && (x.resizable === false || x.collapsed === true) + (x) => x && (x.nonResizable || x.collapsed) ); } @@ -138,7 +138,7 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< cancel: () => {}, }); - addKeybindings(this) + addKeybindings(this, { skip: this._shouldSkipResize }) .set(arrowUp, this._handleResizePanes) .set(arrowDown, this._handleResizePanes) .set(arrowLeft, this._handleResizePanes) @@ -157,28 +157,30 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< }); } - private _handleResizePanes(event: KeyboardEvent) { - if (this._resizeDisallowed) { - return false; + private _shouldSkipResize(_node: Element, event: KeyboardEvent): boolean { + if (this._resizeDisallowed && !event.ctrlKey) { + return true; } if ( (event.key === arrowUp || event.key === arrowDown) && - this._orientation === 'horizontal' + this._orientation === 'horizontal' && + !event.ctrlKey ) { - return false; + return true; } if ( (event.key === arrowLeft || event.key === arrowRight) && - this._orientation === 'vertical' + this._orientation === 'vertical' && + !event.ctrlKey ) { - return false; - } - let delta = 0; - if (event.key === arrowUp || event.key === arrowLeft) { - delta = -10; - } else { - delta = 10; + return true; } + return false; + } + + private _handleResizePanes(event: KeyboardEvent) { + const delta = event.key === arrowUp || event.key === arrowLeft ? -10 : 10; + this.emitEvent('igcMovingStart', { detail: this._siblingPanes[0]! }); this.emitEvent('igcMoving', { detail: delta }); this.emitEvent('igcMovingEnd', { detail: delta }); @@ -214,10 +216,8 @@ export default class IgcSplitterBarComponent extends EventEmitterMixin< Object.assign(this._internalStyles, { '--cursor': this._cursor }); this.requestUpdate(); }, - filter: [IgcSplitterPaneComponent.tagName], config: { - attributeFilter: ['collapsed', 'resizable'], - subtree: true, + attributeFilter: ['collapsed', 'non-resizable'], }, }); } diff --git a/src/components/splitter/splitter-pane.ts b/src/components/splitter/splitter-pane.ts index 382143cf9..ff107f33c 100644 --- a/src/components/splitter/splitter-pane.ts +++ b/src/components/splitter/splitter-pane.ts @@ -124,8 +124,8 @@ export default class IgcSplitterPaneComponent extends EventEmitterMixin< * Defines if the pane is resizable or not. * @attr */ - @property({ type: Boolean, reflect: true }) - public resizable = true; + @property({ type: Boolean, reflect: true, attribute: 'non-resizable' }) + public nonResizable = false; /** * Collapsed state of the pane. diff --git a/src/components/splitter/splitter.spec.ts b/src/components/splitter/splitter.spec.ts index b107bb8e8..066cf4f24 100644 --- a/src/components/splitter/splitter.spec.ts +++ b/src/components/splitter/splitter.spec.ts @@ -129,13 +129,13 @@ describe('Splitter', () => { const style = getComputedStyle(firstBar); expect(style.cursor).to.equal('col-resize'); - firstPane.resizable = false; + firstPane.nonResizable = true; await elementUpdated(splitter); await nextFrame(); expect(style.cursor).to.equal('default'); - firstPane.resizable = true; + firstPane.nonResizable = false; secondPane.collapsed = true; await elementUpdated(splitter); await nextFrame(); @@ -249,8 +249,8 @@ describe('Splitter', () => { expect(splitter.panes[0].size).to.equal('30%'); expect(splitter.panes[1].size).to.equal('70%'); - expect(style1.flex).to.equal('1 1 30%'); - expect(style2.flex).to.equal('1 1 70%'); + expect(style1.flex).to.equal('0 1 30%'); + expect(style2.flex).to.equal('0 1 70%'); expect(pane1.minSize).to.equal('20%'); expect(pane1.maxSize).to.equal('80%'); @@ -323,7 +323,7 @@ describe('Splitter', () => { expect(pane.collapsed).to.be.false; }); - it('should toggle the previous pane when the bar expander-end is clicked', async () => { + it('should toggle the next pane when the bar expander-end is clicked', async () => { const bars = getSplitterBars(splitter); const firstBar = bars[0]; const firstPane = splitter.panes[0]; @@ -340,10 +340,10 @@ describe('Splitter', () => { await elementUpdated(splitter); await nextFrame(); - expect(firstPane.collapsed).to.be.true; - expect(secondPane.collapsed).to.be.false; - expect(expanderStart.hidden).to.be.true; - expect(expanderEnd.hidden).to.be.false; + expect(firstPane.collapsed).to.be.false; + expect(secondPane.collapsed).to.be.true; + expect(expanderStart.hidden).to.be.false; + expect(expanderEnd.hidden).to.be.true; expanderEnd.dispatchEvent( new PointerEvent('pointerdown', { bubbles: true }) @@ -357,7 +357,7 @@ describe('Splitter', () => { expect(expanderEnd.hidden).to.be.false; }); - it('should toggle the next pane when the bar expander-start is clicked', async () => { + it('should toggle the previous pane when the bar expander-start is clicked', async () => { const bars = getSplitterBars(splitter); const firstBar = bars[0]; const firstPane = splitter.panes[0]; @@ -374,10 +374,10 @@ describe('Splitter', () => { await elementUpdated(splitter); await nextFrame(); - expect(secondPane.collapsed).to.be.true; - expect(firstPane.collapsed).to.be.false; - expect(expanderStart.hidden).to.be.false; - expect(expanderEnd.hidden).to.be.true; + expect(firstPane.collapsed).to.be.true; + expect(secondPane.collapsed).to.be.false; + expect(expanderStart.hidden).to.be.true; + expect(expanderEnd.hidden).to.be.false; expanderStart.dispatchEvent( new PointerEvent('pointerdown', { bubbles: true }) diff --git a/src/components/splitter/themes/splitter-bar.base.scss b/src/components/splitter/themes/splitter-bar.base.scss index 21575d725..8191a5f07 100644 --- a/src/components/splitter/themes/splitter-bar.base.scss +++ b/src/components/splitter/themes/splitter-bar.base.scss @@ -2,6 +2,7 @@ :host { --bar-size: 5px; + display: flex; background-color: var(--ig-gray-200); diff --git a/src/components/splitter/themes/splitter-pane.scss b/src/components/splitter/themes/splitter-pane.scss index 9aa3b017a..74085da18 100644 --- a/src/components/splitter/themes/splitter-pane.scss +++ b/src/components/splitter/themes/splitter-pane.scss @@ -12,7 +12,6 @@ height: 100%; } - :host([collapsed]) [part='base'] { display: none; } diff --git a/src/components/splitter/themes/splitter.base.scss b/src/components/splitter/themes/splitter.base.scss index 6d219793a..97c29509b 100644 --- a/src/components/splitter/themes/splitter.base.scss +++ b/src/components/splitter/themes/splitter.base.scss @@ -32,5 +32,4 @@ [part='base'] { flex-direction: column; } - } diff --git a/stories/splitter.stories.ts b/stories/splitter.stories.ts index 71a5a38b7..2bd38d63a 100644 --- a/stories/splitter.stories.ts +++ b/stories/splitter.stories.ts @@ -14,21 +14,21 @@ type SplitterStoryArgs = IgcSplitterComponent & { pane1MinSize?: string; pane1MaxSize?: string; pane1Collapsed?: boolean; - pane1Resizable?: boolean; + pane1NonResizable?: boolean; /* Pane 2 properties */ pane2Size?: string; pane2MinSize?: string; pane2MaxSize?: string; pane2Collapsed?: boolean; - pane2Resizable?: boolean; + pane2NonResizable?: boolean; /* Pane 3 properties */ pane3Size?: string; pane3MinSize?: string; pane3MaxSize?: string; pane3Collapsed?: boolean; - pane3Resizable?: boolean; + pane3NonResizable?: boolean; }; const metadata: Meta = { @@ -69,7 +69,7 @@ const metadata: Meta = { description: 'Collapsed state of the first pane', table: { category: 'Pane 1' }, }, - pane1Resizable: { + pane1NonResizable: { control: 'boolean', description: 'Whether the first pane is resizable', table: { category: 'Pane 1' }, @@ -94,7 +94,7 @@ const metadata: Meta = { description: 'Collapsed state of the second pane', table: { category: 'Pane 2' }, }, - pane2Resizable: { + pane2NonResizable: { control: 'boolean', description: 'Whether the second pane is resizable', table: { category: 'Pane 2' }, @@ -119,7 +119,7 @@ const metadata: Meta = { description: 'Collapsed state of the third pane', table: { category: 'Pane 3' }, }, - pane3Resizable: { + pane3NonResizable: { control: 'boolean', description: 'Whether the third pane is resizable', table: { category: 'Pane 3' }, @@ -129,13 +129,13 @@ const metadata: Meta = { orientation: 'horizontal', nonCollapsible: false, pane1Size: 'auto', - pane1Resizable: true, + pane1NonResizable: false, pane1Collapsed: false, pane2Size: 'auto', - pane2Resizable: true, + pane2NonResizable: false, pane2Collapsed: false, pane3Size: 'auto', - pane3Resizable: true, + pane3NonResizable: false, pane3Collapsed: false, }, }; @@ -165,17 +165,17 @@ export const Default: Story = { pane1MinSize, pane1MaxSize, pane1Collapsed, - pane1Resizable, + pane1NonResizable, pane2Size, pane2MinSize, pane2MaxSize, pane2Collapsed, - pane2Resizable, + pane2NonResizable, pane3Size, pane3MinSize, pane3MaxSize, pane3Collapsed, - pane3Resizable, + pane3NonResizable, }) => html` + + //
+ // + // + //
Pane 1
+ //
+ // + //
Pane 2
+ //
+ // + //
Pane 3
+ //
+ //
+ //
+ // Change All Panes Min/Max Sizes + // `, render: ({ orientation, nonCollapsible, - pane1Size, - pane1MinSize, - pane1MaxSize, - pane1Collapsed, - pane1NonResizable, - pane2Size, - pane2MinSize, - pane2MaxSize, - pane2Collapsed, - pane2NonResizable, - pane3Size, - pane3MinSize, - pane3MaxSize, - pane3Collapsed, - pane3NonResizable, + nonResizable, + startCollapsed, + endCollapsed, }) => html` - +
- -
Top Left Pane
-
+
Top Left Pane
- -
Bottom Left Pane
-
+
Bottom Left Pane
- +
- +
- -
Top Right Pane
-
- - -
Bottom Right Pane
-
+
Top Right Pane
+
Bottom Right Pane
- +
`, }; From c3ffbde9053ce0e8a2add9b088f065c8bd9b48d9 Mon Sep 17 00:00:00 2001 From: Bozhidara Pachilova Date: Tue, 18 Nov 2025 11:04:39 +0200 Subject: [PATCH 20/24] refactor existing tests; add initial resize tests; minor refactor --- src/components/splitter/splitter.spec.ts | 556 +++++++++++------- src/components/splitter/splitter.ts | 77 ++- .../splitter/themes/splitter.base.scss | 4 +- stories/splitter.stories.ts | 264 +++------ 4 files changed, 465 insertions(+), 436 deletions(-) diff --git a/src/components/splitter/splitter.spec.ts b/src/components/splitter/splitter.spec.ts index 066cf4f24..5d89bae1b 100644 --- a/src/components/splitter/splitter.spec.ts +++ b/src/components/splitter/splitter.spec.ts @@ -6,24 +6,25 @@ import { nextFrame, } from '@open-wc/testing'; import { defineComponents } from '../common/definitions/defineComponents.js'; +import { roundPrecise } from '../common/util.js'; +import { + simulateLostPointerCapture, + simulatePointerDown, + simulatePointerMove, +} from '../common/utils.spec.js'; import type { SplitterOrientation } from '../types.js'; import IgcSplitterComponent from './splitter.js'; -import IgcSplitterBarComponent from './splitter-bar.js'; -import IgcSplitterPaneComponent from './splitter-pane.js'; describe('Splitter', () => { before(() => { - defineComponents( - IgcSplitterComponent, - IgcSplitterPaneComponent, - IgcSplitterBarComponent - ); + defineComponents(IgcSplitterComponent); }); let splitter: IgcSplitterComponent; beforeEach(async () => { splitter = await fixture(createSplitter()); + await elementUpdated(splitter); }); describe('Rendering', () => { @@ -37,19 +38,61 @@ describe('Splitter', () => { await expect(splitter).shadowDom.to.be.accessible(); }); - it('should render a split bar for each splitter pane except the last one', async () => { - await elementUpdated(splitter); + it('should render start and end slots', async () => { + let slot = getSplitterSlot(splitter, 'start'); + let elements = slot.assignedElements(); + expect(elements).to.have.lengthOf(1); + expect(elements[0].textContent).to.equal('Pane 1'); + + slot = getSplitterSlot(splitter, 'end'); + elements = slot.assignedElements(); + expect(elements).to.have.lengthOf(1); + expect(elements[0].textContent).to.equal('Pane 2'); + }); - expect(splitter.panes).to.have.lengthOf(3); + it('should render splitter bar between start and end parts', async () => { + const base = getSplitterPart(splitter, 'base'); + const startPart = getSplitterPart(splitter, 'startPane'); + const endPart = getSplitterPart(splitter, 'endPane'); + const bar = getSplitterPart(splitter, 'bar'); - const bars = getSplitterBars(splitter); - expect(bars).to.have.lengthOf(2); + expect(base).to.exist; + expect(startPart).to.exist; + expect(endPart).to.exist; + expect(bar).to.exist; - bars.forEach((bar, index) => { - const pane = splitter.panes[index]; - const paneBase = getSplitterPaneBase(pane) as HTMLElement; - expect(bar.previousElementSibling).to.equal(paneBase); - }); + expect(base.contains(startPart)).to.be.true; + expect(base.contains(endPart)).to.be.true; + expect(base.contains(bar)).to.be.true; + + expect(startPart.nextElementSibling).to.equal(bar); + expect(bar.nextElementSibling).to.equal(endPart); + }); + + it('should render splitter bar parts', async () => { + const bar = getSplitterPart(splitter, 'bar'); + const expanderStart = getSplitterPart(splitter, 'expander-start'); + const barHandle = getSplitterPart(splitter, 'handle'); + const expanderEnd = getSplitterPart(splitter, 'expander-end'); + + expect(expanderStart).to.exist; + expect(barHandle).to.exist; + expect(expanderEnd).to.exist; + + expect(bar.contains(expanderStart)).to.be.true; + expect(bar.contains(expanderEnd)).to.be.true; + expect(bar.contains(barHandle)).to.be.true; + + expect(expanderStart.nextElementSibling).to.equal(barHandle); + expect(barHandle.nextElementSibling).to.equal(expanderEnd); + }); + + it('should not display the bar elements if the splitter is nonCollapsible', async () => { + splitter.nonCollapsible = true; + await elementUpdated(splitter); + + const bar = getSplitterPart(splitter, 'bar'); + expect(bar.children).to.have.lengthOf(0); }); it('should have default horizontal orientation', () => { @@ -72,82 +115,91 @@ describe('Splitter', () => { ); await elementUpdated(nestedSplitter); - expect(nestedSplitter.panes).to.have.lengthOf(2); - expect(nestedSplitter.orientation).to.equal('horizontal'); - - const outerBars = getSplitterBars(nestedSplitter); - expect(outerBars).to.have.lengthOf(1); - - const firstPane = nestedSplitter.panes[0]; - const leftSplitter = firstPane.querySelector( - IgcSplitterComponent.tagName - ) as IgcSplitterComponent; - - expect(leftSplitter).to.exist; - expect(leftSplitter.orientation).to.equal('vertical'); - - expect(leftSplitter.panes).to.have.lengthOf(2); - - const leftBars = getSplitterBars(leftSplitter); - expect(leftBars).to.have.lengthOf(1); - - const secondPane = nestedSplitter.panes[1]; - const rightSplitter = secondPane.querySelector( - IgcSplitterComponent.tagName - ) as IgcSplitterComponent; + const outerStartSlot = getSplitterSlot(nestedSplitter, 'start'); + const startElements = outerStartSlot.assignedElements(); + expect(startElements).to.have.lengthOf(1); + expect(startElements[0].tagName.toLowerCase()).to.equal( + IgcSplitterComponent.tagName.toLowerCase() + ); - expect(rightSplitter).to.exist; - expect(rightSplitter.orientation).to.equal('vertical'); + const outerEndSlot = getSplitterSlot(nestedSplitter, 'end'); + const endElements = outerEndSlot.assignedElements(); + expect(endElements).to.have.lengthOf(1); + expect(endElements[0].tagName.toLowerCase()).to.equal( + IgcSplitterComponent.tagName.toLowerCase() + ); - expect(rightSplitter.panes).to.have.lengthOf(2); + const innerStartSlot1 = getSplitterSlot( + startElements[0] as IgcSplitterComponent, + 'start' + ); + expect(innerStartSlot1.assignedElements()[0].textContent).to.equal( + 'Top Left Pane' + ); - const rightBars = getSplitterBars(rightSplitter); - expect(rightBars).to.have.lengthOf(1); - }); + const innerEndSlot1 = getSplitterSlot( + startElements[0] as IgcSplitterComponent, + 'end' + ); + expect(innerEndSlot1.assignedElements()[0].textContent).to.equal( + 'Bottom Left Pane' + ); - it('should not display the bar elements if the splitter is nonCollapsible', async () => { - splitter.nonCollapsible = true; - await elementUpdated(splitter); + const innerStartSlot2 = getSplitterSlot( + endElements[0] as IgcSplitterComponent, + 'start' + ); + expect(innerStartSlot2.assignedElements()[0].textContent).to.equal( + 'Top Right Pane' + ); - const bars = getSplitterBars(splitter); - bars.forEach((bar) => { - const base = bar.shadowRoot!.querySelector( - '[part~="base"]' - ) as HTMLElement; - expect(base.children).to.have.lengthOf(0); - }); + const innerEndSlot2 = getSplitterSlot( + endElements[0] as IgcSplitterComponent, + 'end' + ); + expect(innerEndSlot2.assignedElements()[0].textContent).to.equal( + 'Bottom Right Pane' + ); }); - it('should set a default cursor on the bar in case any of its siblings is not resizable or collapsed', async () => { - const firstPane = splitter.panes[0]; - const secondPane = splitter.panes[1]; - const bars = getSplitterBars(splitter); - const firstBar = bars[0].shadowRoot!.querySelector( - '[part~="base"]' - ) as HTMLElement; + it('should set a default cursor on the bar in case splitter is not resizable or any pane is collapsed', async () => { + const bar = getSplitterPart(splitter, 'bar'); - const style = getComputedStyle(firstBar); + const style = getComputedStyle(bar); expect(style.cursor).to.equal('col-resize'); - firstPane.nonResizable = true; + splitter.nonResizable = true; await elementUpdated(splitter); await nextFrame(); expect(style.cursor).to.equal('default'); - firstPane.nonResizable = false; - secondPane.collapsed = true; + splitter.nonResizable = false; + splitter.endCollapsed = true; await elementUpdated(splitter); await nextFrame(); expect(style.cursor).to.equal('default'); - secondPane.collapsed = false; + splitter.endCollapsed = false; await elementUpdated(splitter); await nextFrame(); expect(style.cursor).to.equal('col-resize'); }); + + it('should change the bar cursor based on the orientation', async () => { + const bar = getSplitterPart(splitter, 'bar'); + + const style = getComputedStyle(bar); + expect(style.cursor).to.equal('col-resize'); + + splitter.orientation = 'vertical'; + await elementUpdated(splitter); + await nextFrame(); + + expect(style.cursor).to.equal('row-resize'); + }); }); describe('Properties', () => { @@ -160,30 +212,29 @@ describe('Splitter', () => { }); it('should reset pane sizes when orientation changes', async () => { - const pane = splitter.panes[0]; - pane.size = '200px'; + splitter.startSize = '200px'; await elementUpdated(splitter); - const base = getSplitterPaneBase(pane) as HTMLElement; - const style = getComputedStyle(base); + const startPart = getSplitterPart(splitter, 'startPane'); + const style = getComputedStyle(startPart); expect(style.flex).to.equal('0 0 200px'); splitter.orientation = 'vertical'; await elementUpdated(splitter); - expect(pane.size).to.equal('auto'); + expect(splitter.startSize).to.equal('auto'); + expect(style.flex).to.equal('1 1 auto'); }); - it('should use default min/max values when not specified', async () => { + // TODO: verify the attribute type, default value, reflection + it('should properly set default min/max values when not specified', async () => { await elementUpdated(splitter); - const pane = splitter.panes[0]; - const base = getSplitterPaneBase(pane) as HTMLElement; - const style = getComputedStyle(base); + const startPart = getSplitterPart(splitter, 'startPane'); + const style = getComputedStyle(startPart); expect(style.flex).to.equal('1 1 auto'); - expect(pane.size).to.equal('auto'); - + expect(splitter.startSize).to.equal('auto'); expect(style.minWidth).to.equal('0px'); expect(style.maxWidth).to.equal('100%'); @@ -197,16 +248,15 @@ describe('Splitter', () => { it('should apply minSize and maxSize to panes for horizontal orientation', async () => { splitter = await fixture( createTwoPanesWithSizesAndConstraints({ - minSize1: '100px', - maxSize1: '500px', + startMinSize: '100px', + startMaxSize: '500px', }) ); await elementUpdated(splitter); - const pane = splitter.panes[0]; - const base = getSplitterPaneBase(pane) as HTMLElement; - const style = getComputedStyle(base); + const startPane = getSplitterPart(splitter, 'startPane'); + const style = getComputedStyle(startPane); expect(style.minWidth).to.equal('100px'); expect(style.maxWidth).to.equal('500px'); }); @@ -214,16 +264,15 @@ describe('Splitter', () => { it('should apply minSize and maxSize to panes for vertical orientation', async () => { splitter = await fixture( createTwoPanesWithSizesAndConstraints({ - minSize1: '100px', - maxSize1: '500px', + startMinSize: '100px', + startMaxSize: '500px', orientation: 'vertical', }) ); await elementUpdated(splitter); - const pane = splitter.panes[0]; - const base = getSplitterPaneBase(pane) as HTMLElement; - const style = getComputedStyle(base); + const startPane = getSplitterPart(splitter, 'startPane'); + const style = getComputedStyle(startPane); expect(style.minHeight).to.equal('100px'); expect(style.maxHeight).to.equal('500px'); }); @@ -231,29 +280,27 @@ describe('Splitter', () => { it('should handle percentage sizes', async () => { const splitter = await fixture( createTwoPanesWithSizesAndConstraints({ - size1: '30%', - size2: '70%', - minSize1: '20%', - maxSize1: '80%', + startSize: '30%', + endSize: '70%', + startMinSize: '20%', + startMaxSize: '80%', }) ); await elementUpdated(splitter); - const pane1 = splitter.panes[0]; - const base1 = getSplitterPaneBase(pane1) as HTMLElement; - const style1 = getComputedStyle(base1); + const startPane = getSplitterPart(splitter, 'startPane'); + const style1 = getComputedStyle(startPane); - const pane2 = splitter.panes[1]; - const base2 = getSplitterPaneBase(pane2) as HTMLElement; - const style2 = getComputedStyle(base2); + const endPane = getSplitterPart(splitter, 'endPane'); + const style2 = getComputedStyle(endPane); - expect(splitter.panes[0].size).to.equal('30%'); - expect(splitter.panes[1].size).to.equal('70%'); + expect(splitter.startSize).to.equal('30%'); + expect(splitter.endSize).to.equal('70%'); expect(style1.flex).to.equal('0 1 30%'); expect(style2.flex).to.equal('0 1 70%'); - expect(pane1.minSize).to.equal('20%'); - expect(pane1.maxSize).to.equal('80%'); + expect(splitter.startMinSize).to.equal('20%'); + expect(splitter.startMaxSize).to.equal('80%'); expect(style1.minWidth).to.equal('20%'); expect(style1.maxWidth).to.equal('80%'); @@ -263,76 +310,48 @@ describe('Splitter', () => { it('should handle mixed px and % constraints', async () => { const mixedConstraintSplitter = await fixture( createTwoPanesWithSizesAndConstraints({ - minSize1: '100px', - maxSize1: '50%', + startMinSize: '100px', + startMaxSize: '50%', }) ); await elementUpdated(mixedConstraintSplitter); - const pane = mixedConstraintSplitter.panes[0]; - const base1 = getSplitterPaneBase(pane) as HTMLElement; - const style = getComputedStyle(base1); + const startPane = getSplitterPart(mixedConstraintSplitter, 'startPane'); + const style = getComputedStyle(startPane); - expect(pane.minSize).to.equal('100px'); - expect(pane.maxSize).to.equal('50%'); + expect(mixedConstraintSplitter.startMinSize).to.equal('100px'); + expect(mixedConstraintSplitter.startMaxSize).to.equal('50%'); expect(style.minWidth).to.equal('100px'); expect(style.maxWidth).to.equal('50%'); // TODO: test with drag }); - - it('should dynamically update when panes are added', async () => { - expect(splitter.panes).to.have.lengthOf(3); - - const newPane = document.createElement( - 'igc-splitter-pane' - ) as IgcSplitterPaneComponent; - newPane.textContent = 'New Pane'; - splitter.appendChild(newPane); - - await elementUpdated(splitter); - - expect(splitter.panes).to.have.lengthOf(4); - }); - - it('should dynamically update when panes are removed', async () => { - expect(splitter.panes).to.have.lengthOf(3); - - const paneToRemove = splitter.panes[1]; - paneToRemove.remove(); - - await elementUpdated(splitter); - - expect(splitter.panes).to.have.lengthOf(2); - }); }); describe('Methods, Events & Interactions', () => { it('should expand/collapse panes when toggle is invoked', async () => { - const pane = splitter.panes[0]; - expect(pane.collapsed).to.be.false; - - pane.toggle(); + splitter.toggle('start'); await elementUpdated(splitter); + expect(splitter.startCollapsed).to.be.true; - expect(pane.collapsed).to.be.true; + splitter.toggle('start'); + await elementUpdated(splitter); + expect(splitter.startCollapsed).to.be.false; - pane.toggle(); + splitter.toggle('end'); await elementUpdated(splitter); + expect(splitter.endCollapsed).to.be.true; - expect(pane.collapsed).to.be.false; + // edge case: supports collapsing both at a time? + splitter.toggle('start'); + await elementUpdated(splitter); + expect(splitter.startCollapsed).to.be.true; + expect(splitter.endCollapsed).to.be.true; }); it('should toggle the next pane when the bar expander-end is clicked', async () => { - const bars = getSplitterBars(splitter); - const firstBar = bars[0]; - const firstPane = splitter.panes[0]; - const secondPane = splitter.panes[1]; - - expect(firstPane.collapsed).to.be.false; - - const expanderStart = getExpander(firstBar, 'start'); - const expanderEnd = getExpander(firstBar, 'end'); + const expanderStart = getSplitterPart(splitter, 'expander-start'); + const expanderEnd = getSplitterPart(splitter, 'expander-end'); expanderEnd.dispatchEvent( new PointerEvent('pointerdown', { bubbles: true }) @@ -340,8 +359,8 @@ describe('Splitter', () => { await elementUpdated(splitter); await nextFrame(); - expect(firstPane.collapsed).to.be.false; - expect(secondPane.collapsed).to.be.true; + expect(splitter.startCollapsed).to.be.false; + expect(splitter.endCollapsed).to.be.true; expect(expanderStart.hidden).to.be.false; expect(expanderEnd.hidden).to.be.true; @@ -351,22 +370,15 @@ describe('Splitter', () => { await elementUpdated(splitter); await nextFrame(); - expect(firstPane.collapsed).to.be.false; - expect(secondPane.collapsed).to.be.false; + expect(splitter.startCollapsed).to.be.false; + expect(splitter.endCollapsed).to.be.false; expect(expanderStart.hidden).to.be.false; expect(expanderEnd.hidden).to.be.false; }); it('should toggle the previous pane when the bar expander-start is clicked', async () => { - const bars = getSplitterBars(splitter); - const firstBar = bars[0]; - const firstPane = splitter.panes[0]; - const secondPane = splitter.panes[1]; - - expect(secondPane.collapsed).to.be.false; - - const expanderStart = getExpander(firstBar, 'start'); - const expanderEnd = getExpander(firstBar, 'end'); + const expanderStart = getSplitterPart(splitter, 'expander-start'); + const expanderEnd = getSplitterPart(splitter, 'expander-end'); expanderStart.dispatchEvent( new PointerEvent('pointerdown', { bubbles: true }) @@ -374,8 +386,8 @@ describe('Splitter', () => { await elementUpdated(splitter); await nextFrame(); - expect(firstPane.collapsed).to.be.true; - expect(secondPane.collapsed).to.be.false; + expect(splitter.startCollapsed).to.be.true; + expect(splitter.endCollapsed).to.be.false; expect(expanderStart.hidden).to.be.true; expect(expanderEnd.hidden).to.be.false; @@ -385,20 +397,89 @@ describe('Splitter', () => { await elementUpdated(splitter); await nextFrame(); - expect(firstPane.collapsed).to.be.false; - expect(secondPane.collapsed).to.be.false; + expect(splitter.startCollapsed).to.be.false; + expect(splitter.endCollapsed).to.be.false; expect(expanderStart.hidden).to.be.false; expect(expanderEnd.hidden).to.be.false; }); + + it('should resize horizontally in both directions', async () => { + const startPane = getSplitterPart(splitter, 'startPane'); + const endPane = getSplitterPart(splitter, 'endPane'); + const startSizeBefore = roundPrecise( + startPane.getBoundingClientRect().width + ); + const endSizeBefore = roundPrecise(endPane.getBoundingClientRect().width); + let deltaX = 100; + + await resize(splitter, deltaX, 0); + await elementUpdated(splitter); + + const startSizeAfter = roundPrecise( + startPane.getBoundingClientRect().width + ); + const endSizeAfter = roundPrecise(endPane.getBoundingClientRect().width); + + expect(startSizeAfter).to.equal(startSizeBefore + deltaX); + expect(endSizeAfter).to.equal(endSizeBefore - deltaX); + + deltaX *= -1; + await resize(splitter, deltaX, 0); + + const startSizeFinal = roundPrecise( + startPane.getBoundingClientRect().width + ); + const endSizeFinal = roundPrecise(endPane.getBoundingClientRect().width); + + expect(startSizeFinal).to.equal(startSizeBefore); + expect(endSizeFinal).to.equal(endSizeBefore); + }); + + it('should resize vertically in both directions', async () => { + splitter.orientation = 'vertical'; + await elementUpdated(splitter); + + const startPane = getSplitterPart(splitter, 'startPane'); + const endPane = getSplitterPart(splitter, 'endPane'); + const startSizeBefore = roundPrecise( + startPane.getBoundingClientRect().height + ); + const endSizeBefore = roundPrecise( + endPane.getBoundingClientRect().height + ); + let deltaY = 100; + + await resize(splitter, 0, deltaY); + + const startSizeAfter = roundPrecise( + startPane.getBoundingClientRect().height + ); + const endSizeAfter = roundPrecise(endPane.getBoundingClientRect().height); + + expect(startSizeAfter).to.equal(startSizeBefore + deltaY); + expect(endSizeAfter).to.equal(endSizeBefore - deltaY); + + deltaY *= -1; + await resize(splitter, 0, deltaY); + + const startSizeFinal = roundPrecise( + startPane.getBoundingClientRect().height + ); + const endSizeFinal = roundPrecise(endPane.getBoundingClientRect().height); + + expect(startSizeFinal).to.equal(startSizeBefore); + expect(endSizeFinal).to.equal(endSizeBefore); + }); + // TODO: test when the slots have assigned sizes/min sizes + edge cases + // currently observing issue when resizing to the end and panes have fixed px sizes }); }); function createSplitter() { return html` - - Pane 1 - Pane 2 - Pane 3 + +
Pane 1
+
Pane 2
`; } @@ -406,29 +487,25 @@ function createSplitter() { function createNestedSplitter() { return html` - - - Top Left Pane - Bottom Left Pane - - - - - Top Right Pane - Bottom Right Pane - - + +
Top Left Pane
+
Bottom Left Pane
+
+ +
Top Right Pane
+
Bottom Right Pane
+
`; } type SplitterTestSizesAndConstraints = { - size1?: string; - size2?: string; - minSize1?: string; - maxSize1?: string; - minSize2?: string; - maxSize2?: string; + startSize?: string; + endSize?: string; + startMinSize?: string; + startMaxSize?: string; + endMinSize?: string; + endMaxSize?: string; orientation?: SplitterOrientation; }; @@ -436,43 +513,82 @@ function createTwoPanesWithSizesAndConstraints( config: SplitterTestSizesAndConstraints ) { return html` - - - Pane 1 - - - Pane 2 - + +
Pane 1
+
Pane 2
`; } -function getSplitterPaneBase(pane: IgcSplitterPaneComponent) { - return pane.shadowRoot!.querySelector('div[part~="base"]'); +function getSplitterSlot( + splitter: IgcSplitterComponent, + which: 'start' | 'end' +) { + return splitter.renderRoot.querySelector( + `slot[name="${which}"]` + ) as HTMLSlotElement; +} + +// TODO: more parts and names? +type SplitterParts = + | 'startPane' + | 'endPane' + | 'bar' + | 'base' + | 'expander-start' + | 'expander-end' + | 'handle'; + +function getSplitterPart(splitter: IgcSplitterComponent, which: SplitterParts) { + return splitter.shadowRoot!.querySelector( + `[part~="${which}"]` + ) as HTMLElement; } -function getSplitterBars(splitter: IgcSplitterComponent) { - const bars: IgcSplitterBarComponent[] = []; +async function resize( + splitter: IgcSplitterComponent, + deltaX: number, + deltaY: number +) { + const bar = getSplitterPart(splitter, 'bar'); + const barRect = bar.getBoundingClientRect(); - splitter.panes.forEach((pane) => { - const bar = pane.shadowRoot!.querySelector(IgcSplitterBarComponent.tagName); - if (bar) { - bars.push(bar); - } + simulatePointerDown(bar, { + clientX: barRect.left, + clientY: barRect.top, }); - return bars; + await elementUpdated(splitter); + + simulatePointerMove( + bar, + { + clientX: barRect.left, + clientY: barRect.top, + }, + { x: deltaX, y: deltaY } + ); + await elementUpdated(splitter); + + simulateLostPointerCapture(bar); + await elementUpdated(splitter); + await nextFrame(); } -function getExpander(bar: IgcSplitterBarComponent, which: 'start' | 'end') { - return bar.shadowRoot!.querySelector( - `[part="expander-${which}"]` - ) as HTMLElement; -} +// function checkPanesAreWithingBounds( +// splitter: IgcSplitterComponent, +// startSize: number, +// endSize: number, +// dimension: 'x' | 'y' +// ) { +// const splitterSize = +// splitter.getBoundingClientRect()[dimension === 'x' ? 'width' : 'height']; +// expect(startSize + endSize).to.be.at.most(splitterSize); +// } diff --git a/src/components/splitter/splitter.ts b/src/components/splitter/splitter.ts index 007b10e21..eee3d88ce 100644 --- a/src/components/splitter/splitter.ts +++ b/src/components/splitter/splitter.ts @@ -56,6 +56,7 @@ export default class IgcSplitterComponent extends EventEmitterMixin< private readonly _barRef = createRef(); private _startPaneInternalStyles: StyleInfo = {}; private _endPaneInternalStyles: StyleInfo = {}; + private _barInternalStyles: StyleInfo = {}; private _startSize = 'auto'; private _endSize = 'auto'; private _startPaneInitialSize!: number; @@ -135,6 +136,13 @@ export default class IgcSplitterComponent extends EventEmitterMixin< return `${grow} ${shrink} ${this._endSize}`; } + private get _barCursor(): string { + if (this._resizeDisallowed) { + return 'default'; + } + return this.orientation === 'horizontal' ? 'col-resize' : 'row-resize'; + } + /** * The minimum size of the start pane. * @attr @@ -216,7 +224,13 @@ export default class IgcSplitterComponent extends EventEmitterMixin< @watch('orientation', { waitUntilFirstUpdate: true }) protected _orientationChange(): void { this._internals.setARIA({ ariaOrientation: this.orientation }); - this._resetPane(); + Object.assign(this._barInternalStyles, { '--cursor': this._barCursor }); + this._resetPanes(); + } + + @watch('nonResizable') + protected _changeCursor(): void { + Object.assign(this._barInternalStyles, { '--cursor': this._barCursor }); } @watch('startCollapsed', { waitUntilFirstUpdate: true }) @@ -224,6 +238,7 @@ export default class IgcSplitterComponent extends EventEmitterMixin< protected _collapsedChange(): void { this.startSize = 'auto'; this.endSize = 'auto'; + this._changeCursor(); } protected override willUpdate(changed: PropertyValues) { @@ -235,18 +250,7 @@ export default class IgcSplitterComponent extends EventEmitterMixin< changed.has('endMinSize') || changed.has('endMaxSize') ) { - this._initPane( - this.startMinSize!, - this.startMaxSize!, - this._startFlex, - this._startPaneInternalStyles - ); - this._initPane( - this.endMinSize!, - this.endMaxSize!, - this._endFlex, - this._endPaneInternalStyles - ); + this._initPanes(); } } @@ -301,6 +305,10 @@ export default class IgcSplitterComponent extends EventEmitterMixin< }); } + protected override firstUpdated() { + this._initPanes(); + } + //#endregion /** Toggles the collapsed state of the pane. */ @@ -473,44 +481,46 @@ export default class IgcSplitterComponent extends EventEmitterMixin< return size - barSize; } - private _resetPane() { + private _resetPanes() { this.startSize = 'auto'; this.endSize = 'auto'; - Object.assign(this._startPaneInternalStyles, { + const commonStyles = { minWidth: 0, maxWidth: '100%', minHeight: 0, maxHeight: '100%', + }; + Object.assign(this._startPaneInternalStyles, { + ...commonStyles, flex: this._startFlex, }); Object.assign(this._endPaneInternalStyles, { - minWidth: 0, - maxWidth: '100%', - minHeight: 0, - maxHeight: '100%', + ...commonStyles, flex: this._endFlex, }); } - private _initPane( - minSize: string, - maxSize: string, - flex: string, - internalStyles: StyleInfo - ) { + private _initPanes() { let sizes = {}; if (this.orientation === 'horizontal') { sizes = { - minWidth: minSize ?? 0, - maxWidth: maxSize ?? '100%', + minWidth: this.startMinSize ?? 0, + maxWidth: this.startMaxSize ?? '100%', }; } else { sizes = { - minHeight: minSize ?? 0, - maxHeight: maxSize ?? '100%', + minHeight: this.startMinSize ?? 0, + maxHeight: this.startMaxSize ?? '100%', }; } - Object.assign(internalStyles, { ...sizes, flex: flex }); + Object.assign(this._startPaneInternalStyles, { + ...sizes, + flex: this._startFlex, + }); + Object.assign(this._endPaneInternalStyles, { + ...sizes, + flex: this._endFlex, + }); this.requestUpdate(); } @@ -563,7 +573,12 @@ export default class IgcSplitterComponent extends EventEmitterMixin<
-
+
${this._renderBarControls()}
diff --git a/src/components/splitter/themes/splitter.base.scss b/src/components/splitter/themes/splitter.base.scss index e89185972..5f6c3b12d 100644 --- a/src/components/splitter/themes/splitter.base.scss +++ b/src/components/splitter/themes/splitter.base.scss @@ -33,6 +33,7 @@ height: 100%; min-width: 0; min-height: 0; + box-sizing: border-box; } // Bar styles (moved from splitter-bar.base.scss) @@ -79,7 +80,6 @@ flex-direction: column; width: var(--bar-size); height: 100%; - --cursor: col-resize; [part='handle'] { height: 50px; @@ -96,8 +96,8 @@ [part='bar'] { flex-direction: row; width: 100%; + height: var(--bar-size); - --cursor: row-resize; [part='handle'] { width: 50px; diff --git a/stories/splitter.stories.ts b/stories/splitter.stories.ts index c23bbff33..82c9bad8a 100644 --- a/stories/splitter.stories.ts +++ b/stories/splitter.stories.ts @@ -7,30 +7,7 @@ import { disableStoryControls } from './story.js'; defineComponents(IgcSplitterComponent); -type SplitterStoryArgs = IgcSplitterComponent & { - /* Pane 1 properties */ - pane1Size?: string; - pane1MinSize?: string; - pane1MaxSize?: string; - pane1Collapsed?: boolean; - pane1NonResizable?: boolean; - - /* Pane 2 properties */ - pane2Size?: string; - pane2MinSize?: string; - pane2MaxSize?: string; - pane2Collapsed?: boolean; - pane2NonResizable?: boolean; - - /* Pane 3 properties */ - pane3Size?: string; - pane3MinSize?: string; - pane3MaxSize?: string; - pane3Collapsed?: boolean; - pane3NonResizable?: boolean; -}; - -const metadata: Meta = { +const metadata: Meta = { title: 'Splitter', component: 'igc-splitter', parameters: { @@ -48,80 +25,52 @@ const metadata: Meta = { description: 'Orientation of the splitter.', table: { defaultValue: { summary: 'horizontal' } }, }, - pane1Size: { - control: 'text', - description: 'Size of the first pane (e.g., "auto", "100px", "30%")', - table: { category: 'Pane 1' }, - }, - pane1MinSize: { - control: 'text', - description: 'Minimum size of the first pane', - table: { category: 'Pane 1' }, - }, - pane1MaxSize: { - control: 'text', - description: 'Maximum size of the first pane', - table: { category: 'Pane 1' }, - }, - pane1Collapsed: { + nonCollapsible: { + type: 'boolean', + description: 'Disables pane collapsing.', control: 'boolean', - description: 'Collapsed state of the first pane', - table: { category: 'Pane 1' }, + table: { defaultValue: { summary: 'false' } }, }, - pane1NonResizable: { + nonResizable: { + type: 'boolean', + description: 'Disables pane resizing.', control: 'boolean', - description: 'Whether the first pane is resizable', - table: { category: 'Pane 1' }, + table: { defaultValue: { summary: 'false' } }, }, - pane2Size: { - control: 'text', - description: 'Size of the second pane (e.g., "auto", "100px", "30%")', - table: { category: 'Pane 2' }, + startCollapsed: { + type: 'boolean', + description: 'Collapses the start pane.', + table: { defaultValue: { summary: 'false' } }, }, - pane2MinSize: { - control: 'text', - description: 'Minimum size of the second pane', - table: { category: 'Pane 2' }, - }, - pane2MaxSize: { - control: 'text', - description: 'Maximum size of the second pane', - table: { category: 'Pane 2' }, - }, - pane2Collapsed: { + endCollapsed: { + type: 'boolean', + description: 'Collapses the end pane.', control: 'boolean', - description: 'Collapsed state of the second pane', - table: { category: 'Pane 2' }, + table: { defaultValue: { summary: 'false' } }, }, - pane2NonResizable: { - control: 'boolean', - description: 'Whether the second pane is resizable', - table: { category: 'Pane 2' }, + startSize: { + control: { type: 'text' }, + description: 'Size of the start pane (e.g., "200px", "50%", "auto").', }, - pane3Size: { - control: 'text', - description: 'Size of the third pane (e.g., "auto", "100px", "30%")', - table: { category: 'Pane 3' }, + endSize: { + control: { type: 'text' }, + description: 'Size of the end pane (e.g., "200px", "50%", "auto").', }, - pane3MinSize: { - control: 'text', - description: 'Minimum size of the third pane', - table: { category: 'Pane 3' }, + startMinSize: { + control: { type: 'text' }, + description: 'Minimum size of the start pane.', }, - pane3MaxSize: { - control: 'text', - description: 'Maximum size of the third pane', - table: { category: 'Pane 3' }, + startMaxSize: { + control: { type: 'text' }, + description: 'Maximum size of the start pane.', }, - pane3Collapsed: { - control: 'boolean', - description: 'Collapsed state of the third pane', - table: { category: 'Pane 3' }, + endMinSize: { + control: { type: 'text' }, + description: 'Minimum size of the end pane.', }, - pane3NonResizable: { - control: 'boolean', - description: 'Whether the third pane is resizable', - table: { category: 'Pane 3' }, + endMaxSize: { + control: { type: 'text' }, + description: 'Maximum size of the end pane.', }, }, args: { @@ -130,112 +79,51 @@ const metadata: Meta = { nonResizable: false, startCollapsed: false, endCollapsed: false, - pane1Size: 'auto', - pane1NonResizable: false, - pane1Collapsed: false, - pane2Size: 'auto', - pane2NonResizable: false, - pane2Collapsed: false, - pane3Size: 'auto', - pane3NonResizable: false, - pane3Collapsed: false, }, }; export default metadata; -type Story = StoryObj; -// function changePaneMinMaxSizes() { -// const splitter = document.querySelector('igc-splitter'); -// const panes = splitter?.panes; -// if (!panes) { -// return; -// } -// panes[0].minSize = '50px'; -// panes[0].maxSize = '200px'; -// panes[1].minSize = '100px'; -// panes[1].maxSize = '300px'; -// panes[2].minSize = '150px'; -// panes[2].maxSize = '450px'; -// } +interface IgcSplitterArgs { + orientation: 'horizontal' | 'vertical'; + nonCollapsible: boolean; + nonResizable: boolean; + startCollapsed: boolean; + endCollapsed: boolean; + startSize?: string; + endSize?: string; + startMinSize?: string; + startMaxSize?: string; + endMinSize?: string; + endMaxSize?: string; +} + +type Story = StoryObj; + +function changePaneMinMaxSizes() { + const splitter = document.querySelector('igc-splitter'); + if (!splitter) { + return; + } + splitter.startMinSize = '50px'; + splitter.startMaxSize = '200px'; + splitter.endMinSize = '100px'; + splitter.endMaxSize = '300px'; +} export const Default: Story = { - // render: ({ - // orientation, - // nonCollapsible, - // pane1Size, - // pane1MinSize, - // pane1MaxSize, - // pane1Collapsed, - // pane1NonResizable, - // pane2Size, - // pane2MinSize, - // pane2MaxSize, - // pane2Collapsed, - // pane2NonResizable, - // pane3Size, - // pane3MinSize, - // pane3MaxSize, - // pane3Collapsed, - // pane3NonResizable, - // }) => html` - // - - //
- // - // - //
Pane 1
- //
- // - //
Pane 2
- //
- // - //
Pane 3
- //
- //
- //
- // Change All Panes Min/Max Sizes - // `, render: ({ orientation, nonCollapsible, nonResizable, startCollapsed, endCollapsed, + startSize, + endSize, + startMinSize, + startMaxSize, + endMinSize, + endMaxSize, }) => html`