Skip to content

Commit 39017a0

Browse files
committed
feat(grid): add navigateTo method with optional activation
1 parent a9cbaa5 commit 39017a0

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 active 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,12 +1,23 @@
1+
import { setupIgnoreWindowResizeObserverLoopErrors } from '@lit-labs/virtualizer/support/resize-observer-errors.js';
12
import { expect } from '@open-wc/testing';
23
import type { Keys } from '../src/internal/types.js';
34
import GridTestFixture from './utils/grid-fixture.js';
45
import data, { type TestData } from './utils/test-data.js';
56

6-
const TDD = new GridTestFixture(data);
7+
// reduced width to force scroll:
8+
const TDD = new GridTestFixture(data, { width: '300px' });
79
const keys = Object.keys(data[0]) as Array<Keys<TestData>>;
810

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

@@ -118,4 +129,108 @@ describe('Grid activation', () => {
118129
expect(TDD.rows.last.cells.first.active).to.be.true;
119130
});
120131
});
132+
133+
describe('navigateTo API', () => {
134+
// extend test data to move into virtualized scroll:
135+
const testData = [
136+
...data,
137+
...data.map((x) => ({ ...x, id: x.id + data.length })),
138+
...data.map((x) => ({ ...x, id: x.id + data.length * 2 })),
139+
];
140+
141+
beforeEach(async () => {
142+
await TDD.updateProperty('data', testData);
143+
// expect horizontal and vertical scroll to ensure test setup doesn't change
144+
expect(TDD.grid.scrollWidth > TDD.grid.clientWidth).to.be.true;
145+
expect(TDD.gridBody.scrollHeight > TDD.gridBody.clientHeight).to.be.true;
146+
});
147+
148+
it('navigates to rows only', async () => {
149+
await TDD.grid.navigateTo(testData.length - 1);
150+
151+
let cell = TDD.rows.last.cells.first;
152+
expect(cell.active).to.be.false;
153+
expect(isVisibleGrid(cell.element)).to.be.true;
154+
155+
await TDD.grid.navigateTo(0);
156+
157+
cell = TDD.rows.first.cells.first;
158+
expect(cell.active).to.be.false;
159+
expect(isVisibleGrid(cell.element)).to.be.true;
160+
});
161+
162+
it('navigates to a specific row and column', async () => {
163+
await TDD.grid.navigateTo(testData.length - 2, 'name');
164+
165+
const cell = TDD.rows.get(-2).cells.get('name');
166+
expect(cell.active).to.be.false;
167+
expect(isVisibleGrid(cell.element)).to.be.true;
168+
});
169+
170+
it('navigates to a cell within already scrolled-to row', async () => {
171+
await TDD.grid.navigateTo(testData.length - 1, 'id');
172+
173+
let cell = TDD.rows.last.cells.get('id');
174+
expect(isVisibleGrid(cell.element)).to.be.true;
175+
176+
// last cell
177+
cell = TDD.rows.last.cells.last;
178+
expect(isVisibleGrid(cell.element)).to.be.false;
179+
await TDD.grid.navigateTo(testData.length - 1, keys.at(-1));
180+
181+
expect(isVisibleGrid(cell.element)).to.be.true;
182+
});
183+
184+
it('navigates through all columns', async () => {
185+
for (const key of keys) {
186+
await TDD.grid.navigateTo(0, key);
187+
const cell = TDD.rows.first.cells.get(key);
188+
expect(isVisibleGrid(cell.element)).to.be.true;
189+
}
190+
});
191+
192+
it('updates active state when navigating', async () => {
193+
await TDD.grid.navigateTo(0, 'id', true);
194+
expect(TDD.rows.first.cells.get('id').active).to.be.true;
195+
196+
await TDD.grid.navigateTo(1, 'name', true);
197+
expect(TDD.rows.first.cells.get('id').active).to.be.false;
198+
199+
let cell = TDD.rows.get(1).cells.get('name');
200+
expect(cell.active).to.be.true;
201+
expect(isVisibleGrid(cell.element)).to.be.true;
202+
203+
// last cell
204+
await TDD.grid.navigateTo(testData.length - 1, keys.at(-1), true);
205+
206+
cell = TDD.rows.last.cells.last;
207+
expect(cell.active).to.be.true;
208+
expect(isVisibleGrid(cell.element)).to.be.true;
209+
});
210+
211+
it('can navigate after click activation', async () => {
212+
await TDD.clickCell(TDD.rows.first.cells.first);
213+
expect(TDD.rows.first.cells.first.active).to.be.true;
214+
215+
await TDD.grid.navigateTo(3, 'active', true);
216+
expect(TDD.rows.first.cells.first.active).to.be.false;
217+
218+
const cell = TDD.rows.get(3).cells.get('active');
219+
expect(cell.active).to.be.true;
220+
expect(isVisibleGrid(cell.element)).to.be.true;
221+
});
222+
223+
it('keyboard navigation works after navigateTo', async () => {
224+
await TDD.grid.navigateTo(2, 'id', true);
225+
expect(TDD.rows.get(2).cells.get('id').active).to.be.true;
226+
227+
await TDD.fireNavigationEvent({ key: 'ArrowRight' });
228+
expect(TDD.rows.get(2).cells.get('name').active).to.be.true;
229+
230+
await TDD.fireNavigationEvent({ key: 'ArrowDown' });
231+
const cell = TDD.rows.get(3).cells.get('name');
232+
expect(cell.active).to.be.true;
233+
expect(isVisibleGrid(cell.element)).to.be.true;
234+
});
235+
});
121236
});

0 commit comments

Comments
 (0)