Skip to content

Column pinning - both sides. #16132

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 21 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6af5d06
feat(column-pinning): Pinning both sides, initial api and collections.
Aug 5, 2025
ddf757a
chore(*): Name collections pinnedStartColumns/pinnedEndColumns.
Aug 11, 2025
d300ee5
chore(*): Initial template updates.
Aug 11, 2025
62804fb
chore(*): Update header templates.
Aug 11, 2025
b0de0c0
chore(*): Adjust getters for styles.
Aug 11, 2025
e84e0d0
chore(*): Remove and update all old checks for root grid pin direction.
Aug 11, 2025
7d30464
chore(*): Fix summary cell context.
Aug 11, 2025
04b444d
chore(*): Fix offset calcs.
Aug 11, 2025
cf316eb
chore(*): Fix overwrite position. Update generic pinned collection.
Aug 11, 2025
a93fc98
chore(*): Fix moving pinned cols via exc.style moving.
Aug 11, 2025
71dec02
chore(*): Update position when moving from one pinned area to another.
Aug 12, 2025
45b7baa
chore(*): Fix mrl scenarios with both pin.
Aug 12, 2025
4100594
chore(*): Fix circular dep.
Aug 12, 2025
e8e56d4
chore(*): Add some tests for pin on both sides.
Aug 12, 2025
3de7752
chore(*): Fix mrl col indexes.
Aug 13, 2025
5f572b8
chore(*): Fix pivot scrollbar placeholder size.
Aug 13, 2025
5db917c
chore(*): Update internal pinned with hidden columns.
Aug 13, 2025
cc8fdb0
chore(*): Split pinned widths calc in pivot.
Aug 13, 2025
70a5d23
Merge branch 'master' into mkirova/col-pin-directions
MayaKirova Aug 13, 2025
f56872d
chore(*): Add mch and mrl tests with pin on both sides.
Aug 13, 2025
6786884
Merge branch 'mkirova/col-pin-directions' of https://github.com/Ignit…
Aug 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,10 @@ export class IgxColumnLayoutComponent extends IgxColumnGroupComponent implements
}

const unpinnedColumns = this.grid.unpinnedColumns.filter(c => c.columnLayout && !c.hidden);
const pinnedColumns = this.grid.pinnedColumns.filter(c => c.columnLayout && !c.hidden);
let vIndex = -1;

if (!this.pinned) {
const indexInCollection = unpinnedColumns.indexOf(this);
vIndex = indexInCollection === -1 ? -1 : pinnedColumns.length + indexInCollection;
} else {
vIndex = pinnedColumns.indexOf(this);
}
const pinnedStart = this.grid.pinnedStartColumns.filter(c => c.columnLayout && !c.hidden);
const pinnedEndColumns = this.grid.pinnedEndColumns.filter(c => c.columnLayout && !c.hidden);
const ordered = pinnedStart.concat(unpinnedColumns).concat(pinnedEndColumns);
let vIndex = ordered.indexOf(this);
this._vIndex = vIndex;
return vIndex;
}
Expand Down Expand Up @@ -158,7 +153,7 @@ export class IgxColumnLayoutComponent extends IgxColumnGroupComponent implements
public override populateVisibleIndexes() {
this.childrenVisibleIndexes = [];
const columns = this.grid?.pinnedColumns && this.grid?.unpinnedColumns
? this.grid.pinnedColumns.concat(this.grid.unpinnedColumns)
? this.grid.pinnedStartColumns.concat(this.grid.unpinnedColumns).concat(this.grid.pinnedEndColumns)
: [];
const orderedCols = columns
.filter(x => !x.columnGroup && !x.hidden)
Expand Down
101 changes: 72 additions & 29 deletions projects/igniteui-angular/src/lib/grids/columns/column.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import { IColumnVisibilityChangingEventArgs, IPinColumnCancellableEventArgs, IPi
import { isConstructor, PlatformUtil } from '../../core/utils';
import { IgxGridCell } from '../grid-public-cell';
import { NG_VALIDATORS, Validator } from '@angular/forms';
import { Size } from '../common/enums';
import { ColumnPinningPosition, Size } from '../common/enums';
import { ExpressionsTreeUtil } from '../../data-operations/expressions-tree-util';

const DEFAULT_DATE_FORMAT = 'mediumDate';
Expand Down Expand Up @@ -1039,6 +1039,28 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy
return (this.grid as any)._columns.indexOf(this);
}

/**
* Gets the pinning direction of the column.
* ```typescript
* let pinningPosition = this.column.pinningPosition;
*/
@WatchColumnChanges()
@Input()
public get pinningPosition(): ColumnPinningPosition {
const userSet = this._pinningPosition !== null && this._pinningPosition !== undefined;
return userSet ? this._pinningPosition : this.grid.pinning.columns;
}

/**
* Sets the pinning direction of the column.
*```html
* <igx-column [pinningPosition]="ColumnPinningPosition.End"></igx-column>
* ```
*/
public set pinningPosition(value: ColumnPinningPosition) {
this._pinningPosition = value;
}

/**
* Gets whether the column is `pinned`.
* ```typescript
Expand Down Expand Up @@ -1485,7 +1507,8 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy
return this._vIndex;
}
const unpinnedColumns = this.grid.unpinnedColumns.filter(c => !c.columnGroup);
const pinnedColumns = this.grid.pinnedColumns.filter(c => !c.columnGroup);
const pinnedStartColumns = this.grid.pinnedStartColumns.filter(c => !c.columnGroup);
const pinnedEndColumns = this.grid.pinnedEndColumns.filter(c => !c.columnGroup);

let col = this;
let vIndex = -1;
Expand All @@ -1500,15 +1523,13 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy
if (!this.pinned) {
const indexInCollection = unpinnedColumns.indexOf(col);
vIndex = indexInCollection === -1 ?
-1 :
(this.grid.isPinningToStart ?
pinnedColumns.length + indexInCollection :
indexInCollection);
-1 : pinnedStartColumns.length + indexInCollection;
} else {
const indexInCollection = pinnedColumns.indexOf(col);
vIndex = this.grid.isPinningToStart ?
const indexInCollection = this.pinningPosition === ColumnPinningPosition.Start ?
pinnedStartColumns.indexOf(col) : pinnedEndColumns.indexOf(col);
vIndex = this.pinningPosition === ColumnPinningPosition.Start ?
indexInCollection :
unpinnedColumns.length + indexInCollection;
pinnedStartColumns.length + unpinnedColumns.length + indexInCollection;
}
this._vIndex = vIndex;
return vIndex;
Expand Down Expand Up @@ -1586,20 +1607,20 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy

/** @hidden @internal **/
public get isLastPinned(): boolean {
return this.grid.isPinningToStart &&
this.grid.pinnedColumns[this.grid.pinnedColumns.length - 1] === this;
return this.pinningPosition === ColumnPinningPosition.Start &&
this.grid.pinnedStartColumns[this.grid.pinnedStartColumns.length - 1] === this;
}

/** @hidden @internal **/
public get isFirstPinned(): boolean {
const pinnedCols = this.grid.pinnedColumns.filter(x => !x.columnGroup);
return !this.grid.isPinningToStart && pinnedCols[0] === this;
const pinnedCols = this.grid.pinnedEndColumns.filter(x => !x.columnGroup);
return this.pinningPosition === ColumnPinningPosition.End && pinnedCols[0] === this;
}

/** @hidden @internal **/
public get rightPinnedOffset(): string {
return this.pinned && !this.grid.isPinningToStart ?
- this.grid.pinnedWidth - this.grid.headerFeaturesWidth + 'px' :
return this.pinned && this.pinningPosition === ColumnPinningPosition.End ?
- this.grid.pinnedEndWidth - this.grid.pinnedStartWidth + 'px' :
null;
}

Expand Down Expand Up @@ -1792,6 +1813,7 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy
protected _applySelectableClass = false;

protected _vIndex = NaN;
protected _pinningPosition = null;
/**
* @hidden
*/
Expand Down Expand Up @@ -2198,7 +2220,7 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy
*
* @memberof IgxColumnComponent
*/
public pin(index?: number): boolean {
public pin(index?: number, pinningPosition?: ColumnPinningPosition): boolean {
// TODO: Probably should the return type of the old functions
// should be moved as a event parameter.
const grid = (this.grid as any);
Expand All @@ -2207,19 +2229,23 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy
}

if (this.parent && !this.parent.pinned) {
return this.topLevelParent.pin(index);
}

const hasIndex = index !== undefined;
if (hasIndex && (index < 0 || index > grid.pinnedColumns.length)) {
return this.topLevelParent.pin(index, pinningPosition);
}
const targetPinPosition = pinningPosition !== null && pinningPosition !== undefined ? pinningPosition : this.pinningPosition;
const pinningVisibleCollection = targetPinPosition === ColumnPinningPosition.Start ?
grid.pinnedStartColumns : grid.pinnedEndColumns;
const pinningCollection = targetPinPosition === ColumnPinningPosition.Start ?
grid._pinnedStartColumns : grid._pinnedEndColumns;
const hasIndex = index !== undefined && index !== null;
if (hasIndex && (index < 0 || index > pinningVisibleCollection.length)) {
return false;
}

if (!this.parent && !this.pinnable) {
return false;
}

const rootPinnedCols = grid._pinnedColumns.filter((c) => c.level === 0);
const rootPinnedCols = pinningCollection.filter((c) => c.level === 0);
index = hasIndex ? index : rootPinnedCols.length;
const args: IPinColumnCancellableEventArgs = { column: this, insertAtIndex: index, isPinned: false, cancel: false };
this.grid.columnPin.emit(args);
Expand All @@ -2231,14 +2257,20 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy
this.grid.crudService.endEdit(false);

this._pinned = true;
if (pinningPosition !== null && pinningPosition !== undefined) {
// if user has set some position in the params, overwrite the column's position.
this._pinningPosition = pinningPosition;
}

this.pinnedChange.emit(this._pinned);
// it is possible that index is the last position, so will need to find target column by [index-1]
const targetColumn = args.insertAtIndex === grid._pinnedColumns.length ?
grid._pinnedColumns[args.insertAtIndex - 1] : grid._pinnedColumns[args.insertAtIndex];
const targetColumn = args.insertAtIndex === pinningCollection.length ?
pinningCollection[args.insertAtIndex - 1] : pinningCollection[args.insertAtIndex];

if (grid._pinnedColumns.indexOf(this) === -1) {
if (pinningCollection.indexOf(this) === -1) {
if (!grid.hasColumnGroups) {
grid._pinnedColumns.splice(args.insertAtIndex, 0, this);
pinningCollection.splice(args.insertAtIndex, 0, this);
grid._pinnedColumns = grid._pinnedStartColumns.concat(grid._pinnedEndColumns);
} else {
// insert based only on root collection
if (this.level === 0) {
Expand All @@ -2252,6 +2284,11 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy
allPinned = allPinned.concat(group.allChildren);
});
grid._pinnedColumns = allPinned;
if (this.pinningPosition === ColumnPinningPosition.Start) {
grid._pinnedStartColumns = allPinned;
} else {
grid._pinnedEndColumns = allPinned;
}
}

if (grid._unpinnedColumns.indexOf(this) !== -1) {
Expand All @@ -2261,12 +2298,12 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy
}

if (hasIndex) {
index === grid._pinnedColumns.length - 1 ?
index === pinningCollection.length - 1 ?
grid._moveColumns(this, targetColumn, DropPosition.AfterDropTarget) : grid._moveColumns(this, targetColumn, DropPosition.BeforeDropTarget);
}

if (this.columnGroup) {
this.allChildren.forEach(child => child.pin());
this.allChildren.forEach(child => child.pin(null, targetPinPosition));
grid.reinitPinStates();
}

Expand Down Expand Up @@ -2302,7 +2339,7 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy
if (this.parent && this.parent.pinned) {
return this.topLevelParent.unpin(index);
}
const hasIndex = index !== undefined;
const hasIndex = index !== undefined && index !== null;
if (hasIndex && (index < 0 || index > grid._unpinnedColumns.length)) {
return false;
}
Expand Down Expand Up @@ -2337,6 +2374,12 @@ export class IgxColumnComponent implements AfterContentInit, OnDestroy, ColumnTy
if (grid._pinnedColumns.indexOf(this) !== -1) {
grid._pinnedColumns.splice(grid._pinnedColumns.indexOf(this), 1);
}
if (this.pinningPosition === ColumnPinningPosition.Start && grid._pinnedStartColumns.indexOf(this) !== -1) {
grid._pinnedStartColumns.splice(grid._pinnedStartColumns.indexOf(this), 1);
}
if (this.pinningPosition === ColumnPinningPosition.End && grid._pinnedEndColumns.indexOf(this) !== -1) {
grid._pinnedEndColumns.splice(grid._pinnedEndColumns.indexOf(this), 1);
}
}

if (hasIndex) {
Expand Down
14 changes: 9 additions & 5 deletions projects/igniteui-angular/src/lib/grids/common/grid.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ export interface ColumnType extends FieldType {
toggleVisibility(value?: boolean): void;
populateVisibleIndexes?(): void;
/** Pins the column at the specified index (if not already pinned). */
pin(index?: number): boolean;
pin(index?: number, pinningPosition?: ColumnPinningPosition): boolean;
/** Unpins the column at the specified index (if not already unpinned). */
unpin(index?: number): boolean;
}
Expand Down Expand Up @@ -776,13 +776,13 @@ export interface GridType extends IGridDataBindable {
isRowSelectable: boolean;
/** Indicates whether the selectors of the rows are visible */
showRowSelectors: boolean;
/** Indicates whether the grid's element is pinned to the start of the grid */
isPinningToStart: boolean;
/** Indicates if the column of the grid is in drag mode */
columnInDrag: any;
/** @hidden @internal */
/** The width of pinned element */
pinnedWidth: number;
/** The width of pinned element for pinning at start. */
pinnedStartWidth: number;
/** The width of pinned element for pinning at end. */
pinnedEndWidth: number;
/** @hidden @internal */
/** The width of unpinned element */
unpinnedWidth: number;
Expand Down Expand Up @@ -917,6 +917,10 @@ export interface GridType extends IGridDataBindable {
unpinnedColumns: ColumnType[];
/** An array of columns, but it counts only the ones that are pinned */
pinnedColumns: ColumnType[];
/** An array of columns, but it counts only the ones that are pinned to the start. */
pinnedStartColumns: ColumnType[];
/** An array of columns, but it counts only the ones that are pinned to the end. */
pinnedEndColumns: ColumnType[];
/** represents an array of the headers of the columns */
/** @hidden @internal */
headerCellList: any[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BaseFilteringComponent } from './base-filtering.component';
import { IgxIconComponent } from '../../../icon/icon.component';
import { IgxButtonDirective } from '../../../directives/button/button.directive';
import { IgxButtonGroupComponent } from '../../../buttonGroup/buttonGroup.component';
import { ColumnPinningPosition } from '../../common/enums';

/**
* A component used for presenting Excel style column moving UI.
Expand Down Expand Up @@ -49,25 +50,26 @@ export class IgxExcelStyleMovingComponent {
public onMoveButtonClicked(moveDirection) {
let targetColumn;
if (this.esf.column.pinned) {
if (this.esf.column.isLastPinned && moveDirection === 1 && this.esf.grid.isPinningToStart) {
if (this.esf.column.isLastPinned && moveDirection === 1 && this.esf.column.pinningPosition === ColumnPinningPosition.Start) {
targetColumn = this.esf.grid.unpinnedColumns[0];
moveDirection = 0;
} else if (this.esf.column.isFirstPinned && moveDirection === 0 && !this.esf.grid.isPinningToStart) {
} else if (this.esf.column.isFirstPinned && moveDirection === 0 && this.esf.column.pinningPosition === ColumnPinningPosition.End) {
targetColumn = this.esf.grid.unpinnedColumns[this.esf.grid.unpinnedColumns.length - 1];
moveDirection = 1;
} else {
targetColumn = this.findColumn(moveDirection, this.esf.grid.pinnedColumns);
}
} else if (this.esf.grid.unpinnedColumns.indexOf(this.esf.column) === 0 && moveDirection === 0 &&
this.esf.grid.isPinningToStart) {
targetColumn = this.esf.grid.pinnedColumns[this.esf.grid.pinnedColumns.length - 1];
} else if (this.esf.grid.unpinnedColumns.indexOf(this.esf.column) === 0 && moveDirection === 0) {
// moving first unpinned, left (into pin start area)
targetColumn = this.esf.grid.pinnedStartColumns[this.esf.grid.pinnedStartColumns.length - 1];
if (targetColumn.parent) {
targetColumn = targetColumn.topLevelParent;
}
moveDirection = 1;
} else if (this.esf.grid.unpinnedColumns.indexOf(this.esf.column) === this.esf.grid.unpinnedColumns.length - 1 &&
moveDirection === 1 && !this.esf.grid.isPinningToStart) {
targetColumn = this.esf.grid.pinnedColumns[0];
moveDirection === 1) {
// moving last unpinned, right (into pin end area)
targetColumn = this.esf.grid.pinnedEndColumns[0];
moveDirection = 0;
} else {
targetColumn = this.findColumn(moveDirection, this.esf.grid.unpinnedColumns);
Expand Down
Loading
Loading