diff options
Diffstat (limited to 'chromium/ui/webui/resources/js/cr/ui/focus_grid.ts')
-rw-r--r-- | chromium/ui/webui/resources/js/cr/ui/focus_grid.ts | 172 |
1 files changed, 172 insertions, 0 deletions
diff --git a/chromium/ui/webui/resources/js/cr/ui/focus_grid.ts b/chromium/ui/webui/resources/js/cr/ui/focus_grid.ts new file mode 100644 index 00000000000..a765f727cc4 --- /dev/null +++ b/chromium/ui/webui/resources/js/cr/ui/focus_grid.ts @@ -0,0 +1,172 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// clang-format off +import {assert} from '../../assert.m.js'; + +import {FocusRow, FocusRowDelegate} from './focus_row.m.js'; +// clang-format on + +/** + * A class to manage grid of focusable elements in a 2D grid. For example, + * given this grid: + * + * focusable [focused] focusable (row: 0, col: 1) + * focusable focusable focusable + * focusable focusable focusable + * + * Pressing the down arrow would result in the focus moving down 1 row and + * keeping the same column: + * + * focusable focusable focusable + * focusable [focused] focusable (row: 1, col: 1) + * focusable focusable focusable + * + * And pressing right or tab at this point would move the focus to: + * + * focusable focusable focusable + * focusable focusable [focused] (row: 1, col: 2) + * focusable focusable focusable + */ +export class FocusGrid implements FocusRowDelegate { + rows: FocusRow[] = []; + private ignoreFocusChange_: boolean = false; + private lastFocused_: EventTarget|null = null; + + onFocus(row: FocusRow, e: Event) { + if (this.ignoreFocusChange_) { + this.ignoreFocusChange_ = false; + } else { + this.lastFocused_ = e.currentTarget; + } + + this.rows.forEach(function(r) { + r.makeActive(r === row); + }); + } + + onKeydown(row: FocusRow, e: KeyboardEvent) { + const rowIndex = this.rows.indexOf(row); + assert(rowIndex >= 0); + + let newRow = -1; + + if (e.key === 'ArrowUp') { + newRow = rowIndex - 1; + } else if (e.key === 'ArrowDown') { + newRow = rowIndex + 1; + } else if (e.key === 'PageUp') { + newRow = 0; + } else if (e.key === 'PageDown') { + newRow = this.rows.length - 1; + } + + const rowToFocus = this.rows[newRow]; + if (rowToFocus) { + this.ignoreFocusChange_ = true; + rowToFocus.getEquivalentElement(this.lastFocused_ as HTMLElement).focus(); + e.preventDefault(); + return true; + } + + return false; + } + + getCustomEquivalent(_sampleElement: HTMLElement) { + return null; + } + + /** + * Unregisters event handlers and removes all |this.rows|. + */ + destroy() { + this.rows.forEach(function(row) { + row.destroy(); + }); + this.rows.length = 0; + } + + /** + * @param target A target item to find in this grid. + * @return The row index. -1 if not found. + */ + getRowIndexForTarget(target: HTMLElement): number { + for (let i = 0; i < this.rows.length; ++i) { + if (this.rows[i]!.getElements().indexOf(target) >= 0) { + return i; + } + } + return -1; + } + + /** + * @param root An element to search for. + * @return The row with root of |root| or null. + */ + getRowForRoot(root: HTMLElement): FocusRow|null { + for (let i = 0; i < this.rows.length; ++i) { + if (this.rows[i]!.root === root) { + return this.rows[i]!; + } + } + return null; + } + + /** + * Adds |row| to the end of this list. + * @param row The row that needs to be added to this grid. + */ + addRow(row: FocusRow) { + this.addRowBefore(row, null); + } + + /** + * Adds |row| before |nextRow|. If |nextRow| is not in the list or it's + * null, |row| is added to the end. + * @param row The row that needs to be added to this grid. + * @param nextRow The row that should follow |row|. + */ + addRowBefore(row: FocusRow, nextRow: FocusRow|null) { + row.delegate = row.delegate || this; + + const nextRowIndex = nextRow ? this.rows.indexOf(nextRow) : -1; + if (nextRowIndex === -1) { + this.rows.push(row); + } else { + this.rows.splice(nextRowIndex, 0, row); + } + } + + /** + * Removes a row from the focus row. No-op if row is not in the grid. + * @param row The row that needs to be removed. + */ + removeRow(row: FocusRow|null) { + const nextRowIndex = row ? this.rows.indexOf(row) : -1; + if (nextRowIndex > -1) { + this.rows.splice(nextRowIndex, 1); + } + } + + /** + * Makes sure that at least one row is active. Should be called once, after + * adding all rows to FocusGrid. + * @param preferredRow The row to select if no other row is + * active. Selects the first item if this is beyond the range of the + * grid. + */ + ensureRowActive(preferredRow?: number) { + if (this.rows.length === 0) { + return; + } + + for (let i = 0; i < this.rows.length; ++i) { + if (this.rows[i]!.isActive()) { + return; + } + } + + (this.rows[preferredRow || 0] || this.rows[0]!).makeActive(true); + } +} |