diff --git a/bigbluebutton-html5/imports/ui/components/button/styles.scss b/bigbluebutton-html5/imports/ui/components/button/styles.scss index edb66ce87a2814fe5fc7a7836aede6d0f0953f7f..6c224d37af860404041df92ccc730d3ea6978c07 100755 --- a/bigbluebutton-html5/imports/ui/components/button/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/button/styles.scss @@ -85,6 +85,14 @@ $btn-lg-padding: $lg-padding-y $lg-padding-x; background: none; border: 0; padding: 0 !important; + + &:active { + &:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; + } + } } .label { @@ -149,13 +157,16 @@ $btn-lg-padding: $lg-padding-y $lg-padding-x; border-color: $border; &:hover, - &:focus { + &:focus, + .buttonWrapper:hover &, + .buttonWrapper:focus & { color: $color; background-color: $active-background; border-color: $active-border; } - &:active { + &:active, + .buttonWrapper:active & { color: $color; background-color: $active-background; border-color: $active-border; @@ -177,13 +188,16 @@ $btn-lg-padding: $lg-padding-y $lg-padding-x; border-color: $color; &:focus, - &:hover { + &:hover, + .buttonWrapper:hover &, + .buttonWrapper:focus & { color: $variant; background-color: $color; border-color: $color; } - &:active { + &:active, + .buttonWrapper:active & { color: $variant; background-color: $color; border-color: $color; diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx index 6eed15995cb114bf1f6962b2fce014a5a57ec7c4..c4d22bb45233486575eddc63d312c5f4598312e8 100755 --- a/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx @@ -4,7 +4,12 @@ import styles from './styles'; import DropdownTrigger from './trigger/component'; import DropdownContent from './content/component'; +const FOCUSABLE_CHILDREN = `[tabindex]:not([tabindex="-1"]), a, input, button`; + const propTypes = { + /** + * The dropdown needs a trigger and a content component as childrens + */ children: (props, propName, componentName) => { const children = props[propName]; @@ -34,6 +39,9 @@ const propTypes = { }, }; +const defaultProps = { +}; + export default class Dropdown extends Component { constructor(props) { super(props); @@ -46,10 +54,15 @@ export default class Dropdown extends Component { handleShow() { this.setState({ isOpen: true }); + + const contentElement = findDOMNode(this.refs.content); + contentElement.querySelector(FOCUSABLE_CHILDREN).focus(); } handleHide() { this.setState({ isOpen: false }); + const triggerElement = findDOMNode(this.refs.trigger); + triggerElement.focus(); } componentDidMount () { @@ -83,19 +96,31 @@ export default class Dropdown extends Component { const { children } = this.props; let trigger = children.find(x => x.type === DropdownTrigger); - const content = children.find(x => x.type === DropdownContent); + let content = children.find(x => x.type === DropdownContent); trigger = React.cloneElement(trigger, { - handleToggle: this.handleToggle, + ref: 'trigger', + dropdownToggle: this.handleToggle, + dropdownShow: this.handleShow, + dropdownHide: this.handleHide, + }); + + content = React.cloneElement(content, { + ref: 'content', + 'aria-expanded': this.state.isOpen, + dropdownToggle: this.handleToggle, + dropdownShow: this.handleShow, + dropdownHide: this.handleHide, }); return ( <div className={styles.dropdown}> {trigger} - {this.state.isOpen ? content : null} + {content} </div> ); } } Dropdown.propTypes = propTypes; +Dropdown.defaultProps = defaultProps; diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/content/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/content/component.jsx index af69df9ad219ddb32f823f22418a327835484d9a..3170dd6a27ebc56cf2d47c00c8b20b7b24860a79 100755 --- a/bigbluebutton-html5/imports/ui/components/dropdown/content/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/dropdown/content/component.jsx @@ -1,12 +1,53 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component, PropTypes, Children, cloneElement } from 'react'; +import cx from 'classnames'; import styles from '../styles'; +const PLACEMENTS = [ + 'top left', 'top', 'top right', + 'right top', 'right', 'right bottom', + 'bottom right', 'bottom', 'bottom left', + 'left bottom', 'left', 'left top', +]; + +const propTypes = { + /** + * Placements of the dropdown and its caret + * @defaultValue 'top' + */ + placement: PropTypes.oneOf(PLACEMENTS), +}; + +const defaultProps = { + placement: 'top', + 'aria-expanded': false, +}; + export default class DropdownContent extends Component { constructor(props) { super(props); } render() { - return <div className={styles.content}>{this.props.children}</div>; + const { placement, className, children } = this.props; + const { dropdownToggle, dropdownShow, dropdownHide } = this.props; + + let placementName = placement.split(' ').join('-'); + + const boundChildren = Children.map(children, child => cloneElement(child, { + dropdownToggle: dropdownToggle, + dropdownShow: dropdownShow, + dropdownHide: dropdownHide, + })); + + return ( + <div + aria-expanded={this.props['aria-expanded']} + className={cx(styles.content, styles[placementName], className)}> + {boundChildren} + </div> + ); } } + +DropdownContent.propTypes = propTypes; +DropdownContent.defaultProps = defaultProps; diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/list/component.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b0b4afde2cc5a997b3b7a307e6c0faeeabe68167 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/component.jsx @@ -0,0 +1,132 @@ +import React, { Component, PropTypes, Children, cloneElement } from 'react'; +import styles from './styles'; + +import KEY_CODES from '/imports/utils/keyCodes'; + +import ListItem from './item/component'; +import ListSeparator from './separator/component'; + +const propTypes = { + children: PropTypes.arrayOf((propValue, key, componentName, location, propFullName) => { + if (propValue[key].type !== ListItem && propValue[key].type !== ListSeparator) { + return new Error( + 'Invalid prop `' + propFullName + '` supplied to' + + ' `' + componentName + '`. Validation failed.' + ); + } + }), +}; + +export default class DropdownList extends Component { + constructor(props) { + super(props); + this.childrenRefs = []; + this.handleItemKeyDown = this.handleItemKeyDown.bind(this); + this.handleItemClick = this.handleItemClick.bind(this); + } + + componentWillMount() { + this.setState({ + activeItemIndex: 0, + }); + } + + componentDidUpdate(prevProps, prevState) { + const { activeItemIndex } = this.state; + const activeRef = this.childrenRefs[activeItemIndex]; + + if (activeRef) { + activeRef.focus(); + } + } + + handleItemKeyDown(event, callback) { + const { dropdownHide } = this.props; + const { activeItemIndex } = this.state; + + event.preventDefault(); + event.stopPropagation(); + + if ([KEY_CODES.SPACE, KEY_CODES.ENTER].includes(event.which)) { + return event.currentTarget.click(); + } + + let nextActiveItemIndex = null; + + if (KEY_CODES.ARROW_UP === event.which) { + nextActiveItemIndex = activeItemIndex - 1; + } + + if (KEY_CODES.ARROW_DOWN === event.which) { + nextActiveItemIndex = activeItemIndex + 1; + } + + if (KEY_CODES.ESCAPE === event.which) { + dropdownHide(); + } + + if (nextActiveItemIndex > (this.childrenRefs.length - 1)) { + nextActiveItemIndex = 0; + } + + if (nextActiveItemIndex < 0) { + nextActiveItemIndex = this.childrenRefs.length - 1; + } + + this.setState({ activeItemIndex: nextActiveItemIndex }); + + if (typeof callback === 'function') { + callback(event); + } + } + + handleItemClick(event, callback) { + const { dropdownHide } = this.props; + + this.setState({ activeItemIndex: null }); + dropdownHide(); + + if (typeof callback === 'function') { + callback(event); + } + } + + render() { + const boundChildren = Children.map(this.props.children, + (item, i) => { + if (item.type === ListSeparator) { + return item; + } + + return cloneElement(item, { + tabIndex: 0, + injectRef: ref => { + if (ref && !this.childrenRefs.includes(ref)) + this.childrenRefs.push(ref); + }, + + onClick: event => { + let { onClick } = item.props; + onClick = onClick ? onClick.bind(item) : null; + + this.handleItemClick(event, onClick); + }, + + onKeyDown: event => { + let { onKeyDown } = item.props; + onKeyDown = onKeyDown ? onKeyDown.bind(item) : null; + + this.handleItemKeyDown(event, onKeyDown); + }, + }); + }); + + return ( + <ul className={styles.list} role="menu"> + {boundChildren} + </ul> + ); + } +} + +DropdownList.propTypes = propTypes; diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9c3462c5585bcfd0bef7813fd8be4675e5030e1b --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx @@ -0,0 +1,63 @@ +import React, { Component, PropTypes } from 'react'; +import styles from '../styles'; +import _ from 'underscore'; + +import Icon from '/imports/ui/components/icon/component'; + +const propTypes = { + icon: PropTypes.string, + label: PropTypes.string, + description: PropTypes.string, +}; + +export default class DropdownListItem extends Component { + constructor(props) { + super(props); + this.labelID = _.uniqueId('dropdown-item-label-'); + this.descID = _.uniqueId('dropdown-item-desc-'); + } + + renderDefault() { + let children = []; + const { icon, label } = this.props; + + return [ + (<Icon iconName={icon} key="icon" className={styles.itemIcon}/>), + (<span className={styles.itemLabel} key="label">{label}</span>), + ]; + } + + render() { + const { label, description, children, + injectRef, tabIndex, onClick, onKeyDown, } = this.props; + + return ( + <li + ref={injectRef} + onClick={onClick} + onKeyDown={onKeyDown} + tabIndex={tabIndex} + aria-labelledby={this.labelID} + aria-describedby={this.descID} + className={styles.item} + role="menuitem"> + { + children ? children + : this.renderDefault() + } + { + label ? + (<span id={this.labelID} key="labelledby" hidden>{label}</span>) + : null + } + { + description ? + (<span id={this.descID} key="describedby" hidden>{description}</span>) + : null + } + </li> + ); + } +} + +DropdownListItem.propTypes = propTypes; diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/separator/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/list/separator/component.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7c2aca38861d210b796d24f590a96ad879a5c24c --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/separator/component.jsx @@ -0,0 +1,8 @@ +import React, { Component, PropTypes } from 'react'; +import styles from '../styles'; + +export default class DropdownListSeparator extends Component { + render() { + return <li className={styles.separator} role="separator" />; + } +} diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss b/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss new file mode 100644 index 0000000000000000000000000000000000000000..c652284a7202079fb2c8f19d9c5fd7bb929abeb2 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss @@ -0,0 +1,66 @@ +@import "../../../stylesheets/variables/_all"; + +.list { + list-style: none; + font-size: $font-size-base; + margin: 0; + padding: 0; + text-align: left; + color: $color-gray-dark; + padding: ($line-height-computed / 2); + display: flex; + flex-direction: column; +} + + +.separator { + display: flex; + flex: 1 1 100%; + height: 1px; + background-color: $color-gray-lighter; + padding: 0; + margin-left: -($line-height-computed / 2); + margin-right: -($line-height-computed / 2); + margin-top: $line-height-computed * .5; + margin-bottom: $line-height-computed * .5; +} + +.item { + flex: 1 1 100%; + padding: ($line-height-computed / 3) 0; + display: flex; + align-items: center; + justify-content: flex-start; + + &:first-child { + padding-top: 0; + } + + &:last-child { + padding-bottom: 0; + } + + &:hover, + &:focus { + cursor: pointer; + color: $color-primary; + + .itemIcon, + .itemLabel { + color: inherit; + } + } +} + +.itemIcon { + margin-right: ($line-height-computed / 2); + color: $color-text; + flex: 0 0; +} + +.itemLabel { + color: $color-gray-dark; + font-size: 90%; + flex: 1; + white-space: nowrap; +} diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/styles.scss b/bigbluebutton-html5/imports/ui/components/dropdown/styles.scss index 08fd2d910054d65d97f01d8b385adb13cb8d0beb..f5c14261f02d8da05c77a5850525453f2e3d35b2 100755 --- a/bigbluebutton-html5/imports/ui/components/dropdown/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/dropdown/styles.scss @@ -1,7 +1,218 @@ @import "../../stylesheets/variables/_all"; -.dropdown {} +$dropdown-bg: $color-white; +$dropdown-color: $color-text; -.content {} +$dropdown-caret-width: 12px; +$dropdown-caret-height: 8px; + +.dropdown { + position: relative; +} + +.content { + position: absolute; + background: $dropdown-bg; + border-radius: $border-radius; + box-shadow: 0 6px 12px rgba(0, 0, 0, .175); + border: 1px solid rgba(0, 0, 0, .15); + padding: $line-height-computed / 2; + // min-width: 150px; + z-index: 1000; + + &:after { + content: ''; + position: absolute; + width: 0; + height: 0; + } + + &[aria-expanded="false"] { + display: none; + } + + &[aria-expanded="true"] { + display: block; + } +} .trigger {} + + +/* Placements + * ========== + */ + +%down-caret { + bottom: 100%; + left: 50%; + transform: translateX(-50%); + margin-bottom: $dropdown-caret-height * 1.25; + + &:after { + border-left: $dropdown-caret-width solid transparent; + border-right: $dropdown-caret-width solid transparent; + border-top: $dropdown-caret-height solid $dropdown-bg; + bottom: 0; + margin-bottom: -($dropdown-caret-height); + } +} + +%up-caret { + top: 100%; + left: 50%; + transform: translateX(-50%); + margin-top: $dropdown-caret-height * 1.25; + + &:after { + border-left: $dropdown-caret-width solid transparent; + border-right: $dropdown-caret-width solid transparent; + border-bottom: $dropdown-caret-height solid $dropdown-bg; + margin-top: -($dropdown-caret-height); + top: 0; + } +} + +%right-caret { + top: 50%; + transform: translateX(-100%) translateY(-50%); + left: -($dropdown-caret-height * 1.25); + + &:after { + border-top: $dropdown-caret-width solid transparent; + border-bottom: $dropdown-caret-width solid transparent; + border-left: $dropdown-caret-height solid $dropdown-bg; + margin-right: -($dropdown-caret-height); + top: 50%; + right: 0; + } +} + +%left-caret { + top: 50%; + transform: translateX(100%) translateY(-50%); + right: -($dropdown-caret-height * 1.25); + + &:after { + border-top: $dropdown-caret-width solid transparent; + border-bottom: $dropdown-caret-width solid transparent; + border-right: $dropdown-caret-height solid $dropdown-bg; + margin-left: -($dropdown-caret-height); + top: 50%; + left: 0; + } +} + +%horz-center-caret { + &:after { + margin-left: -($dropdown-caret-width); + } +} + +%horz-left-caret { + transform: translateX(-100%); + left: 100%; + + &:after { + right: $dropdown-caret-width / 2; + } +} + +%horz-right-caret { + transform: translateX(100%); + right: 100%; + left: auto; + + &:after { + left: $dropdown-caret-width / 2; + } +} + +%vert-center-caret { + &:after { + margin-top: -($dropdown-caret-width); + } +} + +%vert-top-caret { + top: 0; + + &:after { + top: 0; + margin-top: $dropdown-caret-width / 2; + } +} + +%vert-bottom-caret { + top: auto; + bottom: 0; + + &:after { + top: auto; + bottom: $dropdown-caret-width / 2; + } +} + +.top { + @extend %down-caret; + @extend %horz-center-caret; +} + +.top-left { + @extend %down-caret; + @extend %horz-right-caret; +} + +.top-right { + @extend %down-caret; + @extend %horz-left-caret; +} + +.bottom { + @extend %up-caret; + @extend %horz-center-caret; +} + +.bottom-left { + @extend %up-caret; + @extend %horz-right-caret; +} + +.bottom-right { + @extend %up-caret; + @extend %horz-left-caret; +} + +.left { + @extend %right-caret; + @extend %vert-center-caret; +} + +.left-top { + @extend %right-caret; + @extend %vert-top-caret; + transform: translateX(-100%) translateY(0); +} + +.left-bottom { + @extend %right-caret; + @extend %vert-bottom-caret; + transform: translateX(-100%) translateY(0); +} + +.right { + @extend %left-caret; + @extend %vert-center-caret; +} + +.right-top { + @extend %left-caret; + @extend %vert-top-caret; + transform: translateX(100%) translateY(0); +} + +.right-bottom { + @extend %left-caret; + @extend %vert-bottom-caret; + transform: translateX(100%) translateY(0); +} diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/trigger/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/trigger/component.jsx index fc9579f5accdb1a4f724b4df3338b689daa7d47c..11d832c8b596513ac8275bfaeca4fc7b6fb4631c 100755 --- a/bigbluebutton-html5/imports/ui/components/dropdown/trigger/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/dropdown/trigger/component.jsx @@ -1,4 +1,7 @@ import React, { Component, PropTypes } from 'react'; +import { findDOMNode } from 'react-dom'; + +import KEY_CODES from '/imports/utils/keyCodes'; const propTypes = { children: React.PropTypes.element.isRequired, @@ -7,14 +10,42 @@ const propTypes = { export default class DropdownTrigger extends Component { constructor(props) { super(props); + this.handleClick = this.handleClick.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + } + + handleClick() { + const { dropdownToggle } = this.props; + return dropdownToggle(); + } + + handleKeyDown(event) { + const { dropdownShow, dropdownHide } = this.props; + + event.preventDefault(); + event.stopPropagation(); + + if ([KEY_CODES.SPACE, KEY_CODES.ENTER].includes(event.which)) { + return findDOMNode(this).click(); + } + + if ([KEY_CODES.ARROW_UP, KEY_CODES.ARROW_DOWN].includes(event.which)) { + dropdownShow(); + } + + if (KEY_CODES.ESCAPE === event.which) { + dropdownHide(); + } + } render() { - const { children, handleToggle } = this.props; + const { children } = this.props; const TriggerComponent = React.Children.only(children); - const TriggerComponentBounded = React.cloneElement(TriggerComponent, { - onClick: handleToggle, + const TriggerComponentBounded = React.cloneElement(children, { + onClick: this.handleClick, + onKeyDown: this.handleKeyDown, 'aria-haspopup': true, }); diff --git a/bigbluebutton-html5/imports/ui/components/leave-confirmation/component.jsx b/bigbluebutton-html5/imports/ui/components/leave-confirmation/component.jsx index d97b015674dabd8998a1597f0c92d08297dfd822..d2e1c076b23af3fda38069373c0d23c2e8d95420 100644 --- a/bigbluebutton-html5/imports/ui/components/leave-confirmation/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/leave-confirmation/component.jsx @@ -43,7 +43,7 @@ class LeaveConfirmation extends Component { <Modal title={intl.formatMessage(intlMessages.title)} confirm={{ - callback: this.handleLeave, + callback: this.handleLeaveConfirmation, label: intl.formatMessage(intlMessages.confirmBtn), description: null, }} diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx index 2aa8caef0890c789e330aac2565f62e908fc11ca..c51a89a914107347d1da80c1fd9020b5330fc365 100644 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx @@ -13,207 +13,43 @@ import Button from '/imports/ui/components/button/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'; - -export default class SettingsDropdown extends Component { - constructor(props) { - super(props); - this.menus = [ - { - callback: (() => { console.log('SHOULD ENTER FULLSCREEN MODE'); }), - props: { - title: 'Fullscreen', - icon: 'full-screen', - ariaLabelleby: 'fullscreenLabel', - ariaDescribedby: 'fullscreenDesc', - }, - tabIndex: 1, - }, - { - callback: (() => { showModal(<Settings />); }), - props: { - title: 'Settings', - icon: 'more', - ariaLabelleby: 'settingsLabel', - ariaDescribedby: 'settingsDesc', - }, - tabIndex: 2, - }, - { - callback: (() => { showModal(<LogoutConfirmation />); }), - props: { - title: 'Leave Session', - icon: 'logout', - ariaLabelleby: 'leaveSessionLabel', - ariaDescribedby: 'leaveSessionDesc', - }, - tabIndex: 3, - }, - ]; - this.openWithKey = this.openWithKey.bind(this); - } - - componentWillMount() { - this.setState({ activeMenu: -1, focusedMenu: 0, }); - } - - componentWillUpdate() { - const DROPDOWN = this.refs.dropdown; - if (DROPDOWN.state.isMenuOpen && this.state.activeMenu >= 0) { - this.setState({ activeMenu: -1, focusedMenu: 0, }); +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'; + +const toggleFullScreen = () => { + let element = document.documentElement; + + if (document.fullscreenEnabled + || document.mozFullScreenEnabled + || document.webkitFullscreenEnabled) { + if (element.requestFullscreen) { + element.requestFullscreen(); + } else if (element.mozRequestFullScreen) { + element.mozRequestFullScreen(); + } else if (element.webkitRequestFullscreen) { + element.webkitRequestFullscreen(); + } else if (element.msRequestFullscreen) { + element.msRequestFullscreen(); } - } - - 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.handleHide(); // FIXME: We should not use internal functions by ref - } - } 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; + } else { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); } - - //User pressed ESC - if (pressedKey == 27) { - this.setState({ activeMenu: -1, focusedMenu: 0, }); - this.refs.dropdown.handleHide(); // FIXME: We should not use internal functions by ref - } - - return; - } - - handleFocus(index) { - this.setState({ focusedMenu: index, }, - () => { this.setFocus(); }); - } - - clickMenu(i) { - this.setState({ activeMenu: i, }); - this.refs.dropdown.handleHide(); // FIXME: We should not use internal functions by ref - this.menus[i].callback(); - } - - 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> +const openSettings = () => showModal(<Settings />); - {/* 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> - ); +const openLogoutConfirmation = () => showModal(<LogoutConfirmation />); +export default class SettingsDropdown extends Component { + constructor(props) { + super(props); } render() { @@ -227,39 +63,34 @@ export default class SettingsDropdown extends Component { ghost={true} circle={true} hideLabel={true} + + // FIXME: Without onClick react proptypes keep warning + // even after the DropdownTrigger inject an onClick handler + onClick={() => null} /> </DropdownTrigger> - <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 placement="bottom right"> + <DropdownList> + <DropdownListItem + icon="full-screen" + label="Fullscreen" + defaultMessage="Make the application fullscreen" + onClick={toggleFullScreen.bind(this)} + /> + <DropdownListItem + icon="more" + label="Settings" + description="Change the general settings" + onClick={openSettings.bind(this)} + /> + <DropdownListSeparator /> + <DropdownListItem + icon="logout" + label="Leave Session" + description="Leave the meeting" + onClick={openLogoutConfirmation.bind(this)} + /> + </DropdownList> </DropdownContent> </Dropdown> ); diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/styles.scss b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/styles.scss index d4a75cc03fd63ef07299733bb8371a03a5c94b90..746c3831b0420f0ba9b2f5c6c75c2140df5d8eac 100644 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/styles.scss @@ -1,67 +1 @@ @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; - } diff --git a/bigbluebutton-html5/imports/utils/keyCodes.js b/bigbluebutton-html5/imports/utils/keyCodes.js new file mode 100644 index 0000000000000000000000000000000000000000..555e0ef1bf49204671ed80199b7a03229d2d7f83 --- /dev/null +++ b/bigbluebutton-html5/imports/utils/keyCodes.js @@ -0,0 +1,15 @@ +export const SPACE = 32; +export const ENTER = 13; +export const TAB = 9; +export const ESCAPE = 27; +export const ARROW_UP = 38; +export const ARROW_DOWN = 40; + +export default { + SPACE, + ENTER, + TAB, + ESCAPE, + ARROW_UP, + ARROW_DOWN, +};