blob: 7b1254a8a3af7406c6f203eaf0725338ff21c8dd [file] [log] [blame]
// Copyright 2020 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.
import {assert} from 'chrome://resources/js/assert.m.js';
import {FocusOutlineManager} from 'chrome://resources/js/cr/ui/focus_outline_manager.m.js';
/** @extends {HTMLElement} */
export class CrMenuSelector extends HTMLElement {
static get is() {
return 'cr-menu-selector';
}
constructor() {
super();
/** @private {!FocusOutlineManager} */
this.focusOutlineManager_;
this.addEventListener(
'focusin', e => this.onFocusin_(/** @type {!FocusEvent} */ (e)));
this.addEventListener(
'keydown', e => this.onKeydown_(/** @type {!KeyboardEvent} */ (e)));
}
connectedCallback() {
this.focusOutlineManager_ = FocusOutlineManager.forDocument(document);
this.setAttribute('role', 'menu');
}
/**
* @return {!Array<!HTMLElement>}
* @private
*/
getItems_() {
return /** @type {!Array<!HTMLElement>} */ (
Array.from(this.querySelectorAll(
'[role=menuitem]:not([disabled]):not([hidden])')));
}
/**
* @param {!FocusEvent} e
* @private
*/
onFocusin_(e) {
// If the focus was moved by keyboard and is coming in from a relatedTarget
// that is not within this menu, move the focus to the first menu item. This
// ensures that the first menu item is always the first focused item when
// focusing into the menu.
const focusMovedWithKeyboard = this.focusOutlineManager_.visible;
const focusMovedFromOutside = e.relatedTarget &&
!this.contains(/** @type {!HTMLElement} */ (e.relatedTarget));
if (focusMovedWithKeyboard && focusMovedFromOutside) {
this.getItems_()[0].focus();
}
}
/**
* @param {!KeyboardEvent} event
* @private
*/
onKeydown_(event) {
const items = this.getItems_();
assert(items.length >= 1);
const currentFocusedIndex = items.indexOf(
/** @type {!HTMLElement} */ (this.querySelector(':focus')));
let newFocusedIndex = currentFocusedIndex;
switch (event.key) {
case 'Tab':
if (event.shiftKey) {
// If pressing Shift+Tab, immediately focus the first element so that
// when the event is finished processing, the browser automatically
// focuses the previous focusable element outside of the menu.
items[0].focus();
} else {
// If pressing Tab, immediately focus the last element so that when
// the event is finished processing, the browser automatically focuses
// the next focusable element outside of the menu.
items[items.length - 1].focus();
}
return;
case 'ArrowDown':
newFocusedIndex = (currentFocusedIndex + 1) % items.length;
break;
case 'ArrowUp':
newFocusedIndex =
(currentFocusedIndex + items.length - 1) % items.length;
break;
case 'Home':
newFocusedIndex = 0;
break;
case 'End':
newFocusedIndex = items.length - 1;
break;
}
if (newFocusedIndex === currentFocusedIndex) {
return;
}
event.preventDefault();
items[newFocusedIndex].focus();
}
}
customElements.define(CrMenuSelector.is, CrMenuSelector);