Angular Hierarchical Grid Keyboard Navigation
IgxHierarchicalGrid 의 키보드 탐색은 사용자에게 다양한 키보드 상호 작용을 제공합니다. IgxHierarchicalGrid 의 접근성을 향상하고 내부의 모든 유형의 요소(셀, 행, 열 머리글, 도구 모음, 바닥글 등)를 탐색할 수 있습니다. 이 기능은 기본적으로 활성화되어 있으며 개발자는 기본 동작을 쉽게 재정의할 수 있는 옵션이 있습니다.
탐색이 W3C 접근성 표준을 준수하고 사용하기 편리하도록 IgxHierarchicalGrid의 표가 줄어들었습니다.
현재 IgxHierarchicalGrid에는 다음 탭 정지가 도입되었습니다.
GroupBy 또는 도구 모음 영역 (활성화된 경우)
IgxHierarchicalGrid header ;
IgxHierarchicalGrid body ;
열 요약 (활성화된 경우)
IgxHierarchicalGrid paginator (if enabled);
이러한 변경으로 인해 IgxHierarchicalGrid에서는 Tab 및 Shift + Tab을 사용한 셀 간 탐색이 더 이상 지원되지 않습니다. 이제 Tab 키를 누르면 GroupBy / Toolbar -> Headers -> Body -> Summaries -> Footer / Paginator 순서로 탭 정지가 진행됩니다.
템플릿을 통해 포커스 가능한 요소를 IgxHierarchicalGrid 본문에 노출하면 기본 브라우저 동작이 방지되지 않으므로 키보드 탐색에 부작용이 발생할 수 있습니다. 이를 적절하게 방지하거나 수정하는 것은 개발자의 책임입니다.
전체 키보드 탐색 에서 지원 Igx계층적 그리드 이제 헤더가 도입되었습니다. 열 머리글은 화살표 키를 사용하여 쉽게 이동할 수 있습니다. 또한 다음과 같이 열에 대한 작업을 트리거하는 여러 가지 키 조합이 있습니다. 필터링 , 정렬 , 그룹화 등등. Igx계층적 그리드 헤더 컨테이너에 초점이 맞춰지면 다음 키 조합을 사용할 수 있습니다.
Key Combinations
위쪽 화살표는 머리글에서 한 셀 위로 이동합니다(반복 없음). 다중 행 레이아웃(MRL) 또는 다중 열 헤더(MCH)가 정의된 경우에만 사용할 수 있습니다.
아래쪽 화살표는 헤더에서 한 셀 아래로 이동합니다(줄 바꿈 없음). 다중 행 레이아웃(MRL) 또는 다중 열 헤더(MCH)가 정의된 경우에만 사용할 수 있습니다.
왼쪽 화살표는 한 셀 왼쪽으로 이동합니다(반복 없음).
오른쪽 화살표 는 한 셀 오른쪽으로 이동합니다(줄 바꿈 없음).
Ctrl + 왼쪽 화살표는 행의 가장 왼쪽 셀로 이동합니다. MRL 또는 MCH가 활성화된 경우 동일한 수준의 가장 왼쪽 셀로 이동합니다.
홈은 행의 가장 왼쪽 셀로 이동합니다. MRL 또는 MCH가 활성화된 경우 동일한 수준의 가장 왼쪽 셀로 이동합니다.
Ctrl + 오른쪽 화살표는 행의 가장 오른쪽 셀로 이동합니다. MRL 또는 MCH가 활성화된 경우 동일한 수준의 가장 오른쪽 셀로 이동합니다.
End는 행의 가장 오른쪽 셀로 이동합니다. MRL 또는 MCH가 활성화된 경우 동일한 수준의 가장 오른쪽 셀로 이동합니다.
고급 필터링이 활성화된 경우 Alt + L은 고급 필터링 대화 상자를 엽니다.
Ctrl + Shift + L은 Excel 스타일 필터 대화 상자를 열거나 열을 필터링할 수 있는 경우 기본(행) 필터를 엽니다.
Ctrl + 위쪽 화살표는 활성 열 머리글을 ASC 순서로 정렬합니다. 열이 이미 ASC로 정렬되어 있는 경우 정렬 상태가 지워집니다.
Ctrl + 아래쪽 화살표는 활성 열 머리글을 DSC 순서로 정렬합니다. DSC에서 열이 이미 정렬된 경우 정렬 상태가 지워집니다.
Space는 열을 선택합니다. 열이 이미 선택되어 있으면 선택이 지워집니다.
Body navigation
IgxHierarchicalGrid 본문에 초점이 맞춰지면 다음 키 조합을 사용할 수 있습니다.
Key Combination
위쪽 화살표 - 한 셀 위로 이동하거나 필요한 경우 그리드 계층 구조에서 한 수준 위로 이동합니다(줄 바꿈 없음).
아래쪽 화살표는 한 셀 아래로 탐색하거나 필요한 경우 그리드 계층 구조에서 한 수준 아래로 탐색합니다(줄바꿈 없음).
왼쪽 화살표는 한 셀 왼쪽으로 이동합니다(줄 바꿈 없음).
오른쪽 화살표 - 한 셀 오른쪽으로 이동합니다(줄 바꿈 없음).
Ctrl + 왼쪽 화살표는 행의 가장 왼쪽 셀로 이동합니다.
Ctrl + 오른쪽 화살표는 행의 가장 오른쪽 셀로 이동합니다.
Ctrl + 위쪽 화살표는 열의 첫 번째 셀로 이동합니다.
Ctrl + 아래쪽 화살표는 열의 마지막 셀로 이동합니다.
홈은 행의 가장 왼쪽 셀로 이동합니다.
End는 행의 가장 오른쪽 셀로 이동합니다.
Ctrl + Home은 그리드의 가장 왼쪽 상단 데이터 셀로 이동합니다.
Ctrl + End는 그리드의 가장 오른쪽 하단 데이터 셀로 이동합니다.
Page Up은 한 페이지(뷰 포트) 위로 스크롤합니다.
Page Down은 한 페이지(뷰 포트) 아래로 스크롤합니다.
Enter 편집 모드로 들어갑니다.
F2는 편집 모드로 들어갑니다
Esc를 눌러 편집 모드를 종료합니다.
편집 모드에 셀이 있는 경우에만 탭을 사용할 수 있습니다. 행의 편집 가능한 다음 셀로 포커스를 이동합니다. 행의 마지막 셀에 도달한 후 다음 행의 편집 가능한 첫 번째 셀로 포커스를 이동합니다. 행 편집이 활성화되면 행의 가장 오른쪽 편집 가능한 셀에서 CANCEL 및 DONE 버튼으로 포커스가 이동하고 DONE 버튼에서 행의 가장 왼쪽 편집 가능한 셀로 포커스가 이동됩니다.
Shift + Tab - 편집 모드에 셀이 있는 경우에만 사용할 수 있습니다. 행의 편집 가능한 이전 셀로 포커스를 이동합니다. 행의 첫 번째 셀에 도달한 후 이전 행의 편집 가능한 마지막 셀로 포커스를 이동합니다. 행 편집이 활성화되면 행의 가장 오른쪽 편집 가능한 셀에서 CANCEL 및 DONE 버튼으로 초점이 이동하고 DONE 버튼에서 행의 가장 오른쪽 편집 가능한 셀로 이동됩니다.
공백 -행 선택이 활성화된 경우 행을 선택합니다.
Alt + Arrow Left or Alt + Arrow Up - collapses the row island
Alt + Arrow Right or Alt + Arrow Down - expands the row island
아래 데모 샘플에서 위에 언급된 모든 작업을 연습해 보세요. 탐색 가능한 그리드 요소에 초점을 맞추면 해당 요소에 사용 가능한 일부 작업이 포함된 목록이 표시되어 안내됩니다.
데모
import { animate, state, style, transition, trigger } from '@angular/animations' ;
import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from '@angular/core' ;
import { CellType, IgxColumnComponent, IgxColumnGroupComponent, IgxHierarchicalGridComponent, IgxListComponent, IgxPaginatorComponent, IgxGridToolbarComponent, IgxRowIslandComponent, IgxGridToolbarDirective, IgxListItemComponent, IgxListLineTitleDirective, IgxListLineSubTitleDirective, IgxCheckboxComponent, IgxEmptyListTemplateDirective } from 'igniteui-angular' ;
import { fromEvent, Subject } from 'rxjs' ;
import { takeUntil } from 'rxjs/operators' ;
import { CUSTOMERS } from '../../data/hierarchical-data' ;
import { NgClass } from '@angular/common' ;
enum GridSection {
THEAD = 'igx-grid__thead-wrapper' ,
TBODY = 'igx-grid__tbody-content' ,
FOOTER = 'igx-grid__tfoot'
}
enum ItemAction {
Filterable,
Sortable,
Selectable,
Groupable,
Collapsible,
Expandable,
Editable,
Always
}
enum ElementTags {
GROUPBY_ROW = 'IGX-GRID-GROUPBY-ROW' ,
COLUMN_GROUP = 'IGX-COLUMN-GROUP'
}
class Item {
public title: string ;
public subTitle: string ;
public action: ItemAction;
public active = false ;
private _completed: boolean ;
public constructor (title: string , subTitle: string , completed: boolean , itemAction?: ItemAction ) {
this .title = title;
this .subTitle = subTitle;
this .completed = completed;
this .action = itemAction;
if (itemAction === ItemAction.Always) {
this .active = true ;
}
}
public set completed (value: boolean ) {
if (this .active || (!value && !this .completed)) {
this ._completed = value;
}
}
public get completed () {
return this ._completed;
}
}
class KeyboardHandler {
private _collection: Item[];
private _section: GridSection;
public constructor (colleciton: Item[], section: GridSection ) {
this ._collection = colleciton;
this ._section = section;
}
public set collection (collection: Item[] ) {
this ._collection = collection;
}
public get collection () {
return this ._collection;
}
public set gridSection (section: GridSection ) {
this ._section = section;
}
public get gridSection () {
return this ._section;
}
public enableActionItems (action: ItemAction[] ) {
this .resetCollection();
action.forEach(element => {
this ._collection
.filter(e => e.action === element)
.map(e => e.active = true );
});
}
public resetCollection ( ) {
this ._collection.forEach(e => {
if (e.action !== ItemAction.Always) {
e.active = false ;
}
});
}
public selectItem (idx: number ) {
this ._collection[idx].completed = true ;
}
public deselectItem (idx: number ) {
this ._collection[idx].completed = false ;
}
}
const theadKeyCombinations = [
new Item('space key' , 'select column' , false , ItemAction.Selectable),
new Item('ctrl + arrow up/down' , 'sorts the column asc/desc' , false , ItemAction.Sortable),
new Item('alt + arrow left/right/up/down' , 'expand/collapse active multi column header' ,
false ,
ItemAction.Collapsible),
new Item('alt + l' , 'opens the advanced filtering' , false , ItemAction.Filterable),
new Item('ctrl + shift + l' , 'opens the excel style filtering' , false , ItemAction.Filterable)
];
const tbodyKeyCombinations: Item[] = [
new Item('enter' , 'enter in edit mode' , false , ItemAction.Editable),
new Item('alt + arrow left/up' , 'collapse row island row' , false , ItemAction.Collapsible),
new Item('alt + arrow right/down' , 'expand row island row' , false , ItemAction.Collapsible),
new Item('ctrl + Home/End' , 'navigates to the upper-left/bottom-right cell' , false , ItemAction.Always)
];
const summaryCombinations: Item[] = [
new Item('ArrowLeft' , 'navigates one summary cell right' , false , ItemAction.Always),
new Item('ArrowRight' , 'navigates one summary cell left' , false , ItemAction.Always),
new Item('Home' , 'navigates to the first summary cell' , false , ItemAction.Always),
new Item('End' , 'navigates to the last summary cell' , false , ItemAction.Always)
];
@Component ({
selector : 'grid-keyboardnav' ,
templateUrl : './hgrid-keyboard-guide.component.html' ,
styleUrls : ['hgrid-keyboard-guide.component.scss' ],
animations : [
trigger('toggle' , [
state('selected' , style({
color : '#4eb862'
})),
state('deselected' , style({
color : 'black'
})),
transition('deselected => selected' , [
animate('.3s' )
]),
transition('selected => deselected' , [
animate('.3s' )
])
]),
trigger('load' , [
transition(':enter' , [
style({ opacity : 0 }),
animate('.3s' , style({ opacity : .4 }))
])
])
],
imports : [IgxHierarchicalGridComponent, IgxPaginatorComponent, IgxGridToolbarComponent, IgxColumnComponent, IgxColumnGroupComponent, IgxRowIslandComponent, IgxGridToolbarDirective, IgxListComponent, IgxListItemComponent, NgClass, IgxListLineTitleDirective, IgxListLineSubTitleDirective, IgxCheckboxComponent, IgxEmptyListTemplateDirective]
})
export class HGridKeyboardnavGuide implements OnInit , OnDestroy {
@ViewChild (IgxHierarchicalGridComponent, { static : true })
public hGrid: IgxHierarchicalGridComponent;
@ViewChild (IgxListComponent, { static : true })
public listref: IgxListComponent;
public get keyboardCollection () {
return this .keyboardHandler.collection;
}
public get headerList () {
return this .keyboardHandler.gridSection === GridSection.THEAD ?
'HEADER COMBINATIONS' : this .keyboardHandler.gridSection === GridSection.TBODY ?
'BODY COMBITNATIONS' : this .keyboardHandler.gridSection === GridSection.FOOTER ?
'SUMMARY COMBINATIONS' : '' ;
}
public gridTarget: GridUnderManagement;
private _destroyer = new Subject<void >();
private keyboardHandler = new KeyboardHandler([], GridSection.THEAD);
public constructor (
private cdr: ChangeDetectorRef ) { }
public onActiveNodeChange (evt ) {
if (this .hGrid.crudService.cell) {
return ;
}
const grid = evt.owner || this .hGrid;
const gridSection = evt.row < 0 ? GridSection.THEAD : evt.row === grid.dataView.length ? GridSection.FOOTER : GridSection.TBODY;
this .changeKeyboardCollection(gridSection);
this .gridTarget.toggleHeaderCombinations(evt);
this .gridTarget.toggleBodyCombinations(evt);
}
public ngOnInit ( ) {
this .hGrid.data = CUSTOMERS;
for (const item of this .hGrid.data) {
const names = item.CompanyName.split(' ' );
item.FirstName = names[0 ];
item.LastName = names[names.length - 1 ];
item.FullAddress = `${item.Address} , ${item.City} , ${item.Country} ` ;
item.PersonelDetails = `${item.ContactTitle} : ${item.ContactName} ` ;
item.CompanysAnnualProfit = (100000 + (Math .random() * Math .floor(1000000 ))).toFixed(0 );
}
this .gridTarget = new GridUnderManagement(this .hGrid, this .keyboardHandler, this ._destroyer, this .cdr);
this .gridTarget.subscribe();
this .listref.itemClicked.pipe(takeUntil(this ._destroyer))
.subscribe((args ) => {
args.event.stopPropagation();
});
}
public ngOnDestroy ( ) {
this ._destroyer.next();
}
public expandChange ( ) {
if (!this .keyboardHandler.collection.length) {
return ;
}
this .keyboardHandler.selectItem(2 );
}
public onCheckChange (evt, idx ) {
evt.checked ? this .keyboardHandler.selectItem(idx) : this .keyboardHandler.deselectItem(idx);
}
public changeKeyboardCollection (gridSection: GridSection ) {
switch (gridSection) {
case GridSection.THEAD:
this .keyboardHandler.collection = theadKeyCombinations;
break ;
case GridSection.TBODY:
this .keyboardHandler.collection = tbodyKeyCombinations;
break ;
case GridSection.FOOTER:
this .keyboardHandler.collection = summaryCombinations;
break ;
default :
this .keyboardHandler.collection = [];
return ;
}
this .keyboardHandler.gridSection = gridSection;
}
public onGridCreated (evt ) {
fromEvent(evt.grid.elementRef.nativeElement, 'click' ).pipe(takeUntil(this ._destroyer))
.subscribe(() => {
this .gridTarget = new GridUnderManagement(evt.grid, this .keyboardHandler, this ._destroyer, this .cdr);
this .gridTarget.subscribe();
});
fromEvent(evt.grid.elementRef.nativeElement, 'focus' ).pipe(takeUntil(this ._destroyer))
.subscribe(() => {
this .gridTarget = new GridUnderManagement(evt.grid, this .keyboardHandler, this ._destroyer, this .cdr);
this .gridTarget.subscribe();
});
fromEvent((evt.grid as IgxHierarchicalGridComponent).tbody.nativeElement, 'focus' )
.pipe(takeUntil(this ._destroyer)).subscribe(() => {
this .gridTarget = new GridUnderManagement(evt.grid, this .keyboardHandler, this ._destroyer, this .cdr);
this .gridTarget.subscribe();
});
}
public gridKeydown (evt ) {
const key = evt.key.toLowerCase();
if (this .keyboardHandler.gridSection === GridSection.FOOTER) {
switch (key) {
case 'end' :
this .keyboardHandler.selectItem(3 );
break ;
case 'home' :
this .keyboardHandler.selectItem(2 );
break ;
case 'arrowleft' :
this .keyboardHandler.selectItem(0 );
break ;
case 'arrowright' :
this .keyboardHandler.selectItem(1 );
break ;
default :
break ;
}
return ;
}
const activeNode = this .gridTarget.hGrid.navigation.activeNode;
if (this .keyboardHandler.gridSection === GridSection.THEAD) {
if (key === 'l' && evt.altKey) {
this .keyboardHandler.selectItem(3 );
return ;
}
const col = this .gridTarget.hGrid.visibleColumns.find
(c => c.visibleIndex === activeNode.column && c.level === activeNode.level);
if (key === 'l' && evt.ctrlKey && evt.shiftKey && col && !col.columnGroup && col.filterable) {
this .keyboardHandler.selectItem(4 );
}
if ((key === 'arrowup' || key === 'arrowdown' ) && evt.ctrlKey && col && !col.columnGroup && col.sortable) {
this .keyboardHandler.selectItem(1 );
}
}
if (this .keyboardHandler.gridSection === GridSection.TBODY) {
if (key === 'enter' ) {
const columnName = this .gridTarget.hGrid.getColumnByVisibleIndex(activeNode.column).field;
const cell = this .gridTarget.hGrid.getCellByColumn(activeNode.row, columnName);
if (cell && cell.column.editable && cell.editMode) {
this .keyboardHandler.selectItem(0 );
}
}
if ((evt.code === 'End' || evt.code === 'Home' ) && evt.ctrlKey) {
this .keyboardHandler.selectItem(3 );
}
}
}
}
export class GridUnderManagement {
public hGrid: IgxHierarchicalGridComponent;
public destroyer = new Subject<void >();
public keyboardHandler: KeyboardHandler;
public cdr: ChangeDetectorRef;
constructor (hGrid: IgxHierarchicalGridComponent, keyboardHandler: KeyboardHandler,
destroyer: Subject<void >, cdr: ChangeDetectorRef ) {
this .hGrid = hGrid;
this .keyboardHandler = keyboardHandler;
this .destroyer = destroyer;
this .cdr = cdr;
}
public subscribe ( ) {
this .hGrid.columnSelectionChanging.pipe(takeUntil(this .destroyer))
.subscribe((args ) => {
const evt = args.event;
if (evt.type === 'keydown' ) {
this .keyboardHandler.selectItem(0 );
}
});
this .hGrid.rowToggle.pipe(takeUntil(this .destroyer))
.subscribe((args ) => {
const evt = args.event as KeyboardEvent;
if (!evt || evt.type !== 'keydown' ) {
return ;
}
return evt.code === 'ArrowLeft' || evt.code === 'ArrowUp' ? this .keyboardHandler.selectItem(1 ) :
this .keyboardHandler.selectItem(2 );
});
}
public toggleHeaderCombinations (activeNode ) {
if (this .keyboardHandler.gridSection !== GridSection.THEAD || activeNode.column === undefined || activeNode.level === undefined ) {
return ;
}
const grid = activeNode.owner || this .hGrid;
const currColumn = grid.columnList
.find(c => c.visibleIndex === activeNode.column && c.level === activeNode.level);
const actions = this .extractColumnActions(currColumn);
this .keyboardHandler.enableActionItems(actions);
}
public toggleBodyCombinations (activeNode ) {
const grid = activeNode.owner || this .hGrid;
const rowRef = grid.getRowByIndex(activeNode.row);
if (this .keyboardHandler.gridSection !== GridSection.TBODY || !rowRef) {
return ;
}
if (rowRef.isGroupByRow) {
this .keyboardHandler.enableActionItems([ItemAction.Expandable]);
} else {
const cell = grid.getCellByColumn(activeNode.row,
grid.columnList.find((col ) => col.visibleIndex === activeNode.column).field);
this .toggleCellCombinations(cell);
}
}
public toggleCellCombinations (cell?: CellType ) {
if (this .keyboardHandler.gridSection !== GridSection.TBODY || !cell) {
return ;
}
const actions = this .extractCellActions(cell);
this .keyboardHandler.enableActionItems(actions);
}
public extractColumnActions (col: IgxColumnComponent | IgxColumnGroupComponent ) {
const res = [];
if (col.sortable) {
res.push(ItemAction.Sortable);
}
if (col.filterable && !col.columnGroup) {
res.push(ItemAction.Filterable);
}
if (col.collapsible) {
res.push(ItemAction.Collapsible);
}
if (col.groupable) {
res.push(ItemAction.Groupable);
}
if (col.selectable) {
res.push(ItemAction.Selectable);
}
return res;
}
public extractCellActions (cell: CellType ) {
const res = [];
if (cell.editable) {
res.push(ItemAction.Editable);
}
res.push(ItemAction.Collapsible);
return res;
}
}
ts コピー <div class ="sample" >
<div class ="grid_wrapper" >
<igx-hierarchical-grid height ="570px" width ="100%" [allowAdvancedFiltering ]="true"
columnSelection ="multiple" [allowFiltering ]="true" [filterMode ]="'excelStyleFilter'"
(keydown )="gridKeydown($event)" (activeNodeChange )="onActiveNodeChange($event)" >
<igx-paginator > </igx-paginator >
@if (false) {
<igx-grid-toolbar > </igx-grid-toolbar >
}
<igx-column field ="CustomerID" [filterable ]="true" [sortable ]="true" [editable ]="true" > </igx-column >
<igx-column-group header ="General Information" >
<igx-column field ="CompanyName" [sortable ]="true" > </igx-column >
<igx-column-group header ="Personal Details" >
<igx-column field ="ContactName" [filterable ]="true" [hasSummary ]="true" [editable ]="true" > </igx-column >
<igx-column field ="ContactTitle" [sortable ]="true" > </igx-column >
</igx-column-group >
</igx-column-group >
<igx-column-group header ="Address Information" >
<igx-column-group header ="Location" [collapsible ]="true" [expanded ]="false" (expandedChange )="expandChange()" >
<igx-column field ="FullAddress" header ="Full Address" [width ]="'250px'" [visibleWhenCollapsed ]="true"
[dataType ]="'string'" [sortable ]="true" [editable ]="true" > </igx-column >
<igx-column [visibleWhenCollapsed ]="false" field ="Address" [selectable ]="false" [filterable ]="true" > </igx-column >
<igx-column [visibleWhenCollapsed ]="false" field ="City" [sortable ]="true" > </igx-column >
<igx-column [visibleWhenCollapsed ]="false" field ="PostalCode" [selectable ]="false" [filterable ]="true" [hasSummary ]="true" > </igx-column >
<igx-column [visibleWhenCollapsed ]="false" field ="Country" [selectable ]="false" > </igx-column >
</igx-column-group >
<igx-column-group header ="Contact Information" >
<igx-column field ="Phone" [filterable ]="true" > </igx-column >
<igx-column field ="Fax" > </igx-column >
</igx-column-group >
</igx-column-group >
<igx-row-island [height ]="null" [key ]="'Orders'" [autoGenerate ]="false" columnSelection ="multiple" (gridCreated )="onGridCreated($event)" (activeNodeChange )="onActiveNodeChange($event)" [allowAdvancedFiltering ]="true" [allowFiltering ]="true" [filterMode ]="'excelStyleFilter'" >
<igx-grid-toolbar *igxGridToolbar ="let childGrid" > </igx-grid-toolbar >
<igx-column-group header ="Order Information" >
<igx-column-group header ="Order Details" >
<igx-column [editable ]="true" [sortable ]="true" field ="OrderID" > </igx-column >
<igx-column [editable ]="true" [sortable ]="true" field ="EmployeeID" > </igx-column >
<igx-column [editable ]="true" [sortable ]="true" field ="OrderDate" [dataType ]="'date'" [hasSummary ]="true" > </igx-column >
<igx-column [editable ]="true" field ="RequiredDate" [dataType ]="'date'" > </igx-column >
</igx-column-group >
<igx-column-group header ="General Shipping Information" >
<igx-column field ="ShippedDate" [dataType ]="'date'" > </igx-column >
<igx-column field ="ShipVia" [selectable ]="false" > </igx-column >
<igx-column field ="Freight" [selectable ]="false" > </igx-column >
<igx-column field ="ShipName" [hasSummary ]="true" > </igx-column >
</igx-column-group >
<igx-column-group header ="Shipping Location" >
<igx-column field ="ShipAddress" > </igx-column >
<igx-column field ="ShipCity" > </igx-column >
<igx-column field ="ShipPostalCode" > </igx-column >
<igx-column field ="ShipCountry" > </igx-column >
</igx-column-group >
</igx-column-group >
<igx-row-island [height ]="null" [key ]="'OrderDetails'" [autoGenerate ]="false" columnSelection ="single" (gridCreated )="onGridCreated($event)" [allowAdvancedFiltering ]="true" [allowFiltering ]="true" [filterMode ]="'excelStyleFilter'" (activeNodeChange )="onActiveNodeChange($event)" >
<igx-grid-toolbar *igxGridToolbar ="let childGrid" > </igx-grid-toolbar >
<igx-column [editable ]="true" [sortable ]="true" field ="ProductID" > </igx-column >
<igx-column [editable ]="true" [sortable ]="true" field ="UnitPrice" > </igx-column >
<igx-column [editable ]="true" [sortable ]="true" field ="Quantity" [selectable ]="false" > </igx-column >
<igx-column [editable ]="true" field ="Discount" [hasSummary ]="true" > </igx-column >
</igx-row-island >
</igx-row-island >
</igx-hierarchical-grid >
</div >
<div class ="list-sample" >
<igx-list >
@if (keyboardCollection.length > 0) {
<igx-list-item [isHeader ]="true" > {{headerList}}</igx-list-item >
}
@for (c of keyboardCollection; track c; let idx = $index) {
<igx-list-item @load [ngClass ]="{ 'active': c.active, 'disabled': !c.active}" [@toggle ]="c.completed ? 'selected' : 'deselected'" >
<h4 igxListLineTitle > {{ c.title }}</h4 >
<p igxListLineSubTitle > {{ c.subTitle }}</p >
<igx-checkbox [disabled ]="!c.active" [checked ]="c.completed" (change )="onCheckChange($event, idx)" > </igx-checkbox >
</igx-list-item >
}
<ng-template igxEmptyList >
<span class ="empty-list" >
<h6 > Use the native navigation of the browser until you reach some of the following grid sections below:</h6 >
<ul >
<li > Header</li >
<li > Body</li >
<li > Summary</li >
</ul >
<h6 > When reached, an <b > action list</b > will be shown.</h6 >
</span >
</ng-template >
</igx-list >
</div >
</div >
html コピー @use '../../../variables' as *;
$my-color : color($color : 'success' );
$custom-checkbox-theme : checkbox-theme(
$fill-color : $my-color ,
$border-radius : 10px
);
.list-sample ::ng-deep {
@include css-vars($custom-checkbox-theme );
}
.sample {
display : flex;
.grid_wrapper {
--ig-size: var(--ig-size-small);
padding-left : 15px ;
padding-top : 15px ;
width : 75% ;
}
.list-sample {
padding-top : 15px ;
width : 20% ;
.disabled {
opacity : .4 ;
}
.active {
opacity : 1 ;
}
igx-list {
.igx-list__item-line-title , .igx-list__item-line-subtitle {
font-size : 13px ;
}
height : 570px ;
box-shadow : 0 1px 3px 0 rgba(0 , 0 , 0 , 0.2 ),
0 1px 1px 0 rgba(0 , 0 , 0 , 0.14 ),
0 2px 1px -1px rgba(0 , 0 , 0 , 0.12 );
}
.empty-list {
opacity : 1 ;
h6 {
padding : 15px ;
font-size : 15px ;
}
ul {
li {
font-size : 13px ;
}
margin-left : 15px ;
font-weight : 400 ;
}
}
}
}
scss コピー
Like this sample? Get access to our complete Ignite UI for Angular toolkit and start building your own apps in minutes. Download it for free.
Custom keyboard navigation
특정 키 또는 키 조합에 대한 기본 동작을 재정의하는 것은 키보드 탐색 기능이 제공하는 이점 중 하나입니다. 예를 들어 다음 셀이나 아래 셀로 이동하려면 Enter 또는 Tab 키를 누르세요. 이 탐색 시나리오나 다른 탐색 시나리오는 키보드 탐색 API를 통해 쉽게 달성할 수 있습니다.
API
설명
인수
gridKeydown
위에서 설명한 키 누름/조합이 수행될 때 발생하는 이벤트입니다. 취소될 수 있습니다. 다른 키 누르기/조합의 경우 기본값을 사용하십시오.onkeydown
이벤트.
IGridKeydownEventArgs
activeNodeChange
활성 노드가 변경될 때 발생하는 이벤트입니다. 이를 사용하여 활성 포커스 위치(헤더, tbody 등), 열 인덱스, 행 인덱스 또는 중첩 수준을 결정할 수 있습니다.
IActiveNodeChangeEventArgs
navigateTo
제공된 기준에 따라 그리드의 위치로 이동합니다.rowindex
그리고visibleColumnIndex
. 또한 유형의 매개변수를 허용하는 콜백 함수를 통해 대상 요소에 대해 사용자 정의 논리를 실행할 수도 있습니다.{ targetType: GridKeydownTargetType, target: Object }
. 용법:Grid.navigateTo(10, 3, (args) => { args.target.nativeElement.focus(); });
rowindex
: number, visibleColumnIndex
: number, callback
: ({ targetType: GridKeydownTargetType, target: Object }
) => {}
getNextCell
보고ICellPosition
다음 셀을 정의하는 객체rowIndex
그리고visibileColumnIndex
. 콜백 함수는 세 번째 매개변수로 전달될 수 있습니다.getNextCell
방법. 콜백 함수는 다음을 허용합니다.IgxColumnComponent
매개변수로 반환하고boolean
주어진 기준이 충족되면 값 표시:const nextEditableCell = Grid.getNextCell(0, 4, (col) => col.editable);
currentRowIndex
: 숫자,currentVisibleColumnIndex
: 숫자,callback
: (IgxColumnComponent
) => 부울
getPreviousCell
보고ICellPosition
이전 셀을 정의하는 객체rowIndex
그리고visibileColumnIndex
. 콜백 함수는 세 번째 매개변수로 전달될 수 있습니다.getPreviousCell
방법. 콜백 함수는 다음을 허용합니다.IgxColumnComponent
매개변수로 반환하고boolean
주어진 기준이 충족되면 값 표시:const prevEditableCell = Grid.getPreviousCell(0, 4, (col) => col.editable);
currentRowIndex
: 숫자,currentVisibleColumnIndex
: 숫자,callback
: (IgxColumnComponent
) => 부울
사용자 입력 유효성 검사 및 사용자 지정 탐색과 같은 일반적인 시나리오를 달성하는 방법을 보여주기 위해 API를 사용해 보겠습니다. 먼저 gridKeydown
이벤트에 대한 이벤트 핸들러를 등록해야 합니다.
<igx-hierarchical-grid #grid1 [data ]="data" (gridKeydown )="customKeydown($event, grid1)" >
<igx-row-island [key ]="'Albums'" (gridCreated )="childGridCreated($event)" >
</igx-row-island >
</igx-hierarchical-grid >
html
igxHierarchicalGrid 하위 그리드에도 사용자 정의 키보드 탐색을 추가하려면 각 하위 그리드가 gridKeydown
이벤트를 구독해야 합니다. 이것이 바로 위의 예에서 우리가 gridCreated
이벤트에 대한 이벤트 핸들러를 등록한 이유입니다.
public childGridCreated (event: IGridCreatedEventArgs ) {
const grid = event.grid;
event.grid.gridKeydown.subscribe((args ) => {
this .customKeydown(args, grid);
});
}
typescript
public customKeydown (args: IGridKeydownEventArgs ) {
const target: IgxGridCell = args.target as IgxGridCell;
const evt: KeyboardEvent = args.event as KeyboardEvent;
const type = args.targetType;
if (type === 'dataCell' && target.inEditMode && evt.key.toLowerCase() === 'tab' ) {
}
if (type === 'dataCell' && evt.key.toLowerCase() === 'enter' ) {
}
}
typescript
IGridKeydownEventArgs 값을 기반으로 우리는 자체 논리를 제공하는 두 가지 사례를 식별했습니다(위 참조). 이제 API의 메서드를 사용하여 원하는 작업을 수행해 보겠습니다. 사용자가 편집 모드에서 셀 위에서 Tab 키를 누르면 입력에 대한 유효성 검사가 수행됩니다. 사용자가 셀 위에서 Enter 키를 누르면 초점이 다음 행의 셀로 이동됩니다.
if (target.column.dataType === 'number' && target.editValue < 0 ) {
return ;
}
const nexRowIndex = target.row.expanded ? target.rowIndex + 2 : target.rowIndex + 1 ;
grid.navigateTo(nexRowIndex, target.visibleColumnIndex,
(obj ) => { obj.target.nativeElement.focus(); });
typescript
참고: 전체 구현 세부정보는 샘플 코드를 참조하세요.
아래 데모를 사용하여 방금 구현한 사용자 지정 시나리오를 시험해 보세요.
더블클릭 또는 누르기 F2 셀에 있는 키 Grammy Nominations
열에서 값을 다음으로 변경합니다.-2
그리고 누르세요 탭 열쇠. 프롬프트 메시지가 표시됩니다.
셀을 선택하고 Enter 키를 두 번 누릅니다. 키를 누를 때마다 동일한 열 아래의 다음 행에 있는 셀로 초점이 이동합니다.
데모
import { Component, OnInit, ViewChild } from '@angular/core' ;
import { IGridCreatedEventArgs, IGridKeydownEventArgs, CellType, IgxHierarchicalGridComponent, IgxColumnComponent, IgxCellTemplateDirective, IgxRowIslandComponent, IgxGridToolbarDirective, IgxGridToolbarComponent, IgxGridToolbarTitleComponent } from 'igniteui-angular' ;
import { SINGERS } from '../../data/singersData' ;
import { IgxPreventDocumentScrollDirective } from '../../directives/prevent-scroll.directive' ;
@Component ({
selector : 'app-hierarchical-grid-custom-kb-navigation-sample' ,
styleUrls : ['./hierarchical-grid-custom-kb-navigation-sample.component.scss' ],
templateUrl : 'hierarchical-grid-custom-kb-navigation-sample.component.html' ,
imports : [IgxHierarchicalGridComponent, IgxPreventDocumentScrollDirective, IgxColumnComponent, IgxCellTemplateDirective, IgxRowIslandComponent, IgxGridToolbarDirective, IgxGridToolbarComponent, IgxGridToolbarTitleComponent]
})
export class HGridCustomKBNavigationComponent implements OnInit {
@ViewChild ('grid1' , { read : IgxHierarchicalGridComponent, static : true })
public grid1: IgxHierarchicalGridComponent;
public data: any [];
constructor ( ) {
}
public ngOnInit(): void {
this .data = SINGERS;
}
public formatter = (a ) => a;
public childGridCreated (event: IGridCreatedEventArgs ) {
const grid = event.grid;
event.grid.gridKeydown.subscribe((args ) => {
this .customKeydown(args, grid);
});
}
public customKeydown (args: IGridKeydownEventArgs, grid ) {
const target: CellType = args.target as CellType;
const evt: KeyboardEvent = args.event as KeyboardEvent;
const type = args.targetType;
if (type === 'dataCell' && target.editMode && evt.key.toLowerCase() === 'tab' ) {
args.event.preventDefault();
args.cancel = true ;
if (target.column.dataType === 'number' && target.editValue < 0 ) {
alert('The value should be less than 0' );
return ;
}
const cell = evt.shiftKey ?
grid.getPreviousCell(target.row.index, target.column.visibleIndex, (col ) => col.editable) :
grid.getNextCell(target.row.index, target.column.visibleIndex, (col ) => col.editable);
grid.navigateTo(cell.rowIndex, cell.visibleColumnIndex,
(obj ) => { obj.target.activate(); });
} else if (type === 'dataCell' && evt.key.toLowerCase() === 'enter' ) {
args.cancel = true ;
const nexRowIndex = target.row.expanded ? target.row.index + 2 : target.row.index + 1 ;
grid.navigateTo(nexRowIndex, target.column.visibleIndex,
(obj ) => { obj.target.activate(); });
}
}
}
ts コピー <div class ="grid__wrapper" >
<igx-hierarchical-grid [igxPreventDocumentScroll ]="true" #grid1 class ="hgrid" [data ]="data" (gridKeydown )="customKeydown($event, grid1)"
[height ]="'500px'" [width ]="'100%'" [rowHeight ]="'65px'" >
<igx-column field ="Artist" [editable ]="true" width ="20%" > </igx-column >
<igx-column field ="Photo" width ="20%" >
<ng-template igxCell let-cell ="cell" >
<div class ="cell__inner_2" >
<img [src ]="cell.value" class ="photo" />
</div >
</ng-template >
</igx-column >
<igx-column field ="Debut" [editable ]="true" width ="20%" dataType ="number" [formatter ]="formatter" > </igx-column >
<igx-column field ="GrammyNominations" header ="Grammy Nominations" [editable ]="true" dataType ="number" width ="20%" > </igx-column >
<igx-column field ="GrammyAwards" header ="Grammy Awards" [editable ]="true" dataType ="number" width ="20%" > </igx-column >
<igx-row-island [height ]="null" [key ]="'Albums'" [autoGenerate ]="false" (gridCreated )="childGridCreated($event)" >
<igx-grid-toolbar *igxGridToolbar ="let childGrid" >
<igx-grid-toolbar-title > Employees</igx-grid-toolbar-title >
</igx-grid-toolbar >
<igx-column field ="Album" > </igx-column >
<igx-column field ="LaunchDate" header ="Launch Date" [dataType ]="'date'" > </igx-column >
<igx-column field ="BillboardReview" header ="Billboard Review" [editable ]="true" > </igx-column >
<igx-column field ="USBillboard200" header ="US Billboard 200" [editable ]="true" > </igx-column >
</igx-row-island >
</igx-hierarchical-grid >
</div >
html コピー .grid__wrapper {
margin : 5px 16px ;
}
scss コピー
Known Limitations
한정
설명
스크롤 가능한 상위 컨테이너를 사용하여 그리드 내부를 탐색합니다.
그리드가 스크롤 가능한 상위 컨테이너 내부에 배치되고 사용자가 보이지 않는 그리드 셀로 이동하는 경우 상위 컨테이너는 스크롤되지 않습니다.
API References
Additional Resources
우리 커뮤니티는 활동적이며 항상 새로운 아이디어를 환영합니다.