bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx

211 lines
5.4 KiB
React
Raw Normal View History

import React, { Component } from 'react';
import PropTypes from 'prop-types';
2016-09-02 04:19:37 +08:00
import { findDOMNode } from 'react-dom';
import cx from 'classnames';
import { defineMessages, injectIntl } from 'react-intl';
import Button from '/imports/ui/components/button/component';
2018-04-24 11:48:34 +08:00
import screenreaderTrap from 'makeup-screenreader-trap';
2018-01-08 14:17:18 +08:00
import { styles } from './styles';
2016-09-02 04:19:37 +08:00
import DropdownTrigger from './trigger/component';
import DropdownContent from './content/component';
2017-03-16 04:11:34 +08:00
const intlMessages = defineMessages({
close: {
id: 'app.dropdown.close',
2017-04-10 23:50:03 +08:00
description: 'Close button label',
2017-03-16 04:11:34 +08:00
},
});
const noop = () => {};
2016-09-02 04:19:37 +08:00
const propTypes = {
/**
* The dropdown needs a trigger and a content component as children
*/
2016-09-02 04:19:37 +08:00
children: (props, propName, componentName) => {
const children = props[propName];
if (!children || children.length < 2) {
return new Error(`Invalid prop \`${propName}\` supplied to` +
` \`${componentName}\`. Validation failed.`);
2016-08-20 05:20:17 +08:00
}
2016-09-02 04:19:37 +08:00
const trigger = children.find(x => x.type === DropdownTrigger);
const content = children.find(x => x.type === DropdownContent);
2016-08-20 05:20:17 +08:00
2016-09-02 04:19:37 +08:00
if (!trigger) {
return new Error(`Invalid prop \`${propName}\` supplied to` +
` \`${componentName}\`. Missing \`DropdownTrigger\`. Validation failed.`);
2016-08-20 05:20:17 +08:00
}
2016-09-02 04:19:37 +08:00
if (!content) {
return new Error(`Invalid prop \`${propName}\` supplied to` +
` \`${componentName}\`. Missing \`DropdownContent\`. Validation failed.`);
2016-08-20 05:20:17 +08:00
}
return null;
2016-09-02 04:19:37 +08:00
},
isOpen: PropTypes.bool,
onHide: PropTypes.func,
onShow: PropTypes.func,
autoFocus: PropTypes.bool,
2016-09-02 04:19:37 +08:00
};
2016-08-20 05:20:17 +08:00
const defaultProps = {
children: null,
2018-12-28 04:20:07 +08:00
keepOpen: null,
onShow: noop,
onHide: noop,
2017-05-19 02:38:07 +08:00
autoFocus: false,
};
2017-03-16 04:11:34 +08:00
class Dropdown extends Component {
2016-09-02 04:19:37 +08:00
constructor(props) {
super(props);
2017-06-03 03:25:02 +08:00
this.state = { isOpen: false };
2016-09-02 04:19:37 +08:00
this.handleShow = this.handleShow.bind(this);
this.handleHide = this.handleHide.bind(this);
this.handleToggle = this.handleToggle.bind(this);
this.handleWindowClick = this.handleWindowClick.bind(this);
2016-08-20 05:20:17 +08:00
}
2018-04-24 11:48:34 +08:00
componentWillUpdate(nextProps, nextState) {
2018-04-24 22:43:11 +08:00
return nextState.isOpen ? screenreaderTrap.trap(this.dropdown) : screenreaderTrap.untrap();
2018-04-24 11:48:34 +08:00
}
componentDidUpdate(prevProps, prevState) {
const {
onShow,
onHide,
2019-01-31 19:12:41 +08:00
keepOpen,
} = this.props;
2019-01-31 19:12:41 +08:00
2018-12-28 04:20:07 +08:00
const { isOpen } = this.state;
2018-12-28 04:20:07 +08:00
if (isOpen && !prevState.isOpen) { onShow(); }
2018-12-28 04:20:07 +08:00
if (!isOpen && prevState.isOpen) { onHide(); }
2019-01-31 19:12:41 +08:00
if (prevProps.keepOpen && !keepOpen) onHide();
2016-09-13 04:11:03 +08:00
}
2016-09-02 04:19:37 +08:00
handleShow() {
2019-01-31 19:12:41 +08:00
const {
onShow,
} = this.props;
this.setState({ isOpen: true }, () => {
const { addEventListener } = window;
2019-01-31 19:12:41 +08:00
onShow();
addEventListener('click', this.handleWindowClick, true);
});
2016-08-20 05:20:17 +08:00
}
handleHide() {
2019-01-31 19:12:41 +08:00
const { onHide } = this.props;
2018-07-09 11:51:22 +08:00
this.setState({ isOpen: false }, () => {
const { removeEventListener } = window;
2019-01-31 19:12:41 +08:00
onHide();
2018-07-09 11:51:22 +08:00
removeEventListener('click', this.handleWindowClick, true);
});
2016-08-20 05:20:17 +08:00
}
2018-11-13 00:45:37 +08:00
handleWindowClick(event) {
2018-12-28 04:20:07 +08:00
const { keepOpen, onHide } = this.props;
const { isOpen } = this.state;
const triggerElement = findDOMNode(this.trigger);
const contentElement = findDOMNode(this.content);
2018-12-28 04:20:07 +08:00
if (keepOpen === null) {
if (triggerElement.contains(event.target)) {
return;
}
2018-08-02 01:47:03 +08:00
}
2018-12-28 04:20:07 +08:00
if (triggerElement && triggerElement.contains(event.target)) {
if (keepOpen) return onHide();
if (isOpen) return this.handleHide();
}
if (keepOpen && isOpen && !contentElement.contains(event.target)) {
onHide();
this.handleHide();
return;
}
if (keepOpen !== null) {
2018-08-02 02:26:16 +08:00
return;
2016-09-02 04:19:37 +08:00
}
this.handleHide();
2016-09-02 04:19:37 +08:00
}
2016-08-24 06:19:13 +08:00
2016-09-02 04:19:37 +08:00
handleToggle() {
2018-12-28 04:20:07 +08:00
const { isOpen } = this.state;
return isOpen ? this.handleHide() : this.handleShow();
2016-08-20 05:20:17 +08:00
}
render() {
2017-05-10 23:12:24 +08:00
const {
children,
className,
2017-05-19 02:38:07 +08:00
style,
intl,
2018-12-28 04:20:07 +08:00
keepOpen,
...otherProps
2017-05-10 23:12:24 +08:00
} = this.props;
2018-12-28 04:20:07 +08:00
const { isOpen } = this.state;
2016-08-24 06:19:13 +08:00
2016-09-02 04:19:37 +08:00
let trigger = children.find(x => x.type === DropdownTrigger);
let content = children.find(x => x.type === DropdownContent);
2016-08-24 06:19:13 +08:00
2016-09-02 04:19:37 +08:00
trigger = React.cloneElement(trigger, {
ref: (ref) => { this.trigger = ref; },
2018-12-28 04:20:07 +08:00
dropdownIsOpen: isOpen,
dropdownToggle: this.handleToggle,
dropdownShow: this.handleShow,
dropdownHide: this.handleHide,
});
content = React.cloneElement(content, {
ref: (ref) => { this.content = ref; },
2018-12-28 04:20:07 +08:00
'aria-expanded': isOpen,
dropdownIsOpen: isOpen,
dropdownToggle: this.handleToggle,
dropdownShow: this.handleShow,
dropdownHide: this.handleHide,
2016-09-02 04:19:37 +08:00
});
2018-12-28 04:20:07 +08:00
const showCloseBtn = (isOpen && keepOpen) || (isOpen && keepOpen === null);
2016-08-24 06:19:13 +08:00
2016-09-02 04:19:37 +08:00
return (
2017-05-10 23:12:24 +08:00
<div
2017-05-19 02:38:07 +08:00
style={style}
className={cx(styles.dropdown, className)}
aria-live={otherProps['aria-live']}
aria-relevant={otherProps['aria-relevant']}
aria-haspopup={otherProps['aria-haspopup']}
aria-label={otherProps['aria-label']}
data-isopen={this.state.isOpen}
2018-04-24 11:48:34 +08:00
ref={(node) => { this.dropdown = node; }}
tabIndex={-1}
>
2016-09-02 04:19:37 +08:00
{trigger}
{content}
2018-12-28 04:20:07 +08:00
{showCloseBtn ?
<Button
className={styles.close}
2017-03-16 04:11:34 +08:00
label={intl.formatMessage(intlMessages.close)}
size="lg"
color="default"
onClick={this.handleHide}
/> : null}
2016-08-20 05:20:17 +08:00
</div>
);
}
}
2016-09-02 04:19:37 +08:00
Dropdown.propTypes = propTypes;
Dropdown.defaultProps = defaultProps;
2017-03-16 04:11:34 +08:00
export default injectIntl(Dropdown);