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 #2964

Merged
merged 7 commits into from
Nov 9, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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 @@ -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 { first } from 'rxjs/operators';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this import needed? If so what about last which is also used several times in this file?


/**
* @hidden
Expand Down Expand Up @@ -96,52 +97,40 @@ 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();
}
} else {
if (this.isLastElementFocused()) {
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();
}
} else {
eventArgs.preventDefault();
if (!this.filteringService.grid.filteredData || this.filteringService.grid.filteredData.length > 0) {
this.navService.goToFirstCell();
}
} else if (!this.navService.isColumnFullyVisible(this.column.visibleIndex + 1)) {
eventArgs.preventDefault();
this.filteringService.grid.headerContainer.scrollTo(this.column.visibleIndex + 1);
this.ScrollToChip(this.column.visibleIndex + 1, 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.isFisrtElementFocused()) {
if (this.column.visibleIndex > 0 && !this.navService.isColumnLeftFullyVisible(this.column.visibleIndex - 1)) {
eventArgs.preventDefault();
this.ScrollToChip(this.column.visibleIndex - 1, false);
} else if (this.column.visibleIndex === 0) {
eventArgs.preventDefault();
}
}
eventArgs.stopPropagation();
}

/**
Expand Down Expand Up @@ -199,6 +188,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 +214,34 @@ 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(fucusFirst?: boolean) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo - focus instead of fucus

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method has very high cyclomatic complexity (12). The good practice is to keep cyclomatic complexity below 6-7. Think of refactoring (extract code to other methods, combine if/else clauses, etc.) the method to avoid so many if/else expressions.
https://scottlilly.com/cyclomatic-complexity-in-visual-studio/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method has very high cyclomatic complexity (12). Consider refactoring this method by extracting code to other methods, joining if/else clauses, etc.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to provide a default value for the optional parameters. This makes the code using this parameter simpler and avoids misunderstandings.
fucusFirst?: boolean => fucusFirst: boolean = false.

if (this.currentTemplate === this.defaultFilter) {
if (this.isMoreIconVisible() === false) {
this.moreIcon.nativeElement.focus();
if (fucusFirst) {
if (this.chipsArea.chipsList.length > 0) {
this.chipsArea.chipsList.first.elementRef.nativeElement.querySelector(`.igx-chip__item`).focus();
} else {
this.moreIcon.nativeElement.focus();
}
} else if (this.filteringService.focusNext) {
if (this.isMoreIconHidden() === false && this.chipsArea.chipsList.length === 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use !this.isMoreIconHidden() instead of this.isMoreIconHidden() === false.

this.moreIcon.nativeElement.focus();
} else {
this.chipsArea.chipsList.first.elementRef.nativeElement.querySelector(`.igx-chip__item`).focus();
}
} else {
this.chipsArea.chipsList.last.elementRef.nativeElement.querySelector(`.igx-chip__item`).focus();
if (this.isMoreIconHidden() === false) {
this.moreIcon.nativeElement.focus();
} else {
this.chipsArea.chipsList.last.elementRef.nativeElement.querySelector(`.igx-chip__remove`).focus();
}
}
} else if (this.currentTemplate === this.emptyFilter) {
this.ghostChip.elementRef.nativeElement.querySelector(`.igx-chip__item`).focus();
Expand All @@ -264,7 +268,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 +313,35 @@ export class IgxGridFilteringCellComponent implements AfterViewInit, OnInit {
this.cdr.detectChanges();
}
}

private ScrollToChip(columnIndex: number, scrollRight: boolean) {
this.filteringService.grid.nativeElement.focus({preventScroll: true});
this.filteringService.columnToFocus = this.filteringService.grid.visibleColumns[columnIndex];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting columnToFocus and focusNext happens only in filtering cell component and in navigation service. And in the both places these two fields are set together and are never read. So in order to make the code a little bit easier to use and error prone we could create a method in the filtering service to set those two fields. They are only used in the service so make them private.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

focusNext is read in the filtering cell when we decide in which direction we should move the focus.

this.filteringService.focusNext = scrollRight;
this.filteringService.grid.headerContainer.scrollTo(columnIndex);
}

private isFisrtElementFocused(): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo - Fisrt.

if (this.chipsArea && this.chipsArea.chipsList.length > 0 &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could combine all this code into one return statement.

this.chipsArea.chipsList.first.elementRef.nativeElement.querySelector(`.igx-chip__item`) !== document.activeElement) {
return false;
} else {
return true;
}
}

private isLastElementFocused(): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could simplify this method. Too many if/else clauses.

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;
} else {
return true;
}
} else {
return true;
}
}
}
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.grid.filterCellList.find(cell => cell.column === this.filteringService.filteredColumn).updateFilterCellArea();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has to be done in the filtering service. It's not appropriate one component to directly manipulate another.
In the filtering service there is method updateFilteringCell which does the same (have to be made public). You could add another to focus a chip.

this.filteringService.grid.filterCellList.find(cell => cell.column === this.filteringService.filteredColumn).focusChip(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 focusNext = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps more meaningful name would be something like shouldFocusNext or shouldFocusRight.

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.grid.filterCellList.find(cell => cell.column === this.columnToFocus).focusChip();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the newly created method from my previous comment.

this.columnToFocus = null;
}
});

this.grid.onColumnMovingEnd.pipe(takeUntil(this.destroy$)).subscribe((event) => {
Expand Down
47 changes: 30 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 @@ -36,26 +36,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 +265,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 @@ -419,8 +420,20 @@ export class IgxGridNavigationService {
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.grid.rowList.first.cells.first._clearCellSelection();
const visColLength = this.grid.visibleColumns.length;
if (this.grid.headerContainer.getItemCountInView() === visColLength) {
this.grid.filterCellList.last.focusChip();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be done through the new method in the filtering service.

} else {
this.grid.filteringService.columnToFocus = this.grid.visibleColumns[visColLength - 1];
this.grid.filteringService.focusNext = false;
this.grid.headerContainer.scrollTo(visColLength - 1);
}
} 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
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
<div class="igx-grid__thead-item igx-grid__th--pinned">
<igx-grid-header [igxColumnMovingDrag]="col" [attr.droppable]="true" [igxColumnMovingDrop]="col" [gridID]="id"
[column]="col" [style.min-width.px]="col.width" [style.flex-basis.px]="col.width" [style.max-width.px]='col.width'></igx-grid-header>
<igx-grid-filtering-cell *ngIf="allowFiltering && !col.columnGroup && filteringService.filteredColumn !== col"
<igx-grid-filtering-cell *ngIf="allowFiltering && !col.columnGroup"
[column]="col" [style.min-width.px]="col.width" [style.flex-basis.px]='col.width' [style.max-width.px]='col.width'></igx-grid-filtering-cell>
</div>
</ng-template>
Expand All @@ -67,7 +67,7 @@
<div class="igx-grid__thead-item">
<igx-grid-header [igxColumnMovingDrag]="col" [attr.droppable]="true" [igxColumnMovingDrop]="col" [gridID]="id" [column]="col"
[style.min-width.px]="col.width" [style.flex-basis.px]='col.width' [style.max-width.px]='col.width'></igx-grid-header>
<igx-grid-filtering-cell *ngIf="allowFiltering && !col.columnGroup && filteringService.filteredColumn !== col" [column]="col"
<igx-grid-filtering-cell *ngIf="allowFiltering && !col.columnGroup" [column]="col"
[style.min-width.px]="col.width" [style.flex-basis.px]='col.width' [style.max-width.px]='col.width'></igx-grid-filtering-cell>
</div>
</ng-template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<div class="igx-grid__thead-item igx-grid__th--pinned">
<igx-grid-header [igxColumnMovingDrag]="col" [attr.droppable]="true" [igxColumnMovingDrop]="col" [gridID]="id"
[column]="col" [style.min-width.px]="col.width" [style.flex-basis.px]="col.width"></igx-grid-header>
<igx-grid-filtering-cell *ngIf="allowFiltering && !col.columnGroup && filteringService.filteredColumn !== col"
<igx-grid-filtering-cell *ngIf="allowFiltering && !col.columnGroup"
[column]="col" [style.min-width.px]="col.width" [style.flex-basis.px]='col.width' [style.max-width.px]='col.width'></igx-grid-filtering-cell>
</div>
</ng-template>
Expand All @@ -44,7 +44,7 @@
<div class="igx-grid__thead-item">
<igx-grid-header [igxColumnMovingDrag]="col" [attr.droppable]="true" [igxColumnMovingDrop]="col" [gridID]="id" [column]="col"
[style.min-width.px]="col.width" [style.flex-basis.px]='col.width'></igx-grid-header>
<igx-grid-filtering-cell *ngIf="allowFiltering && !col.columnGroup && filteringService.filteredColumn !== col"
<igx-grid-filtering-cell *ngIf="allowFiltering && !col.columnGroup"
[column]="col" [style.min-width.px]="col.width" [style.flex-basis.px]='col.width' [style.max-width.px]='col.width'></igx-grid-filtering-cell>
</div>
</ng-template>
Expand Down