Implements react tether in virtualizedlist

This commit is contained in:
Tainan Felipe 2020-03-30 16:41:36 -03:00
parent b451ce24f8
commit d388fb070d
8 changed files with 180 additions and 58 deletions

View File

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

View File

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

View File

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

View File

@ -417,14 +417,30 @@ const muteAllExceptPresenter = (userId) => { makeCall('muteAllExceptPresenter',
const changeRole = (userId, role) => { makeCall('changeRole', userId, role); }; 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; this.selectedElement = element;
const numberOfChilds = elementsList.childElementCount;
const menuOpen = Session.get('dropdownOpen') || false; const menuOpen = Session.get('dropdownOpen') || false;
if (menuOpen) { if (menuOpen) {
const menuChildren = document.activeElement.getElementsByTagName('li'); const menuChildren = document.activeElement.getElementsByTagName('li');
if ([KEY_CODES.ESCAPE, KEY_CODES.ARROW_LEFT].includes(event.keyCode)) { if ([KEY_CODES.ESCAPE, KEY_CODES.ARROW_LEFT].includes(event.keyCode)) {
Session.set('dropdownOpen', false);
document.activeElement.click(); document.activeElement.click();
} }
@ -445,13 +461,15 @@ const roving = (event, changeState, elementsList, element) => {
} }
if ([KEY_CODES.ESCAPE, KEY_CODES.TAB].includes(event.keyCode)) { if ([KEY_CODES.ESCAPE, KEY_CODES.TAB].includes(event.keyCode)) {
Session.set('dropdownOpen', false);
document.activeElement.blur(); document.activeElement.blur();
changeState(null); changeState(null);
} }
if (event.keyCode === KEY_CODES.ARROW_DOWN) { if (event.keyCode === KEY_CODES.ARROW_DOWN) {
const firstElement = elementsList.firstChild; const firstElement = elementsList.firstChild;
let elRef = element ? element.nextSibling : firstElement; let elRef = element && numberOfChilds > 1 ? element.nextSibling : firstElement;
elRef = elRef || firstElement; elRef = elRef || firstElement;
changeState(elRef); changeState(elRef);
} }
@ -464,7 +482,10 @@ const roving = (event, changeState, elementsList, element) => {
} }
if ([KEY_CODES.ARROW_RIGHT, KEY_CODES.SPACE, KEY_CODES.ENTER].includes(event.keyCode)) { 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();
} }
}; };
@ -526,4 +547,5 @@ export default {
hasPrivateChatBetweenUsers, hasPrivateChatBetweenUsers,
toggleUserLock, toggleUserLock,
requestUserInformation, requestUserInformation,
focusFirstDropDownItem,
}; };

View File

@ -70,16 +70,6 @@ class UserParticipants extends Component {
this.rowRenderer = this.rowRenderer.bind(this); this.rowRenderer = this.rowRenderer.bind(this);
} }
componentDidMount() {
const { compact } = this.props;
if (!compact) {
this.refScrollContainer.addEventListener(
'keydown',
this.rove,
);
}
}
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const isPropsEqual = _.isEqual(this.props, nextProps); const isPropsEqual = _.isEqual(this.props, nextProps);
const isStateEqual = _.isEqual(this.state, nextState); const isStateEqual = _.isEqual(this.state, nextState);
@ -87,12 +77,20 @@ class UserParticipants extends Component {
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
const { selectedUser } = this.state; const { compact } = this.props;
if (selectedUser === prevState.selectedUser) return; const { selectedUser, scrollArea } = this.state;
if (!compact && (!prevState.scrollArea && scrollArea)) {
scrollArea.addEventListener(
'keydown',
this.rove,
);
}
if (selectedUser) { if (selectedUser) {
const { firstChild } = selectedUser; const { firstChild } = selectedUser;
if (firstChild) firstChild.focus(); if (!firstChild.isEqualNode(document.activeElement)) {
firstChild.focus();
}
} }
} }
@ -153,8 +151,9 @@ class UserParticipants extends Component {
rove(event) { rove(event) {
const { roving } = this.props; const { roving } = this.props;
const { selectedUser } = this.state; const { selectedUser, scrollArea } = this.state;
const usersItemsRef = findDOMNode(this.refScrollItems); const usersItemsRef = findDOMNode(scrollArea.firstChild);
roving(event, this.changeState, usersItemsRef, selectedUser); roving(event, this.changeState, usersItemsRef, selectedUser);
} }
@ -220,8 +219,8 @@ class UserParticipants extends Component {
this.listRef = ref; this.listRef = ref;
} }
if (!scrollArea) { if (ref !== null && !scrollArea) {
this.setState({ scrollArea: findDOMNode(this.listRef) }); this.setState({ scrollArea: findDOMNode(ref) });
} }
}} }}
rowHeight={this.cache.rowHeight} rowHeight={this.cache.rowHeight}

View File

@ -4,12 +4,12 @@ import PropTypes from 'prop-types';
import { findDOMNode } from 'react-dom'; import { findDOMNode } from 'react-dom';
import UserAvatar from '/imports/ui/components/user-avatar/component'; import UserAvatar from '/imports/ui/components/user-avatar/component';
import Icon from '/imports/ui/components/icon/component'; import Icon from '/imports/ui/components/icon/component';
import Dropdown from './theteredDropdown/component';
import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component'; import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component';
import DropdownContent from '/imports/ui/components/dropdown/content/component'; import DropdownContent from '/imports/ui/components/dropdown/content/component';
import DropdownList from '/imports/ui/components/dropdown/list/component'; import DropdownList from '/imports/ui/components/dropdown/list/component';
import DropdownListItem from '/imports/ui/components/dropdown/list/item/component'; import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/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 lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
import _ from 'lodash'; import _ from 'lodash';
@ -17,6 +17,7 @@ import { Session } from 'meteor/session';
import { styles } from './styles'; import { styles } from './styles';
import UserName from '../user-name/component'; import UserName from '../user-name/component';
import UserIcons from '../user-icons/component'; import UserIcons from '../user-icons/component';
import Service from '../../../../service';
const messages = defineMessages({ const messages = defineMessages({
presenter: { presenter: {
@ -287,7 +288,10 @@ class UserDropdown extends PureComponent {
{ {
showNestedOptions: true, showNestedOptions: true,
isActionsOpen: true, isActionsOpen: true,
}, Session.set('dropdownOpen', true), }, () => {
Session.set('dropdownOpen', true);
Service.focusFirstDropDownItem();
},
), ),
'user', 'user',
'right_arrow', 'right_arrow',
@ -561,7 +565,7 @@ class UserDropdown extends PureComponent {
<div <div
data-test={isMe(user.userId) ? 'userListItemCurrent' : 'userListItem'} data-test={isMe(user.userId) ? 'userListItemCurrent' : 'userListItem'}
className={!actions.length ? styles.userListItem : null} className={!actions.length ? styles.userListItem : null}
> >
<div className={styles.userItemContents}> <div className={styles.userItemContents}>
<div className={styles.userAvatar}> <div className={styles.userAvatar}>
{this.renderUserAvatar()} {this.renderUserAvatar()}
@ -601,7 +605,8 @@ class UserDropdown extends PureComponent {
aria-live="assertive" aria-live="assertive"
aria-relevant="additions" aria-relevant="additions"
placement={placement} placement={placement}
getContent={(dropdownContent) => this.dropdownContent = dropdownContent} getContent={dropdownContent => this.dropdownContent = dropdownContent}
tethered
> >
<DropdownTrigger> <DropdownTrigger>
{contents} {contents}

View File

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

View File

@ -101,7 +101,7 @@ class Dropdown extends Component {
if (!isOpen && prevState.isOpen) { onHide(); } if (!isOpen && prevState.isOpen) { onHide(); }
if (prevProps.keepOpen && !keepOpen) onHide(); if (prevProps.keepOpen && !keepOpen) { onHide(); }
} }
handleShow() { handleShow() {
@ -196,6 +196,7 @@ class Dropdown extends Component {
getContent(ref); getContent(ref);
this.content = ref; this.content = ref;
}, },
keepOpen,
'aria-expanded': isOpen, 'aria-expanded': isOpen,
dropdownIsOpen: isOpen, dropdownIsOpen: isOpen,
dropdownToggle: this.handleToggle, dropdownToggle: this.handleToggle,
@ -236,7 +237,7 @@ class Dropdown extends Component {
} }
targetAttachment={ targetAttachment={
isMobile ? '' isMobile ? ''
: targetAttachments[placements] : targetAttachments[placements]
} }
constraints={[ constraints={[
@ -245,10 +246,10 @@ class Dropdown extends Component {
}, },
]} ]}
renderTarget={ref => ( renderTarget={ref => (
<span ref={ref}> <span ref={ref}>
{trigger} {trigger}
</span>)} </span>)}
renderElement={ref => ( renderElement={ref => (
<div <div
ref={ref} ref={ref}