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

fix: cell selection range with key combos were incorrect #1244

Merged
merged 2 commits into from
Dec 2, 2023
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"rimraf": "^5.0.5",
"rxjs": "^7.8.1",
"servor": "^4.0.2",
"slickgrid": "^4.1.5",
"slickgrid": "^4.1.6",
"sortablejs": "^1.15.1",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"flatpickr": "^4.6.13",
"moment-mini": "^2.29.4",
"multiple-select-vanilla": "^0.6.3",
"slickgrid": "^4.1.5",
"slickgrid": "^4.1.6",
"sortablejs": "^1.15.1",
"un-flatten-tree": "^2.0.12"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ const NB_ITEMS = 200;
const CALCULATED_PAGE_ROW_COUNT = 23; // pageRowCount with our mocked sizes is 23 => ((600 - 17) / 25)
jest.mock('flatpickr', () => { });

const addVanillaEventPropagation = function (event, commandKey = '', keyName = '') {
const addVanillaEventPropagation = function (event, commandKeys: string[] = [], keyName = '') {
Object.defineProperty(event, 'isPropagationStopped', { writable: true, configurable: true, value: jest.fn() });
Object.defineProperty(event, 'isImmediatePropagationStopped', { writable: true, configurable: true, value: jest.fn() });
if (commandKey) {
Object.defineProperty(event, commandKey, { writable: true, configurable: true, value: true });
if (commandKeys.length) {
for (const commandKey of commandKeys) {
Object.defineProperty(event, commandKey, { writable: true, configurable: true, value: true });
}
}
if (keyName) {
Object.defineProperty(event, 'key', { writable: true, configurable: true, value: keyName });
Expand All @@ -38,6 +40,12 @@ const dataViewStub = {
getPagingInfo: () => ({ pageSize: 0 }),
};

const mockColumns = [
{ id: 'firstName', field: 'firstName' },
{ id: 'lastName', field: 'lastName' },
{ id: 'age', field: 'age' },
]

const gridStub = {
canCellBeSelected: jest.fn(),
getActiveCell: jest.fn(),
Expand All @@ -46,12 +54,13 @@ const gridStub = {
getCellFromEvent: jest.fn(),
getCellFromPoint: jest.fn(),
getCellNodeBox: jest.fn(),
getColumns: () => mockColumns,
getData: () => dataViewStub,
getDataLength: jest.fn(),
getEditorLock: () => getEditorLockMock,
getOptions: () => mockGridOptions,
getUID: () => GRID_UID,
getScrollbarDimensions: () => ({ height: 17, width: 17}),
getScrollbarDimensions: () => ({ height: 17, width: 17 }),
getViewportNode: jest.fn(),
focus: jest.fn(),
registerPlugin: jest.fn(),
Expand Down Expand Up @@ -229,7 +238,7 @@ describe('CellSelectionModel Plugin', () => {
plugin.setSelectedRanges(mockRanges);

const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), 'shiftKey', 'ArrowLeft');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), ['shiftKey'], 'ArrowLeft');
gridStub.onKeyDown.notify({ cell: 2, row: 3, grid: gridStub }, keyDownEvent, gridStub);

expect(setSelectRangeSpy).toHaveBeenCalledWith([{
Expand All @@ -250,7 +259,7 @@ describe('CellSelectionModel Plugin', () => {
{ fromCell: 2, fromRow: 3, toCell: 3, toRow: 4 }
] as unknown as SlickRange[]);
const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), 'shiftKey', 'ArrowRight');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), ['shiftKey'], 'ArrowRight');
gridStub.onKeyDown.notify({ cell: 2, row: 3, grid: gridStub }, keyDownEvent, gridStub);

expect(setSelectRangeSpy).toHaveBeenCalledWith([{
Expand All @@ -268,7 +277,7 @@ describe('CellSelectionModel Plugin', () => {
{ fromCell: 2, fromRow: 3, toCell: 3, toRow: 4 }
] as unknown as SlickRange[]);
const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), 'shiftKey', 'ArrowUp');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), ['shiftKey'], 'ArrowUp');
gridStub.onKeyDown.notify({ cell: 2, row: 3, grid: gridStub }, keyDownEvent, gridStub);

expect(setSelectRangeSpy).toHaveBeenCalledWith([{
Expand All @@ -286,7 +295,7 @@ describe('CellSelectionModel Plugin', () => {
{ fromCell: 2, fromRow: 3, toCell: 3, toRow: 4, contains: () => false }
] as unknown as SlickRange[]);
const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), 'shiftKey', 'ArrowDown');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), ['shiftKey'], 'ArrowDown');
gridStub.onKeyDown.notify({ cell: 2, row: 3, grid: gridStub }, keyDownEvent, gridStub);

expect(setSelectRangeSpy).toHaveBeenCalledWith([{
Expand All @@ -308,7 +317,7 @@ describe('CellSelectionModel Plugin', () => {
{ fromCell: 2, fromRow: 3, toCell: 3, toRow: 4, contains: () => false }
] as unknown as SlickRange[]);
const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), 'shiftKey', 'ArrowDown');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), ['shiftKey'], 'ArrowDown');
gridStub.onKeyDown.notify({ cell: 2, row: 3, grid: gridStub }, keyDownEvent, gridStub);

const expectedRangeCalled = [
Expand Down Expand Up @@ -337,7 +346,7 @@ describe('CellSelectionModel Plugin', () => {
{ fromCell: 2, fromRow: notifyingRowNumber, toCell: 3, toRow: 4, contains: () => false }
] as unknown as SlickRange[]);
const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), 'shiftKey', 'PageDown');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), ['shiftKey'], 'PageDown');
gridStub.onKeyDown.notify({ cell: 2, row: 3, grid: gridStub }, keyDownEvent, gridStub);

const expectedRangeCalled = [
Expand All @@ -364,7 +373,7 @@ describe('CellSelectionModel Plugin', () => {
{ fromCell: 2, fromRow: notifyingRowNumber, toCell: 3, toRow: 4, contains: () => false }
] as unknown as SlickRange[]);
const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), 'shiftKey', 'PageDown');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), ['shiftKey'], 'PageDown');
gridStub.onKeyDown.notify({ cell: 2, row: 3, grid: gridStub }, keyDownEvent, gridStub);

const expectedRangeCalled = [
Expand All @@ -391,7 +400,7 @@ describe('CellSelectionModel Plugin', () => {
{ fromCell: 2, fromRow: notifyingRowNumber, toCell: 3, toRow: 120, contains: () => false }
] as unknown as SlickRange[]);
const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), 'shiftKey', 'PageUp');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), ['shiftKey'], 'PageUp');
gridStub.onKeyDown.notify({ cell: 2, row: 101, grid: gridStub }, keyDownEvent, gridStub);

const expectedRangeCalled = [
Expand All @@ -418,7 +427,7 @@ describe('CellSelectionModel Plugin', () => {
{ fromCell: 2, fromRow: notifyingRowNumber, toCell: 3, toRow: 4, contains: () => false }
] as unknown as SlickRange[]);
const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), 'shiftKey', 'PageUp');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), ['shiftKey'], 'PageUp');
gridStub.onKeyDown.notify({ cell: 2, row: 3, grid: gridStub }, keyDownEvent, gridStub);

const expectedRangeCalled = [
Expand All @@ -432,7 +441,61 @@ describe('CellSelectionModel Plugin', () => {
expect(scrollCellSpy).toHaveBeenCalledWith(firstRowIndex, 2, false);
});

it('should call "setSelectedRanges" with Slick Range from current position to row index 0 when using Shift+Home key combo when triggered by "onKeyDown"', () => {
it('should call "setSelectedRanges" with Slick Range from current position to row index 0 horizontally when using Shift+Home key combo when triggered by "onKeyDown"', () => {
const notifyingRowNumber = 100;
const expectedRowZeroIdx = 0;
jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: notifyingRowNumber });
jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true);
const scrollCellSpy = jest.spyOn(gridStub, 'scrollCellIntoView');

plugin.init(gridStub);
plugin.setSelectedRanges([
{ fromCell: 1, fromRow: 99, toCell: 3, toRow: 120, contains: () => false },
{ fromCell: 2, fromRow: notifyingRowNumber, toCell: 3, toRow: 120, contains: () => false }
] as unknown as SlickRange[]);
const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), ['shiftKey'], 'Home');
gridStub.onKeyDown.notify({ cell: 2, row: 101, grid: gridStub }, keyDownEvent, gridStub);

const expectedRangeCalled = [
{ fromCell: 1, fromRow: 99, toCell: 3, toRow: 120, contains: expect.toBeFunction(), } as unknown as SlickRange,
{
fromCell: expectedRowZeroIdx, fromRow: notifyingRowNumber, toCell: 2, toRow: 100,
contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(),
},
];
expect(setSelectRangeSpy).toHaveBeenCalledWith(expectedRangeCalled);
expect(scrollCellSpy).toHaveBeenCalledWith(100, 2, false);
});

it('should call "setSelectedRanges" with Slick Range from current position to same row last cell index horizontally when using Shift+End key combo when triggered by "onKeyDown"', () => {
const notifyingRowNumber = 100;
const columnsLn = mockColumns.length;
jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: notifyingRowNumber });
jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true);
const scrollCellSpy = jest.spyOn(gridStub, 'scrollCellIntoView');

plugin.init(gridStub);
plugin.setSelectedRanges([
{ fromCell: 1, fromRow: 99, toCell: 3, toRow: 120, contains: () => false },
{ fromCell: 2, fromRow: notifyingRowNumber, toCell: 3, toRow: 120, contains: () => false }
] as unknown as SlickRange[]);
const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), ['shiftKey'], 'End');
gridStub.onKeyDown.notify({ cell: 1, row: 101, grid: gridStub }, keyDownEvent, gridStub);

const expectedRangeCalled = [
{ fromCell: 1, fromRow: 99, toCell: 3, toRow: 120, contains: expect.toBeFunction(), } as unknown as SlickRange,
{
fromCell: columnsLn - 1, fromRow: notifyingRowNumber, toCell: 2, toRow: 100,
contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(),
},
];
expect(setSelectRangeSpy).toHaveBeenCalledWith(expectedRangeCalled);
expect(scrollCellSpy).toHaveBeenCalledWith(100, 2, false);
});

it('should call "setSelectedRanges" with Slick Range from current position to cell,row index 0 when using Ctrl+Shift+Home key combo when triggered by "onKeyDown"', () => {
const notifyingRowNumber = 100;
const expectedRowZeroIdx = 0;
jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: notifyingRowNumber });
Expand All @@ -445,21 +508,21 @@ describe('CellSelectionModel Plugin', () => {
{ fromCell: 2, fromRow: notifyingRowNumber, toCell: 3, toRow: 120, contains: () => false }
] as unknown as SlickRange[]);
const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), 'shiftKey', 'Home');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), ['ctrlKey', 'shiftKey'], 'Home');
gridStub.onKeyDown.notify({ cell: 2, row: 101, grid: gridStub }, keyDownEvent, gridStub);

const expectedRangeCalled = [
{ fromCell: 1, fromRow: 99, toCell: 3, toRow: 120, contains: expect.toBeFunction(), } as unknown as SlickRange,
{
fromCell: 2, fromRow: expectedRowZeroIdx, toCell: 2, toRow: 100,
fromCell: 0, fromRow: expectedRowZeroIdx, toCell: 2, toRow: 100,
contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(),
},
];
expect(setSelectRangeSpy).toHaveBeenCalledWith(expectedRangeCalled);
expect(scrollCellSpy).toHaveBeenCalledWith(expectedRowZeroIdx, 2, false);
});

it('should call "setSelectedRanges" with Slick Range from current position to last row index when using Shift+End key combo when triggered by "onKeyDown"', () => {
it('should call "setSelectedRanges" with Slick Range from current position to last row index when using Ctrl+Shift+End key combo when triggered by "onKeyDown"', () => {
const notifyingRowNumber = 100;
const expectedLastRowIdx = NB_ITEMS - 1;
jest.spyOn(gridStub, 'getActiveCell').mockReturnValue({ cell: 2, row: notifyingRowNumber });
Expand All @@ -472,7 +535,7 @@ describe('CellSelectionModel Plugin', () => {
{ fromCell: 2, fromRow: notifyingRowNumber, toCell: 3, toRow: 120, contains: () => false }
] as unknown as SlickRange[]);
const setSelectRangeSpy = jest.spyOn(plugin, 'setSelectedRanges');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), 'shiftKey', 'End');
const keyDownEvent = addVanillaEventPropagation(new Event('keydown'), ['ctrlKey', 'shiftKey'], 'End');
gridStub.onKeyDown.notify({ cell: 2, row: 101, grid: gridStub }, keyDownEvent, gridStub);

const expectedRangeCalled = [
Expand Down
31 changes: 23 additions & 8 deletions packages/common/src/extensions/slickCellSelectionModel.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isDefined } from '@slickgrid-universal/utils';

import type { CellRange, OnActiveCellChangedEventArgs, SlickDataView, SlickEventHandler, SlickGrid, SlickNamespace, SlickRange } from '../interfaces/index';
import { SlickCellRangeSelector } from './index';

Expand Down Expand Up @@ -159,9 +161,12 @@ export class SlickCellSelectionModel {

protected handleActiveCellChange(_e: Event, args: OnActiveCellChangedEventArgs) {
this._prevSelectedRow = undefined;
if (this._addonOptions?.selectActiveCell && args.row !== null && args.cell !== null) {
const isCellDefined = isDefined(args.cell);
const isRowDefined = isDefined(args.row);

if (this._addonOptions?.selectActiveCell && isRowDefined && isCellDefined) {
this.setSelectedRanges([new Slick.Range(args.row, args.cell)]);
} else if (!this._addonOptions?.selectActiveCell) {
} else if (!this._addonOptions?.selectActiveCell || (!isRowDefined && !isCellDefined)) {
// clear the previous selection once the cell changes
this.setSelectedRanges([]);
}
Expand All @@ -186,8 +191,8 @@ export class SlickCellSelectionModel {
protected handleKeyDown(e: KeyboardEvent) {
let ranges: CellRange[];
let last: SlickRange;
const colLn = this._grid.getColumns().length;
const active = this._grid.getActiveCell();
const metaKey = e.ctrlKey || e.metaKey;

let dataLn = 0;
if (this._dataView) {
Expand All @@ -196,7 +201,7 @@ export class SlickCellSelectionModel {
dataLn = this._grid.getDataLength();
}

if (active && e.shiftKey && !metaKey && !e.altKey && this.isKeyAllowed(e.key)) {
if (active && (e.shiftKey || e.ctrlKey) && !e.altKey && this.isKeyAllowed(e.key)) {
ranges = this.getSelectedRanges().slice();
if (!ranges.length) {
ranges.push(new Slick.Range(active.row, active.cell));
Expand All @@ -216,9 +221,10 @@ export class SlickCellSelectionModel {
const dirRow = active.row === last.fromRow ? 1 : -1;
const dirCell = active.cell === last.fromCell ? 1 : -1;
const isSingleKeyMove = e.key.startsWith('Arrow');
let toCell: undefined | number;
let toRow = 0;

if (isSingleKeyMove) {
if (isSingleKeyMove && !e.ctrlKey) {
// single cell move: (Arrow{Up/ArrowDown/ArrowLeft/ArrowRight})
if (e.key === 'ArrowLeft') {
dCell -= dirCell;
Expand All @@ -239,9 +245,17 @@ export class SlickCellSelectionModel {
this._prevSelectedRow = active.row;
}

if (e.key === 'Home') {
if (e.shiftKey && !e.ctrlKey && e.key === 'Home') {
toCell = 0;
toRow = active.row;
} else if (e.shiftKey && !e.ctrlKey && e.key === 'End') {
toCell = colLn - 1;
toRow = active.row;
} else if (e.ctrlKey && e.shiftKey && e.key === 'Home') {
toCell = 0;
toRow = 0;
} else if (e.key === 'End') {
} else if (e.ctrlKey && e.shiftKey && e.key === 'End') {
toCell = colLn - 1;
toRow = dataLn - 1;
} else if (e.key === 'PageUp') {
if (this._prevSelectedRow >= 0) {
Expand All @@ -262,7 +276,8 @@ export class SlickCellSelectionModel {
}

// define new selection range
const newLast = new Slick.Range(active.row, active.cell, toRow, active.cell + dirCell * dCell);
toCell ??= active.cell + dirCell * dCell;
const newLast = new Slick.Range(active.row, active.cell, toRow, toCell);
if (this.removeInvalidRanges([newLast]).length) {
ranges.push(newLast);
const viewRow = dirRow > 0 ? newLast.toRow : newLast.fromRow;
Expand Down
21 changes: 21 additions & 0 deletions packages/utils/src/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
objectAssignAndExtend,
emptyObject,
hasData,
isDefined,
isEmptyObject,
isNumber,
isPrimitiveValue,
Expand Down Expand Up @@ -98,6 +99,26 @@ describe('Service/Utilies', () => {
});
});

describe('isDefined method', () => {
it('should be truthy when comparing against any defined variable', () => {
const result1 = isDefined({ firstName: 'John', lastName: 'Doe' });
const result2 = isDefined('hello');

expect(result1).toBeTruthy();
expect(result2).toBeTruthy();
});

it('should be falsy when comparing against empty string, null or undefined', () => {
const result1 = isDefined('');
const result2 = isDefined(null);
const result3 = isDefined(undefined);

expect(result1).toBeFalsy();
expect(result2).toBeFalsy();
expect(result3).toBeFalsy();
});
});

describe('isEmptyObject method', () => {
it('should return True when comparing against an object that has properties', () => {
const result = isEmptyObject({ firstName: 'John', lastName: 'Doe' });
Expand Down
4 changes: 4 additions & 0 deletions packages/utils/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ export function isEmptyObject(obj: any): boolean {
return Object.entries(obj).length === 0;
}

export function isDefined<T>(value: T | undefined | null): value is T {
return <T>value !== undefined && <T>value !== null && <T>value !== '';
}

/**
* Simple object check.
* @param item
Expand Down
2 changes: 1 addition & 1 deletion packages/vanilla-bundle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"@slickgrid-universal/utils": "workspace:~",
"dequal": "^2.0.3",
"flatpickr": "^4.6.13",
"slickgrid": "^4.1.5",
"slickgrid": "^4.1.6",
"sortablejs": "^1.15.1",
"whatwg-fetch": "^3.6.19"
},
Expand Down
Loading