diff --git a/docs/dropdown/demo/basic.md b/docs/dropdown/demo/basic.md index 8dccfa12c1..73b03529d1 100644 --- a/docs/dropdown/demo/basic.md +++ b/docs/dropdown/demo/basic.md @@ -27,7 +27,10 @@ const menu = ( ); ReactDOM.render( - Hello dropdown} afterOpen={() => console.log('after open')}> - {menu} - , mountNode); +
+ Hello dropdown} triggerType={["click", "hover"]} afterOpen={() => console.log('after open')}> + {menu} + +
+, mountNode); ```` diff --git a/docs/dropdown/index.en-us.md b/docs/dropdown/index.en-us.md index 5e57b76fa6..6e4ef25fd7 100644 --- a/docs/dropdown/index.en-us.md +++ b/docs/dropdown/index.en-us.md @@ -13,6 +13,10 @@ You can storage operation command with dropdown component when there are too much command. There will be a drop-down menu after you click or hover the trigger element. Then choose a command and run it. +### Note + +- Dropdown is accessible when using like `` (triggerType="focus" is deprecated). In our opinion, menu elements need to be confirmed by users before they are expanded when it comes to accessibility. + ## API ### Dropdown @@ -25,7 +29,7 @@ You can storage operation command with dropdown component when there are too muc | defaultVisible | overlay display or not in default situation | Boolean | false | | onVisibleChange | callback function when toggle visible of overlay

**signatures**:
Function(visible: Boolean, type: String, e: Object) => void
**params**:
_visible_: {Boolean} overlay display or not
_type_: {String} orign of trigger overlay toggle visible
_e_: {Object} DOM Event| Function | func.noop | | trigger | trigger element | ReactNode | - | -| triggerType | operation type of trigger overlay toggle visible

**options**:
'hover', 'click', 'focus' | Enum | 'hover' | +| triggerType | operation type of trigger overlay toggle visible

**options**:
'hover', 'click' | Enum | 'hover' | | disabled | overlay can not toggle visible if you set disabled attribute | Boolean | false | | align | overlay position relative to trigger element, see details Overlay align | String | 'tl bl' | | offset | overlay adjust position relative to trigger element | Array | [0, 0] | diff --git a/docs/dropdown/index.md b/docs/dropdown/index.md index 81f46fb304..768d77546f 100644 --- a/docs/dropdown/index.md +++ b/docs/dropdown/index.md @@ -13,6 +13,10 @@ 当页面上的操作命令过多时,用此组件可以收纳操作元素。点击或移入触点,会出现一个下拉菜单。可在列表中进行选择,并执行相应的命令。 +### 使用注意 + +- 若要使用无障碍的Dropdown,推荐使用`` (请勿使用triggerType="focus")。我们认为,菜单类元素需要由用户确认后再展开才是一种无障碍友好的实践。 + ## API ### Dropdown @@ -26,7 +30,7 @@ | defaultVisible | 弹层默认是否显示 | Boolean | false | | onVisibleChange | 弹层显示或隐藏时触发的回调函数

**签名**:
Function(visible: Boolean, type: String, e: Object) => void
**参数**:
_visible_: {Boolean} 弹层是否显示
_type_: {String} 触发弹层显示或隐藏的来源
_e_: {Object} DOM事件 | Function | func.noop | | trigger | 触发弹层显示或者隐藏的元素 | ReactNode | - | -| triggerType | 触发弹层显示或隐藏的操作类型

**可选值**:
'hover', 'click', 'focus' | Enum | 'hover' | +| triggerType | 触发弹层显示或隐藏的操作类型,可以是 'click','hover',或者它们组成的数组,如 ['hover', 'click'] | String/Array | ['hover'] | | disabled | 设置此属性,弹层无法显示或隐藏 | Boolean | false | | align | 弹层相对于触发元素的定位, 详见 Overlay 的定位部分 | String | 'tl bl' | | offset | 弹层相对于触发元素定位的微调 | Array | [0, 0] | diff --git a/src/dropdown/dropdown.jsx b/src/dropdown/dropdown.jsx new file mode 100644 index 0000000000..f00311a23f --- /dev/null +++ b/src/dropdown/dropdown.jsx @@ -0,0 +1,168 @@ +import React, { Component, Children } from 'react'; +import PropTypes from 'prop-types'; +import Overlay from '../overlay'; +import { func } from '../util'; + +const { noop, makeChain, bindCtx } = func; +const Popup = Overlay.Popup; + +/** + * Dropdown + * @description 继承 Popup 的 API,除非特别说明 + */ +export default class Dropdown extends Component { + static propTypes = { + prefix: PropTypes.string, + pure: PropTypes.bool, + rtl: PropTypes.bool, + className: PropTypes.string, + /** + * 弹层内容 + */ + children: PropTypes.node, + /** + * 弹层当前是否显示 + */ + visible: PropTypes.bool, + /** + * 弹层默认是否显示 + */ + defaultVisible: PropTypes.bool, + /** + * 弹层显示或隐藏时触发的回调函数 + * @param {Boolean} visible 弹层是否显示 + * @param {String} type 触发弹层显示或隐藏的来源 + * @param {Object} e DOM事件 + */ + onVisibleChange: PropTypes.func, + /** + * 触发弹层显示或者隐藏的元素 + */ + trigger: PropTypes.node, + /** + * 触发弹层显示或隐藏的操作类型,可以是 'click','hover',或者它们组成的数组,如 ['hover', 'click'] + */ + triggerType: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + /** + * 设置此属性,弹层无法显示或隐藏 + */ + disabled: PropTypes.bool, + /** + * 弹层相对于触发元素的定位, 详见 Overlay 的定位部分 + */ + align: PropTypes.string, + /** + * 弹层相对于触发元素定位的微调 + */ + offset: PropTypes.array, + /** + * 弹层显示或隐藏的延时时间(以毫秒为单位),在 triggerType 被设置为 hover 时生效 + */ + delay: PropTypes.number, + /** + * 弹层打开时是否让其中的元素自动获取焦点 + */ + autoFocus: PropTypes.bool, + /** + * 是否显示遮罩 + */ + hasMask: PropTypes.bool, + /** + * 隐藏时是否保留子节点 + */ + cache: PropTypes.bool, + /** + * 配置动画的播放方式,支持 { in: 'enter-class', out: 'leave-class' } 的对象参数,如果设置为 false,则不播放动画 + * @default { in: 'expandInDown', out: 'expandOutUp' } + */ + animation: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), + }; + static defaultProps = { + prefix: 'next-', + pure: false, + defaultVisible: false, + onVisibleChange: noop, + triggerType: 'hover', + disabled: false, + align: 'tl bl', + offset: [0, 0], + delay: 200, + hasMask: false, + cache: false, + onPosition: noop, + }; + + constructor(props) { + super(props); + + this.state = { + visible: + 'visible' in props + ? props.visible + : props.defaultVisible || false, + autoFocus: 'autoFocus' in props ? props.autoFocus : false, + }; + + bindCtx(this, ['onTriggerKeyDown', 'onMenuClick', 'onVisibleChange']); + } + + getVisible(props = this.props) { + return 'visible' in props ? props.visible : this.state.visible; + } + + onMenuClick() { + this.onVisibleChange(false, 'fromContent'); + } + + onVisibleChange(visible, from) { + this.setState({ visible }); + + this.props.onVisibleChange(visible, from); + } + + onTriggerKeyDown() { + let autoFocus = true; + + if ('autoFocus' in this.props) { + autoFocus = this.props.autoFocus; + } + + this.setState({ + autoFocus, + }); + } + + render() { + let child = Children.only(this.props.children); + if (typeof child.type === 'function' && child.type.isNextMenu) { + child = React.cloneElement(child, { + onItemClick: makeChain( + this.onMenuClick, + child.props.onItemClick + ), + }); + } + + const { trigger, rtl } = this.props; + const newTrigger = React.cloneElement(trigger, { + onKeyDown: makeChain( + this.onTriggerKeyDown, + trigger.props.onKeyDown + ), + }); + + return ( + + {child} + + ); + } +} diff --git a/src/dropdown/index.jsx b/src/dropdown/index.jsx index 5667f9d7f5..aec84fa0da 100644 --- a/src/dropdown/index.jsx +++ b/src/dropdown/index.jsx @@ -1,171 +1,22 @@ -import React, { Component, Children } from 'react'; -import PropTypes from 'prop-types'; -import Overlay from '../overlay'; import ConfigProvider from '../config-provider'; -import { func } from '../util'; - -const { noop, makeChain, bindCtx } = func; -const Popup = Overlay.Popup; - -/** - * Dropdown - * @description 继承 Popup 的 API,除非特别说明 - */ -class Dropdown extends Component { - static propTypes = { - prefix: PropTypes.string, - pure: PropTypes.bool, - rtl: PropTypes.bool, - className: PropTypes.string, - /** - * 弹层内容 - */ - children: PropTypes.node, - /** - * 弹层当前是否显示 - */ - visible: PropTypes.bool, - /** - * 弹层默认是否显示 - */ - defaultVisible: PropTypes.bool, - /** - * 弹层显示或隐藏时触发的回调函数 - * @param {Boolean} visible 弹层是否显示 - * @param {String} type 触发弹层显示或隐藏的来源 - * @param {Object} e DOM事件 - */ - onVisibleChange: PropTypes.func, - /** - * 触发弹层显示或者隐藏的元素 - */ - trigger: PropTypes.node, - /** - * 触发弹层显示或隐藏的操作类型 - */ - triggerType: PropTypes.oneOf(['hover', 'click', 'focus']), - /** - * 设置此属性,弹层无法显示或隐藏 - */ - disabled: PropTypes.bool, - /** - * 弹层相对于触发元素的定位, 详见 Overlay 的定位部分 - */ - align: PropTypes.string, - /** - * 弹层相对于触发元素定位的微调 - */ - offset: PropTypes.array, - /** - * 弹层显示或隐藏的延时时间(以毫秒为单位),在 triggerType 被设置为 hover 时生效 - */ - delay: PropTypes.number, - /** - * 弹层打开时是否让其中的元素自动获取焦点 - */ - autoFocus: PropTypes.bool, - /** - * 是否显示遮罩 - */ - hasMask: PropTypes.bool, - /** - * 隐藏时是否保留子节点 - */ - cache: PropTypes.bool, - /** - * 配置动画的播放方式,支持 { in: 'enter-class', out: 'leave-class' } 的对象参数,如果设置为 false,则不播放动画 - * @default { in: 'expandInDown', out: 'expandOutUp' } - */ - animation: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), - }; - static defaultProps = { - prefix: 'next-', - pure: false, - defaultVisible: false, - onVisibleChange: noop, - triggerType: 'hover', - disabled: false, - align: 'tl bl', - offset: [0, 0], - delay: 200, - hasMask: false, - cache: false, - onPosition: noop, - }; - - constructor(props) { - super(props); - - this.state = { - visible: - 'visible' in props - ? props.visible - : props.defaultVisible || false, - autoFocus: 'autoFocus' in props ? props.autoFocus : false, - }; - - bindCtx(this, ['onTriggerKeyDown', 'onMenuClick', 'onVisibleChange']); - } - - getVisible(props = this.props) { - return 'visible' in props ? props.visible : this.state.visible; - } - - onMenuClick() { - this.onVisibleChange(false, 'fromContent'); - } - - onVisibleChange(visible, from) { - this.setState({ visible }); - - this.props.onVisibleChange(visible, from); - } - - onTriggerKeyDown() { - let autoFocus = true; - - if ('autoFocus' in this.props) { - autoFocus = this.props.autoFocus; - } - - this.setState({ - autoFocus, - }); - } - - render() { - let child = Children.only(this.props.children); - if (typeof child.type === 'function' && child.type.isNextMenu) { - child = React.cloneElement(child, { - onItemClick: makeChain( - this.onMenuClick, - child.props.onItemClick - ), - }); +import Dropdown from './dropdown'; + +export default ConfigProvider.config(Dropdown, { + transform: /* istanbul ignore next */ (props, deprecated) => { + if ('triggerType' in props) { + const triggerType = Array.isArray(props.triggerType) + ? [...props.triggerType] + : [props.triggerType]; + + if (triggerType.indexOf('focus') > -1) { + deprecated( + 'triggerType[focus]', + 'triggerType[hover, click]', + 'Balloon' + ); + } } - const { trigger, rtl } = this.props; - const newTrigger = React.cloneElement(trigger, { - onKeyDown: makeChain( - this.onTriggerKeyDown, - trigger.props.onKeyDown - ), - }); - - return ( - - {child} - - ); - } -} - -export default ConfigProvider.config(Dropdown); + return props; + }, +}); diff --git a/test/dropdown/index-spec.js b/test/dropdown/index-spec.js index 7d20a0403e..13772aa5ef 100644 --- a/test/dropdown/index-spec.js +++ b/test/dropdown/index-spec.js @@ -184,8 +184,8 @@ describe('Dropdown', () => { document.querySelectorAll('.next-menu-item')[0] ); - ReactTestUtils.Simulate.keyDown(document.activeElement, { - keyCode: KEYCODE.DOWN, + ReactTestUtils.Simulate.keyDown(trigger, { + keyCode: KEYCODE.SPACE, }); setTimeout(() => { @@ -209,7 +209,6 @@ describe('Dropdown', () => { ReactDOM.render( Hello dropdown} animation={false} > @@ -250,7 +249,7 @@ describe('Dropdown', () => { ReactDOM.render( Hello dropdown} animation={false} >