Merge pull request #8926 from Tainan404/virtualized-userlist

Virtualized user list
This commit is contained in:
Anton Georgiev 2020-08-12 14:12:48 -04:00 committed by GitHub
commit b03f29497f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 602 additions and 76 deletions

View File

@ -56,6 +56,21 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
border: 0;
}
.set-z-index {
z-index: 15;
width: 100% !important;
height: 100% !important;
}
.remove-z-index {
z-index: 0;
}
/* .full-screen {
height: 100% !important;
width: 100% !important;
transform: translateX(0) translateY(0) translateZ(0) !important;
} */
[hidden]:not([hidden="false"]) {
display: none !important;
}
@ -73,6 +88,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
</head>
<body style="background-color: #06172A">
<div id="app" role="document"></div>
<span id="destination"></span>
<audio id="remote-media" autoPlay="autoplay">
<track kind="captions" /> {/* These captions are brought to you by eslint */}
</audio>

View File

@ -1,6 +1,8 @@
import React, { Component } from 'react';
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { findDOMNode } from 'react-dom';
import { isMobile } from 'react-device-detect';
import TetherComponent from 'react-tether';
import cx from 'classnames';
import { defineMessages, injectIntl, intlShape } from 'react-intl';
import Button from '/imports/ui/components/button/component';
@ -16,7 +18,7 @@ const intlMessages = defineMessages({
},
});
const noop = () => {};
const noop = () => { };
const propTypes = {
/**
@ -51,6 +53,7 @@ const propTypes = {
onShow: PropTypes.func,
autoFocus: PropTypes.bool,
intl: intlShape.isRequired,
tethered: PropTypes.bool,
};
const defaultProps = {
@ -60,6 +63,16 @@ const defaultProps = {
autoFocus: false,
isOpen: false,
keepOpen: null,
getContent: () => {},
};
const attachments = {
'right-bottom': 'bottom left',
'right-top': 'bottom left',
};
const targetAttachments = {
'right-bottom': 'bottom right',
'right-top': 'top right',
};
class Dropdown extends Component {
@ -162,10 +175,24 @@ class Dropdown extends Component {
className,
intl,
keepOpen,
tethered,
placement,
getContent,
...otherProps
} = this.props;
const { isOpen } = this.state;
const placements = placement && placement.replace(' ', '-');
const test = isMobile ? {
width: '100%',
height: '100%',
transform: 'translateY(0)',
} : {
width: '',
height: '',
transform: '',
};
let trigger = children.find(x => x.type === DropdownTrigger);
let content = children.find(x => x.type === DropdownContent);
@ -176,15 +203,20 @@ class Dropdown extends Component {
dropdownToggle: this.handleToggle,
dropdownShow: this.handleShow,
dropdownHide: this.handleHide,
keepOpen,
});
content = React.cloneElement(content, {
ref: (ref) => { this.content = ref; },
ref: (ref) => {
getContent(ref);
this.content = ref;
},
'aria-expanded': isOpen,
dropdownIsOpen: isOpen,
dropdownToggle: this.handleToggle,
dropdownShow: this.handleShow,
dropdownHide: this.handleHide,
keepOpen,
});
const showCloseBtn = (isOpen && keepOpen) || (isOpen && keepOpen === null);
@ -199,18 +231,67 @@ class Dropdown extends Component {
ref={(node) => { this.dropdown = node; }}
tabIndex={-1}
>
{trigger}
{content}
{showCloseBtn
? (
<Button
className={styles.close}
label={intl.formatMessage(intlMessages.close)}
size="lg"
color="default"
onClick={this.handleHide}
/>
) : null}
{
tethered ?
(
<TetherComponent
style={{
zIndex: isOpen ? 15 : '',
...test,
}}
attachment={
isMobile ? 'middle bottom'
: attachments[placements]
}
targetAttachment={
isMobile ? ''
: targetAttachments[placements]
}
constraints={[
{
to: 'scrollParent',
},
]}
renderTarget={ref => (
<span ref={ref}>
{trigger}
</span>)}
renderElement={ref => (
<div
ref={ref}
>
{content}
{showCloseBtn
? (
<Button
className={styles.close}
label={intl.formatMessage(intlMessages.close)}
size="lg"
color="default"
onClick={this.handleHide}
/>
) : null}
</div>
)
}
/>)
: (
<Fragment>
{trigger}
{content}
{showCloseBtn
? (
<Button
className={styles.close}
label={intl.formatMessage(intlMessages.close)}
size="lg"
color="default"
onClick={this.handleHide}
/>
) : null}
</Fragment>
)
}
</div>
);
}

View File

@ -26,8 +26,14 @@ const defaultProps = {
export default class DropdownContent extends Component {
render() {
const {
placement, children, className,
dropdownToggle, dropdownShow, dropdownHide, dropdownIsOpen,
placement,
children,
className,
dropdownToggle,
dropdownShow,
dropdownHide,
dropdownIsOpen,
keepOpen,
...restProps
} = this.props;
@ -38,6 +44,7 @@ export default class DropdownContent extends Component {
dropdownToggle,
dropdownShow,
dropdownHide,
keepOpen,
}));
return (

View File

@ -45,8 +45,8 @@ export default class DropdownList extends Component {
}
componentDidUpdate() {
const { focusedIndex } = this.state;
const { focusedIndex } = this.state;
const children = [].slice.call(this._menu.children);
this.menuRefs = children.filter(child => child.getAttribute('role') === 'menuitem');
@ -126,13 +126,14 @@ export default class DropdownList extends Component {
}
handleItemClick(event, callback) {
const { getDropdownMenuParent, onActionsHide, dropdownHide } = this.props;
if (getDropdownMenuParent) {
onActionsHide();
} else {
this.setState({ focusedIndex: null });
dropdownHide();
const { getDropdownMenuParent, onActionsHide, dropdownHide, keepOpen} = this.props;
if(!keepOpen) {
if (getDropdownMenuParent) {
onActionsHide();
} else {
this.setState({ focusedIndex: null });
dropdownHide();
}
}
if (typeof callback === 'function') {

View File

@ -435,14 +435,30 @@ const muteAllExceptPresenter = (userId) => { makeCall('muteAllExceptPresenter',
const changeRole = (userId, role) => { makeCall('changeRole', userId, role); };
const roving = (event, changeState, elementsList, element) => {
const focusFirstDropDownItem = () => {
const dropdownContent = document.querySelector('div[data-test="dropdownContent"][style="visibility: visible;"]');
if (!dropdownContent) return;
const list = dropdownContent.getElementsByTagName('li');
list[0].focus();
};
const roving = (...args) => {
const [
event,
changeState,
elementsList,
element,
] = args;
this.selectedElement = element;
const numberOfChilds = elementsList.childElementCount;
const menuOpen = Session.get('dropdownOpen') || false;
if (menuOpen) {
const menuChildren = document.activeElement.getElementsByTagName('li');
if ([KEY_CODES.ESCAPE, KEY_CODES.ARROW_LEFT].includes(event.keyCode)) {
Session.set('dropdownOpen', false);
document.activeElement.click();
}
@ -463,13 +479,15 @@ const roving = (event, changeState, elementsList, element) => {
}
if ([KEY_CODES.ESCAPE, KEY_CODES.TAB].includes(event.keyCode)) {
Session.set('dropdownOpen', false);
document.activeElement.blur();
changeState(null);
}
if (event.keyCode === KEY_CODES.ARROW_DOWN) {
const firstElement = elementsList.firstChild;
let elRef = element ? element.nextSibling : firstElement;
let elRef = element && numberOfChilds > 1 ? element.nextSibling : firstElement;
elRef = elRef || firstElement;
changeState(elRef);
}
@ -482,7 +500,10 @@ const roving = (event, changeState, elementsList, element) => {
}
if ([KEY_CODES.ARROW_RIGHT, KEY_CODES.SPACE, KEY_CODES.ENTER].includes(event.keyCode)) {
document.activeElement.firstChild.click();
const tether = document.activeElement.firstChild;
const dropdownTrigger = tether.firstChild;
dropdownTrigger.click();
focusFirstDropDownItem();
}
};
@ -549,4 +570,5 @@ export default {
hasPrivateChatBetweenUsers,
toggleUserLock,
requestUserInformation,
focusFirstDropDownItem,
};

View File

@ -144,6 +144,7 @@
.userListColumn {
@extend %flex-column;
min-height: 0;
flex-grow: 1;
}
.enter,

View File

@ -37,6 +37,35 @@
outline-color: transparent !important;
}
.virtulizedScrollableList {
@include elementFocus(var(--list-item-bg-hover));
@include scrollbox-vertical(var(--user-list-bg));
@include highContrastOutline();
&:focus-within,
&:focus {
outline-style: solid;
}
&:active {
box-shadow: none;
border-radius: none;
}
outline-width: 1px !important;
outline-color: transparent !important;
flex-grow: 1;
flex-shrink: 1;
margin: 0 0 1px var(--md-padding-y);
[dir="rtl"] & {
margin: 0 var(--md-padding-y) 1px 0;
}
margin-left: 0;
}
.list {
margin: 0 0 1px var(--md-padding-y);
@ -152,6 +181,11 @@
flex-shrink: 1;
}
.scrollStyle {
@include scrollbox-vertical($bg-color: #f3f6f9);
}
.noteLock {
font-weight: 200;
font-size: var(--font-size-smaller);

View File

@ -1,11 +1,15 @@
import React, { Component } from 'react';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import { defineMessages } from 'react-intl';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { styles } from '/imports/ui/components/user-list/user-list-content/styles';
import _ from 'lodash';
import { findDOMNode } from 'react-dom';
import {
List,
AutoSizer,
CellMeasurer,
CellMeasurerCache,
} from 'react-virtualized';
import UserListItemContainer from './user-list-item/container';
import UserOptionsContainer from './user-options/container';
@ -47,8 +51,15 @@ class UserParticipants extends Component {
constructor() {
super();
this.cache = new CellMeasurerCache({
fixedWidth: true,
keyMapper: () => 1,
});
this.state = {
selectedUser: null,
isOpen: false,
scrollArea: false,
};
this.userRefs = [];
@ -56,7 +67,7 @@ class UserParticipants extends Component {
this.getScrollContainerRef = this.getScrollContainerRef.bind(this);
this.rove = this.rove.bind(this);
this.changeState = this.changeState.bind(this);
this.getUsers = this.getUsers.bind(this);
this.rowRenderer = this.rowRenderer.bind(this);
this.handleClickSelectedUser = this.handleClickSelectedUser.bind(this);
}
@ -82,13 +93,20 @@ class UserParticipants extends Component {
}
componentDidUpdate(prevProps, prevState) {
const { selectedUser } = this.state;
if (selectedUser === prevState.selectedUser) return;
const { compact } = this.props;
const { selectedUser, scrollArea } = this.state;
if (!compact && (!prevState.scrollArea && scrollArea)) {
scrollArea.addEventListener(
'keydown',
this.rove,
);
}
if (selectedUser) {
const { firstChild } = selectedUser;
if (firstChild) firstChild.focus();
if (!firstChild.isEqualNode(document.activeElement)) {
firstChild.focus();
}
}
}
@ -101,7 +119,12 @@ class UserParticipants extends Component {
return this.refScrollContainer;
}
getUsers() {
rowRenderer({
index,
parent,
style,
key,
}) {
const {
compact,
setEmojiStatus,
@ -110,21 +133,22 @@ class UserParticipants extends Component {
currentUser,
meetingIsBreakout,
} = this.props;
const { scrollArea } = this.state;
const user = users[index];
let index = -1;
return users.map(u => (
<CSSTransition
classNames={listTransition}
appear
enter
exit
timeout={0}
component="div"
className={cx(styles.participantsList)}
key={u.userId}
return (
<CellMeasurer
key={key}
cache={this.cache}
columnIndex={0}
parent={parent}
rowIndex={index}
>
<div ref={(node) => { this.userRefs[index += 1] = node; }}>
<span
style={style}
key={key}
id={`user-${user.userId}`}
>
<UserListItemContainer
{...{
compact,
@ -132,13 +156,14 @@ class UserParticipants extends Component {
requestUserInformation,
currentUser,
meetingIsBreakout,
scrollArea,
}}
user={u}
user={user}
getScrollContainerRef={this.getScrollContainerRef}
/>
</div>
</CSSTransition>
));
</span>
</CellMeasurer>
);
}
handleClickSelectedUser(event) {
@ -151,8 +176,9 @@ class UserParticipants extends Component {
rove(event) {
const { roving } = this.props;
const { selectedUser } = this.state;
const usersItemsRef = findDOMNode(this.refScrollItems);
const { selectedUser, scrollArea } = this.state;
const usersItemsRef = findDOMNode(scrollArea.firstChild);
roving(event, this.changeState, usersItemsRef, selectedUser);
}
@ -169,6 +195,7 @@ class UserParticipants extends Component {
currentUser,
meetingIsBreakout,
} = this.props;
const { isOpen, scrollArea } = this.state;
return (
<div className={styles.userListColumn}>
@ -198,15 +225,40 @@ class UserParticipants extends Component {
: <hr className={styles.separator} />
}
<div
className={styles.scrollableList}
className={styles.virtulizedScrollableList}
tabIndex={0}
ref={(ref) => { this.refScrollContainer = ref; }}
ref={(ref) => {
this.refScrollContainer = ref;
}}
>
<div className={styles.list}>
<TransitionGroup ref={(ref) => { this.refScrollItems = ref; }}>
{this.getUsers()}
</TransitionGroup>
</div>
<span id="destination" />
<AutoSizer>
{({ height, width }) => (
<List
{...{
isOpen,
users,
}}
ref={(ref) => {
if (ref !== null) {
this.listRef = ref;
}
if (ref !== null && !scrollArea) {
this.setState({ scrollArea: findDOMNode(ref) });
}
}}
rowHeight={this.cache.rowHeight}
rowRenderer={this.rowRenderer}
rowCount={users.length}
height={height - 1}
width={width - 1}
className={styles.scrollStyle}
overscanRowCount={30}
deferredMeasurementCache={this.cache}
/>
)}
</AutoSizer>
</div>
</div>
);

View File

@ -45,6 +45,10 @@ class UserListItem extends PureComponent {
isMeteorConnected,
isMe,
voiceUser,
scrollArea,
notify,
raiseHandAudioAlert,
raiseHandPushAlert,
} = this.props;
const contents = (
@ -76,6 +80,10 @@ class UserListItem extends PureComponent {
isMeteorConnected,
isMe,
voiceUser,
scrollArea,
notify,
raiseHandAudioAlert,
raiseHandPushAlert,
}}
/>
);

View File

@ -4,12 +4,12 @@ import PropTypes from 'prop-types';
import { findDOMNode } from 'react-dom';
import UserAvatar from '/imports/ui/components/user-avatar/component';
import Icon from '/imports/ui/components/icon/component';
import Dropdown from '/imports/ui/components/dropdown/component';
import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component';
import DropdownContent from '/imports/ui/components/dropdown/content/component';
import DropdownList from '/imports/ui/components/dropdown/list/component';
import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component';
import Dropdown from '/imports/ui/components/dropdown/component';
import lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
import { withModalMounter } from '/imports/ui/components/modal/service';
import RemoveUserModal from '/imports/ui/components/modal/remove-user/component';
@ -18,6 +18,7 @@ import { Session } from 'meteor/session';
import { styles } from './styles';
import UserName from '../user-name/component';
import UserIcons from '../user-icons/component';
import Service from '../../../../service';
const messages = defineMessages({
presenter: {
@ -305,7 +306,10 @@ class UserDropdown extends PureComponent {
{
showNestedOptions: true,
isActionsOpen: true,
}, Session.set('dropdownOpen', true),
}, () => {
Session.set('dropdownOpen', true);
Service.focusFirstDropDownItem();
},
),
'user',
'right_arrow',
@ -468,26 +472,24 @@ class UserDropdown extends PureComponent {
* Check if the dropdown is visible, if so, check if should be draw on top or bottom direction.
*/
checkDropdownDirection() {
const { getScrollContainerRef } = this.props;
const { scrollArea } = this.props;
if (this.isDropdownActivedByUser()) {
const dropdown = this.getDropdownMenuParent();
const dropdownTrigger = dropdown.children[0];
const dropdownContent = dropdown.children[1];
const scrollContainer = getScrollContainerRef();
const nextState = {
dropdownVisible: true,
};
const dropdownContent = findDOMNode(this.dropdownContent);
const dropdownBoundaries = dropdownContent.getBoundingClientRect();
const isDropdownVisible = UserDropdown.checkIfDropdownIsVisible(
dropdownContent.offsetTop,
dropdownContent.offsetHeight,
dropdownBoundaries.y,
dropdownBoundaries.height,
);
if (!isDropdownVisible) {
if (!isDropdownVisible && scrollArea) {
const { offsetTop, offsetHeight } = dropdownTrigger;
const offsetPageTop = (offsetTop + offsetHeight) - scrollContainer.scrollTop;
const offsetPageTop = (offsetTop + offsetHeight) - scrollArea.scrollTop;
nextState.dropdownOffset = window.innerHeight - offsetPageTop;
nextState.dropdownDirection = 'bottom';
@ -620,7 +622,7 @@ class UserDropdown extends PureComponent {
);
if (!actions.length) return contents;
const placement = `right ${dropdownDirection}`;
return (
<Dropdown
ref={(ref) => { this.dropdown = ref; }}
@ -632,6 +634,9 @@ class UserDropdown extends PureComponent {
aria-haspopup="true"
aria-live="assertive"
aria-relevant="additions"
placement={placement}
getContent={dropdownContent => this.dropdownContent = dropdownContent}
tethered
>
<DropdownTrigger>
{contents}
@ -639,10 +644,9 @@ class UserDropdown extends PureComponent {
<DropdownContent
style={{
visibility: dropdownVisible ? 'visible' : 'hidden',
[dropdownDirection]: `${dropdownOffset}px`,
}}
className={styles.dropdownContent}
placement={`right ${dropdownDirection}`}
placement={placement}
>
<DropdownList
ref={(ref) => { this.list = ref; }}

View File

@ -133,6 +133,12 @@
@extend %text-elipsis;
cursor: default;
min-width: 10vw;
@include mq($medium-only) {
min-width: 13vw;
}
@include mq($large-up) {
min-width: 8vw;
}
max-width: 100%;
overflow: visible;
}

View File

@ -0,0 +1,279 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { findDOMNode } from 'react-dom';
import cx from 'classnames';
import { isMobile } from 'react-device-detect';
import { defineMessages, injectIntl, intlShape } from 'react-intl';
import Button from '/imports/ui/components/button/component';
import screenreaderTrap from 'makeup-screenreader-trap';
import TetherComponent from 'react-tether';
import { styles } from '/imports/ui/components/dropdown/styles';
import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component';
import DropdownContent from '/imports/ui/components/dropdown/content/component';
const intlMessages = defineMessages({
close: {
id: 'app.dropdown.close',
description: 'Close button label',
},
});
const noop = () => { };
const propTypes = {
/**
* The dropdown needs a trigger and a content component as children
*/
children: (props, propName, componentName) => {
const children = props[propName];
if (!children || children.length < 2) {
return new Error(`Invalid prop \`${propName}\` supplied to`
+ ` \`${componentName}\`. Validation failed.`);
}
const trigger = children.find(x => x.type === DropdownTrigger);
const content = children.find(x => x.type === DropdownContent);
if (!trigger) {
return new Error(`Invalid prop \`${propName}\` supplied to`
+ ` \`${componentName}\`. Missing \`DropdownTrigger\`. Validation failed.`);
}
if (!content) {
return new Error(`Invalid prop \`${propName}\` supplied to`
+ ` \`${componentName}\`. Missing \`DropdownContent\`. Validation failed.`);
}
return null;
},
isOpen: PropTypes.bool,
keepOpen: PropTypes.bool,
onHide: PropTypes.func,
onShow: PropTypes.func,
autoFocus: PropTypes.bool,
intl: intlShape.isRequired,
};
const defaultProps = {
children: null,
onShow: noop,
onHide: noop,
autoFocus: false,
isOpen: false,
keepOpen: null,
};
const attachments = {
'right-bottom': 'bottom left',
'right-top': 'bottom left',
};
const targetAttachments = {
'right-bottom': 'bottom right',
'right-top': 'top right',
};
class Dropdown extends Component {
constructor(props) {
super(props);
this.state = { isOpen: false };
this.handleShow = this.handleShow.bind(this);
this.handleHide = this.handleHide.bind(this);
this.handleToggle = this.handleToggle.bind(this);
this.handleWindowClick = this.handleWindowClick.bind(this);
}
componentWillUpdate(nextProps, nextState) {
return nextState.isOpen ? screenreaderTrap.trap(this.dropdown) : screenreaderTrap.untrap();
}
componentDidUpdate(prevProps, prevState) {
const {
onShow,
onHide,
keepOpen,
} = this.props;
const { isOpen } = this.state;
if (isOpen && !prevState.isOpen) { onShow(); }
if (!isOpen && prevState.isOpen) { onHide(); }
if (prevProps.keepOpen && !keepOpen) { onHide(); }
}
handleShow() {
Session.set('dropdownOpen', true);
const {
onShow,
} = this.props;
this.setState({ isOpen: true }, () => {
const { addEventListener } = window;
onShow();
addEventListener('click', this.handleWindowClick, true);
});
}
handleHide() {
Session.set('dropdownOpen', false);
const { onHide } = this.props;
this.setState({ isOpen: false }, () => {
const { removeEventListener } = window;
onHide();
removeEventListener('click', this.handleWindowClick, true);
});
}
handleWindowClick(event) {
const { keepOpen, onHide } = this.props;
const { isOpen } = this.state;
const triggerElement = findDOMNode(this.trigger);
const contentElement = findDOMNode(this.content);
if (!(triggerElement && contentElement)) return;
if (triggerElement && triggerElement.contains(event.target)) {
if (keepOpen) {
onHide();
return;
}
if (isOpen) {
this.handleHide();
return;
}
}
if (keepOpen && isOpen && !contentElement.contains(event.target)) {
if (triggerElement) {
const { parentElement } = triggerElement;
if (parentElement) parentElement.focus();
}
onHide();
this.handleHide();
return;
}
if (keepOpen && triggerElement) {
const { parentElement } = triggerElement;
if (parentElement) parentElement.focus();
}
if (keepOpen !== null) return;
this.handleHide();
}
handleToggle() {
const { isOpen } = this.state;
return isOpen ? this.handleHide() : this.handleShow();
}
render() {
const {
children,
className,
intl,
keepOpen,
getContent,
placement,
...otherProps
} = this.props;
const { isOpen } = this.state;
let trigger = children.find(x => x.type === DropdownTrigger);
let content = children.find(x => x.type === DropdownContent);
trigger = React.cloneElement(trigger, {
ref: (ref) => { this.trigger = ref; },
dropdownIsOpen: isOpen,
dropdownToggle: this.handleToggle,
dropdownShow: this.handleShow,
dropdownHide: this.handleHide,
});
content = React.cloneElement(content, {
ref: (ref) => {
getContent(ref);
this.content = ref;
},
keepOpen,
'aria-expanded': isOpen,
dropdownIsOpen: isOpen,
dropdownToggle: this.handleToggle,
dropdownShow: this.handleShow,
dropdownHide: this.handleHide,
});
const showCloseBtn = (isOpen && keepOpen) || (isOpen && keepOpen === null);
const placements = placement.replace(' ', '-');
// workaround
const test = isMobile ? {
width: '100%',
height: '100%',
transform: 'translateY(0)',
} : {
width: '',
height: '',
transform: '',
};
return (
<div
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']}
ref={(node) => { this.dropdown = node; }}
tabIndex={-1}
>
<TetherComponent
style={{
zIndex: isOpen ? 15 : '',
...test,
}}
attachment={
isMobile ? 'middle bottom'
: attachments[placements]
}
targetAttachment={
isMobile ? ''
: targetAttachments[placements]
}
constraints={[
{
to: 'scrollParent',
},
]}
renderTarget={ref => (
<span ref={ref}>
{trigger}
</span>)}
renderElement={ref => (
<div
ref={ref}
>
{content}
{showCloseBtn
? (
<Button
className={styles.close}
label={intl.formatMessage(intlMessages.close)}
size="lg"
color="default"
onClick={this.handleHide}
/>
) : null}
</div>
)
}
/>
</div>
);
}
}
Dropdown.propTypes = propTypes;
Dropdown.defaultProps = defaultProps;
export default injectIntl(Dropdown);

View File

@ -8899,6 +8899,15 @@
"prop-types": "^15.5.0"
}
},
"react-tether": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/react-tether/-/react-tether-2.0.7.tgz",
"integrity": "sha512-OZAMoT0y1//SN357HiJKic+Ax/kMe3CwdaDT+05P/DHMR9adTYH2RTMDZMjw/OGMmLlBFg6UrDFXiulVKKIBRw==",
"requires": {
"prop-types": "^15.6.2",
"tether": "^1.4.5"
}
},
"react-toastify": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-4.5.2.tgz",
@ -10291,6 +10300,11 @@
"minimatch": "^3.0.4"
}
},
"tether": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/tether/-/tether-1.4.7.tgz",
"integrity": "sha512-Z0J1aExjoFU8pybVkQAo/vD2wfSO63r+XOPfWQMC5qtf1bI7IWqNk4MiyBcgvvnY8kqnY06dVdvwTK2S3PU/Fw=="
},
"text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",

View File

@ -67,6 +67,7 @@
"react-player": "^2.5.0",
"react-render-in-browser": "^1.1.1",
"react-tabs": "^2.3.1",
"react-tether": "^2.0.7",
"react-toastify": "^4.5.2",
"react-toggle": "~4.0.2",
"react-transition-group": "^2.9.0",