Merge pull request #3964 from KDSBrowne/x0-userlist-accessibility-suggestions

[HTML5] - Userlist Accessibility Fix
This commit is contained in:
Anton Georgiev 2017-06-26 15:59:31 -04:00 committed by GitHub
commit 3cf46a94a8
10 changed files with 373 additions and 385 deletions

View File

@ -58,8 +58,8 @@ class ActionsDropdown extends Component {
return null; // temporarily disabling the functionality
return (
<Dropdown>
<DropdownTrigger>
<Dropdown ref={(ref) => { this._dropdown = ref; }}>
<DropdownTrigger tabIndex={0}>
<Button
label={intl.formatMessage(intlMessages.actionsLabel)}
icon="add"

View File

@ -29,8 +29,8 @@ class EmojiMenu extends Component {
} = this.props;
return (
<Dropdown autoFocus>
<DropdownTrigger placeInTabOrder>
<Dropdown autoFocus={true}>
<DropdownTrigger tabIndex={0}>
<Button
role="button"
label={intl.formatMessage(intlMessages.statusTriggerLabel)}

View File

@ -52,6 +52,7 @@ const propTypes = {
const defaultProps = {
isOpen: false,
autoFocus: false,
};
class Dropdown extends Component {
@ -104,13 +105,15 @@ class Dropdown extends Component {
}
handleWindowClick(event) {
const dropdownElement = findDOMNode(this);
const shouldUpdateState = event.target !== dropdownElement &&
if (this.state.isOpen) {
const dropdownElement = findDOMNode(this);
const shouldUpdateState = event.target !== dropdownElement &&
!dropdownElement.contains(event.target) &&
this.state.isOpen;
if (shouldUpdateState) {
this.handleHide();
if (shouldUpdateState) {
this.handleHide();
}
}
}
@ -124,10 +127,9 @@ class Dropdown extends Component {
const {
children,
className,
style, intl,
hasPopup,
ariaLive,
ariaRelevant,
style,
intl,
...otherProps,
} = this.props;
let trigger = children.find(x => x.type === DropdownTrigger);
@ -152,10 +154,9 @@ class Dropdown extends Component {
<div
style={style}
className={cx(styles.dropdown, className)}
aria-live={ariaLive}
aria-relevant={ariaRelevant}
aria-haspopup={hasPopup}
>
aria-live={otherProps['aria-live']}
aria-relevant={otherProps['aria-relevant']}
aria-haspopup={otherProps['aria-haspopup']}>
{trigger}
{content}
{ this.state.isOpen ?

View File

@ -2,9 +2,7 @@ import React, { Component, Children, cloneElement } from 'react';
import PropTypes from 'prop-types';
import styles from './styles';
import cx from 'classnames';
import KEY_CODES from '/imports/utils/keyCodes';
import ListItem from './item/component';
import ListSeparator from './separator/component';
import ListTitle from './title/component';
@ -26,77 +24,99 @@ export default class DropdownList extends Component {
constructor(props) {
super(props);
this.childrenRefs = [];
this.menuRefs = [];
this.handleItemKeyDown = this.handleItemKeyDown.bind(this);
this.handleItemClick = this.handleItemClick.bind(this);
}
componentDidMount() {
this._menu.addEventListener('keydown', event=>this.handleItemKeyDown(event));
}
componentWillMount() {
this.setState({
activeItemIndex: 0,
focusedIndex: 0,
});
}
componentDidUpdate(prevProps, prevState) {
let { activeItemIndex } = this.state;
let { focusedIndex } = this.state;
if (activeItemIndex === null) {
activeItemIndex = 0;
this.menuRefs = [];
for (let i = 0; i < (this._menu.children.length); i++) {
if (this._menu.children[i].getAttribute("role") === 'menuitem') {
this.menuRefs.push(this._menu.children[i]);
}
}
const activeRef = this.childrenRefs[activeItemIndex];
const activeRef = this.menuRefs[focusedIndex];
if (activeRef) {
activeRef.focus();
}
}
handleItemKeyDown(event, callback) {
const { dropdownHide } = this.props;
const { activeItemIndex } = this.state;
if ([KEY_CODES.SPACE, KEY_CODES.ENTER].includes(event.which)) {
event.preventDefault();
event.stopPropagation();
return event.currentTarget.click();
}
let nextActiveItemIndex = null;
const { onActionsHide, getDropdownMenuParent, } = this.props;
let nextFocusedIndex = this.state.focusedIndex;
if (KEY_CODES.ARROW_UP === event.which) {
nextActiveItemIndex = activeItemIndex - 1;
event.stopPropagation();
nextFocusedIndex -= 1;
if (nextFocusedIndex < 0) {
nextFocusedIndex = this.menuRefs.length - 1;
}else if (nextFocusedIndex > this.menuRefs.length - 1) {
nextFocusedIndex = 0;
}
}
if (KEY_CODES.ARROW_DOWN === event.which) {
nextActiveItemIndex = activeItemIndex + 1;
if ([KEY_CODES.ARROW_DOWN].includes(event.keyCode)) {
event.stopPropagation();
nextFocusedIndex += 1;
if (nextFocusedIndex > this.menuRefs.length - 1) {
nextFocusedIndex = 0;
}
}
if (nextActiveItemIndex > (this.childrenRefs.length - 1)) {
nextActiveItemIndex = 0;
if ([KEY_CODES.ENTER, KEY_CODES.ARROW_RIGHT].includes(event.keyCode)) {
event.stopPropagation();
document.activeElement.firstChild.click();
}
if (nextActiveItemIndex < 0) {
nextActiveItemIndex = this.childrenRefs.length - 1;
}
if ([KEY_CODES.ESCAPE].includes(event.which)) {
nextActiveItemIndex = 0;
if ([KEY_CODES.ESCAPE, KEY_CODES.TAB, KEY_CODES.ARROW_LEFT].includes(event.keyCode)) {
const { dropdownHide } = this.props;
event.stopPropagation();
event.preventDefault();
dropdownHide();
if (getDropdownMenuParent) {
getDropdownMenuParent().focus();
}
}
this.setState({ activeItemIndex: nextActiveItemIndex });
this.setState({focusedIndex: nextFocusedIndex});
if (typeof callback === 'function') {
callback(event);
}
}
handleItemClick(event, callback) {
const { getDropdownMenuParent, onActionsHide} = this.props;
const { dropdownHide } = this.props;
this.setState({ activeItemIndex: null });
dropdownHide();
if ( getDropdownMenuParent ) {
onActionsHide();
}else{
this.setState({ focusedIndex: null });
dropdownHide();
}
if (typeof callback === 'function') {
callback(event);
@ -121,7 +141,6 @@ export default class DropdownList extends Component {
onClick: (event) => {
let { onClick } = item.props;
onClick = onClick ? onClick.bind(item) : null;
this.handleItemClick(event, onClick);
},
@ -135,8 +154,11 @@ export default class DropdownList extends Component {
});
return (
<ul style={style} className={cx(styles.list, className)} role="menu">
{boundChildren}
<ul
style={style}
className={cx(styles.list, className)}
role="menu" ref={(r) => this._menu = r}>
{boundChildren}
</ul>
);
}

View File

@ -11,6 +11,9 @@ const propTypes = {
description: PropTypes.string,
};
const defaultProps = {
};
export default class DropdownListItem extends Component {
constructor(props) {
super(props);
@ -30,16 +33,14 @@ export default class DropdownListItem extends Component {
render() {
const { label, description, children, injectRef, tabIndex, onClick, onKeyDown,
className, style, separator, intl, placeInTabOrder } = this.props;
const index = (placeInTabOrder) ? 0 : -1;
className, style, separator, intl, } = this.props;
return (
<li
ref={injectRef}
onClick={onClick}
onKeyDown={onKeyDown}
tabIndex={index}
tabIndex={tabIndex}
aria-labelledby={this.labelID}
aria-describedby={this.descID}
className={cx(styles.item, className)}
@ -65,3 +66,4 @@ export default class DropdownListItem extends Component {
}
DropdownListItem.propTypes = propTypes;
DropdownListItem.defaultProps = defaultProps;

View File

@ -41,17 +41,16 @@ export default class DropdownTrigger extends Component {
}
render() {
const { children, style, className, placeInTabOrder } = this.props;
const { children, style, className, tabIndex, } = this.props;
const TriggerComponent = React.Children.only(children);
const index = (placeInTabOrder) ? '0' : '-1';
const TriggerComponentBounded = React.cloneElement(children, {
onClick: this.handleClick,
onKeyDown: this.handleKeyDown,
'aria-haspopup': true,
tabIndex: index,
style,
tabIndex: tabIndex,
style: style,
className: cx(children.props.className, className),
});

View File

@ -103,13 +103,11 @@ class SettingsDropdown extends Component {
}
return (
<Dropdown
isOpen={this.state.isSettingOpen}
onShow={this.onActionsShow}
onHide={this.onActionsHide}
autoFocus
>
<DropdownTrigger placeInTabOrder>
<Dropdown autoFocus={true}
isOpen={this.state.isSettingOpen}
onShow={this.onActionsShow}
onHide={this.onActionsHide}>
<DropdownTrigger tabIndex={0}>
<Button
label={intl.formatMessage(intlMessages.optionsLabel)}
icon="more"

View File

@ -35,126 +35,83 @@ class UserList extends Component {
this.rovingIndex = this.rovingIndex.bind(this);
this.focusList = this.focusList.bind(this);
this.focusListItem = this.focusListItem.bind(this);
this.counter = -1;
this.focusedItemIndex = -1;
}
focusList(activeElement, list) {
activeElement.tabIndex = -1;
this.counter = 0;
focusList(list) {
document.activeElement.tabIndex = -1;
this.focusedItemIndex = -1;
list.tabIndex = 0;
list.focus();
}
focusListItem(active, direction, element, count) {
function select() {
element.tabIndex = 0;
element.focus();
}
active.tabIndex = -1;
switch (direction) {
case 'down':
element.childNodes[this.counter].tabIndex = 0;
element.childNodes[this.counter].focus();
this.counter++;
break;
case 'up':
this.counter--;
element.childNodes[this.counter].tabIndex = 0;
element.childNodes[this.counter].focus();
break;
case 'upLoopUp':
case 'upLoopDown':
this.counter = count - 1;
select();
break;
case 'downLoopDown':
this.counter = -1;
select();
break;
case 'downLoopUp':
this.counter = 1;
select();
break;
}
}
rovingIndex(...Args) {
rovingIndex(event, listType) {
const { users, openChats } = this.props;
const active = document.activeElement;
let active = document.activeElement;
let list;
let items;
let count;
switch (Args[1]) {
let numberOfItems;
const focusElement = () => {
active.tabIndex = -1;
items.childNodes[this.focusedItemIndex].tabIndex = 0;
items.childNodes[this.focusedItemIndex].focus();
}
switch (listType) {
case 'users':
list = this._usersList;
items = this._userItems;
count = users.length;
numberOfItems = users.length;
break;
case 'messages':
list = this._msgsList;
items = this._msgItems;
count = openChats.length;
numberOfItems = openChats.length;
break;
}
if (Args[0].keyCode === KEY_CODES.ESCAPE
|| this.counter === -1
|| this.counter > count) {
this.focusList(active, list);
if (event.keyCode === KEY_CODES.ESCAPE
|| this.focusedItemIndex < 0
|| this.focusedItemIndex > numberOfItems) {
this.focusList(list);
}
if (Args[0].keyCode === KEY_CODES.ENTER
|| Args[0].keyCode === KEY_CODES.ARROW_RIGHT
|| Args[0].keyCode === KEY_CODES.ARROW_LEFT) {
if ([KEY_CODES.ARROW_RIGHT, KEY_CODES.ARROW_SPACE].includes(event.keyCode)) {
active.firstChild.click();
}
if (Args[0].keyCode === KEY_CODES.ARROW_DOWN) {
if (this.counter < count) {
this.focusListItem(active, 'down', items);
} else if (this.counter === count) {
this.focusListItem(active, 'downLoopDown', list);
} else if (this.counter === 0) {
this.focusListItem(active, 'downLoopUp', list);
if (event.keyCode === KEY_CODES.ARROW_DOWN) {
this.focusedItemIndex += 1;
if (this.focusedItemIndex == numberOfItems) {
this.focusedItemIndex = 0;
}
focusElement();
}
if (Args[0].keyCode === KEY_CODES.ARROW_UP) {
if (this.counter < count && this.counter !== 0) {
this.focusListItem(active, 'up', items);
} else if (this.counter === 0) {
this.focusListItem(active, 'upLoopUp', list, count);
} else if (this.counter === count) {
this.focusListItem(active, 'upLoopDown', list, count);
if (event.keyCode === KEY_CODES.ARROW_UP) {
this.focusedItemIndex -= 1;
if (this.focusedItemIndex < 0) {
this.focusedItemIndex = numberOfItems - 1;
}
focusElement();
}
}
componentDidMount() {
const _this = this;
if (!this.state.compact) {
this._msgsList.addEventListener('keypress', function (event) {
_this.rovingIndex.call(this, event, 'messages');
});
this._msgsList.addEventListener('keydown',
event=>this.rovingIndex(event, "messages"));
this._usersList.addEventListener('keypress', function (event) {
_this.rovingIndex.call(this, event, 'users');
});
this._usersList.addEventListener('keydown',
event=>this.rovingIndex(event, "users"));
}
}
componentWillUnmount() {
this._msgsList.removeEventListener('keypress', (event) => {}, false);
this._usersList.removeEventListener('keypress', (event) => {}, false);
}
render() {
return (
<div className={styles.userList}>

View File

@ -54,13 +54,9 @@ const messages = defineMessages({
id: 'app.userlist.menuTitleContext',
description: 'adds context to userListItem menu title',
},
userItemStatusAriaLabel: {
id: 'app.userlist.useritem.status.arialabel',
description: 'adds aria label for user and status',
},
userItemAriaLabel: {
id: 'app.userlist.useritem.nostatus.arialabel',
description: 'aria label for user',
userAriaLabel: {
id: 'app.userlist.userAriaLabel',
description: 'aria label for each user in the userlist',
},
});
@ -97,6 +93,7 @@ class UserListItem extends Component {
this.handleScroll = this.handleScroll.bind(this);
this.onActionsShow = this.onActionsShow.bind(this);
this.onActionsHide = this.onActionsHide.bind(this);
this.getDropdownMenuParent = this.getDropdownMenuParent.bind(this);
}
handleScroll() {
@ -153,6 +150,7 @@ class UserListItem extends Component {
* Check if the dropdown is visible, if so, check if should be draw on top or bottom direction.
*/
checkDropdownDirection() {
if (this.isDropdownActivedByUser()) {
const dropdown = findDOMNode(this.dropdown);
const dropdownTrigger = dropdown.children[0];
@ -174,7 +172,7 @@ class UserListItem extends Component {
nextState.dropdownOffset = window.innerHeight - offsetPageTop;
nextState.dropdownDirection = 'bottom';
}
this.setState(nextState);
}
}
@ -186,6 +184,17 @@ class UserListItem extends Component {
*/
isDropdownActivedByUser() {
const { isActionsOpen, dropdownVisible } = this.state;
const list = findDOMNode(this.list);
if (isActionsOpen, dropdownVisible) {
for (let i = 0; i < list.children.length; i++) {
if (list.children[i].getAttribute('role') === 'menuitem') {
list.children[i].focus();
break;
}
}
}
return isActionsOpen && !dropdownVisible;
}
@ -215,6 +224,10 @@ class UserListItem extends Component {
scrollContainer.addEventListener('scroll', this.handleScroll, false);
}
getDropdownMenuParent() {
return findDOMNode(this.dropdown);
}
onActionsHide() {
this.setState({
isActionsOpen: false,
@ -238,23 +251,20 @@ class UserListItem extends Component {
intl,
} = this.props;
const you = (user.isCurrent) ? intl.formatMessage(messages.you) : null;
let you = (user.isCurrent) ? intl.formatMessage(messages.you) : '';
const presenter = (user.isPresenter)
? intl.formatMessage(messages.presenter)
: null;
: '';
const userAriaLabel = (user.emoji.status === 'none')
? intl.formatMessage(messages.userItemAriaLabel,
{ username: user.name, presenter, you })
: intl.formatMessage(messages.userItemStatusAriaLabel,
{ username: user.name,
presenter,
you,
status: user.emoji.status });
const actions = this.getAvailableActions();
const contents = (
const userAriaLabel = intl.formatMessage(messages.userAriaLabel,
{ 0: user.name,
1: presenter,
2: you,
3: user.emoji.status });
let actions = this.getAvailableActions();
let contents = (
<div
className={cx(styles.userListItem, userItemContentsStyle)}
aria-label={userAriaLabel}
@ -271,7 +281,7 @@ class UserListItem extends Component {
return contents;
}
const { dropdownOffset, dropdownDirection, dropdownVisible } = this.state;
const { dropdownOffset, dropdownDirection, dropdownVisible, } = this.state;
return (
<Dropdown
@ -281,10 +291,9 @@ class UserListItem extends Component {
onHide={this.onActionsHide}
className={styles.dropdown}
autoFocus={false}
hasPopup="true"
ariaLive="assertive"
ariaRelevant="additions"
>
aria-haspopup="true"
aria-live="assertive"
aria-relevant="additions">
<DropdownTrigger>
{contents}
</DropdownTrigger>
@ -297,7 +306,10 @@ class UserListItem extends Component {
placement={`right ${dropdownDirection}`}
>
<DropdownList>
<DropdownList
ref={(ref) => { this.list = ref; }}
getDropdownMenuParent={this.getDropdownMenuParent}
onActionsHide={this.onActionsHide}>
{
[
(<DropdownListTitle

View File

@ -1,206 +1,203 @@
{
"app.home.greeting": "Welcome {0}! Your presentation will begin shortly...",
"app.userlist.usersTitle": "Users",
"app.userlist.participantsTitle": "Participants",
"app.userlist.messagesTitle": "Messages",
"app.userlist.presenter": "Presenter",
"app.userlist.you": "You",
"app.userlist.locked": "Locked",
"app.userlist.Label": "User List",
"app.chat.submitLabel": "Send Message",
"app.chat.inputLabel": "Message input for chat {0}",
"app.chat.inputPlaceholder": "Message {0}",
"app.chat.errorMinMessageLength": "The message is {0} characters(s) too short",
"app.chat.errorMaxMessageLength": "The message is {0} characters(s) too long",
"app.chat.titlePublic": "Public Chat",
"app.chat.titlePrivate": "Private Chat with {0}",
"app.chat.partnerDisconnected": "{0} has left the meeting",
"app.chat.closeChatLabel": "Close {0}",
"app.chat.hideChatLabel": "Hide {0}",
"app.chat.moreMessages": "More messages below",
"app.userlist.menuTitleContext": "available options",
"app.userlist.chatlistitem.unreadSingular": "{0} New Message",
"app.userlist.chatlistitem.unreadPlural": "{0} New Messages",
"app.userlist.menu.chat.label": "Chat",
"app.userlist.menu.clearStatus.label": "Clear Status",
"app.userlist.menu.makePresenter.label": "Make Presenter",
"app.userlist.menu.kickUser.label": "Kick user",
"app.userlist.menu.muteUserAudio.label": "Mute user",
"app.userlist.menu.unmuteUserAudio.label": "Unmute user",
"app.userlist.useritem.nostatus.arialabel": "{username} {presenter} {you}",
"app.userlist.useritem.status.arialabel": "{username} {presenter} {you} current status {status}",
"app.chat.Label": "Chat",
"app.chat.emptyLogLabel": "Chat log empty",
"app.media.Label": "Media",
"app.presentation.presentationToolbar.prevSlideLabel": "Previous slide",
"app.presentation.presentationToolbar.prevSlideDescrip": "Change the presentation to the previous slide",
"app.presentation.presentationToolbar.nextSlideLabel": "Next slide",
"app.presentation.presentationToolbar.nextSlideDescrip": "Change the presentation to the next slide",
"app.presentation.presentationToolbar.skipSlideLabel": "Skip slide",
"app.presentation.presentationToolbar.skipSlideDescrip": "Change the presentation to a specific slide",
"app.presentation.presentationToolbar.fitWidthLabel": "Fit to width",
"app.presentation.presentationToolbar.fitWidthDescrip": "Display the whole width of the slide",
"app.presentation.presentationToolbar.fitScreenLabel": "Fit to screen",
"app.presentation.presentationToolbar.fitScreenDescrip": "Display the whole slide",
"app.presentation.presentationToolbar.zoomLabel": "Zoom",
"app.presentation.presentationToolbar.zoomDescrip": "Change the zoom level of the presentation",
"app.polling.pollingTitle": "Polling Options",
"app.failedMessage": "Apologies, trouble connecting to the server.",
"app.connectingMessage": "Connecting...",
"app.waitingMessage": "Disconnected. Trying to reconnect in {0} seconds...",
"app.navBar.settingsDropdown.optionsLabel": "Options",
"app.navBar.settingsDropdown.fullscreenLabel": "Make fullscreen",
"app.navBar.settingsDropdown.settingsLabel": "Open settings",
"app.navBar.settingsDropdown.aboutLabel": "About",
"app.navBar.settingsDropdown.leaveSessionLabel": "Logout",
"app.navBar.settingsDropdown.fullscreenDesc": "Make the settings menu fullscreen",
"app.navBar.settingsDropdown.settingsDesc": "Change the general settings",
"app.navBar.settingsDropdown.aboutDesc": "Show information about the client",
"app.navBar.settingsDropdown.leaveSessionDesc": "Leave the meeting",
"app.navBar.settingsDropdown.exitFullScreenLabel": "Exit fullscreen",
"app.navBar.settingsDropdown.exitFullScreenDesc": "Exit fullscreen mode",
"app.navBar.userListToggleBtnLabel": "User List Toggle",
"app.navbar.toggleUserList.newMessages": "with new message notification",
"app.leaveConfirmation.title": "Leave Session",
"app.leaveConfirmation.message": "Do you want to leave this meeting?",
"app.leaveConfirmation.confirmLabel": "Leave",
"app.leaveConfirmation.confirmDesc": "Logs you out of the meeting",
"app.leaveConfirmation.dismissLabel": "Cancel",
"app.leaveConfirmation.dismissDesc": "Closes and rejects the leave confirmation",
"app.about.title": "About",
"app.about.version": "Client Build:",
"app.about.copyright": "Copyright:",
"app.about.confirmLabel": "OK",
"app.about.confirmDesc": "OK",
"app.about.dismissLabel": "Cancel",
"app.about.dismissDesc": "Close about client information",
"app.actionsBar.changeStatusLabel": "Change Status",
"app.actionsBar.muteLabel": "Mute",
"app.actionsBar.unmuteLabel": "Unmute",
"app.actionsBar.camOffLabel": "Cam Off",
"app.actionsBar.raiseLabel": "Raise",
"app.actionsBar.Label": "Actions Bar",
"app.submenu.application.applicationSectionTitle": "Application",
"app.submenu.application.audioNotifyLabel": "Audio notifications for chat",
"app.submenu.application.pushNotifyLabel": "Push notifications for chat",
"app.submenu.application.fontSizeControlLabel": "Font size",
"app.submenu.application.increaseFontBtnLabel": "Increase Application Font Size",
"app.submenu.application.decreaseFontBtnLabel": "Decrease Application Font Size",
"app.submenu.application.languageLabel": "Application Language",
"app.submenu.application.ariaLanguageLabel": "Change Application Language",
"app.submenu.application.languageOptionLabel": "Choose language",
"app.submenu.application.noLocaleOptionLabel": "No active locales",
"app.submenu.audio.micSourceLabel": "Microphone source",
"app.submenu.audio.speakerSourceLabel": "Speaker source",
"app.submenu.audio.streamVolumeLabel": "Your audio stream volume",
"app.submenu.video.title": "Video",
"app.submenu.video.videoSourceLabel": "View source",
"app.submenu.video.videoOptionLabel": "Choose view source",
"app.submenu.video.videoQualityLabel": "Video Quality",
"app.submenu.video.qualityOptionLabel": "Choose the video quality",
"app.submenu.video.participantsCamLabel": "Viewing participants webcams",
"app.submenu.closedCaptions.closedCaptionsLabel": "Closed Captions",
"app.submenu.closedCaptions.takeOwnershipLabel": "Take ownership",
"app.submenu.closedCaptions.languageLabel": "Language",
"app.submenu.closedCaptions.localeOptionLabel": "Choose language",
"app.submenu.closedCaptions.noLocaleOptionLabel": "No active locales",
"app.submenu.closedCaptions.fontFamilyLabel": "Font family",
"app.submenu.closedCaptions.fontFamilyOptionLabel": "Choose Font-family",
"app.submenu.closedCaptions.fontSizeLabel": "Font size",
"app.submenu.closedCaptions.fontSizeOptionLabel": "Choose Font size",
"app.submenu.closedCaptions.backgroundColorLabel": "Background color",
"app.submenu.closedCaptions.fontColorLabel": "Font color",
"app.submenu.participants.muteAllLabel": "Mute all except the presenter",
"app.submenu.participants.lockAllLabel": "Lock all participants",
"app.submenu.participants.lockItemLabel": "Participants {0}",
"app.submenu.participants.lockMicDesc": "Disables the microphone for all locked participants",
"app.submenu.participants.lockCamDesc": "Disables the webcam for all locked participants",
"app.submenu.participants.lockPublicChatDesc": "Disables public chat for all locked participants",
"app.submenu.participants.lockPrivateChatDesc": "Disables private chat for all locked participants",
"app.submenu.participants.lockLayoutDesc": "Locks layout for all locked participants",
"app.submenu.participants.lockMicAriaLabel": "Microphone lock",
"app.submenu.participants.lockCamAriaLabel": "Webcam lock",
"app.submenu.participants.lockPublicChatAriaLabel": "Public chat lock",
"app.submenu.participants.lockPrivateChatAriaLabel": "Private chat lock",
"app.submenu.participants.lockLayoutAriaLabel": "Layout lock",
"app.submenu.participants.lockMicLabel": "Microphone",
"app.submenu.participants.lockCamLabel": "Webcam",
"app.submenu.participants.lockPublicChatLabel": "Public Chat",
"app.submenu.participants.lockPrivateChatLabel": "Private Chat",
"app.submenu.participants.lockLayoutLabel": "Layout",
"app.settings.applicationTab.label": "Application",
"app.settings.audioTab.label": "Audio",
"app.settings.videoTab.label": "Video",
"app.settings.closedcaptionTab.label": "Closed Captions",
"app.settings.usersTab.label": "Participants",
"app.settings.main.label": "Settings",
"app.settings.main.cancel.label": "Cancel",
"app.settings.main.cancel.label.description": "Discards the changes and closes the settings menu",
"app.settings.main.save.label": "Save",
"app.settings.main.save.label.description": "Saves the changes and closes the settings menu",
"app.actionsBar.actionsDropdown.actionsLabel": "Actions",
"app.actionsBar.actionsDropdown.presentationLabel": "Upload a presentation",
"app.actionsBar.actionsDropdown.initPollLabel": "Initiate a poll",
"app.actionsBar.actionsDropdown.desktopShareLabel": "Share your screen",
"app.actionsBar.actionsDropdown.presentationDesc": "Upload your presentation",
"app.actionsBar.actionsDropdown.initPollDesc": "Initiate a poll",
"app.actionsBar.actionsDropdown.desktopShareDesc": "Share your screen with others",
"app.actionsBar.emojiMenu.statusTriggerLabel": "Status",
"app.actionsBar.emojiMenu.awayLabel": "Away",
"app.actionsBar.emojiMenu.awayDesc": "Change your status to away",
"app.actionsBar.emojiMenu.raiseLabel": "Raise",
"app.actionsBar.emojiMenu.raiseDesc": "Raise your hand to ask a question",
"app.actionsBar.emojiMenu.undecidedLabel": "Undecided",
"app.actionsBar.emojiMenu.undecidedDesc": "Change your status to undecided",
"app.actionsBar.emojiMenu.confusedLabel": "Confused",
"app.actionsBar.emojiMenu.confusedDesc": "Change your status to confused",
"app.actionsBar.emojiMenu.sadLabel": "Sad",
"app.actionsBar.emojiMenu.sadDesc": "Change your status to sad",
"app.actionsBar.emojiMenu.happyLabel": "Happy",
"app.actionsBar.emojiMenu.happyDesc": "Change your status to happy",
"app.actionsBar.emojiMenu.clearLabel": "Clear",
"app.actionsBar.emojiMenu.clearDesc": "Clear your status",
"app.actionsBar.emojiMenu.applauseLabel": "Applaud",
"app.actionsBar.emojiMenu.applauseDesc": "Change your status to applause",
"app.actionsBar.emojiMenu.thumbsupLabel": "Thumbs up",
"app.actionsBar.emojiMenu.thumbsupDesc": "Change your status to thumbs up",
"app.actionsBar.emojiMenu.thumbsdownLabel": "Thumbs down",
"app.actionsBar.emojiMenu.thumbsdownDesc": "Change your status to thumbs down",
"app.actionsBar.currentStatusDesc": "current status {0}",
"app.audioNotification.audioFailedMessage": "Your audio connection failed to connect",
"app.audioNotification.mediaFailedMessage": "getUserMicMedia failed, Only secure origins are allowed",
"app.audioNotification.closeLabel": "Close",
"app.breakoutJoinConfirmation.title": "Join Breakout Room",
"app.breakoutJoinConfirmation.message": "Do you want to join",
"app.breakoutJoinConfirmation.confirmLabel": "Join",
"app.breakoutJoinConfirmation.confirmDesc": "Join you to the Breakout Room",
"app.breakoutJoinConfirmation.dismissLabel": "Cancel",
"app.breakoutJoinConfirmation.dismissDesc": "Closes and rejects Joining the Breakout Room",
"app.breakoutTimeRemainingMessage": "Breakout Room time remaining: {0}",
"app.breakoutWillCloseMessage": "Time ended. Breakout Room will close soon",
"app.calculatingBreakoutTimeRemaining": "Calculating remaining time...",
"app.audioModal.microphoneLabel": "Microphone",
"app.audioModal.listenOnlyLabel": "Listen Only",
"app.audioModal.audioChoiceLabel": "How would you like to join the audio?",
"app.audioModal.audioChoiceDescription": "Select how to join the audio in this meeting",
"app.audioModal.closeLabel": "Close",
"app.audio.joinAudio": "Join Audio",
"app.audio.leaveAudio": "Leave Audio",
"app.audio.enterSessionLabel": "Enter Session",
"app.audio.playSoundLabel": "Play Sound",
"app.audio.backLabel": "Back",
"app.audio.audioSettings.titleLabel": "Choose your audio settings",
"app.audio.audioSettings.descriptionLabel": "Please note, a dialog will appear in your browser, requiring you to accept sharing your microphone.",
"app.audio.audioSettings.microphoneSourceLabel": "Microphone source",
"app.audio.audioSettings.speakerSourceLabel": "Speaker source",
"app.audio.audioSettings.microphoneStreamLabel": "Your audio stream volume",
"app.audio.listenOnly.backLabel": "Back",
"app.audio.listenOnly.closeLabel": "Close",
"app.error.kicked": "You have been kicked out of the meeting",
"app.dropdown.close": "Close",
"app.error.500": "Ops, something went wrong",
"app.error.404": "Not found",
"app.error.401": "Unauthorized",
"app.error.403": "Forbidden",
"app.error.leaveLabel": "Log in again"
"app.home.greeting": "Welcome {0}! Your presentation will begin shortly...",
"app.userlist.usersTitle": "Users",
"app.userlist.participantsTitle": "Participants",
"app.userlist.messagesTitle": "Messages",
"app.userlist.presenter": "Presenter",
"app.userlist.you": "You",
"app.userlist.locked": "Locked",
"app.userlist.Label": "User List",
"app.chat.submitLabel": "Send Message",
"app.chat.inputLabel": "Message input for chat {0}",
"app.chat.inputPlaceholder": "Message {0}",
"app.chat.titlePublic": "Public Chat",
"app.chat.titlePrivate": "Private Chat with {0}",
"app.chat.partnerDisconnected": "{0} has left the meeting",
"app.chat.closeChatLabel": "Close {0}",
"app.chat.hideChatLabel": "Hide {0}",
"app.chat.moreMessages": "More messages below",
"app.userlist.menuTitleContext": "available options",
"app.userlist.chatlistitem.unreadSingular": "{0} New Message",
"app.userlist.chatlistitem.unreadPlural": "{0} New Messages",
"app.userlist.menu.chat.label": "Chat",
"app.userlist.menu.clearStatus.label": "Clear Status",
"app.userlist.menu.makePresenter.label": "Make Presenter",
"app.userlist.menu.kickUser.label": "Kick user",
"app.userlist.menu.muteUserAudio.label": "Mute user",
"app.userlist.menu.unmuteUserAudio.label": "Unmute user",
"app.userlist.userAriaLabel": "user : {0} role: {1} person: {2} status: {3}",
"app.chat.Label": "Chat",
"app.chat.emptyLogLabel": "Chat log empty",
"app.media.Label": "Media",
"app.presentation.presentationToolbar.prevSlideLabel": "Previous slide",
"app.presentation.presentationToolbar.prevSlideDescrip": "Change the presentation to the previous slide",
"app.presentation.presentationToolbar.nextSlideLabel": "Next slide",
"app.presentation.presentationToolbar.nextSlideDescrip": "Change the presentation to the next slide",
"app.presentation.presentationToolbar.skipSlideLabel": "Skip slide",
"app.presentation.presentationToolbar.skipSlideDescrip": "Change the presentation to a specific slide",
"app.presentation.presentationToolbar.fitWidthLabel": "Fit to width",
"app.presentation.presentationToolbar.fitWidthDescrip": "Display the whole width of the slide",
"app.presentation.presentationToolbar.fitScreenLabel": "Fit to screen",
"app.presentation.presentationToolbar.fitScreenDescrip": "Display the whole slide",
"app.presentation.presentationToolbar.zoomLabel": "Zoom",
"app.presentation.presentationToolbar.zoomDescrip": "Change the zoom level of the presentation",
"app.polling.pollingTitle": "Polling Options",
"app.failedMessage": "Apologies, trouble connecting to the server.",
"app.connectingMessage": "Connecting...",
"app.waitingMessage": "Disconnected. Trying to reconnect in {0} seconds...",
"app.navBar.settingsDropdown.optionsLabel": "Options",
"app.navBar.settingsDropdown.fullscreenLabel": "Make fullscreen",
"app.navBar.settingsDropdown.settingsLabel": "Open settings",
"app.navBar.settingsDropdown.aboutLabel": "About",
"app.navBar.settingsDropdown.leaveSessionLabel": "Logout",
"app.navBar.settingsDropdown.fullscreenDesc": "Make the settings menu fullscreen",
"app.navBar.settingsDropdown.settingsDesc": "Change the general settings",
"app.navBar.settingsDropdown.aboutDesc": "Show information about the client",
"app.navBar.settingsDropdown.leaveSessionDesc": "Leave the meeting",
"app.navBar.settingsDropdown.exitFullScreenLabel": "Exit fullscreen",
"app.navBar.settingsDropdown.exitFullScreenDesc": "Exit fullscreen mode",
"app.navBar.userListToggleBtnLabel": "User List Toggle",
"app.navbar.toggleUserList.newMessages": "with new message notification",
"app.leaveConfirmation.title": "Leave Session",
"app.leaveConfirmation.message": "Do you want to leave this meeting?",
"app.leaveConfirmation.confirmLabel": "Leave",
"app.leaveConfirmation.confirmDesc": "Logs you out of the meeting",
"app.leaveConfirmation.dismissLabel": "Cancel",
"app.leaveConfirmation.dismissDesc": "Closes and rejects the leave confirmation",
"app.about.title": "About",
"app.about.version": "Client Build:",
"app.about.copyright": "Copyright:",
"app.about.confirmLabel": "OK",
"app.about.confirmDesc": "OK",
"app.about.dismissLabel": "Cancel",
"app.about.dismissDesc": "Close about client information",
"app.actionsBar.changeStatusLabel": "Change Status",
"app.actionsBar.muteLabel": "Mute",
"app.actionsBar.unmuteLabel": "Unmute",
"app.actionsBar.camOffLabel": "Cam Off",
"app.actionsBar.raiseLabel": "Raise",
"app.actionsBar.Label": "Actions Bar",
"app.submenu.application.applicationSectionTitle": "Application",
"app.submenu.application.audioNotifyLabel": "Audio notifications for chat",
"app.submenu.application.pushNotifyLabel": "Push notifications for chat",
"app.submenu.application.fontSizeControlLabel": "Font size",
"app.submenu.application.increaseFontBtnLabel": "Increase Application Font Size",
"app.submenu.application.decreaseFontBtnLabel": "Decrease Application Font Size",
"app.submenu.application.languageLabel": "Application Language",
"app.submenu.application.ariaLanguageLabel": "Change Application Language",
"app.submenu.application.languageOptionLabel": "Choose language",
"app.submenu.application.noLocaleOptionLabel": "No active locales",
"app.submenu.audio.micSourceLabel": "Microphone source",
"app.submenu.audio.speakerSourceLabel": "Speaker source",
"app.submenu.audio.streamVolumeLabel": "Your audio stream volume",
"app.submenu.video.title": "Video",
"app.submenu.video.videoSourceLabel": "View source",
"app.submenu.video.videoOptionLabel": "Choose view source",
"app.submenu.video.videoQualityLabel": "Video Quality",
"app.submenu.video.qualityOptionLabel": "Choose the video quality",
"app.submenu.video.participantsCamLabel": "Viewing participants webcams",
"app.submenu.closedCaptions.closedCaptionsLabel": "Closed Captions",
"app.submenu.closedCaptions.takeOwnershipLabel": "Take ownership",
"app.submenu.closedCaptions.languageLabel": "Language",
"app.submenu.closedCaptions.localeOptionLabel": "Choose language",
"app.submenu.closedCaptions.noLocaleOptionLabel": "No active locales",
"app.submenu.closedCaptions.fontFamilyLabel": "Font family",
"app.submenu.closedCaptions.fontFamilyOptionLabel": "Choose Font-family",
"app.submenu.closedCaptions.fontSizeLabel": "Font size",
"app.submenu.closedCaptions.fontSizeOptionLabel": "Choose Font size",
"app.submenu.closedCaptions.backgroundColorLabel": "Background color",
"app.submenu.closedCaptions.fontColorLabel": "Font color",
"app.submenu.participants.muteAllLabel": "Mute all except the presenter",
"app.submenu.participants.lockAllLabel": "Lock all participants",
"app.submenu.participants.lockItemLabel": "Participants {0}",
"app.submenu.participants.lockMicDesc": "Disables the microphone for all locked participants",
"app.submenu.participants.lockCamDesc": "Disables the webcam for all locked participants",
"app.submenu.participants.lockPublicChatDesc": "Disables public chat for all locked participants",
"app.submenu.participants.lockPrivateChatDesc": "Disables private chat for all locked participants",
"app.submenu.participants.lockLayoutDesc": "Locks layout for all locked participants",
"app.submenu.participants.lockMicAriaLabel": "Microphone lock",
"app.submenu.participants.lockCamAriaLabel": "Webcam lock",
"app.submenu.participants.lockPublicChatAriaLabel": "Public chat lock",
"app.submenu.participants.lockPrivateChatAriaLabel": "Private chat lock",
"app.submenu.participants.lockLayoutAriaLabel": "Layout lock",
"app.submenu.participants.lockMicLabel": "Microphone",
"app.submenu.participants.lockCamLabel": "Webcam",
"app.submenu.participants.lockPublicChatLabel": "Public Chat",
"app.submenu.participants.lockPrivateChatLabel": "Private Chat",
"app.submenu.participants.lockLayoutLabel": "Layout",
"app.settings.applicationTab.label": "Application",
"app.settings.audioTab.label": "Audio",
"app.settings.videoTab.label": "Video",
"app.settings.closedcaptionTab.label": "Closed Captions",
"app.settings.usersTab.label": "Participants",
"app.settings.main.label": "Settings",
"app.settings.main.cancel.label": "Cancel",
"app.settings.main.cancel.label.description": "Discards the changes and closes the settings menu",
"app.settings.main.save.label": "Save",
"app.settings.main.save.label.description": "Saves the changes and closes the settings menu",
"app.actionsBar.actionsDropdown.actionsLabel": "Actions",
"app.actionsBar.actionsDropdown.presentationLabel": "Upload a presentation",
"app.actionsBar.actionsDropdown.initPollLabel": "Initiate a poll",
"app.actionsBar.actionsDropdown.desktopShareLabel": "Share your screen",
"app.actionsBar.actionsDropdown.presentationDesc": "Upload your presentation",
"app.actionsBar.actionsDropdown.initPollDesc": "Initiate a poll",
"app.actionsBar.actionsDropdown.desktopShareDesc": "Share your screen with others",
"app.actionsBar.emojiMenu.statusTriggerLabel": "Status",
"app.actionsBar.emojiMenu.awayLabel": "Away",
"app.actionsBar.emojiMenu.awayDesc": "Change your status to away",
"app.actionsBar.emojiMenu.raiseLabel": "Raise",
"app.actionsBar.emojiMenu.raiseDesc": "Raise your hand to ask a question",
"app.actionsBar.emojiMenu.undecidedLabel": "Undecided",
"app.actionsBar.emojiMenu.undecidedDesc": "Change your status to undecided",
"app.actionsBar.emojiMenu.confusedLabel": "Confused",
"app.actionsBar.emojiMenu.confusedDesc": "Change your status to confused",
"app.actionsBar.emojiMenu.sadLabel": "Sad",
"app.actionsBar.emojiMenu.sadDesc": "Change your status to sad",
"app.actionsBar.emojiMenu.happyLabel": "Happy",
"app.actionsBar.emojiMenu.happyDesc": "Change your status to happy",
"app.actionsBar.emojiMenu.clearLabel": "Clear",
"app.actionsBar.emojiMenu.clearDesc": "Clear your status",
"app.actionsBar.emojiMenu.applauseLabel": "Applaud",
"app.actionsBar.emojiMenu.applauseDesc": "Change your status to applause",
"app.actionsBar.emojiMenu.thumbsupLabel": "Thumbs up",
"app.actionsBar.emojiMenu.thumbsupDesc": "Change your status to thumbs up",
"app.actionsBar.emojiMenu.thumbsdownLabel": "Thumbs down",
"app.actionsBar.emojiMenu.thumbsdownDesc": "Change your status to thumbs down",
"app.actionsBar.currentStatusDesc": "current status {0}",
"app.audioNotification.audioFailedMessage": "Your audio connection failed to connect",
"app.audioNotification.mediaFailedMessage": "getUserMicMedia failed, Only secure origins are allowed",
"app.audioNotification.closeLabel": "Close",
"app.breakoutJoinConfirmation.title": "Join Breakout Room",
"app.breakoutJoinConfirmation.message": "Do you want to join",
"app.breakoutJoinConfirmation.confirmLabel": "Join",
"app.breakoutJoinConfirmation.confirmDesc": "Join you to the Breakout Room",
"app.breakoutJoinConfirmation.dismissLabel": "Cancel",
"app.breakoutJoinConfirmation.dismissDesc": "Closes and rejects Joining the Breakout Room",
"app.breakoutTimeRemainingMessage": "Breakout Room time remaining: {0}",
"app.breakoutWillCloseMessage": "Time ended. Breakout Room will close soon",
"app.calculatingBreakoutTimeRemaining": "Calculating remaining time...",
"app.audioModal.microphoneLabel": "Microphone",
"app.audioModal.listenOnlyLabel": "Listen Only",
"app.audioModal.audioChoiceLabel": "How would you like to join the audio?",
"app.audioModal.audioChoiceDescription": "Select how to join the audio in this meeting",
"app.audioModal.closeLabel": "Close",
"app.audio.joinAudio": "Join Audio",
"app.audio.leaveAudio": "Leave Audio",
"app.audio.enterSessionLabel": "Enter Session",
"app.audio.playSoundLabel": "Play Sound",
"app.audio.backLabel": "Back",
"app.audio.audioSettings.titleLabel": "Choose your audio settings",
"app.audio.audioSettings.descriptionLabel": "Please note, a dialog will appear in your browser, requiring you to accept sharing your microphone.",
"app.audio.audioSettings.microphoneSourceLabel": "Microphone source",
"app.audio.audioSettings.speakerSourceLabel": "Speaker source",
"app.audio.audioSettings.microphoneStreamLabel": "Your audio stream volume",
"app.audio.listenOnly.backLabel": "Back",
"app.audio.listenOnly.closeLabel": "Close",
"app.error.kicked": "You have been kicked out of the meeting",
"app.dropdown.close": "Close",
"app.error.500": "Ops, something went wrong",
"app.error.404": "Not found",
"app.error.401": "Unauthorized",
"app.error.403": "Forbidden",
"app.error.leaveLabel": "Log in again"
}