• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-webcomponents / 22964298907

11 Mar 2026 04:55PM UTC coverage: 98.313%. First build
22964298907

Pull #1969

github

web-flow
Merge 233c4273d into cd9d9c162
Pull Request #1969: Splitter component

5538 of 5827 branches covered (95.04%)

Branch coverage included in aggregate %.

1009 of 1024 new or added lines in 3 files covered. (98.54%)

38232 of 38694 relevant lines covered (98.81%)

1536.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.73
/src/components/splitter/splitter.ts
1
import { html, LitElement, type PropertyValues } from 'lit';
9✔
2
import { eventOptions, property, query, state } from 'lit/decorators.js';
9✔
3
import { createRef, ref } from 'lit/directives/ref.js';
9✔
4
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
9✔
5
import { addThemingController } from '../../theming/theming-controller.js';
9✔
6
import {
9✔
7
  addKeybindings,
9✔
8
  arrowDown,
9✔
9
  arrowLeft,
9✔
10
  arrowRight,
9✔
11
  arrowUp,
9✔
12
  ctrlKey,
9✔
13
  endKey,
9✔
14
  homeKey,
9✔
15
} from '../common/controllers/key-bindings.js';
9✔
16
import { createResizeObserverController } from '../common/controllers/resize-observer.js';
9✔
17
import { addSlotController, setSlots } from '../common/controllers/slot.js';
9✔
18
import { registerComponent } from '../common/definitions/register.js';
9✔
19
import type { Constructor } from '../common/mixins/constructor.js';
9✔
20
import { EventEmitterMixin } from '../common/mixins/event-emitter.js';
9✔
21
import { partMap } from '../common/part-map.js';
9✔
22
import {
9✔
23
  asNumber,
9✔
24
  asPercent,
9✔
25
  bindIf,
9✔
26
  clamp,
9✔
27
  isLTR,
9✔
28
  roundPrecise,
9✔
29
} from '../common/util.js';
9✔
30
import type { SplitterOrientation } from '../types.js';
9✔
31
import { styles as shared } from './themes/shared/splitter.common.css.js';
9✔
32
import { styles } from './themes/splitter.base.css.js';
9✔
33
import { all } from './themes/themes.js';
9✔
34

9✔
35
export interface IgcSplitterResizeEventDetail {
9✔
36
  /** The current size of the start panel in pixels */
9✔
37
  startPanelSize: number;
9✔
38
  /** The current size of the end panel in pixels */
9✔
39
  endPanelSize: number;
9✔
40
  /** The change in size since the resize operation started (only for igcResizing and igcResizeEnd) */
9✔
41
  delta?: number;
9✔
42
}
9✔
43

9✔
44
export interface IgcSplitterComponentEventMap {
9✔
45
  igcResizeStart: CustomEvent<IgcSplitterResizeEventDetail>;
9✔
46
  igcResizing: CustomEvent<IgcSplitterResizeEventDetail>;
9✔
47
  igcResizeEnd: CustomEvent<IgcSplitterResizeEventDetail>;
9✔
48
}
9✔
49

9✔
50
interface PaneResizeSnapshot {
9✔
51
  initialSize: number;
9✔
52
  isPercentageBased: boolean;
9✔
53
  minSizePx?: number;
9✔
54
  maxSizePx?: number;
9✔
55
}
9✔
56

9✔
57
interface SplitterResizeState {
9✔
58
  startPane: PaneResizeSnapshot | null;
9✔
59
  endPane: PaneResizeSnapshot | null;
9✔
60
  isDragging: boolean;
9✔
61
  dragStartPosition: { x: number; y: number };
9✔
62
  dragPointerId: number;
9✔
63
}
9✔
64

9✔
65
const DEFAULT_RESIZE_STATE: SplitterResizeState = {
9✔
66
  startPane: null,
9✔
67
  endPane: null,
9✔
68
  isDragging: false,
9✔
69
  dragStartPosition: { x: 0, y: 0 },
9✔
70
  dragPointerId: -1,
9✔
71
};
9✔
72

9✔
73
interface SplitterPaneState {
9✔
74
  size?: string;
9✔
75
  minSize?: string;
9✔
76
  maxSize?: string;
9✔
77
  savedSize?: string;
9✔
78
}
9✔
79

9✔
80
const DEFAULT_PANE_STATE: SplitterPaneState = {
9✔
81
  size: 'auto',
9✔
82
};
9✔
83

9✔
84
type PanePosition = 'start' | 'end';
9✔
85

9✔
86
/**
9✔
87
 * The Splitter component provides a framework for a simple layout, splitting the view horizontally or vertically
9✔
88
 * into multiple smaller resizable and collapsible areas.
9✔
89
 *
9✔
90
 * @element igc-splitter
9✔
91
 *
9✔
92
 * @fires igcResizeStart - Emitted when resizing starts.
9✔
93
 * @fires igcResizing - Emitted while resizing.
9✔
94
 * @fires igcResizeEnd - Emitted when resizing ends.
9✔
95
 *
9✔
96
 * @slot start - Content for the start pane.
9✔
97
 * @slot end - Content for the end pane.
9✔
98
 *
9✔
99
 * @csspart splitter-bar - The resizable bar element between the two panels.
9✔
100
 * @csspart drag-handle - The drag handle icon/element on the splitter bar.
9✔
101
 * @csspart start-pane - The container for the start panel content.
9✔
102
 * @csspart end-pane - The container for the end panel content.
9✔
103
 * @csspart start-collapse-btn - The button to collapse the start panel.
9✔
104
 * @csspart end-collapse-btn - The button to collapse the end panel.
9✔
105
 * @csspart start-expand-btn - The button to expand the start panel when collapsed.
9✔
106
 * @csspart end-expand-btn - The button to expand the end panel when collapsed.
9✔
107
 */
9✔
108
export default class IgcSplitterComponent extends EventEmitterMixin<
9✔
109
  IgcSplitterComponentEventMap,
9✔
110
  Constructor<LitElement>
9✔
111
>(LitElement) {
9✔
112
  public static readonly tagName = 'igc-splitter';
9✔
113
  public static styles = [styles, shared];
9✔
114

9✔
115
  /* blazorSuppress */
9✔
116
  public static register(): void {
9✔
117
    registerComponent(IgcSplitterComponent);
1✔
118
  }
1✔
119

9✔
120
  //#region Private Properties
9✔
121

9✔
122
  private readonly _barRef = createRef<HTMLElement>();
9✔
123
  private _startPaneInternalStyles: StyleInfo = {};
9✔
124
  private _endPaneInternalStyles: StyleInfo = {};
9✔
125
  private _barInternalStyles: StyleInfo = {};
9✔
126

9✔
127
  @state()
9✔
128
  private _collapsedPane: PanePosition | null = null;
9✔
129

9✔
130
  @state()
9✔
131
  private _startPaneState: SplitterPaneState = { ...DEFAULT_PANE_STATE };
9✔
132

9✔
133
  @state()
9✔
134
  private _endPaneState: SplitterPaneState = { ...DEFAULT_PANE_STATE };
9✔
135

9✔
136
  @state()
9✔
137
  private _resizeState: SplitterResizeState = { ...DEFAULT_RESIZE_STATE };
9✔
138

9✔
139
  @query('[part~="base"]', true)
9✔
140
  private readonly _base!: HTMLElement;
9✔
141

9✔
142
  @query('[part~="start-pane"]', true)
9✔
143
  private readonly _startPane!: HTMLElement;
9✔
144

9✔
145
  @query('[part~="end-pane"]', true)
9✔
146
  private readonly _endPane!: HTMLElement;
9✔
147

9✔
148
  private get _resizeDisallowed() {
9✔
149
    return this.disableResize || this._collapsedPane !== null;
1,403✔
150
  }
1,403✔
151

9✔
152
  private get _barCursor(): string {
9✔
153
    if (this._resizeDisallowed) {
607✔
154
      return 'default';
69✔
155
    }
69✔
156
    return this.orientation === 'horizontal' ? 'col-resize' : 'row-resize';
607✔
157
  }
607✔
158

9✔
159
  private get _barTabIndex(): number {
9✔
160
    return this.disableCollapse && this.disableResize ? -1 : 0;
702✔
161
  }
702✔
162

9✔
163
  private get _isStartCollapsed(): boolean {
9✔
164
    return this._collapsedPane === 'start';
2,850✔
165
  }
2,850✔
166

9✔
167
  private get _isEndCollapsed(): boolean {
9✔
168
    return this._collapsedPane === 'end';
2,830✔
169
  }
2,830✔
170

9✔
171
  //#endregion
9✔
172

9✔
173
  //#region Public Properties
9✔
174

9✔
175
  /** Gets/Sets the orientation of the splitter.
9✔
176
   * @remarks
9✔
177
   * Default value is `horizontal`.
9✔
178
   * @attr
9✔
179
   */
9✔
180
  @property({ reflect: true })
9✔
181
  public orientation: SplitterOrientation = 'horizontal';
9✔
182

9✔
183
  /**
9✔
184
   * Sets whether collapsing the panes is disabled.
9✔
185
   * @remarks
9✔
186
   * Default value is `false`.
9✔
187
   * @attr
9✔
188
   */
9✔
189
  @property({ type: Boolean, attribute: 'disable-collapse', reflect: true })
9✔
190
  public disableCollapse = false;
9✔
191

9✔
192
  /**
9✔
193
   * Sets whether the user can resize the panels by interacting with the splitter bar.
9✔
194
   * @attr
9✔
195
   */
9✔
196
  @property({ type: Boolean, reflect: true, attribute: 'disable-resize' })
9✔
197
  public disableResize = false;
9✔
198

9✔
199
  /**
9✔
200
   * Controls the visibility of the expand/collapse buttons on the splitter bar.
9✔
201
   * @remarks
9✔
202
   * Default value is `false`.
9✔
203
   * @attr
9✔
204
   */
9✔
205
  @property({
9✔
206
    type: Boolean,
9✔
207
    attribute: 'hide-collapse-buttons',
9✔
208
    reflect: true,
9✔
209
  })
9✔
210
  public hideCollapseButtons = false;
9✔
211

9✔
212
  /**
9✔
213
   * Controls the visibility of the drag handle on the splitter bar.
9✔
214
   * @remarks
9✔
215
   * Default value is `false`.
9✔
216
   * @attr
9✔
217
   */
9✔
218
  @property({
9✔
219
    type: Boolean,
9✔
220
    attribute: 'hide-drag-handle',
9✔
221
    reflect: true,
9✔
222
  })
9✔
223
  public hideDragHandle = false;
9✔
224

9✔
225
  /**
9✔
226
   * The minimum size of the start pane.
9✔
227
   * @attr
9✔
228
   */
9✔
229
  @property({ attribute: 'start-min-size' })
9✔
230
  public set startMinSize(value: string | undefined) {
9✔
231
    this._startPaneState = {
42✔
232
      ...this._startPaneState,
42✔
233
      minSize: this._normalizeValue(value),
42✔
234
    };
42✔
235
  }
42✔
236

9✔
237
  public get startMinSize(): string | undefined {
9✔
238
    return this._startPaneState.minSize;
937✔
239
  }
937✔
240

9✔
241
  /**
9✔
242
   * The minimum size of the end pane.
9✔
243
   * @attr
9✔
244
   */
9✔
245
  @property({ attribute: 'end-min-size' })
9✔
246
  public set endMinSize(value: string | undefined) {
9✔
247
    this._endPaneState = {
41✔
248
      ...this._endPaneState,
41✔
249
      minSize: this._normalizeValue(value),
41✔
250
    };
41✔
251
  }
41✔
252

9✔
253
  public get endMinSize(): string | undefined {
9✔
254
    return this._endPaneState.minSize;
226✔
255
  }
226✔
256

9✔
257
  /**
9✔
258
   * The maximum size of the start pane.
9✔
259
   * @attr
9✔
260
   */
9✔
261
  @property({ attribute: 'start-max-size' })
9✔
262
  public set startMaxSize(value: string | undefined) {
9✔
263
    this._startPaneState = {
42✔
264
      ...this._startPaneState,
42✔
265
      maxSize: this._normalizeValue(value),
42✔
266
    };
42✔
267
  }
42✔
268

9✔
269
  public get startMaxSize(): string | undefined {
9✔
270
    return this._startPaneState.maxSize;
937✔
271
  }
937✔
272

9✔
273
  /**
9✔
274
   * The maximum size of the end pane.
9✔
275
   * @attr
9✔
276
   */
9✔
277
  @property({ attribute: 'end-max-size' })
9✔
278
  public set endMaxSize(value: string | undefined) {
9✔
279
    this._endPaneState = {
39✔
280
      ...this._endPaneState,
39✔
281
      maxSize: this._normalizeValue(value),
39✔
282
    };
39✔
283
  }
39✔
284

9✔
285
  public get endMaxSize(): string | undefined {
9✔
286
    return this._endPaneState.maxSize;
222✔
287
  }
222✔
288

9✔
289
  /**
9✔
290
   * The size of the start pane.
9✔
291
   * @attr
9✔
292
   */
9✔
293
  @property({ attribute: 'start-size' })
9✔
294
  public set startSize(value: string | undefined) {
9✔
295
    this._startPaneState = {
313✔
296
      ...this._startPaneState,
313✔
297
      size: this._normalizeValue(value, 'auto')!,
313✔
298
    };
313✔
299
  }
313✔
300

9✔
301
  public get startSize(): string | undefined {
9✔
302
    return this._startPaneState.size;
793✔
303
  }
793✔
304

9✔
305
  /**
9✔
306
   * The size of the end pane.
9✔
307
   * @attr
9✔
308
   */
9✔
309
  @property({ attribute: 'end-size' })
9✔
310
  public set endSize(value: string | undefined) {
9✔
311
    this._endPaneState = {
301✔
312
      ...this._endPaneState,
301✔
313
      size: this._normalizeValue(value, 'auto')!,
301✔
314
    };
301✔
315
  }
301✔
316

9✔
317
  public get endSize(): string | undefined {
9✔
318
    return this._endPaneState.size;
759✔
319
  }
759✔
320

9✔
321
  //#endregion
9✔
322

9✔
323
  //#region Lifecycle
9✔
324

9✔
325
  constructor() {
9✔
326
    super();
142✔
327
    addThemingController(this, all);
142✔
328

142✔
329
    addSlotController(this, {
142✔
330
      slots: setSlots('start', 'end'),
142✔
331
    });
142✔
332
    addKeybindings(this, {
142✔
333
      ref: this._barRef,
142✔
334
    })
142✔
335
      .set(arrowUp, () => this._handleResizePanes(-1, 'vertical'))
142✔
336
      .set(arrowDown, () => this._handleResizePanes(1, 'vertical'))
142✔
337
      .set(arrowLeft, () => this._handleResizePanes(-1, 'horizontal'))
142✔
338
      .set(arrowRight, () => this._handleResizePanes(1, 'horizontal'))
142✔
339
      .set(homeKey, () => this._handleMinMaxResize('min'))
142✔
340
      .set(endKey, () => this._handleMinMaxResize('max'))
142✔
341
      .set([ctrlKey, arrowUp], () =>
142✔
342
        this._handleArrowsExpandCollapse('start', 'vertical')
4✔
343
      )
142✔
344
      .set([ctrlKey, arrowDown], () =>
142✔
345
        this._handleArrowsExpandCollapse('end', 'vertical')
4✔
346
      )
142✔
347
      .set([ctrlKey, arrowLeft], () =>
142✔
348
        this._handleArrowsExpandCollapse('start', 'horizontal')
7✔
349
      )
142✔
350
      .set([ctrlKey, arrowRight], () =>
142✔
351
        this._handleArrowsExpandCollapse('end', 'horizontal')
7✔
352
      );
142✔
353

142✔
354
    createResizeObserverController(this, {
142✔
355
      callback: () => this.requestUpdate(),
142✔
356
    });
142✔
357
  }
142✔
358

9✔
359
  protected override update(changed: PropertyValues<this>): void {
9✔
360
    if (
702✔
361
      changed.has('orientation') &&
702✔
362
      changed.get('orientation') !== undefined
160✔
363
    ) {
702✔
364
      this._resetPanes();
18✔
365
    }
18✔
366

702✔
367
    if (this.hasUpdated) {
702✔
368
      this._updatePanes();
560✔
369
    }
560✔
370
    super.update(changed);
702✔
371
  }
702✔
372

9✔
373
  protected override updated(): void {
9✔
374
    this._updateBarAria();
702✔
375
  }
702✔
376

9✔
377
  //#endregion
9✔
378

9✔
379
  //#region Resize Event Handlers
9✔
380

9✔
381
  private _handleBarPointerDown(e: PointerEvent): void {
9✔
382
    if (e.button !== 0) {
43!
NEW
383
      return;
×
NEW
384
    }
×
385

43✔
386
    e.preventDefault();
43✔
387

43✔
388
    this._resizeState = {
43✔
389
      ...this._resizeState,
43✔
390
      isDragging: true,
43✔
391
      dragPointerId: e.pointerId,
43✔
392
      dragStartPosition: { x: e.clientX, y: e.clientY },
43✔
393
    };
43✔
394

43✔
395
    this._resizeStart();
43✔
396
    this._barRef.value?.setPointerCapture(this._resizeState.dragPointerId);
43✔
397
  }
43✔
398

9✔
399
  private _handleBarPointerMove(e: PointerEvent): void {
9✔
400
    if (e.pointerId !== this._resizeState.dragPointerId) {
43!
NEW
401
      return;
×
NEW
402
    }
×
403

43✔
404
    const deltaX = e.clientX - this._resizeState.dragStartPosition.x;
43✔
405
    const deltaY = e.clientY - this._resizeState.dragStartPosition.y;
43✔
406
    const delta = this._resolveDelta(deltaX, deltaY);
43✔
407

43✔
408
    if (delta !== 0) {
43✔
409
      this._resizing(delta);
43✔
410
    }
43✔
411
  }
43✔
412

9✔
413
  private _handleEndDrag(e: PointerEvent): void {
9✔
414
    if (e.pointerId !== this._resizeState.dragPointerId) {
43!
NEW
415
      return;
×
NEW
416
    }
×
417

43✔
418
    const deltaX = e.clientX - this._resizeState.dragStartPosition.x;
43✔
419
    const deltaY = e.clientY - this._resizeState.dragStartPosition.y;
43✔
420
    const delta = this._resolveDelta(deltaX, deltaY);
43✔
421

43✔
422
    if (delta !== 0) {
43✔
423
      this._resizeEnd(delta);
43✔
424
    }
43✔
425

43✔
426
    this._endDrag();
43✔
427
  }
43✔
428

9✔
429
  private _endDrag(): void {
9✔
430
    if (this._resizeState.dragPointerId !== -1) {
43✔
431
      this._barRef.value?.releasePointerCapture(
43✔
432
        this._resizeState.dragPointerId
43✔
433
      );
43✔
434
    }
43✔
435
    this._resizeState = { ...DEFAULT_RESIZE_STATE };
43✔
436
  }
43✔
437

9✔
438
  //#endregion
9✔
439

9✔
440
  //#region Public Methods
9✔
441

9✔
442
  /** Toggles the collapsed state of the specified pane. */
9✔
443
  public toggle(position: PanePosition): void {
9✔
444
    const isCollapsing = this._collapsedPane === null;
47✔
445

47✔
446
    if (isCollapsing) {
47✔
447
      this._savePaneSizes();
26✔
448
    }
26✔
449

47✔
450
    // If the requested pane is already collapsed, expand it (set to null)
47✔
451
    // Otherwise, collapse the requested pane (this also handles switching from one collapsed pane to another)
47✔
452
    this._collapsedPane = this._collapsedPane === position ? null : position;
47✔
453

47✔
454
    this.toggleAttribute('start-collapsed', this._isStartCollapsed);
47✔
455
    this.toggleAttribute('end-collapsed', this._isEndCollapsed);
47✔
456

47✔
457
    this._restoreSizesOnExpandCollapse();
47✔
458
    this._updateCursor();
47✔
459
  }
47✔
460

9✔
461
  //#endregion
9✔
462

9✔
463
  //#region Internal API
9✔
464

9✔
465
  private _savePaneSizes() {
9✔
466
    this._startPaneState = {
26✔
467
      ...this._startPaneState,
26✔
468
      savedSize: `${this._paneRectAsPercent(0)}%`,
26✔
469
    };
26✔
470
    this._endPaneState = {
26✔
471
      ...this._endPaneState,
26✔
472
      savedSize: `${this._paneRectAsPercent(1)}%`,
26✔
473
    };
26✔
474
  }
26✔
475

9✔
476
  /* Reset sizes on collapse; restore saved sizes on expand */
9✔
477
  private _restoreSizesOnExpandCollapse() {
9✔
478
    if (this._collapsedPane !== null) {
47✔
479
      this._startPaneState = { ...this._startPaneState, size: 'auto' };
28✔
480
      this._endPaneState = { ...this._endPaneState, size: 'auto' };
28✔
481
    } else {
47✔
482
      this._startPaneState = {
19✔
483
        ...this._startPaneState,
19✔
484
        size: this._startPaneState.savedSize ?? this.startSize,
19!
485
      };
19✔
486
      this._endPaneState = {
19✔
487
        ...this._endPaneState,
19✔
488
        size: this._endPaneState.savedSize ?? this.endSize,
19!
489
      };
19✔
490
    }
19✔
491
  }
47✔
492

9✔
493
  private _updateCursor() {
9✔
494
    Object.assign(this._barInternalStyles, { '--cursor': this._barCursor });
607✔
495
  }
607✔
496

9✔
497
  /** Measures the actual rendered size of a pane and returns it as a percentage of total size. */
9✔
498
  private _paneRectAsPercent(paneIndex: 0 | 1): number {
9✔
499
    const totalSize = this._getTotalSize();
722✔
500
    if (totalSize === 0) {
722✔
501
      return 0;
17✔
502
    }
17✔
503
    return roundPrecise(asPercent(this._rectSize()[paneIndex], totalSize), 0);
705✔
504
  }
722✔
505

9✔
506
  /** Converts a CSS size string (px or %) to a percentage of total size. */
9✔
507
  private _sizeToPercent(sizeValue: string): number {
9✔
508
    const totalSize = this._getTotalSize();
451✔
509
    if (totalSize === 0) {
451!
NEW
510
      return 0;
×
NEW
511
    }
×
512

451✔
513
    if (sizeValue.includes('%')) {
451✔
514
      return asNumber(sizeValue);
140✔
515
    }
140✔
516

311✔
517
    const pxValue = asNumber(sizeValue);
311✔
518
    return roundPrecise(asPercent(pxValue, totalSize), 0);
311✔
519
  }
451✔
520

9✔
521
  private _getStartPaneSizePercent(): number {
9✔
522
    if (!this._startPane || this._isStartCollapsed) {
702✔
523
      return 0;
20✔
524
    }
20✔
525
    if (this._isEndCollapsed) {
702✔
526
      return 100;
12✔
527
    }
12✔
528
    return this._paneRectAsPercent(0);
670✔
529
  }
702✔
530

9✔
531
  private _getMinMaxAsPercent(type: 'min' | 'max'): number {
9✔
532
    const value = type === 'min' ? this.startMinSize : this.startMaxSize;
1,404✔
533
    const defaultValue = type === 'min' ? 0 : 100;
1,404✔
534

1,404✔
535
    return value ? this._sizeToPercent(value) : defaultValue;
1,404✔
536
  }
1,404✔
537

9✔
538
  private _updateBarAria(): void {
9✔
539
    const bar = this._barRef.value;
702✔
540
    if (!bar) return;
702!
541

702✔
542
    bar.ariaValueNow = this._getStartPaneSizePercent().toString();
702✔
543
    bar.ariaValueMin = this._getMinMaxAsPercent('min').toString();
702✔
544
    bar.ariaValueMax = this._getMinMaxAsPercent('max').toString();
702✔
545
  }
702✔
546

9✔
547
  private _isPercentageSize(which: PanePosition): boolean {
9✔
548
    const targetSize =
254✔
549
      which === 'start' ? this._startPaneState.size : this._endPaneState.size;
254✔
550
    return !!targetSize && targetSize.includes('%');
254✔
551
  }
254✔
552

9✔
553
  private _isAutoSize(which: PanePosition): boolean {
9✔
554
    const targetSize =
2,376✔
555
      which === 'start' ? this._startPaneState.size : this._endPaneState.size;
2,376✔
556
    return !!targetSize && targetSize === 'auto';
2,376✔
557
  }
2,376✔
558

9✔
559
  private _normalizeValue(
9✔
560
    value: string | undefined,
778✔
561
    fallback?: 'auto'
778✔
562
  ): string | undefined {
778✔
563
    const trimmed = value?.trim();
778✔
564
    if (!trimmed || trimmed === 'auto') {
778✔
565
      return fallback;
97✔
566
    }
97✔
567

681✔
568
    const numericValue = asNumber(trimmed, -1);
681✔
569
    if (numericValue < 0) return fallback;
778✔
570
    if (trimmed.includes('%') && numericValue > 100) return fallback;
778✔
571

668✔
572
    return trimmed;
668✔
573
  }
778✔
574

9✔
575
  private _getFlex(which: PanePosition): string {
9✔
576
    const grow = this._isAutoSize(which) ? 1 : 0;
1,120✔
577
    const shrink = 1;
1,120✔
578
    const size = this._isAutoSize(which)
1,120✔
579
      ? '0px'
530✔
580
      : which === 'start'
590✔
581
        ? this._startPaneState.size
293✔
582
        : this._endPaneState.size;
297✔
583
    return `${grow} ${shrink} ${size}`;
1,120✔
584
  }
1,120✔
585

9✔
586
  private _handleResizePanes(
9✔
587
    direction: -1 | 1,
90✔
588
    validOrientation: SplitterOrientation
90✔
589
  ): void {
90✔
590
    if (this._resizeDisallowed || this.orientation !== validOrientation) {
90✔
591
      return;
6✔
592
    }
6✔
593
    const delta = this._resolveDelta(10, 10, direction);
84✔
594

84✔
595
    this._resizeStart();
84✔
596
    this._resizing(delta);
84✔
597
    this._resizeEnd(delta);
84✔
598
  }
90✔
599

9✔
600
  @eventOptions({ passive: false })
9✔
601
  private _preventDefaultForEvent(e: Event): void {
9✔
NEW
602
    e.preventDefault();
×
NEW
603
  }
×
604

9✔
605
  private _resolveDelta(
9✔
606
    deltaX: number,
170✔
607
    deltaY: number,
170✔
608
    direction?: -1 | 1
170✔
609
  ): number {
170✔
610
    const isHorizontal = this.orientation === 'horizontal';
170✔
611
    const rtlMultiplier = isHorizontal && !isLTR(this) ? -1 : 1;
170✔
612
    const delta = isHorizontal ? deltaX : deltaY;
170✔
613
    return delta * rtlMultiplier * (direction ?? 1);
170✔
614
  }
170✔
615

9✔
616
  private _handleMinMaxResize(type: 'min' | 'max'): void {
9✔
617
    if (this._resizeDisallowed) {
4!
NEW
618
      return;
×
NEW
619
    }
×
620

4✔
621
    const totalSize = this._getTotalSize();
4✔
622
    const boundaryValue =
4✔
623
      type === 'min' ? this.startMinSize : this.startMaxSize;
4✔
624
    const isPercentage = boundaryValue
4✔
625
      ? boundaryValue.includes('%')
2✔
626
      : type === 'max';
2✔
627

4✔
628
    const targetStartSizePx =
4✔
629
      this._setMinMaxInPx('start', type) ?? (type === 'min' ? 0 : totalSize);
4✔
630
    const targetEndSizePx = totalSize - targetStartSizePx;
4✔
631

4✔
632
    if (isPercentage) {
4✔
633
      this.startSize = `${roundPrecise(asPercent(targetStartSizePx, totalSize), 2)}%`;
2✔
634
      this.endSize = `${roundPrecise(asPercent(targetEndSizePx, totalSize), 2)}%`;
2✔
635
    } else {
2✔
636
      this.startSize = `${targetStartSizePx}px`;
2✔
637
      this.endSize = `${targetEndSizePx}px`;
2✔
638
    }
2✔
639
  }
4✔
640

9✔
641
  private _handleExpanderStartAction(): void {
9✔
642
    const target = this._isEndCollapsed ? 'end' : 'start';
13✔
643
    this.toggle(target);
13✔
644
  }
13✔
645

9✔
646
  private _handleExpanderEndAction() {
9✔
647
    const target = this._isStartCollapsed ? 'start' : 'end';
13✔
648
    this.toggle(target);
13✔
649
  }
13✔
650

9✔
651
  private _handleArrowsExpandCollapse(
9✔
652
    target: PanePosition,
22✔
653
    validOrientation: SplitterOrientation
22✔
654
  ): void {
22✔
655
    if (this.disableCollapse || this.orientation !== validOrientation) {
22✔
656
      return;
4✔
657
    }
4✔
658
    let effectiveTarget = target;
18✔
659
    if (validOrientation === 'horizontal' && !isLTR(this)) {
22✔
660
      effectiveTarget = target === 'start' ? 'end' : 'start';
4✔
661
    }
4✔
662

18✔
663
    effectiveTarget === 'start'
18✔
664
      ? this._handleExpanderStartAction()
9✔
665
      : this._handleExpanderEndAction();
9✔
666
  }
22✔
667

9✔
668
  private _resizeStart(): void {
9✔
669
    const [startSize, endSize] = this._rectSize();
127✔
670

127✔
671
    this._resizeState = {
127✔
672
      ...this._resizeState,
127✔
673
      startPane: this._createPaneState('start', startSize),
127✔
674
      endPane: this._createPaneState('end', endSize),
127✔
675
    };
127✔
676
    this.emitEvent('igcResizeStart', {
127✔
677
      detail: { startPanelSize: startSize, endPanelSize: endSize },
127✔
678
    });
127✔
679
  }
127✔
680

9✔
681
  private _createPaneState(
9✔
682
    pane: PanePosition,
254✔
683
    size: number
254✔
684
  ): PaneResizeSnapshot {
254✔
685
    return {
254✔
686
      initialSize: size,
254✔
687
      isPercentageBased: this._isPercentageSize(pane) || this._isAutoSize(pane),
254✔
688
      minSizePx: this._setMinMaxInPx(pane, 'min'),
254✔
689
      maxSizePx: this._setMinMaxInPx(pane, 'max'),
254✔
690
    };
254✔
691
  }
254✔
692

9✔
693
  private _setMinMaxInPx(
9✔
694
    pane: PanePosition,
1,338✔
695
    type: 'min' | 'max'
1,338✔
696
  ): number | undefined {
1,338✔
697
    const paneState =
1,338✔
698
      pane === 'start' ? this._startPaneState : this._endPaneState;
1,338✔
699
    const value = type === 'max' ? paneState.maxSize : paneState.minSize;
1,338✔
700
    if (!value) {
1,338✔
701
      return undefined;
260✔
702
    }
260✔
703
    const totalSize = this._getTotalSize();
1,078✔
704
    const result = value.includes('%')
1,078✔
705
      ? (asNumber(value) / 100) * totalSize
255✔
706
      : asNumber(value);
823✔
707
    return result;
1,338✔
708
  }
1,338✔
709

9✔
710
  private _resizing(delta: number): void {
9✔
711
    const [startPaneSize, endPaneSize] = this._calcNewSizes(delta);
127✔
712

127✔
713
    this.startSize = `${startPaneSize}px`;
127✔
714
    this.endSize = `${endPaneSize}px`;
127✔
715

127✔
716
    this.emitEvent('igcResizing', {
127✔
717
      detail: {
127✔
718
        startPanelSize: startPaneSize,
127✔
719
        endPanelSize: endPaneSize,
127✔
720
        delta,
127✔
721
      },
127✔
722
    });
127✔
723
  }
127✔
724

9✔
725
  private _computeSize(pane: PaneResizeSnapshot, paneSize: number): string {
9✔
726
    const totalSize = this._getTotalSize();
254✔
727
    if (pane.isPercentageBased) {
254✔
728
      const percentPaneSize = asPercent(paneSize, totalSize);
158✔
729
      return `${percentPaneSize}%`;
158✔
730
    }
158✔
731
    return `${roundPrecise(paneSize, 0)}px`;
96✔
732
  }
254✔
733

9✔
734
  private _resizeEnd(delta: number): void {
9✔
735
    if (!this._resizeState.startPane || !this._resizeState.endPane) return;
127!
736
    const [startPaneSize, endPaneSize] = this._calcNewSizes(delta);
127✔
737

127✔
738
    this.startSize = this._computeSize(
127✔
739
      this._resizeState.startPane,
127✔
740
      startPaneSize
127✔
741
    );
127✔
742
    this.endSize = this._computeSize(this._resizeState.endPane, endPaneSize);
127✔
743

127✔
744
    this.emitEvent('igcResizeEnd', {
127✔
745
      detail: {
127✔
746
        startPanelSize: startPaneSize,
127✔
747
        endPanelSize: endPaneSize,
127✔
748
        delta,
127✔
749
      },
127✔
750
    });
127✔
751
  }
127✔
752

9✔
753
  private _rectSize(): [number, number] {
9✔
754
    const relevantDimension =
832✔
755
      this.orientation === 'horizontal' ? 'width' : 'height';
832✔
756
    const startPaneRect = this._startPane.getBoundingClientRect();
832✔
757
    const endPaneRect = this._endPane.getBoundingClientRect();
832✔
758
    return [startPaneRect[relevantDimension], endPaneRect[relevantDimension]];
832✔
759
  }
832✔
760

9✔
761
  private _calcNewSizes(delta: number): [number, number] {
9✔
762
    if (!this._resizeState.startPane || !this._resizeState.endPane)
254✔
763
      return [0, 0];
254!
764

254✔
765
    const start = this._resizeState.startPane;
254✔
766
    const end = this._resizeState.endPane;
254✔
767
    const minStart = start.minSizePx || 0;
254✔
768
    const minEnd = end.minSizePx || 0;
254✔
769
    const maxStart =
254✔
770
      start.maxSizePx || start.initialSize + end.initialSize - minEnd;
254✔
771
    const maxEnd =
254✔
772
      end.maxSizePx || start.initialSize + end.initialSize - minStart;
254✔
773

254✔
774
    const maxPosDelta = Math.min(
254✔
775
      maxStart - start.initialSize,
254✔
776
      end.initialSize - minEnd
254✔
777
    );
254✔
778
    const maxNegDelta = Math.min(
254✔
779
      start.initialSize - minStart,
254✔
780
      maxEnd - end.initialSize
254✔
781
    );
254✔
782
    const finalDelta = clamp(delta, -maxNegDelta, maxPosDelta);
254✔
783

254✔
784
    return [start.initialSize + finalDelta, end.initialSize - finalDelta];
254✔
785
  }
254✔
786

9✔
787
  private _getTotalSize(): number {
9✔
788
    if (!this._base) {
3,665!
NEW
789
      return 0;
×
NEW
790
    }
×
791

3,665✔
792
    const dimension = this.orientation === 'horizontal' ? 'width' : 'height';
3,665✔
793
    const barSize = this._barRef.value
3,665✔
794
      ? roundPrecise(this._barRef.value.getBoundingClientRect()[dimension])
3,665!
NEW
795
      : 0;
×
796

3,665✔
797
    const rect = this._base.getBoundingClientRect();
3,665✔
798
    const size = rect[dimension];
3,665✔
799
    return size - barSize;
3,665✔
800
  }
3,665✔
801

9✔
802
  private _resetPanes(): void {
9✔
803
    this._startPaneState = {
50✔
804
      ...this._startPaneState,
50✔
805
      size: 'auto',
50✔
806
      minSize: undefined,
50✔
807
      maxSize: undefined,
50✔
808
    };
50✔
809
    this._endPaneState = {
50✔
810
      ...this._endPaneState,
50✔
811
      size: 'auto',
50✔
812
      minSize: undefined,
50✔
813
      maxSize: undefined,
50✔
814
    };
50✔
815

50✔
816
    this._setPaneMinMaxSizes('start', '0', '100%');
50✔
817
    this._setPaneMinMaxSizes('end', '0', '100%');
50✔
818
  }
50✔
819

9✔
820
  private _updatePanes(): void {
9✔
821
    if (this._collapsedPane) {
560✔
822
      this._resetPanes();
32✔
823
    } else {
560✔
824
      this._setPaneMinMaxSizes(
528✔
825
        'start',
528✔
826
        this._startPaneState.minSize,
528✔
827
        this._startPaneState.maxSize
528✔
828
      );
528✔
829
      this._setPaneMinMaxSizes(
528✔
830
        'end',
528✔
831
        this._endPaneState.minSize,
528✔
832
        this._endPaneState.maxSize
528✔
833
      );
528✔
834
    }
528✔
835

560✔
836
    this._setPaneFlex(this._startPaneInternalStyles, this._getFlex('start'));
560✔
837
    this._setPaneFlex(this._endPaneInternalStyles, this._getFlex('end'));
560✔
838
    this._updateCursor();
560✔
839
  }
560✔
840

9✔
841
  private _setPaneMinMaxSizes(
9✔
842
    pane: PanePosition,
1,156✔
843
    minSize?: string,
1,156✔
844
    maxSize?: string
1,156✔
845
  ): void {
1,156✔
846
    const isHorizontal = this.orientation === 'horizontal';
1,156✔
847

1,156✔
848
    const min = this._ensureMinConstraintIsWithinBounds(pane, minSize) ?? 0;
1,156✔
849
    const max = maxSize ?? '100%';
1,156✔
850

1,156✔
851
    const sizes = isHorizontal
1,156✔
852
      ? {
792✔
853
          minWidth: min,
792✔
854
          maxWidth: max,
792✔
855
          minHeight: 0,
792✔
856
          maxHeight: '100%',
792✔
857
        }
792✔
858
      : {
364✔
859
          minWidth: 0,
364✔
860
          maxWidth: '100%',
364✔
861
          minHeight: min,
364✔
862
          maxHeight: max,
364✔
863
        };
364✔
864

1,156✔
865
    if (pane === 'start') {
1,156✔
866
      this._startPaneInternalStyles = {
578✔
867
        ...this._startPaneInternalStyles,
578✔
868
        ...sizes,
578✔
869
      };
578✔
870
    } else {
578✔
871
      this._endPaneInternalStyles = {
578✔
872
        ...this._endPaneInternalStyles,
578✔
873
        ...sizes,
578✔
874
      };
578✔
875
    }
578✔
876
  }
1,156✔
877

9✔
878
  private _ensureMinConstraintIsWithinBounds(
9✔
879
    pane: PanePosition,
1,156✔
880
    minSize?: string
1,156✔
881
  ): string | undefined {
1,156✔
882
    const totalSize = this._getTotalSize();
1,156✔
883

1,156✔
884
    let validatedMin = minSize;
1,156✔
885
    if (minSize && totalSize > 0) {
1,156✔
886
      const minPx = this._setMinMaxInPx(pane, 'min') ?? 0;
458✔
887

458✔
888
      const otherMinSize =
458✔
889
        pane === 'start'
458✔
890
          ? this._endPaneState.minSize
241✔
891
          : this._startPaneState.minSize;
217✔
892
      const otherMinPx = otherMinSize
458✔
893
        ? (this._setMinMaxInPx(pane === 'start' ? 'end' : 'start', 'min') ?? 0)
368!
894
        : 0;
90✔
895

458✔
896
      // Ignore constraint if it exceeds total or combined exceeds total to prevent content overflow
458✔
897
      // Once container grows to accommodate the constraint, it will be applied
458✔
898
      if (minPx > totalSize || minPx + otherMinPx > totalSize) {
458✔
899
        validatedMin = undefined;
14✔
900
      }
14✔
901
    }
458✔
902
    return validatedMin;
1,156✔
903
  }
1,156✔
904

9✔
905
  private _setPaneFlex(styles: StyleInfo, flex: string): void {
9✔
906
    Object.assign(styles, {
1,120✔
907
      flex: flex,
1,120✔
908
    });
1,120✔
909
  }
1,120✔
910

9✔
911
  private _handleExpanderClick(pane: PanePosition, event: PointerEvent): void {
9✔
912
    // Prevent resize action being initiated
8✔
913
    event.stopPropagation();
8✔
914

8✔
915
    pane === 'start'
8✔
916
      ? this._handleExpanderStartAction()
4✔
917
      : this._handleExpanderEndAction();
4✔
918
  }
8✔
919

9✔
920
  //#endregion
9✔
921

9✔
922
  //#region Rendering
9✔
923

9✔
924
  private _resolvePartNames(expander: PanePosition): Record<string, boolean> {
9✔
925
    if (expander === 'start') {
1,404✔
926
      return {
702✔
927
        'end-expand-btn': this._isEndCollapsed,
702✔
928
        'start-collapse-btn': !this._isEndCollapsed,
702✔
929
      };
702✔
930
    }
702✔
931

702✔
932
    return {
702✔
933
      'start-expand-btn': this._isStartCollapsed,
702✔
934
      'end-collapse-btn': !this._isStartCollapsed,
702✔
935
    };
702✔
936
  }
1,404✔
937

9✔
938
  private _getExpanderHiddenState(): {
9✔
939
    prevButtonHidden: boolean;
702✔
940
    nextButtonHidden: boolean;
702✔
941
  } {
702✔
942
    const hidden = this.disableCollapse || this.hideCollapseButtons;
702✔
943
    return {
702✔
944
      prevButtonHidden: hidden || !!this._isStartCollapsed,
702✔
945
      nextButtonHidden: hidden || !!this._isEndCollapsed,
702✔
946
    };
702✔
947
  }
702✔
948

9✔
949
  private _renderBarControls() {
9✔
950
    const dragHandleHidden = this.hideDragHandle || this.disableResize;
702✔
951
    const { prevButtonHidden, nextButtonHidden } =
702✔
952
      this._getExpanderHiddenState();
702✔
953

702✔
954
    return html`
702✔
955
      <div
702✔
956
        part="${partMap(this._resolvePartNames('start'))}"
702✔
957
        ?hidden=${prevButtonHidden}
702✔
958
        @pointerdown=${(e: PointerEvent) =>
702✔
959
          this._handleExpanderClick('start', e)}
702✔
960
      ></div>
702✔
961
      <div part="drag-handle" ?hidden=${dragHandleHidden}></div>
702✔
962
      <div
702✔
963
        part="${partMap(this._resolvePartNames('end'))}"
702✔
964
        ?hidden=${nextButtonHidden}
702✔
965
        @pointerdown=${(e: PointerEvent) => this._handleExpanderClick('end', e)}
702✔
966
      ></div>
702✔
967
    `;
702✔
968
  }
702✔
969

9✔
970
  protected override render() {
9✔
971
    const isDragging = this._resizeState.isDragging;
702✔
972
    const canResize = !this._resizeDisallowed;
702✔
973

702✔
974
    return html`
702✔
975
      <div part="base">
702✔
976
        <div
702✔
977
          part="start-pane"
702✔
978
          id="start-pane"
702✔
979
          style=${styleMap(this._startPaneInternalStyles)}
702✔
980
        >
702✔
981
          <slot name="start"></slot>
702✔
982
        </div>
702✔
983
        <div
702✔
984
          ${ref(this._barRef)}
702✔
985
          part="splitter-bar"
702✔
986
          role="separator"
702✔
987
          tabindex=${this._barTabIndex}
702✔
988
          aria-controls="start-pane end-pane"
702✔
989
          aria-orientation=${this.orientation}
702✔
990
          style=${styleMap(this._barInternalStyles)}
702✔
991
          @touchstart=${bindIf(canResize, this._preventDefaultForEvent)}
702✔
992
          @contextmenu=${bindIf(canResize, this._preventDefaultForEvent)}
702✔
993
          @pointerdown=${bindIf(canResize, this._handleBarPointerDown)}
702✔
994
          @pointermove=${bindIf(isDragging, this._handleBarPointerMove)}
702✔
995
          @pointerup=${bindIf(isDragging, this._handleEndDrag)}
702✔
996
          @lostpointercapture=${bindIf(isDragging, this._handleEndDrag)}
702✔
997
          @pointercancel=${bindIf(isDragging, this._endDrag)}
702✔
998
        >
702✔
999
          ${this._renderBarControls()}
702✔
1000
        </div>
702✔
1001
        <div
702✔
1002
          part="end-pane"
702✔
1003
          id="end-pane"
702✔
1004
          style=${styleMap(this._endPaneInternalStyles)}
702✔
1005
        >
702✔
1006
          <slot name="end"></slot>
702✔
1007
        </div>
702✔
1008
      </div>
702✔
1009
    `;
702✔
1010
  }
702✔
1011

9✔
1012
  //#endregion
9✔
1013
}
9✔
1014

9✔
1015
declare global {
9✔
1016
  interface HTMLElementTagNameMap {
9✔
1017
    'igc-splitter': IgcSplitterComponent;
9✔
1018
  }
9✔
1019
}
9✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc