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  }