diff --git a/components/menu-button/__docs__/adaptor/index.jsx b/components/menu-button/__docs__/adaptor/index.jsx deleted file mode 100644 index 768564f7db..0000000000 --- a/components/menu-button/__docs__/adaptor/index.jsx +++ /dev/null @@ -1,207 +0,0 @@ -/* eslint-disable no-use-before-define */ -import React from 'react'; -import { MenuButton, Menu, Icon } from '@alifd/next'; -import { Types, parseData, ContentType } from '@alifd/adaptor-helper'; - -const createDataSouce = (list, keys = { selected: [], expanded: {} }, level = 0, prefix='') => { - const array = []; - let group = []; - let grouping = false; - let index = 0; - - list.forEach((item) => { - switch(item.type) { - // eslint-disable-next-line no-case-declarations - case 'node': - const key = `${prefix || level }-${index++}`; - - if (item.children && item.children.length > 0) { - item.children = createDataSouce(item.children, keys, level + 1, key); - } - - if (grouping) { - group.push({ - ...item, - key, - }); - } else { - array.push({ - ...item, - key - }); - } - - if (item.state === 'active') { - if (item.children && item.children.length > 0) { - keys.expanded.push(key); - } else { - keys.selected.push(key); - } - } - - return; - case 'comment': - if (group.length > 0) { - array.push({ - type: 'group', - value: grouping, - children: group, - key: `${prefix || level }-${index++}` - }); - group = []; - } - grouping = item.value; - return; - case 'divider': - if (group.length > 0) { - array.push({ - type: 'group', - value: grouping, - children: group, - key: `${prefix || level }-${index++}` - }); - group = []; - } - grouping = false; - array.push({ - type: 'divider', - key: `${prefix || level }-${index++}`, - }); - return; - default: return; - } - }); - - if (group.length > 0) { - array.push({ - type: 'group', - value: grouping, - children: group, - key: `${prefix || level }-${index++}` - }); - group = []; - } - - return array; -}; - -const createMenuItem = (item) => { - if (item.children.length > 0) { - return ( - type === ContentType.text).map(({ value }) => value).join('') : ''}> - {createContents(item.children)} - - ); - } - - return type === 'icon' ? : value)} />; -}; - -const createContents = (array = []) => { - - return array.map((item) => { - if (item.type === 'group' && item.children.length > 0) { - return {item.children.map(it => createMenuItem(it))}; - } - - if (item.type === 'divider') { - return ; - } - - return createMenuItem(item); - }); -}; - -const _propsValue = ({ shape, level, size, data, ...others}) => { - const list = parseData(data, { parseContent: true }); - const buttonItem = list[0] ? list[0] : { value: []}; - - return { - ...others, - size, - disabled: buttonItem.state === 'disabled', - visible: buttonItem.state === 'active', - type: shape === 'ghost' ? 'normal' : level, - popupProps: { needAdjust: false }, - ghost: shape === 'ghost' ? level : false, - selectMode: "multiple", - }; -}; - -export default { - name: 'MenuButton', - shape: ['normal', 'text', 'ghost'], - propsValue: _propsValue, - editor: (shape = 'normal') => ({ - props: [{ - name: 'level', - type: Types.enum, - options: shape === 'text' ? ['normal', 'primary'] : shape === 'ghost' ? ['light', 'dark'] : ['normal', 'primary', 'secondary'], - default: shape === 'ghost' ? 'light' : 'normal', - }, { - name: 'size', - type: Types.enum, - options: ['large', 'medium', 'small'], - default: 'medium' - }], - data: { - icon: true, - active: true, - disable: true, - default: 'Edit Document\n\tUndo\n\t*Redo\n\tCut\n\tCopy\n\tPaste' - } - }), - adaptor: ({ shape, level, size, data, ...others}) => { - const list = parseData(data, { parseContent: true }); - const buttonItem = list[0] ? list[0] : { value: []}; - - if (buttonItem.type !== 'node') return null; - - const keys = { selected: [], expanded: [] }; - const dataSouce = createDataSouce(list[0] ? list[0].children : [], keys); - - const label = buttonItem.value.map(({ type, value}) => { - if (type === 'icon') return ; - return value; - }); - - const props = _propsValue({ shape, level, size, data, ...others}); - - return ( - node} - text={shape === 'text'} - menuProps={{ openKeys: keys.expanded, style: { textAlign: 'left' } }} - selectedKeys={keys.selected} - label={label} - > - {createContents(dataSouce)} - - ); - }, - demoOptions: (demo) => { - const { node = { props: {} } } = demo; - const { level, data } = node.props; - if (data.indexOf('*') === 0) { - demo = { - ...demo, - height: 250 - }; - } - - if (level === 'dark') { - demo = { - ...demo, - background: '#333' - }; - } else if(level === 'light') { - demo = { - ...demo, - background: 'rgb(235, 236, 240)', - }; - } - - return demo; - } -}; diff --git a/components/menu-button/__docs__/adaptor/index.tsx b/components/menu-button/__docs__/adaptor/index.tsx new file mode 100644 index 0000000000..b1112fc2d6 --- /dev/null +++ b/components/menu-button/__docs__/adaptor/index.tsx @@ -0,0 +1,277 @@ +import * as React from 'react'; +import { MenuButton, Menu, Icon } from '@alifd/next'; +import { Types, parseData, ContentType } from '@alifd/adaptor-helper'; + +const createDataSource = ( + list: any = [], + keys = { selected: [] as string[], expanded: [] as string[] }, + level = 0, + prefix = '' +) => { + const array = []; + let group: any = []; + let grouping = false; + let index = 0; + + list.forEach((item: any) => { + let key; + switch (item.type) { + case 'node': + key = `${prefix || level}-${index++}`; + + if (item.children && item.children.length > 0) { + item.children = createDataSource(item.children, keys, level + 1, key); + } + + if (grouping) { + group.push({ + ...item, + key, + }); + } else { + array.push({ + ...item, + key, + }); + } + + if (item.state === 'active') { + if (item.children && item.children.length > 0) { + keys.expanded.push(key); + } else { + keys.selected.push(key); + } + } + + return; + case 'comment': + if (group.length > 0) { + array.push({ + type: 'group', + value: grouping, + children: group, + key: `${prefix || level}-${index++}`, + }); + group = []; + } + grouping = item.value; + return; + case 'divider': + if (group.length > 0) { + array.push({ + type: 'group', + value: grouping, + children: group, + key: `${prefix || level}-${index++}`, + }); + group = []; + } + grouping = false; + array.push({ + type: 'divider', + key: `${prefix || level}-${index++}`, + }); + return; + default: + return; + } + }); + + if (group.length > 0) { + array.push({ + type: 'group', + value: grouping, + children: group, + key: `${prefix || level}-${index++}`, + }); + group = []; + } + + return array; +}; + +const createChildMenuItem = (childItem: any) => { + return ( + + type === 'icon' ? ( + + ) : ( + value + ) + )} + /> + ); +}; + +const createMenuItem = (item: any) => { + if (item.children && item.children.length > 0) { + return ( + type === ContentType.text) + .map(({ value }: any) => value) + .join('') + : '' + } + > + {item.children.map((childItem: any) => createChildMenuItem(childItem))} + + ); + } + return ( + + type === 'icon' ? ( + + ) : ( + value + ) + )} + /> + ); +}; + +const createContents = (items: any) => { + return items.map((item: any) => { + if (item.type === 'group' && item.children.length > 0) { + return ( + + {item.children.map(createChildMenuItem)} + + ); + } + if (item.type === 'divider') { + return ; + } + return createMenuItem(item); + }); +}; + +const _propsValue = ({ shape, level, size, data, ...others }: any) => { + const list = parseData(data, { parseContent: true }); + const buttonItem = list[0] ? list[0] : ({ value: [] } as any); + + return { + ...others, + size, + disabled: buttonItem.state === 'disabled', + visible: buttonItem.state === 'active', + type: shape === 'ghost' ? 'normal' : level, + popupProps: { needAdjust: false }, + ghost: shape === 'ghost' ? level : false, + selectMode: 'multiple', + }; +}; + +export default { + name: 'MenuButton', + shape: ['normal', 'text', 'ghost'], + propsValue: _propsValue, + editor: (shape = 'normal') => ({ + props: [ + { + name: 'level', + type: Types.enum, + options: + shape === 'text' + ? ['normal', 'primary'] + : shape === 'ghost' + ? ['light', 'dark'] + : ['normal', 'primary', 'secondary'], + default: shape === 'ghost' ? 'light' : 'normal', + }, + { + name: 'size', + type: Types.enum, + options: ['large', 'medium', 'small'], + default: 'medium', + }, + ], + data: { + icon: true, + active: true, + disable: true, + default: 'Edit Document\n\tUndo\n\t*Redo\n\tCut\n\tCopy\n\tPaste', + }, + }), + adaptor: ({ shape, level, size, data, ...others }: any) => { + const list = parseData(data, { parseContent: true }); + const buttonItem = list[0] ? list[0] : ({ value: [] } as any); + + if (buttonItem.type !== 'node') return null; + + const keys = { selected: [], expanded: [] }; + const dataSource = createDataSource(list[0] ? list[0].children : [], keys); + + let label; + if (typeof buttonItem.value === 'object') { + label = buttonItem.value.map(({ type, value }: any) => { + if (type === 'icon') return ; + return value; + }); + } + + const props = _propsValue({ shape, level, size, data, ...others }); + + return ( + node} + text={shape === 'text'} + menuProps={{ openKeys: keys.expanded, style: { textAlign: 'left' } }} + selectedKeys={keys.selected} + label={label} + > + {createContents(dataSource)} + + ); + }, + demoOptions: (demo: any) => { + const { node = { props: {} } } = demo; + const { level, data } = node.props; + if (data.indexOf('*') === 0) { + demo = { + ...demo, + height: 250, + }; + } + + if (level === 'dark') { + demo = { + ...demo, + background: '#333', + }; + } else if (level === 'light') { + demo = { + ...demo, + background: 'rgb(235, 236, 240)', + }; + } + + return demo; + }, +}; diff --git a/components/menu-button/__docs__/demo/menu/index.tsx b/components/menu-button/__docs__/demo/menu/index.tsx index c241742f05..ae696b6351 100644 --- a/components/menu-button/__docs__/demo/menu/index.tsx +++ b/components/menu-button/__docs__/demo/menu/index.tsx @@ -4,7 +4,7 @@ import { MenuButton } from '@alifd/next'; const { Item, Group, Divider } = MenuButton; -function selectItem(id) { +function selectItem(id: any) { console.log(id); } diff --git a/components/menu-button/__docs__/theme/index.jsx b/components/menu-button/__docs__/theme/index.jsx deleted file mode 100644 index 2f7d6b9266..0000000000 --- a/components/menu-button/__docs__/theme/index.jsx +++ /dev/null @@ -1,129 +0,0 @@ -import React from 'react'; -import MenuButton from '../../index'; -import { Demo, DemoGroup, DemoHead, initDemo } from '../../../demo-helper'; -import ConfigProvider from '../../../config-provider'; -import '../../../demo-helper/style'; -import '../../style'; - -/* eslint-disable */ -const i18nMap = { - 'zh-cn': { - menuButton: '菜单按钮', - textMenuButton: '文本菜单按钮', - ghostMenuButton: '幽灵菜单按钮', - normal: '普通', - expand: '展开', - disable: '禁用', - large: '大', - medium: '中', - small: '小', - editDocument: '编辑文档', - undo: '撤销', - redo: '重做', - cut: '剪切', - copy: '复制', - paste: '粘贴', - }, - 'en-us': { - menuButton: 'Menu Button', - textMenuButton: 'Text Menu Button', - ghostMenuButton: 'Ghost Menu Button', - normal: 'Normal', - expand: 'Expanded', - disable: 'Disabled', - large: 'L', - medium: 'M', - small: 'S', - editDocument: 'Edit Document', - undo: 'Undo', - redo: 'Redo', - cut: 'Cut', - copy: 'Copy', - paste: 'Paste', - }, -}; - -function renderButton(type, locale, props) { - const menu = ['undo', 'redo', 'cut', 'copy', 'paste'].map(item => {locale[item]}); - const cols = [locale.large, locale.medium, locale.small]; - const newLabel =
- {locale.editDocument} - {/* --------- this is for config platform ----------- */} -
-
-
- {/* --------- this is for config platform ----------- */} -
; - - const commonProps = { - ...props, - type: type.toLowerCase(), - label: newLabel, - }; - - let style; - - if (props) { - switch (props.ghost) { - case 'light': { - style = { - backgroundColor: '#ebecf0', - }; - break; - } - case 'dark': { - style = { - backgroundColor: '#333', - }; - break; - } - } - } - - return ( - - - {menu} - {menu} - {menu} - - - {menu} - {menu} - {menu} - - - {menu} - {menu} - {menu} - - ) -} - -function render(locale, lang) { - return ReactDOM.render(( -
- - {renderButton('Normal', locale)} - {renderButton('Primary', locale)} - {renderButton('Secondary', locale)} - - - {renderButton('Normal', locale, { text: true })} - {renderButton('Primary', locale, { text: true })} - - - {renderButton('Normal', locale, { ghost: 'light' })} - {renderButton('Normal', locale, { ghost: 'dark' })} - -
- ), document.getElementById('container')); -} - -window.renderDemo = function(lang = 'en-us') { - render(i18nMap[lang], lang); -}; - -renderDemo(); - -initDemo('menu-button'); diff --git a/components/menu-button/__docs__/theme/index.tsx b/components/menu-button/__docs__/theme/index.tsx new file mode 100644 index 0000000000..39af36a95a --- /dev/null +++ b/components/menu-button/__docs__/theme/index.tsx @@ -0,0 +1,172 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import MenuButton from '../../index'; +import { Demo, DemoGroup, DemoHead, initDemo } from '../../../demo-helper'; +import ConfigProvider from '../../../config-provider'; +import '../../../demo-helper/style'; +import '../../style'; + +/* eslint-disable */ +const i18nMap = { + 'zh-cn': { + menuButton: '菜单按钮', + textMenuButton: '文本菜单按钮', + ghostMenuButton: '幽灵菜单按钮', + normal: '普通', + expand: '展开', + disable: '禁用', + large: '大', + medium: '中', + small: '小', + editDocument: '编辑文档', + undo: '撤销', + redo: '重做', + cut: '剪切', + copy: '复制', + paste: '粘贴', + }, + 'en-us': { + menuButton: 'Menu Button', + textMenuButton: 'Text Menu Button', + ghostMenuButton: 'Ghost Menu Button', + normal: 'Normal', + expand: 'Expanded', + disable: 'Disabled', + large: 'L', + medium: 'M', + small: 'S', + editDocument: 'Edit Document', + undo: 'Undo', + redo: 'Redo', + cut: 'Cut', + copy: 'Copy', + paste: 'Paste', + }, +}; + +function renderButton(type: string, locale: any, props: any = {}) { + const menu = ['undo', 'redo', 'cut', 'copy', 'paste'].map(item => ( + {locale[item]} + )); + const cols = [locale.large, locale.medium, locale.small]; + const newLabel = ( +
+ {locale.editDocument} + {/* --------- this is for config platform ----------- */} +
+
+
+ {/* --------- this is for config platform ----------- */} +
+ ); + + const commonProps = { + ...props, + type: type.toLowerCase(), + label: newLabel, + }; + + let style; + + if (props) { + switch (props.ghost) { + case 'light': { + style = { + backgroundColor: '#ebecf0', + }; + break; + } + case 'dark': { + style = { + backgroundColor: '#333', + }; + break; + } + } + } + + return ( + + + + + {menu} + + + {menu} + + + {menu} + + + + + {menu} + + + {menu} + + + {menu} + + + + + {menu} + + + {menu} + + + {menu} + + + + ); +} + +function render(locale: any, lang: string) { + return ReactDOM.render( + +
+ + {renderButton('Normal', locale)} + {renderButton('Primary', locale)} + {renderButton('Secondary', locale)} + + + {renderButton('Normal', locale, { text: true })} + {renderButton('Primary', locale, { text: true })} + + + {renderButton('Normal', locale, { ghost: 'light' })} + {renderButton('Normal', locale, { ghost: 'dark' })} + +
+
, + document.getElementById('container') + ); +} + +window.renderDemo = function (lang = 'en-us') { + render(i18nMap[lang], lang); +}; + +renderDemo(); + +initDemo('menu-button'); diff --git a/components/menu-button/__tests__/a11y-spec.js b/components/menu-button/__tests__/a11y-spec.tsx similarity index 61% rename from components/menu-button/__tests__/a11y-spec.js rename to components/menu-button/__tests__/a11y-spec.tsx index e1bbcbd5e4..aac615316e 100644 --- a/components/menu-button/__tests__/a11y-spec.js +++ b/components/menu-button/__tests__/a11y-spec.tsx @@ -1,11 +1,7 @@ -import React from 'react'; -import Enzyme from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; +import * as React from 'react'; import MenuButton from '../index'; import '../style'; -import { unmount, test, mountReact } from '../../util/__tests__/legacy/a11y/validate'; - -Enzyme.configure({ adapter: new Adapter() }); +import { testReact } from '../../util/__tests__/a11y/validate'; const wrapperClassName = 'js-a11y-test'; const popupProps = { wrapperClassName }; @@ -15,27 +11,16 @@ const menu = ['a', 'b'].map(item => {item} { - let wrapper; - - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - wrapper = null; - } - unmount(); - }); - it('should not have any violations', async () => { - wrapper = await mountReact( + await testReact( {menu} ); - return test(`.${wrapperClassName}`); }); it('should not have any violations when text button', async () => { - wrapper = await mountReact( + await testReact(
{menu} @@ -43,6 +28,5 @@ describe.skip('MenuButton A11y', () => {   
); - return test(`.${wrapperClassName}`); }); }); diff --git a/components/menu-button/__tests__/index-spec.js b/components/menu-button/__tests__/index-spec.js deleted file mode 100644 index 4419172adc..0000000000 --- a/components/menu-button/__tests__/index-spec.js +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react'; -import Enzyme, { mount } from 'enzyme'; -import sinon from 'sinon'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; -import MenuButton from '../index'; -import '../style'; - -Enzyme.configure({ adapter: new Adapter() }); - -/* eslint-disable no-undef,react/jsx-filename-extension */ -describe('MenuButton', () => { - const menu = ['a', 'b'].map(item => {item}); - - let wrapper; - - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - wrapper = null; - } - }); - - describe('render', () => { - it('should render', () => { - const wrapper = mount({menu}); - assert(wrapper.find('button.next-menu-btn').length === 1); - }); - - it('should controlled selectedKeys', () => { - const wrapper = mount( - - {menu} - - ); - wrapper.setProps({ selectedKeys: ['b'] }); - assert(wrapper.find('li[title="b"][role="option"]').hasClass('next-selected')); - }); - - it('should controlled popup visible', () => { - const wrapper = mount({menu}); - assert(wrapper.find('.next-menu').length === 0); - wrapper.setProps({ visible: true }); - assert(wrapper.find('.next-menu').length === 1); - }); - }); - - describe('action', () => { - it('should click trigger to open the popup', () => { - let visible; - const wrapper = mount( - (visible = vis)}> - {menu} - - ); - wrapper.find('button.next-menu-btn').simulate('click'); - assert(wrapper.find('.next-menu').length === 1); - assert(visible); - }); - - it('should select in uncontrolled mode', () => { - const wrapper = mount( - - {menu} - - ); - wrapper.find('li[title="b"][role="option"]').simulate('click'); - assert(wrapper.find('li[title="b"][role="option"]').hasClass('next-selected')); - }); - - it('should select in controlled mode', () => { - const wrapper = mount( - - {menu} - - ); - wrapper.find('li[title="b"][role="option"]').simulate('click'); - assert(wrapper.find('li[title="a"][role="option"]').hasClass('next-selected')); - }); - - it('should mulitple select can`t close', () => { - const onVisibleChange = sinon.spy(); - const onItemClick = sinon.spy(); - const wrapper = mount( - - {menu} - - ); - wrapper.find('li[title="b"][role="option"]').simulate('click'); - assert(onItemClick.calledOnce); - assert(onVisibleChange.notCalled); - }); - }); -}); diff --git a/components/menu-button/__tests__/index-spec.tsx b/components/menu-button/__tests__/index-spec.tsx new file mode 100644 index 0000000000..51f01d2591 --- /dev/null +++ b/components/menu-button/__tests__/index-spec.tsx @@ -0,0 +1,91 @@ +import * as React from 'react'; +import { MountReturn } from 'cypress/react'; +import '../style'; +import MenuButton from '../index'; + +describe('MenuButton', () => { + const menu = ['a', 'b'].map(item => {item}); + + describe('render', () => { + it('should render', () => { + cy.mount({menu}); + cy.get('button.next-menu-btn').should('have.length', 1); + }); + + it('should controlled selectedKeys', () => { + cy.mount( + + {menu} + + ).as('menu-btn'); + cy.get('@menu-btn').then(({ component, rerender }) => { + return rerender(React.cloneElement(component, { selectedKeys: ['b'] })); + }); + cy.get('li[title="b"][role="option"]').should('have.class', 'next-selected'); + }); + + it('should controlled popup visible', () => { + cy.mount({menu}).as('menu-btn'); + cy.get('.next-menu').should('have.length', 0); + cy.get('@menu-btn').then(({ component, rerender }) => { + return rerender(React.cloneElement(component, { visible: true })); + }); + cy.get('.next-menu').should('have.length', 1); + }); + }); + + describe('action', () => { + it('should click trigger to open the popup', () => { + let visible: boolean; + cy.mount( + (visible = vis)}> + {menu} + + ); + cy.get('button.next-menu-btn').click(); + cy.then(() => { + cy.get('.next-menu').should('have.length', 1); + cy.wrap(visible).should('be.true'); + }); + }); + + it('should select in uncontrolled mode', () => { + cy.mount( + + {menu} + + ); + cy.get('li[title="b"][role="option"]').click(); + cy.get('li[title="b"][role="option"]').should('have.class', 'next-selected'); + }); + + it('should select in controlled mode', () => { + cy.mount( + + {menu} + + ); + cy.get('li[title="b"][role="option"]').click(); + cy.get('li[title="a"][role="option"]').should('have.class', 'next-selected'); + }); + + it('should mulitple select can`t close', () => { + const onVisibleChange = cy.spy(); + const onItemClick = cy.spy(); + cy.mount( + + {menu} + + ); + cy.get('li[title="b"][role="option"]').click(); + cy.wrap(onItemClick).should('be.calledOnce'); + cy.wrap(onVisibleChange).should('not.have.been.called'); + }); + }); +}); diff --git a/components/menu-button/index.jsx b/components/menu-button/index.tsx similarity index 68% rename from components/menu-button/index.jsx rename to components/menu-button/index.tsx index 83e3e367e2..b164840e38 100644 --- a/components/menu-button/index.jsx +++ b/components/menu-button/index.tsx @@ -1,90 +1,44 @@ -import React from 'react'; +import * as React from 'react'; import { findDOMNode } from 'react-dom'; -import PropTypes from 'prop-types'; +import * as PropTypes from 'prop-types'; import { polyfill } from 'react-lifecycles-compat'; -import classnames from 'classnames'; +import * as classnames from 'classnames'; import Button from '../button'; import Icon from '../icon'; -import Menu from '../menu'; +import Menu, { MenuProps } from '../menu'; import Overlay from '../overlay'; import ConfigProvider from '../config-provider'; import { obj, func } from '../util'; +import type { MenuButtonProps, MenuButtonState } from './types'; const { Popup } = Overlay; /** * MenuButton */ -class MenuButton extends React.Component { +class MenuButton extends React.Component { + static Item = Menu.Item; + static Group = Menu.Group; + static Divider = Menu.Divider; + static propTypes = { prefix: PropTypes.string, - /** - * 按钮上的文本内容 - */ label: PropTypes.node, - /** - * 弹层是否与按钮宽度相同 - */ autoWidth: PropTypes.bool, - /** - * 弹层触发方式 - */ popupTriggerType: PropTypes.oneOf(['click', 'hover']), - /** - * 弹层容器 - */ popupContainer: PropTypes.any, - /** - * 弹层展开状态 - */ visible: PropTypes.bool, - /** - * 弹层默认是否展开 - */ defaultVisible: PropTypes.bool, - /** - * 弹层在显示和隐藏触发的事件 - */ onVisibleChange: PropTypes.func, - /** - * 弹层自定义样式 - */ popupStyle: PropTypes.object, - /** - * 弹层自定义样式类 - */ popupClassName: PropTypes.string, - /** - * 弹层属性透传 - */ popupProps: PropTypes.object, - /** - * 是否跟随滚动 - */ followTrigger: PropTypes.bool, - /** - * 默认激活的菜单项(用法同 Menu 非受控) - */ defaultSelectedKeys: PropTypes.array, - /** - * 激活的菜单项(用法同 Menu 受控) - */ selectedKeys: PropTypes.array, - /** - * 菜单的选择模式,同 Menu - */ selectMode: PropTypes.oneOf(['single', 'multiple']), - /** - * 点击菜单项后的回调,同 Menu - */ onItemClick: PropTypes.func, - /** - * 选择菜单后的回调,同 Menu - */ onSelect: PropTypes.func, - /** - * 菜单属性透传 - */ menuProps: PropTypes.object, style: PropTypes.object, className: PropTypes.string, @@ -102,7 +56,7 @@ class MenuButton extends React.Component { menuProps: {}, }; - constructor(props, context) { + constructor(props: MenuButtonProps, context?: unknown) { super(props, context); this.state = { selectedKeys: props.defaultSelectedKeys, @@ -110,8 +64,8 @@ class MenuButton extends React.Component { }; } - static getDerivedStateFromProps(props) { - const st = {}; + static getDerivedStateFromProps(props: MenuButtonProps) { + const st: MenuButtonProps = {}; if ('visible' in props) { st.visible = props.visible; @@ -123,11 +77,17 @@ class MenuButton extends React.Component { return st; } + menu: HTMLElement | undefined; - clickMenuItem = (key, ...others) => { + clickMenuItem: MenuProps['onItemClick'] = (key, item, event) => { const { selectMode } = this.props; - this.props.onItemClick(key, ...others); + if (typeof this.props.onItemClick === 'function') { + this.props.onItemClick(key, item, event); + } + if (selectMode === 'single') { + this.onPopupVisibleChange(false, 'menuSelect'); + } if (selectMode === 'multiple') { return; @@ -136,35 +96,40 @@ class MenuButton extends React.Component { this.onPopupVisibleChange(false, 'menuSelect'); }; - selectMenu = (keys, ...others) => { + selectMenu: MenuProps['onSelect'] = (keys, ...others) => { if (!('selectedKeys' in this.props)) { this.setState({ selectedKeys: keys, }); } - this.props.onSelect(keys, ...others); + if (typeof this.props.onSelect === 'function') { + this.props.onSelect(keys, ...others); + } }; onPopupOpen = () => { - const button = findDOMNode(this); - if (this.props.autoWidth && button && this.menu) { - this.menu.style.width = `${button.offsetWidth}px`; + const button = findDOMNode(this) as HTMLElement; + if (this.props.autoWidth && button && this?.menu) { + this.menu.style.width = `${button?.offsetWidth}px`; } }; - onPopupVisibleChange = (visible, type) => { + onPopupVisibleChange = (visible: boolean, type: string) => { if (!('visible' in this.props)) { this.setState({ visible, }); } - this.props.onVisibleChange(visible, type); + if (typeof this.props.onVisibleChange === 'function') { + this.props.onVisibleChange(visible, type); + } }; - _menuRefHandler = ref => { - this.menu = findDOMNode(ref); + _menuRefHandler = (ref: React.ComponentRef | null) => { + this.menu = findDOMNode(ref) as HTMLElement; - const refFn = this.props.menuProps.ref; + //@ts-expect-error menuProps 缺少 ref 类型 React.ComponentRef | null + const refFn = this.props?.menuProps?.ref; if (typeof refFn === 'function') { refFn(ref); } @@ -207,7 +172,11 @@ class MenuButton extends React.Component { ); const trigger = ( - ); @@ -242,9 +211,7 @@ class MenuButton extends React.Component { } } -MenuButton.Item = Menu.Item; -MenuButton.Group = Menu.Group; -MenuButton.Divider = Menu.Divider; +export type { MenuButtonProps }; export default ConfigProvider.config(polyfill(MenuButton), { componentName: 'MenuButton', diff --git a/components/menu-button/mobile/index.jsx b/components/menu-button/mobile/index.jsx deleted file mode 100644 index 80b6bf519b..0000000000 --- a/components/menu-button/mobile/index.jsx +++ /dev/null @@ -1,6 +0,0 @@ -import { MenuButton as MeetMenuButton } from '@alifd/meet-react'; -import NextMenuButton from '../index'; - -const MenuButton = MeetMenuButton ? MeetMenuButton : NextMenuButton; - -export default MenuButton; diff --git a/components/menu-button/mobile/index.tsx b/components/menu-button/mobile/index.tsx new file mode 100644 index 0000000000..67cdb85c29 --- /dev/null +++ b/components/menu-button/mobile/index.tsx @@ -0,0 +1,3 @@ +import MenuButton from '../index'; + +export default MenuButton; diff --git a/components/menu-button/style.js b/components/menu-button/style.ts similarity index 100% rename from components/menu-button/style.js rename to components/menu-button/style.ts diff --git a/components/menu-button/index.d.ts b/components/menu-button/types.ts similarity index 55% rename from components/menu-button/index.d.ts rename to components/menu-button/types.ts index ffbb9ce599..cd43baa396 100644 --- a/components/menu-button/index.d.ts +++ b/components/menu-button/types.ts @@ -1,100 +1,108 @@ -/// - -import React from 'react'; -import { Item, Group, Divider, MenuProps } from '../menu'; +import * as React from 'react'; +import { MenuProps } from '../menu'; import { CommonProps } from '../util'; import { ButtonProps } from '../button'; import { PopupProps } from '../overlay'; +/** + * @api MenuButton + */ export interface MenuButtonProps extends Omit, CommonProps { /** * 按钮上的文本内容 + * @en The text in button */ label?: React.ReactNode; - /** * 弹层是否与按钮宽度相同 + * @en If popup width equals to button width + * @defaultValue true */ autoWidth?: boolean; - /** * 弹层触发方式 + * @en Trigger type of popup + * @defaultValue 'click' */ popupTriggerType?: 'click' | 'hover'; - /** * 弹层容器 + * @en Container of popup */ popupContainer?: string | HTMLElement | ((target: HTMLElement) => HTMLElement); - /** * 弹层展开状态 + * @en Visible state of popup */ visible?: boolean; - /** * 弹层默认是否展开 + * @en Default visible state of popup */ defaultVisible?: boolean; - /** * 弹层在显示和隐藏触发的事件 + * @en Callback when visible state changes + * @defaultValue function.noop */ onVisibleChange?: (visible: boolean, type: string) => void; - /** * 弹层自定义样式 + * @en Custom style of popup */ popupStyle?: React.CSSProperties; - /** * 弹层自定义样式类 + * @en Custom className of popup */ popupClassName?: string; - /** * 弹层属性透传 + * @en Props of popup */ popupProps?: PopupProps; - /** * 菜单是否跟随滚动 + * @en follow Trigger or not */ followTrigger?: boolean; - /** * 默认激活的菜单项(用法同 Menu 非受控) + * @en Default selected items, as Menu + * @defaultValue [] */ - defaultSelectedKeys?: Array; - + defaultSelectedKeys?: Array; /** * 激活的菜单项(用法同 Menu 受控) + * @en Selected items, as Menu */ - selectedKeys?: string | Array; - + selectedKeys?: string | Array; /** * 菜单的选择模式,同 Menu + * @en Select mode of menu, see Menu */ selectMode?: 'single' | 'multiple'; - /** * 点击菜单项后的回调,同 Menu + * @en Callback when click the menu item, see Menu + * @defaultValue function.noop */ - onItemClick?: (key: string, item: any, event: React.MouseEvent) => void; - + onItemClick?: (key: string, item: object, event: React.MouseEvent) => void; /** * 选择菜单后的回调,同 Menu + * @en Callback when select the menu, see Menu + * @defaultValue function.noop */ - onSelect?: (selectedKeys: Array, item: any, extra: any) => void; - + onSelect?: (selectedKeys: Array, item: object, extra: object) => void; /** * 菜单属性透传 + * @en Props of menu + * @defaultValue */ menuProps?: MenuProps; } -export default class MenuButton extends React.Component { - static Item: typeof Item; - static Group: typeof Group; - static Divider: typeof Divider; +export interface MenuButtonState { + selectedKeys?: Array; + visible?: boolean; }