dropdown.vue 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. <script>
  2. import Clickoutside from 'element-ui/src/utils/clickoutside';
  3. import Emitter from 'element-ui/src/mixins/emitter';
  4. import Migrating from 'element-ui/src/mixins/migrating';
  5. import ElButton from 'element-ui/packages/button';
  6. import ElButtonGroup from 'element-ui/packages/button-group';
  7. import { generateId } from 'element-ui/src/utils/util';
  8. export default {
  9. name: 'ElDropdown',
  10. componentName: 'ElDropdown',
  11. mixins: [Emitter, Migrating],
  12. directives: { Clickoutside },
  13. components: {
  14. ElButton,
  15. ElButtonGroup
  16. },
  17. provide() {
  18. return {
  19. dropdown: this
  20. };
  21. },
  22. props: {
  23. trigger: {
  24. type: String,
  25. default: 'hover'
  26. },
  27. type: String,
  28. size: {
  29. type: String,
  30. default: ''
  31. },
  32. splitButton: Boolean,
  33. hideOnClick: {
  34. type: Boolean,
  35. default: true
  36. },
  37. placement: {
  38. type: String,
  39. default: 'bottom-end'
  40. },
  41. visibleArrow: {
  42. default: true
  43. },
  44. showTimeout: {
  45. type: Number,
  46. default: 250
  47. },
  48. hideTimeout: {
  49. type: Number,
  50. default: 150
  51. },
  52. tabindex: {
  53. type: Number,
  54. default: 0
  55. },
  56. disabled: {
  57. type: Boolean,
  58. default: false
  59. }
  60. },
  61. data() {
  62. return {
  63. timeout: null,
  64. visible: false,
  65. triggerElm: null,
  66. menuItems: null,
  67. menuItemsArray: null,
  68. dropdownElm: null,
  69. focusing: false,
  70. listId: `dropdown-menu-${generateId()}`
  71. };
  72. },
  73. computed: {
  74. dropdownSize() {
  75. return this.size || (this.$ELEMENT || {}).size;
  76. }
  77. },
  78. mounted() {
  79. this.$on('menu-item-click', this.handleMenuItemClick);
  80. },
  81. watch: {
  82. visible(val) {
  83. this.broadcast('ElDropdownMenu', 'visible', val);
  84. this.$emit('visible-change', val);
  85. },
  86. focusing(val) {
  87. const selfDefine = this.$el.querySelector('.el-dropdown-selfdefine');
  88. if (selfDefine) { // 自定义
  89. if (val) {
  90. selfDefine.className += ' focusing';
  91. } else {
  92. selfDefine.className = selfDefine.className.replace('focusing', '');
  93. }
  94. }
  95. }
  96. },
  97. methods: {
  98. getMigratingConfig() {
  99. return {
  100. props: {
  101. 'menu-align': 'menu-align is renamed to placement.'
  102. }
  103. };
  104. },
  105. show() {
  106. if (this.disabled) return;
  107. clearTimeout(this.timeout);
  108. this.timeout = setTimeout(() => {
  109. this.visible = true;
  110. }, this.trigger === 'click' ? 0 : this.showTimeout);
  111. },
  112. hide() {
  113. if (this.disabled) return;
  114. this.removeTabindex();
  115. if (this.tabindex >= 0) {
  116. this.resetTabindex(this.triggerElm);
  117. }
  118. clearTimeout(this.timeout);
  119. this.timeout = setTimeout(() => {
  120. this.visible = false;
  121. }, this.trigger === 'click' ? 0 : this.hideTimeout);
  122. },
  123. handleClick() {
  124. if (this.disabled) return;
  125. if (this.visible) {
  126. this.hide();
  127. } else {
  128. this.show();
  129. }
  130. },
  131. handleTriggerKeyDown(ev) {
  132. const keyCode = ev.keyCode;
  133. if ([38, 40].indexOf(keyCode) > -1) { // up/down
  134. this.removeTabindex();
  135. this.resetTabindex(this.menuItems[0]);
  136. this.menuItems[0].focus();
  137. ev.preventDefault();
  138. ev.stopPropagation();
  139. } else if (keyCode === 13) { // space enter选中
  140. this.handleClick();
  141. } else if ([9, 27].indexOf(keyCode) > -1) { // tab || esc
  142. this.hide();
  143. }
  144. },
  145. handleItemKeyDown(ev) {
  146. const keyCode = ev.keyCode;
  147. const target = ev.target;
  148. const currentIndex = this.menuItemsArray.indexOf(target);
  149. const max = this.menuItemsArray.length - 1;
  150. let nextIndex;
  151. if ([38, 40].indexOf(keyCode) > -1) { // up/down
  152. if (keyCode === 38) { // up
  153. nextIndex = currentIndex !== 0 ? currentIndex - 1 : 0;
  154. } else { // down
  155. nextIndex = currentIndex < max ? currentIndex + 1 : max;
  156. }
  157. this.removeTabindex();
  158. this.resetTabindex(this.menuItems[nextIndex]);
  159. this.menuItems[nextIndex].focus();
  160. ev.preventDefault();
  161. ev.stopPropagation();
  162. } else if (keyCode === 13) { // enter选中
  163. this.triggerElmFocus();
  164. target.click();
  165. if (this.hideOnClick) { // click关闭
  166. this.visible = false;
  167. }
  168. } else if ([9, 27].indexOf(keyCode) > -1) { // tab // esc
  169. this.hide();
  170. this.triggerElmFocus();
  171. }
  172. },
  173. resetTabindex(ele) { // 下次tab时组件聚焦元素
  174. this.removeTabindex();
  175. ele.setAttribute('tabindex', '0'); // 下次期望的聚焦元素
  176. },
  177. removeTabindex() {
  178. this.triggerElm.setAttribute('tabindex', '-1');
  179. this.menuItemsArray.forEach((item) => {
  180. item.setAttribute('tabindex', '-1');
  181. });
  182. },
  183. initAria() {
  184. this.dropdownElm.setAttribute('id', this.listId);
  185. this.triggerElm.setAttribute('aria-haspopup', 'list');
  186. this.triggerElm.setAttribute('aria-controls', this.listId);
  187. if (!this.splitButton) { // 自定义
  188. this.triggerElm.setAttribute('role', 'button');
  189. this.triggerElm.setAttribute('tabindex', this.tabindex);
  190. this.triggerElm.setAttribute('class', (this.triggerElm.getAttribute('class') || '') + ' el-dropdown-selfdefine'); // 控制
  191. }
  192. },
  193. initEvent() {
  194. let { trigger, show, hide, handleClick, splitButton, handleTriggerKeyDown, handleItemKeyDown } = this;
  195. this.triggerElm = splitButton
  196. ? this.$refs.trigger.$el
  197. : this.$slots.default[0].elm;
  198. let dropdownElm = this.dropdownElm;
  199. this.triggerElm.addEventListener('keydown', handleTriggerKeyDown); // triggerElm keydown
  200. dropdownElm.addEventListener('keydown', handleItemKeyDown, true); // item keydown
  201. // 控制自定义元素的样式
  202. if (!splitButton) {
  203. this.triggerElm.addEventListener('focus', () => {
  204. this.focusing = true;
  205. });
  206. this.triggerElm.addEventListener('blur', () => {
  207. this.focusing = false;
  208. });
  209. this.triggerElm.addEventListener('click', () => {
  210. this.focusing = false;
  211. });
  212. }
  213. if (trigger === 'hover') {
  214. this.triggerElm.addEventListener('mouseenter', show);
  215. this.triggerElm.addEventListener('mouseleave', hide);
  216. dropdownElm.addEventListener('mouseenter', show);
  217. dropdownElm.addEventListener('mouseleave', hide);
  218. } else if (trigger === 'click') {
  219. this.triggerElm.addEventListener('click', handleClick);
  220. }
  221. },
  222. handleMenuItemClick(command, instance) {
  223. if (this.hideOnClick) {
  224. this.visible = false;
  225. }
  226. this.$emit('command', command, instance);
  227. },
  228. triggerElmFocus() {
  229. this.triggerElm.focus && this.triggerElm.focus();
  230. },
  231. initDomOperation() {
  232. this.dropdownElm = this.popperElm;
  233. this.menuItems = this.dropdownElm.querySelectorAll("[tabindex='-1']");
  234. this.menuItemsArray = [].slice.call(this.menuItems);
  235. this.initEvent();
  236. this.initAria();
  237. }
  238. },
  239. render(h) {
  240. let { hide, splitButton, type, dropdownSize, disabled } = this;
  241. const handleMainButtonClick = (event) => {
  242. this.$emit('click', event);
  243. hide();
  244. };
  245. let triggerElm = null;
  246. if (splitButton) {
  247. triggerElm = <el-button-group>
  248. <el-button type={type} size={dropdownSize} nativeOn-click={handleMainButtonClick} disabled={disabled}>
  249. {this.$slots.default}
  250. </el-button>
  251. <el-button ref="trigger" type={type} size={dropdownSize} class="el-dropdown__caret-button" disabled={disabled}>
  252. <i class="el-dropdown__icon el-icon-arrow-down"></i>
  253. </el-button>
  254. </el-button-group>;
  255. } else {
  256. triggerElm = this.$slots.default;
  257. const vnodeData = triggerElm[0].data || {};
  258. let { attrs = {} } = vnodeData;
  259. if (disabled && !attrs.disabled) {
  260. attrs.disabled = true;
  261. vnodeData.attrs = attrs;
  262. }
  263. }
  264. const menuElm = disabled ? null : this.$slots.dropdown;
  265. return (
  266. <div class="el-dropdown" v-clickoutside={hide} aria-disabled={disabled}>
  267. {triggerElm}
  268. {menuElm}
  269. </div>
  270. );
  271. }
  272. };
  273. </script>