diff --git a/CHANGELOG.md b/CHANGELOG.md
index 834140fb71f..a4ee7998ff3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,21 @@ All notable changes for each version of this project will be documented in this
### New Features
+- `igxSplitter` component added.
+ - Allows rendering a vertical or horizontal splitter with multiple splitter panes with templatable content.
+ Panes can be resized or collapsed/expanded via the UI. Splitter orientation is defined via the `type` input.
+
+ ```html
+
+
+ ...
+
+
+ ...
+
+
+ ```
+
- `IgxGrid`, `IgxTreeGrid`, `IgxHierarchicalGrid`
- Added ability to pin rows to top or bottom depending on the new `pinning` input.
And new API methods `pinRow` and `unpinRow`.
diff --git a/projects/igniteui-angular/src/lib/core/styles/components/splitter/_splitter-component.scss b/projects/igniteui-angular/src/lib/core/styles/components/splitter/_splitter-component.scss
new file mode 100644
index 00000000000..75da58e80a8
--- /dev/null
+++ b/projects/igniteui-angular/src/lib/core/styles/components/splitter/_splitter-component.scss
@@ -0,0 +1,52 @@
+/// @group components
+/// @author Simeon Simeonoff
+/// @author Maya Kirova
+/// @requires {mixin} bem-block
+/// @requires {mixin} bem-elem
+/// @requires {mixin} bem-mod
+////
+
+@include b(igx-splitter) {
+ // Register the component in the component registry
+ $this: str-slice(bem--selector-to-string(&), 2, -1);
+ @include register-component($this);
+
+ @include b(#{$this}-bar) {
+ @extend %igx-splitter-bar !optional;
+
+ @include e(handle) {
+ @extend %igx-splitter-handle !optional;
+ @extend %igx-splitter-handle--horizontal !optional;
+ }
+
+ @include e(expander, 'start') {
+ @extend %igx-splitter-expander !optional;
+ @extend %igx-splitter-expander--start !optional;
+ }
+
+ @include e(expander, 'end') {
+ @extend %igx-splitter-expander !optional;
+ @extend %igx-splitter-expander--end !optional;
+ }
+
+ @include m('vertical') {
+ @extend %igx-splitter-bar--vertical !optional;
+
+ @include e(handle) {
+ @extend %igx-splitter-handle !optional;
+ @extend %igx-splitter-handle--vertical !optional;
+ }
+
+ @include e(expander, 'start') {
+ @extend %igx-splitter-expander !optional;
+ @extend %igx-splitter-expander--start-vertical !optional;
+ }
+
+ @include e(expander, 'end') {
+ @extend %igx-splitter-expander !optional;
+ @extend %igx-splitter-expander--end-vertical !optional;
+ }
+ }
+ }
+}
+
diff --git a/projects/igniteui-angular/src/lib/core/styles/components/splitter/_splitter-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/splitter/_splitter-theme.scss
new file mode 100644
index 00000000000..b0733eb6575
--- /dev/null
+++ b/projects/igniteui-angular/src/lib/core/styles/components/splitter/_splitter-theme.scss
@@ -0,0 +1,206 @@
+////
+/// @group themes
+@function igx-splitter-theme(
+ $palette: $default-palette,
+ $schema: $light-schema,
+ $elevations: $elevations,
+
+ $bar-color: null,
+ $handle-color: null,
+ $expander-color: null,
+ $border-radius: null,
+ $size: null
+) {
+ $name: 'igx-splitter';
+ $splitter-schema: ();
+
+ @if map-has-key($schema, $name) {
+ $splitter-schema: map-get($schema, $name);
+ } @else {
+ $splitter-schema: $schema;
+ }
+
+ $border-radius-handle: round-borders(
+ if($border-radius, $border-radius, map-get($splitter-schema, 'border-radius')), 0, 2px
+ );
+
+ $theme: apply-palette($splitter-schema, $palette);
+
+ @if not($handle-color) and $bar-color {
+ $handle-color: text-contrast($bar-color);
+ }
+
+ @if not($expander-color) and $bar-color {
+ $expander-color: text-contrast($bar-color);
+ }
+
+ @return extend($theme, (
+ name: $name,
+ palette: $palette,
+ bar-color: $bar-color,
+ handle-color: $handle-color,
+ expander-color: $expander-color,
+ border-radius: $border-radius-handle,
+ size: $size
+ ));
+}
+
+/// @param {Map} $theme - The theme used to style the component.
+/// @requires {mixin} igx-root-css-vars
+/// @requires rem
+/// @requires --var
+@mixin igx-splitter($theme) {
+ @include igx-root-css-vars($theme);
+ $splitter-color: --var($theme, 'bar-color');
+ $hitbox-size: 4px;
+ $debug-hitbox: false;
+ $hitbox-debug-color: rgba(coral, .24);
+
+ %handle-area {
+ position: absolute;
+ content: '';
+ width: 100%;
+ height: $hitbox-size;
+ background: if($debug-hitbox, $hitbox-debug-color, transparent);
+ }
+
+ %handle-area--vertical {
+ width: $hitbox-size;
+ height: 100%;
+ }
+
+ %igx-splitter-bar {
+ position: relative;
+ display: flex;
+ flex-grow: 1;
+ justify-content: center;
+ align-items: center;
+ background: $splitter-color;
+ border: 1px solid $splitter-color;
+ cursor: row-resize;
+ z-index: 99;
+ opacity: .68;
+ transition: opacity .15s $ease-out-quad !important;
+
+ &::before {
+ @extend %handle-area;
+ top: 100%;
+ }
+
+ &::after {
+ @extend %handle-area;
+ bottom: 100%;
+ }
+
+ &:hover {
+ transition: all .25s ease-out;
+ opacity: 1;
+ }
+ }
+
+ %igx-splitter-bar--vertical {
+ flex-direction: column;
+ height: 100%;
+ cursor: col-resize;
+
+ &::before {
+ @extend %handle-area--vertical;
+ top: 0;
+ right: 100%;
+ }
+
+ &::after {
+ @extend %handle-area--vertical;
+ top: 0;
+ left: 100%;
+ }
+ }
+
+ %igx-splitter-handle {
+ background: --var($theme, 'handle-color');
+ border-radius: --var($theme, 'border-radius');
+ }
+
+ %igx-splitter-handle--horizontal {
+ width: 25%;
+ height: --var($theme, 'size');
+ margin: 0 rem(48px);
+ }
+
+ %igx-splitter-handle--vertical {
+ width: --var($theme, 'size');
+ height: 25%;
+ margin: rem(48px) 0;
+ }
+
+ %igx-splitter-hitbox {
+ position: absolute;
+ content: '';
+ background: if($debug-hitbox, $hitbox-debug-color, transparent);
+ }
+
+ %igx-splitter-expander {
+ position: relative;
+ width: 0;
+ height: 0;
+ border-right: --var($theme, 'size') solid transparent;
+ border-left: --var($theme, 'size') solid transparent;
+ cursor: pointer;
+ z-index: 1;
+ }
+
+ %igx-splitter-expander--start {
+ border-bottom: --var($theme, 'size') solid --var($theme, 'expander-color');
+
+ &::before {
+ @extend %igx-splitter-hitbox;
+ top: calc(100% - #{map-get($theme, 'size')});
+ left: calc(100% - #{map-get($theme, 'size') * 2});
+ width: #{map-get($theme, 'size') * 4};
+ height: #{map-get($theme, 'size') * 3};
+ }
+ }
+
+ %igx-splitter-expander--end {
+ border-bottom: unset;
+ border-top: --var($theme, 'size') solid --var($theme, 'expander-color');
+
+ &::before {
+ @extend %igx-splitter-hitbox;
+ top: calc(100% - #{map-get($theme, 'size') * 2});
+ left: calc(100% - #{map-get($theme, 'size') * 2});
+ width: #{map-get($theme, 'size') * 4};
+ height: #{map-get($theme, 'size') * 3};
+ }
+ }
+
+ %igx-splitter-expander--start-vertical {
+ border-top: --var($theme, 'size') solid transparent;
+ border-right: --var($theme, 'size') solid --var($theme, 'expander-color');
+ border-bottom: --var($theme, 'size') solid transparent;
+ border-left: unset;
+
+ &::before {
+ @extend %igx-splitter-hitbox;
+ top: calc(100% - #{map-get($theme, 'size') * 2});
+ left: calc(100% - #{map-get($theme, 'size')});
+ width: #{map-get($theme, 'size') * 3};
+ height: #{map-get($theme, 'size') * 4};
+ }
+ }
+
+ %igx-splitter-expander--end-vertical {
+ border-top: --var($theme, 'size') solid transparent;
+ border-right: unset;
+ border-bottom: --var($theme, 'size') solid transparent;
+ border-left: --var($theme, 'size') solid --var($theme, 'expander-color');
+
+ &::before {
+ @extend %igx-splitter-hitbox;
+ left: calc(100% - #{map-get($theme, 'size') * 2});
+ top: calc(100% - #{map-get($theme, 'size') * 2});
+ height: #{map-get($theme, 'size') * 4};
+ width: #{map-get($theme, 'size') * 3};
+ }
+ }
+}
diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/_core.scss b/projects/igniteui-angular/src/lib/core/styles/themes/_core.scss
index 8fc3548ad05..8e335642c48 100644
--- a/projects/igniteui-angular/src/lib/core/styles/themes/_core.scss
+++ b/projects/igniteui-angular/src/lib/core/styles/themes/_core.scss
@@ -53,6 +53,7 @@
@import '../components/radio/radio-component';
@import '../components/scrollbar/scrollbar-component';
@import '../components/slider/slider-component';
+@import '../components/splitter/splitter-component';
@import '../components/snackbar/snackbar-component';
@import '../components/switch/switch-component';
@import '../components/tabs/tabs-component';
diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/_index.scss b/projects/igniteui-angular/src/lib/core/styles/themes/_index.scss
index b8eb967d04d..bdacac534b4 100644
--- a/projects/igniteui-angular/src/lib/core/styles/themes/_index.scss
+++ b/projects/igniteui-angular/src/lib/core/styles/themes/_index.scss
@@ -38,6 +38,7 @@
@import '../components/switch/switch-theme';
@import '../components/snackbar/snackbar-theme';
@import '../components/slider/slider-theme';
+@import '../components/splitter/splitter-theme';
@import '../components/ripple/ripple-theme';
@import '../components/radio/radio-theme';
@import '../components/progress/progress-theme';
@@ -198,6 +199,14 @@
));
}
+ @if not(index($exclude, 'igx-splitter')) {
+ @include igx-splitter(igx-splitter-theme(
+ $palette,
+ $schema,
+ $border-radius: $roundness,
+ ));
+ }
+
@if not(index($exclude, 'igx-checkbox')) {
@include igx-checkbox(igx-checkbox-theme(
$palette,
diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_index.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_index.scss
index c7cdbec9843..a16012b71f8 100644
--- a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_index.scss
+++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_index.scss
@@ -38,6 +38,7 @@
@import './scrollbar';
@import './slider';
@import './snackbar';
+@import './splitter';
@import './switch';
@import './tabs';
@import './time-picker';
@@ -82,6 +83,7 @@
/// @property {Map} igx-scrollbar [$_dark-scrollbar]
/// @property {Map} igx-slider [$_dark-slider]
/// @property {Map} igx-snackbar [$_dark-snackbar]
+/// @property {Map} igx-splitter [$_dark-splitter]
/// @property {Map} igx-switch [$_dark-switch]
/// @property {Map} igx-tabs [$_dark-tabs]
/// @property {Map} igx-time-picker [$_dark-time-picker]
@@ -124,6 +126,7 @@ $dark-schema: (
igx-scrollbar: $_dark-scrollbar,
igx-slider: $_dark-slider,
igx-snackbar: $_dark-snackbar,
+ igx-splitter: $_dark-splitter,
igx-switch: $_dark-switch,
igx-tabs: $_dark-tabs,
igx-time-picker: $_dark-time-picker,
@@ -169,6 +172,7 @@ $dark-schema: (
/// @property {map} igx-scrollbar [$_dark-fluent-scrollbar],
/// @property {map} igx-slider [$_dark-fluent-slider],
/// @property {map} igx-snackbar [$_dark-fluent-snackbar],
+/// @property {map} igx-splitter [$_dark-fluent-splitter],
/// @property {map} igx-switch [$_dark-fluent-switch],
/// @property {map} igx-tabs [$_dark-fluent-tabs],
/// @property {map} igx-time-picker [$_dark-fluent-time-picker],
@@ -211,6 +215,7 @@ $dark-fluent-schema: (
igx-scrollbar: $_dark-fluent-scrollbar,
igx-slider: $_dark-fluent-slider,
igx-snackbar: $_dark-fluent-snackbar,
+ igx-splitter: $_dark-fluent-splitter,
igx-switch: $_dark-fluent-switch,
igx-tabs: $_dark-fluent-tabs,
igx-time-picker: $_dark-fluent-time-picker,
@@ -256,6 +261,7 @@ $dark-fluent-schema: (
/// @property {map} igx-scrollbar [$_dark-bootstrap-scrollbar],
/// @property {map} igx-slider [$_dark-bootstrap-slider],
/// @property {map} igx-snackbar [$_dark-bootstrap-snackbar],
+/// @property {map} igx-splitter [$_dark-bootstrap-splitter],
/// @property {map} igx-switch [$_dark-bootstrap-switch],
/// @property {map} igx-tabs [$_dark-bootstrap-tabs],
/// @property {map} igx-time-picker [$_dark-bootstrap-time-picker],
@@ -298,6 +304,7 @@ $dark-bootstrap-schema: (
igx-scrollbar: $_dark-bootstrap-scrollbar,
igx-slider: $_dark-bootstrap-slider,
igx-snackbar: $_dark-bootstrap-snackbar,
+ igx-splitter: $_dark-bootstrap-splitter,
igx-switch: $_dark-bootstrap-switch,
igx-tabs: $_dark-bootstrap-tabs,
igx-time-picker: $_dark-bootstrap-time-picker,
diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_scrollbar.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_scrollbar.scss
index 1ae63442f09..daa82c7bf96 100644
--- a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_scrollbar.scss
+++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_scrollbar.scss
@@ -20,11 +20,6 @@ $_dark-scrollbar: extend(
igx-color: 'surface',
lighten: 20%
),
-
- track-background: (
- igx-color: 'surface',
- lighten: 5%
- ),
)
);
diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_splitter.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_splitter.scss
new file mode 100644
index 00000000000..27f81fe897c
--- /dev/null
+++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_splitter.scss
@@ -0,0 +1,39 @@
+@import '../light/splitter';
+
+////
+/// @group schemas
+/// @access private
+/// @author Simeon Simeonoff
+////
+
+/// Generates a dark splitter schema.
+/// @type {Map}
+/// @requires {function} extend
+/// @requires $_light-splitter
+$_dark-splitter: extend(
+ $_light-splitter,
+ (
+ handle-color: (
+ igx-color: 'surface',
+ lighten: 20%
+ ),
+
+ expander-color: (
+ igx-color: 'surface',
+ lighten: 20%
+ ),
+ )
+);
+
+/// Generates a fluent splitter schema.
+/// @type {Map}
+/// @requires {function} extend
+/// @requires $_fluent-splitter
+$_dark-fluent-splitter: extend($_fluent-splitter);
+
+/// Generates a bootstrap splitter schema.
+/// @type {Map}
+/// @requires {function} extend
+/// @requires $_bootstrap-splitter
+$_dark-bootstrap-splitter: extend($_bootstrap-splitter);
+
diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_index.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_index.scss
index c91b32b9d8a..4f67c8e036e 100644
--- a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_index.scss
+++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_index.scss
@@ -39,6 +39,7 @@
@import './scrollbar';
@import './slider';
@import './snackbar';
+@import './splitter';
@import './switch';
@import './tabs';
@import './time-picker';
@@ -83,6 +84,7 @@
/// @property {Map} igx-scrollbar [$_light-scrollbar]
/// @property {Map} igx-slider [$_light-slider]
/// @property {Map} igx-snackbar [$_light-snackbar]
+/// @property {Map} igx-splitter [$_light-splitter]
/// @property {Map} igx-switch [$_light-switch]
/// @property {Map} igx-tabs [$_light-tabs]
/// @property {Map} igx-time-picker [$_light-time-picker]
@@ -125,6 +127,7 @@ $light-schema: (
igx-scrollbar: $_light-scrollbar,
igx-slider: $_light-slider,
igx-snackbar: $_light-snackbar,
+ igx-splitter: $_light-splitter,
igx-switch: $_light-switch,
igx-tabs: $_light-tabs,
igx-time-picker: $_light-time-picker,
@@ -169,6 +172,7 @@ $light-fluent-schema: (
igx-scrollbar: $_fluent-scrollbar,
igx-slider: $_fluent-slider,
igx-snackbar: $_fluent-snackbar,
+ igx-splitter: $_fluent-splitter,
igx-switch: $_fluent-switch,
igx-tabs: $_fluent-tabs,
igx-time-picker: $_fluent-time-picker,
@@ -213,6 +217,7 @@ $light-bootstrap-schema: (
igx-scrollbar: $_bootstrap-scrollbar,
igx-slider: $_bootstrap-slider,
igx-snackbar: $_bootstrap-snackbar,
+ igx-splitter: $_bootstrap-splitter,
igx-switch: $_bootstrap-switch,
igx-tabs: $_bootstrap-tabs,
igx-time-picker: $_bootstrap-time-picker,
diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_splitter.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_splitter.scss
new file mode 100644
index 00000000000..08b0b2e9777
--- /dev/null
+++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_splitter.scss
@@ -0,0 +1,57 @@
+@import '../shape/splitter';
+
+////
+/// @group schemas
+/// @access private
+/// @author Simeon Simeonoff
+////
+
+/// Generates a light splitter schema.
+/// @type {Map}
+/// @property {Map} icon-background [igx-color: ('grays', 400)]- The background color used for splitters of type icon.
+/// @property {Map} icon-color [igx-color: ('grays', 800)] - The icon color used for splitters of type icon.
+/// @property {Map} initials-background [igx-color: ('grays', 400)] - The background color used for splitters of type initials.
+/// @property {Map} initials-color [igx-color: ('grays', 800)] - The text color used for splitters of type initials.
+/// @property {Color} image-background [transparent] - The background color used for splitters of type image.
+/// @see $default-palette
+$_light-splitter: extend(
+ $_default-shape-splitter,
+ (
+ bar-color: (
+ igx-color: 'surface',
+ darken: 5%
+ ),
+
+ handle-color: (
+ igx-color: 'surface',
+ darken: 20%
+ ),
+
+ expander-color: (
+ igx-color: 'surface',
+ darken: 20%
+ ),
+
+ size: 4px
+ )
+);
+
+/// Generates a fluent splitter schema.
+/// @type {Map}
+/// @requires {function} extend
+/// @requires $_light-splitter
+$_fluent-splitter: extend($_light-splitter);
+
+/// Generates a bootstrap splitter schema.
+/// @type {Map}
+/// @requires {function} extend
+/// @requires $_light-splitter
+/// @requires $_bootstrap-shape-splitter
+$_bootstrap-splitter: extend(
+ $_light-splitter,
+ $_bootstrap-shape-splitter,
+ (
+ variant: 'bootstrap',
+ )
+);
+
diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/shape/_splitter.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/shape/_splitter.scss
new file mode 100644
index 00000000000..6d82113c37e
--- /dev/null
+++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/shape/_splitter.scss
@@ -0,0 +1,23 @@
+////
+/// @group schemas
+/// @access private
+/// @author Simeon Simeonoff
+////
+$_default-shape-splitter: (
+ border-radius: 1,
+);
+
+/// @type Map
+$_round-shape-splitter: (
+ border-radius: 1,
+);
+
+/// @type Map
+$_square-shape-splitter: (
+ border-radius: 0,
+);
+
+/// @type Map
+$_bootstrap-shape-splitter: (
+ border-radius: 1,
+);
diff --git a/projects/igniteui-angular/src/lib/splitter/splitter-bar/splitter-bar.component.html b/projects/igniteui-angular/src/lib/splitter/splitter-bar/splitter-bar.component.html
new file mode 100644
index 00000000000..3cda47ff07c
--- /dev/null
+++ b/projects/igniteui-angular/src/lib/splitter/splitter-bar/splitter-bar.component.html
@@ -0,0 +1,12 @@
+
diff --git a/projects/igniteui-angular/src/lib/splitter/splitter-bar/splitter-bar.component.ts b/projects/igniteui-angular/src/lib/splitter/splitter-bar/splitter-bar.component.ts
new file mode 100644
index 00000000000..f730f0ea018
--- /dev/null
+++ b/projects/igniteui-angular/src/lib/splitter/splitter-bar/splitter-bar.component.ts
@@ -0,0 +1,214 @@
+import { Component, Input, HostBinding, EventEmitter, Output, HostListener } from '@angular/core';
+import { SplitterType } from '../splitter.component';
+import { IgxSplitterPaneComponent } from '../splitter-pane/splitter-pane.component';
+import { IDragMoveEventArgs, IDragStartEventArgs, DragDirection } from '../../directives/drag-drop/drag-drop.directive';
+
+
+export const SPLITTER_INTERACTION_KEYS = new Set('right down left up arrowright arrowdown arrowleft arrowup'.split(' '));
+
+/**
+ * Provides reference to `SplitBarComponent` component.
+ * Represents the draggable gripper that visually separates panes and allows for changing their sizes.
+ * @export
+ * @class SplitBarComponent
+ */
+@Component({
+ selector: 'igx-splitter-bar',
+ templateUrl: './splitter-bar.component.html'
+})
+export class IgxSplitBarComponent {
+
+ /**
+ * Sets/gets `IgxSplitBarComponent` orientation.
+ * @type SplitterType
+ */
+ @Input()
+ public type: SplitterType = SplitterType.Vertical;
+
+ /**
+ * Sets/gets `IgxSplitBarComponent` element order.
+ * @type SplitterType
+ */
+ @HostBinding('style.order')
+ @Input()
+ public order!: number;
+
+ /**
+ * @hidden
+ * @internal
+ */
+ @HostBinding('attr.tabindex')
+ public tabindex = 0;
+
+ /**
+ * Sets/gets the `SplitPaneComponent` associated with the current `SplitBarComponent`.
+ * @memberof SplitBarComponent
+ */
+ @Input()
+ public pane!: IgxSplitterPaneComponent;
+
+ /**
+ * Sets/Gets the `SplitPaneComponent` sibling components associated with the current `SplitBarComponent`.
+ */
+ @Input()
+ public siblings!: Array;
+
+ /**
+ * An event that is emitted whenever we start dragging the current `SplitBarComponent`.
+ * @memberof SplitBarComponent
+ */
+ @Output()
+ public moveStart = new EventEmitter();
+
+ /**
+ * An event that is emitted while we are dragging the current `SplitBarComponent`.
+ * @memberof SplitBarComponent
+ */
+ @Output()
+ public moving = new EventEmitter();
+
+ /**
+ * An event that is emitted when collapsing the pane
+ */
+ @Output()
+ public togglePane = new EventEmitter();
+ /**
+ * A temporary holder for the pointer coordinates.
+ * @private
+ * @memberof SplitBarComponent
+ */
+ private startPoint!: number;
+
+ /**
+ * @hidden @internal
+ */
+ public get prevButtonHidden() {
+ return this.siblings[0].hidden && !this.siblings[1].hidden;
+ }
+
+ /**
+ * @hidden @internal
+ */
+ @HostListener('keydown', ['$event'])
+ keyEvent(event: KeyboardEvent) {
+ const key = event.key.toLowerCase();
+ const ctrl = event.ctrlKey;
+ event.stopPropagation();
+ if (SPLITTER_INTERACTION_KEYS.has(key)) {
+ event.preventDefault();
+ }
+ switch (key) {
+ case 'arrowup':
+ case 'up':
+ if (this.type === SplitterType.Vertical) {
+ if (ctrl) {
+ this.onCollapsing(false);
+ break;
+ }
+ if (!this.resizeDisallowed) {
+ event.preventDefault();
+ this.moveStart.emit(this.pane);
+ this.moving.emit(10);
+ }
+ }
+ break;
+ case 'arrowdown':
+ case 'down':
+ if (this.type === SplitterType.Vertical) {
+ if (ctrl) {
+ this.onCollapsing(true);
+ break;
+ }
+ if (!this.resizeDisallowed) {
+ event.preventDefault();
+ this.moveStart.emit(this.pane);
+ this.moving.emit(-10);
+ }
+ }
+ break;
+ case 'arrowleft':
+ case 'left':
+ if (this.type === SplitterType.Horizontal) {
+ if (ctrl) {
+ this.onCollapsing(false);
+ break;
+ }
+ if (!this.resizeDisallowed) {
+ event.preventDefault();
+ this.moveStart.emit(this.pane);
+ this.moving.emit(10);
+ }
+ }
+ break;
+ case 'arrowright':
+ case 'right':
+ if (this.type === SplitterType.Horizontal) {
+ if (ctrl) {
+ this.onCollapsing(true);
+ break;
+ }
+ if (!this.resizeDisallowed) {
+ event.preventDefault();
+ this.moveStart.emit(this.pane);
+ this.moving.emit(-10);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * @hidden @internal
+ */
+ public get dragDir() {
+ return this.type === SplitterType.Horizontal ? DragDirection.VERTICAL : DragDirection.HORIZONTAL;
+ }
+
+ /**
+ * @hidden @internal
+ */
+ public get nextButtonHidden() {
+ return this.siblings[1].hidden && !this.siblings[0].hidden;
+ }
+
+ public onDragStart(event: IDragStartEventArgs) {
+ if (this.resizeDisallowed) {
+ event.cancel = true;
+ return;
+ }
+ this.startPoint = this.type === SplitterType.Horizontal ? event.startX : event.startY;
+ this.moveStart.emit(this.pane);
+ }
+
+ public onDragMove(event: IDragMoveEventArgs) {
+ const isHorizontal = this.type === SplitterType.Horizontal;
+ const curr = isHorizontal ? event.pageX : event.pageY;
+ const delta = this.startPoint - curr;
+ if (delta !== 0) {
+ this.moving.emit(delta);
+ event.cancel = true;
+ event.owner.element.nativeElement.style.transform = '';
+ }
+ }
+
+ protected get resizeDisallowed() {
+ const relatedTabs = this.siblings;
+ return !!relatedTabs.find(x => x.resizable === false);
+ }
+
+ public onCollapsing(next: boolean) {
+ const prevSibling = this.siblings[0];
+ const nextSibling = this.siblings[1];
+ let target;
+ if (next) {
+ // if next is clicked when prev pane is hidden, show prev pane, else hide next pane.
+ target = prevSibling.hidden ? prevSibling : nextSibling;
+ } else {
+ // if prev is clicked when next pane is hidden, show next pane, else hide prev pane.
+ target = nextSibling.hidden ? nextSibling : prevSibling;
+ }
+ this.togglePane.emit(target);
+ }
+}
diff --git a/projects/igniteui-angular/src/lib/splitter/splitter-pane/splitter-pane.component.html b/projects/igniteui-angular/src/lib/splitter/splitter-pane/splitter-pane.component.html
new file mode 100644
index 00000000000..95a0b70bdc7
--- /dev/null
+++ b/projects/igniteui-angular/src/lib/splitter/splitter-pane/splitter-pane.component.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/projects/igniteui-angular/src/lib/splitter/splitter-pane/splitter-pane.component.ts b/projects/igniteui-angular/src/lib/splitter/splitter-pane/splitter-pane.component.ts
new file mode 100644
index 00000000000..6921ac02016
--- /dev/null
+++ b/projects/igniteui-angular/src/lib/splitter/splitter-pane/splitter-pane.component.ts
@@ -0,0 +1,131 @@
+import { Component, HostBinding, Input, ElementRef, Output, EventEmitter } from '@angular/core';
+
+/**
+ * Provides reference to `SplitPaneComponent` component.
+ * Represents individual resizable panes. Users can control the resize behavior via the min and max size properties.
+ * @export
+ * @class SplitPaneComponent
+ */
+@Component({
+ selector: 'igx-splitter-pane',
+ templateUrl: './splitter-pane.component.html'
+})
+export class IgxSplitterPaneComponent {
+
+ public _size = 'auto';
+ /**
+ * Sets/gets the size of the current `IgxSplitterPaneComponent`.
+ */
+ @Input()
+ get size() {
+ return this._size;
+ }
+
+ set size(value) {
+ this._size = value;
+ this.el.nativeElement.style.flex = this.flex;
+ this.sizeChange.emit(value);
+ }
+
+ /**
+ * @hidden @internal
+ */
+ @Output()
+ public sizeChange = new EventEmitter();
+
+ /**
+ * Sets/gets the minimum allowable size of the current `IgxSplitterPaneComponent`.
+ */
+ @Input()
+ public minSize!: string;
+
+ /**
+ * Sets/gets the maximum allowable size of the current `IgxSplitterPaneComponent`.
+ */
+ @Input()
+ public maxSize!: string;
+
+ /**
+ * Sets/Gets whether pane is resizable.
+ */
+ @Input()
+ public resizable = true;
+
+
+ /**
+ * Sets/gets the `order` property of the current `IgxSplitterPaneComponent`.
+ */
+ @Input()
+ @HostBinding('style.order')
+ public order!: number;
+
+ /**
+ * Gets the host native element.
+ * @readonly
+ * @type *
+ */
+ public get element(): any {
+ return this.el.nativeElement;
+ }
+
+ /**
+ * Sets/gets the `overflow` property of the current `IgxSplitterPaneComponent`.
+ */
+ @HostBinding('style.overflow')
+ public overflow = 'auto';
+
+ /**
+ * Sets/gets the `minHeight` and `minWidth` propertis of the current `IgxSplitterPaneComponent`.
+ */
+ @HostBinding('style.min-height')
+ @HostBinding('style.min-width')
+ public minHeight = 0;
+
+ /**
+ * Sets/gets the `maxHeight` and `maxWidth` propertis of the current `IgxSplitterPaneComponent`.
+ */
+ @HostBinding('style.max-height')
+ @HostBinding('style.max-width')
+ public maxHeight = '100%';
+
+ /**
+ * Gets the `flex` property of the current `IgxSplitterPaneComponent`.
+ * @readonly
+ */
+ @HostBinding('style.flex')
+ public get flex() {
+ const grow = this.size !== 'auto' ? 0 : 1;
+ const shrink = this.size !== 'auto' ? 0 : 1;
+
+ return `${grow} ${shrink} ${this.size}`;
+ }
+
+ /**
+ * Sets/gets the 'display' property of the current `IgxSplitterPaneComponent`
+ */
+ @HostBinding('style.display')
+ public display = 'flex';
+
+ private _hidden = false;
+
+ /**
+ * Sets/gets whether current `IgxSplitterPanecomponent` is hidden
+ */
+ @Input()
+ public set hidden(value) {
+ this._hidden = value;
+ this.display = this._hidden ? 'none' : 'flex' ;
+ }
+
+ public get hidden() {
+ return this._hidden;
+ }
+
+ /**
+ * Event fired when collapsing and changing the hidden state of the current pane
+ */
+ @Output()
+ public onPaneToggle = new EventEmitter();
+
+ constructor(private el: ElementRef) { }
+}
diff --git a/projects/igniteui-angular/src/lib/splitter/splitter.component.html b/projects/igniteui-angular/src/lib/splitter/splitter.component.html
new file mode 100644
index 00000000000..087c7895f52
--- /dev/null
+++ b/projects/igniteui-angular/src/lib/splitter/splitter.component.html
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/projects/igniteui-angular/src/lib/splitter/splitter.component.spec.ts b/projects/igniteui-angular/src/lib/splitter/splitter.component.spec.ts
new file mode 100644
index 00000000000..b43fbf13c88
--- /dev/null
+++ b/projects/igniteui-angular/src/lib/splitter/splitter.component.spec.ts
@@ -0,0 +1,357 @@
+import { IgxSplitterModule } from './splitter.module';
+import { configureTestSuite } from '../test-utils/configure-suite';
+import { TestBed, async } from '@angular/core/testing';
+import { Component, ViewChild, DebugElement } from '@angular/core';
+import { SplitterType, IgxSplitterComponent } from './splitter.component';
+import { By } from '@angular/platform-browser';
+import { UIInteractions } from '../test-utils/ui-interactions.spec';
+
+
+const SPLITTERBAR_CLASS = 'igx-splitter-bar';
+const SPLITTERBAR_DIV_CLASS = '.igx-splitter-bar';
+const SPLITTER_BAR_VERTICAL_CLASS = 'igx-splitter-bar--vertical';
+
+describe('IgxSplitter', () => {
+ configureTestSuite();
+ beforeAll(async(() => {
+
+ return TestBed.configureTestingModule({
+ declarations: [ SplitterTestComponent ],
+ imports: [
+ IgxSplitterModule
+ ]
+ }).compileComponents();
+ }));
+
+ let fixture, splitter;
+ beforeEach(async(() => {
+ fixture = TestBed.createComponent(SplitterTestComponent);
+ fixture.detectChanges();
+ splitter = fixture.componentInstance.splitter;
+ }));
+
+ it('should render pane content correctly in splitter.', () => {
+ expect(splitter.panes.length).toBe(2);
+ const firstPane = splitter.panes.toArray()[0].element;
+ const secondPane = splitter.panes.toArray()[1].element;
+ expect(firstPane.textContent.trim()).toBe('Pane 1');
+ expect(secondPane.textContent.trim()).toBe('Pane 2');
+
+ const splitterBar = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS)).nativeElement;
+ expect(firstPane.style.order).toBe('0');
+ expect(splitterBar.style.order).toBe('1');
+ expect(secondPane.style.order).toBe('2');
+ });
+
+ it('should render vertical splitter.', () => {
+ fixture.componentInstance.type = SplitterType.Vertical;
+ fixture.detectChanges();
+
+ const splitterBarDIV = fixture.debugElement.query(By.css(SPLITTERBAR_DIV_CLASS));
+ const hasVerticalClass = splitterBarDIV.nativeElement.classList.contains(SPLITTER_BAR_VERTICAL_CLASS);
+ expect(hasVerticalClass).toBeFalsy();
+ });
+ it('should render horizontal splitter.', () => {
+ const splitterBarDIV = fixture.debugElement.query(By.css(SPLITTERBAR_DIV_CLASS));
+ const hasVerticalClass = splitterBarDIV.nativeElement.classList.contains(SPLITTER_BAR_VERTICAL_CLASS);
+ expect(hasVerticalClass).toBeTruthy();
+ });
+ it('should allow resizing vertical splitter', () => {
+ fixture.componentInstance.type = SplitterType.Vertical;
+ fixture.detectChanges();
+ const pane1 = splitter.panes.toArray()[0];
+ const pane2 = splitter.panes.toArray()[1];
+ expect(pane1.size).toBe('auto');
+ expect(pane2.size).toBe('auto');
+ const pane1_originalSize = pane1.element.offsetHeight;
+ const pane2_originalSize = pane2.element.offsetHeight;
+ const splitterBarComponent = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS)).context;
+ splitterBarComponent.moveStart.emit(pane1);
+ splitterBarComponent.moving.emit(-100);
+ fixture.detectChanges();
+ expect(pane1.size).toBe(pane1_originalSize + 100 + 'px');
+ expect(pane2.size).toBe(pane2_originalSize - 100 + 'px');
+
+ splitterBarComponent.moving.emit(100);
+ fixture.detectChanges();
+ expect(pane1.size).toBe(pane1_originalSize - 100 + 'px');
+ expect(pane2.size).toBe(pane2_originalSize + 100 + 'px');
+ });
+ it('should allow resizing horizontal splitter', () => {
+ const pane1 = splitter.panes.toArray()[0];
+ const pane2 = splitter.panes.toArray()[1];
+ expect(pane1.size).toBe('auto');
+ expect(pane2.size).toBe('auto');
+ const pane1_originalSize = pane1.element.offsetWidth;
+ const pane2_originalSize = pane2.element.offsetWidth;
+ const splitterBarComponent = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS)).context;
+ splitterBarComponent.moveStart.emit(pane1);
+ splitterBarComponent.moving.emit(-100);
+ fixture.detectChanges();
+
+ expect(parseFloat(pane1.size)).toBeCloseTo(pane1_originalSize + 100, 0);
+ expect(parseFloat(pane2.size)).toBeCloseTo(pane2_originalSize - 100, 0);
+
+ splitterBarComponent.moving.emit(100);
+ fixture.detectChanges();
+ expect(parseFloat(pane1.size)).toBeCloseTo(pane1_originalSize - 100, 0);
+ expect(parseFloat(pane2.size)).toBeCloseTo(pane2_originalSize + 100, 0);
+ });
+ it('should honor minSize/maxSize when resizing.', () => {
+ fixture.componentInstance.type = SplitterType.Vertical;
+ fixture.detectChanges();
+ const pane1 = splitter.panes.toArray()[0];
+ const pane2 = splitter.panes.toArray()[1];
+ pane1.minSize = '100px';
+ pane1.maxSize = '300px';
+ fixture.detectChanges();
+
+ const splitterBarComponent = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS)).context;
+ splitterBarComponent.moveStart.emit(pane1);
+ splitterBarComponent.moving.emit(100);
+ splitterBarComponent.moveStart.emit(pane1);
+ splitterBarComponent.moving.emit(100);
+ fixture.detectChanges();
+ expect(pane1.size).toBe('100px');
+ expect(pane2.size).toBe('300px');
+
+ splitterBarComponent.moveStart.emit(pane1);
+ splitterBarComponent.moving.emit(-200);
+ splitterBarComponent.moveStart.emit(pane1);
+ splitterBarComponent.moving.emit(-50);
+ fixture.detectChanges();
+ expect(pane1.size).toBe('300px');
+ expect(pane2.size).toBe('100px');
+ });
+
+ it('should not allow drag resize if resizable is set to false.', () => {
+ const pane1 = splitter.panes.toArray()[0];
+ pane1.resizable = false;
+ const splitterBarComponent = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS)).context;
+ const args = {cancel: false};
+ splitterBarComponent.onDragStart(args);
+ expect(args.cancel).toBeTruthy();
+ });
+
+ it('should allow resizing with up/down arrow keys', () => {
+ fixture.componentInstance.type = SplitterType.Vertical;
+ fixture.detectChanges();
+ const pane1 = splitter.panes.toArray()[0];
+ const pane2 = splitter.panes.toArray()[1];
+ expect(pane1.size).toBe('auto');
+ expect(pane2.size).toBe('auto');
+ const pane1_originalSize = pane1.element.offsetHeight;
+ const pane2_originalSize = pane2.element.offsetHeight;
+ const splitterBarComponent: DebugElement = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS));
+ splitterBarComponent.nativeElement.focus();
+ UIInteractions.triggerEventHandlerKeyDown('ArrowUp', splitterBarComponent);
+ fixture.detectChanges();
+ expect(pane1.size).toBe(pane1_originalSize - 10 + 'px');
+ expect(pane2.size).toBe(pane2_originalSize + 10 + 'px');
+
+ UIInteractions.triggerEventHandlerKeyDown('ArrowDown', splitterBarComponent);
+ UIInteractions.triggerEventHandlerKeyDown('ArrowDown', splitterBarComponent);
+ fixture.detectChanges();
+ expect(pane1.size).toBe(pane1_originalSize + 10 + 'px');
+ expect(pane2.size).toBe(pane2_originalSize - 10 + 'px');
+
+ pane2.resizable = false;
+ UIInteractions.triggerEventHandlerKeyDown('ArrowDown', splitterBarComponent);
+ fixture.detectChanges();
+ expect(pane1.size).toBe(pane1_originalSize + 10 + 'px');
+ expect(pane2.size).toBe(pane2_originalSize - 10 + 'px');
+ });
+
+ it('should allow resizing with left/right arrow keys', () => {
+ fixture.componentInstance.type = SplitterType.Horizontal;
+ fixture.detectChanges();
+ const pane1 = splitter.panes.toArray()[0];
+ const pane2 = splitter.panes.toArray()[1];
+ expect(pane1.size).toBe('auto');
+ expect(pane2.size).toBe('auto');
+ const pane1_originalSize = pane1.element.offsetWidth;
+ const pane2_originalSize = pane2.element.offsetWidth;
+ const splitterBarComponent: DebugElement = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS));
+ splitterBarComponent.nativeElement.focus();
+ UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', splitterBarComponent);
+ fixture.detectChanges();
+ expect(parseFloat(pane1.size)).toBeCloseTo(pane1_originalSize - 10, 0);
+ expect(parseFloat(pane2.size)).toBeCloseTo(pane2_originalSize + 10, 0);
+
+ UIInteractions.triggerEventHandlerKeyDown('ArrowRight', splitterBarComponent);
+ UIInteractions.triggerEventHandlerKeyDown('ArrowRight', splitterBarComponent);
+ fixture.detectChanges();
+ expect(parseFloat(pane1.size)).toBeCloseTo(pane1_originalSize + 10, 0);
+ expect(parseFloat(pane2.size)).toBeCloseTo(pane2_originalSize - 10, 0);
+
+ pane1.resizable = false;
+ UIInteractions.triggerEventHandlerKeyDown('ArrowRight', splitterBarComponent);
+ fixture.detectChanges();
+ expect(parseFloat(pane1.size)).toBeCloseTo(pane1_originalSize + 10, 0);
+ expect(parseFloat(pane2.size)).toBeCloseTo(pane2_originalSize - 10, 0);
+ });
+
+ it('should allow expand/collapse with Ctrl + up/down arrow keys', () => {
+ fixture.componentInstance.type = SplitterType.Vertical;
+ fixture.detectChanges();
+ const pane1 = splitter.panes.toArray()[0];
+ const pane2 = splitter.panes.toArray()[1];
+ expect(pane1.size).toBe('auto');
+ expect(pane2.size).toBe('auto');
+ const splitterBarComponent: DebugElement = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS));
+ splitterBarComponent.nativeElement.focus();
+ UIInteractions.triggerEventHandlerKeyDown('ArrowUp', splitterBarComponent, false, false, true);
+ fixture.detectChanges();
+ expect(pane1.hidden).toBeTruthy();
+ UIInteractions.triggerEventHandlerKeyDown('ArrowDown', splitterBarComponent, false, false, true);
+ fixture.detectChanges();
+ expect(pane1.hidden).toBeFalsy();
+ UIInteractions.triggerEventHandlerKeyDown('ArrowDown', splitterBarComponent, false, false, true);
+ fixture.detectChanges();
+ expect(pane2.hidden).toBeTruthy();
+ UIInteractions.triggerEventHandlerKeyDown('ArrowUp', splitterBarComponent, false, false, true);
+ fixture.detectChanges();
+ expect(pane2.hidden).toBeFalsy();
+ });
+
+ it('should allow expand/collapse with Ctrl + left/right arrow keys', () => {
+ fixture.componentInstance.type = SplitterType.Horizontal;
+ fixture.detectChanges();
+ const pane1 = splitter.panes.toArray()[0];
+ const pane2 = splitter.panes.toArray()[1];
+ expect(pane1.size).toBe('auto');
+ expect(pane2.size).toBe('auto');
+ const splitterBarComponent: DebugElement = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS));
+ splitterBarComponent.nativeElement.focus();
+ UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', splitterBarComponent, false, false, true);
+ fixture.detectChanges();
+ expect(pane1.hidden).toBeTruthy();
+ UIInteractions.triggerEventHandlerKeyDown('ArrowRight', splitterBarComponent, false, false, true);
+ fixture.detectChanges();
+ expect(pane1.hidden).toBeFalsy();
+ UIInteractions.triggerEventHandlerKeyDown('ArrowRight', splitterBarComponent, false, false, true);
+ fixture.detectChanges();
+ expect(pane2.hidden).toBeTruthy();
+ UIInteractions.triggerEventHandlerKeyDown('ArrowLeft', splitterBarComponent, false, false, true);
+ fixture.detectChanges();
+ expect(pane2.hidden).toBeFalsy();
+ });
+
+});
+
+describe('IgxSplitter pane toggle', () => {
+ configureTestSuite();
+ beforeAll(async(() => {
+
+ return TestBed.configureTestingModule({
+ declarations: [ SplitterTogglePaneComponent ],
+ imports: [
+ IgxSplitterModule
+ ]
+ }).compileComponents();
+ }));
+
+ let fixture, splitter;
+ beforeEach(async(() => {
+ fixture = TestBed.createComponent(SplitterTogglePaneComponent);
+ fixture.detectChanges();
+ splitter = fixture.componentInstance.splitter;
+ }));
+
+ it('should collapse/expand panes', () => {
+ const pane1 = splitter.panes.toArray()[0];
+ const splitterBarComponent = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS)).context;
+
+ // collapse left sibling pane
+ splitterBarComponent.onCollapsing(0);
+ fixture.detectChanges();
+ expect(pane1.hidden).toBeTruthy();
+
+ // expand left sibling pane
+ splitterBarComponent.onCollapsing(1);
+ fixture.detectChanges();
+ expect(pane1.hidden).toBeFalsy();
+ });
+
+ it('should be able to expand both siblings when they are collapsed', () => {
+ const panes = splitter.panes.toArray();
+ const pane1 = panes[0];
+ const pane2 = panes[1];
+ const splitterBarComponents = fixture.debugElement.queryAll(By.css(SPLITTERBAR_CLASS));
+ const splitterBar1 = splitterBarComponents[0].context;
+ const splitterBar2 = splitterBarComponents[1].context;
+
+ splitterBar1.onCollapsing(0);
+ splitterBar2.onCollapsing(0);
+ fixture.detectChanges();
+
+ expect(pane1.hidden).toBeTruthy();
+ expect(pane2.hidden).toBeTruthy();
+
+ splitterBar1.onCollapsing(1);
+ fixture.detectChanges();
+ expect(pane1.hidden).toBeFalsy();
+ });
+
+ it('should not be able to resize a pane when it is hidden', () => {
+ const pane1 = splitter.panes.toArray()[0];
+ const splitterBarComponent = fixture.debugElement.query(By.css(SPLITTERBAR_CLASS)).context;
+
+ // collapse left sibling pane
+ splitterBarComponent.onCollapsing(0);
+ fixture.detectChanges();
+ expect(pane1.hidden).toBeTruthy();
+ expect(pane1.resizable).toBeFalsy();
+
+ splitterBarComponent.onCollapsing(1);
+ fixture.detectChanges();
+ expect(pane1.hidden).toBeFalsy();
+ expect(pane1.resizable).toBeTruthy();
+ });
+});
+@Component({
+ template: `
+
+
+
+ Pane 1
+
+
+
+
+ Pane 2
+
+
+
+ `,
+})
+export class SplitterTestComponent {
+ type = SplitterType.Horizontal;
+ @ViewChild(IgxSplitterComponent, { static: true })
+ public splitter: IgxSplitterComponent;
+}
+
+@Component({
+ template: `
+
+
+
+ Pane 1
+
+
+
+
+ Pane 2
+
+
+
+
+ Pane 3
+
+
+
+ `,
+})
+
+export class SplitterTogglePaneComponent extends SplitterTestComponent {
+}
diff --git a/projects/igniteui-angular/src/lib/splitter/splitter.component.ts b/projects/igniteui-angular/src/lib/splitter/splitter.component.ts
new file mode 100644
index 00000000000..e011f7181e9
--- /dev/null
+++ b/projects/igniteui-angular/src/lib/splitter/splitter.component.ts
@@ -0,0 +1,217 @@
+import { Component, QueryList, Input, ContentChildren, AfterContentInit, HostBinding, Output, EventEmitter } from '@angular/core';
+import { IgxSplitterPaneComponent } from './splitter-pane/splitter-pane.component';
+
+/**
+ * An enumeration that defines the `SplitterComponent` panes orientation.
+ * @export
+ * @enum {number}
+ */
+export enum SplitterType {
+ Horizontal,
+ Vertical
+}
+
+/**
+ * Provides reference to `SplitterComponent` component.
+ * The splitter consists of resizable panes that can be arranged either vertically or horizontally.
+ * There is a gripper between each couple of panes that helps widen or shrink them.
+ * @export
+ * @class SplitterComponent
+ * @implements AfterContentInit
+ */
+@Component({
+ selector: 'igx-splitter',
+ templateUrl: './splitter.component.html'
+})
+export class IgxSplitterComponent implements AfterContentInit {
+ private _type: SplitterType = SplitterType.Vertical;
+ /**
+ * Sets/gets `SplitterComponent` orientation.
+ * @type SplitterType
+ * @memberof SplitterComponent
+ */
+ @Input()
+ get type() {
+ return this._type;
+ }
+ set type(value) {
+ this._type = value;
+ if (this.panes) {
+ // if type is changed runtime, should reset sizes.
+ this.panes.forEach(x => x.size = 'auto');
+ }
+ }
+
+ /**
+ * Event emitted when panes collection is changed.
+ */
+ @Output()
+ public panesChange = new EventEmitter();
+
+ /**
+ * A list of all `IgxSplitterPaneComponent` items.
+ * @memberof SplitterComponent
+ */
+ @ContentChildren(IgxSplitterPaneComponent, { read: IgxSplitterPaneComponent })
+ public panes!: QueryList;
+
+ /**
+ * Gets the `flex-direction` property of the current `SplitterComponent`.
+ * @readonly
+ * @type string
+ * @memberof SplitterComponent
+ */
+ @HostBinding('style.flex-direction')
+ public get direction(): string {
+ return this.type === SplitterType.Horizontal ? 'row' : 'column';
+ }
+
+ /**
+ * Sets/gets the `overflow` property of the current `SplitterComponent`.
+ * @memberof SplitterComponent
+ */
+ @HostBinding('style.overflow')
+ public overflow = 'hidden';
+
+ /**
+ * Sets/gets the `display` property of the current `SplitterComponent`.
+ * @memberof SplitterComponent
+ */
+ @HostBinding('style.display')
+ public display = 'flex';
+
+ /**
+ * A field that holds the initial size of the main `IgxSplitterPaneComponent` in each couple of panes devided by a gripper.
+ * @private
+ * @memberof SplitterComponent
+ */
+ private initialPaneSize!: number;
+
+ /**
+ * A field that holds the initial size of the sibling `IgxSplitterPaneComponent` in each couple of panes devided by a gripper.
+ * @private
+ * @memberof SplitterComponent
+ */
+ private initialSiblingSize!: number;
+
+ /**
+ * The main `IgxSplitterPaneComponent` in each couple of panes devided by a gripper.
+ * @private
+ * @memberof SplitterComponent
+ */
+ private pane!: IgxSplitterPaneComponent;
+
+ /**
+ * The sibling `IgxSplitterPaneComponent` in each couple of panes devided by a gripper.
+ * @private
+ * @memberof SplitterComponent
+ */
+ private sibling!: IgxSplitterPaneComponent;
+
+ /** @hidden @internal */
+ public ngAfterContentInit(): void {
+ this.assignFlexOrder();
+ this.panes.changes.subscribe(() => {
+ requestAnimationFrame(() => {
+ this.panesChange.emit(this.panes.toArray());
+ this.assignFlexOrder();
+ });
+ });
+ }
+
+ /**
+ * @hidden @internal
+ * This method performs some initialization logic when the user starts dragging the gripper between each couple of panes.
+ * @param {IgxSplitterPaneComponent} pane
+ * The main `IgxSplitterPaneComponent` associated with the currently dragged `SplitBarComponent`.
+ */
+ public onMoveStart(pane: IgxSplitterPaneComponent) {
+ const panes = this.panes.toArray();
+ this.pane = pane;
+ this.sibling = panes[panes.indexOf(this.pane) + 1];
+
+ const paneRect = this.pane.element.getBoundingClientRect();
+ this.initialPaneSize = this.type === SplitterType.Horizontal ? paneRect.width : paneRect.height;
+ if (this.pane.size === 'auto') {
+ this.pane.size = this.type === SplitterType.Horizontal ? paneRect.width : paneRect.height;
+ }
+
+ const siblingRect = this.sibling.element.getBoundingClientRect();
+ this.initialSiblingSize = this.type === SplitterType.Horizontal ? siblingRect.width : siblingRect.height;
+ if (this.sibling.size === 'auto') {
+ this.sibling.size = this.type === SplitterType.Horizontal ? siblingRect.width : siblingRect.height;
+ }
+ }
+
+ /**
+ * @hidden @internal
+ * This method performs some calculations concerning the sizes each couple of panes while the gripper between them is being dragged.
+ * @param {number} delta The differnce along the X (or Y) axis between the initial and the current point while dragging the gripper.
+ */
+ public onMoving(delta: number) {
+ const min = parseInt(this.pane.minSize, 10) || 0;
+ const max = parseInt(this.pane.maxSize, 10) || this.initialPaneSize + this.initialSiblingSize;
+ const minSibling = parseInt(this.sibling.minSize, 10) || 0;
+ const maxSibling = parseInt(this.sibling.maxSize, 10) || this.initialPaneSize + this.initialSiblingSize;
+
+ const paneSize = this.initialPaneSize - delta;
+ const siblingSize = this.initialSiblingSize + delta;
+ if (paneSize < min || paneSize > max || siblingSize < minSibling || siblingSize > maxSibling) {
+ return;
+ }
+
+ this.pane.size = paneSize + 'px';
+ this.sibling.size = siblingSize + 'px';
+ }
+
+ /**
+ * Toggles pane visibility.
+ * @param pane - The pane to be hidden/shown.
+ */
+ public togglePane(pane: IgxSplitterPaneComponent) {
+ if (!pane) {
+ return;
+ }
+ // reset sibling sizes when pane is collapsed.
+ this._getSiblings(pane).forEach(sibling => sibling.size = 'auto');
+ pane.hidden = !pane.hidden;
+ pane.resizable = !pane.hidden;
+ pane.onPaneToggle.emit(pane);
+ }
+
+ /**
+ * @hidden @internal
+ * This method takes care for assigning an `order` property on each `IgxSplitterPaneComponent`.
+ */
+ private assignFlexOrder() {
+ let k = 0;
+ this.panes.forEach((pane: IgxSplitterPaneComponent) => {
+ pane.order = k;
+ k += 2;
+ });
+ }
+
+ /** @hidden @internal */
+ public getPaneSiblingsByOrder(order: number, barIndex: number): Array {
+ const panes = this.panes.toArray();
+ const prevPane = panes[order - barIndex - 1];
+ const nextPane = panes[order - barIndex];
+ const siblings = [prevPane, nextPane];
+ return siblings;
+ }
+
+
+ /** @hidden @internal */
+ private _getSiblings(pane: IgxSplitterPaneComponent) {
+ const panes = this.panes.toArray();
+ const index = panes.indexOf(pane);
+ const siblings = [];
+ if (index !== 0) {
+ siblings.push(panes[index - 1]);
+ }
+ if (index !== panes.length - 1) {
+ siblings.push(panes[index + 1]);
+ }
+ return siblings;
+ }
+}
diff --git a/projects/igniteui-angular/src/lib/splitter/splitter.module.ts b/projects/igniteui-angular/src/lib/splitter/splitter.module.ts
new file mode 100644
index 00000000000..a36c897d2ac
--- /dev/null
+++ b/projects/igniteui-angular/src/lib/splitter/splitter.module.ts
@@ -0,0 +1,24 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { IgxSplitBarComponent } from './splitter-bar/splitter-bar.component';
+import { IgxSplitterPaneComponent } from './splitter-pane/splitter-pane.component';
+import { IgxSplitterComponent } from './splitter.component';
+import { IgxIconModule } from '../icon/index';
+import { IgxDragDropModule } from '../directives/drag-drop/drag-drop.directive';
+
+@NgModule({
+ imports: [
+ CommonModule, IgxIconModule, IgxDragDropModule
+ ],
+ declarations: [
+ IgxSplitterComponent,
+ IgxSplitterPaneComponent,
+ IgxSplitBarComponent
+ ],
+ exports: [
+ IgxSplitterComponent,
+ IgxSplitterPaneComponent,
+ IgxSplitBarComponent
+ ]
+})
+export class IgxSplitterModule { }
diff --git a/projects/igniteui-angular/src/public_api.ts b/projects/igniteui-angular/src/public_api.ts
index 720ee06e21b..120c2568817 100644
--- a/projects/igniteui-angular/src/public_api.ts
+++ b/projects/igniteui-angular/src/public_api.ts
@@ -91,6 +91,7 @@ export * from './lib/tabs/index';
export * from './lib/time-picker/time-picker.component';
export * from './lib/toast/toast.component';
export * from './lib/select/index';
+export * from './lib/splitter/splitter.module';
/**
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index e76c796b9f3..d8e106a95c3 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -354,9 +354,14 @@ export class AppComponent implements OnInit {
},
{
link: '/slider',
- icon: 'linear_scale',
+ icon: 'tab',
name: 'Slider'
},
+ {
+ link: '/splitter',
+ icon: 'linear_scale',
+ name: 'Splitter'
+ },
{
link: '/snackbar',
icon: 'feedback',
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 436538a5c65..d1e60b83431 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -38,6 +38,7 @@ import { NavdrawerSampleComponent } from './navdrawer/navdrawer.sample';
import { ProgressbarSampleComponent } from './progressbar/progressbar.sample';
import { RippleSampleComponent } from './ripple/ripple.sample';
import { SliderSampleComponent } from './slider/slider.sample';
+import { SplitterSampleComponent } from './splitter/slitter.sample';
import { SnackbarSampleComponent } from './snackbar/snackbar.sample';
import { ColorsSampleComponent } from './styleguide/colors/color.sample';
import { ShadowsSampleComponent } from './styleguide/shadows/shadows.sample';
@@ -160,6 +161,7 @@ const components = [
RippleSampleComponent,
SelectSampleComponent,
SliderSampleComponent,
+ SplitterSampleComponent,
SnackbarSampleComponent,
BottomNavSampleComponent,
BottomNavRoutingSampleComponent,
diff --git a/src/app/routing.ts b/src/app/routing.ts
index 2745466630d..3a506d660d2 100644
--- a/src/app/routing.ts
+++ b/src/app/routing.ts
@@ -23,6 +23,7 @@ import { NavdrawerSampleComponent } from './navdrawer/navdrawer.sample';
import { ProgressbarSampleComponent } from './progressbar/progressbar.sample';
import { RippleSampleComponent } from './ripple/ripple.sample';
import { SliderSampleComponent } from './slider/slider.sample';
+import { SplitterSampleComponent } from './splitter/slitter.sample';
import { SnackbarSampleComponent } from './snackbar/snackbar.sample';
import { ColorsSampleComponent } from './styleguide/colors/color.sample';
import { ShadowsSampleComponent } from './styleguide/shadows/shadows.sample';
@@ -250,6 +251,10 @@ const appRoutes = [
path: 'slider',
component: SliderSampleComponent
},
+ {
+ path: 'splitter',
+ component: SplitterSampleComponent
+ },
{
path: 'snackbar',
component: SnackbarSampleComponent
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index abfa6badabb..990f2026a79 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -32,6 +32,7 @@ import {
IgxSliderModule,
IgxSnackbarModule,
IgxSwitchModule,
+ IgxSplitterModule,
IgxTabsModule,
IgxTimePickerModule,
IgxToastModule,
@@ -74,6 +75,7 @@ const igniteModules = [
IgxSliderModule,
IgxSnackbarModule,
IgxSwitchModule,
+ IgxSplitterModule,
IgxTabsModule,
IgxTimePickerModule,
IgxToastModule,
diff --git a/src/app/splitter/slitter.sample.ts b/src/app/splitter/slitter.sample.ts
new file mode 100644
index 00000000000..6e209b779ce
--- /dev/null
+++ b/src/app/splitter/slitter.sample.ts
@@ -0,0 +1,94 @@
+import { Component, ViewChild } from '@angular/core';
+import { SplitterType } from 'projects/igniteui-angular/src/lib/splitter/splitter.component';
+import { RemoteService } from '../shared/remote.service';
+import { IgxGridComponent } from 'igniteui-angular';
+
+@Component({
+ selector: 'app-splitter-sample',
+ styleUrls: ['splitter.sample.scss'],
+ templateUrl: 'splitter.sample.html'
+})
+export class SplitterSampleComponent {
+ type = SplitterType.Horizontal;
+ data1 = [];
+ data2 = [];
+ data3 = [];
+ primaryKeys = [
+ { name: 'CustomerID', type: 'string', level: 0 },
+ { name: 'OrderID', type: 'number', level: 1 },
+ { name: 'EmployeeID', type: 'number', level: 2 },
+ { name: 'ProductID', type: 'number', level: 2 }
+ ];
+
+ @ViewChild('grid1', { static: true })
+ grid1: IgxGridComponent;
+
+ @ViewChild('grid2', { static: true })
+ grid2: IgxGridComponent;
+
+ @ViewChild('grid3', { static: true })
+ grid3: IgxGridComponent;
+
+ constructor(private remoteService: RemoteService) {
+ remoteService.url = 'https://services.odata.org/V4/Northwind/Northwind.svc/';
+
+ this.remoteService.urlBuilder = (dataState) => this.buildUrl(dataState);
+ }
+
+ public buildUrl(dataState) {
+ let qS = '';
+ if (dataState) {
+ qS += `${dataState.key}?`;
+
+ const level = dataState.level;
+ if (level > 0) {
+ const parentKey = this.primaryKeys.find((key) => key.level === level - 1);
+ const parentID = typeof dataState.parentID !== 'object' ? dataState.parentID : dataState.parentID[parentKey.name];
+
+ if (parentKey.type === 'string') {
+ qS += `$filter=${parentKey.name} eq '${parentID}'`;
+ } else {
+ qS += `$filter=${parentKey.name} eq ${parentID}`;
+ }
+ }
+ }
+ return `${this.remoteService.url}${qS}`;
+ }
+
+ changeType() {
+ if (this.type === SplitterType.Horizontal) {
+ this.type = SplitterType.Vertical;
+ } else {
+ this.type = SplitterType.Horizontal;
+ }
+ }
+
+ public ngAfterViewInit() {
+ this.remoteService.getData({ parentID: null, level: 0, key: 'Customers' }, (data) => {
+ this.data1 = data['value'];
+ this.grid1.isLoading = false;
+ this.grid1.selectRows([this.data1[0].CustomerID], true);
+ const evt = { newSelection: [this.data1[0].CustomerID]};
+ this.onCustomerSelection(evt);
+ });
+ }
+
+ public onCustomerSelection(evt) {
+ const newSelection = evt.newSelection[0];
+ this.remoteService.getData({ parentID: newSelection, level: 1, key: 'Orders' }, (data) => {
+ this.data2 = data['value'];
+ this.grid2.isLoading = false;
+ this.grid2.selectRows([this.data2[0].OrderID], true);
+ const evt = { newSelection: [this.data2[0].OrderID]};
+ this.onOrderSelection(evt);
+ });
+ }
+
+ public onOrderSelection(evt) {
+ const newSelection = evt.newSelection[0];
+ this.remoteService.getData({ parentID: newSelection, level: 2, key: 'Order_Details' }, (data) => {
+ this.data3 = data['value'];
+ this.grid3.isLoading = false;
+ });
+ }
+}
diff --git a/src/app/splitter/splitter.sample.html b/src/app/splitter/splitter.sample.html
new file mode 100644
index 00000000000..f1712652efe
--- /dev/null
+++ b/src/app/splitter/splitter.sample.html
@@ -0,0 +1,76 @@
+
+
Toggle Splitter Direction
+
Simple sample
+
+
+
+ Pane 1
+
+
+
+
+ Pane 2
+
+
+
+
+ Pane 3
+
+
+
+
Nested splitters sample
+
+
+
+
+ Pane1.1
+
+
+ Pane1.2
+
+
+
+
+
+
+ Pane2.1
+
+
+ Pane2.2
+
+
+
+
+
+
Sample with Grids
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/splitter/splitter.sample.scss b/src/app/splitter/splitter.sample.scss
new file mode 100644
index 00000000000..ef4134bb18c
--- /dev/null
+++ b/src/app/splitter/splitter.sample.scss
@@ -0,0 +1 @@
+@import '../../../projects/igniteui-angular/src/lib/core/styles/themes/utilities';
diff --git a/src/styles/igniteui-theme.scss b/src/styles/igniteui-theme.scss
index 43b370ee527..6c938f4f0a3 100644
--- a/src/styles/igniteui-theme.scss
+++ b/src/styles/igniteui-theme.scss
@@ -14,7 +14,7 @@ $igx-foreground-color: text-contrast($igx-background-color);
body {
background: $igx-background-color;
- color: #222;
+ color: rgba($igx-foreground-color, .87);
}
@include igx-core($direction: ltr);