2023-03-14 13:18:55 +00:00
|
|
|
/**
|
|
|
|
* Copyright (c) HashiCorp, Inc.
|
2023-08-11 13:12:13 +00:00
|
|
|
* SPDX-License-Identifier: BUSL-1.1
|
2023-03-14 13:18:55 +00:00
|
|
|
*/
|
|
|
|
|
2022-02-09 09:47:45 +00:00
|
|
|
import Modifier from 'ember-modifier';
|
|
|
|
import { inject as service } from '@ember/service';
|
|
|
|
import { action } from '@ember/object';
|
2023-10-17 13:27:42 +00:00
|
|
|
import { registerDestructor } from '@ember/destroyable';
|
2022-02-09 09:47:45 +00:00
|
|
|
|
|
|
|
const TAB = 9;
|
|
|
|
const ESC = 27;
|
|
|
|
const END = 35;
|
|
|
|
const HOME = 36;
|
|
|
|
const ARROW_UP = 38;
|
|
|
|
const ARROW_DOWN = 40;
|
|
|
|
|
|
|
|
const keys = {
|
|
|
|
vertical: {
|
|
|
|
[ARROW_DOWN]: ($items, i = -1) => {
|
|
|
|
return (i + 1) % $items.length;
|
|
|
|
},
|
|
|
|
[ARROW_UP]: ($items, i = 0) => {
|
|
|
|
if (i === 0) {
|
|
|
|
return $items.length - 1;
|
|
|
|
} else {
|
|
|
|
return i - 1;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[HOME]: ($items, i) => {
|
|
|
|
return 0;
|
|
|
|
},
|
|
|
|
[END]: ($items, i) => {
|
|
|
|
return $items.length - 1;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
horizontal: {},
|
|
|
|
};
|
|
|
|
|
|
|
|
const MENU_ITEMS = '[role^="menuitem"]';
|
|
|
|
|
2023-10-17 13:27:42 +00:00
|
|
|
function cleanup(instance) {
|
|
|
|
if (instance) {
|
|
|
|
instance?.doc?.removeEventListener('keydown', instance?.keydown);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-09 09:47:45 +00:00
|
|
|
export default class AriaMenuModifier extends Modifier {
|
|
|
|
@service('-document') doc;
|
|
|
|
orientation = 'vertical';
|
|
|
|
|
|
|
|
@action
|
|
|
|
async keydown(e) {
|
|
|
|
if (e.keyCode === ESC) {
|
|
|
|
this.options.onclose(e);
|
2023-10-17 13:27:42 +00:00
|
|
|
this.$trigger?.focus();
|
2022-02-09 09:47:45 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const $items = [...this.element.querySelectorAll(MENU_ITEMS)];
|
2022-09-15 08:43:17 +00:00
|
|
|
const pos = $items.findIndex(($item) => $item === this.doc.activeElement);
|
2022-02-09 09:47:45 +00:00
|
|
|
if (e.keyCode === TAB) {
|
|
|
|
if (e.shiftKey) {
|
|
|
|
if (pos === 0) {
|
|
|
|
this.options.onclose(e);
|
2023-10-17 13:27:42 +00:00
|
|
|
this.$trigger?.focus();
|
2022-02-09 09:47:45 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (pos === $items.length - 1) {
|
2022-09-15 08:43:17 +00:00
|
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
2022-02-09 09:47:45 +00:00
|
|
|
this.options.onclose(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (typeof keys[this.orientation][e.keyCode] === 'undefined') {
|
|
|
|
return;
|
|
|
|
}
|
2023-10-17 13:27:42 +00:00
|
|
|
$items[keys[this.orientation][e.keyCode]($items, pos)]?.focus();
|
2022-02-09 09:47:45 +00:00
|
|
|
e.stopPropagation();
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
@action
|
|
|
|
async focus(e) {
|
|
|
|
if (e.pointerType === '') {
|
|
|
|
await Promise.resolve();
|
|
|
|
this.keydown({
|
|
|
|
keyCode: HOME,
|
|
|
|
stopPropagation: () => {},
|
|
|
|
preventDefault: () => {},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-17 13:27:42 +00:00
|
|
|
modify(element, positional, named) {
|
|
|
|
this.params = positional;
|
|
|
|
this.options = named;
|
2022-02-09 09:47:45 +00:00
|
|
|
|
2023-10-17 13:27:42 +00:00
|
|
|
if (!this.$trigger) {
|
|
|
|
this.element = element;
|
|
|
|
this.$trigger = this.doc.getElementById(element.getAttribute('aria-labelledby'));
|
|
|
|
if (typeof named.openEvent !== 'undefined') {
|
|
|
|
this.focus(named.openEvent);
|
|
|
|
}
|
2022-02-09 09:47:45 +00:00
|
|
|
|
2023-10-17 13:27:42 +00:00
|
|
|
this.doc.addEventListener('keydown', this.keydown);
|
|
|
|
}
|
|
|
|
registerDestructor(this, cleanup);
|
2022-02-09 09:47:45 +00:00
|
|
|
}
|
|
|
|
}
|