mirror of
https://github.com/bitwarden/browser
synced 2026-02-21 03:43:58 +00:00
different approach
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user