|
@@ -1,6 +1,7 @@
|
|
|
<script>
|
|
|
import { isDef } from 'element-ui/src/utils/shared';
|
|
|
import scrollIntoView from 'element-ui/src/utils/scroll-into-view';
|
|
|
+ import { generateId } from 'element-ui/src/utils/util';
|
|
|
|
|
|
const copyArray = (arr, props) => {
|
|
|
if (!arr || !Array.isArray(arr) || !props) return arr;
|
|
@@ -39,7 +40,8 @@
|
|
|
value: [],
|
|
|
expandTrigger: 'click',
|
|
|
changeOnSelect: false,
|
|
|
- popperClass: ''
|
|
|
+ popperClass: '',
|
|
|
+ hoverTimer: 0
|
|
|
};
|
|
|
},
|
|
|
|
|
@@ -94,6 +96,9 @@
|
|
|
formatOptions(optionsCopy);
|
|
|
return loadActiveOptions(optionsCopy);
|
|
|
}
|
|
|
+ },
|
|
|
+ id() {
|
|
|
+ return generateId();
|
|
|
}
|
|
|
},
|
|
|
|
|
@@ -135,11 +140,41 @@
|
|
|
activeOptions,
|
|
|
visible,
|
|
|
expandTrigger,
|
|
|
- popperClass
|
|
|
+ popperClass,
|
|
|
+ hoverThreshold
|
|
|
} = this;
|
|
|
+ let itemId = null;
|
|
|
+ let itemIndex = 0;
|
|
|
+
|
|
|
+ let hoverMenuRefs = {};
|
|
|
+ const hoverMenuHandler = e => {
|
|
|
+ const offsetX = e.offsetX;
|
|
|
+ const width = hoverMenuRefs.activeMenu.offsetWidth;
|
|
|
+ const height = hoverMenuRefs.activeMenu.offsetHeight;
|
|
|
+
|
|
|
+ if (e.target === hoverMenuRefs.activeItem) {
|
|
|
+ clearTimeout(this.hoverTimer);
|
|
|
+ const {activeItem} = hoverMenuRefs;
|
|
|
+ const offsetY_top = activeItem.offsetTop;
|
|
|
+ const offsetY_Bottom = offsetY_top + activeItem.offsetHeight;
|
|
|
+
|
|
|
+ hoverMenuRefs.hoverZone.innerHTML = `
|
|
|
+ <path style="pointer-events: auto;" fill="transparent" d="M${offsetX} ${offsetY_top} L${width} 0 V${offsetY_top} Z" />
|
|
|
+ <path style="pointer-events: auto;" fill="transparent" d="M${offsetX} ${offsetY_Bottom} L${width} ${height} V${offsetY_Bottom} Z" />
|
|
|
+ `;
|
|
|
+ } else {
|
|
|
+ if (!this.hoverTimer) {
|
|
|
+ this.hoverTimer = setTimeout(() => {
|
|
|
+ hoverMenuRefs.hoverZone.innerHTML = '';
|
|
|
+ }, hoverThreshold);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
const menus = this._l(activeOptions, (menu, menuIndex) => {
|
|
|
let isFlat = false;
|
|
|
+ const menuId = `menu-${this.id}-${ menuIndex}`;
|
|
|
+ const ownsId = `menu-${this.id}-${ menuIndex + 1 }`;
|
|
|
const items = this._l(menu, item => {
|
|
|
const events = {
|
|
|
on: {}
|
|
@@ -148,12 +183,52 @@
|
|
|
if (item.__IS__FLAT__OPTIONS) isFlat = true;
|
|
|
|
|
|
if (!item.disabled) {
|
|
|
+ // keydown up/down/left/right/enter
|
|
|
+ events.on.keydown = (ev) => {
|
|
|
+ const keyCode = ev.keyCode;
|
|
|
+ if (![37, 38, 39, 40, 13, 9, 27].indexOf(keyCode) > -1) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const currentEle = ev.target;
|
|
|
+ const parentEle = this.$refs.menus[menuIndex];
|
|
|
+ const menuItemList = parentEle.querySelectorAll("[tabindex='-1']");
|
|
|
+ const currentIndex = Array.prototype.indexOf.call(menuItemList, currentEle); // 当前索引
|
|
|
+ let nextIndex, nextMenu;
|
|
|
+ if ([38, 40].indexOf(keyCode) > -1) {
|
|
|
+ if (keyCode === 38) { // up键
|
|
|
+ nextIndex = currentIndex !== 0 ? (currentIndex - 1) : currentIndex;
|
|
|
+ } else if (keyCode === 40) { // down
|
|
|
+ nextIndex = currentIndex !== (menuItemList.length - 1) ? currentIndex + 1 : currentIndex;
|
|
|
+ }
|
|
|
+ menuItemList[nextIndex].focus();
|
|
|
+ } else if (keyCode === 37) { // left键
|
|
|
+ if (menuIndex !== 0) {
|
|
|
+ const previousMenu = this.$refs.menus[menuIndex - 1];
|
|
|
+ previousMenu.querySelector('[aria-expanded=true]').focus();
|
|
|
+ }
|
|
|
+ } else if (keyCode === 39) { // right
|
|
|
+ if (item.children) {
|
|
|
+ // 有子menu 选择子menu的第一个menuitem
|
|
|
+ nextMenu = this.$refs.menus[menuIndex + 1];
|
|
|
+ nextMenu.querySelectorAll("[tabindex='-1']")[0].focus();
|
|
|
+ }
|
|
|
+ } else if (keyCode === 13) {
|
|
|
+ if (!item.children) {
|
|
|
+ const id = currentEle.getAttribute('id');
|
|
|
+ parentEle.setAttribute('aria-activedescendant', id);
|
|
|
+ this.select(item, menuIndex);
|
|
|
+ this.$nextTick(() => this.scrollMenu(this.$refs.menus[menuIndex]));
|
|
|
+ }
|
|
|
+ } else if (keyCode === 9 || keyCode === 27) { // esc tab
|
|
|
+ this.$emit('closeInside');
|
|
|
+ }
|
|
|
+ };
|
|
|
if (item.children) {
|
|
|
let triggerEvent = {
|
|
|
click: 'click',
|
|
|
hover: 'mouseenter'
|
|
|
}[expandTrigger];
|
|
|
- events.on[triggerEvent] = () => {
|
|
|
+ events.on[triggerEvent] = events.on['focus'] = () => { // focus 选中
|
|
|
this.activeItem(item, menuIndex);
|
|
|
this.$nextTick(() => {
|
|
|
// adjust self and next level
|
|
@@ -168,7 +243,10 @@
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+ if (!item.disabled && !item.children) { // no children set id
|
|
|
+ itemId = `${menuId}-${itemIndex}`;
|
|
|
+ itemIndex++;
|
|
|
+ }
|
|
|
return (
|
|
|
<li
|
|
|
class={{
|
|
@@ -177,7 +255,14 @@
|
|
|
'is-active': item.value === activeValue[menuIndex],
|
|
|
'is-disabled': item.disabled
|
|
|
}}
|
|
|
+ ref={item.value === activeValue[menuIndex] ? 'activeItem' : null}
|
|
|
{...events}
|
|
|
+ tabindex= { item.disabled ? null : -1 }
|
|
|
+ role="menuitem"
|
|
|
+ aria-haspopup={ !!item.children }
|
|
|
+ aria-expanded={ item.value === activeValue[menuIndex] }
|
|
|
+ id = { itemId }
|
|
|
+ aria-owns = { !item.children ? null : ownsId }
|
|
|
>
|
|
|
{item.label}
|
|
|
</li>
|
|
@@ -188,19 +273,68 @@
|
|
|
menuStyle.minWidth = this.inputWidth + 'px';
|
|
|
}
|
|
|
|
|
|
+ const isHoveredMenu = expandTrigger === 'hover' && activeValue.length - 1 === menuIndex;
|
|
|
+ const hoverMenuEvent = {
|
|
|
+ on: {
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ if (isHoveredMenu) {
|
|
|
+ hoverMenuEvent.on.mousemove = hoverMenuHandler;
|
|
|
+ menuStyle.position = 'relative';
|
|
|
+ }
|
|
|
+
|
|
|
return (
|
|
|
<ul
|
|
|
class={{
|
|
|
'el-cascader-menu': true,
|
|
|
'el-cascader-menu--flexible': isFlat
|
|
|
}}
|
|
|
+ {...hoverMenuEvent}
|
|
|
style={menuStyle}
|
|
|
refInFor
|
|
|
- ref="menus">
|
|
|
+ ref="menus"
|
|
|
+ role="menu"
|
|
|
+ id = { menuId }
|
|
|
+ >
|
|
|
{items}
|
|
|
+ {
|
|
|
+ isHoveredMenu
|
|
|
+ ? (<svg
|
|
|
+ ref="hoverZone"
|
|
|
+ style={{
|
|
|
+ position: 'absolute',
|
|
|
+ top: 0,
|
|
|
+ height: '100%',
|
|
|
+ width: '100%',
|
|
|
+ left: 0,
|
|
|
+ pointerEvents: 'none'
|
|
|
+ }}
|
|
|
+ ></svg>) : null
|
|
|
+ }
|
|
|
</ul>
|
|
|
);
|
|
|
});
|
|
|
+
|
|
|
+ if (expandTrigger === 'hover') {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ const activeItem = this.$refs.activeItem;
|
|
|
+
|
|
|
+ if (activeItem) {
|
|
|
+ const activeMenu = activeItem.parentElement;
|
|
|
+ const hoverZone = this.$refs.hoverZone;
|
|
|
+
|
|
|
+ hoverMenuRefs = {
|
|
|
+ activeMenu,
|
|
|
+ activeItem,
|
|
|
+ hoverZone
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ hoverMenuRefs = {};
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
return (
|
|
|
<transition name="el-zoom-in-top" on-before-enter={this.handleMenuEnter} on-after-leave={this.handleMenuLeave}>
|
|
|
<div
|