Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
139 changes: 130 additions & 9 deletions packages/common/src/core/__tests__/slickGrid.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2886,9 +2886,9 @@ describe('SlickGrid core file', () => {
grid = new SlickGrid<any, Column>(container, items, columns, defaultOptions);

expect(grid).toBeTruthy();
expect(grid._container.getAttribute('role')).toBe('grid');
expect(grid._container.getAttribute('aria-colcount')).toBe('1');
expect(grid._container.getAttribute('aria-rowcount')).toBe('11');
expect(grid.getContainerNode().getAttribute('role')).toBe('grid');
expect(grid.getContainerNode().getAttribute('aria-colcount')).toBe('1');
expect(grid.getContainerNode().getAttribute('aria-rowcount')).toBe('11');
});

it('should return undefined editor when getDataItem() did not find any associated cell item', () => {
Expand Down Expand Up @@ -5931,15 +5931,16 @@ describe('SlickGrid core file', () => {
expect(secondItemAgeCell.classList.contains('to-be-deleted-highlight')).toBeFalsy();
});

it('should call handleContainerKeyDown and handleGridKeyDown a11y/keyboard coverage', () => {
it('should trigger focusGridCell() when user typed Tab and filter element equals the ancestor header row an', () => {
// Use a real DOM structure to avoid infinite recursion
const testGridInstance = new TestGrid(container, items, columns, { ...defaultOptions, enableCellNavigation: true, autoEditByKeypress: true });
const headerRowCol = document.createElement('div');
headerRowCol.className = 'slick-headerrow-column';
headerRowCol.tabIndex = 0;
const focusGridCellSpy = vi.spyOn(testGridInstance, 'focusGridCell');
const headerRowCol = createDomElement('div', { className: 'slick-headerrow-column' });
const inputFilterElm = createDomElement('input', { className: 'slick-filter-input', tabIndex: 0 });
headerRowCol.appendChild(inputFilterElm);
container.appendChild(headerRowCol);
const tabEvent = new KeyboardEvent('keydown', { key: 'Tab' }) as any;
Object.defineProperty(tabEvent, 'target', { value: headerRowCol });
Object.defineProperty(tabEvent, 'target', { value: inputFilterElm });
testGridInstance.callHandleContainerKeyDown(tabEvent);

const keyEvent = {
Expand All @@ -5951,9 +5952,129 @@ describe('SlickGrid core file', () => {
preventDefault: vi.fn(),
} as any;
testGridInstance.setActiveCell(0, 1);
testGridInstance.setCurrentEditorNull();
testGridInstance.callHandleGridKeyDown(keyEvent);
skipGridDestroy = true;

expect(focusGridCellSpy).toHaveBeenCalled();
});

it('should trigger focusGridMenu() when user typed Shift+Tab and filter element equals the ancestor header row an', () => {
// Use a real DOM structure to avoid infinite recursion
const testGridInstance = new TestGrid(container, items, columns, { ...defaultOptions, enableCellNavigation: true, autoEditByKeypress: true });
const focusGridMenuSpy = vi.spyOn(testGridInstance, 'focusGridMenu');
const headerRowCol = createDomElement('div', { className: 'slick-headerrow-column' });
const inputFilterElm = createDomElement('input', { className: 'slick-filter-input', tabIndex: 0 });
headerRowCol.appendChild(inputFilterElm);
container.appendChild(headerRowCol);
const tabEvent = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true }) as any;
Object.defineProperty(tabEvent, 'target', { value: inputFilterElm });
testGridInstance.callHandleContainerKeyDown(tabEvent);

const keyEvent = {
key: 'a',
ctrlKey: false,
originalEvent: {},
target: container.querySelector('.slick-cell.l1.r1'),
stopPropagation: vi.fn(),
preventDefault: vi.fn(),
} as any;
testGridInstance.setActiveCell(0, 1);
testGridInstance.callHandleGridKeyDown(keyEvent);
skipGridDestroy = true;

expect(focusGridMenuSpy).toHaveBeenCalled();
});

it('should focus on right pane first filter element when user typed Tab and frozenColumn is set to 0', () => {
// Use a real DOM structure to avoid infinite recursion
const testGridInstance = new TestGrid(container, items, columns, {
...defaultOptions,
enableCellNavigation: true,
autoEditByKeypress: true,
frozenColumn: 0,
});
const focusGridMenuSpy = vi.spyOn(testGridInstance, 'focusGridMenu');
const headerRowCol1 = createDomElement('div', { className: 'slick-headerrow-column' });
const headerRowCol2 = createDomElement('div', { className: 'slick-headerrow-column' });
const headerRowCol3 = createDomElement('div', { className: 'slick-headerrow-column' });
const headerRowCol4 = createDomElement('div', { className: 'slick-headerrow-column' });
const inputFilter1 = createDomElement('input', { className: 'slick-filter-input filter1', tabIndex: 0 });
const inputFilter2 = createDomElement('input', { className: 'slick-filter-input filter2', tabIndex: 0 });
const inputFilter3 = createDomElement('input', { className: 'slick-filter-input filter3', tabIndex: 0 });
const inputFilter4 = createDomElement('input', { className: 'slick-filter-input filter4', tabIndex: 0 });
const filter3Spy = vi.spyOn(inputFilter3, 'focus');
headerRowCol1.appendChild(inputFilter1);
headerRowCol2.appendChild(inputFilter2);
headerRowCol3.appendChild(inputFilter3);
headerRowCol4.appendChild(inputFilter4);
container.querySelector('.slick-pane-left')!.appendChild(headerRowCol1);
container.querySelector('.slick-pane-left')!.appendChild(headerRowCol2);
container.querySelector('.slick-pane-right')!.appendChild(headerRowCol3);
container.querySelector('.slick-pane-right')!.appendChild(headerRowCol4);

const tabEvent = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: false }) as any;
Object.defineProperty(tabEvent, 'target', { value: inputFilter2 });
testGridInstance.callHandleContainerKeyDown(tabEvent);

const keyEvent = {
key: 'a',
ctrlKey: false,
originalEvent: {},
target: container.querySelector('.slick-cell.l1.r1'),
stopPropagation: vi.fn(),
preventDefault: vi.fn(),
} as any;
testGridInstance.setActiveCell(0, 1);
testGridInstance.callHandleGridKeyDown(keyEvent);
skipGridDestroy = true;

expect(filter3Spy).toHaveBeenCalled();
});

it('should focus on right pane first filter element when user typed Shift+Tab and frozenColumn is set to 0', () => {
// Use a real DOM structure to avoid infinite recursion
const testGridInstance = new TestGrid(container, items, columns, {
...defaultOptions,
enableCellNavigation: true,
autoEditByKeypress: true,
frozenColumn: 0,
});
const focusGridMenuSpy = vi.spyOn(testGridInstance, 'focusGridMenu');
const headerRowCol1 = createDomElement('div', { className: 'slick-headerrow-column' });
const headerRowCol2 = createDomElement('div', { className: 'slick-headerrow-column' });
const headerRowCol3 = createDomElement('div', { className: 'slick-headerrow-column' });
const headerRowCol4 = createDomElement('div', { className: 'slick-headerrow-column' });
const inputFilter1 = createDomElement('input', { className: 'slick-filter-input filter1', tabIndex: 0 });
const inputFilter2 = createDomElement('input', { className: 'slick-filter-input filter2', tabIndex: 0 });
const inputFilter3 = createDomElement('input', { className: 'slick-filter-input filter3', tabIndex: 0 });
const inputFilter4 = createDomElement('input', { className: 'slick-filter-input filter4', tabIndex: 0 });
const filter3Spy = vi.spyOn(inputFilter2, 'focus');
headerRowCol1.appendChild(inputFilter1);
headerRowCol2.appendChild(inputFilter2);
headerRowCol3.appendChild(inputFilter3);
headerRowCol4.appendChild(inputFilter4);
container.querySelector('.slick-pane-left')!.appendChild(headerRowCol1);
container.querySelector('.slick-pane-left')!.appendChild(headerRowCol2);
container.querySelector('.slick-pane-right')!.appendChild(headerRowCol3);
container.querySelector('.slick-pane-right')!.appendChild(headerRowCol4);

const tabEvent = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true }) as any;
Object.defineProperty(tabEvent, 'target', { value: inputFilter3 });
testGridInstance.callHandleContainerKeyDown(tabEvent);

const keyEvent = {
key: 'a',
ctrlKey: false,
originalEvent: {},
target: container.querySelector('.slick-cell.l1.r1'),
stopPropagation: vi.fn(),
preventDefault: vi.fn(),
} as any;
testGridInstance.setActiveCell(0, 1);
testGridInstance.callHandleGridKeyDown(keyEvent);
skipGridDestroy = true;

expect(filter3Spy).toHaveBeenCalled();
});

it('should trigger onHeaderKeyDown and sortCallback on Enter/Space', () => {
Expand Down
47 changes: 35 additions & 12 deletions packages/common/src/core/slickGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5766,8 +5766,10 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
focusHeaderRowFilter(focusOnLast = false): boolean {
const headerRow = this.getHeaderRow();
if (this._options.showHeaderRow && headerRow) {
const firstHeaderRow = headerRow instanceof HTMLElement ? headerRow : headerRow[0];
const allFilterElms = firstHeaderRow?.querySelectorAll<HTMLElement>('.slick-headerrow-column [tabIndex="0"]') || [];
const headerRows = headerRow instanceof HTMLElement ? [headerRow] : [...headerRow];
const allFilterElms = headerRows.flatMap((row) =>
Array.from(row.querySelectorAll<HTMLElement>('.slick-headerrow-column *[tabIndex="0"]'))
);
const filterLn = allFilterElms.length;
let closestVisibleFilter: HTMLElement | null = null;
if (filterLn > 0) {
Expand Down Expand Up @@ -5871,18 +5873,39 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e

protected handleContainerKeyDown(e: KeyboardEvent & { originalEvent: Event }): void {
if (e.key === 'Tab' && !e.ctrlKey && !e.altKey) {
let headerRowSelector = '.slick-headerrow-column *[tabIndex="0"]';
const ancestorHeaderRow = (e.target as HTMLElement)?.closest(headerRowSelector);
if (!e.shiftKey) {
// when using Tab, find last header row filter
headerRowSelector = '.slick-headerrow-column:last-child *[tabIndex="0"]';
const headerRowSelector = '.slick-headerrow-column *[tabIndex="0"]';
const allFilterElms = this._container.querySelectorAll(headerRowSelector);
const allLeftFilterElms = this._container.querySelectorAll(`.slick-pane-left ${headerRowSelector}`);
const allRightFilterElms = this._container.querySelectorAll(`.slick-pane-right ${headerRowSelector}`);
const ancestorHeaderRow = e.target instanceof HTMLElement ? e.target.closest(headerRowSelector) : null;

if (allFilterElms.length > 0) {
const targetFilterElm = e.shiftKey ? allFilterElms[0] : allFilterElms[allFilterElms.length - 1];

if (targetFilterElm === ancestorHeaderRow) {
// focus grid menu when Shift+Tab OR focus on first cell when using Tab
this.stopFullBubbling(e);
e.shiftKey ? this.focusGridMenu() : this.focusGridCell();
} else if (this._options.frozenColumn! >= 0) {
// when using frozen columns
const lastLeftFilterElm = allLeftFilterElms[allLeftFilterElms.length - 1];
if (e.shiftKey && e.target === allRightFilterElms[0]) {
// using Shift+Tab and we're on the first filter element of the right pane, let's focus on last element of the left pane
this.focusElementWithoutBubbling(e, lastLeftFilterElm);
} else if (!e.shiftKey && e.target === lastLeftFilterElm) {
// using frozen columns and we're on the last filter element on the left pane, let's focus on the first element of the right pane
this.focusElementWithoutBubbling(e, allRightFilterElms[0]);
}
}
}
}
}

if (this._container.querySelector(headerRowSelector) === ancestorHeaderRow) {
this.stopFullBubbling(e);
// focus grid menu when Shift+Tab or focus on first cell when using Tab
e.shiftKey ? this.focusGridMenu() : this.focusGridCell();
}
// focus element and stop event bubbling (for keyboard events)
protected focusElementWithoutBubbling(e: KeyboardEvent, target: Element | null): void {
if (target) {
(target as HTMLElement).focus();
this.stopFullBubbling(e);
}
}

Expand Down
Loading