-
Notifications
You must be signed in to change notification settings - Fork 161
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
Changes from 2 commits
bd73494
35ad755
938d241
acb1c82
00cc67a
3d10f2e
947daa4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
||
/** | ||
* @hidden | ||
|
@@ -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(); | ||
} | ||
|
||
/** | ||
|
@@ -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(); | ||
} | ||
|
||
/** | ||
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo - focus instead of fucus There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
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(); | ||
|
@@ -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); | ||
} | ||
|
||
|
@@ -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]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Setting There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo - |
||
if (this.chipsArea && this.chipsArea.chipsList.length > 0 && | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
---|---|---|
|
@@ -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(); | ||
|
@@ -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; | ||
|
@@ -338,6 +342,9 @@ export class IgxGridFilteringRowComponent implements AfterViewInit, OnDestroy { | |
}); | ||
} | ||
|
||
this.filteringService.grid.filterCellList.find(cell => cell.column === this.filteringService.filteredColumn).updateFilterCellArea(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
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; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps more meaningful name would be something like |
||
public columnToMoreIconHidden = new Map<string, boolean>(); | ||
public columnStartIndex = -1; | ||
|
||
constructor(private gridAPI: GridBaseAPIService<IgxGridBaseComponent>, private iconService: IgxIconService) {} | ||
|
||
|
@@ -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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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[] { | ||
|
@@ -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); | ||
|
@@ -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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
There was a problem hiding this comment.
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?