fix: rework data-grid scroll into view (#6310)

* examples

* rework scroll into view

* Change files

* spec and story fix

* schema

* fix story

* add test

* release tag

* remove console spew
This commit is contained in:
Stephane Comeau 2022-12-13 10:03:41 -08:00 коммит произвёл GitHub
Родитель fedf41e522
Коммит 03ec71cfce
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 166 добавлений и 133 удалений

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "rework scroll into view",
"packageName": "@microsoft/fast-foundation",
"email": "stephcomeau@msn.com",
"dependentChangeType": "prerelease"
}

Просмотреть файл

@ -982,6 +982,7 @@ export class FASTDataGrid extends FASTElement {
noTabbing: boolean;
// (undocumented)
protected noTabbingChanged(): void;
pageSize: number | undefined;
// @internal
rowElements: HTMLElement[];
rowElementTag: string;

Просмотреть файл

@ -229,19 +229,20 @@ export const myDataGrid = DataGrid.compose({
#### Fields
| Name | Privacy | Type | Default | Description | Inherited From |
| ------------------------ | ------- | ---------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
| `noTabbing` | public | `boolean` | `false` | When true the component will not add itself to the tab queue. Default is false. | |
| `generateHeader` | public | `GenerateHeaderOptions` | | Whether the grid should automatically generate a header row and its type | |
| `gridTemplateColumns` | public | `string` | | String that gets applied to the the css gridTemplateColumns attribute of child rows | |
| `rowsData` | public | `object[]` | `[]` | The data being displayed in the grid | |
| `columnDefinitions` | public | `ColumnDefinition[] or null` | `null` | The column definitions of the grid | |
| `rowItemTemplate` | public | `ViewTemplate` | | The template to use for the programmatic generation of rows | |
| `cellItemTemplate` | public | `ViewTemplate or undefined` | | The template used to render cells in generated rows. | |
| `headerCellItemTemplate` | public | `ViewTemplate or undefined` | | The template used to render header cells in generated rows. | |
| `focusRowIndex` | public | `number` | `0` | The index of the row that will receive focus the next time the grid is focused. This value changes as focus moves to different rows within the grid. Changing this value when focus is already within the grid moves focus to the specified row. | |
| `focusColumnIndex` | public | `number` | `0` | The index of the column that will receive focus the next time the grid is focused. This value changes as focus moves to different rows within the grid. Changing this value when focus is already within the grid moves focus to the specified column. | |
| `rowElementTag` | public | `string` | | Set by the component templates. | |
| Name | Privacy | Type | Default | Description | Inherited From |
| ------------------------ | ------- | ---------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
| `noTabbing` | public | `boolean` | `false` | When true the component will not add itself to the tab queue. Default is false. | |
| `generateHeader` | public | `GenerateHeaderOptions` | | Whether the grid should automatically generate a header row and its type | |
| `gridTemplateColumns` | public | `string` | | String that gets applied to the the css gridTemplateColumns attribute of child rows | |
| `pageSize` | public | `number or undefined` | | The number of rows to move selection on page up/down keystrokes. When undefined the grid will use viewport height/the height of the first non-header row. If the grid itself is a scrolling container it will be considered the viewport for this purpose, otherwise the document will be used. | |
| `rowsData` | public | `object[]` | `[]` | The data being displayed in the grid | |
| `columnDefinitions` | public | `ColumnDefinition[] or null` | `null` | The column definitions of the grid | |
| `rowItemTemplate` | public | `ViewTemplate` | | The template to use for the programmatic generation of rows | |
| `cellItemTemplate` | public | `ViewTemplate or undefined` | | The template used to render cells in generated rows. | |
| `headerCellItemTemplate` | public | `ViewTemplate or undefined` | | The template used to render header cells in generated rows. | |
| `focusRowIndex` | public | `number` | `0` | The index of the row that will receive focus the next time the grid is focused. This value changes as focus moves to different rows within the grid. Changing this value when focus is already within the grid moves focus to the specified row. | |
| `focusColumnIndex` | public | `number` | `0` | The index of the column that will receive focus the next time the grid is focused. This value changes as focus moves to different rows within the grid. Changing this value when focus is already within the grid moves focus to the specified column. | |
| `rowElementTag` | public | `string` | | Set by the component templates. | |
#### Methods
@ -259,6 +260,7 @@ export const myDataGrid = DataGrid.compose({
| `no-tabbing` | noTabbing | |
| `generate-header` | generateHeader | |
| `grid-template-columns` | gridTemplateColumns | |
| `page-size` | pageSize | |
#### Slots

Просмотреть файл

@ -432,21 +432,35 @@ test.describe("Data grid", () => {
await expect(cells.nth(0)).toBeFocused();
});
test("should auto generate grid-columns from a manual row", async () => {
test("should scroll into view on focus", async () => {
await root.evaluate(node => {
node.innerHTML = /* html */ `
<fast-data-grid generate-header="none">
<fast-data-grid-row>
<fast-data-grid generate-header="none" style="height:100px; overflow-y: scroll;">
<fast-data-grid-row style="height:100px;">
<fast-data-grid-cell>1</fast-data-grid-cell>
</fast-data-grid-row>
<fast-data-grid-row style="height:100px;">
<fast-data-grid-cell>2</fast-data-grid-cell>
</fast-data-grid-row>
<fast-data-grid-row style="height:100px;">
<fast-data-grid-cell>3</fast-data-grid-cell>
</fast-data-grid-row>
<fast-data-grid-row style="height:100px;">
<fast-data-grid-cell>3</fast-data-grid-cell>
</fast-data-grid-row>
</fast-data-grid>
`;
});
const lastRow = element.locator("fast-data-grid-row");
const cells = element.locator("fast-data-grid-cell");
await expect(cells).toHaveCount(4);
await expect(lastRow).toHaveJSProperty("gridTemplateColumns", "1fr 1fr 1fr");
await expect(element).toHaveJSProperty("scrollTop", 0);
await cells.nth(0).focus();
await expect(element).toHaveJSProperty("scrollTop", 0);
await cells.nth(1).focus();
await expect(element).toHaveJSProperty("scrollTop", 100);
await cells.nth(2).focus();
await expect(element).toHaveJSProperty("scrollTop", 200);
});
});

Просмотреть файл

@ -178,6 +178,10 @@ The index of the row in the parent grid. This is typically set by the parent gri
- `row-type`
The row can either be either "default", "header" or "sticky-header" type according to the `DataGridRowTypes` enum. This determines the type of cells the row generates and what css classes get applied to it.
- `page-size`
The number of rows to move selection on page up/down keystrokes.
When undefined the grid will use viewport height/the height of the first non-header row. If the grid itself is a scrolling container it will be considered the viewport for this purpose, otherwise the document will be used.
*properties:*
- `rowData`
The object that contains the data to be displayed in this row.

Просмотреть файл

@ -3,6 +3,7 @@ import {
attr,
bind,
FASTElement,
nullableNumberConverter,
observable,
RepeatDirective,
Updates,
@ -184,6 +185,19 @@ export class FASTDataGrid extends FASTElement {
}
}
/**
* The number of rows to move selection on page up/down keystrokes.
* When undefined the grid will use viewport height/the height of the first non-header row.
* If the grid itself is a scrolling container it will be considered the viewport for this purpose,
* otherwise the document will be used.
*
* @public
* @remarks
* HTML Attribute: page-size
*/
@attr({ attribute: "page-size", converter: nullableNumberConverter })
public pageSize: number | undefined;
/**
* The data being displayed in the grid
*
@ -398,7 +412,7 @@ export class FASTDataGrid extends FASTElement {
* @internal
*/
public handleFocus(e: FocusEvent): void {
this.focusOnCell(this.focusRowIndex, this.focusColumnIndex, true);
this.focusOnCell(this.focusRowIndex, this.focusColumnIndex, "nearest");
}
/**
@ -419,84 +433,51 @@ export class FASTDataGrid extends FASTElement {
}
let newFocusRowIndex: number;
const maxIndex = this.rowElements.length - 1;
const currentGridBottom: number = this.offsetHeight + this.scrollTop;
const lastRow: HTMLElement = this.rowElements[maxIndex] as HTMLElement;
switch (e.key) {
case keyArrowUp:
e.preventDefault();
// focus up one row
this.focusOnCell(this.focusRowIndex - 1, this.focusColumnIndex, true);
this.focusOnCell(
this.focusRowIndex - 1,
this.focusColumnIndex,
"nearest"
);
break;
case keyArrowDown:
e.preventDefault();
// focus down one row
this.focusOnCell(this.focusRowIndex + 1, this.focusColumnIndex, true);
this.focusOnCell(
this.focusRowIndex + 1,
this.focusColumnIndex,
"nearest"
);
break;
case keyPageUp:
e.preventDefault();
if (this.rowElements.length === 0) {
this.focusOnCell(0, 0, false);
this.focusOnCell(0, 0, "nearest");
break;
}
if (this.focusRowIndex === 0) {
this.focusOnCell(0, this.focusColumnIndex, false);
return;
}
newFocusRowIndex = this.focusRowIndex - 1;
newFocusRowIndex = Math.max(0, this.focusRowIndex - this.getPageSize());
for (newFocusRowIndex; newFocusRowIndex >= 0; newFocusRowIndex--) {
const thisRow: HTMLElement = this.rowElements[newFocusRowIndex];
if (thisRow.offsetTop < this.scrollTop) {
this.scrollTop =
thisRow.offsetTop + thisRow.clientHeight - this.clientHeight;
break;
}
}
this.focusOnCell(newFocusRowIndex, this.focusColumnIndex, false);
this.focusOnCell(newFocusRowIndex, this.focusColumnIndex, "start");
break;
case keyPageDown:
e.preventDefault();
if (this.rowElements.length === 0) {
this.focusOnCell(0, 0, false);
this.focusOnCell(0, 0, "nearest");
break;
}
// focus down one "page"
if (
this.focusRowIndex >= maxIndex ||
lastRow.offsetTop + lastRow.offsetHeight <= currentGridBottom
) {
this.focusOnCell(maxIndex, this.focusColumnIndex, false);
return;
}
newFocusRowIndex = this.focusRowIndex + 1;
for (newFocusRowIndex; newFocusRowIndex <= maxIndex; newFocusRowIndex++) {
const thisRow: HTMLElement = this.rowElements[
newFocusRowIndex
] as HTMLElement;
if (thisRow.offsetTop + thisRow.offsetHeight > currentGridBottom) {
let stickyHeaderOffset: number = 0;
if (
this.generateHeader === GenerateHeaderOptions.sticky &&
this.generatedHeader !== null
) {
stickyHeaderOffset = this.generatedHeader.clientHeight;
}
this.scrollTop = thisRow.offsetTop - stickyHeaderOffset;
break;
}
}
this.focusOnCell(newFocusRowIndex, this.focusColumnIndex, false);
newFocusRowIndex = Math.min(
this.rowElements.length - 1,
this.focusRowIndex + this.getPageSize()
);
this.focusOnCell(newFocusRowIndex, this.focusColumnIndex, "end");
break;
@ -504,7 +485,7 @@ export class FASTDataGrid extends FASTElement {
if (e.ctrlKey) {
e.preventDefault();
// focus first cell of first row
this.focusOnCell(0, 0, true);
this.focusOnCell(0, 0, "nearest");
}
break;
@ -515,24 +496,54 @@ export class FASTDataGrid extends FASTElement {
this.focusOnCell(
this.rowElements.length - 1,
this.columnDefinitions.length - 1,
true
"nearest"
);
}
break;
}
}
private getPageSize(): number {
if (this.pageSize) {
return this.pageSize;
}
let rowHeight = 50;
this.rowElements.forEach(element => {
if (
!element.hasAttribute("rowType") ||
!element.getAttribute("rowType")?.includes("header")
) {
rowHeight = element.clientHeight;
}
});
let pageSize: number = 1;
if (rowHeight === 0) {
return pageSize;
}
if (this.clientHeight < this.scrollHeight) {
pageSize = this.clientHeight / rowHeight;
} else {
pageSize = document.body.clientHeight / rowHeight;
}
pageSize = Math.max(Math.round(pageSize), 1);
return pageSize;
}
private focusOnCell = (
rowIndex: number,
columnIndex: number,
scrollIntoView: boolean
alignment: ScrollLogicalPosition
): void => {
if (this.rowElements.length === 0) {
this.focusRowIndex = 0;
this.focusColumnIndex = 0;
return;
}
const focusRowIndex = Math.max(
0,
Math.min(this.rowElements.length - 1, rowIndex)
@ -547,16 +558,7 @@ export class FASTDataGrid extends FASTElement {
const focusTarget: HTMLElement = cells[focusColumnIndex] as HTMLElement;
if (
scrollIntoView &&
this.scrollHeight !== this.clientHeight &&
((focusRowIndex < this.focusRowIndex && this.scrollTop > 0) ||
(focusRowIndex > this.focusRowIndex &&
this.scrollTop < this.scrollHeight - this.clientHeight))
) {
focusTarget.scrollIntoView({ block: "center", inline: "center" });
}
focusTarget.scrollIntoView({ block: alignment });
focusTarget.focus();
};
@ -575,7 +577,7 @@ export class FASTDataGrid extends FASTElement {
private updateFocus(): void {
this.pendingFocusUpdate = false;
this.focusOnCell(this.focusRowIndex, this.focusColumnIndex, true);
this.focusOnCell(this.focusRowIndex, this.focusColumnIndex, "nearest");
}
private toggleGeneratedHeader(): void {

Просмотреть файл

@ -5,67 +5,64 @@ import { FASTDataGrid, GenerateHeaderOptions } from "../data-grid.js";
const storyTemplate = html<StoryArgs<FASTDataGrid>>`
<fast-data-grid
style="${x => x.style}"
:rowsData="${x => x.rowsData}"
?no-tabbing="${x => x.noTabbing}"
no-tabbing="${x => x.noTabbing}"
generate-header="${x => x.generateHeader}"
grid-template-columns="${x => x.gridTemplateColumns}"
page-size="${x => x.pageSize}"
>
${x => x.storyContent}
${x => x.content}
</fast-data-grid>
`;
function newDataRow(id: string): object {
return {
rowId: `rowid-${id}`,
item1: `value 1-${id}`,
item2: `value 2-${id}`,
item3: `value 3-${id}`,
item4: `value 4-${id}`,
item5: `value 5-${id}`,
item6: `value 6-${id}`,
};
}
function newDataSet(rowCount: number): any[] {
return Array.from({ length: rowCount }, (v, i) => newDataRow(`${i + 1}`));
}
export default {
title: "Data Grid",
args: {
noTabbing: false,
rowsData: [
{
item1: `value 1-1`,
item2: `value 2-1`,
item3: `value 3-1`,
item4: `value 4-1`,
item5: `value 5-1`,
},
{
item1: `value 1-2`,
item2: `value 2-2`,
item3: `value 3-2`,
item4: `value 4-2`,
item5: `value 5-2`,
},
{
item1: `value 1-3`,
item2: `value 2-3`,
item3: `value 3-3`,
item4: `value 4-3`,
item5: `value 5-3`,
},
{
item1: `value 1-4`,
item2: `value 2-4`,
item3: `value 3-4`,
item4: `value 4-4`,
item5: `value 5-4`,
},
{
item1: `value 1-5`,
item2: `value 2-5`,
item3: `value 3-5`,
item4: `value 4-5`,
item5: `value 5-5`,
},
],
rowsData: newDataSet(100),
},
argTypes: {
generateHeader: {
control: "select",
options: Object.values(GenerateHeaderOptions),
style: {
control: { type: "text" },
},
content: { table: { disable: true } },
noTabbing: {
control: { type: "boolean" },
},
generateHeader: {
options: ["none", "default", "sticky"],
control: { type: "select" },
},
pageSize: {
control: { type: "number" },
},
gridTemplateColumns: {
control: { type: "text" },
},
gridTemplateColumns: { control: "text" },
noTabbing: { control: "boolean" },
rowsData: { control: "object" },
storyContent: { table: { disable: true } },
},
} as Meta<FASTDataGrid>;
export const DataGrid: Story<FASTDataGrid> = renderComponent(storyTemplate).bind({});
export const DataGridFixedHeight: Story<FASTDataGrid> = renderComponent(
storyTemplate
).bind({});
DataGridFixedHeight.args = {
style: "height: 200px; overflow-y: scroll;",
};

Просмотреть файл

@ -31,6 +31,12 @@
"mapsToAttribute": "no-tabbing",
"type": "boolean"
},
"page-size": {
"title": "Page size",
"description": "The number of rows to move selection on page up/down keystrokes.",
"mapsToAttribute": "page-size",
"type": "number"
},
"Slot": {
"title": "Default slot",
"description": "The content as data grid rows",