feat: update menu

pull/1845/head
tangjinzhou 2020-02-21 20:59:02 +08:00
parent c7b0cb0732
commit b85bc0e738
17 changed files with 256 additions and 51 deletions

View File

@ -1,5 +1,5 @@
module.exports = {
dev: {
componentName: 'list', // dev components
componentName: 'menu', // dev components
},
};

View File

@ -8,8 +8,9 @@ export default {
props: itemProps,
inject: {
getInlineCollapsed: { default: () => noop },
layoutSiderContext: { default: () => ({}) },
},
isMenuItem: 1,
isMenuItem: true,
methods: {
onKeyDown(e) {
this.$refs.menuItem.onKeyDown(e);
@ -20,22 +21,28 @@ export default {
const { level, title, rootPrefixCls } = props;
const { getInlineCollapsed, $slots, $attrs: attrs } = this;
const inlineCollapsed = getInlineCollapsed();
let titleNode;
if (inlineCollapsed) {
titleNode = title || (level === 1 ? $slots.default : '');
const tooltipProps = {
title: title || (level === 1 ? $slots.default : ''),
};
const siderCollapsed = this.layoutSiderContext.sCollapsed;
if (!siderCollapsed && !inlineCollapsed) {
tooltipProps.title = null;
// Reset `visible` to fix control mode tooltip display not correct
// ref: https://github.com/ant-design/ant-design/issues/16742
tooltipProps.visible = false;
}
const itemProps = {
props: {
...props,
title: inlineCollapsed ? null : title,
title,
},
attrs,
on: getListeners(this),
};
const toolTipProps = {
props: {
title: titleNode,
...tooltipProps,
placement: 'right',
overlayClassName: `${rootPrefixCls}-inline-collapsed-tooltip`,
},

View File

@ -0,0 +1,42 @@
import { SubMenu as VcSubMenu } from '../vc-menu';
import { getListeners } from '../_util/props-util';
import classNames from 'classnames';
export default {
name: 'ASubMenu',
isSubMenu: true,
props: { ...VcSubMenu.props },
inject: {
menuPropsContext: { default: () => ({}) },
},
methods: {
onKeyDown(e) {
this.$refs.subMenu.onKeyDown(e);
},
},
render() {
const { $slots, $scopedSlots } = this;
const { rootPrefixCls, popupClassName } = this.$props;
const { theme: antdMenuTheme } = this.menuPropsContext;
const props = {
props: {
...this.$props,
popupClassName: classNames(`${rootPrefixCls}-${antdMenuTheme}`, popupClassName),
},
ref: 'subMenu',
on: getListeners(this),
scopedSlots: $scopedSlots,
};
const slotsKey = Object.keys($slots);
return (
<VcSubMenu {...props}>
{slotsKey.length
? slotsKey.map(name => {
return <template slot={name}>{$slots[name]}</template>;
})
: null}
</VcSubMenu>
);
},
};

View File

@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils';
import { asyncExpect } from '@/tests/utils';
import Menu from '..';
import Icon from '../../icon';
import mountTest from '../../../tests/shared/mountTest';
jest.mock('mutationobserver-shim', () => {
global.MutationObserver = function MutationObserver() {
@ -15,6 +16,17 @@ function $$(className) {
return document.body.querySelectorAll(className);
}
describe('Menu', () => {
mountTest({
render() {
return (
<Menu>
<Menu.Item />
<Menu.ItemGroup />
<Menu.SubMenu />
</Menu>
);
},
});
beforeEach(() => {
document.body.innerHTML = '';
// jest.useFakeTimers()

View File

@ -16,9 +16,13 @@ const md = {
导航菜单是一个网站的灵魂用户依赖导航在各个页面中进行跳转一般分为顶部导航和侧边导航顶部导航提供全局性的类目和功能侧边导航提供多级结构来收纳和排列网站架构
## 代码演示`,
us: `# Menu
Menu list of Navigation.
A versatile menu for navigation.
## When To Use
Navigation menu is important for a website, it helps users jump from one site section to another quickly. Mostly, it includes top navigation and side navigation. Top navigation provides all the category and functions of the website. Side navigation provides the Multi-level structure of the website.
Navigation is an important part of any website, as a good navigation setup allows users to move around the site quickly and efficiently. Ant Design offers top and side navigation options. Top navigation provides all the categories and functions of the website. Side navigation provides the multi-level structure of the website.
More layouts with navigation: [Layout](/components/layout).
## Examples`,
};
export default {

View File

@ -50,11 +50,12 @@
### Menu.SubMenu
| Param | Description | Type | Default value |
| -------- | ----------------------------------- | ------------ | ------------- |
| disabled | whether sub menu is disabled or not | boolean | false |
| key | unique id of the sub menu | string | |
| title | title of the sub menu | string\|slot | |
| Param | Description | Type | Default value | Version |
| -------------- | ----------------------------------- | ------------ | ------------- | ------- |
| popupClassName | Sub-menu class name | string | | 1.5.0 |
| disabled | whether sub menu is disabled or not | boolean | false | |
| key | Unique ID of the sub menu | string | | |
| title | title of the sub menu | string\|slot | | |
The children of Menu.SubMenu must be `MenuItem` or `SubMenu`.
@ -68,7 +69,7 @@ The children of Menu.SubMenu must be `MenuItem` or `SubMenu`.
| Param | Description | Type | Default value |
| -------- | ------------------ | ------------ | ------------- |
| children | sub menu items | MenuItem\[] | |
| children | sub-menu items | MenuItem\[] | |
| title | title of the group | string\|slot | |
The children of Menu.ItemGroup must be `MenuItem`.

View File

@ -1,14 +1,16 @@
import omit from 'omit.js';
import VcMenu, { Divider, ItemGroup, SubMenu } from '../vc-menu';
import VcMenu, { Divider, ItemGroup } from '../vc-menu';
import SubMenu from './SubMenu';
import PropTypes from '../_util/vue-types';
import animation from '../_util/openAnimation';
import warning from '../_util/warning';
import Item from './MenuItem';
import { hasProp, getListeners } from '../_util/props-util';
import { hasProp, getListeners, getOptionProps } from '../_util/props-util';
import BaseMixin from '../_util/BaseMixin';
import commonPropsType from '../vc-menu/commonPropsType';
import { ConfigConsumerProps } from '../config-provider';
import Base from '../base';
// import raf from '../_util/raf';
export const MenuMode = PropTypes.oneOf([
'vertical',
@ -47,6 +49,7 @@ const Menu = {
provide() {
return {
getInlineCollapsed: this.getInlineCollapsed,
menuPropsContext: this.$props,
};
},
mixins: [BaseMixin],
@ -58,12 +61,12 @@ const Menu = {
prop: 'selectedKeys',
event: 'selectChange',
},
created() {
this.preProps = { ...this.$props };
},
updated() {
this.propsUpdating = false;
},
// beforeDestroy() {
// raf.cancel(this.mountRafId);
// },
watch: {
mode(val, oldVal) {
if (oldVal === 'inline' && val !== 'inline') {
@ -81,9 +84,10 @@ const Menu = {
},
},
data() {
const props = this.$props;
const props = getOptionProps(this);
warning(
!(hasProp(this, 'inlineCollapsed') && props.mode !== 'inline'),
!('inlineCollapsed' in props && props.mode !== 'inline'),
'Menu',
"`inlineCollapsed` should only be used when Menu's `mode` is inline.",
);
this.switchingModeFromInline = false;
@ -91,9 +95,9 @@ const Menu = {
this.inlineOpenKeys = [];
let sOpenKeys;
if (hasProp(this, 'openKeys')) {
if ('openKeys' in props) {
sOpenKeys = props.openKeys;
} else if (hasProp(this, 'defaultOpenKeys')) {
} else if ('defaultOpenKeys' in props) {
sOpenKeys = props.defaultOpenKeys;
}
return {
@ -137,10 +141,20 @@ const Menu = {
// when inlineCollapsed menu width animation finished
// https://github.com/ant-design/ant-design/issues/12864
const widthCollapsed = e.propertyName === 'width' && e.target === e.currentTarget;
// Fix SVGElement e.target.className.indexOf is not a function
// https://github.com/ant-design/ant-design/issues/15699
const { className } = e.target;
// SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during an animation.
const classNameValue =
Object.prototype.toString.call(className) === '[object SVGAnimatedString]'
? className.animVal
: className;
// Fix for <Menu style={{ width: '100%' }} />, the width transition won't trigger when menu is collapsed
// https://github.com/ant-design/ant-design-pro/issues/2783
const iconScaled =
e.propertyName === 'font-size' && e.target.className.indexOf('anticon') >= 0;
const iconScaled = e.propertyName === 'font-size' && classNameValue.indexOf('anticon') >= 0;
if (widthCollapsed || iconScaled) {
this.restoreModeVerticalFromInline();
}
@ -254,11 +268,11 @@ const Menu = {
}
// https://github.com/ant-design/ant-design/issues/8587
if (
const hideMenu =
this.getInlineCollapsed() &&
(collapsedWidth === 0 || collapsedWidth === '0' || collapsedWidth === '0px')
) {
return null;
(collapsedWidth === 0 || collapsedWidth === '0' || collapsedWidth === '0px');
if (hideMenu) {
menuProps.props.openKeys = [];
}
return (

View File

@ -49,11 +49,12 @@
### Menu.SubMenu
| 参数 | 说明 | 类型 | 默认值 |
| -------- | ---------- | ------------ | ------ |
| disabled | 是否禁用 | boolean | false |
| key | 唯一标志 | string | |
| title | 子菜单项值 | string\|slot | |
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| -------------- | ---------- | ------------ | ------ | ----- |
| popupClassName | 子菜单样式 | string | | 1.5.0 |
| disabled | 是否禁用 | boolean | false | |
| key | 唯一标志 | string | | |
| title | 子菜单项值 | string\|slot | | |
Menu.SubMenu 的子元素必须是 `MenuItem` 或者 `SubMenu`.

View File

@ -85,7 +85,7 @@ const DOMWrap = {
this.resizeObserver.disconnect();
}
if (this.mutationObserver) {
this.resizeObserver.disconnect();
this.mutationObserver.disconnect();
}
},
methods: {
@ -98,9 +98,9 @@ const DOMWrap = {
}
// filter out all overflowed indicator placeholder
return [].slice.call(ul.children).filter(node => {
return node.className.split(' ').indexOf(`${prefixCls}-overflowed-submenu`) < 0;
});
return [].slice
.call(ul.children)
.filter(node => node.className.split(' ').indexOf(`${prefixCls}-overflowed-submenu`) < 0);
},
getOverflowedSubMenuItem(keyPrefix, overflowedItems, renderPlaceholder) {
@ -111,10 +111,11 @@ const DOMWrap = {
// put all the overflowed item inside a submenu
// with a title of overflow indicator ('...')
const copy = this.$slots.default[0];
const { title, eventKey, ...rest } = getPropsData(copy); // eslint-disable-line no-unused-vars
const { title, ...rest } = getPropsData(copy); // eslint-disable-line no-unused-vars
let style = {};
let key = `${keyPrefix}-overflowed-indicator`;
let eventKey = `${keyPrefix}-overflowed-indicator`;
if (overflowedItems.length === 0 && renderPlaceholder !== true) {
style = {
@ -127,6 +128,7 @@ const DOMWrap = {
position: 'absolute',
};
key = `${key}-placeholder`;
eventKey = `${eventKey}-placeholder`;
}
const popupClassName = theme ? `${prefixCls}-${theme}` : '';
@ -141,7 +143,7 @@ const DOMWrap = {
title: overflowedIndicator,
popupClassName,
...props,
eventKey: `${keyPrefix}-overflowed-indicator`,
eventKey,
disabled: false,
},
class: `${prefixCls}-overflowed-submenu`,
@ -226,7 +228,7 @@ const DOMWrap = {
this.menuItemSizes.forEach(liWidth => {
currentSumWidth += liWidth;
if (currentSumWidth + this.overflowedIndicatorWidth <= width) {
lastVisibleIndex++;
lastVisibleIndex += 1;
}
});
}
@ -251,7 +253,7 @@ const DOMWrap = {
{
style: { display: 'none' },
props: { eventKey: `${eventKey}-hidden` },
class: { ...getClass(childNode), [MENUITEM_OVERFLOWED_CLASSNAME]: true },
class: MENUITEM_OVERFLOWED_CLASSNAME,
},
);
}

View File

@ -40,16 +40,22 @@ const MenuItem = {
mixins: [BaseMixin],
isMenuItem: true,
created() {
this.prevActive = this.active;
// invoke customized ref to expose component to mixin
this.callRef();
},
updated() {
this.$nextTick(() => {
if (this.active) {
const { active, parentMenu, eventKey } = this.$props;
if (!this.prevActive && active && (!parentMenu || !parentMenu[`scrolled-${eventKey}`])) {
scrollIntoView(this.$el, this.parentMenu.$el, {
onlyScrollIfNeeded: true,
});
parentMenu[`scrolled-${eventKey}`] = true;
} else if (parentMenu && parentMenu[`scrolled-${eventKey}`]) {
delete parentMenu[`scrolled-${eventKey}`];
}
this.prevActive = active;
});
this.callRef();
},

View File

@ -1,5 +1,6 @@
import PropTypes from '../_util/vue-types';
import { getComponentFromProp, getListeners } from '../_util/props-util';
// import { menuAllProps } from './util'
const MenuItemGroup = {

View File

@ -10,6 +10,7 @@ import { getComponentFromProp, filterEmpty, getListeners } from '../_util/props-
import { requestAnimationTimeout, cancelAnimationTimeout } from '../_util/requestAnimationTimeout';
import { noop, loopMenuItemRecursively, getMenuIdFromSubMenuEventKey } from './util';
import getTransitionProps from '../_util/getTransitionProps';
import { MenuItem } from './MenuItem';
let guid = 0;
@ -171,6 +172,7 @@ const SubMenu = {
if (isOpen && (keyCode === KeyCode.UP || keyCode === KeyCode.DOWN)) {
return menu.onKeyDown(e);
}
return undefined;
},
onPopupVisibleChange(visible) {
@ -368,7 +370,7 @@ const SubMenu = {
deselect,
openChange,
},
id: this._menuId,
id: this.internalMenuId,
};
const baseProps = subPopupMenuProps.props;
const haveRendered = this.haveRendered;
@ -429,11 +431,11 @@ const SubMenu = {
[this.getSelectedClassName()]: this.isChildrenSelected(),
};
if (!this._menuId) {
if (!this.internalMenuId) {
if (props.eventKey) {
this._menuId = `${props.eventKey}$Menu`;
this.internalMenuId = `${props.eventKey}$Menu`;
} else {
this._menuId = `$__$${++guid}$Menu`;
this.internalMenuId = `$__$${++guid}$Menu`;
}
}
@ -466,7 +468,7 @@ const SubMenu = {
// since corresponding node cannot be found
if (isOpen) {
ariaOwns = {
'aria-owns': this._menuId,
'aria-owns': this.internalMenuId,
};
}
const titleProps = {

View File

@ -193,6 +193,7 @@ const SubPopupMenu = {
return 1;
}
return undefined;
},
onItemHover(e) {

View File

@ -1,4 +1,4 @@
// based on rc-menu 7.4.21
// based on rc-menu 7.5.5
import Menu from './Menu';
import SubMenu from './SubMenu';
import MenuItem, { menuItemProps } from './MenuItem';

View File

@ -1,4 +1,4 @@
const isMobile = require('ismobilejs');
import isMobile from './utils/isMobile';
export function noop() {}

View File

@ -0,0 +1,110 @@
// MIT License from https://github.com/kaimallea/isMobile
const applePhone = /iPhone/i;
const appleIpod = /iPod/i;
const appleTablet = /iPad/i;
const androidPhone = /\bAndroid(?:.+)Mobile\b/i; // Match 'Android' AND 'Mobile'
const androidTablet = /Android/i;
const amazonPhone = /\bAndroid(?:.+)SD4930UR\b/i;
const amazonTablet = /\bAndroid(?:.+)(?:KF[A-Z]{2,4})\b/i;
const windowsPhone = /Windows Phone/i;
const windowsTablet = /\bWindows(?:.+)ARM\b/i; // Match 'Windows' AND 'ARM'
const otherBlackberry = /BlackBerry/i;
const otherBlackberry10 = /BB10/i;
const otherOpera = /Opera Mini/i;
const otherChrome = /\b(CriOS|Chrome)(?:.+)Mobile/i;
const otherFirefox = /Mobile(?:.+)Firefox\b/i; // Match 'Mobile' AND 'Firefox'
function match(regex, userAgent) {
return regex.test(userAgent);
}
function isMobile(userAgent) {
let ua = userAgent || (typeof navigator !== 'undefined' ? navigator.userAgent : '');
// Facebook mobile app's integrated browser adds a bunch of strings that
// match everything. Strip it out if it exists.
let tmp = ua.split('[FBAN');
if (typeof tmp[1] !== 'undefined') {
[ua] = tmp;
}
// Twitter mobile app's integrated browser on iPad adds a "Twitter for
// iPhone" string. Same probably happens on other tablet platforms.
// This will confuse detection so strip it out if it exists.
tmp = ua.split('Twitter');
if (typeof tmp[1] !== 'undefined') {
[ua] = tmp;
}
const result = {
apple: {
phone: match(applePhone, ua) && !match(windowsPhone, ua),
ipod: match(appleIpod, ua),
tablet: !match(applePhone, ua) && match(appleTablet, ua) && !match(windowsPhone, ua),
device:
(match(applePhone, ua) || match(appleIpod, ua) || match(appleTablet, ua)) &&
!match(windowsPhone, ua),
},
amazon: {
phone: match(amazonPhone, ua),
tablet: !match(amazonPhone, ua) && match(amazonTablet, ua),
device: match(amazonPhone, ua) || match(amazonTablet, ua),
},
android: {
phone:
(!match(windowsPhone, ua) && match(amazonPhone, ua)) ||
(!match(windowsPhone, ua) && match(androidPhone, ua)),
tablet:
!match(windowsPhone, ua) &&
!match(amazonPhone, ua) &&
!match(androidPhone, ua) &&
(match(amazonTablet, ua) || match(androidTablet, ua)),
device:
(!match(windowsPhone, ua) &&
(match(amazonPhone, ua) ||
match(amazonTablet, ua) ||
match(androidPhone, ua) ||
match(androidTablet, ua))) ||
match(/\bokhttp\b/i, ua),
},
windows: {
phone: match(windowsPhone, ua),
tablet: match(windowsTablet, ua),
device: match(windowsPhone, ua) || match(windowsTablet, ua),
},
other: {
blackberry: match(otherBlackberry, ua),
blackberry10: match(otherBlackberry10, ua),
opera: match(otherOpera, ua),
firefox: match(otherFirefox, ua),
chrome: match(otherChrome, ua),
device:
match(otherBlackberry, ua) ||
match(otherBlackberry10, ua) ||
match(otherOpera, ua) ||
match(otherFirefox, ua) ||
match(otherChrome, ua),
},
// Additional
any: null,
phone: null,
tablet: null,
};
result.any =
result.apple.device || result.android.device || result.windows.device || result.other.device;
// excludes 'other' devices and ipods, targeting touchscreen phones
result.phone = result.apple.phone || result.android.phone || result.windows.phone;
result.tablet = result.apple.tablet || result.android.tablet || result.windows.tablet;
return result;
}
const defaultResult = {
...isMobile(),
isMobile,
};
export default defaultResult;

View File

@ -24,4 +24,6 @@ export declare class SubMenu extends AntdComponent {
* @type string | slot
*/
title: any;
popupClassName: string;
}