fix: menu

pull/4134/head
tangjinzhou 2021-05-23 22:40:42 +08:00
parent 67952ab5a6
commit 7b494fd445
7 changed files with 185 additions and 105 deletions

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

@ -1,6 +1,6 @@
import { defineComponent, inject } from 'vue'; import { defineComponent, inject } from 'vue';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import isMobile from '../vc-menu/utils/isMobile'; import isMobile from '../_util/isMobile';
import Input from './Input'; import Input from './Input';
import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined';
import SearchOutlined from '@ant-design/icons-vue/SearchOutlined'; import SearchOutlined from '@ant-design/icons-vue/SearchOutlined';

View File

@ -14,6 +14,11 @@ Menu.install = function(app: App) {
return app; return app;
}; };
Menu.Item = MenuItem;
Menu.Divider = Divider;
Menu.SubMenu = SubMenu;
Menu.ItemGroup = ItemGroup;
export default Menu as typeof Menu & export default Menu as typeof Menu &
Plugin & { Plugin & {
readonly Item: typeof MenuItem; readonly Item: typeof MenuItem;

View File

@ -146,10 +146,10 @@ export default defineComponent({
...info, ...info,
selectedKeys: newSelectedKeys, selectedKeys: newSelectedKeys,
}; };
if (!shallowEqual(newSelectedKeys, mergedSelectedKeys.value)) {
if (!('selectedKeys' in props)) { if (!('selectedKeys' in props)) {
mergedSelectedKeys.value = newSelectedKeys; mergedSelectedKeys.value = newSelectedKeys;
} }
if (!shallowEqual(newSelectedKeys, mergedSelectedKeys.value)) {
emit('update:selectedKeys', newSelectedKeys); emit('update:selectedKeys', newSelectedKeys);
if (exist && props.multiple) { if (exist && props.multiple) {
emit('deselect', selectInfo); emit('deselect', selectInfo);
@ -266,6 +266,10 @@ export default defineComponent({
triggerSelection(info); triggerSelection(info);
}; };
const onInternalKeyDown = (e: KeyboardEvent) => {
console.log('onInternalKeyDown', e);
};
const onInternalOpenChange = (eventKey: Key, open: boolean) => { const onInternalOpenChange = (eventKey: Key, open: boolean) => {
const { key, childrenEventKeys } = store[eventKey]; const { key, childrenEventKeys } = store[eventKey];
let newOpenKeys = mergedOpenKeys.value.filter(k => k !== key); let newOpenKeys = mergedOpenKeys.value.filter(k => k !== key);
@ -322,7 +326,11 @@ export default defineComponent({
isRootMenu: true, isRootMenu: true,
}); });
return () => { return () => {
return <ul class={className.value}>{slots.default?.()}</ul>; return (
<ul class={className.value} tabindex="0" onKeydown={onInternalKeyDown}>
{slots.default?.()}
</ul>
);
}; };
}, },
}); });

View File

@ -6,6 +6,7 @@ import { useInjectFirstLevel, useInjectMenu } from './hooks/useMenuContext';
import { cloneElement } from '../../_util/vnode'; import { cloneElement } from '../../_util/vnode';
import Tooltip from '../../tooltip'; import Tooltip from '../../tooltip';
import { MenuInfo } from './interface'; import { MenuInfo } from './interface';
import KeyCode from 'ant-design-vue/es/_util/KeyCode';
let indexGuid = 0; let indexGuid = 0;
@ -18,7 +19,7 @@ export default defineComponent({
title: { type: [String, Boolean], default: undefined }, title: { type: [String, Boolean], default: undefined },
icon: PropTypes.VNodeChild, icon: PropTypes.VNodeChild,
}, },
emits: ['mouseenter', 'mouseleave', 'click'], emits: ['mouseenter', 'mouseleave', 'click', 'keydown', 'focus'],
slots: ['icon'], slots: ['icon'],
inheritAttrs: false, inheritAttrs: false,
setup(props, { slots, emit, attrs }) { setup(props, { slots, emit, attrs }) {
@ -80,7 +81,7 @@ export default defineComponent({
}; };
}); });
const getEventInfo = (e: MouseEvent): MenuInfo => { const getEventInfo = (e: MouseEvent | KeyboardEvent): MenuInfo => {
return { return {
key: key, key: key,
eventKey: eventKey, eventKey: eventKey,
@ -104,7 +105,6 @@ export default defineComponent({
const onMouseEnter = (event: MouseEvent) => { const onMouseEnter = (event: MouseEvent) => {
if (!mergedDisabled.value) { if (!mergedDisabled.value) {
changeActiveKeys(keysPath.value); changeActiveKeys(keysPath.value);
console.log('item mouseenter', keysPath.value);
emit('mouseenter', event); emit('mouseenter', event);
} }
}; };
@ -115,6 +115,27 @@ export default defineComponent({
} }
}; };
const onInternalKeyDown = (e: KeyboardEvent) => {
emit('keydown', e);
if (e.which === KeyCode.ENTER) {
const info = getEventInfo(e);
// Legacy. Key will also trigger click event
emit('click', e);
onItemClick(info);
}
};
/**
* Used for accessibility. Helper will focus element without key board.
* We should manually trigger an active
*/
const onInternalFocus = (e: FocusEvent) => {
changeActiveKeys(keysPath.value);
emit('focus', e);
};
const renderItemChildren = (icon: any, children: any) => { const renderItemChildren = (icon: any, children: any) => {
// inline-collapsed.md demo span , icon span // inline-collapsed.md demo span , icon span
// ref: https://github.com/ant-design/ant-design/pull/23456 // ref: https://github.com/ant-design/ant-design/pull/23456
@ -182,6 +203,8 @@ export default defineComponent({
onMouseenter={onMouseEnter} onMouseenter={onMouseEnter}
onMouseleave={onMouseLeave} onMouseleave={onMouseLeave}
onClick={onInternalClick} onClick={onInternalClick}
onKeydown={onInternalKeyDown}
onFocus={onInternalFocus}
title={typeof title === 'string' ? title : undefined} title={typeof title === 'string' ? title : undefined}
> >
{cloneElement(icon, { {cloneElement(icon, {

View File

@ -1,8 +1,10 @@
import Menu, { MenuItem } from '../../vc-menu'; import Menu from '../../menu';
import PropTypes from '../../_util/vue-types'; import PropTypes from '../../_util/vue-types';
import { OptionProps } from './Option'; import { OptionProps } from './Option';
import { inject } from 'vue'; import { inject } from 'vue';
const MenuItem = Menu.Item;
function noop() {} function noop() {}
export default { export default {
name: 'DropdownMenu', name: 'DropdownMenu',

View File

@ -1,107 +1,39 @@
<template> <template>
<div style="width: 256px"> <div>
<a-button type="primary" style="margin-bottom: 16px" @click="toggleCollapsed"> <demo />
<MenuUnfoldOutlined v-if="collapsed" />
<MenuFoldOutlined v-else />
</a-button>
<a-menu
v-model:openKeys="openKeys"
v-model:selectedKeys="selectedKeys"
mode="inline"
theme="dark"
:inline-collapsed="collapsed"
>
<!-- <a-menu-item key="1">
<template #icon>
<PieChartOutlined />
</template>
<span>Option 1</span>
</a-menu-item>
<a-menu-item key="2">
<template #icon>
<DesktopOutlined />
</template>
<span>Option 2</span>
</a-menu-item>
<a-menu-item key="3">
<template #icon>
<InboxOutlined />
</template>
<span>Option 3</span>
</a-menu-item>
<a-sub-menu key="sub1">
<template #title>
<span>
<MailOutlined />
<span>Navigation One</span>
</span>
</template>
<a-menu-item key="5">Option 5</a-menu-item>
<a-menu-item key="6">Option 6</a-menu-item>
<a-menu-item key="7">Option 7</a-menu-item>
<a-menu-item key="8">Option 8</a-menu-item>
</a-sub-menu> -->
<a-sub-menu key="sub2">
<template #title>
<span>
<AppstoreOutlined />
<span>Navigation Two</span>
</span>
</template>
<a-menu-item key="9">Option 9</a-menu-item>
<a-menu-item key="10">Option 10</a-menu-item>
<a-sub-menu key="sub3" title="Submenu">
<a-menu-item key="11">Option 11</a-menu-item>
<a-menu-item key="12">Option 12</a-menu-item>
</a-sub-menu>
</a-sub-menu>
</a-menu>
</div> </div>
</template> </template>
<script lang="ts"> <script>
import { defineComponent, reactive, toRefs, watch } from 'vue'; import { defineComponent } from 'vue';
import { import demo from '../v2-doc/src/docs/mentions/demo/index.vue';
MenuFoldOutlined, // import Affix from '../components/affix';
MenuUnfoldOutlined,
PieChartOutlined,
MailOutlined,
DesktopOutlined,
InboxOutlined,
AppstoreOutlined,
} from '@ant-design/icons-vue';
export default defineComponent({ export default defineComponent({
components: { components: {
MenuFoldOutlined, demo,
MenuUnfoldOutlined, // Affix,
PieChartOutlined,
MailOutlined,
DesktopOutlined,
InboxOutlined,
AppstoreOutlined,
}, },
setup() { data() {
const state = reactive({
collapsed: false,
selectedKeys: ['1'],
openKeys: ['sub1'],
preOpenKeys: ['sub1'],
});
watch(
() => state.openKeys,
(val, oldVal) => {
state.preOpenKeys = oldVal;
},
);
const toggleCollapsed = () => {
state.collapsed = !state.collapsed;
// state.openKeys = state.collapsed ? [] : state.preOpenKeys;
};
return { return {
...toRefs(state), visible: false,
toggleCollapsed, pStyle: {
fontSize: '16px',
color: 'rgba(0,0,0,0.85)',
lineHeight: '24px',
display: 'block',
marginBottom: '16px',
},
pStyle2: {
marginBottom: '24px',
},
}; };
}, },
methods: {
showDrawer() {
this.visible = true;
},
onClose() {
this.visible = false;
},
},
}); });
</script> </script>