Merge pull request #3269 from JaeeunCho/dropdown

HTML5 dropdown for settings menu
This commit is contained in:
Anton Georgiev 2016-08-25 13:27:18 -04:00 committed by GitHub
commit 4811b521a6
13 changed files with 487 additions and 46 deletions

View File

@ -57,7 +57,6 @@ modules-runtime@0.7.6
mongo@1.1.11
mongo-id@1.0.5
mrt:external-file-loader@0.1.4
mrt:redis@0.1.3
nathantreid:css-modules@2.2.2
nathantreid:css-modules-import-path-helpers@0.1.4
npm-mongo@1.5.46

View File

@ -25,5 +25,12 @@
"app.whiteboard.slideControls.zoomDescrip": "Change the zoom level of the presentation",
"app.failedMessage": "Apologies, trouble connecting to the server.",
"app.connectingMessage": "Connecting...",
"app.waitingMessage": "Disconnected. Trying to reconnect in {seconds} seconds..."
"app.waitingMessage": "Disconnected. Trying to reconnect in {seconds} seconds...",
"app.dropdown.options": "Options",
"app.dropdown.fullscreenLabel": "Make fullscreen",
"app.dropdown.settingsLabel": "Open settings",
"app.dropdown.leaveSessionLabel": "Logout",
"app.dropdown.fullscreenDesc": "Make the settings menu fullscreen",
"app.dropdown.settingsDesc": "Change the general settings",
"app.dropdown.leaveSessionDesc": "Leave the meeting"
}

View File

@ -5,14 +5,12 @@ import { subscribeForData, wasUserKicked, redirectToLogoutUrl } from './service'
import NavBarContainer from '../nav-bar/container';
import ActionsBarContainer from '../actions-bar/container';
import MediaContainer from '../media/container';
import SettingsModal from '../modals/settings/SettingsModal';
import ClosedCaptionsContainer from '../closed-captions/container';
const defaultProps = {
navbar: <NavBarContainer />,
actionsbar: <ActionsBarContainer />,
media: <MediaContainer />,
settings: <SettingsModal />,
//CCs UI is commented till the next pull request
//captions: <ClosedCaptionsContainer />,

View File

@ -55,7 +55,7 @@ export default class ButtonBase extends Component {
this.internalKeyUpHandler = this.internalKeyUpHandler.bind(this);
}
validateDisabled(eventHandler, args) {
validateDisabled(eventHandler, ...args) {
if (!this.props.disabled && typeof eventHandler === 'function') {
return eventHandler(...args);
}

View File

@ -0,0 +1,264 @@
import React, { Component, PropTyes } from 'react';
import ReactDOM from 'react-dom';
import Icon from '/imports/ui/components/icon/component';
import Button from '/imports/ui/components/button/component';
import classNames from 'classnames';
import styles from './styles';
import { FormattedMessage } from 'react-intl';
import SettingsModal from '../modals/settings/SettingsModal';
import SessionMenu from '../modals/settings/submenus/SessionMenu';
import Dropdown from './dropdown-menu/component';
import DropdownTrigger from './dropdown-trigger/component';
import DropdownContent from './dropdown-content/component';
export default class SettingsDropdown extends Component {
constructor(props) {
super(props);
this.menus = [];
this.openWithKey = this.openWithKey.bind(this);
}
componentWillMount() {
this.setState({ activeMenu: -1, focusedMenu: 0, });
this.menus.push({ className: '',
props: { title: 'Fullscreen', prependIconName: 'icon-', icon: 'bbb-full-screen',
ariaLabelleby: 'fullscreenLabel', ariaDescribedby:'fullscreenDesc', },
tabIndex: 1, });
this.menus.push({ className: SettingsModal,
props: { title: 'Settings', prependIconName: 'icon-', icon: 'bbb-more',
ariaLabelleby: 'settingsLabel', ariaDescribedby:'settingsDesc', },
tabIndex: 2, });
this.menus.push({ className: SessionMenu,
props: { title: 'Leave Session', prependIconName: 'icon-', icon: 'bbb-logout',
ariaLabelleby: 'leaveSessionLabel', ariaDescribedby:'leaveSessionDesc', },
tabIndex: 3, });
}
componentWillUpdate() {
const DROPDOWN = this.refs.dropdown;
if (DROPDOWN.state.isMenuOpen && this.state.activeMenu >= 0) {
this.setState({ activeMenu: -1, focusedMenu: 0, });
}
}
setFocus() {
ReactDOM.findDOMNode(this.refs[`menu${this.state.focusedMenu}`]).focus();
}
handleListKeyDown(event) {
const pressedKey = event.keyCode;
let numOfMenus = this.menus.length - 1;
// User pressed tab
if (pressedKey === 9) {
let newIndex = 0;
if (this.state.focusedMenu >= numOfMenus) { // Checks if at end of menu
newIndex = 0;
if (!event.shiftKey) {
this.refs.dropdown.hideMenu();
}
} else {
newIndex = this.state.focusedMenu;
}
this.setState({ focusedMenu: newIndex, });
return;
}
// User pressed shift + tab
if (event.shiftKey && pressedKey === 9) {
let newIndex = 0;
if (this.state.focusedMenu <= 0) { // Checks if at beginning of menu
newIndex = numOfMenus;
} else {
newIndex = this.state.focusedMenu - 1;
}
this.setState({ focusedMenu: newIndex, });
return;
}
// User pressed up key
if (pressedKey === 38) {
if (this.state.focusedMenu <= 0) { // Checks if at beginning of menu
this.setState({ focusedMenu: numOfMenus, },
() => { this.setFocus(); });
} else {
this.setState({ focusedMenu: this.state.focusedMenu - 1, },
() => { this.setFocus(); });
}
return;
}
// User pressed down key
if (pressedKey === 40) {
if (this.state.focusedMenu >= numOfMenus) { // Checks if at end of menu
this.setState({ focusedMenu: 0, },
() => { this.setFocus(); });
} else {
this.setState({ focusedMenu: this.state.focusedMenu + 1, },
() => { this.setFocus(); });
}
return;
}
// User pressed enter and spaceBar
if (pressedKey === 13 || pressedKey === 32) {
this.clickMenu(this.state.focusedMenu);
return;
}
//User pressed ESC
if (pressedKey == 27) {
this.setState({ activeMenu: -1, focusedMenu: 0, });
this.refs.dropdown.hideMenu();
}
return;
}
handleFocus(index) {
this.setState({ focusedMenu: index, },
() => { this.setFocus(); });
}
clickMenu(i) {
this.setState({ activeMenu: i, });
this.refs.dropdown.hideMenu();
}
createMenu() {
const curr = this.state.activeMenu;
switch (curr) {
case 0:
console.log(this.menus[curr].props.title);
break;
case 1:
return <SettingsModal />;
break;
case 2:
return <SessionMenu />;
break;
default:
return;
}
}
openWithKey(event) {
// Focus first menu option
if (event.keyCode === 9) {
event.preventDefault();
event.stopPropagation();
}
this.setState({ focusedMenu: 0 }, () => { this.setFocus(); });
}
renderAriaLabelsDescs() {
return (
<div>
{/* aria-labelledby */}
<p id="fullscreenLabel" hidden>
<FormattedMessage
id="app.dropdown.fullscreenLabel"
description="Aria label for fullscreen"
defaultMessage="Make fullscreen"
/>
</p>
<p id="settingsLabel" hidden>
<FormattedMessage
id="app.dropdown.settingsLabel"
description="Aria label for settings"
defaultMessage="Open Settings"
/>
</p>
<p id="leaveSessionLabel" hidden>
<FormattedMessage
id="app.dropdown.leaveSessionLabel"
description="Aria label for logout"
defaultMessage="Logout"
/>
</p>
{/* aria-describedby */}
<p id="fullscreenDesc" hidden>
<FormattedMessage
id="app.dropdown.fullscreenDesc"
description="Aria label for fullscreen"
defaultMessage="Make the settings menu fullscreen"
/>
</p>
<p id="settingsDesc" hidden>
<FormattedMessage
id="app.dropdown.settingsDesc"
description="Aria label for settings"
defaultMessage="Change the general settings"
/>
</p>
<p id="leaveSessionDesc" hidden>
<FormattedMessage
id="app.dropdown.leaveSessionDesc"
description="Aria label for logout"
defaultMessage="Leave the meeting"
/>
</p>
</div>
);
}
render() {
return (
<div>
<Dropdown ref='dropdown' focusMenu={this.openWithKey}>
<DropdownTrigger labelBtn='setting' iconBtn='more' />
<DropdownContent>
<div className={styles.triangleOnDropdown}></div>
<div className={styles.dropdownActiveContent}>
<ul className={styles.menuList} role="menu">
{this.menus.map((value, index) => (
<li
key={index}
role='menuitem'
tabIndex={value.tabIndex}
onClick={this.clickMenu.bind(this, index)}
onKeyDown={this.handleListKeyDown.bind(this)}
onFocus={this.handleFocus.bind(this, index)}
ref={'menu' + index}
className={styles.settingsMenuItem}
aria-labelledby={value.props.ariaLabelleby}
aria-describedby={value.props.ariaDescribedby}>
<Icon
key={index}
prependIconName={value.props.prependIconName}
iconName={value.props.icon}
title={value.props.title}
className={styles.iconColor}/>
<span className={styles.settingsMenuItemText}>{value.props.title}</span>
{index == '0' ? <hr className={styles.hrDropdown}/> : null}
</li>
))}
</ul>
{this.renderAriaLabelsDescs()}
</div>
</DropdownContent>
</Dropdown>
<div role='presentation'>{this.createMenu()}</div>
<p id="settingsDropdown" hidden>
<FormattedMessage
id="app.dropdown.options"
description="Aria label for Options"
defaultMessage="Options"
/>
</p>
</div>
);
}
}

View File

@ -0,0 +1,12 @@
import React, { Component, PropTypes } from 'react';
import styles from '../styles';
export default class DropdownContent extends Component {
constructor(props) {
super(props);
}
render() {
return <div>{this.props.children}</div>;
}
}

View File

@ -0,0 +1,89 @@
import React, { Component, PropTypes } from 'react';
import { findDOMNode } from 'react-dom';
import styles from '../styles';
import DropdownTrigger from '../dropdown-trigger/component';
export default class Dropdown extends Component {
constructor(props) {
super(props);
this.state = { isMenuOpen: false, };
this.showMenu = this.showMenu.bind(this);
this.hideMenu = this.hideMenu.bind(this);
this.toggle = this.toggle.bind(this);
this.onWindowClick = this.onWindowClick.bind(this);
}
showMenu(event) {
let pressedKey = event.keyCode;
this.setState({ isMenuOpen: true, });
// User pressed tab
if (pressedKey === 9) {
this.hideMenu();
}
// User pressed down arrow or enter or space
if (pressedKey === 40 || pressedKey === 13 || pressedKey === 32) {
this.props.focusMenu(event);
}
}
hideMenu() {
this.setState({ isMenuOpen: false, });
}
componentDidMount () {
const { addEventListener } = window;
addEventListener('click', this.onWindowClick, false);
}
componentWillUnmount () {
const { removeEventListener } = window;
removeEventListener('click', this.onWindowClick, false);
}
onWindowClick(event) {
const dropdownElement = findDOMNode(this);
const shouldUpdateState = event.target !== dropdownElement &&
!dropdownElement.contains(event.target) &&
this.state.isMenuOpen;
if (shouldUpdateState) {
this.hideMenu();
}
}
toggle(event) {
if (this.state.isMenuOpen) {
this.hideMenu();
} else {
this.showMenu(event);
}
}
render() {
const toggle = this.toggle;
// stick callback on trigger element
const boundChildren = React.Children.map(this.props.children, (child) => {
if (child.type === DropdownTrigger) {
child = React.cloneElement(child, {
toggle: toggle,
});
}
return child;
});
let trigger = boundChildren[0];
let content = boundChildren[1];
return (
<div className={styles.dropdown}>
{trigger}
{this.state.isMenuOpen ?
content : null}
</div>
);
}
}

View File

@ -0,0 +1,28 @@
import React, { Component, PropTypes } from 'react';
import Button from '/imports/ui/components/button/component';
import styles from '../styles';
export default class DropdownTrigger extends Component {
constructor(props) {
super(props);
this.toggle = this.props.toggle.bind(this);
}
render() {
const { labelBtn, iconBtn } = this.props;
return (
<Button
className={styles.settingBtn}
role='button'
label={labelBtn}
icon={iconBtn}
ghost={true}
circle={true}
hideLabel={true}
onClick={this.toggle}
onKeyDown={this.toggle}
aria-haspopup={'true'}/>
);
}
}

View File

@ -0,0 +1,67 @@
@import "../../stylesheets/variables/_all";
.settingsMenuItem {
padding-top:20px;
}
.settingsMenuItemText {
margin-left: 10px;
}
.menuList {
list-style-type: none;
padding-left: 0px;
}
.settingBtn {
-ms-transform: rotate( 90deg ); /* IE 9 */
-webkit-transform: rotate( 90deg ); /* Safari */
transform: rotate( 90deg );
color: #ffffff;
span {
border: 0px solid;
box-shadow: none;
}
}
.hrDropdown {
border: 0.3px solid $color-gray-light;
width: 165px;
text-align: center;
}
.dropdown {
position: relative;
}
.dropdownActiveContent {
margin-top: 14px;
margin-right: -3px;
padding-left: 15px;
height: auto;
width: 190px;
display: block;
background-color: #ffffff;
position: absolute;
right: 0;
text-align: left;
font-size: $font-size-base;
border-radius: 3%;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
}
.triangleOnDropdown {
position: absolute;
margin-top: 5px;
margin-left: -2px;
width: 0;
height: 0;
border-left: 18px solid transparent;
border-right: 18px solid transparent;
border-bottom: 18px solid #ffffff;
}
.iconColor {
font-size: $font-size-base;
color: $color-gray-light;
}

View File

@ -23,15 +23,13 @@ export default class BaseModal extends React.Component {
constructor(props) {
super(props);
this.state = {
modalIsOpen: false,
// You need to set isOpen={true} on the modal component for it to render its children.
modalIsOpen: true,
title: props.title || 'title',
content: <div>hello</div>,
};
this.openModal = this.openModal.bind(this);
this.closeModal = this.closeModal.bind(this);
this.handleModalCloseRequest = this.handleModalCloseRequest.bind(this);
this.handleSaveClicked = this.handleSaveClicked.bind(this);
this.afterOpenModal = this.afterOpenModal.bind(this);
this.setTitle = this.setTitle.bind(this);
}
@ -47,18 +45,6 @@ export default class BaseModal extends React.Component {
this.setState({ modalIsOpen: false });
}
afterOpenModal() {}
handleModalCloseRequest() {
// opportunity to validate something and keep the modal open even if it
// requested to be closed
this.setState({ modalIsOpen: false });
}
handleSaveClicked(e) {
alert('Save button was clicked');
}
getContent() {
return (<div>parent content</div>);
}
@ -68,11 +54,8 @@ export default class BaseModal extends React.Component {
<span>
<Modal
isOpen={this.state.modalIsOpen}
onAfterOpen={this.afterOpenModal}
onRequestClose={this.closeModal}
shouldCloseOnOverlayClick={false}
style={customStyles} >
<span className={classNames(styles.modalHeaderTitle, 'largeFont')}>
{this.state.title}
</span>

View File

@ -19,7 +19,6 @@ export default class SettingsModal extends BaseModal {
}
componentWillMount() {
this.setState({ activeSubmenu: 0 });
this.submenus.push({ className: AudioMenu,
props: { title: 'Audio', prependIconName: 'ion-', icon: 'ios-mic-outline', }, });
this.submenus.push({ className: VideoMenu,
@ -33,11 +32,7 @@ export default class SettingsModal extends BaseModal {
}
componentDidMount() {
ReactDOM.render(
<Button componentClass='button' style={{ width: '30px', height: '30px', float: 'right' }}
onClick={this.openModal} i_class='icon ion-gear-b' rel='tooltip' title='Settings'>
<Icon iconName='icon ion-gear-b' className='mediumFont icon ion-gear-b'/>
</Button>, document.getElementById('settingsButtonPlaceHolder'));
return (<div onClick={this.openModal}></div>);
}
createMenu() {
@ -54,17 +49,9 @@ export default class SettingsModal extends BaseModal {
}
clickSubmenu(i) {
if (this.submenus[i].className != SessionMenu) {
this.setState({ activeSubmenu: i });
};
this.setState({ activeSubmenu: i });
}
doubleClickSubmenu(i) {
if (this.submenus[i].className == SessionMenu) {
this.setState({ activeSubmenu: i });
}
}
getContent() {
return (
<div style={{ clear: 'both' }}>
@ -72,7 +59,6 @@ export default class SettingsModal extends BaseModal {
<ul style={{ listStyleType: 'none' }}>
{this.submenus.map((value, index) => (
<li key={index} onClick={this.clickSubmenu.bind(this, index)}
onDoubleClick={this.doubleClickSubmenu.bind(this, index)}
className={classNames(styles.settingsSubmenuItem,
index == this.state.activeSubmenu ? styles.settingsSubmenuItemActive : null)}>
<Icon key={index} prependIconName={value.props.prependIconName}

View File

@ -1,4 +1,5 @@
import React from 'react';
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import Modal from 'react-modal';
import Button from '/imports/ui/components/button/component';
import BaseMenu from './BaseMenu';
@ -40,18 +41,24 @@ export default class SessionMenu extends BaseMenu {
<span>
<Modal
isOpen={this.state.openConfirm}
style={customModal} >
style={customModal}
onRequestClose={this.closeLogout}
tabIndex="-1">
<span className={classNames(styles.modalHeaderTitle, 'largeFont')}> Leave Session</span>
<hr className={styles.modalHorizontalRule} />
<span>Do you want to leave this meeting?</span>
<br />
<button onClick={Auth.completeLogout}
className={classNames(styles.modalButton, styles.done)}
tabindex="0"
tabIndex='8'
aria-labelledby="logout_okay"
aria-describedby="logout_okay"
role="button">Yes</button>
<button onClick={this.closeLogout}
className={classNames(styles.modalButton, styles.close)}
tabindex="1"
tabIndex='9'
aria-labelledby="logout_cancel"
aria-describedby="logout_cancel"
role="button">No</button>
</Modal>
</span>

View File

@ -2,6 +2,7 @@ import React, { Component, PropTypes } from 'react';
import styles from './styles.scss';
import Button from '../button/component';
import RecordButton from './recordbutton/component';
import SettingsDropdown from '../dropdown/component';
const propTypes = {
presentationTitle: PropTypes.string.isRequired,
@ -51,7 +52,7 @@ class NavBar extends Component {
</div>
</div>
<div className={styles.right}>
<span id="settingsButtonPlaceHolder"></span>
<SettingsDropdown />
</div>
</div>
);