1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-21 03:43:58 +00:00

different approach

This commit is contained in:
William Martin
2025-05-20 21:04:46 -04:00
parent 337977444a
commit 74d30f95b0

View File

@@ -11,6 +11,8 @@ import {
input,
signal,
} from "@angular/core";
import { toObservable } from "@angular/core/rxjs-interop";
import { firstValueFrom, skip, filter, take } from "rxjs";
import type { A11yCellDirective } from "./a11y-cell.directive";
import { A11yRowDirective } from "./a11y-row.directive";
@@ -36,6 +38,7 @@ export class A11yGridDirective {
pageSize = input(5);
private rows = contentChildren(A11yRowDirective);
private rows$ = toObservable(this.rows);
private grid: Signal<A11yCellDirective[][]> = computed(() =>
this.rows().map((row) => [...row.cells()]),
@@ -45,63 +48,117 @@ export class A11yGridDirective {
return this.viewPort ? this.viewPort.getDataLength() : this.rows().length;
}
/** The row that currently has focus */
private activeRow = signal(0);
private renderedRow = computed(() => this.convertRealRowToViewportRow(this.activeRow()));
/** The cell that currently has focus */
private activeCol = signal(0);
private renderedRow = computed(() => this.convertRealRowToViewportRow(this.activeRow()));
private numColumns = computed(() => this.grid()[this.renderedRow()].length);
private focusTarget = computed(() =>
this.grid()?.[this.renderedRow()]?.[this.activeCol()]?.getFocusTarget(),
);
constructor() {
// Consolidated effect for row/cell roles and tabIndex management (no focus here!)
effect(() => {
this.rows().forEach((row) => (row.role = "row"));
this.grid()
.flat()
.forEach((cell) => {
cell.role = "gridcell";
if (cell.getFocusTarget() !== this.focusTarget()) {
cell.getFocusTarget().tabIndex = -1;
}
});
const focusTarget = this.focusTarget();
const rows = this.rows();
const grid = this.grid();
// Set row roles
rows.forEach((row) => (row.role = "row"));
// Set cell roles and tabIndex
grid.flat().forEach((cell) => {
cell.role = "gridcell";
if (cell.getFocusTarget() !== focusTarget) {
cell.getFocusTarget().tabIndex = -1;
}
});
});
}
private async updateRow(delta: number) {
const prev = this.activeRow();
let nextRow = prev + delta;
// Clamp to bounds
nextRow = Math.max(0, Math.min(nextRow, this.numRows - 1));
// If the row is not rendered, scroll and wait for it to render before updating
if (this.viewPort) {
const { start, end } = this.viewPort.getRenderedRange();
if (nextRow < start || nextRow >= end) {
this.viewPort.scrollToIndex(nextRow);
// Wait until the row is rendered and the cell exists
await firstValueFrom(
this.rows$.pipe(
skip(1),
filter((rows) => {
const renderedIdx = this.convertRealRowToViewportRow(nextRow);
return !!rows[renderedIdx];
}),
take(1),
),
);
}
}
// Only set activeRow after ensuring the row is rendered
this.activeRow.set(nextRow);
this.focusIfPossible();
}
private updateCol(delta: number) {
this.activeCol.update((prev) => {
let nextCol = prev + delta;
const cols = this.numColumns();
nextCol = Math.max(0, Math.min(nextCol, cols - 1));
return nextCol;
});
this.focusIfPossible();
}
private focusIfPossible() {
const focusTarget = this.focusTarget();
if (focusTarget && document.body.contains(focusTarget)) {
focusTarget.tabIndex = 0;
focusTarget.focus();
}
}
@HostListener("keydown", ["$event"])
onKeyDown(event: KeyboardEvent) {
async onKeyDown(event: KeyboardEvent) {
switch (event.code) {
case "ArrowUp":
this.updateActiveCell(-1, 0);
await this.updateRow(-1);
break;
case "ArrowRight":
this.updateActiveCell(0, 1);
this.updateCol(1);
break;
case "ArrowDown":
this.updateActiveCell(1, 0);
await this.updateRow(1);
break;
case "ArrowLeft":
this.updateActiveCell(0, -1);
this.updateCol(-1);
break;
case "Home":
this.updateActiveCell(-this.activeRow(), 0);
await this.updateRow(-this.activeRow());
break;
case "End":
this.updateActiveCell(this.numRows, 0);
await this.updateRow(this.numRows);
break;
case "PageUp":
this.updateActiveCell(-this.pageSize(), 0);
await this.updateRow(-this.pageSize());
break;
case "PageDown":
this.updateActiveCell(this.pageSize(), 0);
await this.updateRow(this.pageSize());
break;
default:
return;
}
/** Prevent default scrolling behavior */
event.preventDefault();
}
@@ -113,80 +170,10 @@ export class A11yGridDirective {
private convertRealRowToViewportRow(row: number): number {
if (this.viewPort) {
const { start } = this.viewPort.getRenderedRange();
// TODO, removing this console log makes things break
// eslint-disable-next-line no-console
console.log(`row: ${row}, start: ${start}, gridLength: ${this.grid().length}`);
if (row >= start) {
return row - start;
}
}
return row;
}
/** Get the number of columns for a particular row */
private getNumColumns(row: number) {
return this.grid()[this.convertRealRowToViewportRow(row)].length;
}
private scrollRowIntoViewport(row: number) {
if (!this.viewPort) {
return;
}
const { start, end } = this.viewPort.getRenderedRange();
if (row >= start && row <= end) {
return;
}
this.viewPort.scrollToIndex(row);
}
/** Move focus via a delta against the currently active gridcell */
private updateActiveCell(rowDelta: number, colDelta: number) {
let nextCol = this.activeCol() + colDelta;
let nextRow = this.activeRow() + rowDelta;
// Row upper bound
if (nextRow >= this.numRows) {
nextRow = this.grid().length - 1;
}
// Row lower bound
if (nextRow < 0) {
nextRow = 0;
}
// The row must exist in the viewport before we can query its columns
this.scrollRowIntoViewport(nextRow);
// Column upper bound
if (nextCol >= this.getNumColumns(nextRow)) {
if (nextRow < this.numRows - 1) {
// Wrap to next row on right arrow
nextCol = 0;
nextRow += 1;
} else {
nextCol = this.getNumColumns(nextRow) - 1;
}
}
// Column lower bound
if (nextCol < 0) {
if (nextRow > 0) {
// Wrap to prev row on left arrow
nextRow -= 1;
nextCol = this.getNumColumns(nextRow) - 1;
} else {
nextCol = 0;
}
}
this.activeCol.set(nextCol);
this.activeRow.set(nextRow);
this.focusTarget().tabIndex = 0;
this.focusTarget().focus();
}
}