picker.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import Keyboard from '../modules/keyboard';
  2. import DropdownIcon from '../assets/icons/dropdown.svg';
  3. let optionsCounter = 0;
  4. function toggleAriaAttribute(element, attribute) {
  5. element.setAttribute(attribute, !(element.getAttribute(attribute) === 'true'));
  6. }
  7. class Picker {
  8. constructor(select) {
  9. this.select = select;
  10. this.container = document.createElement('span');
  11. this.buildPicker();
  12. this.select.style.display = 'none';
  13. this.select.parentNode.insertBefore(this.container, this.select);
  14. this.label.addEventListener('mousedown', () => {
  15. this.togglePicker();
  16. });
  17. this.label.addEventListener('keydown', (event) => {
  18. switch(event.keyCode) {
  19. // Allows the "Enter" key to open the picker
  20. case Keyboard.keys.ENTER:
  21. this.togglePicker();
  22. break;
  23. // Allows the "Escape" key to close the picker
  24. case Keyboard.keys.ESCAPE:
  25. this.escape();
  26. event.preventDefault();
  27. break;
  28. default:
  29. }
  30. });
  31. this.select.addEventListener('change', this.update.bind(this));
  32. }
  33. togglePicker() {
  34. this.container.classList.toggle('ql-expanded');
  35. // Toggle aria-expanded and aria-hidden to make the picker accessible
  36. toggleAriaAttribute(this.label, 'aria-expanded');
  37. toggleAriaAttribute(this.options, 'aria-hidden');
  38. }
  39. buildItem(option) {
  40. let item = document.createElement('span');
  41. item.tabIndex = '0';
  42. item.setAttribute('role', 'button');
  43. item.classList.add('ql-picker-item');
  44. if (option.hasAttribute('value')) {
  45. item.setAttribute('data-value', option.getAttribute('value'));
  46. }
  47. if (option.textContent) {
  48. item.setAttribute('data-label', option.textContent);
  49. }
  50. item.addEventListener('click', () => {
  51. this.selectItem(item, true);
  52. });
  53. item.addEventListener('keydown', (event) => {
  54. switch(event.keyCode) {
  55. // Allows the "Enter" key to select an item
  56. case Keyboard.keys.ENTER:
  57. this.selectItem(item, true);
  58. event.preventDefault();
  59. break;
  60. // Allows the "Escape" key to close the picker
  61. case Keyboard.keys.ESCAPE:
  62. this.escape();
  63. event.preventDefault();
  64. break;
  65. default:
  66. }
  67. });
  68. return item;
  69. }
  70. buildLabel() {
  71. let label = document.createElement('span');
  72. label.classList.add('ql-picker-label');
  73. label.innerHTML = DropdownIcon;
  74. label.tabIndex = '0';
  75. label.setAttribute('role', 'button');
  76. label.setAttribute('aria-expanded', 'false');
  77. this.container.appendChild(label);
  78. return label;
  79. }
  80. buildOptions() {
  81. let options = document.createElement('span');
  82. options.classList.add('ql-picker-options');
  83. // Don't want screen readers to read this until options are visible
  84. options.setAttribute('aria-hidden', 'true');
  85. options.tabIndex = '-1';
  86. // Need a unique id for aria-controls
  87. options.id = `ql-picker-options-${optionsCounter}`;
  88. optionsCounter += 1;
  89. this.label.setAttribute('aria-controls', options.id);
  90. this.options = options;
  91. [].slice.call(this.select.options).forEach((option) => {
  92. let item = this.buildItem(option);
  93. options.appendChild(item);
  94. if (option.selected === true) {
  95. this.selectItem(item);
  96. }
  97. });
  98. this.container.appendChild(options);
  99. }
  100. buildPicker() {
  101. [].slice.call(this.select.attributes).forEach((item) => {
  102. this.container.setAttribute(item.name, item.value);
  103. });
  104. this.container.classList.add('ql-picker');
  105. this.label = this.buildLabel();
  106. this.buildOptions();
  107. }
  108. escape() {
  109. // Close menu and return focus to trigger label
  110. this.close();
  111. // Need setTimeout for accessibility to ensure that the browser executes
  112. // focus on the next process thread and after any DOM content changes
  113. setTimeout(() => this.label.focus(), 1);
  114. }
  115. close() {
  116. this.container.classList.remove('ql-expanded');
  117. this.label.setAttribute('aria-expanded', 'false');
  118. this.options.setAttribute('aria-hidden', 'true');
  119. }
  120. selectItem(item, trigger = false) {
  121. let selected = this.container.querySelector('.ql-selected');
  122. if (item === selected) return;
  123. if (selected != null) {
  124. selected.classList.remove('ql-selected');
  125. }
  126. if (item == null) return;
  127. item.classList.add('ql-selected');
  128. this.select.selectedIndex = [].indexOf.call(item.parentNode.children, item);
  129. if (item.hasAttribute('data-value')) {
  130. this.label.setAttribute('data-value', item.getAttribute('data-value'));
  131. } else {
  132. this.label.removeAttribute('data-value');
  133. }
  134. if (item.hasAttribute('data-label')) {
  135. this.label.setAttribute('data-label', item.getAttribute('data-label'));
  136. } else {
  137. this.label.removeAttribute('data-label');
  138. }
  139. if (trigger) {
  140. if (typeof Event === 'function') {
  141. this.select.dispatchEvent(new Event('change'));
  142. } else if (typeof Event === 'object') { // IE11
  143. let event = document.createEvent('Event');
  144. event.initEvent('change', true, true);
  145. this.select.dispatchEvent(event);
  146. }
  147. this.close();
  148. }
  149. }
  150. update() {
  151. let option;
  152. if (this.select.selectedIndex > -1) {
  153. let item = this.container.querySelector('.ql-picker-options').children[this.select.selectedIndex];
  154. option = this.select.options[this.select.selectedIndex];
  155. this.selectItem(item);
  156. } else {
  157. this.selectItem(null);
  158. }
  159. let isActive = option != null && option !== this.select.querySelector('option[selected]');
  160. this.label.classList.toggle('ql-active', isActive);
  161. }
  162. }
  163. export default Picker;