From 7b494fd44575a41cda4ea7dd182649d04fbb93b9 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Sun, 23 May 2021 22:40:42 +0800 Subject: [PATCH] fix: menu --- components/_util/isMobile.js | 110 +++++++++++++++++ components/input/Search.tsx | 2 +- components/menu/index.tsx | 5 + components/menu/src/Menu.tsx | 16 ++- components/menu/src/MenuItem.tsx | 29 ++++- components/vc-mentions/src/DropdownMenu.jsx | 4 +- examples/App.vue | 124 +++++--------------- 7 files changed, 185 insertions(+), 105 deletions(-) create mode 100644 components/_util/isMobile.js diff --git a/components/_util/isMobile.js b/components/_util/isMobile.js new file mode 100644 index 000000000..96927fd3b --- /dev/null +++ b/components/_util/isMobile.js @@ -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; diff --git a/components/input/Search.tsx b/components/input/Search.tsx index b13b741e7..77388c695 100644 --- a/components/input/Search.tsx +++ b/components/input/Search.tsx @@ -1,6 +1,6 @@ import { defineComponent, inject } from 'vue'; import classNames from '../_util/classNames'; -import isMobile from '../vc-menu/utils/isMobile'; +import isMobile from '../_util/isMobile'; import Input from './Input'; import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; import SearchOutlined from '@ant-design/icons-vue/SearchOutlined'; diff --git a/components/menu/index.tsx b/components/menu/index.tsx index 120eed074..fa839d6b1 100644 --- a/components/menu/index.tsx +++ b/components/menu/index.tsx @@ -14,6 +14,11 @@ Menu.install = function(app: App) { return app; }; +Menu.Item = MenuItem; +Menu.Divider = Divider; +Menu.SubMenu = SubMenu; +Menu.ItemGroup = ItemGroup; + export default Menu as typeof Menu & Plugin & { readonly Item: typeof MenuItem; diff --git a/components/menu/src/Menu.tsx b/components/menu/src/Menu.tsx index 6582e57d2..4f619ad38 100644 --- a/components/menu/src/Menu.tsx +++ b/components/menu/src/Menu.tsx @@ -146,10 +146,10 @@ export default defineComponent({ ...info, selectedKeys: newSelectedKeys, }; - if (!('selectedKeys' in props)) { - mergedSelectedKeys.value = newSelectedKeys; - } if (!shallowEqual(newSelectedKeys, mergedSelectedKeys.value)) { + if (!('selectedKeys' in props)) { + mergedSelectedKeys.value = newSelectedKeys; + } emit('update:selectedKeys', newSelectedKeys); if (exist && props.multiple) { emit('deselect', selectInfo); @@ -266,6 +266,10 @@ export default defineComponent({ triggerSelection(info); }; + const onInternalKeyDown = (e: KeyboardEvent) => { + console.log('onInternalKeyDown', e); + }; + const onInternalOpenChange = (eventKey: Key, open: boolean) => { const { key, childrenEventKeys } = store[eventKey]; let newOpenKeys = mergedOpenKeys.value.filter(k => k !== key); @@ -322,7 +326,11 @@ export default defineComponent({ isRootMenu: true, }); return () => { - return ; + return ( + + ); }; }, }); diff --git a/components/menu/src/MenuItem.tsx b/components/menu/src/MenuItem.tsx index 0221194a7..fb0368eea 100644 --- a/components/menu/src/MenuItem.tsx +++ b/components/menu/src/MenuItem.tsx @@ -6,6 +6,7 @@ import { useInjectFirstLevel, useInjectMenu } from './hooks/useMenuContext'; import { cloneElement } from '../../_util/vnode'; import Tooltip from '../../tooltip'; import { MenuInfo } from './interface'; +import KeyCode from 'ant-design-vue/es/_util/KeyCode'; let indexGuid = 0; @@ -18,7 +19,7 @@ export default defineComponent({ title: { type: [String, Boolean], default: undefined }, icon: PropTypes.VNodeChild, }, - emits: ['mouseenter', 'mouseleave', 'click'], + emits: ['mouseenter', 'mouseleave', 'click', 'keydown', 'focus'], slots: ['icon'], inheritAttrs: false, setup(props, { slots, emit, attrs }) { @@ -80,7 +81,7 @@ export default defineComponent({ }; }); - const getEventInfo = (e: MouseEvent): MenuInfo => { + const getEventInfo = (e: MouseEvent | KeyboardEvent): MenuInfo => { return { key: key, eventKey: eventKey, @@ -104,7 +105,6 @@ export default defineComponent({ const onMouseEnter = (event: MouseEvent) => { if (!mergedDisabled.value) { changeActiveKeys(keysPath.value); - console.log('item mouseenter', keysPath.value); 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) => { // inline-collapsed.md demo 依赖 span 来隐藏文字,有 icon 属性,则内部包裹一个 span // ref: https://github.com/ant-design/ant-design/pull/23456 @@ -182,6 +203,8 @@ export default defineComponent({ onMouseenter={onMouseEnter} onMouseleave={onMouseLeave} onClick={onInternalClick} + onKeydown={onInternalKeyDown} + onFocus={onInternalFocus} title={typeof title === 'string' ? title : undefined} > {cloneElement(icon, { diff --git a/components/vc-mentions/src/DropdownMenu.jsx b/components/vc-mentions/src/DropdownMenu.jsx index e927452bb..b525e48fa 100644 --- a/components/vc-mentions/src/DropdownMenu.jsx +++ b/components/vc-mentions/src/DropdownMenu.jsx @@ -1,8 +1,10 @@ -import Menu, { MenuItem } from '../../vc-menu'; +import Menu from '../../menu'; import PropTypes from '../../_util/vue-types'; import { OptionProps } from './Option'; import { inject } from 'vue'; +const MenuItem = Menu.Item; + function noop() {} export default { name: 'DropdownMenu', diff --git a/examples/App.vue b/examples/App.vue index 81ea7e0f4..65b1ba433 100644 --- a/examples/App.vue +++ b/examples/App.vue @@ -1,107 +1,39 @@ -