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
173 changes: 158 additions & 15 deletions packages/common/src/core/__tests__/slickGrid.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5935,10 +5935,13 @@ describe('SlickGrid core file', () => {
// Use a real DOM structure to avoid infinite recursion
const testGridInstance = new TestGrid(container, items, columns, { ...defaultOptions, enableCellNavigation: true, autoEditByKeypress: true });
const focusGridCellSpy = vi.spyOn(testGridInstance, 'focusGridCell');
const headerRowLeftCols = createDomElement('div', { className: 'slick-headerrow-columns slick-headerrow-columns-left' });
const headerRowCol = createDomElement('div', { className: 'slick-headerrow-column' });
const inputFilterElm = createDomElement('input', { className: 'slick-filter-input', tabIndex: 0 });
headerRowCol.appendChild(inputFilterElm);
container.appendChild(headerRowCol);
headerRowLeftCols.appendChild(headerRowCol);
container.appendChild(headerRowLeftCols);
Object.defineProperty(inputFilterElm, 'offsetParent', { value: 5 });
const tabEvent = new KeyboardEvent('keydown', { key: 'Tab' }) as any;
Object.defineProperty(tabEvent, 'target', { value: inputFilterElm });
testGridInstance.callHandleContainerKeyDown(tabEvent);
Expand All @@ -5962,10 +5965,13 @@ describe('SlickGrid core file', () => {
// 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 headerRowLeftCols = createDomElement('div', { className: 'slick-headerrow-columns slick-headerrow-columns-left' });
const headerRowCol = createDomElement('div', { className: 'slick-headerrow-column' });
const inputFilterElm = createDomElement('input', { className: 'slick-filter-input', tabIndex: 0 });
headerRowCol.appendChild(inputFilterElm);
container.appendChild(headerRowCol);
headerRowLeftCols.appendChild(headerRowCol);
container.appendChild(headerRowLeftCols);
Object.defineProperty(inputFilterElm, 'offsetParent', { value: 5 });
const tabEvent = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true }) as any;
Object.defineProperty(tabEvent, 'target', { value: inputFilterElm });
testGridInstance.callHandleContainerKeyDown(tabEvent);
Expand Down Expand Up @@ -5993,7 +5999,8 @@ describe('SlickGrid core file', () => {
autoEditByKeypress: true,
frozenColumn: 0,
});
const focusGridMenuSpy = vi.spyOn(testGridInstance, 'focusGridMenu');
const headerRowLeftCols = createDomElement('div', { className: 'slick-headerrow-columns slick-headerrow-columns-left' });
const headerRowRightCols = createDomElement('div', { className: 'slick-headerrow-columns slick-headerrow-columns-right' });
const headerRowCol1 = createDomElement('div', { className: 'slick-headerrow-column' });
const headerRowCol2 = createDomElement('div', { className: 'slick-headerrow-column' });
const headerRowCol3 = createDomElement('div', { className: 'slick-headerrow-column' });
Expand All @@ -6007,10 +6014,16 @@ describe('SlickGrid core file', () => {
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);
headerRowLeftCols.appendChild(headerRowCol1);
headerRowLeftCols.appendChild(headerRowCol2);
headerRowRightCols.appendChild(headerRowCol3);
headerRowRightCols.appendChild(headerRowCol4);
container.querySelector('.slick-pane-left')!.appendChild(headerRowLeftCols);
container.querySelector('.slick-pane-right')!.appendChild(headerRowRightCols);
Object.defineProperty(inputFilter1, 'offsetParent', { value: 5 });
Object.defineProperty(inputFilter2, 'offsetParent', { value: 5 });
Object.defineProperty(inputFilter3, 'offsetParent', { value: 5 });
Object.defineProperty(inputFilter4, 'offsetParent', { value: 5 });

const tabEvent = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: false }) as any;
Object.defineProperty(tabEvent, 'target', { value: inputFilter2 });
Expand Down Expand Up @@ -6039,7 +6052,8 @@ describe('SlickGrid core file', () => {
autoEditByKeypress: true,
frozenColumn: 0,
});
const focusGridMenuSpy = vi.spyOn(testGridInstance, 'focusGridMenu');
const headerRowLeftCols = createDomElement('div', { className: 'slick-headerrow-columns slick-headerrow-columns-left' });
const headerRowRightCols = createDomElement('div', { className: 'slick-headerrow-columns slick-headerrow-columns-right' });
const headerRowCol1 = createDomElement('div', { className: 'slick-headerrow-column' });
const headerRowCol2 = createDomElement('div', { className: 'slick-headerrow-column' });
const headerRowCol3 = createDomElement('div', { className: 'slick-headerrow-column' });
Expand All @@ -6048,15 +6062,21 @@ describe('SlickGrid core file', () => {
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');
const filter2Spy = 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);
headerRowLeftCols.appendChild(headerRowCol1);
headerRowLeftCols.appendChild(headerRowCol2);
headerRowRightCols.appendChild(headerRowCol3);
headerRowRightCols.appendChild(headerRowCol4);
container.querySelector('.slick-pane-left')!.appendChild(headerRowLeftCols);
container.querySelector('.slick-pane-right')!.appendChild(headerRowRightCols);
Object.defineProperty(inputFilter1, 'offsetParent', { value: 5 });
Object.defineProperty(inputFilter2, 'offsetParent', { value: 5 });
Object.defineProperty(inputFilter3, 'offsetParent', { value: 5 });
Object.defineProperty(inputFilter4, 'offsetParent', { value: 5 });

const tabEvent = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true }) as any;
Object.defineProperty(tabEvent, 'target', { value: inputFilter3 });
Expand All @@ -6074,7 +6094,113 @@ describe('SlickGrid core file', () => {
testGridInstance.callHandleGridKeyDown(keyEvent);
skipGridDestroy = true;

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

it('should focus on right pane first column header 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 headerLeftCols = createDomElement('div', { className: 'slick-header-columns slick-header-columns-left' });
const headerRightCols = createDomElement('div', { className: 'slick-header-columns slick-header-columns-right' });
const headerCol1 = createDomElement('div', { className: 'slick-header-column', tabIndex: 0 });
const headerCol2 = createDomElement('div', { className: 'slick-header-column', tabIndex: 0 });
const headerCol3 = createDomElement('div', { className: 'slick-header-column', tabIndex: 0 });
const headerCol4 = createDomElement('div', { className: 'slick-header-column', tabIndex: 0 });
const spanTitle1 = createDomElement('span', { className: 'slick-column-name', textContent: 'Title 1' });
const spanTitle2 = createDomElement('span', { className: 'slick-column-name', textContent: 'Title 2' });
const spanTitle3 = createDomElement('span', { className: 'slick-column-name', textContent: 'Title 3' });
const spanTitle4 = createDomElement('span', { className: 'slick-column-name', textContent: 'Title 4' });
const title3Spy = vi.spyOn(headerCol3, 'focus');
headerCol1.appendChild(spanTitle1);
headerCol2.appendChild(spanTitle2);
headerCol3.appendChild(spanTitle3);
headerCol4.appendChild(spanTitle4);
headerLeftCols.appendChild(headerCol1);
headerLeftCols.appendChild(headerCol2);
headerRightCols.appendChild(headerCol3);
headerRightCols.appendChild(headerCol4);
container.querySelector('.slick-pane-left')!.appendChild(headerLeftCols);
container.querySelector('.slick-pane-right')!.appendChild(headerRightCols);
Object.defineProperty(headerCol1, 'offsetParent', { value: 5 });
Object.defineProperty(headerCol2, 'offsetParent', { value: 5 });
Object.defineProperty(headerCol3, 'offsetParent', { value: 5 });
Object.defineProperty(headerCol4, 'offsetParent', { value: 5 });

const tabEvent = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: false }) as any;
Object.defineProperty(tabEvent, 'target', { value: headerCol2 });
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(title3Spy).toHaveBeenCalled();
});

it('should focus on right pane first column header 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 headerLeftCols = createDomElement('div', { className: 'slick-header-columns slick-header-columns-left' });
const headerRightCols = createDomElement('div', { className: 'slick-header-columns slick-header-columns-right' });
const headerCol1 = createDomElement('div', { className: 'slick-header-column', tabIndex: 0 });
const headerCol2 = createDomElement('div', { className: 'slick-header-column', tabIndex: 0 });
const headerCol3 = createDomElement('div', { className: 'slick-header-column', tabIndex: 0 });
const headerCol4 = createDomElement('div', { className: 'slick-header-column', tabIndex: 0 });
const spanTitle1 = createDomElement('span', { className: 'slick-column-name', textContent: 'Title 1' });
const spanTitle2 = createDomElement('span', { className: 'slick-column-name', textContent: 'Title 2' });
const spanTitle3 = createDomElement('span', { className: 'slick-column-name', textContent: 'Title 3' });
const spanTitle4 = createDomElement('span', { className: 'slick-column-name', textContent: 'Title 4' });
const title2Spy = vi.spyOn(headerCol2, 'focus');
headerCol1.appendChild(spanTitle1);
headerCol2.appendChild(spanTitle2);
headerCol3.appendChild(spanTitle3);
headerCol4.appendChild(spanTitle4);
headerLeftCols.appendChild(headerCol1);
headerLeftCols.appendChild(headerCol2);
headerRightCols.appendChild(headerCol3);
headerRightCols.appendChild(headerCol4);
container.querySelector('.slick-pane-left')!.appendChild(headerLeftCols);
container.querySelector('.slick-pane-right')!.appendChild(headerRightCols);
Object.defineProperty(headerCol1, 'offsetParent', { value: 5 });
Object.defineProperty(headerCol2, 'offsetParent', { value: 5 });
Object.defineProperty(headerCol3, 'offsetParent', { value: 5 });
Object.defineProperty(headerCol4, 'offsetParent', { value: 5 });

const tabEvent = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true }) as any;
Object.defineProperty(tabEvent, 'target', { value: headerCol3 });
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(title2Spy).toHaveBeenCalled();
});

it('should trigger onHeaderKeyDown and sortCallback on Enter/Space', () => {
Expand Down Expand Up @@ -7258,7 +7384,7 @@ describe('SlickGrid core file', () => {
});
});

describe('Header Click', () => {
describe('Header Events', () => {
it('should trigger onHeaderClick notify when not column resizing', () => {
const columns = [
{ id: 'name', field: 'name', name: 'Name' },
Expand All @@ -7274,6 +7400,23 @@ describe('SlickGrid core file', () => {

expect(onHeaderClickSpy).toHaveBeenCalledWith({ column: columns[0], grid }, expect.anything(), grid);
});

it('should call scrollToX() when header right is scrolled', () => {
const columns = [
{ id: 'name', field: 'name', name: 'Name' },
{ id: 'age', field: 'age', name: 'Age', editorClass: InputEditor },
] as Column[];
grid = new SlickGrid<any, Column>(container, items, columns, { ...defaultOptions, enableCellNavigation: true, editable: true });
vi.spyOn(grid, 'getCellFromEvent').mockReturnValue(null);
const scrollToXSpy = vi.spyOn(grid, 'scrollToX');
const headerColumns = container.querySelectorAll('.slick-header-column');
const event = new CustomEvent('scroll');
Object.defineProperty(headerColumns[0], 'scrollLeft', { writable: true, value: 100 });
Object.defineProperty(event, 'target', { writable: true, value: headerColumns[0] });
container.querySelector('.slick-header.slick-header-right')!.dispatchEvent(event);

expect(scrollToXSpy).toHaveBeenCalledWith(100);
});
});

describe('Header Context Menu', () => {
Expand Down
36 changes: 23 additions & 13 deletions packages/common/src/core/slickGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,12 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
this._headerScrollerL = createDomElement('div', { className: 'slick-header slick-state-default slick-header-left' }, headerContainerL);
this._headerScrollerR = createDomElement('div', { className: 'slick-header slick-state-default slick-header-right' }, headerContainerR);

// header scroll position could change when using frozen grid and tabbing on next available header
// so we need to make sure that all containers (header, headerrow, toppanel) are all in sync when that happens
this._bindingEventService.bind(this._headerScrollerR, 'scroll', (e) => {
this.scrollToX((e.target as HTMLElement).scrollLeft);
});

// Cache the header scroller containers
this._headerScroller.push(this._headerScrollerL);
this._headerScroller.push(this._headerScrollerR);
Expand Down Expand Up @@ -1057,9 +1063,7 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
this._bindingEventService.bind(this._canvas, 'contextmenu', this.handleContextMenu.bind(this) as EventListener);
this._bindingEventService.bind(this._canvas, 'mouseover', this.handleCellMouseOver.bind(this) as EventListener);
this._bindingEventService.bind(this._canvas, 'mouseout', this.handleCellMouseOut.bind(this) as EventListener);
if (this._options.enableFiltering) {
this._bindingEventService.bind(this._container, 'keydown', this.handleContainerKeyDown.bind(this) as EventListener);
}
this._bindingEventService.bind(this._container, 'keydown', this.handleContainerKeyDown.bind(this) as EventListener);

if (Draggable) {
this.slickDraggableInstance = Draggable({
Expand Down Expand Up @@ -5749,8 +5753,8 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
* @param index - Column index to focus (defaults to 0)
*/
focusHeaderMenuOrColumn(index = 0): void {
const headerMenuElm = this.getHeaderColumn(index).querySelector<HTMLElement>('.slick-header-menu-button[tabIndex="0"]');
if (headerMenuElm && headerMenuElm.offsetParent !== null) {
const [headerMenuElm] = this.getVisibleElements(this.getHeaderColumn(index), '.slick-header-menu-button[tabIndex="0"]');
if (headerMenuElm) {
headerMenuElm.focus();
} else {
this.focusHeaderColumn(index);
Expand Down Expand Up @@ -5778,7 +5782,7 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
const step = focusOnLast ? -1 : 1;
for (let i = start; i !== end; i += step) {
const elm = allFilterElms[i];
if (elm.offsetParent !== null) {
if (elm && elm.offsetParent !== null) {
closestVisibleFilter = elm;
break;
}
Expand Down Expand Up @@ -5871,18 +5875,24 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
this.triggerEvent(this.onDragEnd, dd, e);
}

/** get only visible elemnts from a container and a query selector, e.g. elements with `display: none` will be excluded. */
protected getVisibleElements(container: HTMLElement, selector: string): HTMLElement[] {
return Array.from(container.querySelectorAll<HTMLElement>(selector)).filter((el) => el.offsetParent !== null);
}

protected handleContainerKeyDown(e: KeyboardEvent & { originalEvent: Event }): void {
if (e.key === 'Tab' && !e.ctrlKey && !e.altKey) {
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 (e.target instanceof HTMLElement && e.key === 'Tab' && !e.ctrlKey && !e.altKey) {
const isInHeaderRow = e.target.closest('.slick-headerrow-columns');
const headerSelector = `.slick-${isInHeaderRow ? 'headerrow-column' : 'header-columns'} *[tabIndex="0"]`;
const allFilterElms = this.getVisibleElements(this._container, headerSelector);
const allLeftFilterElms = this.getVisibleElements(this._container, `.slick-pane-left ${headerSelector}`);
const allRightFilterElms = this.getVisibleElements(this._container, `.slick-pane-right ${headerSelector}`);
const ancestorHeaderRow = e.target instanceof HTMLElement ? e.target.closest(headerSelector) : null;

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

if (targetFilterElm === ancestorHeaderRow) {
if (targetFilterElm === ancestorHeaderRow && isInHeaderRow) {
// focus grid menu when Shift+Tab OR focus on first cell when using Tab
this.stopFullBubbling(e);
e.shiftKey ? this.focusGridMenu() : this.focusGridCell();
Expand Down
Loading