github.com/hernad/nomad@v1.6.112/ui/app/components/multi-select-dropdown.js (about)

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