diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx index fb519b7669..e8066568ab 100644 --- a/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { findDOMNode } from 'react-dom'; import cx from 'classnames'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, intlShape } from 'react-intl'; import Button from '/imports/ui/components/button/component'; import screenreaderTrap from 'makeup-screenreader-trap'; import { styles } from './styles'; @@ -26,37 +26,40 @@ const propTypes = { const children = props[propName]; if (!children || children.length < 2) { - return new Error(`Invalid prop \`${propName}\` supplied to` + - ` \`${componentName}\`. Validation failed.`); + 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.`); + 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 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, - keepOpen: null, onShow: noop, onHide: noop, autoFocus: false, + isOpen: false, + keepOpen: false, }; class Dropdown extends Component { @@ -82,6 +85,7 @@ class Dropdown extends Component { const { isOpen } = this.state; + if (isOpen && !prevState.isOpen) { onShow(); } if (!isOpen && prevState.isOpen) { onHide(); } @@ -90,6 +94,7 @@ class Dropdown extends Component { } handleShow() { + Session.set('dropdownOpen', true); const { onShow, } = this.props; @@ -101,6 +106,7 @@ class Dropdown extends Component { } handleHide() { + Session.set('dropdownOpen', false); const { onHide } = this.props; this.setState({ isOpen: false }, () => { const { removeEventListener } = window; @@ -115,28 +121,33 @@ class Dropdown extends Component { const triggerElement = findDOMNode(this.trigger); const contentElement = findDOMNode(this.content); if (!(triggerElement && contentElement)) return; - if (keepOpen === null) { - if (triggerElement.contains(event.target)) { + if (triggerElement && triggerElement.contains(event.target)) { + if (keepOpen) { + onHide(); + return; + } + if (isOpen) { + this.handleHide(); return; } } - if (triggerElement && triggerElement.contains(event.target)) { - if (keepOpen) return onHide(); - if (isOpen) return this.handleHide(); - } - if (keepOpen && isOpen && !contentElement.contains(event.target)) { + if (triggerElement) { + const { parentElement } = triggerElement; + if (parentElement) parentElement.focus(); + } onHide(); this.handleHide(); return; } - - if (keepOpen !== null) { - return; + + if (keepOpen && triggerElement) { + const { parentElement } = triggerElement; + if (parentElement) parentElement.focus(); } - this.handleHide(); + if (!keepOpen) this.handleHide(); } handleToggle() { @@ -148,12 +159,11 @@ class Dropdown extends Component { const { children, className, - style, intl, keepOpen, ...otherProps } = this.props; - + const { isOpen } = this.state; let trigger = children.find(x => x.type === DropdownTrigger); @@ -175,31 +185,31 @@ class Dropdown extends Component { dropdownShow: this.handleShow, dropdownHide: this.handleHide, }); - + const showCloseBtn = (isOpen && keepOpen) || (isOpen && keepOpen === null); return (
{ this.dropdown = node; }} tabIndex={-1} > {trigger} {content} - {showCloseBtn ? -
); } diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js index 9cde5a3dbb..64f9079320 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/service.js +++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js @@ -394,8 +394,11 @@ const muteAllExceptPresenter = (userId) => { makeCall('muteAllExceptPresenter', const changeRole = (userId, role) => { makeCall('changeRole', userId, role); }; -const roving = (event, itemCount, changeState) => { - if (document.activeElement.getAttribute('data-isopen') === 'true') { +const roving = (event, changeState, elementsList, element) => { + this.selectedElement = element; + 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)) { @@ -418,34 +421,23 @@ const roving = (event, itemCount, changeState) => { return; } - if (this.selectedIndex === undefined) { - this.selectedIndex = -1; - } - if ([KEY_CODES.ESCAPE, KEY_CODES.TAB].includes(event.keyCode)) { document.activeElement.blur(); - this.selectedIndex = -1; - changeState(this.selectedIndex); + changeState(null); } if (event.keyCode === KEY_CODES.ARROW_DOWN) { - this.selectedIndex += 1; - - if (this.selectedIndex === itemCount) { - this.selectedIndex = 0; - } - - changeState(this.selectedIndex); + const firstElement = elementsList.firstChild; + let elRef = element ? element.nextSibling : firstElement; + elRef = elRef || firstElement; + changeState(elRef); } if (event.keyCode === KEY_CODES.ARROW_UP) { - this.selectedIndex -= 1; - - if (this.selectedIndex < 0) { - this.selectedIndex = itemCount - 1; - } - - changeState(this.selectedIndex); + const lastElement = elementsList.lastChild; + let elRef = element ? element.previousSibling : lastElement; + elRef = elRef || lastElement; + changeState(elRef); } if ([KEY_CODES.ARROW_RIGHT, KEY_CODES.SPACE, KEY_CODES.ENTER].includes(event.keyCode)) { diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/component.jsx index 4fe15a6347..48694f9e55 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/component.jsx @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import { defineMessages } from 'react-intl'; import cx from 'classnames'; import { styles } from '/imports/ui/components/user-list/user-list-content/styles'; +import { findDOMNode } from 'react-dom'; import ChatListItemContainer from '../../chat-list-item/container'; const propTypes = { @@ -41,38 +42,31 @@ class UserMessages extends PureComponent { super(); this.state = { - index: -1, + selectedChat: null, }; this.activeChatRefs = []; - this.selectedIndex = -1; - this.focusActiveChatItem = this.focusActiveChatItem.bind(this); this.changeState = this.changeState.bind(this); + this.rove = this.rove.bind(this); } componentDidMount() { - const { compact, roving, activeChats } = this.props; + const { compact } = this.props; if (!compact) { this._msgsList.addEventListener( 'keydown', - event => roving( - event, - activeChats.length, - this.changeState, - ), + this.rove, ); } } componentDidUpdate(prevProps, prevState) { - const { index } = this.state; - if (index === -1) { - return; - } + const { selectedChat } = this.state; - if (index !== prevState.index) { - this.focusActiveChatItem(index); + if (selectedChat && selectedChat !== prevState.selectedChat) { + const { firstChild } = selectedChat; + if (firstChild) firstChild.focus(); } } @@ -108,16 +102,15 @@ class UserMessages extends PureComponent { )); } - changeState(newIndex) { - this.setState({ index: newIndex }); + changeState(ref) { + this.setState({ selectedChat: ref }); } - focusActiveChatItem(index) { - if (!this.activeChatRefs[index]) { - return; - } - - this.activeChatRefs[index].firstChild.focus(); + rove(event) { + const { roving } = this.props; + const { selectedChat } = this.state; + const msgItemsRef = findDOMNode(this._msgItems); + roving(event, this.changeState, msgItemsRef, selectedChat); } render() { diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx index cf4e37a004..30b247865a 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx @@ -5,6 +5,7 @@ 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 UserListItemContainer from './user-list-item/container'; import UserOptionsContainer from './user-options/container'; @@ -62,46 +63,43 @@ class UserParticipants extends Component { super(); this.state = { - index: -1, + selectedUser: null, }; this.userRefs = []; - this.selectedIndex = -1; this.getScrollContainerRef = this.getScrollContainerRef.bind(this); - this.focusUserItem = this.focusUserItem.bind(this); + this.rove = this.rove.bind(this); this.changeState = this.changeState.bind(this); this.getUsers = this.getUsers.bind(this); } componentDidMount() { - const { compact, roving, users } = this.props; + const { compact } = this.props; if (!compact) { this.refScrollContainer.addEventListener( 'keydown', - event => roving( - event, - users.length, - this.changeState, - ), + this.rove, ); } } + componentWillUnmount() { + this.refScrollContainer.removeEventListener('keydown', this.rove); + } + shouldComponentUpdate(nextProps, nextState) { const isPropsEqual = _.isEqual(this.props, nextProps); const isStateEqual = _.isEqual(this.state, nextState); return !isPropsEqual || !isStateEqual; } - componentDidUpdate(prevProps, prevState) { - const { index } = this.state; - if (index === -1) { - return; - } + componentDidUpdate() { + const { selectedUser } = this.state; - if (index !== prevState.index) { - this.focusUserItem(index); + if (selectedUser) { + const { firstChild } = selectedUser; + if (firstChild) firstChild.focus(); } } @@ -177,14 +175,15 @@ class UserParticipants extends Component { )); } - focusUserItem(index) { - if (!this.userRefs[index]) return; - - this.userRefs[index].firstChild.focus(); + rove(event) { + const { roving } = this.props; + const { selectedUser } = this.state; + const usersItemsRef = findDOMNode(this.refScrollItems); + roving(event, this.changeState, usersItemsRef, selectedUser); } - changeState(newIndex) { - this.setState({ index: newIndex }); + changeState(ref) { + this.setState({ selectedUser: ref }); } render() { @@ -230,7 +229,6 @@ class UserParticipants extends Component { }
{ this.refScrollContainer = ref; }} > diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx index 391e84967f..a4eb8dd8e9 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx @@ -153,10 +153,13 @@ class UserDropdown extends PureComponent { } componentDidUpdate() { + const { dropdownVisible } = this.props; + if (!dropdownVisible) document.activeElement.blur(); this.checkDropdownDirection(); } onActionsShow() { + Session.set('dropdownOpen', true); const { getScrollContainerRef } = this.props; const dropdown = this.getDropdownMenuParent(); const scrollContainer = getScrollContainerRef(); @@ -193,6 +196,8 @@ class UserDropdown extends PureComponent { if (callback) { return callback; } + + return Session.set('dropdownOpen', false); } getUsersActions() { @@ -251,7 +256,12 @@ class UserDropdown extends PureComponent { actions.push(this.makeDropdownItem( 'back', intl.formatMessage(messages.backTriggerLabel), - () => this.setState({ showNestedOptions: false, isActionsOpen: true }), + () => this.setState( + { + showNestedOptions: false, + isActionsOpen: true, + }, Session.set('dropdownOpen', true), + ), 'left_arrow', )); } @@ -273,7 +283,12 @@ class UserDropdown extends PureComponent { actions.push(this.makeDropdownItem( 'setstatus', intl.formatMessage(messages.statusTriggerLabel), - () => this.setState({ showNestedOptions: true, isActionsOpen: true }), + () => this.setState( + { + showNestedOptions: true, + isActionsOpen: true, + }, Session.set('dropdownOpen', true), + ), 'user', 'right_arrow', )); @@ -440,7 +455,8 @@ class UserDropdown extends PureComponent { ); if (!isDropdownVisible) { - const offsetPageTop = (dropdownTrigger.offsetTop + dropdownTrigger.offsetHeight) - scrollContainer.scrollTop; + const { offsetTop, offsetHeight } = dropdownTrigger; + const offsetPageTop = (offsetTop + offsetHeight) - scrollContainer.scrollTop; nextState.dropdownOffset = window.innerHeight - offsetPageTop; nextState.dropdownDirection = 'bottom';