github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/app/components/multi-select-dropdown.js (about) 1 import Component from '@ember/component'; 2 import { action } from '@ember/object'; 3 import { computed as overridable } from 'ember-overridable-computed'; 4 import { scheduleOnce } from '@ember/runloop'; 5 import { classNames } from '@ember-decorators/component'; 6 import classic from 'ember-classic-decorator'; 7 8 const TAB = 9; 9 const ESC = 27; 10 const SPACE = 32; 11 const ARROW_UP = 38; 12 const ARROW_DOWN = 40; 13 14 @classic 15 @classNames('dropdown') 16 export default class MultiSelectDropdown extends Component { 17 @overridable(() => []) options; 18 @overridable(() => []) selection; 19 20 onSelect() {} 21 22 isOpen = false; 23 dropdown = null; 24 25 capture(dropdown) { 26 // It's not a good idea to grab a dropdown reference like this, but it's necessary 27 // in order to invoke dropdown.actions.close in traverseList as well as 28 // dropdown.actions.reposition when the label or selection length changes. 29 this.set('dropdown', dropdown); 30 } 31 32 didReceiveAttrs() { 33 super.didReceiveAttrs(); 34 const dropdown = this.dropdown; 35 if (this.isOpen && dropdown) { 36 scheduleOnce('afterRender', this, this.repositionDropdown); 37 } 38 } 39 40 repositionDropdown() { 41 this.dropdown.actions.reposition(); 42 } 43 44 @action 45 toggle({ key }) { 46 const newSelection = this.selection.slice(); 47 if (newSelection.includes(key)) { 48 newSelection.removeObject(key); 49 } else { 50 newSelection.addObject(key); 51 } 52 this.onSelect(newSelection); 53 } 54 55 @action 56 openOnArrowDown(dropdown, e) { 57 this.capture(dropdown); 58 59 if (!this.isOpen && e.keyCode === ARROW_DOWN) { 60 dropdown.actions.open(e); 61 e.preventDefault(); 62 } else if (this.isOpen && (e.keyCode === TAB || e.keyCode === ARROW_DOWN)) { 63 const optionsId = this.element 64 .querySelector('.dropdown-trigger') 65 .getAttribute('aria-owns'); 66 const firstElement = document.querySelector( 67 `#${optionsId} .dropdown-option` 68 ); 69 70 if (firstElement) { 71 firstElement.focus(); 72 e.preventDefault(); 73 } 74 } 75 } 76 77 @action 78 traverseList(option, e) { 79 if (e.keyCode === ESC) { 80 // Close the dropdown 81 const dropdown = this.dropdown; 82 if (dropdown) { 83 dropdown.actions.close(e); 84 // Return focus to the trigger so tab works as expected 85 const trigger = this.element.querySelector('.dropdown-trigger'); 86 if (trigger) trigger.focus(); 87 e.preventDefault(); 88 this.set('dropdown', null); 89 } 90 } else if (e.keyCode === ARROW_UP) { 91 // previous item 92 const prev = e.target.previousElementSibling; 93 if (prev) { 94 prev.focus(); 95 e.preventDefault(); 96 } 97 } else if (e.keyCode === ARROW_DOWN) { 98 // next item 99 const next = e.target.nextElementSibling; 100 if (next) { 101 next.focus(); 102 e.preventDefault(); 103 } 104 } else if (e.keyCode === SPACE) { 105 this.send('toggle', option); 106 e.preventDefault(); 107 } 108 } 109 }