Skip to content

Commit bab6ab7

Browse files
committed
feat(grid): add navigateTo method with optional activation
1 parent d7a169a commit bab6ab7

3 files changed

Lines changed: 150 additions & 4 deletions

File tree

src/components/grid.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,16 @@ export class IgcGridLite<T extends object> extends EventEmitterBase<IgcGridLiteE
401401
this.requestUpdate(PIPELINE);
402402
}
403403

404+
/**
405+
* Navigates to a position in the grid based on provided row index and column field.
406+
* @param row The row index to navigate to
407+
* @param column The column field to navigate to, if any
408+
* @param activate Optionally also activate the navigated cell
409+
*/
410+
public async navigateTo(row: number, column?: Keys<T>, activate = false) {
411+
await this._stateController.navigation.navigateTo(row, column, activate);
412+
}
413+
404414
/**
405415
* Returns a {@link ColumnConfiguration} for a given column.
406416
*/

src/controllers/navigation.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,14 @@ export class NavigationController<T extends object> implements ReactiveControlle
5353
].field;
5454
}
5555

56-
protected scrollToCell(node: ActiveNode<T>) {
57-
const row = Array.from(this._virtualizer?.querySelectorAll(GRID_ROW_TAG) ?? []).find(
58-
(row) => row.index === node.row
56+
protected queryRowByIndex(index: number) {
57+
return Array.from(this._virtualizer?.querySelectorAll(GRID_ROW_TAG) ?? []).find(
58+
(row) => row.index === index
5959
) as unknown as IgcGridLiteRow<T>;
60+
}
61+
62+
protected scrollToCell(node: ActiveNode<T>) {
63+
const row = this.queryRowByIndex(node.row);
6064

6165
if (row) {
6266
row.cells
@@ -128,4 +132,21 @@ export class NavigationController<T extends object> implements ReactiveControlle
128132
this.handlers.get(event.key)!.call(this);
129133
}
130134
}
135+
136+
public async navigateTo(row: number, column?: Keys<T>, activate = false) {
137+
if (activate) this.active = Object.assign(this.nextNode, { row, column });
138+
139+
// attempt to resolve row in DOM first as a check if layout change will be needed
140+
let item: Pick<HTMLElement, 'scrollIntoView'> | undefined = this.queryRowByIndex(row);
141+
let completePromise: Promise<void> | undefined;
142+
143+
if (!item) {
144+
item = this._virtualizer?.element(row);
145+
completePromise = item && this._virtualizer?.layoutComplete;
146+
}
147+
148+
item?.scrollIntoView({ block: 'nearest' });
149+
await completePromise;
150+
if (column) this.scrollToCell({ row, column });
151+
}
131152
}

test/activation.test.ts

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
1+
import { setupIgnoreWindowResizeObserverLoopErrors } from '@lit-labs/virtualizer/support/resize-observer-errors.js';
12
import { expect } from '@open-wc/testing';
23
import GridTestFixture from './utils/grid-fixture.js';
34
import data, { generateFieldPaths } from './utils/test-data.js';
45

5-
const TDD = new GridTestFixture(data);
6+
// reduced width to force scroll:
7+
const TDD = new GridTestFixture(data, { width: '300px' });
68
const keys = generateFieldPaths(data[0]);
79

10+
function isVisibleGrid(element: Element): boolean {
11+
const rect = element.getBoundingClientRect();
12+
const { top, bottom } = TDD.gridBody.getBoundingClientRect();
13+
const { left, right } = TDD.grid.getBoundingClientRect();
14+
15+
return rect.top >= top && rect.bottom <= bottom && rect.left >= left && rect.right <= right;
16+
}
17+
818
describe('Grid activation', () => {
19+
setupIgnoreWindowResizeObserverLoopErrors(beforeEach, afterEach);
920
beforeEach(async () => await TDD.setUp());
1021
afterEach(() => TDD.tearDown());
1122

@@ -174,6 +185,110 @@ describe('Grid activation', () => {
174185

175186
await TDD.fireNavigationEvent({ key: 'ArrowUp' });
176187
expect(firstRowCityCell.active).to.be.true;
188+
});
189+
});
190+
191+
describe('navigateTo API', () => {
192+
// extend test data to move into virtualized scroll:
193+
const testData = [
194+
...data,
195+
...data.map((x) => ({ ...x, id: x.id + data.length })),
196+
...data.map((x) => ({ ...x, id: x.id + data.length * 2 })),
197+
];
198+
199+
beforeEach(async () => {
200+
await TDD.updateProperty('data', testData);
201+
// expect horizontal and vertical scroll to ensure test setup doesn't change
202+
expect(TDD.grid.scrollWidth > TDD.grid.clientWidth).to.be.true;
203+
expect(TDD.gridBody.scrollHeight > TDD.gridBody.clientHeight).to.be.true;
204+
});
205+
206+
it('navigates to rows only', async () => {
207+
await TDD.grid.navigateTo(testData.length - 1);
208+
209+
let cell = TDD.rows.last.cells.first;
210+
expect(cell.active).to.be.false;
211+
expect(isVisibleGrid(cell.element)).to.be.true;
212+
213+
await TDD.grid.navigateTo(0);
214+
215+
cell = TDD.rows.first.cells.first;
216+
expect(cell.active).to.be.false;
217+
expect(isVisibleGrid(cell.element)).to.be.true;
218+
});
219+
220+
it('navigates to a specific row and column', async () => {
221+
await TDD.grid.navigateTo(testData.length - 2, 'name');
222+
223+
const cell = TDD.rows.get(-2).cells.get('name');
224+
expect(cell.active).to.be.false;
225+
expect(isVisibleGrid(cell.element)).to.be.true;
226+
});
227+
228+
it('navigates to a cell within already scrolled-to row', async () => {
229+
await TDD.grid.navigateTo(testData.length - 1, 'id');
230+
231+
let cell = TDD.rows.last.cells.get('id');
232+
expect(isVisibleGrid(cell.element)).to.be.true;
233+
234+
// last cell
235+
cell = TDD.rows.last.cells.last;
236+
expect(isVisibleGrid(cell.element)).to.be.false;
237+
await TDD.grid.navigateTo(testData.length - 1, keys.at(-1));
238+
239+
expect(isVisibleGrid(cell.element)).to.be.true;
240+
});
241+
242+
it('navigates through all columns', async () => {
243+
for (const key of keys) {
244+
await TDD.grid.navigateTo(0, key);
245+
const cell = TDD.rows.first.cells.get(key);
246+
expect(isVisibleGrid(cell.element)).to.be.true;
247+
}
248+
});
249+
250+
it('updates active state when navigating', async () => {
251+
await TDD.grid.navigateTo(0, 'id', true);
252+
expect(TDD.rows.first.cells.get('id').active).to.be.true;
253+
254+
await TDD.grid.navigateTo(1, 'name', true);
255+
expect(TDD.rows.first.cells.get('id').active).to.be.false;
256+
257+
let cell = TDD.rows.get(1).cells.get('name');
258+
expect(cell.active).to.be.true;
259+
expect(isVisibleGrid(cell.element)).to.be.true;
260+
261+
// last cell
262+
await TDD.grid.navigateTo(testData.length - 1, keys.at(-1), true);
263+
264+
cell = TDD.rows.last.cells.last;
265+
expect(cell.active).to.be.true;
266+
expect(isVisibleGrid(cell.element)).to.be.true;
267+
});
268+
269+
it('can navigate after click activation', async () => {
270+
await TDD.clickCell(TDD.rows.first.cells.first);
271+
expect(TDD.rows.first.cells.first.active).to.be.true;
272+
273+
await TDD.grid.navigateTo(3, 'active', true);
274+
expect(TDD.rows.first.cells.first.active).to.be.false;
275+
276+
const cell = TDD.rows.get(3).cells.get('active');
277+
expect(cell.active).to.be.true;
278+
expect(isVisibleGrid(cell.element)).to.be.true;
279+
});
280+
281+
it('keyboard navigation works after navigateTo', async () => {
282+
await TDD.grid.navigateTo(2, 'id', true);
283+
expect(TDD.rows.get(2).cells.get('id').active).to.be.true;
284+
285+
await TDD.fireNavigationEvent({ key: 'ArrowRight' });
286+
expect(TDD.rows.get(2).cells.get('name').active).to.be.true;
287+
288+
await TDD.fireNavigationEvent({ key: 'ArrowDown' });
289+
const cell = TDD.rows.get(3).cells.get('name');
290+
expect(cell.active).to.be.true;
291+
expect(isVisibleGrid(cell.element)).to.be.true;
177292
});
178293
});
179294
});

0 commit comments

Comments
 (0)