Skip to content
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

Improve keyboard navigation in filtering #2963

Merged
merged 8 commits into from
Nov 9, 2018
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
</igx-chip>
<span class="igx-filtering-chips__connector" *ngIf="!last">{{filteringService.getOperatorAsString(item.afterOperator)}}</span>
</ng-container>
<div #moreIcon [ngClass]="filteringIndicatorClass()" (click)="onChipClicked()" tabindex="0">
<div #moreIcon [ngClass]="filteringIndicatorClass()" (click)="onChipClicked()" (keydown)="onChipKeyDown($event)" tabindex="0">
<igx-icon>filter_list</igx-icon>
<igx-badge [value]="moreFiltersCount"></igx-badge>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { IBaseChipEventArgs, IgxChipsAreaComponent, IgxChipComponent } from '../
import { IgxFilteringService, ExpressionUI } from './grid-filtering.service';
import { KEYS, cloneArray } from '../../core/utils';
import { IgxGridNavigationService } from '../grid-navigation.service';
import { IgxGridGroupByRowComponent } from '../grid';

/**
* @hidden
Expand Down Expand Up @@ -96,52 +97,56 @@ export class IgxGridFilteringCellComponent implements AfterViewInit, OnInit {
}

ngOnInit(): void {
this.filteringService.columnToChipToFocus.set(this.column.field, false);
this.filteringService.columnToMoreIconHidden.set(this.column.field, true);
}

ngAfterViewInit(): void {
this.updateFilterCellArea();
}

@HostListener('keydown.shift.tab', ['$event'])
@HostListener('keydown.tab', ['$event'])
public onTabKeyDown(eventArgs) {
if (eventArgs.shiftKey) {
if (this.column.visibleIndex > 0 && !this.navService.isColumnLeftFullyVisible(this.column.visibleIndex - 1)) {
eventArgs.preventDefault();
this.filteringService.grid.headerContainer.scrollTo(this.column.visibleIndex - 1);
} else if (this.column.visibleIndex === 0) {
eventArgs.preventDefault();
const pinnedColumns = this.filteringService.grid.pinnedColumns;
let nextIndex = this.column.visibleIndex + 1 - pinnedColumns.length;

if (this.isLastElementFocused()) {
if (nextIndex < this.filteringService.grid.unpinnedColumns.length &&
pinnedColumns.indexOf(this.column) === pinnedColumns.length - 1 &&
!this.navService.isColumnLeftFullyVisible(this.column.visibleIndex + 1)) {
this.ScrollToChip(0, true);
eventArgs.stopPropagation();
return;
}
} else {
if (this.column.visibleIndex === this.filteringService.grid.columnList.length - 1) {
if (this.currentTemplate === this.defaultFilter) {
if (this.isMoreIconVisible() === false) {
if (this.moreIcon.nativeElement === document.activeElement) {
this.navService.goToFirstCell();
}
} else if (this.chipsArea.chipsList.last.elementRef.nativeElement.querySelector(`.igx-chip__item`) ===
document.activeElement) {
this.navService.goToFirstCell();
if (!this.filteringService.grid.filteredData || this.filteringService.grid.filteredData.length > 0) {
if (this.filteringService.grid.rowList.filter(row => row instanceof IgxGridGroupByRowComponent).length > 0) {
eventArgs.stopPropagation();
return;
}
} else {
this.navService.goToFirstCell();
}
} else if (!this.navService.isColumnFullyVisible(this.column.visibleIndex + 1)) {
eventArgs.preventDefault();
this.filteringService.grid.headerContainer.scrollTo(this.column.visibleIndex + 1);
} else if (!this.column.pinned && !this.navService.isColumnFullyVisible(this.column.visibleIndex + 1)) {
eventArgs.preventDefault();
this.ScrollToChip(nextIndex, true);
}
}

eventArgs.stopPropagation();
}

/**
* Returns the chip to be focused.
*/
public getChipToFocus() {
return this.filteringService.columnToChipToFocus.get(this.column.field);
@HostListener('keydown.shift.tab', ['$event'])
public onShiftTabKeyDown(eventArgs) {
if (this.isFirstElementFocused()) {
let prevIndex = this.column.visibleIndex - 1 - this.filteringService.grid.pinnedColumns.length;

if (this.column.visibleIndex > 0 && !this.navService.isColumnLeftFullyVisible(this.column.visibleIndex - 1)) {
eventArgs.preventDefault();
this.ScrollToChip(prevIndex, false);
} else if (this.column.visibleIndex === 0) {
eventArgs.preventDefault();
}
}
eventArgs.stopPropagation();
}

/**
Expand Down Expand Up @@ -199,6 +204,7 @@ export class IgxGridFilteringCellComponent implements AfterViewInit, OnInit {
public onChipRemoved(eventArgs: IBaseChipEventArgs, item: ExpressionUI): void {
const indexToRemove = this.expressionsList.indexOf(item);
this.removeExpression(indexToRemove);
this.focusChip();
}

/**
Expand All @@ -224,20 +230,20 @@ export class IgxGridFilteringCellComponent implements AfterViewInit, OnInit {
*/
public filteringIndicatorClass() {
return {
[this.baseClass]: !this.isMoreIconVisible(),
[`${this.baseClass}--hidden`]: this.isMoreIconVisible()
[this.baseClass]: !this.isMoreIconHidden(),
[`${this.baseClass}--hidden`]: this.isMoreIconHidden()
};
}

/**
* Focus a chip depending on the current visible template.
*/
public focusChip() {
public focusChip(focusFirst: boolean = false) {
if (this.currentTemplate === this.defaultFilter) {
if (this.isMoreIconVisible() === false) {
this.moreIcon.nativeElement.focus();
if (focusFirst) {
this.focusFirstElement();
} else {
this.chipsArea.chipsList.last.elementRef.nativeElement.querySelector(`.igx-chip__item`).focus();
this.focusElement();
}
} else if (this.currentTemplate === this.emptyFilter) {
this.ghostChip.elementRef.nativeElement.querySelector(`.igx-chip__item`).focus();
Expand All @@ -264,7 +270,7 @@ export class IgxGridFilteringCellComponent implements AfterViewInit, OnInit {
this.filteringService.filter(this.column.field, this.rootExpressionsTree);
}

private isMoreIconVisible(): boolean {
private isMoreIconHidden(): boolean {
return this.filteringService.columnToMoreIconHidden.get(this.column.field);
}

Expand Down Expand Up @@ -309,4 +315,52 @@ export class IgxGridFilteringCellComponent implements AfterViewInit, OnInit {
this.cdr.detectChanges();
}
}

private ScrollToChip(columnIndex: number, shouldFocusNext: boolean) {
this.filteringService.grid.nativeElement.focus({preventScroll: true});
this.filteringService.columnToFocus = this.filteringService.grid.unpinnedColumns[columnIndex];
this.filteringService.shouldFocusNext = shouldFocusNext;
this.filteringService.grid.headerContainer.scrollTo(columnIndex);
}

private isFirstElementFocused(): boolean {
return !(this.chipsArea && this.chipsArea.chipsList.length > 0 &&
this.chipsArea.chipsList.first.elementRef.nativeElement.querySelector(`.igx-chip__item`) !== document.activeElement);
}

private isLastElementFocused(): boolean {
if (this.chipsArea) {
if (this.isMoreIconHidden() && this.chipsArea.chipsList.last.elementRef.nativeElement.querySelector(`.igx-chip__remove`) !==
document.activeElement) {
return false;
} else if (!this.isMoreIconHidden() && this.moreIcon.nativeElement !== document.activeElement) {
return false;
}
}
return true;
}

private focusFirstElement(): void {
if (this.chipsArea.chipsList.length > 0) {
this.chipsArea.chipsList.first.elementRef.nativeElement.querySelector(`.igx-chip__item`).focus();
} else {
this.moreIcon.nativeElement.focus();
}
}

private focusElement(): void {
if (this.filteringService.shouldFocusNext) {
if (!this.isMoreIconHidden() && this.chipsArea.chipsList.length === 0) {
this.moreIcon.nativeElement.focus();
} else {
this.chipsArea.chipsList.first.elementRef.nativeElement.querySelector(`.igx-chip__item`).focus();
}
} else {
if (!this.isMoreIconHidden()) {
this.moreIcon.nativeElement.focus();
} else {
this.chipsArea.chipsList.last.elementRef.nativeElement.querySelector(`.igx-chip__remove`).focus();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,6 @@
</button>

<div #buttonsContainer class="igx-grid__filtering-row-editing-buttons">
<button igxButton igxRipple (click)="clearFiltering()" [disabled]="disabled">Reset</button>
<button igxButton igxRipple (click)="clearFiltering()" [disabled]="disabled" [tabindex]="disabled">Reset</button>
<button #closeButton igxButton igxRipple (click)="close()">Close</button>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export class IgxGridFilteringRowComponent implements AfterViewInit, OnDestroy {
this.unaryConditionChanged.unsubscribe();
}

@HostListener('keydown.shift.tab', ['$event'])
@HostListener('keydown.tab', ['$event'])
public onTabKeydown(event) {
event.stopPropagation();
Expand Down Expand Up @@ -299,6 +300,9 @@ export class IgxGridFilteringRowComponent implements AfterViewInit, OnDestroy {
public clearFiltering() {
this.filteringService.clearFilter(this.column.field);
this.resetExpression();
if (this.input) {
this.input.nativeElement.focus();
}
this.cdr.detectChanges();

this.chipAreaScrollOffset = 0;
Expand Down Expand Up @@ -338,6 +342,9 @@ export class IgxGridFilteringRowComponent implements AfterViewInit, OnDestroy {
});
}

this.filteringService.updateFilteringCell(this.column.field);
this.filteringService.focusFilterCellChip(this.column.field, true);

this.filteringService.isFilterRowVisible = false;
this.filteringService.filteredColumn = null;
this.filteringService.selectedExpression = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@ export class IgxFilteringService implements OnDestroy {
private filterPipe = new IgxGridFilterConditionPipe();
private titlecasePipe = new TitleCasePipe();
private datePipe = new DatePipe(window.navigator.language);
private columnStartIndex = -1;

public gridId: string;
public isFilterRowVisible = false;
public filteredColumn = null;
public selectedExpression: IFilteringExpression = null;
public columnToChipToFocus = new Map<string, boolean>();
public columnToFocus = null;
public shouldFocusNext = false;
public columnToMoreIconHidden = new Map<string, boolean>();
public columnStartIndex = -1;

constructor(private gridAPI: GridBaseAPIService<IgxGridBaseComponent>, private iconService: IgxIconService) {}

Expand Down Expand Up @@ -73,12 +74,12 @@ export class IgxFilteringService implements OnDestroy {
this.columnStartIndex = eventArgs.startIndex;
this.grid.filterCellList.forEach((filterCell) => {
filterCell.updateFilterCellArea();
if (filterCell.getChipToFocus()) {
this.columnToChipToFocus.set(filterCell.column.field, false);
filterCell.focusChip();
}
});
}
if (this.columnToFocus) {
this.focusFilterCellChip(this.columnToFocus.field, false);
this.columnToFocus = null;
}
});

this.grid.onColumnMovingEnd.pipe(takeUntil(this.destroy$)).subscribe((event) => {
Expand Down Expand Up @@ -260,13 +261,26 @@ export class IgxFilteringService implements OnDestroy {
}
}

private updateFilteringCell(columnId: string) {
/**
* Updates the content of a filterCell.
*/
public updateFilteringCell(columnId: string) {
const filterCell = this.grid.filterCellList.find(cell => cell.column.field === columnId);
if (filterCell) {
filterCell.updateFilterCellArea();
}
}

/**
* Focus a chip in a filterCell.
*/
public focusFilterCellChip(columnId: string, focusFirst: boolean) {
const filterCell = this.grid.filterCellList.find(cell => cell.column.field === columnId);
if (filterCell) {
filterCell.focusChip(focusFirst);
}
}

private isFilteringTreeComplex(expressions: IFilteringExpressionsTree | IFilteringExpression): boolean {
if (!expressions) {
return false;
Expand Down
52 changes: 35 additions & 17 deletions projects/igniteui-angular/src/lib/grids/grid-navigation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { IgxGridBaseComponent } from './grid-base.component';
import { first } from 'rxjs/operators';
import { IgxColumnComponent } from './column.component';
import { IgxGridRowComponent } from './grid/grid-row.component';

enum MoveDirection {
LEFT = 'left',
Expand Down Expand Up @@ -36,26 +37,35 @@ export class IgxGridNavigationService {
}

public isColumnFullyVisible(visibleColumnIndex: number) {
const horizontalScroll = this.grid.dataRowList.first.virtDirRow.getHorizontalScroll();
let forOfDir;
if (this.grid.dataRowList.length > 0) {
forOfDir = this.grid.dataRowList.first.virtDirRow;
} else {
forOfDir = this.grid.headerContainer;
}
const horizontalScroll = forOfDir.getHorizontalScroll();
if (!horizontalScroll.clientWidth ||
this.grid.columnList.filter(c => !c.columnGroup).find((column) => column.visibleIndex === visibleColumnIndex).pinned) {
return true;
}
const index = this.getColumnUnpinnedIndex(visibleColumnIndex);
return this.displayContainerWidth >=
this.grid.dataRowList.first.virtDirRow.getColumnScrollLeft(index + 1) -
this.displayContainerScrollLeft;
return this.displayContainerWidth >= forOfDir.getColumnScrollLeft(index + 1) - this.displayContainerScrollLeft;
}

public isColumnLeftFullyVisible(visibleColumnIndex) {
const horizontalScroll = this.grid.dataRowList.first.virtDirRow.getHorizontalScroll();
let forOfDir;
if (this.grid.dataRowList.length > 0) {
forOfDir = this.grid.dataRowList.first.virtDirRow;
} else {
forOfDir = this.grid.headerContainer;
}
const horizontalScroll = forOfDir.getHorizontalScroll();
if (!horizontalScroll.clientWidth ||
this.grid.columnList.filter(c => !c.columnGroup).find((column) => column.visibleIndex === visibleColumnIndex).pinned) {
return true;
}
const index = this.getColumnUnpinnedIndex(visibleColumnIndex);
return this.displayContainerScrollLeft <=
this.grid.dataRowList.first.virtDirRow.getColumnScrollLeft(index);
return this.displayContainerScrollLeft <= forOfDir.getColumnScrollLeft(index);
}

public get gridOrderedColumns(): IgxColumnComponent[] {
Expand Down Expand Up @@ -256,14 +266,6 @@ export class IgxGridNavigationService {

public navigateUp(rowElement, currentRowIndex, visibleColumnIndex) {
if (currentRowIndex === 0) {
this.grid.rowList.first.cells.first._clearCellSelection();

if (this.grid.allowFiltering) {
const visColLength = this.grid.visibleColumns.length;
this.grid.headerContainer.scrollTo(visColLength - 1);
this.grid.filteringService.columnToChipToFocus.set(this.grid.visibleColumns[visColLength - 1].field, true);
}

return;
}
const containerTopOffset = parseInt(this.verticalDisplayContainerElement.style.top, 10);
Expand Down Expand Up @@ -413,14 +415,30 @@ export class IgxGridNavigationService {
}
}

public moveFocusToFilterCell() {
this.grid.rowList.find(row => row instanceof IgxGridRowComponent).cells.first._clearCellSelection();
const visColLength = this.grid.unpinnedColumns.length;
if (this.isColumnFullyVisible(visColLength - 1)) {
this.grid.filteringService.focusFilterCellChip(this.grid.filterCellList.last.column.field, false);
} else {
this.grid.filteringService.columnToFocus = this.grid.unpinnedColumns[visColLength - 1];
this.grid.filteringService.shouldFocusNext = false;
this.grid.headerContainer.scrollTo(visColLength - 1);
}
}

public performShiftTabKey(currentRowEl, rowIndex, visibleColumnIndex) {
if (visibleColumnIndex === 0) {
if (this.isRowInEditMode(rowIndex)) {
this.grid.rowEditTabs.last.element.nativeElement.focus();
return;
}
this.navigateUp(currentRowEl, rowIndex,
this.grid.unpinnedColumns[this.grid.unpinnedColumns.length - 1].visibleIndex);
if (rowIndex === 0 && this.grid.allowFiltering) {
this.moveFocusToFilterCell();
} else {
this.navigateUp(currentRowEl, rowIndex,
this.grid.unpinnedColumns[this.grid.unpinnedColumns.length - 1].visibleIndex);
}
} else {
const cell = currentRowEl.querySelector(`igx-grid-cell[data-visibleIndex="${visibleColumnIndex}"]`);
if (cell) {
Expand Down
Loading