Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Add a more accessible dropdown menu #12745

Merged
merged 5 commits into from
Jul 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
151 changes: 151 additions & 0 deletions bigbluebutton-html5/imports/ui/components/menu/component.jsx
@@ -0,0 +1,151 @@
import React from "react";
import PropTypes from "prop-types";
import { defineMessages, injectIntl } from "react-intl";

import Menu from "@material-ui/core/Menu";
import MenuItem from "@material-ui/core/MenuItem";
import { Divider } from "@material-ui/core";

import Icon from "/imports/ui/components/icon/component";
import Button from "/imports/ui/components/button/component";

import { styles } from "./styles";

const intlMessages = defineMessages({
close: {
id: 'app.dropdown.close',
description: 'Close button label',
},
});

//Used to switch to mobile view
const MAX_WIDTH = 640;
Comment on lines +21 to +22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a helper that could be used here to check for mobile devices instead of checking the window size.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I saw the utility to check for mobile, the problem with using that helper is that it excludes regular browsers that are resized to mobile sizes.

image


class BBBMenu extends React.Component {
constructor(props) {
super(props);
this.state = {
anchorEl: null,
};

this.setAnchorEl = this.setAnchorEl.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleClose = this.handleClose.bind(this);
}

handleClick(event) {
this.setState({ anchorEl: event.currentTarget})
};

handleClose() {
const { onCloseCallback } = this.props;
this.setState({ anchorEl: null}, onCloseCallback());
};

setAnchorEl(el) {
this.setState({ anchorEl: el });
};

makeMenuItems() {
const { actions, selectedEmoji } = this.props;

return actions?.map(a => {
const { label, onClick, key } = a;
const itemClasses = [styles.menuitem];

if (key?.toLowerCase()?.includes(selectedEmoji?.toLowerCase())) itemClasses.push(styles.emojiSelected);

return [<MenuItem
key={label}
className={itemClasses.join(' ')}
disableRipple={true}
disableGutters={true}
style={{ paddingLeft: '4px',paddingRight: '4px',paddingTop: '8px', paddingBottom: '8px', marginLeft: '4px', marginRight: '4px' }}
onClick={() => {
onClick();
const close = !label.includes('Set status') && !label.includes('Back');
// prevent menu close for sub menu actions
if (close) this.handleClose();
}}>
<div style={{ display: 'flex', flexFlow: 'row', width: '100%'}}>
{a.icon ? <Icon iconName={a.icon} key="icon" /> : null}
<div className={styles.option}>{label}</div>
{a.iconRight ? <Icon iconName={a.iconRight} key="iconRight" className={styles.iRight} /> : null}
</div>
</MenuItem>,
a.divider && <Divider />
];
});
}

render() {
const { anchorEl } = this.state;
const { trigger, intl, opts, wide } = this.props;
const actionsItems = this.makeMenuItems();

const menuClasses = [styles.menu];
if (wide) menuClasses.push(styles.wide)

return (
<div>
<div onClick={this.handleClick}>{trigger}</div>
<Menu
{...opts}
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={this.handleClose}
className={menuClasses.join(' ')}
>
{actionsItems}
{anchorEl && window.innerWidth < MAX_WIDTH &&
<Button
className={styles.closeBtn}
label={intl.formatMessage(intlMessages.close)}
size="lg"
color="default"
onClick={this.handleClose}
/>
}
</Menu>
</div>
);
}
}

export default injectIntl(BBBMenu);

BBBMenu.defaultProps = {
opts: {
id: "default-dropdown-menu",
keepMounted: true,
transitionDuration: 0,
elevation: 3,
getContentAnchorEl: null,
fullwidth: "true",
anchorOrigin: { vertical: 'top', horizontal: 'right' },
transformorigin: { vertical: 'top', horizontal: 'top' },
},
onCloseCallback: () => {},
wide: false,
};

BBBMenu.propTypes = {
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,

trigger: PropTypes.element.isRequired,

actions: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
icon: PropTypes.string,
iconRight: PropTypes.string,
divider: PropTypes.bool,
})).isRequired,

onCloseCallback: PropTypes.func,

wide: PropTypes.bool,
};
85 changes: 85 additions & 0 deletions bigbluebutton-html5/imports/ui/components/menu/styles.scss
@@ -0,0 +1,85 @@

@import "/imports/ui/stylesheets/variables/breakpoints";
@import "/imports/ui/stylesheets/variables/placeholders";
@import '/imports/ui/stylesheets/mixins/_indicators';

.option {
line-height: 1;
margin-right: 1.65rem;
margin-left: .5rem;
}

.closeBtn {
position: fixed;
bottom: 0;
display: flex;
justify-content: center;
width: 100%;
height: 5rem;
background-color: var(--color-white);
padding: 1rem;

border-radius: 0;
z-index: 1011;
font-size: calc(var(--font-size-large) * 1.1);
box-shadow: 0 0 0 2rem var(--color-white) !important;
border: var(--color-white) !important;
cursor: pointer !important;
}

.iRight {
display: flex;
justify-content: flex-end;
flex: 1;
}

.menu {
@include mq($small-only) {
:global(.MuiPaper-root.MuiMenu-paper.MuiPopover-paper) {
top: 0 !important;
left: 0 !important;
bottom: 0 !important;
right: 0 !important;
max-width: none;
}
}

:global(.MuiPaper-root) {
background-color: var(--dropdown-bg);
border-radius: var(--border-radius);
border: 0;
z-index: 9999;
}
}

.wide {
:global(.MuiPaper-root) {
width: 100%;
}
}

.menuitem {
transition: none !important;
font-size: 90% !important;

&:focus,
&:hover {
color: #FFF;
background-color: var(--color-primary) !important;
}
}

.emojiSelected {
div,
i {
color: var(--color-primary);
}

&:focus,
&:hover {
div,
i {
color: #FFF ;
}
}
}
Expand Up @@ -538,7 +538,7 @@ const roving = (...args) => {
if ([KEY_CODES.ARROW_RIGHT, KEY_CODES.SPACE, KEY_CODES.ENTER].includes(event.keyCode)) {
const tether = document.activeElement.firstChild;
const dropdownTrigger = tether.firstChild;
dropdownTrigger.click();
dropdownTrigger?.click();
focusFirstDropDownItem();
}
};
Expand Down
Expand Up @@ -40,21 +40,19 @@
@include elementFocus(var(--list-item-bg-hover));
@include scrollbox-vertical(var(--user-list-bg));


> div {
outline: none;
}


&:hover {
@extend %highContrastOutline;
}

&:focus,
&:active {
box-shadow: none;
box-shadow: inset 1px 0 3px var(--color-primary);
border-radius: none;
outline-style: none;
outline-style: transparent;
}

flex-grow: 1;
Expand Down
Expand Up @@ -62,6 +62,7 @@ class UserParticipants extends Component {
this.changeState = this.changeState.bind(this);
this.rowRenderer = this.rowRenderer.bind(this);
this.handleClickSelectedUser = this.handleClickSelectedUser.bind(this);
this.selectEl = this.selectEl.bind(this);
}

componentDidMount() {
Expand All @@ -85,13 +86,19 @@ class UserParticipants extends Component {
return !isPropsEqual || !isStateEqual;
}

selectEl(el) {
if (!el) return null;
if (el.getAttribute('tabindex')) return el?.focus();
this.selectEl(el?.firstChild);
}

componentDidUpdate(prevProps, prevState) {
const { selectedUser } = this.state;

if (selectedUser) {
const { firstChild } = selectedUser;
if (!firstChild.isEqualNode(document.activeElement)) {
firstChild.focus();
this.selectEl(selectedUser);
}
}
}
Expand Down Expand Up @@ -166,7 +173,6 @@ class UserParticipants extends Component {
const { roving } = this.props;
const { selectedUser, scrollArea } = this.state;
const usersItemsRef = findDOMNode(scrollArea.firstChild);

roving(event, this.changeState, usersItemsRef, selectedUser);
}

Expand Down Expand Up @@ -213,6 +219,7 @@ class UserParticipants extends Component {
: <hr className={styles.separator} />
}
<div
id={'user-list-virtualized-scroll'}
className={styles.virtulizedScrollableList}
tabIndex={0}
ref={(ref) => {
Expand Down Expand Up @@ -244,6 +251,7 @@ class UserParticipants extends Component {
className={styles.scrollStyle}
overscanRowCount={30}
deferredMeasurementCache={this.cache}
tabIndex={-1}
/>
)}
</AutoSizer>
Expand Down