89136cbf6a
Manual interventions: • decorators on the same line for service and controller injections and most computed property macros • preserving import order when possible, both per-line and intra-line • moving new imports to the bottom • removal of classic decorator for trivial cases • conversion of init to constructor when appropriate
105 lines
2.8 KiB
JavaScript
105 lines
2.8 KiB
JavaScript
import Component from '@ember/component';
|
|
import { action } from '@ember/object';
|
|
import { computed as overridable } from 'ember-overridable-computed';
|
|
import { run } from '@ember/runloop';
|
|
import { classNames } from '@ember-decorators/component';
|
|
import classic from 'ember-classic-decorator';
|
|
|
|
const TAB = 9;
|
|
const ESC = 27;
|
|
const SPACE = 32;
|
|
const ARROW_UP = 38;
|
|
const ARROW_DOWN = 40;
|
|
|
|
@classic
|
|
@classNames('dropdown')
|
|
export default class MultiSelectDropdown extends Component {
|
|
@overridable(() => []) options;
|
|
@overridable(() => []) selection;
|
|
|
|
onSelect() {}
|
|
|
|
isOpen = false;
|
|
dropdown = null;
|
|
|
|
capture(dropdown) {
|
|
// It's not a good idea to grab a dropdown reference like this, but it's necessary
|
|
// in order to invoke dropdown.actions.close in traverseList as well as
|
|
// dropdown.actions.reposition when the label or selection length changes.
|
|
this.set('dropdown', dropdown);
|
|
}
|
|
|
|
didReceiveAttrs() {
|
|
const dropdown = this.dropdown;
|
|
if (this.isOpen && dropdown) {
|
|
run.scheduleOnce('afterRender', this, this.repositionDropdown);
|
|
}
|
|
}
|
|
|
|
repositionDropdown() {
|
|
this.dropdown.actions.reposition();
|
|
}
|
|
|
|
@action
|
|
toggle({ key }) {
|
|
const newSelection = this.selection.slice();
|
|
if (newSelection.includes(key)) {
|
|
newSelection.removeObject(key);
|
|
} else {
|
|
newSelection.addObject(key);
|
|
}
|
|
this.onSelect(newSelection);
|
|
}
|
|
|
|
@action
|
|
openOnArrowDown(dropdown, e) {
|
|
this.capture(dropdown);
|
|
|
|
if (!this.isOpen && e.keyCode === ARROW_DOWN) {
|
|
dropdown.actions.open(e);
|
|
e.preventDefault();
|
|
} else if (this.isOpen && (e.keyCode === TAB || e.keyCode === ARROW_DOWN)) {
|
|
const optionsId = this.element.querySelector('.dropdown-trigger').getAttribute('aria-owns');
|
|
const firstElement = document.querySelector(`#${optionsId} .dropdown-option`);
|
|
|
|
if (firstElement) {
|
|
firstElement.focus();
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
}
|
|
|
|
@action
|
|
traverseList(option, e) {
|
|
if (e.keyCode === ESC) {
|
|
// Close the dropdown
|
|
const dropdown = this.dropdown;
|
|
if (dropdown) {
|
|
dropdown.actions.close(e);
|
|
// Return focus to the trigger so tab works as expected
|
|
const trigger = this.element.querySelector('.dropdown-trigger');
|
|
if (trigger) trigger.focus();
|
|
e.preventDefault();
|
|
this.set('dropdown', null);
|
|
}
|
|
} else if (e.keyCode === ARROW_UP) {
|
|
// previous item
|
|
const prev = e.target.previousElementSibling;
|
|
if (prev) {
|
|
prev.focus();
|
|
e.preventDefault();
|
|
}
|
|
} else if (e.keyCode === ARROW_DOWN) {
|
|
// next item
|
|
const next = e.target.nextElementSibling;
|
|
if (next) {
|
|
next.focus();
|
|
e.preventDefault();
|
|
}
|
|
} else if (e.keyCode === SPACE) {
|
|
this.send('toggle', option);
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
}
|