Skip to content

Commit 00b67ff

Browse files
feat(query-builder): focus last edited expression chip (#15303)
* feat(query-builder): focus last edited expression chip --------- Co-authored-by: INFRAGISTICS\IPetrov <IPetrov@infragistics.com>
1 parent 7d2249a commit 00b67ff

File tree

4 files changed

+200
-17
lines changed

4 files changed

+200
-17
lines changed

projects/igniteui-angular/src/lib/query-builder/query-builder-functions.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,17 @@ export class QueryBuilderFunctions {
657657
});
658658
};
659659

660+
public static verifyFocusedChip = (columnText: string, conditionText: string, valueText?: string) => {
661+
expect(document.activeElement.tagName).toEqual('IGX-CHIP');
662+
const chipElement = document.activeElement;
663+
expect((chipElement.querySelector('.igx-filter-tree__expression-column') as HTMLElement).innerText).toEqual(columnText);
664+
expect((chipElement.querySelector('.igx-filter-tree__expression-condition') as HTMLElement).innerText).toEqual(conditionText);
665+
666+
if (valueText) {
667+
expect((chipElement.querySelector('.igx-chip__content') as HTMLElement).innerText).toEqual(valueText);
668+
}
669+
}
670+
660671
public static verifyTabbableConditionEditLineElements = (editLine: DebugElement) => {
661672
const tabElements = QueryBuilderFunctions.getTabbableElements(editLine.nativeElement);
662673

projects/igniteui-angular/src/lib/query-builder/query-builder-tree.component.html

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@
9696
#addConditionButton
9797
igxButton="flat"
9898
[disabled]="hasEditedExpression"
99-
(click)="addCondition(expressionItem, afterExpression)"
100-
igxDrop
99+
(click)="addCondition(expressionItem, afterExpression, true)"
100+
igxDrop
101101
(enter)="onAddConditionEnter(addConditionButton, expressionItem)"
102102
(leave)="onChipLeave()"
103103
>
@@ -135,6 +135,7 @@
135135
(focusout)="onExpressionBlur($event, expressionItem)"
136136
>
137137
<igx-chip
138+
#expressionChip
138139
[draggable]="canBeDragged()"
139140
[hideBaseOnDrag] = "false"
140141
[animateOnRelease] = "false"
@@ -149,9 +150,9 @@
149150
[data]="expressionItem"
150151
[removable]="isInEditMode() ? 'true' : 'false'"
151152
(keydown)="invokeClick($event)"
152-
(click)="onChipClick(expressionItem)"
153-
(remove)="onChipRemove(expressionItem)"
154-
153+
(click)="onChipClick(expressionItem, expressionChip)"
154+
(remove)="onChipRemove(expressionItem)"
155+
155156
>
156157
<igx-icon igxPrefix
157158
class="igx-drag-indicator"
@@ -236,7 +237,7 @@
236237
<div igxDragIgnore class="igx-filter-tree__expression-actions">
237238
<button #addExpressionButton igxDragIgnore igxIconButton="outlined" [igxDropDownItemNavigation]="addOptionsDropDown"
238239
aria-labelledby="add-expression" (keydown)="invokeClick($event)"
239-
(click)="clickExpressionAdd(addExpressionButton)"
240+
(click)="clickExpressionAdd(addExpressionButton, expressionChip)"
240241
(blur)="addExpressionBlur()">
241242
<igx-icon id="add-expression">add</igx-icon>
242243
</button>
@@ -422,15 +423,15 @@
422423

423424
<div class="igx-filter-tree__inputs-actions">
424425
<button type="button"
425-
igxIconButton="outlined"
426-
[disabled]="!operandCanBeCommitted()"
427-
(click)="commitOperandEdit()"
426+
igxIconButton="flat"
427+
[disabled]="!operandCanBeCommitted()"
428+
(click)="commitExpression()"
428429
>
429430
<igx-icon family="default" name="confirm"></igx-icon>
430431
</button>
431432
<button type="button"
432-
igxIconButton="outlined"
433-
(click)="cancelOperandEdit()"
433+
igxIconButton="flat"
434+
(click)="discardExpression(expressionItem)"
434435
>
435436
<ng-container *ngTemplateOutlet="closeIcon"></ng-container>
436437
</button>

projects/igniteui-angular/src/lib/query-builder/query-builder-tree.component.ts

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const DEFAULT_PIPE_DATE_FORMAT = 'mediumDate';
6464
const DEFAULT_PIPE_TIME_FORMAT = 'mediumTime';
6565
const DEFAULT_PIPE_DATE_TIME_FORMAT = 'medium';
6666
const DEFAULT_PIPE_DIGITS_INFO = '1.0-3';
67+
const DEFAULT_CHIP_FOCUS_DELAY = 50;
6768

6869
@Pipe({
6970
name: 'fieldFormatter',
@@ -354,6 +355,9 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy {
354355
@ViewChild('groupContextMenuDropDown', { read: IgxDropDownComponent })
355356
private groupContextMenuDropDown: IgxDropDownComponent;
356357

358+
@ViewChildren(IgxChipComponent, { read: IgxChipComponent })
359+
private expressionsChips: QueryList<IgxChipComponent>;
360+
357361
/**
358362
* @hidden @internal
359363
*/
@@ -513,6 +517,9 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy {
513517
};
514518

515519
private destroy$ = new Subject<any>();
520+
private _timeoutId: any;
521+
private _lastFocusedChipIndex: number;
522+
private _focusDelay = DEFAULT_CHIP_FOCUS_DELAY;
516523
private _parentExpression: ExpressionOperandItem;
517524
private _selectedEntity: EntityType;
518525
private _selectedReturnFields: string | string[];
@@ -768,7 +775,7 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy {
768775
/**
769776
* @hidden @internal
770777
*/
771-
public addCondition(parent: ExpressionGroupItem, afterExpression?: ExpressionOperandItem) {
778+
public addCondition(parent: ExpressionGroupItem, afterExpression?: ExpressionOperandItem, isUIInteraction?: boolean) {
772779
this.cancelOperandAdd();
773780

774781
const operandItem = new ExpressionOperandItem({
@@ -783,16 +790,27 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy {
783790
this.contextualGroup = groupItem;
784791
this.initialOperator = null;
785792

793+
this._lastFocusedChipIndex = this._lastFocusedChipIndex == undefined ? -1 : this._lastFocusedChipIndex;
794+
786795
if (parent) {
787796
if (afterExpression) {
788797
const index = parent.children.indexOf(afterExpression);
789798
parent.children.splice(index + 1, 0, operandItem);
790799
} else {
791800
parent.children.push(operandItem);
792801
}
802+
this._lastFocusedChipIndex++;
793803
} else {
794804
this.rootGroup = groupItem;
795805
this.rootGroup.children.push(operandItem);
806+
this._lastFocusedChipIndex = 0;
807+
}
808+
809+
this._focusDelay = 250;
810+
811+
if (isUIInteraction && !afterExpression) {
812+
this._lastFocusedChipIndex = this.expressionsChips.length;
813+
this._focusDelay = DEFAULT_CHIP_FOCUS_DELAY;
796814
}
797815

798816
this.enterExpressionEdit(operandItem);
@@ -817,6 +835,24 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy {
817835
public endGroup(groupItem: ExpressionGroupItem) {
818836
this.currentGroup = groupItem.parent;
819837
}
838+
839+
/**
840+
* @hidden @internal
841+
*/
842+
public commitExpression() {
843+
this.commitOperandEdit();
844+
this.focusEditedExpressionChip();
845+
}
846+
847+
/**
848+
* @hidden @internal
849+
*/
850+
public discardExpression(expressionItem?: ExpressionOperandItem) {
851+
this.cancelOperandEdit();
852+
if (expressionItem && expressionItem.expression.fieldName) {
853+
this.focusEditedExpressionChip();
854+
}
855+
}
820856

821857
/**
822858
* @hidden @internal
@@ -1565,26 +1601,28 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy {
15651601
/**
15661602
* @hidden @internal
15671603
*/
1568-
public onChipClick(expressionItem: ExpressionOperandItem) {
1569-
this.enterExpressionEdit(expressionItem);
1604+
public onChipClick(expressionItem: ExpressionOperandItem, chip: IgxChipComponent) {
1605+
this.enterExpressionEdit(expressionItem, chip);
15701606
}
15711607

15721608
/**
15731609
* @hidden @internal
15741610
*/
1575-
public enterExpressionEdit(expressionItem: ExpressionOperandItem) {
1611+
public enterExpressionEdit(expressionItem: ExpressionOperandItem, chip?: IgxChipComponent) {
15761612
this.exitEditAddMode(true);
15771613
this.cdr.detectChanges();
1614+
this._lastFocusedChipIndex = chip ? this.expressionsChips.toArray().findIndex(expr => expr === chip) : this._lastFocusedChipIndex;
15781615
this.enterEditMode(expressionItem);
15791616
}
15801617

15811618

15821619
/**
15831620
* @hidden @internal
15841621
*/
1585-
public clickExpressionAdd(targetButton: HTMLElement) {
1622+
public clickExpressionAdd(targetButton: HTMLElement, chip: IgxChipComponent) {
15861623
this.exitEditAddMode(true);
15871624
this.cdr.detectChanges();
1625+
this._lastFocusedChipIndex = this.expressionsChips.toArray().findIndex(expr => expr === chip);
15881626
this.openExpressionAddDialog(targetButton);
15891627
}
15901628

@@ -2146,6 +2184,21 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy {
21462184
}
21472185
}
21482186

2187+
private focusEditedExpressionChip() {
2188+
if (this._timeoutId) {
2189+
clearTimeout(this._timeoutId);
2190+
}
2191+
2192+
this._timeoutId = setTimeout(() => {
2193+
if (this._lastFocusedChipIndex != -1) {
2194+
const chipElement = this.expressionsChips.toArray()[this._lastFocusedChipIndex].nativeElement;
2195+
chipElement.focus();
2196+
this._lastFocusedChipIndex = -1;
2197+
this._focusDelay = DEFAULT_CHIP_FOCUS_DELAY;
2198+
}
2199+
}, this._focusDelay);
2200+
}
2201+
21492202
private init() {
21502203
this.cancelOperandAdd();
21512204
this.cancelOperandEdit();

projects/igniteui-angular/src/lib/query-builder/query-builder.component.spec.ts

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1572,7 +1572,125 @@ describe('IgxQueryBuilder', () => {
15721572
QueryBuilderFunctions.verifyExpressionChipContent(fix, [1], 'OrderId', 'Greater Than', '3');
15731573
}));
15741574

1575-
it('Should not make bug where existing inner query is leaking to a newly created one', fakeAsync(() => {
1575+
it(`Should focus edited expression chip after click on the 'commit'/'discard' button.`, fakeAsync(() => {
1576+
queryBuilder.expressionTree = QueryBuilderFunctions.generateExpressionTree();
1577+
fix.detectChanges();
1578+
tick(100);
1579+
fix.detectChanges();
1580+
1581+
// Click the 'OrderId' chip to enter edit mode.
1582+
QueryBuilderFunctions.clickQueryBuilderTreeExpressionChip(fix, [1]);
1583+
tick(100);
1584+
fix.detectChanges();
1585+
1586+
// Click on the 'commit' button
1587+
const commitBtn = QueryBuilderFunctions.getQueryBuilderExpressionCommitButton(fix);
1588+
commitBtn.click();
1589+
fix.detectChanges();
1590+
tick(100);
1591+
fix.detectChanges();
1592+
// Verify focused chip
1593+
QueryBuilderFunctions.verifyFocusedChip('OrderId', 'Greater Than', '3');
1594+
1595+
// Click the 'OrderId' chip to enter edit mode.
1596+
QueryBuilderFunctions.clickQueryBuilderTreeExpressionChip(fix, [1]);
1597+
tick(100);
1598+
fix.detectChanges();
1599+
1600+
// Click on the 'discard' button
1601+
const closeBtn = QueryBuilderFunctions.getQueryBuilderExpressionCloseButton(fix);
1602+
closeBtn.click();
1603+
fix.detectChanges();
1604+
tick(100);
1605+
fix.detectChanges();
1606+
// Verify focused chip
1607+
QueryBuilderFunctions.verifyFocusedChip('OrderId', 'Greater Than', '3');
1608+
}));
1609+
1610+
it(`Should focus proper expression chip after switching edit mode and click on the 'commit'/'discard' button.`, fakeAsync(() => {
1611+
queryBuilder.expressionTree = QueryBuilderFunctions.generateExpressionTree();
1612+
fix.detectChanges();
1613+
tick(100);
1614+
fix.detectChanges();
1615+
1616+
// Click the 'OrderDate' chip to enter edit mode.
1617+
QueryBuilderFunctions.clickQueryBuilderTreeExpressionChip(fix, [2]);
1618+
tick(100);
1619+
fix.detectChanges();
1620+
1621+
// Click the 'OrderId' chip to enter edit mode.
1622+
QueryBuilderFunctions.clickQueryBuilderTreeExpressionChip(fix, [1]);
1623+
tick(100);
1624+
fix.detectChanges();
1625+
1626+
// Click on the 'commit' button
1627+
const commitBtn = QueryBuilderFunctions.getQueryBuilderExpressionCommitButton(fix);
1628+
commitBtn.click();
1629+
fix.detectChanges();
1630+
tick(100);
1631+
fix.detectChanges();
1632+
// Verify focused chip
1633+
QueryBuilderFunctions.verifyFocusedChip('OrderId', 'Greater Than', '3');
1634+
}));
1635+
1636+
it('Should focus added through group add buttons expression chip if it is commited.', fakeAsync(() => {
1637+
queryBuilder.expressionTree = QueryBuilderFunctions.generateExpressionTree();
1638+
fix.detectChanges();
1639+
tick(100);
1640+
fix.detectChanges();
1641+
1642+
const group = QueryBuilderFunctions.getQueryBuilderTreeRootGroup(fix) as HTMLElement;
1643+
1644+
// Add new 'expression'.
1645+
const buttonsContainer = Array.from(group.querySelectorAll('.igx-filter-tree__buttons'))[1];
1646+
const buttons = Array.from(buttonsContainer.querySelectorAll('button'));
1647+
(buttons[0] as HTMLElement).click();
1648+
tick();
1649+
fix.detectChanges();
1650+
1651+
QueryBuilderFunctions.selectColumnInEditModeExpression(fix, 3); // Select 'Delivered' column.
1652+
QueryBuilderFunctions.selectOperatorInEditModeExpression(fix, 1); // Select 'True' operator.
1653+
tick(100);
1654+
fix.detectChanges();
1655+
1656+
// Click on the 'commit' button
1657+
const commitBtn = QueryBuilderFunctions.getQueryBuilderExpressionCommitButton(fix);
1658+
commitBtn.click();
1659+
fix.detectChanges();
1660+
tick(300);
1661+
fix.detectChanges();
1662+
1663+
// Verify focused chip
1664+
QueryBuilderFunctions.verifyFocusedChip('Delivered', 'True');
1665+
}));
1666+
1667+
it('Should NOT focus an expression chip if added expression is discarded.', fakeAsync(() => {
1668+
queryBuilder.expressionTree = QueryBuilderFunctions.generateExpressionTree();
1669+
fix.detectChanges();
1670+
tick(100);
1671+
fix.detectChanges();
1672+
1673+
const group = QueryBuilderFunctions.getQueryBuilderTreeRootGroup(fix) as HTMLElement;
1674+
1675+
// Add new 'expression'.
1676+
const buttonsContainer = Array.from(group.querySelectorAll('.igx-filter-tree__buttons'))[1];
1677+
const buttons = Array.from(buttonsContainer.querySelectorAll('button'));
1678+
(buttons[0] as HTMLElement).click();
1679+
tick();
1680+
fix.detectChanges();
1681+
1682+
// Click on the 'close' button
1683+
const closeBtn = QueryBuilderFunctions.getQueryBuilderExpressionCloseButton(fix);
1684+
closeBtn.click();
1685+
fix.detectChanges();
1686+
tick(300);
1687+
fix.detectChanges();
1688+
1689+
// Verify chip is not focused
1690+
expect(document.activeElement.tagName).toEqual('BODY');
1691+
}));
1692+
1693+
it('Should not make bug where existing inner query is leaking to a newly created one.', fakeAsync(() => {
15761694
queryBuilder.expressionTree = QueryBuilderFunctions.generateExpressionTree();
15771695
fix.detectChanges();
15781696
tick(100);

0 commit comments

Comments
 (0)