feat: Make Jitsi WCAG 2.1 compliant (#8921)

* Make Jitsi WCAG 2.1 compliant

* Fixed password form keypress handling

* Added keypress handler to name form

* Removed unneccessary dom query

* Fixed mouse hove style

* Removed obsolete css rules

* accessibilty background feature

* Merge remote-tracking branch 'upstream/master' into nic/fix/merge-conflicts

* fix error

* add german translation

* Fixed merge issue

* Add id prop back to device selection

* Fixed lockfile

Co-authored-by: AHMAD KADRI <52747422+ahmadkadri@users.noreply.github.com>
This commit is contained in:
Steffen Kolmer 2021-06-10 14:48:44 +02:00 committed by GitHub
parent 76f1fe8457
commit e9675453e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
138 changed files with 3076 additions and 886 deletions

View File

@ -32,6 +32,18 @@
.dropdown-menu div[style*="transform"] { .dropdown-menu div[style*="transform"] {
outline: 1px solid #455166; outline: 1px solid #455166;
} }
.dropdown-menu button:not(:active):not(:hover) > span {
color: #B8C7E0;
}
/**
* Override @atlaskit/tab styling when in a modal because the
* tab text color clash with the modal backgrounds.
*/
div[role="tablist"] > div:not([data-selected]):not(:hover),
label > div > span {
color: #B8C7E0 !important;
}
} }
/** /**

View File

@ -9,6 +9,11 @@
max-height: 456px; max-height: 456px;
overflow: auto; overflow: auto;
width: 300px; width: 300px;
&-ul {
margin:0;
padding:0;
list-style-type: none;
}
} }
&-header { &-header {
@ -64,7 +69,13 @@
&-speaker { &-speaker {
position: relative; position: relative;
&:hover { &-ul {
margin:0;
padding:0;
list-style-type: none;
}
&:hover, &:focus-within, &:focus {
.audio-preview-entry { .audio-preview-entry {
background: #36383C; background: #36383C;
margin-left: 0; margin-left: 0;
@ -81,7 +92,7 @@
} }
.audio-preview-entry-text { .audio-preview-entry-text {
max-width: 196px; max-width: 178px;
} }
} }
@ -90,7 +101,7 @@
} }
.audio-preview-entry-text { .audio-preview-entry-text {
max-width: 256px; max-width: 238px;
} }
} }
@ -150,8 +161,9 @@
color: #1C2025; color: #1C2025;
cursor: pointer; cursor: pointer;
font-weight: 600; font-weight: 600;
font-size: 0.8rem;
line-height: 24px; line-height: 24px;
padding: 2px 16px; padding: 2px 8px;
position: absolute; position: absolute;
right: 16px; right: 16px;
top: 5px; top: 5px;
@ -162,4 +174,10 @@
right: 16px; right: 16px;
top: 14px; top: 14px;
} }
// Override @atlaskit/InlineDialog container which is made with styled components
& > div:nth-child(2) {
outline: none;
padding: 0;
}
} }

View File

@ -219,8 +219,9 @@ abbr {
} }
a { a {
color: #3572b0; color: #44A5FF;
text-decoration: none; text-decoration: none;
font-weight: bold;
} }
a:focus, a:focus,
a:hover, a:hover,

View File

@ -1,7 +1,7 @@
.avatar { .avatar {
background-color: #AAA; background-color: #AAA;
border-radius: 50%; border-radius: 50%;
color: rgba(255, 255, 255, 0.6); color: rgba(255, 255, 255, 1);
font-weight: 100; font-weight: 100;
object-fit: cover; object-fit: cover;
@ -25,10 +25,6 @@
width: 100%; width: 100%;
} }
.defaultAvatar {
opacity: 0.6
}
.avatar-badge { .avatar-badge {
position: relative; position: relative;

View File

@ -99,18 +99,19 @@
div { div {
svg { svg {
cursor: pointer; cursor: pointer;
fill: white fill: white;
} }
} }
} }
.chat-header { .chat-header {
height: 70px; height: 70px;
position: relative; position: relative;
width: 100%; width: 100%;
z-index: 1; z-index: 1;
display: flex; display: flex;
justify-content: flex-end; justify-content: space-between;
padding: 16px; padding: 16px;
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
@ -132,6 +133,7 @@
.send-button { .send-button {
background: #1B67EC; background: #1B67EC;
cursor: pointer; cursor: pointer;
margin-left: 0.3rem;
@media (hover: hover) and (pointer: fine) { @media (hover: hover) and (pointer: fine) {
&:hover { &:hover {
@ -188,8 +190,9 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 40px; height: 38px;
width: 40px; width: 38px;
margin: 2px;
border-radius: 3px; border-radius: 3px;
} }
@ -226,6 +229,11 @@
border: 0px none; border: 0px none;
box-shadow: none; box-shadow: none;
} }
#usermsg:focus,
#usermsg:active {
border-bottom: 1px solid white;
padding-bottom: 8px;
}
#nickname { #nickname {
text-align: center; text-align: center;
@ -234,6 +242,16 @@
margin: auto 0; margin: auto 0;
padding: 0 16px; padding: 0 16px;
#nickname-title {
margin-bottom: 5px;
display: block;
}
label[for="nickinput"] {
> div > span {
color: #B8C7E0;
}
}
input { input {
height: 40px; height: 40px;
} }
@ -254,7 +272,7 @@
cursor: pointer; cursor: pointer;
&.disabled { &.disabled {
color: #757575; color: #AFB6BC;
background: #11336E; background: #11336E;
pointer-events: none; pointer-events: none;
} }
@ -301,6 +319,19 @@
} }
} }
.sr-only {
border: 0 !important;
clip: rect(1px, 1px, 1px, 1px) !important;
clip-path: inset(50%) !important;
height: 1px !important;
margin: -1px !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
width: 1px !important;
white-space: nowrap !important;
}
.chatmessage { .chatmessage {
background-color: $chatRemoteMessageBackgroundColor; background-color: $chatRemoteMessageBackgroundColor;
border-radius: 0px 6px 6px 6px; border-radius: 0px 6px 6px 6px;
@ -350,10 +381,6 @@
color: #757575; color: #757575;
} }
.smiley {
font-size: 14pt;
}
#smileys { #smileys {
font-size: 20pt; font-size: 20pt;
margin: auto; margin: auto;
@ -382,7 +409,7 @@
box-sizing: border-box; box-sizing: border-box;
background-color: rgba(0, 0, 0, .6) !important; background-color: rgba(0, 0, 0, .6) !important;
height: auto; height: auto;
max-height: 0; display: none;
overflow: hidden; overflow: hidden;
position: absolute; position: absolute;
width: calc(#{$sidebarWidth} - 32px); width: calc(#{$sidebarWidth} - 32px);
@ -398,6 +425,7 @@
transition: max-height 0.3s; transition: max-height 0.3s;
&.show-smileys { &.show-smileys {
display: flex;
max-height: 500%; max-height: 500%;
} }
@ -413,7 +441,7 @@
.smileyContainer { .smileyContainer {
width: 40px; width: 40px;
height: 36px; height: 40px;
display: inline-block; display: inline-block;
text-align: center; text-align: center;
} }
@ -509,7 +537,7 @@
&-header { &-header {
display: flex; display: flex;
justify-content: flex-end; justify-content: space-between;
align-items: center; align-items: center;
margin: 16px; margin: 16px;
width: calc(100% - 32px); width: calc(100% - 32px);

View File

@ -8,7 +8,10 @@
.read-more { .read-more {
cursor: pointer; cursor: pointer;
opacity: .7; opacity: .9;
color: #fff;
font-size: 0.8rem;
font-weight: bold;
} }
} }

View File

@ -125,7 +125,8 @@
cursor: pointer; cursor: pointer;
} }
&.with-click-handler:hover { &.with-click-handler:hover,
&.with-click-handler:focus {
background-color: #c7ddff; background-color: #c7ddff;
} }
@ -158,7 +159,7 @@
} }
} }
.item:hover { .item:hover, .item:focus, .item:focus-within {
.delete-meeting { .delete-meeting {
display: block; display: block;
} }

View File

@ -3,6 +3,15 @@
&-input-area { &-input-area {
margin: 0 auto; margin: 0 auto;
text-align: center; text-align: center;
&-label {
display: block;
margin-bottom: 5px;
color: #ffffff;
font-weight: 300;
font-size: 15px;
line-height: 24px;
}
} }
&-title { &-title {
@ -74,10 +83,10 @@
z-index: 1; z-index: 1;
&--warning { &--warning {
background: rgba(241, 173, 51, 0.7) background: rgba(241, 173, 51, 1);
} }
&--ok { &--ok {
background: rgba(49, 183, 106, 0.7); background: rgba(49, 183, 106, 1);
} }
} }
@ -92,6 +101,8 @@
&-error-desc { &-error-desc {
margin-right: 4px; margin-right: 4px;
color: #fff;
font-weight: bold;
} }
.settings-button-container { .settings-button-container {

View File

@ -113,7 +113,7 @@
cursor: pointer; cursor: pointer;
color: #fff; color: #fff;
display: flex; display: flex;
flex-direction: row; flex-direction: column;
font-size: 15px; font-size: 15px;
font-weight: 300; font-weight: 300;
justify-content: center; justify-content: center;
@ -139,6 +139,9 @@
margin-left: 10px; margin-left: 10px;
} }
} }
.copy-button{
width: 298px;
}
.copy-meeting-text { .copy-meeting-text {
width: 266px; width: 266px;
@ -177,7 +180,7 @@
} }
&.focused { &.focused {
box-shadow: 0px 0px 4px 3px #0376DA; box-shadow: 0px 0px 1px 1.5px black, 0px 0px 1.3px 4px white;
} }
} }
} }
@ -249,6 +252,10 @@
fill: transparent; fill: transparent;
} }
label {
cursor: pointer;
}
&:hover { &:hover {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);

View File

@ -105,6 +105,7 @@
.helper-link { .helper-link {
cursor: pointer; cursor: pointer;
font-weight: bold;
display: inline-block; display: inline-block;
flex-shrink: 0; flex-shrink: 0;
margin-left: auto; margin-left: auto;

View File

@ -54,6 +54,7 @@
margin-bottom: 16px; margin-bottom: 16px;
position: relative; position: relative;
z-index: $toolbarZ; z-index: $toolbarZ;
pointer-events: none;
.button-group-center, .button-group-center,
.button-group-left, .button-group-left,
@ -103,6 +104,7 @@
flex-direction: column; flex-direction: column;
margin: 0 auto; margin: 0 auto;
max-width: 100%; max-width: 100%;
pointer-events: all;
} }
.toolbox-content-items { .toolbox-content-items {
@ -112,6 +114,7 @@
margin: 0 auto; margin: 0 auto;
padding: 6px; padding: 6px;
text-align: center; text-align: center;
pointer-events: all;
>div { >div {
margin-left: 8px; margin-left: 8px;

View File

@ -79,4 +79,8 @@
white-space: nowrap; white-space: nowrap;
} }
} }
// Override @atlaskit/InlineDialog container which is made with styled components
& > div:nth-child(2) {
padding: 0;
}
} }

View File

@ -428,7 +428,7 @@
right: 0; right: 0;
z-index: $zindex2; z-index: $zindex2;
width: 18px; width: 18px;
height: 13px; height: 18px;
color: #FFF; color: #FFF;
font-size: 10pt; font-size: 10pt;
margin-right: $remoteVideoMenuIconMargin; margin-right: $remoteVideoMenuIconMargin;

View File

@ -3,7 +3,7 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 8px 8px 8px 16px; padding: 8px 8px 8px 16px;
margin-top: 8px; margin-top: 5px;
width: calc(100% - 24px); width: calc(100% - 24px);
height: 24px; height: 24px;

View File

@ -9,10 +9,10 @@ input[type=range]{
} }
/** /**
* Disable the default focus styles for webkit range inputs (sliders). * Show focus for keyboard accessibility.
*/ */
input[type=range]:focus { input[type=range]:focus {
outline: none; outline: 1px solid white !important;
} }
/** /**

View File

@ -16,7 +16,6 @@
padding: 0; padding: 0;
margin: 0; margin: 0;
border: none; border: none;
outline: none;
-webkit-appearance: none; -webkit-appearance: none;

View File

@ -7,7 +7,7 @@
* see. * see.
*/ */
.active-speaker { .active-speaker {
box-shadow: 0 0 5px 3px $videoThumbnailSelected box-shadow: 0px 0px 1px 1.5px black, 0px 0px 1.3px 4px $videoThumbnailSelected;
} }
#filmstripRemoteVideos { #filmstripRemoteVideos {

View File

@ -81,7 +81,7 @@
.local-video-menu-trigger, .local-video-menu-trigger,
.remote-video-menu-trigger { .remote-video-menu-trigger {
margin-bottom: 7px; margin-bottom: 3px;
margin-left: $remoteVideoMenuIconMargin; margin-left: $remoteVideoMenuIconMargin;
} }
} }

View File

@ -103,7 +103,7 @@
font-size: 14px; font-size: 14px;
a { a {
color: #2684FF; color: #6FB1EA;
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
} }
@ -119,7 +119,7 @@
height: 8px; height: 8px;
.audio-input-preview-level { .audio-input-preview-level {
background: #4C9AFF; background: #75B1FF;
border-radius: 5px; border-radius: 5px;
height: 100%; height: 100%;
-webkit-transition: width .1s ease-in-out; -webkit-transition: width .1s ease-in-out;

View File

@ -94,5 +94,9 @@
}; };
} }
.star-btn:focus,
.star-btn:active {
outline: 1px solid #B8C7E0;
}
} }
} }

View File

@ -30,20 +30,26 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
margin-right: 5px;
} }
.info-password-none, .info-password-none,
.info-password-remote { .info-password-remote {
opacity: 0.5; color: #fff;
} }
.info-password-input { .info-password-input {
width: 100%; width: 100%;
background-color: transparent; background-color: #0E1624;
border: none; border-radius: 3px;
border: 2px solid #202B3D;
color: inherit; color: inherit;
padding-left: 0; padding-left: 0;
} }
.info-password-input:focus ,
.info-password-input:active {
border: 2px solid #B8C7E0;
}
.info-password-local { .info-password-local {
user-select: text; user-select: text;

View File

@ -130,6 +130,7 @@
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
cursor: pointer; cursor: pointer;
height: 24px;
} }
} }
@ -141,7 +142,7 @@
& > a { & > a {
display: inline-block; display: inline-block;
height: 24px; height: 24px;
width: 48px; min-width: 48px;
border-radius: 3px; border-radius: 3px;
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;

View File

@ -16,12 +16,19 @@
} }
.mock-atlaskit-label { .mock-atlaskit-label {
color: #56637A; color: #b8c7e0;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
line-height: 1.33; line-height: 1.33;
padding: 20px 0px 4px 0px; padding: 20px 0px 4px 0px;
} }
input[type="checkbox"]:checked + svg {
--checkbox-background-color: #6492e7;
--checkbox-border-color: #6492e7;
}
input[type="checkbox"] + svg + span {
color: #b8c7e0;
}
input[type="checkbox"] + svg + span { input[type="checkbox"] + svg + span {
color: #9FB0CC; color: #9FB0CC;
@ -65,4 +72,14 @@
.sign-out-cta { .sign-out-cta {
margin-bottom: 20px; margin-bottom: 20px;
} }
@media only screen and (max-width: $smallScreen) {
.device-selection {
display: flex;
flex-direction: column;
}
.more-tab {
flex-direction: column;
}
}
} }

View File

@ -86,6 +86,7 @@
.video-quality-dialog-label-container.active { .video-quality-dialog-label-container.active {
color: $videoQualityActive; color: $videoQualityActive;
font-weight: bold;
&::before { &::before {
background: $videoQualityActive; background: $videoQualityActive;

View File

@ -7,9 +7,7 @@
grid-template-columns: auto auto auto auto auto; grid-template-columns: auto auto auto auto auto;
column-gap: 9px; column-gap: 9px;
cursor: pointer; cursor: pointer;
.desktop-share:hover, .thumbnail:hover, .blur:hover, .slight-blur:hover, .virtual-background-none:hover{ .desktop-share:hover, .thumbnail:hover, .blur:hover, .slight-blur:hover, .virtual-background-none:hover{
height: 56px;
width: 103px;
opacity: .5; opacity: .5;
border: 2px solid #99bbf3; border: 2px solid #99bbf3;
@media (min-width: 432px) and (min-width: 432px) and (max-width: 632px) { @media (min-width: 432px) and (min-width: 432px) and (max-width: 632px) {
@ -21,152 +19,75 @@
width: 56px; width: 56px;
} }
} }
.thumbnail { .background-option {
margin-top: 8px; margin-top: 8px;
border-radius: 6px; border-radius: 6px;
object-fit: cover;
height: 60px; height: 60px;
width: 107px; width: 107px;
text-align: center;
justify-content: center;
font-weight: bold;
box-sizing: border-box;
display: flex;
align-items: center;
}
.thumbnail {
object-fit: cover;
} }
.thumbnail:hover ~ .delete-image-icon { .thumbnail:hover ~ .delete-image-icon {
display: block; display: block;
} }
.thumbnail-selected { .thumbnail-selected {
margin-top: 8px;
border-radius: 6px;
object-fit: cover; object-fit: cover;
height: 56px;
width: 103px;
border: 2px solid #246FE5; border: 2px solid #246FE5;
} }
.blur{ .blur{
box-shadow: inset 0 0 12px #000000; box-shadow: inset 0 0 12px #000000;
margin-top: 8px;
background: #7E8287; background: #7E8287;
font-weight: bold; padding: 0 10px;
height: 60px;
width: 107px;
border-radius: 6px;
text-align: center;
vertical-align: middle;
line-height: 60px;
} }
.blur-selected { .blur-selected {
box-shadow: inset 0 0 12px #000000; box-shadow: inset 0 0 12px #000000;
margin-top: 8px;
background: #7E8287; background: #7E8287;
font-weight: bold;
height: 56px;
width: 103px;
border-radius: 6px;
border: 2px solid #246FE5; border: 2px solid #246FE5;
text-align: center; padding: 0 10px;
vertical-align: middle;
line-height: 60px;
} }
.slight-blur{ .slight-blur{
box-shadow: inset 0 0 12px #000000; box-shadow: inset 0 0 12px #000000;
margin-top: 8px;
background: #A4A4A4; background: #A4A4A4;
font-weight: bold; padding: 0 10px;
height: 60px;
width: 107px;
border-radius: 6px;
text-align: center;
vertical-align: middle;
line-height: 60px;
} }
.slight-blur-selected{ .slight-blur-selected{
box-shadow: inset 0 0 12px #000000; box-shadow: inset 0 0 12px #000000;
margin-top: 8px;
background: #A4A4A4; background: #A4A4A4;
font-weight: bold;
height: 56px;
width: 103px;
border-radius: 6px;
border: 2px solid #246FE5; border: 2px solid #246FE5;
text-align: center; padding: 0 10px;
vertical-align: middle;
line-height: 60px;
} }
.virtual-background-none { .virtual-background-none {
margin-top: 8px;
background: #525252; background: #525252;
font-weight: bold; padding: 0 10px;
height: 60px;
width: 107px;
border-radius: 6px;
text-align: center;
vertical-align: middle;
line-height: 60px;
} }
.none-selected { .none-selected {
margin-top: 8px;
background: #525252; background: #525252;
font-weight: bold;
height: 56px;
width: 103px;
border-radius: 6px;
border: 2px solid #246FE5; border: 2px solid #246FE5;
text-align: center; padding: 0 10px;
vertical-align: middle;
line-height: 60px;
} }
.desktop-share{ .desktop-share{
margin-top: 8px;
background: #525252; background: #525252;
font-weight: bold;
height: 60px;
width: 107px;
border-radius: 6px;
text-align: center;
vertical-align: middle;
line-height: 60px;
} }
.desktop-share-selected{ .desktop-share-selected{
margin-top: 8px;
background: #525252; background: #525252;
font-weight: bold;
height: 56px;
width: 103px;
border-radius: 6px;
border: 2px solid #246FE5; border: 2px solid #246FE5;
text-align: center; padding: 0 10px;
vertical-align: middle;
line-height: 60px;
}
.share-desktop-icon{
margin-top: 15%;
} }
@media (min-width: 432px) and (max-width: 632px) { @media (min-width: 432px) and (max-width: 632px) {
font-size: 1.5vw; font-size: 1.5vw;
.share-desktop-icon{
margin-top: 25%;
}
.desktop-share, .virtual-background-none, .thumbnail, .blur, .slight-blur{
height: 60px;
width: 60px;
}
.desktop-share-selected, .thumbnail-selected, .none-selected, .blur-selected, .slight-blur-selected{
height: 56px;
width: 56px;
}
} }
@media (max-width: 432px){ @media (max-width: 432px){
.share-desktop-icon{
margin-top: 25%;
}
font-size: 1.5vw; font-size: 1.5vw;
.desktop-share, .virtual-background-none, .thumbnail, .blur, .slight-blur{
height: 60px;
width: 60px;
}
.desktop-share-selected, .thumbnail-selected, .none-selected, .blur-selected, .slight-blur-selected{
height: 56px;
width: 56px;
}
} }
} }
@ -205,12 +126,18 @@
left: 51px left: 51px
} }
} }
.delete-image-icon:hover { .delete-image-icon:hover {
display: block; display: block;
} }
.thumbnail-container { .thumbnail-container {
position: relative; position: relative;
&:focus-within {
.thumbnail ~ .delete-image-icon{
display: block;
}
}
} }
.add-background{ .add-background{

View File

@ -107,4 +107,4 @@ $selectActiveItemBg: darken($controlBackground, 20%);
/** /**
* TODO: Replace by themed component. * TODO: Replace by themed component.
*/ */
$videoQualityActive: #4C9AFF; $videoQualityActive: #57A0ff;

View File

@ -186,10 +186,10 @@
<!--#include virtual="static/settingsToolbarAdditionalContent.html" --> <!--#include virtual="static/settingsToolbarAdditionalContent.html" -->
</head> </head>
<body> <body>
<noscript> <noscript aria-hidden="true">
<div>JavaScript is disabled. </br>For this site to work you have to enable JavaScript.</div> <div>JavaScript is disabled. </br>For this site to work you have to enable JavaScript.</div>
</noscript> </noscript>
<!--#include virtual="body.html" --> <!--#include virtual="body.html" -->
<div id="react"></div> <div id="react" role="main"></div>
</body> </body>
</html> </html>

View File

@ -70,12 +70,17 @@
}, },
"privateNotice": "Private Nachricht an {{recipient}}", "privateNotice": "Private Nachricht an {{recipient}}",
"title": "Chatten", "title": "Chatten",
"you": "Sie" "you": "Sie",
"message": "Nachricht",
"messageAccessibleTitle": "{{user}} sagt:",
"messageAccessibleTitleMe": "Ich sage:",
"smileysPanel": "Emoji-Auswahl"
}, },
"chromeExtensionBanner": { "chromeExtensionBanner": {
"installExtensionText": "Installieren Sie die Erweiterung für die Integration von Google Calendar und Office 365", "installExtensionText": "Installieren Sie die Erweiterung für die Integration von Google Calendar und Office 365",
"buttonText": "Chrome-Erweiterung installieren", "buttonText": "Chrome-Erweiterung installieren",
"dontShowAgain": "Hinweis nicht mehr anzeigen" "dontShowAgain": "Hinweis nicht mehr anzeigen",
"close": "Schließen"
}, },
"connectingOverlay": { "connectingOverlay": {
"joiningRoom": "Eine Verbindung zu Ihrem Meeting wird hergestellt…" "joiningRoom": "Eine Verbindung zu Ihrem Meeting wird hergestellt…"
@ -204,6 +209,8 @@
"e2eeLabel": "Ende-zu-Ende-Verschlüsselung aktivieren", "e2eeLabel": "Ende-zu-Ende-Verschlüsselung aktivieren",
"e2eeWarning": "WARNUNG: Nicht alle Personen dieser Konferenz scheinen Ende-zu-Ende-Verschlüsselung zu unterstützen. Wenn Sie diese aktivieren, können die entsprechenden Personen nichts mehr sehen oder hören.", "e2eeWarning": "WARNUNG: Nicht alle Personen dieser Konferenz scheinen Ende-zu-Ende-Verschlüsselung zu unterstützen. Wenn Sie diese aktivieren, können die entsprechenden Personen nichts mehr sehen oder hören.",
"enterDisplayName": "Bitte geben Sie hier Ihren Namen ein", "enterDisplayName": "Bitte geben Sie hier Ihren Namen ein",
"enterDisplayNameToJoin" : "Benutzername für Konferenz eingeben" ,
"embedMeeting": "Besprechung einbetten",
"error": "Fehler", "error": "Fehler",
"gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.", "gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
"grantModeratorDialog": "Möchten Sie wirklich Moderationsrechte an diese Person vergeben?", "grantModeratorDialog": "Möchten Sie wirklich Moderationsrechte an diese Person vergeben?",
@ -295,7 +302,7 @@
"Share": "Teilen", "Share": "Teilen",
"shareVideoLinkError": "Bitte einen gültigen YouTube-Link angeben.", "shareVideoLinkError": "Bitte einen gültigen YouTube-Link angeben.",
"shareVideoTitle": "Video teilen", "shareVideoTitle": "Video teilen",
"shareYourScreen": "Bildschirm freigeben", "shareYourScreen": "Bildschirmfreigabe ein-/ausschalten",
"shareYourScreenDisabled": "Bildschirmfreigabe deaktiviert.", "shareYourScreenDisabled": "Bildschirmfreigabe deaktiviert.",
"startLiveStreaming": "Livestream starten", "startLiveStreaming": "Livestream starten",
"startRecording": "Aufnahme starten", "startRecording": "Aufnahme starten",
@ -320,7 +327,9 @@
"WaitForHostMsgWOk": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Falls Sie die Konferenz leiten, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.", "WaitForHostMsgWOk": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Falls Sie die Konferenz leiten, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
"WaitingForHostTitle": "Warten auf den Beginn der Konferenz …", "WaitingForHostTitle": "Warten auf den Beginn der Konferenz …",
"Yes": "Ja", "Yes": "Ja",
"yourEntireScreen": "Ganzer Bildschirm" "yourEntireScreen": "Ganzer Bildschirm",
"remoteUserControls": "Remote Benutzersteuerung von {{username}}",
"localUserControls": "Lokale Benutzersteuerung"
}, },
"dialOut": { "dialOut": {
"statusMessage": "ist jetzt {{status}}" "statusMessage": "ist jetzt {{status}}"
@ -340,8 +349,20 @@
"slightBlur": "Hintergrund leicht unscharf", "slightBlur": "Hintergrund leicht unscharf",
"removeBackground": "Hintergrund entfernen", "removeBackground": "Hintergrund entfernen",
"uploadImage": "Bild hochladen", "uploadImage": "Bild hochladen",
"addBackground": "Hintergrund hinzufügen",
"pleaseWait": "Bitte warten...", "pleaseWait": "Bitte warten...",
"none": "keiner" "none": "keiner",
"uploadedImage": "Hochgeladenes Bild {{index}}",
"deleteImage": "Bild löschen",
"image1" : "Strand",
"image2" : "Weiße neutrale Wand",
"image3" : "Weißer leerer Raum",
"image4" : "Schwarze Stehlampe",
"image5" : "Berg",
"image6" : "Wald",
"image7" : "Sonnenaufgang",
"desktopShareError": "Desktop konnte nicht freigegeben werden",
"desktopShare":"Desktopfreigabe"
}, },
"feedback": { "feedback": {
"average": "Durchschnittlich", "average": "Durchschnittlich",
@ -350,7 +371,8 @@
"good": "Gut", "good": "Gut",
"rateExperience": "Bitte bewerten Sie diese Konferenz", "rateExperience": "Bitte bewerten Sie diese Konferenz",
"veryBad": "Sehr schlecht", "veryBad": "Sehr schlecht",
"veryGood": "Sehr gut" "veryGood": "Sehr gut",
"star": "Sterne"
}, },
"incomingCall": { "incomingCall": {
"answer": "Antworten", "answer": "Antworten",
@ -367,6 +389,7 @@
"country": "Land", "country": "Land",
"dialANumber": "Um am Meeting teilzunehmen, müssen Sie eine dieser Nummern wählen und dann die PIN eingeben.", "dialANumber": "Um am Meeting teilzunehmen, müssen Sie eine dieser Nummern wählen und dann die PIN eingeben.",
"dialInConferenceID": "PIN:", "dialInConferenceID": "PIN:",
"copyNumber":"Nummer kopieren",
"dialInNotSupported": "Entschuldigung, leider wird das Einwählen derzeit nicht unterstützt.", "dialInNotSupported": "Entschuldigung, leider wird das Einwählen derzeit nicht unterstützt.",
"dialInNumber": "Einwählen:", "dialInNumber": "Einwählen:",
"dialInSummaryError": "Fehler beim Abrufen der Einwahlinformationen. Versuchen Sie es später erneut.", "dialInSummaryError": "Fehler beim Abrufen der Einwahlinformationen. Versuchen Sie es später erneut.",
@ -404,6 +427,7 @@
"support": "Support", "support": "Support",
"supportMsg": "Wenn der Fehler erneut auftritt, bitte kontaktieren Sie" "supportMsg": "Wenn der Fehler erneut auftritt, bitte kontaktieren Sie"
}, },
"jitsiHome": "{{logo}} Logo, verlinkt zur Homepage",
"keyboardShortcuts": { "keyboardShortcuts": {
"focusLocal": "Lokales Video fokussieren", "focusLocal": "Lokales Video fokussieren",
"focusRemote": "Auf das Video einer anderen Person fokussieren", "focusRemote": "Auf das Video einer anderen Person fokussieren",
@ -524,7 +548,19 @@
"OldElectronAPPTitle": "Sicherheitslücke!", "OldElectronAPPTitle": "Sicherheitslücke!",
"oldElectronClientDescription1": "Sie scheinen eine alte Version des Jitsi-Meet-Clients zu nutzen. Diese hat bekannte Schwachstellen. Bitte aktualisieren Sie auf unsere ", "oldElectronClientDescription1": "Sie scheinen eine alte Version des Jitsi-Meet-Clients zu nutzen. Diese hat bekannte Schwachstellen. Bitte aktualisieren Sie auf unsere ",
"oldElectronClientDescription2": "aktuelle Version", "oldElectronClientDescription2": "aktuelle Version",
"oldElectronClientDescription3": "!" "oldElectronClientDescription3": "!",
"groupTitle": "Benachrichtigungen"
},
"participantsPane": {
"close": "Schließen",
"headings": {
"lobby": "Lobby ({{count}})",
"participantsList": "Teilnehmer ({{count}})"
},
"actions": {
"muteAll": "Alle stummschalten",
"stopVideo": "Video stoppen"
}
}, },
"participantsPane": { "participantsPane": {
"headings": { "headings": {
@ -589,6 +625,7 @@
"linkCopied": "Link in die Zwischenablage kopiert", "linkCopied": "Link in die Zwischenablage kopiert",
"lookGood": "Ihr Mikrofon scheint zu funktionieren.", "lookGood": "Ihr Mikrofon scheint zu funktionieren.",
"or": "oder", "or": "oder",
"keyboardShortcuts" : "Tastaturkurzbefehle aktivieren",
"premeeting": "Vorschau", "premeeting": "Vorschau",
"showScreen": "Konferenzvorschau aktivieren", "showScreen": "Konferenzvorschau aktivieren",
"startWithPhone": "Mit Telefonaudio starten", "startWithPhone": "Mit Telefonaudio starten",
@ -612,6 +649,7 @@
"ringing": "Es klingelt …" "ringing": "Es klingelt …"
}, },
"profile": { "profile": {
"avatar": "Benutzerbild",
"setDisplayNameLabel": "Anzeigename festlegen", "setDisplayNameLabel": "Anzeigename festlegen",
"setEmailInput": "E-Mail eingeben", "setEmailInput": "E-Mail eingeben",
"setEmailLabel": "E-Mail-Adresse für Gravatar", "setEmailLabel": "E-Mail-Adresse für Gravatar",
@ -728,29 +766,29 @@
"title": "Die Konferenz wurde unterbrochen, weil der Standby-Modus aktiviert wurde." "title": "Die Konferenz wurde unterbrochen, weil der Standby-Modus aktiviert wurde."
}, },
"toolbar": { "toolbar": {
"accessibilityLabel": { "accessibilityLabel": {
"audioOnly": "„Nur Audio“ ein-/ausschalten", "audioOnly": "„Nur Audio“ ein-/ausschalten",
"audioRoute": "Audiogerät auswählen", "audioRoute": "Audiogerät auswählen",
"callQuality": "Qualitätseinstellungen", "callQuality": "Qualitätseinstellungen",
"cc": "Untertitel ein-/ausschalten", "cc": "Untertitel ein-/ausschalten",
"chat": "Chatfenster ein-/ausblenden", "chat": "Chatfenster öffnen / schließen",
"document": "Geteiltes Dokument schließen", "document": "Geteiltes Dokument schließen",
"download": "Unsere Apps herunterladen", "download": "Unsere Apps herunterladen",
"embedMeeting": "Konferenz einbetten", "embedMeeting": "Konferenz einbetten",
"feedback": "Feedback hinterlassen", "feedback": "Feedback hinterlassen",
"fullScreen": "Vollbildmodus ein-/ausschalten", "fullScreen": "Vollbildmodus ein-/ausschalten",
"grantModerator": "Moderationsrechte vergeben", "grantModerator": "Moderationsrechte vergeben",
"hangup": "Anruf beenden", "hangup": "Konferenz verlassen",
"help": "Hilfe", "help": "Hilfe",
"invite": "Person einladen", "invite": "Person einladen",
"kick": "Person entfernen", "kick": "Person entfernen",
"lobbyButton": "Lobbymodus ein-/ausschalten", "lobbyButton": "Lobbymodus ein-/ausschalten",
"localRecording": "Lokale Aufzeichnungssteuerelemente ein-/ausschalten", "localRecording": "Lokale Aufzeichnungssteuerelemente ein-/ausschalten",
"lockRoom": "Konferenzpasswort ein-/ausschalten", "lockRoom": "Konferenzpasswort ein-/ausschalten",
"moreActions": "Menü „Weitere Aktionen“ ein-/ausschalten", "moreActions": "Menü „Weitere Einstellungen“ ein-/ausschalten",
"moreActionsMenu": "Menü „Weitere Aktionen“", "moreActionsMenu": "Menü „Weitere Einstellungen“",
"moreOptions": "Menü „Weitere Optionen“", "moreOptions": "Menü „Weitere Optionen“",
"mute": "„Audio stummschalten“ ein-/ausschalten", "mute": "Mikrofon aktivieren / deaktivieren",
"muteEveryone": "Alle stummschalten", "muteEveryone": "Alle stummschalten",
"muteEveryoneElse": "Alle anderen stummschalten", "muteEveryoneElse": "Alle anderen stummschalten",
"muteEveryonesVideo": "Alle Kameras ausschalten", "muteEveryonesVideo": "Alle Kameras ausschalten",
@ -759,7 +797,7 @@
"pip": "Bild-in-Bild-Modus ein-/ausschalten", "pip": "Bild-in-Bild-Modus ein-/ausschalten",
"privateMessage": "Private Nachricht senden", "privateMessage": "Private Nachricht senden",
"profile": "Profil bearbeiten", "profile": "Profil bearbeiten",
"raiseHand": "„Melden“ ein-/ausschalten", "raiseHand": "Hand erheben / senken",
"recording": "Aufzeichnung ein-/ausschalten", "recording": "Aufzeichnung ein-/ausschalten",
"remoteMute": "Personen stummschalten", "remoteMute": "Personen stummschalten",
"remoteVideoMute": "Kamera von dieser Person ausschalten", "remoteVideoMute": "Kamera von dieser Person ausschalten",
@ -776,7 +814,9 @@
"toggleCamera": "Kamera wechseln", "toggleCamera": "Kamera wechseln",
"toggleFilmstrip": "Miniaturansichten ein-/ausschalten", "toggleFilmstrip": "Miniaturansichten ein-/ausschalten",
"videomute": "„Video stummschalten“ ein-/ausschalten", "videomute": "„Video stummschalten“ ein-/ausschalten",
"selectBackground": "Hintergrund auswählen" "selectBackground": "Hintergrund auswählen",
"expand": "Ausklappen",
"collapse": "Einklappen"
}, },
"addPeople": "Personen zur Konferenz hinzufügen", "addPeople": "Personen zur Konferenz hinzufügen",
"audioSettings": "Ton-Einstellungen", "audioSettings": "Ton-Einstellungen",
@ -797,7 +837,7 @@
"exitFullScreen": "Vollbildmodus verlassen", "exitFullScreen": "Vollbildmodus verlassen",
"exitTileView": "Kachelansicht ausschalten", "exitTileView": "Kachelansicht ausschalten",
"feedback": "Feedback hinterlassen", "feedback": "Feedback hinterlassen",
"hangup": "Verlassen", "hangup": "Konferenz verlassen",
"help": "Hilfe", "help": "Hilfe",
"invite": "Personen einladen", "invite": "Personen einladen",
"lobbyButtonDisable": "Lobbymodus deaktivieren", "lobbyButtonDisable": "Lobbymodus deaktivieren",
@ -807,7 +847,7 @@
"lowerYourHand": "Hand senken", "lowerYourHand": "Hand senken",
"moreActions": "Weitere Einstellungen", "moreActions": "Weitere Einstellungen",
"moreOptions": "Weitere Optionen", "moreOptions": "Weitere Optionen",
"mute": "Stummschaltung aktivieren / deaktivieren", "mute": "Mikrofon aktivieren / deaktivieren",
"muteEveryone": "Alle stummschalten", "muteEveryone": "Alle stummschalten",
"muteEveryonesVideo": "Alle Kameras ausschalten", "muteEveryonesVideo": "Alle Kameras ausschalten",
"noAudioSignalTitle": "Es kommt kein Input von Ihrem Mikrofon!", "noAudioSignalTitle": "Es kommt kein Input von Ihrem Mikrofon!",
@ -822,7 +862,7 @@
"pip": "Bild-in-Bild-Modus einschalten", "pip": "Bild-in-Bild-Modus einschalten",
"privateMessage": "Private Nachricht senden", "privateMessage": "Private Nachricht senden",
"profile": "Profil bearbeiten", "profile": "Profil bearbeiten",
"raiseHand": "Hand erheben", "raiseHand": "Hand erheben / senken",
"raiseYourHand": "Melden", "raiseYourHand": "Melden",
"security": "Sicherheitsoptionen", "security": "Sicherheitsoptionen",
"Settings": "Einstellungen", "Settings": "Einstellungen",
@ -867,6 +907,7 @@
"react-nativeGrantPermissions": "Wählen Sie <b><i>Erlauben</i></b>, wenn der Browser um Berechtigungen bittet.", "react-nativeGrantPermissions": "Wählen Sie <b><i>Erlauben</i></b>, wenn der Browser um Berechtigungen bittet.",
"safariGrantPermissions": "Wählen Sie <b><i>OK</i></b>, wenn der Browser um Berechtigungen bittet." "safariGrantPermissions": "Wählen Sie <b><i>OK</i></b>, wenn der Browser um Berechtigungen bittet."
}, },
"volumeSlider": "Lautstärkeregler",
"videoSIPGW": { "videoSIPGW": {
"busy": "Es stehen keine freien Ressourcen zur Verfügung. Bitte versuchen Sie es später noch einmal.", "busy": "Es stehen keine freien Ressourcen zur Verfügung. Bitte versuchen Sie es später noch einmal.",
"busyTitle": "Keine freien Ressourcen", "busyTitle": "Keine freien Ressourcen",
@ -911,6 +952,7 @@
"videomute": "Person hat die Kamera angehalten" "videomute": "Person hat die Kamera angehalten"
}, },
"welcomepage": { "welcomepage": {
"addMeetingName": "Besprechungsnamen hinzufügen",
"accessibilityLabel": { "accessibilityLabel": {
"join": "Zum Teilnehmen tippen", "join": "Zum Teilnehmen tippen",
"roomname": "Konferenzname eingeben" "roomname": "Konferenzname eingeben"
@ -933,6 +975,9 @@
"join": "ERSTELLEN / BEITRETEN", "join": "ERSTELLEN / BEITRETEN",
"jitsiOnMobile": "Jitsi unterwegs einfach unsere Apps herunterladen und Meetings von überall starten", "jitsiOnMobile": "Jitsi unterwegs einfach unsere Apps herunterladen und Meetings von überall starten",
"moderatedMessage": "Oder <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservieren Sie sich eine Konferenz-URL</a>, die nur Sie moderieren.", "moderatedMessage": "Oder <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservieren Sie sich eine Konferenz-URL</a>, die nur Sie moderieren.",
"mobileDownLoadLinkIos": "iOS App Download",
"mobileDownLoadLinkAndroid": "Android App Download",
"mobileDownLoadLinkFDroid": "F-Droid App Download",
"privacy": "Datenschutz", "privacy": "Datenschutz",
"recentList": "Verlauf", "recentList": "Verlauf",
"recentListDelete": "Eintrag löschen", "recentListDelete": "Eintrag löschen",
@ -944,7 +989,15 @@
"sendFeedback": "Feedback senden", "sendFeedback": "Feedback senden",
"startMeeting": "Meeting starten", "startMeeting": "Meeting starten",
"terms": "AGB", "terms": "AGB",
"title": "Sichere, voll funktionale und komplett kostenlose Videokonferenzen" "title": "Sichere, voll funktionale und komplett kostenlose Videokonferenzen",
"logo":{
"calendar":"Kalender Logo",
"microsoftLogo":"Microsoft Logo",
"logoDeepLinking":"Jitsi Meet Logo",
"desktopPreviewThumbnail":"Desktop-Vorschau Thumbnail",
"googleLogo":"Google Logo",
"policyLogo":"Richtlinienlogo"
}
}, },
"lonelyMeetingExperience": { "lonelyMeetingExperience": {
"button": "Andere einladen", "button": "Andere einladen",

View File

@ -70,12 +70,17 @@
}, },
"privateNotice": "Private message to {{recipient}}", "privateNotice": "Private message to {{recipient}}",
"title": "Chat", "title": "Chat",
"you": "you" "you": "you",
"message": "Message",
"messageAccessibleTitle": "{{user}} says:",
"messageAccessibleTitleMe": "me says:",
"smileysPanel": "Emoji panel"
}, },
"chromeExtensionBanner": { "chromeExtensionBanner": {
"installExtensionText": "Install the extension for Google Calendar and Office 365 integration", "installExtensionText": "Install the extension for Google Calendar and Office 365 integration",
"buttonText": "Install Chrome Extension", "buttonText": "Install Chrome Extension",
"dontShowAgain": "Dont show me this again" "dontShowAgain": "Dont show me this again",
"close": "Close"
}, },
"connectingOverlay": { "connectingOverlay": {
"joiningRoom": "Connecting you to your meeting..." "joiningRoom": "Connecting you to your meeting..."
@ -204,6 +209,8 @@
"e2eeLabel": "Enable End-to-End Encryption", "e2eeLabel": "Enable End-to-End Encryption",
"e2eeWarning": "WARNING: Not all participants in this meeting seem to have support for End-to-End encryption. If you enable it they won't be able to see nor hear you.", "e2eeWarning": "WARNING: Not all participants in this meeting seem to have support for End-to-End encryption. If you enable it they won't be able to see nor hear you.",
"enterDisplayName": "Please enter your name here", "enterDisplayName": "Please enter your name here",
"enterDisplayNameToJoin": "Please enter your name to join",
"embedMeeting": "Embed meeting",
"error": "Error", "error": "Error",
"gracefulShutdown": "Our service is currently down for maintenance. Please try again later.", "gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
"grantModeratorDialog": "Are you sure you want to make this participant a moderator?", "grantModeratorDialog": "Are you sure you want to make this participant a moderator?",
@ -320,7 +327,9 @@
"WaitForHostMsgWOk": "The conference <b>{{room}}</b> has not yet started. If you are the host then please press Ok to authenticate. Otherwise, please wait for the host to arrive.", "WaitForHostMsgWOk": "The conference <b>{{room}}</b> has not yet started. If you are the host then please press Ok to authenticate. Otherwise, please wait for the host to arrive.",
"WaitingForHostTitle": "Waiting for the host ...", "WaitingForHostTitle": "Waiting for the host ...",
"Yes": "Yes", "Yes": "Yes",
"yourEntireScreen": "Your entire screen" "yourEntireScreen": "Your entire screen",
"remoteUserControls": "Remote user controls of {{username}}",
"localUserControls": "Local user controls"
}, },
"dialOut": { "dialOut": {
"statusMessage": "is now {{status}}" "statusMessage": "is now {{status}}"
@ -341,8 +350,19 @@
"slightBlur": "Slight Blur", "slightBlur": "Slight Blur",
"removeBackground": "Remove background", "removeBackground": "Remove background",
"addBackground": "Add background", "addBackground": "Add background",
"pleaseWait": "Please wait...",
"none": "None", "none": "None",
"desktopShareError": "Could not create desktop share" "uploadedImage": "Uploaded image {{index}}",
"deleteImage": "Delete image",
"image1" : "Beach",
"image2" : "White neutral wall",
"image3" : "White empty room",
"image4" : "Black floor lamp",
"image5" : "Mountain",
"image6" : "Forest ",
"image7" : "Sunrise",
"desktopShareError": "Could not create desktop share",
"desktopShare":"Desktop share"
}, },
"feedback": { "feedback": {
"average": "Average", "average": "Average",
@ -351,7 +371,8 @@
"good": "Good", "good": "Good",
"rateExperience": "Rate your meeting experience", "rateExperience": "Rate your meeting experience",
"veryBad": "Very Bad", "veryBad": "Very Bad",
"veryGood": "Very Good" "veryGood": "Very Good",
"star": "Star"
}, },
"incomingCall": { "incomingCall": {
"answer": "Answer", "answer": "Answer",
@ -368,6 +389,7 @@
"country": "Country", "country": "Country",
"dialANumber": "To join your meeting, dial one of these numbers and then enter the pin.", "dialANumber": "To join your meeting, dial one of these numbers and then enter the pin.",
"dialInConferenceID": "PIN:", "dialInConferenceID": "PIN:",
"copyNumber":"Copy number",
"dialInNotSupported": "Sorry, dialing in is currently not supported.", "dialInNotSupported": "Sorry, dialing in is currently not supported.",
"dialInNumber": "Dial-in:", "dialInNumber": "Dial-in:",
"dialInSummaryError": "Error fetching dial-in info now. Please try again later.", "dialInSummaryError": "Error fetching dial-in info now. Please try again later.",
@ -405,6 +427,7 @@
"support": "Support", "support": "Support",
"supportMsg": "If this keeps happening, reach out to" "supportMsg": "If this keeps happening, reach out to"
}, },
"jitsiHome": "{{logo}} Logo, links to Homepage",
"keyboardShortcuts": { "keyboardShortcuts": {
"focusLocal": "Focus on your video", "focusLocal": "Focus on your video",
"focusRemote": "Focus on another person's video", "focusRemote": "Focus on another person's video",
@ -525,9 +548,11 @@
"OldElectronAPPTitle": "Security vulnerability!", "OldElectronAPPTitle": "Security vulnerability!",
"oldElectronClientDescription1": "You appear to be using an old version of the Jitsi Meet client which has known security vulnerabilities. Please make sure you update to our ", "oldElectronClientDescription1": "You appear to be using an old version of the Jitsi Meet client which has known security vulnerabilities. Please make sure you update to our ",
"oldElectronClientDescription2": "latest build", "oldElectronClientDescription2": "latest build",
"oldElectronClientDescription3": " now!" "oldElectronClientDescription3": " now!",
"groupTitle": "Notifications"
}, },
"participantsPane": { "participantsPane": {
"close": "Close",
"headings": { "headings": {
"lobby": "Lobby ({{count}})", "lobby": "Lobby ({{count}})",
"participantsList": "Meeting participants ({{count}})" "participantsList": "Meeting participants ({{count}})"
@ -592,6 +617,7 @@
"or": "or", "or": "or",
"premeeting": "Pre meeting", "premeeting": "Pre meeting",
"showScreen": "Enable pre meeting screen", "showScreen": "Enable pre meeting screen",
"keyboardShortcuts" : "Enable Keyboard shortcuts",
"startWithPhone": "Start with phone audio", "startWithPhone": "Start with phone audio",
"screenSharingError": "Screen sharing error:", "screenSharingError": "Screen sharing error:",
"videoOnlyError": "Video error:", "videoOnlyError": "Video error:",
@ -613,6 +639,7 @@
"ringing": "Ringing..." "ringing": "Ringing..."
}, },
"profile": { "profile": {
"avatar": "avatar",
"setDisplayNameLabel": "Set your display name", "setDisplayNameLabel": "Set your display name",
"setEmailInput": "Enter e-mail", "setEmailInput": "Enter e-mail",
"setEmailLabel": "Set your gravatar email", "setEmailLabel": "Set your gravatar email",
@ -735,24 +762,24 @@
"audioRoute": "Select the sound device", "audioRoute": "Select the sound device",
"callQuality": "Manage video quality", "callQuality": "Manage video quality",
"cc": "Toggle subtitles", "cc": "Toggle subtitles",
"chat": "Toggle chat window", "chat": "Open / Close chat",
"document": "Toggle shared document", "document": "Toggle shared document",
"download": "Download our apps", "download": "Download our apps",
"embedMeeting": "Embed meeting", "embedMeeting": "Embed meeting",
"feedback": "Leave feedback", "feedback": "Leave feedback",
"fullScreen": "Toggle full screen", "fullScreen": "Toggle full screen",
"grantModerator": "Grant Moderator", "grantModerator": "Grant Moderator",
"hangup": "Leave the call", "hangup": "Leave the meeting",
"help": "Help", "help": "Help",
"invite": "Invite people", "invite": "Invite people",
"kick": "Kick participant", "kick": "Kick participant",
"lobbyButton": "Enable/disable lobby mode", "lobbyButton": "Enable/disable lobby mode",
"localRecording": "Toggle local recording controls", "localRecording": "Toggle local recording controls",
"lockRoom": "Toggle meeting password", "lockRoom": "Toggle meeting password",
"moreActions": "Toggle more actions menu", "moreActions": "More actions",
"moreActionsMenu": "More actions menu", "moreActionsMenu": "More actions menu",
"moreOptions": "Show more options", "moreOptions": "Show more options",
"mute": "Toggle mute audio", "mute": "Mute / Unmute",
"muteEveryone": "Mute everyone", "muteEveryone": "Mute everyone",
"muteEveryoneElse": "Mute everyone else", "muteEveryoneElse": "Mute everyone else",
"muteEveryonesVideo": "Disable everyone's camera", "muteEveryonesVideo": "Disable everyone's camera",
@ -761,7 +788,7 @@
"pip": "Toggle Picture-in-Picture mode", "pip": "Toggle Picture-in-Picture mode",
"privateMessage": "Send private message", "privateMessage": "Send private message",
"profile": "Edit your profile", "profile": "Edit your profile",
"raiseHand": "Toggle raise hand", "raiseHand": "Raise / Lower your hand",
"recording": "Toggle recording", "recording": "Toggle recording",
"remoteMute": "Mute participant", "remoteMute": "Mute participant",
"remoteVideoMute": "Disable camera of participant", "remoteVideoMute": "Disable camera of participant",
@ -770,18 +797,22 @@
"shareaudio": "Share audio", "shareaudio": "Share audio",
"sharedvideo": "Toggle YouTube video sharing", "sharedvideo": "Toggle YouTube video sharing",
"shareRoom": "Invite someone", "shareRoom": "Invite someone",
"shareYourScreen": "Toggle screenshare", "shareYourScreen": "Start / Stop sharing your screen",
"shortcuts": "Toggle shortcuts", "shortcuts": "Toggle shortcuts",
"show": "Show on stage", "show": "Show on stage",
"speakerStats": "Toggle speaker statistics", "speakerStats": "Toggle speaker statistics",
"tileView": "Toggle tile view", "tileView": "Toggle tile view",
"toggleCamera": "Toggle camera", "toggleCamera": "Toggle camera",
"toggleFilmstrip": "Toggle filmstrip", "toggleFilmstrip": "Toggle filmstrip",
"videomute": "Toggle mute video", "videomute": "Start / Stop camera",
"selectBackground": "Select Background" "videoblur": "Toggle video blur",
"selectBackground": "Select Background",
"expand": "Expand",
"collapse": "Collapse"
}, },
"addPeople": "Add people to your call", "addPeople": "Add people to your call",
"audioSettings": "Audio settings", "audioSettings": "Audio settings",
"videoSettings": "Video settings",
"audioOnlyOff": "Disable low bandwidth mode", "audioOnlyOff": "Disable low bandwidth mode",
"audioOnlyOn": "Enable low bandwidth mode", "audioOnlyOn": "Enable low bandwidth mode",
"audioRoute": "Select the sound device", "audioRoute": "Select the sound device",
@ -799,7 +830,7 @@
"exitFullScreen": "Exit full screen", "exitFullScreen": "Exit full screen",
"exitTileView": "Exit tile view", "exitTileView": "Exit tile view",
"feedback": "Leave feedback", "feedback": "Leave feedback",
"hangup": "Leave", "hangup": "Leave the meeting",
"help": "Help", "help": "Help",
"invite": "Invite people", "invite": "Invite people",
"lobbyButtonDisable": "Disable lobby mode", "lobbyButtonDisable": "Disable lobby mode",
@ -842,7 +873,6 @@
"tileViewToggle": "Toggle tile view", "tileViewToggle": "Toggle tile view",
"toggleCamera": "Toggle camera", "toggleCamera": "Toggle camera",
"videomute": "Start / Stop camera", "videomute": "Start / Stop camera",
"videoSettings": "Video settings",
"selectBackground": "Select background" "selectBackground": "Select background"
}, },
"transcribing": { "transcribing": {
@ -869,6 +899,7 @@
"react-nativeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.", "react-nativeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
"safariGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions." "safariGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions."
}, },
"volumeSlider": "Volume slider",
"videoSIPGW": { "videoSIPGW": {
"busy": "We're working on freeing resources. Please try again in a few minutes.", "busy": "We're working on freeing resources. Please try again in a few minutes.",
"busyTitle": "The Room service is currently busy", "busyTitle": "The Room service is currently busy",
@ -913,6 +944,7 @@
"videomute": "Participant has stopped the camera" "videomute": "Participant has stopped the camera"
}, },
"welcomepage": { "welcomepage": {
"addMeetingName": "Add Meeting name",
"accessibilityLabel": { "accessibilityLabel": {
"join": "Tap to join", "join": "Tap to join",
"roomname": "Enter room name" "roomname": "Enter room name"
@ -934,6 +966,9 @@
"info": "Dial-in info", "info": "Dial-in info",
"join": "CREATE / JOIN", "join": "CREATE / JOIN",
"jitsiOnMobile": "Jitsi on mobile download our apps and start a meeting from anywhere", "jitsiOnMobile": "Jitsi on mobile download our apps and start a meeting from anywhere",
"mobileDownLoadLinkIos": "Download mobile app for iOS",
"mobileDownLoadLinkAndroid": "Download mobile app for Android",
"mobileDownLoadLinkFDroid": "Download mobile app for F-Droid",
"moderatedMessage": "Or <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">book a meeting URL</a> in advance where you are the only moderator.", "moderatedMessage": "Or <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">book a meeting URL</a> in advance where you are the only moderator.",
"privacy": "Privacy", "privacy": "Privacy",
"recentList": "Recent", "recentList": "Recent",
@ -946,7 +981,15 @@
"sendFeedback": "Send feedback", "sendFeedback": "Send feedback",
"startMeeting": "Start meeting", "startMeeting": "Start meeting",
"terms": "Terms", "terms": "Terms",
"title": "Secure, fully featured, and completely free video conferencing" "title": "Secure, fully featured, and completely free video conferencing",
"logo":{
"calendar":"Calendar logo",
"microsoftLogo":"Microsoft logo",
"logoDeepLinking":"Jitsi meet logo",
"desktopPreviewThumbnail":"Desktop preview thumbnail",
"googleLogo":"Google Logo",
"policyLogo":"Policy logo"
}
}, },
"lonelyMeetingExperience": { "lonelyMeetingExperience": {
"button": "Invite others", "button": "Invite others",

View File

@ -1,5 +1,5 @@
/* global APP, $ */ /* global APP */
import { jitsiLocalStorage } from '@jitsi/js-utils';
import Logger from 'jitsi-meet-logger'; import Logger from 'jitsi-meet-logger';
import { import {
@ -30,44 +30,66 @@ const _shortcuts = new Map();
const _shortcutsHelp = new Map(); const _shortcutsHelp = new Map();
/** /**
* True if the keyboard shortcuts are enabled and false if not. * The key used to save in local storage if keyboard shortcuts are enabled.
* @type {boolean}
*/ */
let enabled = true; const _enableShortcutsKey = 'enableShortcuts';
/**
* Prefer keyboard handling of these elements over global shortcuts.
* If a button is triggered using the Spacebar it should not trigger PTT.
* If an input element is focused and M is pressed it should not mute audio.
*/
const _elementsBlacklist = [
'input',
'textarea',
'button',
'[role=button]',
'[role=menuitem]',
'[role=radio]',
'[role=tab]',
'[role=option]',
'[role=switch]',
'[role=range]',
'[role=log]'
];
/**
* An element selector for elements that have their own keyboard handling.
*/
const _focusedElementsSelector = `:focus:is(${_elementsBlacklist.join(',')})`;
/** /**
* Maps keycode to character, id of popover for given function and function. * Maps keycode to character, id of popover for given function and function.
*/ */
const KeyboardShortcut = { const KeyboardShortcut = {
isPushToTalkActive: false,
init() { init() {
this._initGlobalShortcuts(); this._initGlobalShortcuts();
window.onkeyup = e => { window.onkeyup = e => {
if (!enabled) { if (!this.getEnabled()) {
return; return;
} }
const key = this._getKeyboardKey(e).toUpperCase(); const key = this._getKeyboardKey(e).toUpperCase();
const num = parseInt(key, 10); const num = parseInt(key, 10);
if (!($(':focus').is('input[type=text]') if (!document.querySelector(_focusedElementsSelector)) {
|| $(':focus').is('input[type=password]')
|| $(':focus').is('textarea'))) {
if (_shortcuts.has(key)) { if (_shortcuts.has(key)) {
_shortcuts.get(key).function(e); _shortcuts.get(key).function(e);
} else if (!isNaN(num) && num >= 0 && num <= 9) { } else if (!isNaN(num) && num >= 0 && num <= 9) {
APP.store.dispatch(clickOnVideo(num)); APP.store.dispatch(clickOnVideo(num));
} }
} }
}; };
window.onkeydown = e => { window.onkeydown = e => {
if (!enabled) { if (!this.getEnabled()) {
return; return;
} }
if (!($(':focus').is('input[type=text]') const focusedElement = document.querySelector(_focusedElementsSelector);
|| $(':focus').is('input[type=password]')
|| $(':focus').is('textarea'))) { if (!focusedElement) {
if (this._getKeyboardKey(e).toUpperCase() === ' ') { if (this._getKeyboardKey(e).toUpperCase() === ' ') {
if (APP.conference.isLocalAudioMuted()) { if (APP.conference.isLocalAudioMuted()) {
sendAnalytics(createShortcutEvent( sendAnalytics(createShortcutEvent(
@ -75,8 +97,14 @@ const KeyboardShortcut = {
PRESSED)); PRESSED));
logger.log('Talk shortcut pressed'); logger.log('Talk shortcut pressed');
APP.conference.muteAudio(false); APP.conference.muteAudio(false);
this.isPushToTalkActive = true;
} }
} }
} else if (this._getKeyboardKey(e).toUpperCase() === 'ESCAPE') {
// Allow to remove focus from selected elements using ESC key.
if (focusedElement && focusedElement.blur) {
focusedElement.blur();
}
} }
}; };
}, },
@ -86,7 +114,13 @@ const KeyboardShortcut = {
* @param {boolean} value - the new value. * @param {boolean} value - the new value.
*/ */
enable(value) { enable(value) {
enabled = value; jitsiLocalStorage.setItem(_enableShortcutsKey, value);
},
getEnabled() {
// Should be enabled if not explicitly set to false
// eslint-disable-next-line no-unneeded-ternary
return jitsiLocalStorage.getItem(_enableShortcutsKey) === 'false' ? false : true;
}, },
/** /**
@ -198,9 +232,12 @@ const KeyboardShortcut = {
// register SPACE shortcut in two steps to insure visibility of help // register SPACE shortcut in two steps to insure visibility of help
// message // message
this.registerShortcut(' ', null, () => { this.registerShortcut(' ', null, () => {
sendAnalytics(createShortcutEvent('push.to.talk', RELEASED)); if (this.isPushToTalkActive) {
logger.log('Talk shortcut released'); sendAnalytics(createShortcutEvent('push.to.talk', RELEASED));
APP.conference.muteAudio(true); logger.log('Talk shortcut released');
APP.conference.muteAudio(true);
this.isPushToTalkActive = false;
}
}); });
this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk'); this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk');

View File

@ -1,4 +1,4 @@
/* @flow */ /* @flow */
import jqueryI18next from 'jquery-i18next'; import jqueryI18next from 'jquery-i18next';
@ -6,6 +6,10 @@ import { i18next } from '../../react/features/base/i18n';
declare var $: Function; declare var $: Function;
type DocumentElement = {
lang: string
}
/** /**
* Notifies that the {@link i18next} instance has finished its initialization. * Notifies that the {@link i18next} instance has finished its initialization.
* *
@ -13,7 +17,12 @@ declare var $: Function;
* @private * @private
*/ */
function _onI18nInitialized() { function _onI18nInitialized() {
const documentElement: DocumentElement
= document.documentElement || {};
$('[data-i18n]').localize(); $('[data-i18n]').localize();
documentElement.lang = i18next.language;
} }
/** /**

47
package-lock.json generated
View File

@ -15262,12 +15262,23 @@
} }
}, },
"react-textarea-autosize": { "react-textarea-autosize": {
"version": "7.1.0", "version": "8.3.0",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-7.1.0.tgz", "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.3.0.tgz",
"integrity": "sha512-c2FlR/fP0qbxmlrW96SdrbgP/v0XZMTupqB90zybvmDVDutytUgPl7beU35klwcTeMepUIQEpQUn3P3bdshGPg==", "integrity": "sha512-3GLWFAan2pbwBeoeNDoqGmSbrShORtgWfaWX0RJDivsUrpShh01saRM5RU/i4Zmf+whpBVEY5cA90Eq8Ub1N3w==",
"requires": { "requires": {
"@babel/runtime": "^7.1.2", "@babel/runtime": "^7.10.2",
"prop-types": "^15.6.0" "use-composed-ref": "^1.0.0",
"use-latest": "^1.0.0"
},
"dependencies": {
"@babel/runtime": {
"version": "7.12.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
"integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
}
} }
}, },
"react-transition-group": { "react-transition-group": {
@ -17690,6 +17701,11 @@
"integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==",
"dev": true "dev": true
}, },
"ts-essentials": {
"version": "2.0.12",
"resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-2.0.12.tgz",
"integrity": "sha512-3IVX4nI6B5cc31/GFFE+i8ey/N2eA0CZDbo6n0yrz0zDX8ZJ8djmU1p+XRz7G3is0F3bB3pu2pAroFdAWQKU3w=="
},
"tslib": { "tslib": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
@ -17975,6 +17991,27 @@
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz",
"integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==" "integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg=="
}, },
"use-composed-ref": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.1.0.tgz",
"integrity": "sha512-my1lNHGWsSDAhhVAT4MKs6IjBUtG6ZG11uUqexPH9PptiIZDQOzaF4f5tEbJ2+7qvNbtXNBbU3SfmN+fXlWDhg==",
"requires": {
"ts-essentials": "^2.0.3"
}
},
"use-isomorphic-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.1.tgz",
"integrity": "sha512-L7Evj8FGcwo/wpbv/qvSfrkHFtOpCzvM5yl2KVyDJoylVuSvzphiiasmjgQPttIGBAy2WKiBNR98q8w7PiNgKQ=="
},
"use-latest": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.0.tgz",
"integrity": "sha512-d2TEuG6nSLKQLAfW3By8mKr8HurOlTkul0sOpxbClIv4SQ4iOd7BYr7VIzdbktUCnv7dua/60xzd8igMU6jmyw==",
"requires": {
"use-isomorphic-layout-effect": "^1.0.0"
}
},
"use-memo-one": { "use-memo-one": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz", "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz",

View File

@ -91,7 +91,7 @@
"react-native-webview": "11.0.2", "react-native-webview": "11.0.2",
"react-native-youtube-iframe": "1.2.3", "react-native-youtube-iframe": "1.2.3",
"react-redux": "7.1.0", "react-redux": "7.1.0",
"react-textarea-autosize": "7.1.0", "react-textarea-autosize": "8.3.0",
"react-transition-group": "2.4.0", "react-transition-group": "2.4.0",
"react-youtube": "7.13.1", "react-youtube": "7.13.1",
"redux": "4.0.4", "redux": "4.0.4",

View File

@ -2,6 +2,7 @@
import React from 'react'; import React from 'react';
import { translate } from '../../../../base/i18n';
import { Icon } from '../../../icons'; import { Icon } from '../../../icons';
import AbstractStatelessAvatar, { type Props as AbstractProps } from '../AbstractStatelessAvatar'; import AbstractStatelessAvatar, { type Props as AbstractProps } from '../AbstractStatelessAvatar';
@ -30,14 +31,19 @@ type Props = AbstractProps & {
/** /**
* TestId of the element, if any. * TestId of the element, if any.
*/ */
testId?: string testId?: string,
/**
* Invoked to obtain translated strings.
*/
t: Function
}; };
/** /**
* Implements a stateless avatar component that renders an avatar purely from what gets passed through * Implements a stateless avatar component that renders an avatar purely from what gets passed through
* props. * props.
*/ */
export default class StatelessAvatar extends AbstractStatelessAvatar<Props> { class StatelessAvatar extends AbstractStatelessAvatar<Props> {
/** /**
* Implements {@code Component#render}. * Implements {@code Component#render}.
* *
@ -64,6 +70,7 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
return ( return (
<div className = { this._getBadgeClassName() }> <div className = { this._getBadgeClassName() }>
<img <img
alt = { this.props.t('profile.avatar') }
className = { this._getAvatarClassName() } className = { this._getAvatarClassName() }
data-testid = { this.props.testId } data-testid = { this.props.testId }
id = { this.props.id } id = { this.props.id }
@ -88,7 +95,7 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
xmlnsXlink = 'http://www.w3.org/1999/xlink'> xmlnsXlink = 'http://www.w3.org/1999/xlink'>
<text <text
dominantBaseline = 'central' dominantBaseline = 'central'
fill = 'rgba(255,255,255,.6)' fill = 'rgba(255,255,255,1)'
fontSize = '40pt' fontSize = '40pt'
textAnchor = 'middle' textAnchor = 'middle'
x = '50' x = '50'
@ -104,6 +111,7 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
return ( return (
<div className = { this._getBadgeClassName() }> <div className = { this._getBadgeClassName() }>
<img <img
alt = { this.props.t('profile.avatar') }
className = { this._getAvatarClassName('defaultAvatar') } className = { this._getAvatarClassName('defaultAvatar') }
data-testid = { this.props.testId } data-testid = { this.props.testId }
id = { this.props.id } id = { this.props.id }
@ -157,3 +165,5 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
_isIcon: (?string | ?Object) => boolean _isIcon: (?string | ?Object) => boolean
} }
export default translate(StatelessAvatar);

View File

@ -3,7 +3,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Icon, IconCheck, IconCopy } from '../../base/icons'; import { Icon, IconCheck, IconCopy } from '../../base/icons';
import { translate } from '../i18n';
import { copyText } from '../util'; import { copyText } from '../util';
@ -32,7 +31,12 @@ type Props = {
/** /**
* The text displayed on copy success * The text displayed on copy success
*/ */
textOnCopySuccess: string textOnCopySuccess: string,
/**
* The id of the button
*/
id?: string,
}; };
/** /**
@ -40,7 +44,7 @@ type Props = {
* *
* @returns {React$Element<any>} * @returns {React$Element<any>}
*/ */
function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnCopySuccess }: Props) { function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnCopySuccess, id }: Props) {
const [ isClicked, setIsClicked ] = useState(false); const [ isClicked, setIsClicked ] = useState(false);
const [ isHovered, setIsHovered ] = useState(false); const [ isHovered, setIsHovered ] = useState(false);
@ -83,6 +87,20 @@ function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnC
setIsHovered(false); setIsHovered(false);
} }
/**
* KeyPress handler for accessibility.
*
* @param {React.KeyboardEventHandler<HTMLDivElement>} e - The key event to handle.
*
* @returns {void}
*/
function onKeyPress(e) {
if (onClick && (e.key === ' ' || e.key === 'Enter')) {
e.preventDefault();
onClick();
}
}
/** /**
* Renders the content of the link based on the state. * Renders the content of the link based on the state.
* *
@ -93,7 +111,7 @@ function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnC
return ( return (
<> <>
<div className = 'copy-button-content selected'> <div className = 'copy-button-content selected'>
{textOnCopySuccess} <span role = { 'alert' }>{ textOnCopySuccess }</span>
</div> </div>
<Icon src = { IconCheck } /> <Icon src = { IconCheck } />
</> </>
@ -112,10 +130,17 @@ function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnC
return ( return (
<div <div
aria-label = { textOnHover }
className = { `${className} copy-button${isClicked ? ' clicked' : ''}` } className = { `${className} copy-button${isClicked ? ' clicked' : ''}` }
id = { id }
onBlur = { onHoverOut }
onClick = { onClick } onClick = { onClick }
onFocus = { onHoverIn }
onKeyPress = { onKeyPress }
onMouseOut = { onHoverOut } onMouseOut = { onHoverOut }
onMouseOver = { onHoverIn }> onMouseOver = { onHoverIn }
role = 'button'
tabIndex = { 0 }>
{ renderContent() } { renderContent() }
</div> </div>
); );
@ -125,4 +150,4 @@ CopyButton.defaultProps = {
className: '' className: ''
}; };
export default translate(CopyButton); export default CopyButton;

View File

@ -10,6 +10,7 @@ import {
} from '@atlaskit/modal-dialog/dist/es2019/styled/Content'; } from '@atlaskit/modal-dialog/dist/es2019/styled/Content';
import React from 'react'; import React from 'react';
import { translate } from '../../../i18n';
import { Icon, IconClose } from '../../../icons'; import { Icon, IconClose } from '../../../icons';
const TitleIcon = ({ appearance }: { appearance?: 'danger' | 'warning' }) => { const TitleIcon = ({ appearance }: { appearance?: 'danger' | 'warning' }) => {
@ -45,11 +46,40 @@ type Props = {
* @class ModalHeader * @class ModalHeader
* @extends {React.Component<Props>} * @extends {React.Component<Props>}
*/ */
export default class ModalHeader extends React.Component<Props> { class ModalHeader extends React.Component<Props> {
static defaultProps = { static defaultProps = {
isHeadingMultiline: true isHeadingMultiline: true
}; };
/**
* Initializes a new {@code ModalHeader} instance.
*
* @param {*} props - The read-only properties with which the new instance
* is to be initialized.
*/
constructor(props) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onKeyPress = this._onKeyPress.bind(this);
}
_onKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onKeyPress(e) {
if (this.props.onClose && (e.key === ' ' || e.key === 'Enter')) {
e.preventDefault();
this.props.onClose();
}
}
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
* *
@ -65,7 +95,8 @@ export default class ModalHeader extends React.Component<Props> {
onClose, onClose,
showKeyline, showKeyline,
isHeadingMultiline, isHeadingMultiline,
testId testId,
t
} = this.props; } = this.props;
if (!heading) { if (!heading) {
@ -83,12 +114,18 @@ export default class ModalHeader extends React.Component<Props> {
{heading} {heading}
</TitleText> </TitleText>
</Title> </Title>
{ {
!hideCloseIconButton && <Icon !hideCloseIconButton && <Icon
ariaLabel = { t('dialog.close') }
onClick = { onClose } onClick = { onClose }
src = { IconClose } /> onKeyPress = { this._onKeyPress }
role = 'button'
src = { IconClose }
tabIndex = { 0 } />
} }
</Header> </Header>
); );
} }
} }
export default translate(ModalHeader);

View File

@ -118,7 +118,7 @@ class StatelessDialog extends Component<Props> {
// Bind event handlers so they are only bound once for every instance. // Bind event handlers so they are only bound once for every instance.
this._onCancel = this._onCancel.bind(this); this._onCancel = this._onCancel.bind(this);
this._onDialogDismissed = this._onDialogDismissed.bind(this); this._onDialogDismissed = this._onDialogDismissed.bind(this);
this._onKeyDown = this._onKeyDown.bind(this); this._onKeyPress = this._onKeyPress.bind(this);
this._onSubmit = this._onSubmit.bind(this); this._onSubmit = this._onSubmit.bind(this);
this._renderFooter = this._renderFooter.bind(this); this._renderFooter = this._renderFooter.bind(this);
this._setDialogElement = this._setDialogElement.bind(this); this._setDialogElement = this._setDialogElement.bind(this);
@ -159,7 +159,7 @@ class StatelessDialog extends Component<Props> {
shouldCloseOnEscapePress = { true } shouldCloseOnEscapePress = { true }
width = { width || 'medium' }> width = { width || 'medium' }>
<div <div
onKeyDown = { this._onKeyDown } onKeyPress = { this._onKeyPress }
ref = { this._setDialogElement }> ref = { this._setDialogElement }>
<form <form
className = 'modal-dialog-form' className = 'modal-dialog-form'
@ -327,7 +327,7 @@ class StatelessDialog extends Component<Props> {
this._dialogElement = element; this._dialogElement = element;
} }
_onKeyDown: (Object) => void; _onKeyPress: (Object) => void;
/** /**
* Handles 'Enter' key in the dialog to submit/hide dialog depending on * Handles 'Enter' key in the dialog to submit/hide dialog depending on
@ -337,7 +337,7 @@ class StatelessDialog extends Component<Props> {
* @private * @private
* @returns {void} * @returns {void}
*/ */
_onKeyDown(event) { _onKeyPress(event) {
// If the event coming to the dialog has been subject to preventDefault // If the event coming to the dialog has been subject to preventDefault
// we don't handle it here. // we don't handle it here.
if (event.defaultPrevented) { if (event.defaultPrevented) {

View File

@ -33,6 +33,7 @@ export function checkChromeExtensionsInstalled(config: Object = {}) {
const img = new Image(); const img = new Image();
img.src = `chrome-extension://${info.id}/${info.path}`; img.src = `chrome-extension://${info.id}/${info.path}`;
img.setAttribute('aria-hidden', 'true');
img.onload = function() { img.onload = function() {
resolve(true); resolve(true);
}; };

View File

@ -1,6 +1,6 @@
// @flow // @flow
import React from 'react'; import React, { useCallback } from 'react';
import { Container } from '../../react/base'; import { Container } from '../../react/base';
import { styleTypeToObject } from '../../styles'; import { styleTypeToObject } from '../../styles';
@ -10,7 +10,7 @@ type Props = {
/** /**
* Class name for the web platform, if any. * Class name for the web platform, if any.
*/ */
className: string, className?: string,
/** /**
* Color of the icon (if not provided by the style object). * Color of the icon (if not provided by the style object).
@ -22,6 +22,11 @@ type Props = {
*/ */
id?: string, id?: string,
/**
* Id of the icon container
*/
containerId?: string,
/** /**
* Function to invoke on click. * Function to invoke on click.
*/ */
@ -40,8 +45,63 @@ type Props = {
/** /**
* Style object to be applied. * Style object to be applied.
*/ */
style?: Object style?: Object,
};
/**
* aria disabled flag for the Icon.
*/
ariaDisabled?: boolean,
/**
* aria label for the Icon.
*/
ariaLabel?: string,
/**
* whether the element has a popup
*/
ariaHasPopup?: boolean,
/**
* whether the element has a pressed
*/
ariaPressed?: boolean,
/**
* id of description label
*/
ariaDescribedBy?: string,
/**
* whether the element popup is expanded
*/
ariaExpanded?: boolean,
/**
* The id of the element this button icon controls
*/
ariaControls?: string,
/**
* tabIndex for the Icon.
*/
tabIndex?: number,
/**
* role for the Icon.
*/
role?: string,
/**
* keypress handler.
*/
onKeyPress?: Function,
/**
* keydown handler.
*/
onKeyDown?: Function
}
export const DEFAULT_COLOR = navigator.product === 'ReactNative' ? 'white' : undefined; export const DEFAULT_COLOR = navigator.product === 'ReactNative' ? 'white' : undefined;
export const DEFAULT_SIZE = navigator.product === 'ReactNative' ? 36 : 22; export const DEFAULT_SIZE = navigator.product === 'ReactNative' ? 36 : 22;
@ -57,11 +117,24 @@ export default function Icon(props: Props) {
className, className,
color, color,
id, id,
containerId,
onClick, onClick,
size, size,
src: IconComponent, src: IconComponent,
style style,
} = props; ariaHasPopup,
ariaLabel,
ariaDisabled,
ariaExpanded,
ariaControls,
tabIndex,
ariaPressed,
ariaDescribedBy,
role,
onKeyPress,
onKeyDown,
...rest
}: Props = props;
const { const {
color: styleColor, color: styleColor,
@ -71,11 +144,33 @@ export default function Icon(props: Props) {
const calculatedColor = color ?? styleColor ?? DEFAULT_COLOR; const calculatedColor = color ?? styleColor ?? DEFAULT_COLOR;
const calculatedSize = size ?? styleSize ?? DEFAULT_SIZE; const calculatedSize = size ?? styleSize ?? DEFAULT_SIZE;
const onKeyPressHandler = useCallback(e => {
if ((e.key === 'Enter' || e.key === ' ') && onClick) {
e.preventDefault();
onClick(e);
} else if (onKeyPress) {
onKeyPress(e);
}
}, [ onClick, onKeyPress ]);
return ( return (
<Container <Container
className = { `jitsi-icon ${className}` } { ...rest }
aria-controls = { ariaControls }
aria-describedby = { ariaDescribedBy }
aria-disabled = { ariaDisabled }
aria-expanded = { ariaExpanded }
aria-haspopup = { ariaHasPopup }
aria-label = { ariaLabel }
aria-pressed = { ariaPressed }
className = { `jitsi-icon ${className || ''}` }
id = { containerId }
onClick = { onClick } onClick = { onClick }
style = { restStyle }> onKeyDown = { onKeyDown }
onKeyPress = { onKeyPressHandler }
role = { role }
style = { restStyle }
tabIndex = { tabIndex }>
<IconComponent <IconComponent
fill = { calculatedColor } fill = { calculatedColor }
height = { calculatedSize } height = { calculatedSize }

View File

@ -129,7 +129,9 @@ class Popover extends Component<Props, State> {
// Bind event handlers so they are only bound once for every instance. // Bind event handlers so they are only bound once for every instance.
this._onHideDialog = this._onHideDialog.bind(this); this._onHideDialog = this._onHideDialog.bind(this);
this._onShowDialog = this._onShowDialog.bind(this); this._onShowDialog = this._onShowDialog.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
this._drawerContainerRef = React.createRef(); this._drawerContainerRef = React.createRef();
this._onEscKey = this._onEscKey.bind(this);
} }
/** /**
@ -207,6 +209,7 @@ class Popover extends Component<Props, State> {
<div <div
className = { className } className = { className }
id = { id } id = { id }
onKeyPress = { this._onKeyPress }
onMouseEnter = { this._onShowDialog } onMouseEnter = { this._onShowDialog }
onMouseLeave = { this._onHideDialog }> onMouseLeave = { this._onHideDialog }>
<InlineDialog <InlineDialog
@ -231,13 +234,13 @@ class Popover extends Component<Props, State> {
this.setState({ showDialog: false }); this.setState({ showDialog: false });
} }
_onShowDialog: () => void; _onShowDialog: (Object) => void;
/** /**
* Displays the {@code InlineDialog} and calls any registered onPopoverOpen * Displays the {@code InlineDialog} and calls any registered onPopoverOpen
* callbacks. * callbacks.
* *
* @param {MouseEvent} event - The mouse event to intercept. * @param {Object} event - The mouse event or the keypress event to intercept.
* @private * @private
* @returns {void} * @returns {void}
*/ */
@ -252,6 +255,45 @@ class Popover extends Component<Props, State> {
} }
} }
_onKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
if (this.state.showDialog) {
this._onHideDialog();
} else {
this._onShowDialog(e);
}
}
}
_onEscKey: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onEscKey(e) {
if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
if (this.state.showDialog) {
this._onHideDialog();
}
}
}
/** /**
* Renders the React Element to be displayed in the {@code InlineDialog}. * Renders the React Element to be displayed in the {@code InlineDialog}.
* Also adds padding to support moving the mouse from the trigger to the * Also adds padding to support moving the mouse from the trigger to the
@ -264,7 +306,9 @@ class Popover extends Component<Props, State> {
const { content, position } = this.props; const { content, position } = this.props;
return ( return (
<div className = 'popover'> <div
className = 'popover'
onKeyDown = { this._onEscKey }>
{ content } { content }
<div className = 'popover-mouse-padding-top' /> <div className = 'popover-mouse-padding-top' />
<div className = { _mapPositionToPaddingClass(position) } /> <div className = { _mapPositionToPaddingClass(position) } />

View File

@ -1,6 +1,6 @@
// @flow // @flow
import React from 'react'; import React, { useCallback } from 'react';
import { Icon, IconArrowDown } from '../../../icons'; import { Icon, IconArrowDown } from '../../../icons';
@ -46,10 +46,36 @@ type Props = {
*/ */
onClick: Function, onClick: Function,
/** /**
* Click handler for options. * Click handler for options.
*/ */
onOptionsClick?: Function onOptionsClick?: Function,
/**
* to navigate with the keyboard.
*/
tabIndex?: number,
/**
* to give a role to the icon.
*/
role?: string,
/**
* to give a aria-pressed to the icon.
*/
ariaPressed?: boolean,
/**
* The Label of the current element
*/
ariaLabel?: string,
/**
* The Label of the child element
*/
ariaDropDownLabel?: string
}; };
/** /**
@ -66,23 +92,57 @@ function ActionButton({
testId, testId,
type = 'primary', type = 'primary',
onClick, onClick,
onOptionsClick onOptionsClick,
tabIndex,
role,
ariaPressed,
ariaLabel,
ariaDropDownLabel
}: Props) { }: Props) {
const onKeyPressHandler = useCallback(e => {
if (onClick && !disabled && (e.key === ' ' || e.key === 'Enter')) {
e.preventDefault();
onClick(e);
}
}, [ onClick, disabled ]);
const onOptionsKeyPressHandler = useCallback(e => {
if (onOptionsClick && !disabled && (e.key === ' ' || e.key === 'Enter')) {
e.preventDefault();
e.stopPropagation();
onOptionsClick(e);
}
}, [ onOptionsClick, disabled ]);
return ( return (
<div <div
aria-disabled = { disabled }
aria-label = { ariaLabel }
className = { `action-btn ${className} ${type} ${disabled ? 'disabled' : ''}` } className = { `action-btn ${className} ${type} ${disabled ? 'disabled' : ''}` }
data-testid = { testId ? testId : undefined } data-testid = { testId ? testId : undefined }
onClick = { disabled ? undefined : onClick }> onClick = { disabled ? undefined : onClick }
onKeyPress = { onKeyPressHandler }
role = 'button'
tabIndex = { 0 } >
{children} {children}
{hasOptions && <div { hasOptions
className = 'options' && <div
data-testid = 'prejoin.joinOptions' aria-disabled = { disabled }
onClick = { disabled ? undefined : onOptionsClick }> aria-haspopup = 'true'
<Icon aria-label = { ariaDropDownLabel }
className = 'icon' aria-pressed = { ariaPressed }
size = { 14 } className = 'options'
src = { OptionsIcon } /> data-testid = 'prejoin.joinOptions'
</div> onClick = { disabled ? undefined : onOptionsClick }
onKeyPress = { onOptionsKeyPressHandler }
role = { role }
tabIndex = { tabIndex }>
<Icon
className = 'icon'
size = { 14 }
src = { OptionsIcon } />
</div>
} }
</div> </div>
); );

View File

@ -1,6 +1,6 @@
// @flow // @flow
import React, { useState } from 'react'; import React, { useCallback, useState } from 'react';
import { translate } from '../../../i18n'; import { translate } from '../../../i18n';
import { Icon, IconArrowDownSmall, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons'; import { Icon, IconArrowDownSmall, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons';
@ -65,24 +65,50 @@ function ConnectionStatus({ connectionDetails, t, connectionType }: Props) {
? 'con-status-details-visible' ? 'con-status-details-visible'
: 'con-status-details-hidden'; : 'con-status-details-hidden';
const onToggleDetails = useCallback(e => {
e.preventDefault();
toggleDetails(!showDetails);
}, [ showDetails, toggleDetails ]);
const onKeyPressToggleDetails = useCallback(e => {
if (toggleDetails && (e.key === ' ' || e.key === 'Enter')) {
e.preventDefault();
toggleDetails(!showDetails);
}
}, [ showDetails, toggleDetails ]);
return ( return (
<div className = 'con-status'> <div className = 'con-status'>
<div className = 'con-status-container'> <div className = 'con-status-container'>
<div className = 'con-status-header'> <div
aria-level = { 1 }
className = 'con-status-header'
role = 'heading'>
<div className = { `con-status-circle ${connectionClass}` }> <div className = { `con-status-circle ${connectionClass}` }>
<Icon <Icon
size = { 16 } size = { 16 }
src = { icon } /> src = { icon } />
</div> </div>
<span className = 'con-status-text'>{t(connectionText)}</span> <span
aria-hidden = { !showDetails }
className = 'con-status-text'
id = 'connection-status-description'>{t(connectionText)}</span>
<Icon <Icon
ariaDescribedBy = 'connection-status-description'
ariaPressed = { showDetails }
className = { arrowClassName } className = { arrowClassName }
// eslint-disable-next-line react/jsx-no-bind onClick = { onToggleDetails }
onClick = { () => toggleDetails(!showDetails) } onKeyPress = { onKeyPressToggleDetails }
role = 'button'
size = { 24 } size = { 24 }
src = { IconArrowDownSmall } /> src = { IconArrowDownSmall }
tabIndex = { 0 } />
</div> </div>
<div className = { `con-status-details ${detailsClassName}` }>{detailsText}</div> <div
aria-level = '2'
className = { `con-status-details ${detailsClassName}` }
role = 'heading'>
{detailsText}</div>
</div> </div>
</div> </div>
); );

View File

@ -2,11 +2,11 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import CopyMeetingLinkSection
from '../../../../invite/components/add-people-dialog/web/CopyMeetingLinkSection';
import { getCurrentConferenceUrl } from '../../../connection'; import { getCurrentConferenceUrl } from '../../../connection';
import { translate } from '../../../i18n'; import { translate } from '../../../i18n';
import { Icon, IconCopy, IconCheck } from '../../../icons';
import { connect } from '../../../redux'; import { connect } from '../../../redux';
import { copyText, getDecodedURI } from '../../../util';
type Props = { type Props = {
@ -27,152 +27,11 @@ type Props = {
_enableAutomaticUrlCopy: boolean, _enableAutomaticUrlCopy: boolean,
}; };
type State = {
/**
* If true it shows the 'copy link' message.
*/
showCopyLink: boolean,
/**
* If true it shows the 'link copied' message.
*/
showLinkCopied: boolean,
};
const COPY_TIMEOUT = 2000;
/** /**
* Component used to copy meeting url on prejoin page. * Component used to copy meeting url on prejoin page.
*/ */
class CopyMeetingUrl extends Component<Props, State> { class CopyMeetingUrl extends Component<Props> {
/**
* Initializes a new {@code Prejoin} instance.
*
* @inheritdoc
*/
constructor(props) {
super(props);
this.state = {
showCopyLink: false,
showLinkCopied: false
};
this._copyUrl = this._copyUrl.bind(this);
this._hideCopyLink = this._hideCopyLink.bind(this);
this._hideLinkCopied = this._hideLinkCopied.bind(this);
this._showCopyLink = this._showCopyLink.bind(this);
this._showLinkCopied = this._showLinkCopied.bind(this);
this._copyUrlAutomatically = this._copyUrlAutomatically.bind(this);
}
_copyUrl: () => void;
/**
* Callback invoked to copy the url to clipboard.
*
* @returns {void}
*/
async _copyUrl() {
const success = await copyText(this.props.url);
if (success) {
this._showLinkCopied();
window.setTimeout(this._hideLinkCopied, COPY_TIMEOUT);
}
}
_hideLinkCopied: () => void;
/**
* Hides the 'Link copied' message.
*
* @private
* @returns {void}
*/
_hideLinkCopied() {
this.setState({
showLinkCopied: false
});
}
_hideCopyLink: () => void;
/**
* Hides the 'Copy link' text.
*
* @private
* @returns {void}
*/
_hideCopyLink() {
this.setState({
showCopyLink: false,
showLinkCopied: false
});
}
_showCopyLink: () => void;
/**
* Shows the dark 'Copy link' text on hover.
*
* @private
* @returns {void}
*/
_showCopyLink() {
this.setState({
showCopyLink: true,
showLinkCopied: false
});
}
_showLinkCopied: () => void;
/**
* Shows the green 'Link copied' message.
*
* @private
* @returns {void}
*/
_showLinkCopied() {
this.setState({
showLinkCopied: true,
showCopyLink: false
});
}
_copyUrlAutomatically: () => void;
/**
* Attempts to automatically copy invitation URL.
* Document has to be focused in order for this to work.
*
* @private
* @returns {void}
*/
async _copyUrlAutomatically() {
const isCopied = await copyText(this.props.url);
if (isCopied) {
this._showLinkCopied();
window.setTimeout(this._hideLinkCopied, COPY_TIMEOUT);
}
}
/**
* Implements React's {@link Component#componentDidMount()}. Invoked
* immediately before mounting occurs.
*
* @inheritdoc
*/
componentDidMount() {
const { _enableAutomaticUrlCopy } = this.props;
if (_enableAutomaticUrlCopy) {
setTimeout(this._copyUrlAutomatically, 2000);
}
}
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
@ -181,29 +40,9 @@ class CopyMeetingUrl extends Component<Props, State> {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
render() { render() {
const { showCopyLink, showLinkCopied } = this.state;
const { url, t } = this.props;
const { _copyUrl, _showCopyLink, _hideCopyLink } = this;
const src = showLinkCopied ? IconCheck : IconCopy;
return ( return (
<div <div className = 'copy-meeting'>
className = 'copy-meeting' <CopyMeetingLinkSection url = { this.props.url } />
onMouseEnter = { _showCopyLink }
onMouseLeave = { _hideCopyLink }>
<div
className = { `url ${showLinkCopied ? 'done' : ''}` }
onClick = { _copyUrl } >
<div className = 'copy-meeting-text'>
{ !showCopyLink && !showLinkCopied && getDecodedURI(url) }
{ showCopyLink && t('prejoin.copyAndShare') }
{ showLinkCopied && t('prejoin.linkCopied') }
</div>
<Icon
onClick = { _copyUrl }
size = { 24 }
src = { src } />
</div>
</div> </div>
); );
} }

View File

@ -44,7 +44,9 @@ type Props = {
/** /**
* Externally provided value. * Externally provided value.
*/ */
value?: string value?: string,
id?: string,
autoComplete?: string
}; };
type State = { type State = {
@ -114,9 +116,11 @@ export default class InputField extends PureComponent<Props, State> {
render() { render() {
return ( return (
<input <input
autoComplete = { this.props.autoComplete }
autoFocus = { this.props.autoFocus } autoFocus = { this.props.autoFocus }
className = { `field ${this.state.focused ? 'focused' : ''} ${this.props.className || ''}` } className = { `field ${this.state.focused ? 'focused' : ''} ${this.props.className || ''}` }
data-testid = { this.props.testId ? this.props.testId : undefined } data-testid = { this.props.testId ? this.props.testId : undefined }
id = { this.props.id }
onBlur = { this._onBlur } onBlur = { this._onBlur }
onChange = { this._onChange } onChange = { this._onChange }
onFocus = { this._onFocus } onFocus = { this._onFocus }

View File

@ -108,9 +108,9 @@ export default class PreMeetingScreen extends PureComponent<Props> {
)} )}
{showConferenceInfo && ( {showConferenceInfo && (
<> <>
<div className = 'title'> <h1 className = 'title'>
{ title } { title }
</div> </h1>
{showSharingButton ? <CopyMeetingUrl /> : null} {showSharingButton ? <CopyMeetingUrl /> : null}
</> </>
)} )}

View File

@ -1,6 +1,6 @@
// @flow // @flow
import React from 'react'; import React, { useCallback } from 'react';
import { Icon, IconCheck } from '../../../icons'; import { Icon, IconCheck } from '../../../icons';
@ -32,10 +32,22 @@ type Props = {
function ToggleButton({ children, isToggled, onClick }: Props) { function ToggleButton({ children, isToggled, onClick }: Props) {
const className = isToggled ? `${mainClass} ${mainClass}--toggled` : mainClass; const className = isToggled ? `${mainClass} ${mainClass}--toggled` : mainClass;
const onKeyPressHandler = useCallback(e => {
if (onClick && (e.key === ' ')) {
e.preventDefault();
onClick();
}
}, [ onClick ]);
return ( return (
<div <div
aria-checked = { isToggled }
className = { className } className = { className }
onClick = { onClick }> id = 'toggle-button'
onClick = { onClick }
onKeyPress = { onKeyPressHandler }
role = 'switch'
tabIndex = { 0 }>
<div className = 'toggle-button-container'> <div className = 'toggle-button-container'>
<div className = 'toggle-button-icon-container'> <div className = 'toggle-button-icon-container'>
<Icon <Icon
@ -43,7 +55,7 @@ function ToggleButton({ children, isToggled, onClick }: Props) {
size = { 10 } size = { 10 }
src = { IconCheck } /> src = { IconCheck } />
</div> </div>
<span>{children}</span> <label htmlFor = 'toggle-button'>{children}</label>
</div> </div>
</div> </div>
); );

View File

@ -4,7 +4,8 @@ import React, { Component } from 'react';
import { import {
getLocalizedDateFormatter, getLocalizedDateFormatter,
getLocalizedDurationFormatter getLocalizedDurationFormatter,
translate
} from '../../../i18n'; } from '../../../i18n';
import { Icon, IconTrash } from '../../../icons'; import { Icon, IconTrash } from '../../../icons';
@ -41,7 +42,12 @@ type Props = {
/** /**
* Handler for deleting an item. * Handler for deleting an item.
*/ */
onItemDelete?: Function onItemDelete?: Function,
/**
* Invoked to obtain translated strings.
*/
t: Function
}; };
/** /**
@ -80,7 +86,7 @@ function _toTimeString(times) {
* *
* @extends Component * @extends Component
*/ */
export default class MeetingsList extends Component<Props> { class MeetingsList extends Component<Props> {
/** /**
* Constructor of the MeetingsList component. * Constructor of the MeetingsList component.
* *
@ -99,7 +105,7 @@ export default class MeetingsList extends Component<Props> {
* @returns {React.ReactNode} * @returns {React.ReactNode}
*/ */
render() { render() {
const { listEmptyComponent, meetings } = this.props; const { listEmptyComponent, meetings, t } = this.props;
/** /**
* If there are no recent meetings we don't want to display anything * If there are no recent meetings we don't want to display anything
@ -107,7 +113,10 @@ export default class MeetingsList extends Component<Props> {
if (meetings) { if (meetings) {
return ( return (
<Container <Container
className = 'meetings-list'> aria-label = { t('welcomepage.recentList') }
className = 'meetings-list'
role = 'menu'
tabIndex = '-1'>
{ {
meetings.length === 0 meetings.length === 0
? listEmptyComponent ? listEmptyComponent
@ -139,6 +148,29 @@ export default class MeetingsList extends Component<Props> {
return null; return null;
} }
_onKeyPress: string => Function;
/**
* Returns a function that is used in the onPress callback of the items.
*
* @param {string} url - The URL of the item to navigate to.
* @private
* @returns {Function}
*/
_onKeyPress(url) {
const { disabled, onPress } = this.props;
if (!disabled && url && typeof onPress === 'function') {
return e => {
if (e.key === ' ' || e.key === 'Enter') {
onPress(url);
}
};
}
return null;
}
_onDelete: Object => Function; _onDelete: Object => Function;
/** /**
@ -158,6 +190,27 @@ export default class MeetingsList extends Component<Props> {
}; };
} }
_onDeleteKeyPress: Object => Function;
/**
* Returns a function that is used on the onDelete keypress callback.
*
* @param {Object} item - The item to be deleted.
* @private
* @returns {Function}
*/
_onDeleteKeyPress(item) {
const { onItemDelete } = this.props;
return e => {
if (onItemDelete && (e.key === ' ' || e.key === 'Enter')) {
e.preventDefault();
e.stopPropagation();
onItemDelete(item);
}
};
}
_renderItem: (Object, number) => React$Node; _renderItem: (Object, number) => React$Node;
/** /**
@ -176,17 +229,22 @@ export default class MeetingsList extends Component<Props> {
title, title,
url url
} = meeting; } = meeting;
const { hideURL = false, onItemDelete } = this.props; const { hideURL = false, onItemDelete, t } = this.props;
const onPress = this._onPress(url); const onPress = this._onPress(url);
const onKeyPress = this._onKeyPress(url);
const rootClassName const rootClassName
= `item ${ = `item ${
onPress ? 'with-click-handler' : 'without-click-handler'}`; onPress ? 'with-click-handler' : 'without-click-handler'}`;
return ( return (
<Container <Container
aria-label = { title }
className = { rootClassName } className = { rootClassName }
key = { index } key = { index }
onClick = { onPress }> onClick = { onPress }
onKeyPress = { onKeyPress }
role = 'menuitem'
tabIndex = { 0 }>
<Container className = 'left-column'> <Container className = 'left-column'>
<Text className = 'title'> <Text className = 'title'>
{ _toDateString(date) } { _toDateString(date) }
@ -216,11 +274,17 @@ export default class MeetingsList extends Component<Props> {
{ elementAfter || null } { elementAfter || null }
{ onItemDelete && <Icon { onItemDelete && <Icon
ariaLabel = { t('welcomepage.recentListDelete') }
className = 'delete-meeting' className = 'delete-meeting'
onClick = { this._onDelete(meeting) } onClick = { this._onDelete(meeting) }
src = { IconTrash } />} onKeyPress = { this._onDeleteKeyPress(meeting) }
role = 'button'
src = { IconTrash }
tabIndex = { 0 } />}
</Container> </Container>
</Container> </Container>
); );
} }
} }
export default translate(MeetingsList);

View File

@ -160,13 +160,15 @@ class Watermarks extends Component<Props, State> {
_logoUrl, _logoUrl,
_showJitsiWatermark _showJitsiWatermark
} = this.props; } = this.props;
const { t } = this.props;
let reactElement = null; let reactElement = null;
if (_showJitsiWatermark) { if (_showJitsiWatermark) {
const style = { const style = {
backgroundImage: `url(${_logoUrl})`, backgroundImage: `url(${_logoUrl})`,
maxWidth: 140, maxWidth: 140,
maxHeight: 70 maxHeight: 70,
position: _logoLink ? 'static' : 'absolute'
}; };
reactElement = (<div reactElement = (<div
@ -176,6 +178,8 @@ class Watermarks extends Component<Props, State> {
if (_logoLink) { if (_logoLink) {
reactElement = ( reactElement = (
<a <a
aria-label = { t('jitsiHome', { logo: interfaceConfig.APP_NAME }) }
className = 'watermark leftwatermark'
href = { _logoLink } href = { _logoLink }
target = '_new'> target = '_new'>
{ reactElement } { reactElement }

View File

@ -246,14 +246,18 @@ export default class AbstractButton<P: Props, S: *> extends Component<P, S> {
* Handles clicking / pressing the button, and toggles the audio mute state * Handles clicking / pressing the button, and toggles the audio mute state
* accordingly. * accordingly.
* *
* @param {Object} e - Event.
* @private * @private
* @returns {void} * @returns {void}
*/ */
_onClick() { _onClick(e) {
const { afterClick } = this.props; const { afterClick } = this.props;
this._handleClick(); this._handleClick();
afterClick && afterClick(); afterClick && afterClick(e);
// blur after click to release focus from button to allow PTT.
e && e.currentTarget && e.currentTarget.blur();
} }
/** /**

View File

@ -6,6 +6,10 @@ import { translate } from '../../i18n';
import { Container, Text } from '../../react'; import { Container, Text } from '../../react';
type Props = { type Props = {
/**
* Invoked to obtain translated strings.
*/
t: Function t: Function
}; };

View File

@ -20,29 +20,21 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this._onKeyDown = this._onKeyDown.bind(this); this._onKeyPress = this._onKeyPress.bind(this);
} }
_onKeyDown: (Object) => void; _onKeyPress: (Object) => void;
/** /**
* Handles 'Enter' key on the button to trigger onClick for accessibility. * Handles 'Enter' and Space key on the button to trigger onClick for accessibility.
* We should be handling Space onKeyUp but it conflicts with PTT.
* *
* @param {Object} event - The key event. * @param {Object} event - The key event.
* @private * @private
* @returns {void} * @returns {void}
*/ */
_onKeyDown(event) { _onKeyPress(event) {
// If the event coming to the dialog has been subject to preventDefault if (event.key === 'Enter' || event.key === ' ') {
// we don't handle it here.
if (event.defaultPrevented) {
return;
}
if (event.key === 'Enter') {
event.preventDefault(); event.preventDefault();
event.stopPropagation();
this.props.onClick(); this.props.onClick();
} }
} }
@ -72,9 +64,9 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
'aria-label': this.accessibilityLabel, 'aria-label': this.accessibilityLabel,
className: className + (disabled ? ' disabled' : ''), className: className + (disabled ? ' disabled' : ''),
onClick: disabled ? undefined : onClick, onClick: disabled ? undefined : onClick,
onKeyDown: this._onKeyDown, onKeyPress: this._onKeyPress,
tabIndex: 0, tabIndex: 0,
role: 'button' role: showLabel ? 'menuitem' : 'button'
}; };
const elementType = showLabel ? 'li' : 'div'; const elementType = showLabel ? 'li' : 'div';

View File

@ -74,6 +74,35 @@ class OverflowMenuItem extends Component<Props> {
disabled: false disabled: false
}; };
/**
* Initializes a new {@code OverflowMenuItem} instance.
*
* @param {*} props - The read-only properties with which the new instance
* is to be initialized.
*/
constructor(props: Props) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onKeyPress = this._onKeyPress.bind(this);
}
_onKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onKeyPress(e) {
if (!this.props.disabled && this.props.onClick && (e.key === ' ' || e.key === 'Enter')) {
e.preventDefault();
this.props.onClick();
}
}
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
* *
@ -89,9 +118,13 @@ class OverflowMenuItem extends Component<Props> {
return ( return (
<li <li
aria-disabled = { disabled }
aria-label = { accessibilityLabel } aria-label = { accessibilityLabel }
className = { className } className = { className }
onClick = { disabled ? null : onClick }> onClick = { disabled ? null : onClick }
onKeyPress = { this._onKeyPress }
role = 'menuitem'
tabIndex = { 0 }>
<span className = 'overflow-menu-item-icon'> <span className = 'overflow-menu-item-icon'>
<Icon <Icon
id = { iconId } id = { iconId }

View File

@ -36,6 +36,36 @@ type Props = {
* Additional styles. * Additional styles.
*/ */
styles?: Object, styles?: Object,
/**
* aria label for the Icon.
*/
ariaLabel?: string,
/**
* whether the element has a popup
*/
ariaHasPopup?: boolean,
/**
* whether the element popup is expanded
*/
ariaExpanded?: boolean,
/**
* The id of the element this button icon controls
*/
ariaControls?: string,
/**
* keydown handler for icon.
*/
onIconKeyDown?: Function,
/**
* The ID of the icon button
*/
iconId: string
}; };
/** /**
@ -51,7 +81,13 @@ export default function ToolboxButtonWithIcon(props: Props) {
iconDisabled, iconDisabled,
iconTooltip, iconTooltip,
onIconClick, onIconClick,
styles onIconKeyDown,
styles,
ariaLabel,
ariaHasPopup,
ariaControls,
ariaExpanded,
iconId
} = props; } = props;
const iconProps = {}; const iconProps = {};
@ -62,6 +98,12 @@ export default function ToolboxButtonWithIcon(props: Props) {
} else { } else {
iconProps.className = 'settings-button-small-icon'; iconProps.className = 'settings-button-small-icon';
iconProps.onClick = onIconClick; iconProps.onClick = onIconClick;
iconProps.onKeyDown = onIconKeyDown;
iconProps.role = 'button';
iconProps.tabIndex = 0;
iconProps.ariaControls = ariaControls;
iconProps.ariaExpanded = ariaExpanded;
iconProps.containerId = iconId;
} }
@ -77,6 +119,8 @@ export default function ToolboxButtonWithIcon(props: Props) {
position = 'top'> position = 'top'>
<Icon <Icon
{ ...iconProps } { ...iconProps }
ariaHasPopup = { ariaHasPopup }
ariaLabel = { ariaLabel }
size = { 9 } size = { 9 }
src = { icon } /> src = { icon } />
</Tooltip> </Tooltip>

View File

@ -19,29 +19,21 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this._onKeyDown = this._onKeyDown.bind(this); this._onKeyPress = this._onKeyPress.bind(this);
} }
_onKeyDown: (Object) => void; _onKeyPress: (Object) => void;
/** /**
* Handles 'Enter' key on the button to trigger onClick for accessibility. * Handles 'Enter' and Space key on the button to trigger onClick for accessibility.
* We should be handling Space onKeyUp but it conflicts with PTT.
* *
* @param {Object} event - The key event. * @param {Object} event - The key event.
* @private * @private
* @returns {void} * @returns {void}
*/ */
_onKeyDown(event) { _onKeyPress(event) {
// If the event coming to the dialog has been subject to preventDefault if (event.key === 'Enter' || event.key === ' ') {
// we don't handle it here.
if (event.defaultPrevented) {
return;
}
if (event.key === 'Enter') {
event.preventDefault(); event.preventDefault();
event.stopPropagation();
this.props.onClick(); this.props.onClick();
} }
} }
@ -71,9 +63,9 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
'aria-label': this.accessibilityLabel, 'aria-label': this.accessibilityLabel,
className: className + (disabled ? ' disabled' : ''), className: className + (disabled ? ' disabled' : ''),
onClick: disabled ? undefined : onClick, onClick: disabled ? undefined : onClick,
onKeyDown: this._onKeyDown, onKeyPress: this._onKeyPress,
tabIndex: 0, tabIndex: 0,
role: 'button' role: showLabel ? 'menuitem' : 'button'
}; };
const elementType = showLabel ? 'li' : 'div'; const elementType = showLabel ? 'li' : 'div';

View File

@ -55,6 +55,7 @@ class AddMeetingUrlButton extends Component<Props> {
// Bind event handler so it is only bound once for every instance. // Bind event handler so it is only bound once for every instance.
this._onClick = this._onClick.bind(this); this._onClick = this._onClick.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
} }
/** /**
@ -67,7 +68,9 @@ class AddMeetingUrlButton extends Component<Props> {
<Tooltip content = { this.props.t('calendarSync.addMeetingURL') }> <Tooltip content = { this.props.t('calendarSync.addMeetingURL') }>
<div <div
className = 'button add-button' className = 'button add-button'
onClick = { this._onClick }> onClick = { this._onClick }
onKeyPress = { this._onKeyPress }
role = 'button'>
<Icon src = { IconAdd } /> <Icon src = { IconAdd } />
</div> </div>
</Tooltip> </Tooltip>
@ -88,6 +91,22 @@ class AddMeetingUrlButton extends Component<Props> {
dispatch(updateCalendarEvent(eventId, calendarId)); dispatch(updateCalendarEvent(eventId, calendarId));
} }
_onKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onClick();
}
}
} }
export default translate(connect()(AddMeetingUrlButton)); export default translate(connect()(AddMeetingUrlButton));

View File

@ -72,6 +72,7 @@ class CalendarList extends AbstractPage<Props> {
this._getRenderListEmptyComponent this._getRenderListEmptyComponent
= this._getRenderListEmptyComponent.bind(this); = this._getRenderListEmptyComponent.bind(this);
this._onOpenSettings = this._onOpenSettings.bind(this); this._onOpenSettings = this._onOpenSettings.bind(this);
this._onKeyPressOpenSettings = this._onKeyPressOpenSettings.bind(this);
this._onRefreshEvents = this._onRefreshEvents.bind(this); this._onRefreshEvents = this._onRefreshEvents.bind(this);
} }
@ -187,7 +188,9 @@ class CalendarList extends AbstractPage<Props> {
return ( return (
<div className = 'meetings-list-empty'> <div className = 'meetings-list-empty'>
<div className = 'meetings-list-empty-image'> <div className = 'meetings-list-empty-image'>
<img src = './images/calendar.svg' /> <img
alt = { t('welcomepage.logo.calendar') }
src = './images/calendar.svg' />
</div> </div>
<div className = 'description'> <div className = 'description'>
{ t('welcomepage.connectCalendarText', { { t('welcomepage.connectCalendarText', {
@ -197,7 +200,9 @@ class CalendarList extends AbstractPage<Props> {
</div> </div>
<div <div
className = 'meetings-list-empty-button' className = 'meetings-list-empty-button'
onClick = { this._onOpenSettings }> onClick = { this._onOpenSettings }
onKeyPress = { this._onKeyPressOpenSettings }
role = 'button'>
<Icon <Icon
className = 'meetings-list-empty-icon' className = 'meetings-list-empty-icon'
src = { IconPlusCalendar } /> src = { IconPlusCalendar } />
@ -221,6 +226,22 @@ class CalendarList extends AbstractPage<Props> {
this.props.dispatch(openSettingsDialog(SETTINGS_TABS.CALENDAR)); this.props.dispatch(openSettingsDialog(SETTINGS_TABS.CALENDAR));
} }
_onKeyPressOpenSettings: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onKeyPressOpenSettings(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onOpenSettings();
}
}
_onRefreshEvents: () => void; _onRefreshEvents: () => void;

View File

@ -45,6 +45,7 @@ class JoinButton extends Component<Props> {
// Bind event handler so it is only bound once for every instance. // Bind event handler so it is only bound once for every instance.
this._onClick = this._onClick.bind(this); this._onClick = this._onClick.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
} }
/** /**
@ -60,7 +61,9 @@ class JoinButton extends Component<Props> {
content = { t('calendarSync.joinTooltip') }> content = { t('calendarSync.joinTooltip') }>
<div <div
className = 'button join-button' className = 'button join-button'
onClick = { this._onClick }> onClick = { this._onClick }
onKeyPress = { this._onKeyPress }
role = 'button'>
<Icon <Icon
size = '14' size = '14'
src = { IconAdd } /> src = { IconAdd } />
@ -81,6 +84,22 @@ class JoinButton extends Component<Props> {
_onClick(event) { _onClick(event) {
this.props.onPress(event, this.props.url); this.props.onPress(event, this.props.url);
} }
_onKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onClick();
}
}
} }
export default translate(JoinButton); export default translate(JoinButton);

View File

@ -2,17 +2,29 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { translate } from '../../base/i18n';
/** /**
* The type of the React {@code Component} props of * The type of the React {@code Component} props of
* {@link MicrosoftSignInButton}. * {@link MicrosoftSignInButton}.
*/ */
type Props = { type Props = {
// The callback to invoke when {@code MicrosoftSignInButton} is clicked. /**
* The callback to invoke when {@code MicrosoftSignInButton} is clicked.
*/
onClick: Function, onClick: Function,
// The text to display within {@code MicrosoftSignInButton}. /**
text: string * The text to display within {@code MicrosoftSignInButton}.
*/
text: string,
/**
* Invoked to obtain translated strings.
*/
t: Function
}; };
/** /**
@ -20,7 +32,7 @@ type Props = {
* *
* @extends Component * @extends Component
*/ */
export default class MicrosoftSignInButton extends Component<Props> { class MicrosoftSignInButton extends Component<Props> {
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
* *
@ -33,6 +45,7 @@ export default class MicrosoftSignInButton extends Component<Props> {
className = 'microsoft-sign-in' className = 'microsoft-sign-in'
onClick = { this.props.onClick }> onClick = { this.props.onClick }>
<img <img
alt = { this.props.t('welcomepage.logo.microsoftLogo') }
className = 'microsoft-logo' className = 'microsoft-logo'
src = 'images/microsoftLogo.svg' /> src = 'images/microsoftLogo.svg' />
<div className = 'microsoft-cta'> <div className = 'microsoft-cta'>
@ -42,3 +55,5 @@ export default class MicrosoftSignInButton extends Component<Props> {
); );
} }
} }
export default translate(MicrosoftSignInButton);

View File

@ -4,6 +4,7 @@ import React from 'react';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { toggleChat } from '../../actions.web';
import AbstractChat, { import AbstractChat, {
_mapStateToProps, _mapStateToProps,
type Props type Props
@ -51,6 +52,8 @@ class Chat extends AbstractChat<Props> {
// Bind event handlers so they are only bound once for every instance. // Bind event handlers so they are only bound once for every instance.
this._renderPanelContent = this._renderPanelContent.bind(this); this._renderPanelContent = this._renderPanelContent.bind(this);
this._onChatInputResize = this._onChatInputResize.bind(this); this._onChatInputResize = this._onChatInputResize.bind(this);
this._onEscClick = this._onEscClick.bind(this);
this._onToggleChat = this._onToggleChat.bind(this);
} }
/** /**
@ -74,6 +77,21 @@ class Chat extends AbstractChat<Props> {
this._scrollMessageContainerToBottom(false); this._scrollMessageContainerToBottom(false);
} }
} }
_onEscClick: (KeyboardEvent) => void;
/**
* Click handler for the chat sidenav.
*
* @param {KeyboardEvent} event - Esc key click to close the popup.
* @returns {void}
*/
_onEscClick(event) {
if (event.key === 'Escape' && this.props._isOpen) {
event.preventDefault();
event.stopPropagation();
this._onToggleChat();
}
}
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
@ -135,7 +153,10 @@ class Chat extends AbstractChat<Props> {
*/ */
_renderChatHeader() { _renderChatHeader() {
return ( return (
<Header className = 'chat-header' /> <Header
className = 'chat-header'
id = 'chat-header'
onCancel = { this._onToggleChat } />
); );
} }
@ -177,8 +198,10 @@ class Chat extends AbstractChat<Props> {
return ( return (
<div <div
aria-haspopup = 'true'
className = { `sideToolbarContainer ${className}` } className = { `sideToolbarContainer ${className}` }
id = 'sideToolbarContainer'> id = 'sideToolbarContainer'
onKeyDown = { this._onEscClick } >
{ ComponentToRender } { ComponentToRender }
</div> </div>
); );
@ -199,6 +222,18 @@ class Chat extends AbstractChat<Props> {
} }
_onSendMessage: (string) => void; _onSendMessage: (string) => void;
_onToggleChat: () => void;
/**
* Toggles the chat window.
*
* @returns {Function}
*/
_onToggleChat() {
this.props.dispatch(toggleChat());
}
} }
export default translate(connect(_mapStateToProps)(Chat)); export default translate(connect(_mapStateToProps)(Chat));

View File

@ -1,7 +1,8 @@
// @flow // @flow
import React from 'react'; import React, { useCallback } from 'react';
import { translate } from '../../../base/i18n';
import { Icon, IconClose } from '../../../base/icons'; import { Icon, IconClose } from '../../../base/icons';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { toggleChat } from '../../actions.web'; import { toggleChat } from '../../actions.web';
@ -17,6 +18,11 @@ type Props = {
* An optional class name. * An optional class name.
*/ */
className: string, className: string,
/**
* Invoked to obtain translated strings.
*/
t: Function
}; };
/** /**
@ -24,17 +30,31 @@ type Props = {
* *
* @returns {React$Element<any>} * @returns {React$Element<any>}
*/ */
function Header({ onCancel, className }: Props) { function Header({ onCancel, className, t }: Props) {
const onKeyPressHandler = useCallback(e => {
if (onCancel && (e.key === ' ' || e.key === 'Enter')) {
e.preventDefault();
onCancel();
}
}, [ onCancel ]);
return ( return (
<div <div
className = { className || 'chat-dialog-header' }> className = { className || 'chat-dialog-header' }
role = 'heading'>
{ t('chat.title') }
<Icon <Icon
ariaLabel = { t('toolbar.closeChat') }
onClick = { onCancel } onClick = { onCancel }
src = { IconClose } /> onKeyPress = { onKeyPressHandler }
role = 'button'
src = { IconClose }
tabIndex = { 0 } />
</div> </div>
); );
} }
const mapDispatchToProps = { onCancel: toggleChat }; const mapDispatchToProps = { onCancel: toggleChat };
export default connect(null, mapDispatchToProps)(Header); export default translate(connect(null, mapDispatchToProps)(Header));

View File

@ -84,6 +84,9 @@ class ChatInput extends Component<Props, State> {
this._onSmileySelect = this._onSmileySelect.bind(this); this._onSmileySelect = this._onSmileySelect.bind(this);
this._onSubmitMessage = this._onSubmitMessage.bind(this); this._onSubmitMessage = this._onSubmitMessage.bind(this);
this._onToggleSmileysPanel = this._onToggleSmileysPanel.bind(this); this._onToggleSmileysPanel = this._onToggleSmileysPanel.bind(this);
this._onEscHandler = this._onEscHandler.bind(this);
this._onToggleSmileysPanelKeyPress = this._onToggleSmileysPanelKeyPress.bind(this);
this._onSubmitMessageKeyPress = this._onSubmitMessageKeyPress.bind(this);
this._setTextAreaRef = this._setTextAreaRef.bind(this); this._setTextAreaRef = this._setTextAreaRef.bind(this);
} }
@ -116,8 +119,15 @@ class ChatInput extends Component<Props, State> {
<div id = 'smileysarea'> <div id = 'smileysarea'>
<div id = 'smileys'> <div id = 'smileys'>
<div <div
aria-expanded = { this.state.showSmileysPanel }
aria-haspopup = 'smileysContainer'
aria-label = { this.props.t('chat.smileysPanel') }
className = 'smiley-button' className = 'smiley-button'
onClick = { this._onToggleSmileysPanel }> onClick = { this._onToggleSmileysPanel }
onKeyDown = { this._onEscHandler }
onKeyPress = { this._onToggleSmileysPanelKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon src = { IconSmile } /> <Icon src = { IconSmile } />
</div> </div>
</div> </div>
@ -129,19 +139,26 @@ class ChatInput extends Component<Props, State> {
</div> </div>
<div className = 'usrmsg-form'> <div className = 'usrmsg-form'>
<TextareaAutosize <TextareaAutosize
autoComplete = 'off'
autoFocus = { true }
id = 'usermsg' id = 'usermsg'
inputRef = { this._setTextAreaRef }
maxRows = { 5 } maxRows = { 5 }
onChange = { this._onMessageChange } onChange = { this._onMessageChange }
onHeightChange = { this.props.onResize } onHeightChange = { this.props.onResize }
onKeyDown = { this._onDetectSubmit } onKeyDown = { this._onDetectSubmit }
placeholder = { this.props.t('chat.messagebox') } placeholder = { this.props.t('chat.messagebox') }
ref = { this._setTextAreaRef }
tabIndex = { 0 }
value = { this.state.message } /> value = { this.state.message } />
</div> </div>
<div className = 'send-button-container'> <div className = 'send-button-container'>
<div <div
aria-label = { this.props.t('chat.sendButton') }
className = 'send-button' className = 'send-button'
onClick = { this._onSubmitMessage }> onClick = { this._onSubmitMessage }
onKeyPress = { this._onSubmitMessageKeyPress }
role = 'button'
tabIndex = { this.state.message.trim() ? 0 : -1 } >
<Icon src = { IconPlane } /> <Icon src = { IconPlane } />
</div> </div>
</div> </div>
@ -192,14 +209,32 @@ class ChatInput extends Component<Props, State> {
* @returns {void} * @returns {void}
*/ */
_onDetectSubmit(event) { _onDetectSubmit(event) {
if (event.keyCode === 13 if (event.key === 'Enter'
&& event.shiftKey === false) { && event.shiftKey === false
&& event.ctrlKey === false) {
event.preventDefault(); event.preventDefault();
event.stopPropagation();
this._onSubmitMessage(); this._onSubmitMessage();
} }
} }
_onSubmitMessageKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onSubmitMessageKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onSubmitMessage();
}
}
_onMessageChange: (Object) => void; _onMessageChange: (Object) => void;
/** /**
@ -224,10 +259,16 @@ class ChatInput extends Component<Props, State> {
* @returns {void} * @returns {void}
*/ */
_onSmileySelect(smileyText) { _onSmileySelect(smileyText) {
this.setState({ if (smileyText) {
message: `${this.state.message} ${smileyText}`, this.setState({
showSmileysPanel: false message: `${this.state.message} ${smileyText}`,
}); showSmileysPanel: false
});
} else {
this.setState({
showSmileysPanel: false
});
}
this._focus(); this._focus();
} }
@ -241,9 +282,44 @@ class ChatInput extends Component<Props, State> {
* @returns {void} * @returns {void}
*/ */
_onToggleSmileysPanel() { _onToggleSmileysPanel() {
if (this.state.showSmileysPanel) {
this._focus();
}
this.setState({ showSmileysPanel: !this.state.showSmileysPanel }); this.setState({ showSmileysPanel: !this.state.showSmileysPanel });
}
this._focus(); _onEscHandler: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onEscHandler(e) {
// Escape handling does not work in onKeyPress
if (this.state.showSmileysPanel && e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
this._onToggleSmileysPanel();
}
}
_onToggleSmileysPanelKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onToggleSmileysPanelKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onToggleSmileysPanel();
}
} }
_setTextAreaRef: (?HTMLTextAreaElement) => void; _setTextAreaRef: (?HTMLTextAreaElement) => void;

View File

@ -23,7 +23,7 @@ class ChatMessage extends AbstractChatMessage<Props> {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
render() { render() {
const { message } = this.props; const { message, t } = this.props;
const processedMessage = []; const processedMessage = [];
// content is an array of text and emoji components // content is an array of text and emoji components
@ -38,12 +38,20 @@ class ChatMessage extends AbstractChatMessage<Props> {
}); });
return ( return (
<div className = 'chatmessage-wrapper'> <div
className = 'chatmessage-wrapper'
tabIndex = { -1 }>
<div className = { `chatmessage ${message.privateMessage ? 'privatemessage' : ''}` }> <div className = { `chatmessage ${message.privateMessage ? 'privatemessage' : ''}` }>
<div className = 'replywrapper'> <div className = 'replywrapper'>
<div className = 'messagecontent'> <div className = 'messagecontent'>
{ this.props.showDisplayName && this._renderDisplayName() } { this.props.showDisplayName && this._renderDisplayName() }
<div className = 'usermessage'> <div className = 'usermessage'>
<span className = 'sr-only'>
{ this.props.message.displayName === this.props.message.recipient
? t('chat.messageAccessibleTitleMe')
: t('chat.messageAccessibleTitle',
{ user: this.props.message.displayName }) }
</span>
{ processedMessage } { processedMessage }
</div> </div>
{ message.privateMessage && this._renderPrivateNotice() } { message.privateMessage && this._renderPrivateNotice() }
@ -77,7 +85,9 @@ class ChatMessage extends AbstractChatMessage<Props> {
*/ */
_renderDisplayName() { _renderDisplayName() {
return ( return (
<div className = 'display-name'> <div
aria-hidden = { true }
className = 'display-name'>
{ this.props.message.displayName } { this.props.message.displayName }
</div> </div>
); );

View File

@ -59,6 +59,7 @@ class DisplayNameForm extends Component<Props, State> {
// Bind event handlers so they are only bound once for every instance. // Bind event handlers so they are only bound once for every instance.
this._onDisplayNameChange = this._onDisplayNameChange.bind(this); this._onDisplayNameChange = this._onDisplayNameChange.bind(this);
this._onSubmit = this._onSubmit.bind(this); this._onSubmit = this._onSubmit.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
} }
/** /**
@ -74,6 +75,8 @@ class DisplayNameForm extends Component<Props, State> {
<div id = 'nickname'> <div id = 'nickname'>
<form onSubmit = { this._onSubmit }> <form onSubmit = { this._onSubmit }>
<FieldTextStateless <FieldTextStateless
aria-describedby = 'nickname-title'
autoComplete = 'name'
autoFocus = { true } autoFocus = { true }
compact = { true } compact = { true }
id = 'nickinput' id = 'nickinput'
@ -86,7 +89,10 @@ class DisplayNameForm extends Component<Props, State> {
</form> </form>
<div <div
className = { `enter-chat${this.state.displayName.trim() ? '' : ' disabled'}` } className = { `enter-chat${this.state.displayName.trim() ? '' : ' disabled'}` }
onClick = { this._onSubmit }> onClick = { this._onSubmit }
onKeyPress = { this._onKeyPress }
role = 'button'
tabIndex = { 0 }>
{ t('chat.enter') } { t('chat.enter') }
</div> </div>
<KeyboardAvoider /> <KeyboardAvoider />
@ -125,6 +131,21 @@ class DisplayNameForm extends Component<Props, State> {
displayName: this.state.displayName displayName: this.state.displayName
})); }));
} }
_onKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
this._onSubmit(e);
}
}
} }
export default translate(connect()(DisplayNameForm)); export default translate(connect()(DisplayNameForm));

View File

@ -70,9 +70,12 @@ export default class MessageContainer extends AbstractMessageContainer<Props> {
return ( return (
<div <div
aria-labelledby = 'chat-header'
id = 'chatconversation' id = 'chatconversation'
onScroll = { this._onChatScroll } onScroll = { this._onChatScroll }
ref = { this._messageListRef }> ref = { this._messageListRef }
role = 'log'
tabIndex = { 0 }>
{ messages } { messages }
<div ref = { this._messagesListEndRef } /> <div ref = { this._messagesListEndRef } />
</div> </div>

View File

@ -15,6 +15,35 @@ import AbstractMessageRecipient, {
* Class to implement the displaying of the recipient of the next message. * Class to implement the displaying of the recipient of the next message.
*/ */
class MessageRecipient extends AbstractMessageRecipient<Props> { class MessageRecipient extends AbstractMessageRecipient<Props> {
/**
* Initializes a new {@code MessageRecipient} instance.
*
* @param {*} props - The read-only properties with which the new instance
* is to be initialized.
*/
constructor(props) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onKeyPress = this._onKeyPress.bind(this);
}
_onKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onKeyPress(e) {
if (this.props._onRemovePrivateMessageRecipient && (e.key === ' ' || e.key === 'Enter')) {
e.preventDefault();
this.props._onRemovePrivateMessageRecipient();
}
}
/** /**
* Implements {@code PureComponent#render}. * Implements {@code PureComponent#render}.
* *
@ -30,13 +59,20 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
const { t } = this.props; const { t } = this.props;
return ( return (
<div id = 'chat-recipient'> <div
id = 'chat-recipient'
role = 'alert'>
<span> <span>
{ t('chat.messageTo', { { t('chat.messageTo', {
recipient: _privateMessageRecipient recipient: _privateMessageRecipient
}) } }) }
</span> </span>
<div onClick = { this.props._onRemovePrivateMessageRecipient }> <div
aria-label = { t('dialog.close') }
onClick = { this.props._onRemovePrivateMessageRecipient }
onKeyPress = { this._onKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon <Icon
src = { IconCancelSelection } /> src = { IconCancelSelection } />
</div> </div>

View File

@ -23,6 +23,69 @@ type Props = {
* @extends Component * @extends Component
*/ */
class SmileysPanel extends PureComponent<Props> { class SmileysPanel extends PureComponent<Props> {
/**
* Initializes a new {@code SmileysPanel} instance.
*
* @param {*} props - The read-only properties with which the new instance
* is to be initialized.
*/
constructor(props: Props) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onClick = this._onClick.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
this._onEscKey = this._onEscKey.bind(this);
}
_onEscKey: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onEscKey(e) {
// Escape handling does not work in onKeyPress
if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
this.props.onSmileySelect();
}
}
_onKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onKeyPress(e) {
if (e.key === ' ') {
e.preventDefault();
this.props.onSmileySelect(e.target.id && smileys[e.target.id]);
}
}
_onClick: (Object) => void;
/**
* Click handler for to select emoji.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onClick(e) {
e.preventDefault();
this.props.onSmileySelect(e.currentTarget.id && smileys[e.currentTarget.id]);
}
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
* *
@ -30,40 +93,33 @@ class SmileysPanel extends PureComponent<Props> {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
render() { render() {
const smileyItems = Object.keys(smileys).map(smileyKey => { const smileyItems = Object.keys(smileys).map(smileyKey => (
const onSelectFunction = this._getOnSmileySelectCallback(smileyKey); <div
className = 'smileyContainer'
return ( id = { smileyKey }
<div key = { smileyKey }
className = 'smileyContainer' onClick = { this._onClick }
id = { smileyKey } onKeyDown = { this._onEscKey }
key = { smileyKey }> onKeyPress = { this._onKeyPress }
<Emoji role = 'option'
onClick = { onSelectFunction } tabIndex = { 0 }>
onlyEmojiClassName = 'smiley' <Emoji
text = { smileys[smileyKey] } /> onlyEmojiClassName = 'smiley'
</div> text = { smileys[smileyKey] } />
); </div>
}); ));
return ( return (
<div id = 'smileysContainer'> <div
aria-orientation = 'horizontal'
id = 'smileysContainer'
onKeyDown = { this._onEscKey }
role = 'listbox'
tabIndex = { -1 }>
{ smileyItems } { smileyItems }
</div> </div>
); );
} }
/**
* Helper method to bind a smiley's click handler.
*
* @param {string} smileyKey - The key from the {@link smileys} object
* that should be added to the chat message.
* @private
* @returns {Function}
*/
_getOnSmileySelectCallback(smileyKey) {
return () => this.props.onSmileySelect(smileys[smileyKey]);
}
} }
export default SmileysPanel; export default SmileysPanel;

View File

@ -107,6 +107,8 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
this._onInstallExtensionClick = this._onInstallExtensionClick.bind(this); this._onInstallExtensionClick = this._onInstallExtensionClick.bind(this);
this._shouldNotRender = this._shouldNotRender.bind(this); this._shouldNotRender = this._shouldNotRender.bind(this);
this._onDontShowAgainChange = this._onDontShowAgainChange.bind(this); this._onDontShowAgainChange = this._onDontShowAgainChange.bind(this);
this._onCloseKeyPress = this._onCloseKeyPress.bind(this);
this._onInstallExtensionKeyPress = this._onInstallExtensionKeyPress.bind(this);
} }
/** /**
@ -169,6 +171,22 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
this.setState({ closePressed: true }); this.setState({ closePressed: true });
} }
_onCloseKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onCloseKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onClosePressed();
}
}
_onInstallExtensionClick: () => void; _onInstallExtensionClick: () => void;
/** /**
@ -182,6 +200,22 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
this.setState({ closePressed: true }); this.setState({ closePressed: true });
} }
_onInstallExtensionKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onInstallExtensionKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onClosePressed();
}
}
_shouldNotRender: () => boolean; _shouldNotRender: () => boolean;
/** /**
@ -236,16 +270,23 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
return ( return (
<div className = { mainClassNames }> <div className = { mainClassNames }>
<div className = 'chrome-extension-banner__container'> <div
aria-aria-describedby = 'chrome-extension-banner__text-container'
className = 'chrome-extension-banner__container'
role = 'banner'>
<div className = 'chrome-extension-banner__icon-container' />
<div <div
className = 'chrome-extension-banner__icon-container' /> className = 'chrome-extension-banner__text-container'
<div id = 'chrome-extension-banner__text-container'>
className = 'chrome-extension-banner__text-container'>
{ t('chromeExtensionBanner.installExtensionText') } { t('chromeExtensionBanner.installExtensionText') }
</div> </div>
<div <div
aria-label = { t('chromeExtensionBanner.close') }
className = 'chrome-extension-banner__close-container' className = 'chrome-extension-banner__close-container'
onClick = { this._onClosePressed }> onClick = { this._onClosePressed }
onKeyPress = { this._onCloseKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon <Icon
className = 'gray' className = 'gray'
size = { 12 } size = { 12 }
@ -255,18 +296,28 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
<div <div
className = 'chrome-extension-banner__button-container'> className = 'chrome-extension-banner__button-container'>
<div <div
aria-labelledby = 'chrome-extension-banner__button-text'
className = 'chrome-extension-banner__button-open-url' className = 'chrome-extension-banner__button-open-url'
onClick = { this._onInstallExtensionClick }> onClick = { this._onInstallExtensionClick }
onKeyPress = { this._onInstallExtensionKeyPress }
role = 'button'
tabIndex = { 0 }>
<div <div
className = 'chrome-extension-banner__button-text'> className = 'chrome-extension-banner__button-text'
id = 'chrome-extension-banner__button-text'>
{ t('chromeExtensionBanner.buttonText') } { t('chromeExtensionBanner.buttonText') }
</div> </div>
</div> </div>
</div> </div>
<div className = 'chrome-extension-banner__checkbox-container'> <div className = 'chrome-extension-banner__checkbox-container'>
<label className = 'chrome-extension-banner__checkbox-label'> <label
className = 'chrome-extension-banner__checkbox-label'
htmlFor = 'chrome-extension-banner__checkbox'
id = 'chrome-extension-banner__checkbox-label'>
<input <input
aria-labelledby = 'chrome-extension-banner__checkbox-label'
checked = { this.state.dontShowAgainChecked } checked = { this.state.dontShowAgainChecked }
id = 'chrome-extension-banner__checkbox'
onChange = { this._onDontShowAgainChange } onChange = { this._onDontShowAgainChange }
type = 'checkbox' /> type = 'checkbox' />
&nbsp;{ t('chromeExtensionBanner.dontShowAgain') } &nbsp;{ t('chromeExtensionBanner.dontShowAgain') }

View File

@ -1,6 +1,6 @@
// @flow // @flow
import React from 'react'; import React, { useCallback } from 'react';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { Icon, IconInviteMore } from '../../../base/icons'; import { Icon, IconInviteMore } from '../../../base/icons';
@ -47,16 +47,28 @@ function InviteMore({
onClick, onClick,
t t
}: Props) { }: Props) {
const onKeyPressHandler = useCallback(e => {
if (onClick && (e.key === ' ' || e.key === 'Enter')) {
e.preventDefault();
onClick();
}
}, [ onClick ]);
return ( return (
_shouldShow _shouldShow
? <div className = { `invite-more-container${_toolboxVisible ? '' : ' elevated'}` }> ? <div className = { `invite-more-container${_toolboxVisible ? '' : ' elevated'}` }>
<div className = 'invite-more-content'> <div className = 'invite-more-content'>
<div className = 'invite-more-header'> <div
className = 'invite-more-header'
role = 'heading'>
{t('addPeople.inviteMoreHeader')} {t('addPeople.inviteMoreHeader')}
</div> </div>
<div <div
className = 'invite-more-button' className = 'invite-more-button'
onClick = { onClick }> onClick = { onClick }
onKeyPress = { onKeyPressHandler }
role = 'button'
tabIndex = { 0 }>
<Icon src = { IconInviteMore } /> <Icon src = { IconInviteMore } />
<div className = 'invite-more-button-text'> <div className = 'invite-more-button-text'>
{t('addPeople.inviteMorePrompt')} {t('addPeople.inviteMorePrompt')}

View File

@ -572,7 +572,9 @@ class ConnectionStatsTable extends Component<Props> {
<span> <span>
<a <a
className = 'savelogs link' className = 'savelogs link'
onClick = { this.props.onSaveLogs } > onClick = { this.props.onSaveLogs }
role = 'button'
tabIndex = { 0 }>
{ this.props.t('connectionindicator.savelogs') } { this.props.t('connectionindicator.savelogs') }
</a> </a>
<span> | </span> <span> | </span>
@ -597,7 +599,9 @@ class ConnectionStatsTable extends Component<Props> {
return ( return (
<a <a
className = 'showmore link' className = 'showmore link'
onClick = { this.props.onShowMore } > onClick = { this.props.onShowMore }
role = 'button'
tabIndex = { 0 }>
{ this.props.t(translationKey) } { this.props.t(translationKey) }
</a> </a>
); );

View File

@ -87,6 +87,7 @@ class DeepLinkingDesktopPage<P : Props> extends Component<P> {
HIDE_DEEP_LINKING_LOGO HIDE_DEEP_LINKING_LOGO
? null ? null
: <img : <img
alt = { t('welcomepage.logo.logoDeepLinking') }
className = 'logo' className = 'logo'
src = 'images/logo-deep-linking.png' /> src = 'images/logo-deep-linking.png' />
} }

View File

@ -119,6 +119,7 @@ class DeepLinkingMobilePage extends Component<Props> {
HIDE_DEEP_LINKING_LOGO HIDE_DEEP_LINKING_LOGO
? null ? null
: <img : <img
alt = { t('welcomepage.logo.logoDeepLinking') }
className = 'logo' className = 'logo'
src = 'images/logo-deep-linking.png' /> src = 'images/logo-deep-linking.png' />
} }
@ -127,6 +128,7 @@ class DeepLinkingMobilePage extends Component<Props> {
{ {
SHOW_DEEP_LINKING_IMAGE SHOW_DEEP_LINKING_IMAGE
? <img ? <img
alt = { t('welcomepage.logo.logoDeepLinking') }
className = 'image' className = 'image'
src = 'images/deep-linking-image.png' /> src = 'images/deep-linking-image.png' />
: null : null

View File

@ -2,6 +2,9 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { translate } from '../../base/i18n';
/** /**
* The type of the React {@code Component} props of * The type of the React {@code Component} props of
* {@link DesktopSourcePreview}. * {@link DesktopSourcePreview}.
@ -35,7 +38,12 @@ type Props = {
/** /**
* The source type of the DesktopCapturerSources to display. * The source type of the DesktopCapturerSources to display.
*/ */
type: string type: string,
/**
* Invoked to obtain translated strings.
*/
t: Function
}; };
/** /**
@ -74,6 +82,7 @@ class DesktopSourcePreview extends Component<Props> {
onDoubleClick = { this._onDoubleClick }> onDoubleClick = { this._onDoubleClick }>
<div className = 'desktop-source-preview-image-container'> <div className = 'desktop-source-preview-image-container'>
<img <img
alt = { this.props.t('welcomepage.logo.desktopPreviewThumbnail') }
className = 'desktop-source-preview-thumbnail' className = 'desktop-source-preview-thumbnail'
src = { this.props.source.thumbnail.toDataURL() } /> src = { this.props.source.thumbnail.toDataURL() } />
</div> </div>
@ -111,4 +120,4 @@ class DesktopSourcePreview extends Component<Props> {
} }
} }
export default DesktopSourcePreview; export default translate(DesktopSourcePreview);

View File

@ -44,6 +44,7 @@ class AudioOutputPreview extends Component<Props> {
this._audioElementReady = this._audioElementReady.bind(this); this._audioElementReady = this._audioElementReady.bind(this);
this._onClick = this._onClick.bind(this); this._onClick = this._onClick.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
} }
/** /**
@ -66,7 +67,12 @@ class AudioOutputPreview extends Component<Props> {
render() { render() {
return ( return (
<div className = 'audio-output-preview'> <div className = 'audio-output-preview'>
<a onClick = { this._onClick }> <a
aria-label = { this.props.t('deviceSelection.testAudio') }
onClick = { this._onClick }
onKeyPress = { this._onKeyPress }
role = 'button'
tabIndex = { 0 }>
{ this.props.t('deviceSelection.testAudio') } { this.props.t('deviceSelection.testAudio') }
</a> </a>
<Audio <Audio
@ -105,6 +111,22 @@ class AudioOutputPreview extends Component<Props> {
&& this._audioElement.play(); && this._audioElement.play();
} }
_onKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onClick();
}
}
/** /**
* Updates the target output device for playing the test sound. * Updates the target output device for playing the test sound.
* *

View File

@ -232,7 +232,9 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
track = { this.state.previewAudioTrack } /> } track = { this.state.previewAudioTrack } /> }
</div> </div>
<div className = 'device-selection-column column-selectors'> <div className = 'device-selection-column column-selectors'>
<div className = 'device-selectors'> <div
aria-live = 'polite all'
className = 'device-selectors'>
{ this._renderSelectors() } { this._renderSelectors() }
</div> </div>
{ !hideAudioOutputSelect { !hideAudioOutputSelect
@ -344,9 +346,11 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
_renderSelector(deviceSelectorProps) { _renderSelector(deviceSelectorProps) {
return ( return (
<div key = { deviceSelectorProps.label }> <div key = { deviceSelectorProps.label }>
<div className = 'device-selector-label'> <label
className = 'device-selector-label'
htmlFor = { deviceSelectorProps.id }>
{ this.props.t(deviceSelectorProps.label) } { this.props.t(deviceSelectorProps.label) }
</div> </label>
<DeviceSelector { ...deviceSelectorProps } /> <DeviceSelector { ...deviceSelectorProps } />
</div> </div>
); );
@ -370,6 +374,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
isDisabled: this.props.disableAudioInputChange isDisabled: this.props.disableAudioInputChange
|| this.props.disableDeviceChange, || this.props.disableDeviceChange,
key: 'audioInput', key: 'audioInput',
id: 'audioInput',
label: 'settings.selectMic', label: 'settings.selectMic',
onSelect: selectedAudioInputId => onSelect: selectedAudioInputId =>
super._onChange({ selectedAudioInputId }), super._onChange({ selectedAudioInputId }),
@ -385,6 +390,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
icon: 'icon-camera', icon: 'icon-camera',
isDisabled: this.props.disableDeviceChange, isDisabled: this.props.disableDeviceChange,
key: 'videoInput', key: 'videoInput',
id: 'videoInput',
label: 'settings.selectCamera', label: 'settings.selectCamera',
onSelect: selectedVideoInputId => onSelect: selectedVideoInputId =>
super._onChange({ selectedVideoInputId }), super._onChange({ selectedVideoInputId }),
@ -400,6 +406,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
icon: 'icon-speaker', icon: 'icon-speaker',
isDisabled: this.props.disableDeviceChange, isDisabled: this.props.disableDeviceChange,
key: 'audioOutput', key: 'audioOutput',
id: 'audioOutput',
label: 'settings.selectAudioOutput', label: 'settings.selectAudioOutput',
onSelect: selectedAudioOutputId => onSelect: selectedAudioOutputId =>
super._onChange({ selectedAudioOutputId }), super._onChange({ selectedAudioOutputId }),

View File

@ -51,7 +51,12 @@ type Props = {
/** /**
* Invoked to obtain translated strings. * Invoked to obtain translated strings.
*/ */
t: Function t: Function,
/**
* The id of the dropdown element
*/
id: string
}; };
/** /**
@ -81,6 +86,10 @@ class DeviceSelector extends Component<Props> {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
render() { render() {
if (this.props.hasPermission === undefined) {
return null;
}
if (!this.props.hasPermission) { if (!this.props.hasPermission) {
return this._renderNoPermission(); return this._renderNoPermission();
} }
@ -134,14 +143,10 @@ class DeviceSelector extends Component<Props> {
_createDropdownItem(device) { _createDropdownItem(device) {
return ( return (
<DropdownItem <DropdownItem
data-deviceid = { device.deviceId }
isSelected = { device.deviceId === this.props.selectedDeviceId }
key = { device.deviceId } key = { device.deviceId }
// eslint-disable-next-line react/jsx-no-bind onClick = { this._onSelect }>
onClick = {
e => {
e.stopPropagation();
this._onSelect(device.deviceId);
}
}>
{ device.label || device.deviceId } { device.label || device.deviceId }
</DropdownItem> </DropdownItem>
); );
@ -183,7 +188,8 @@ class DeviceSelector extends Component<Props> {
shouldFitContainer = { true } shouldFitContainer = { true }
trigger = { triggerText } trigger = { triggerText }
triggerButtonProps = {{ triggerButtonProps = {{
shouldFitContainer: true shouldFitContainer: true,
id: this.props.id
}} }}
triggerType = 'button'> triggerType = 'button'>
<DropdownItemGroup> <DropdownItemGroup>
@ -199,13 +205,16 @@ class DeviceSelector extends Component<Props> {
/** /**
* Invokes the passed in callback to notify of selection changes. * Invokes the passed in callback to notify of selection changes.
* *
* @param {Object} newDeviceId - Selected device id from DropdownMenu option. * @param {Object} e - The key event to handle.
*
* @private * @private
* @returns {void} * @returns {void}
*/ */
_onSelect(newDeviceId) { _onSelect(e) {
if (this.props.selectedDeviceId !== newDeviceId) { const deviceId = e.currentTarget.getAttribute('data-deviceid');
this.props.onSelect(newDeviceId);
if (this.props.selectedDeviceId !== deviceId) {
this.props.onSelect(deviceId);
} }
} }

View File

@ -85,6 +85,7 @@ class E2EESection extends Component<Props, State> {
// Bind event handlers so they are only bound once for every instance. // Bind event handlers so they are only bound once for every instance.
this._onExpand = this._onExpand.bind(this); this._onExpand = this._onExpand.bind(this);
this._onExpandKeyPress = this._onExpandKeyPress.bind(this);
this._onToggle = this._onToggle.bind(this); this._onToggle = this._onToggle.bind(this);
} }
@ -101,12 +102,20 @@ class E2EESection extends Component<Props, State> {
return ( return (
<div id = 'e2ee-section'> <div id = 'e2ee-section'>
<p className = 'description'> <p
aria-live = 'polite'
className = 'description'
id = 'e2ee-section-description'>
{ expand && description } { expand && description }
{ !expand && description.substring(0, 100) } { !expand && description.substring(0, 100) }
{ !expand && <span { !expand && <span
aria-controls = 'e2ee-section-description'
aria-expanded = { expand }
className = 'read-more' className = 'read-more'
onClick = { this._onExpand }> onClick = { this._onExpand }
onKeyPress = { this._onExpandKeyPress }
role = 'button'
tabIndex = { 0 }>
... { t('dialog.readMore') } ... { t('dialog.readMore') }
</span> } </span> }
</p> </p>
@ -142,6 +151,22 @@ class E2EESection extends Component<Props, State> {
}); });
} }
_onExpandKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onExpandKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onExpand();
}
}
_onToggle: () => void; _onToggle: () => void;
/** /**

View File

@ -44,10 +44,12 @@ function EmbedMeeting({ t, url }: Props) {
width = 'small'> width = 'small'>
<div className = 'embed-meeting-dialog'> <div className = 'embed-meeting-dialog'>
<textarea <textarea
aria-label = { t('dialog.embedMeeting') }
className = 'embed-meeting-code' className = 'embed-meeting-code'
readOnly = { true } readOnly = { true }
value = { getEmbedCode() } /> value = { getEmbedCode() } />
<CopyButton <CopyButton
aria-label = { t('addPeople.copyLink') }
className = 'embed-meeting-copy' className = 'embed-meeting-copy'
displayedText = { t('dialog.copy') } displayedText = { t('dialog.copy') }
textOnCopySuccess = { t('dialog.copied') } textOnCopySuccess = { t('dialog.copied') }

View File

@ -36,10 +36,28 @@ function EmbedMeetingTrigger({ t, openEmbedDialog }: Props) {
openEmbedDialog(EmbedMeetingDialog); openEmbedDialog(EmbedMeetingDialog);
} }
/**
* KeyPress handler for accessibility.
*
* @param {React.KeyboardEventHandler<HTMLDivElement>} e - The key event to handle.
*
* @returns {void}
*/
function onKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
onClick();
}
}
return ( return (
<div <div
aria-label = { t('embedMeeting.title') }
className = 'embed-meeting-trigger' className = 'embed-meeting-trigger'
onClick = { onClick }> onClick = { onClick }
onKeyPress = { onKeyPress }
role = 'button'
tabIndex = { 0 }>
{t('embedMeeting.title')} {t('embedMeeting.title')}
</div> </div>
); );

View File

@ -154,6 +154,12 @@ class FeedbackDialog extends Component<Props, State> {
this._scoreClickConfigurations = SCORES.map((textKey, index) => { this._scoreClickConfigurations = SCORES.map((textKey, index) => {
return { return {
_onClick: () => this._onScoreSelect(index), _onClick: () => this._onScoreSelect(index),
_onKeyPres: e => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onScoreSelect(index);
}
},
_onMouseOver: () => this._onScoreMouseOver(index) _onMouseOver: () => this._onScoreMouseOver(index)
}; };
}); });
@ -200,6 +206,8 @@ class FeedbackDialog extends Component<Props, State> {
const scoreToDisplayAsSelected const scoreToDisplayAsSelected
= mousedOverScore > -1 ? mousedOverScore : score; = mousedOverScore > -1 ? mousedOverScore : score;
const { t } = this.props;
const scoreIcons = this._scoreClickConfigurations.map( const scoreIcons = this._scoreClickConfigurations.map(
(config, index) => { (config, index) => {
const isFilled = index <= scoreToDisplayAsSelected; const isFilled = index <= scoreToDisplayAsSelected;
@ -208,11 +216,15 @@ class FeedbackDialog extends Component<Props, State> {
= `star-btn ${scoreAnimationClass} ${activeClass}`; = `star-btn ${scoreAnimationClass} ${activeClass}`;
return ( return (
<a <span
aria-label = { t(SCORES[index]) }
className = { className } className = { className }
key = { index } key = { index }
onClick = { config._onClick } onClick = { config._onClick }
onMouseOver = { config._onMouseOver }> onKeyPress = { config._onKeyPres }
onMouseOver = { config._onMouseOver }
role = 'button'
tabIndex = { 0 }>
{ isFilled { isFilled
? <StarFilledIcon ? <StarFilledIcon
label = 'star-filled' label = 'star-filled'
@ -220,11 +232,10 @@ class FeedbackDialog extends Component<Props, State> {
: <StarIcon : <StarIcon
label = 'star' label = 'star'
size = 'xlarge' /> } size = 'xlarge' /> }
</a> </span>
); );
}); });
const { t } = this.props;
return ( return (
<Dialog <Dialog
@ -234,7 +245,9 @@ class FeedbackDialog extends Component<Props, State> {
titleKey = 'feedback.rateExperience'> titleKey = 'feedback.rateExperience'>
<div className = 'feedback-dialog'> <div className = 'feedback-dialog'>
<div className = 'rating'> <div className = 'rating'>
<div className = 'star-label'> <div
aria-label = { this.props.t('feedback.star') }
className = 'star-label' >
<p id = 'starLabel'> <p id = 'starLabel'>
{ t(SCORES[scoreToDisplayAsSelected]) } { t(SCORES[scoreToDisplayAsSelected]) }
</p> </p>

View File

@ -13,7 +13,8 @@ import { translate } from '../../../base/i18n';
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons'; import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
import { getLocalParticipant } from '../../../base/participants'; import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { isButtonEnabled } from '../../../toolbox/functions.web'; import { showToolbox } from '../../../toolbox/actions.web';
import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
import { LAYOUTS, getCurrentLayout } from '../../../video-layout'; import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
import { setFilmstripVisible } from '../../actions'; import { setFilmstripVisible } from '../../actions';
import { shouldRemoteVideosBeVisible } from '../../functions'; import { shouldRemoteVideosBeVisible } from '../../functions';
@ -83,6 +84,11 @@ type Props = {
*/ */
_visible: boolean, _visible: boolean,
/**
* Whether or not the toolbox is displayed.
*/
_isToolboxVisible: Boolean,
/** /**
* The redux {@code dispatch} function. * The redux {@code dispatch} function.
*/ */
@ -114,6 +120,7 @@ class Filmstrip extends Component <Props> {
// Bind event handlers so they are only bound once for every instance. // Bind event handlers so they are only bound once for every instance.
this._onShortcutToggleFilmstrip = this._onShortcutToggleFilmstrip.bind(this); this._onShortcutToggleFilmstrip = this._onShortcutToggleFilmstrip.bind(this);
this._onToolbarToggleFilmstrip = this._onToolbarToggleFilmstrip.bind(this); this._onToolbarToggleFilmstrip = this._onToolbarToggleFilmstrip.bind(this);
this._onTabIn = this._onTabIn.bind(this);
} }
/** /**
@ -238,6 +245,19 @@ class Filmstrip extends Component <Props> {
); );
} }
_onTabIn: () => void;
/**
* Toggle the toolbar visibility when tabbing into it.
*
* @returns {void}
*/
_onTabIn() {
if (!this.props._isToolboxVisible && this.props._visible) {
this.props.dispatch(showToolbox());
}
}
/** /**
* Dispatches an action to change the visibility of the filmstrip. * Dispatches an action to change the visibility of the filmstrip.
* *
@ -298,12 +318,18 @@ class Filmstrip extends Component <Props> {
const { t } = this.props; const { t } = this.props;
return ( return (
<div className = 'filmstrip__toolbar'> <div
className = 'filmstrip__toolbar'>
<button <button
aria-expanded = { this.props._visible }
aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') } aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }
id = 'toggleFilmstripButton' id = 'toggleFilmstripButton'
onClick = { this._onToolbarToggleFilmstrip }> onClick = { this._onToolbarToggleFilmstrip }
<Icon src = { icon } /> onFocus = { this._onTabIn }
tabIndex = { 0 }>
<Icon
aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }
src = { icon } />
</button> </button>
</div> </div>
); );
@ -342,7 +368,8 @@ function _mapStateToProps(state) {
_participants: state['features/base/participants'], _participants: state['features/base/participants'],
_rows: gridDimensions.rows, _rows: gridDimensions.rows,
_videosClassName: videosClassName, _videosClassName: videosClassName,
_visible: visible _visible: visible,
_isToolboxVisible: isToolboxVisible(state)
}; };
} }

View File

@ -737,12 +737,12 @@ class Thumbnail extends Component<Props, State> {
participantID = { id } /> participantID = { id } />
</div> </div>
{ this._renderAvatar(styles.avatar) } { this._renderAvatar(styles.avatar) }
<span className = 'localvideomenu'>
<LocalVideoMenuTriggerButton />
</span>
<span className = 'audioindicator-container'> <span className = 'audioindicator-container'>
<AudioLevelIndicator audioLevel = { audioLevel } /> <AudioLevelIndicator audioLevel = { audioLevel } />
</span> </span>
<span className = 'localvideomenu'>
<LocalVideoMenuTriggerButton />
</span>
</span> </span>
); );
} }
@ -867,15 +867,15 @@ class Thumbnail extends Component<Props, State> {
className = 'presence-label' className = 'presence-label'
participantID = { id } /> participantID = { id } />
</div> </div>
<span className = 'audioindicator-container'>
<AudioLevelIndicator audioLevel = { audioLevel } />
</span>
<span className = 'remotevideomenu'> <span className = 'remotevideomenu'>
<RemoteVideoMenuTriggerButton <RemoteVideoMenuTriggerButton
initialVolumeValue = { volume } initialVolumeValue = { volume }
onVolumeChange = { onVolumeChange } onVolumeChange = { onVolumeChange }
participantID = { id } /> participantID = { id } />
</span> </span>
<span className = 'audioindicator-container'>
<AudioLevelIndicator audioLevel = { audioLevel } />
</span>
</span> </span>
); );
} }

View File

@ -27,6 +27,7 @@ class GoogleSignInButton extends AbstractGoogleSignInButton {
className = 'google-sign-in' className = 'google-sign-in'
onClick = { this.props.onClick }> onClick = { this.props.onClick }>
<img <img
alt = { t('welcomepage.logo.googleLogo') }
className = 'google-logo' className = 'google-logo'
src = 'images/googleLogo.svg' /> src = 'images/googleLogo.svg' />
<div className = 'google-cta'> <div className = 'google-cta'>

View File

@ -28,10 +28,12 @@ type Props = {
function CopyMeetingLinkSection({ t, url }: Props) { function CopyMeetingLinkSection({ t, url }: Props) {
return ( return (
<> <>
<span>{t('addPeople.shareLink')}</span> <label htmlFor = { 'copy-button-id' }>{t('addPeople.shareLink')}</label>
<CopyButton <CopyButton
aria-label = { t('addPeople.copyLink') }
className = 'invite-more-dialog-conference-url' className = 'invite-more-dialog-conference-url'
displayedText = { getDecodedURI(url) } displayedText = { getDecodedURI(url) }
id = 'copy-button-id'
textOnCopySuccess = { t('addPeople.linkCopied') } textOnCopySuccess = { t('addPeople.linkCopied') }
textOnHover = { t('addPeople.copyLink') } textOnHover = { t('addPeople.copyLink') }
textToCopy = { url } /> textToCopy = { url } />

View File

@ -49,6 +49,7 @@ class DialInNumber extends Component<Props> {
// Bind event handler so it is only bound once for every instance. // Bind event handler so it is only bound once for every instance.
this._onCopyText = this._onCopyText.bind(this); this._onCopyText = this._onCopyText.bind(this);
this._onCopyTextKeyPress = this._onCopyTextKeyPress.bind(this);
} }
_onCopyText: () => void; _onCopyText: () => void;
@ -68,6 +69,22 @@ class DialInNumber extends Component<Props> {
copyText(textToCopy); copyText(textToCopy);
} }
_onCopyTextKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onCopyTextKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onCopyText();
}
}
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
* *
@ -101,8 +118,12 @@ class DialInNumber extends Component<Props> {
</span> </span>
</div> </div>
<a <a
aria-label = { t('info.copyNumber') }
className = 'dial-in-copy' className = 'dial-in-copy'
onClick = { this._onCopyText }> onClick = { this._onCopyText }
onKeyPress = { this._onCopyTextKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon src = { IconCopy } /> <Icon src = { IconCopy } />
</a> </a>
</div> </div>

View File

@ -52,6 +52,20 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
copyText(inviteText); copyText(inviteText);
} }
/**
* Copies the conference invitation to the clipboard.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
function _onCopyTextKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
copyText(inviteText);
}
}
/** /**
* Toggles the email invite drawer. * Toggles the email invite drawer.
* *
@ -61,6 +75,20 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
setIsActive(!isActive); setIsActive(!isActive);
} }
/**
* Toggles the email invite drawer.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
function _onToggleActiveStateKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
setIsActive(!isActive);
}
}
/** /**
* Renders clickable elements that each open an email client * Renders clickable elements that each open an email client
* containing a conference invite. * containing a conference invite.
@ -101,6 +129,7 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
key = { idx } key = { idx }
position = 'top'> position = 'top'>
<a <a
aria-label = { t(tooltipKey) }
className = 'provider-icon' className = 'provider-icon'
href = { url } href = { url }
rel = 'noopener noreferrer' rel = 'noopener noreferrer'
@ -119,8 +148,13 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
<> <>
<div> <div>
<div <div
aria-expanded = { isActive }
aria-label = { t('addPeople.shareInvite') }
className = { `invite-more-dialog email-container${isActive ? ' active' : ''}` } className = { `invite-more-dialog email-container${isActive ? ' active' : ''}` }
onClick = { _onToggleActiveState }> onClick = { _onToggleActiveState }
onKeyPress = { _onToggleActiveStateKeyPress }
role = 'button'
tabIndex = { 0 }>
<span>{t('addPeople.shareInvite')}</span> <span>{t('addPeople.shareInvite')}</span>
<Icon src = { IconArrowDownSmall } /> <Icon src = { IconArrowDownSmall } />
</div> </div>
@ -129,8 +163,12 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
content = { t('addPeople.copyInvite') } content = { t('addPeople.copyInvite') }
position = 'top'> position = 'top'>
<div <div
aria-label = { t('addPeople.copyInvite') }
className = 'copy-invite-icon' className = 'copy-invite-icon'
onClick = { _onCopyText }> onClick = { _onCopyText }
onKeyPress = { _onCopyTextKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon src = { IconCopy } /> <Icon src = { IconCopy } />
</div> </div>
</Tooltip> </Tooltip>

View File

@ -76,9 +76,11 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
// Bind event handlers so they are only bound once per instance. // Bind event handlers so they are only bound once per instance.
this._onClearItems = this._onClearItems.bind(this); this._onClearItems = this._onClearItems.bind(this);
this._onClearItemsKeyPress = this._onClearItemsKeyPress.bind(this);
this._onItemSelected = this._onItemSelected.bind(this); this._onItemSelected = this._onItemSelected.bind(this);
this._onSelectionChange = this._onSelectionChange.bind(this); this._onSelectionChange = this._onSelectionChange.bind(this);
this._onSubmit = this._onSubmit.bind(this); this._onSubmit = this._onSubmit.bind(this);
this._onSubmitKeyPress = this._onSubmitKeyPress.bind(this);
this._parseQueryResults = this._parseQueryResults.bind(this); this._parseQueryResults = this._parseQueryResults.bind(this);
this._setMultiSelectElement = this._setMultiSelectElement.bind(this); this._setMultiSelectElement = this._setMultiSelectElement.bind(this);
this._renderFooterText = this._renderFooterText.bind(this); this._renderFooterText = this._renderFooterText.bind(this);
@ -246,6 +248,22 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
}); });
} }
_onSubmitKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onSubmitKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onSubmit();
}
}
_onKeyDown: (Object) => void; _onKeyDown: (Object) => void;
/** /**
@ -425,6 +443,22 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
this.setState({ inviteItems: [] }); this.setState({ inviteItems: [] });
} }
_onClearItemsKeyPress: () => void;
/**
* Clears the selected items from state and form.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onClearItemsKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onClearItems();
}
}
/** /**
* Renders the add/cancel actions for the form. * Renders the add/cancel actions for the form.
* *
@ -441,13 +475,21 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
return ( return (
<div className = { `invite-more-dialog invite-buttons${this._isAddDisabled() ? ' disabled' : ''}` }> <div className = { `invite-more-dialog invite-buttons${this._isAddDisabled() ? ' disabled' : ''}` }>
<a <a
aria-label = { t('dialog.Cancel') }
className = 'invite-more-dialog invite-buttons-cancel' className = 'invite-more-dialog invite-buttons-cancel'
onClick = { this._onClearItems }> onClick = { this._onClearItems }
onKeyPress = { this._onClearItemsKeyPress }
role = 'button'
tabIndex = { 0 }>
{t('dialog.Cancel')} {t('dialog.Cancel')}
</a> </a>
<a <a
aria-label = { t('addPeople.add') }
className = 'invite-more-dialog invite-buttons-add' className = 'invite-more-dialog invite-buttons-add'
onClick = { this._onSubmit }> onClick = { this._onSubmit }
onKeyPress = { this._onSubmitKeyPress }
role = 'button'
tabIndex = { 0 }>
{t('addPeople.add')} {t('addPeople.add')}
</a> </a>
</div> </div>
@ -480,9 +522,9 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
</span> </span>
<span> <span>
<a <a
aria-label = { supportLink }
href = { supportLink } href = { supportLink }
rel = 'noopener noreferrer' rel = 'noopener noreferrer'>
target = '_blank'>
{ t('inlineDialogFailure.support') } { t('inlineDialogFailure.support') }
</a> </a>
</span> </span>

View File

@ -71,7 +71,9 @@ class KeyboardShortcutsDialog extends Component<Props> {
<li <li
className = 'shortcuts-list__item' className = 'shortcuts-list__item'
key = { keyboardKey }> key = { keyboardKey }>
<span className = 'shortcuts-list__description'> <span
aria-label = { this.props.t(translationKey) }
className = 'shortcuts-list__description'>
{ this.props.t(translationKey) } { this.props.t(translationKey) }
</span> </span>
<span className = 'item-action'> <span className = 'item-action'>

View File

@ -102,7 +102,9 @@ class LargeVideo extends Component<Props> {
* another container for the background and the * another container for the background and the
* largeVideoWrapper in order to hide/show them. * largeVideoWrapper in order to hide/show them.
*/} */}
<div id = 'largeVideoWrapper'> <div
id = 'largeVideoWrapper'
role = 'figure' >
<video <video
autoPlay = { !_noAutoPlayVideo } autoPlay = { !_noAutoPlayVideo }
id = 'largeVideo' id = 'largeVideo'

View File

@ -89,7 +89,9 @@ class LobbySection extends PureComponent<Props, State> {
return ( return (
<> <>
<div id = 'lobby-section'> <div id = 'lobby-section'>
<p className = 'description'> <p
className = 'description'
role = 'banner'>
{ t('lobby.enableDialogText') } { t('lobby.enableDialogText') }
</p> </p>
<div className = 'control-row'> <div className = 'control-row'>

View File

@ -306,11 +306,15 @@ class LocalRecordingInfoDialog extends Component<Props, State> {
<div className = 'localrec-control-action-links'> <div className = 'localrec-control-action-links'>
<div className = 'localrec-control-action-link'> <div className = 'localrec-control-action-link'>
{ isEngaged ? <a { isEngaged ? <a
onClick = { this._onStop }> onClick = { this._onStop }
role = 'button'
tabIndex = { 0 }>
{ t('localRecording.stop') } { t('localRecording.stop') }
</a> </a>
: <a : <a
onClick = { this._onStart }> onClick = { this._onStart }
role = 'button'
tabIndex = { 0 }>
{ t('localRecording.start') } { t('localRecording.start') }
</a> </a>
} }

View File

@ -81,9 +81,9 @@ class Notification extends AbstractNotification<Props> {
// the id is used for testing the UI // the id is used for testing the UI
return ( return (
<div data-testid = { this._getDescriptionKey() } > <p data-testid = { this._getDescriptionKey() } >
{ description } { description }
</div> </p>
); );
} }

View File

@ -3,6 +3,7 @@
import { FlagGroup } from '@atlaskit/flag'; import { FlagGroup } from '@atlaskit/flag';
import React from 'react'; import React from 'react';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import AbstractNotificationsContainer, { import AbstractNotificationsContainer, {
_abstractMapStateToProps, _abstractMapStateToProps,
@ -16,7 +17,12 @@ type Props = AbstractProps & {
/** /**
* Whether we are a SIP gateway or not. * Whether we are a SIP gateway or not.
*/ */
_iAmSipGateway: boolean _iAmSipGateway: boolean,
/**
* Invoked to obtain translated strings.
*/
t: Function
}; };
/** /**
@ -42,6 +48,7 @@ class NotificationsContainer extends AbstractNotificationsContainer<Props> {
return ( return (
<FlagGroup <FlagGroup
id = 'notifications-container' id = 'notifications-container'
label = { this.props.t('notify.groupTitle') }
onDismissed = { this._onDismissed }> onDismissed = { this._onDismissed }>
{ this._renderFlags() } { this._renderFlags() }
</FlagGroup> </FlagGroup>
@ -73,7 +80,6 @@ class NotificationsContainer extends AbstractNotificationsContainer<Props> {
key = { uid } key = { uid }
onDismissed = { this._onDismissed } onDismissed = { this._onDismissed }
uid = { uid } /> uid = { uid } />
); );
}); });
} }
@ -96,4 +102,4 @@ function _mapStateToProps(state) {
} }
export default connect(_mapStateToProps)(NotificationsContainer); export default translate(connect(_mapStateToProps)(NotificationsContainer));

View File

@ -29,12 +29,20 @@ class PageReloadOverlay extends AbstractPageReloadOverlay<Props> {
return ( return (
<OverlayFrame isLightOverlay = { isNetworkFailure }> <OverlayFrame isLightOverlay = { isNetworkFailure }>
<div className = 'inlay'> <div
aria-describedby = 'reload_overlay_text'
aria-labelledby = 'reload_overlay_title'
className = 'inlay'
role = 'dialog'>
<span <span
className = 'reload_overlay_title'> className = 'reload_overlay_title'
id = 'reload_overlay_title'
role = 'heading'>
{ t(title) } { t(title) }
</span> </span>
<span className = 'reload_overlay_text'> <span
className = 'reload_overlay_text'
id = 'reload_overlay_text'>
{ t(message, { seconds: timeLeft }) } { t(message, { seconds: timeLeft }) }
</span> </span>
{ this._renderProgressBar() } { this._renderProgressBar() }

View File

@ -30,12 +30,17 @@ class UserMediaPermissionsOverlay extends AbstractUserMediaPermissionsOverlay {
<div className = 'inlay'> <div className = 'inlay'>
<span className = 'inlay__icon icon-microphone' /> <span className = 'inlay__icon icon-microphone' />
<span className = 'inlay__icon icon-camera' /> <span className = 'inlay__icon icon-camera' />
<h3 className = 'inlay__title'> <h3
aria-label = { t('startupoverlay.genericTitle') }
className = 'inlay__title'
role = 'alert' >
{ {
t('startupoverlay.genericTitle') t('startupoverlay.genericTitle')
} }
</h3> </h3>
<span className = 'inlay__text'> <span
className = 'inlay__text'
role = 'alert' >
{ {
translateToHTML(t, translateToHTML(t,
`userMedia.${browser}GrantPermissions`) `userMedia.${browser}GrantPermissions`)
@ -43,7 +48,9 @@ class UserMediaPermissionsOverlay extends AbstractUserMediaPermissionsOverlay {
</span> </span>
</div> </div>
<div className = 'policy overlay__policy'> <div className = 'policy overlay__policy'>
<p className = 'policy__text'> <p
className = 'policy__text'
role = 'alert'>
{ translateToHTML(t, 'startupoverlay.policyText') } { translateToHTML(t, 'startupoverlay.policyText') }
</p> </p>
{ {
@ -66,7 +73,9 @@ class UserMediaPermissionsOverlay extends AbstractUserMediaPermissionsOverlay {
if (policyLogoSrc) { if (policyLogoSrc) {
return ( return (
<div className = 'policy__logo'> <div className = 'policy__logo'>
<img src = { policyLogoSrc } /> <img
alt = { this.props.t('welcomepage.logo.policyLogo') }
src = { policyLogoSrc } />
</div> </div>
); );
} }

View File

@ -21,7 +21,7 @@ export const InviteButton = () => {
return ( return (
<ParticipantInviteButton <ParticipantInviteButton
aria-label = { t('toolbar.accessibilityLabel.invite') } aria-label = { t('participantsPane.actions.invite') }
onClick = { onInvite }> onClick = { onInvite }>
<Icon <Icon
size = { 20 } size = { 20 }

View File

@ -1,6 +1,7 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { getIsParticipantAudioMuted, getIsParticipantVideoMuted } from '../../base/tracks'; import { getIsParticipantAudioMuted, getIsParticipantVideoMuted } from '../../base/tracks';
@ -38,6 +39,7 @@ export const MeetingParticipantItem = ({
onLeave, onLeave,
participant participant
}: Props) => { }: Props) => {
const { t } = useTranslation();
const isAudioMuted = useSelector(getIsParticipantAudioMuted(participant)); const isAudioMuted = useSelector(getIsParticipantAudioMuted(participant));
const isVideoMuted = useSelector(getIsParticipantVideoMuted(participant)); const isVideoMuted = useSelector(getIsParticipantVideoMuted(participant));
@ -49,7 +51,9 @@ export const MeetingParticipantItem = ({
onLeave = { onLeave } onLeave = { onLeave }
participant = { participant } participant = { participant }
videoMuteState = { isVideoMuted ? MediaState.Muted : MediaState.Unmuted }> videoMuteState = { isVideoMuted ? MediaState.Muted : MediaState.Unmuted }>
<ParticipantActionEllipsis onClick = { onContextMenu } /> <ParticipantActionEllipsis
aria-label = { t('MeetingParticipantItem.ParticipantActionEllipsis.options') }
onClick = { onContextMenu } />
</ParticipantItem> </ParticipantItem>
); );
}; };

View File

@ -30,6 +30,12 @@ export const ParticipantsPane = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const closePane = useCallback(() => dispatch(close(), [ dispatch ])); const closePane = useCallback(() => dispatch(close(), [ dispatch ]));
const closePaneKeyPress = useCallback(e => {
if (closePane && (e.key === ' ' || e.key === 'Enter')) {
e.preventDefault();
closePane();
}
}, [ closePane ]);
const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)), [ dispatch ]); const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)), [ dispatch ]);
return ( return (
@ -41,7 +47,12 @@ export const ParticipantsPane = () => {
) }> ) }>
<div className = 'participants_pane-content'> <div className = 'participants_pane-content'>
<Header> <Header>
<Close onClick = { closePane } /> <Close
aria-label = { t('participantsPane.close', 'Close') }
onClick = { closePane }
onKeyPress = { closePaneKeyPress }
role = 'button'
tabIndex = { 0 } />
</Header> </Header>
<Container> <Container>
<LobbyParticipantList /> <LobbyParticipantList />

View File

@ -183,6 +183,9 @@ class Prejoin extends Component<Props, State> {
this._onDropdownClose = this._onDropdownClose.bind(this); this._onDropdownClose = this._onDropdownClose.bind(this);
this._onOptionsClick = this._onOptionsClick.bind(this); this._onOptionsClick = this._onOptionsClick.bind(this);
this._setName = this._setName.bind(this); this._setName = this._setName.bind(this);
this._onJoinConferenceWithoutAudioKeyPress = this._onJoinConferenceWithoutAudioKeyPress.bind(this);
this._showDialogKeyPress = this._showDialogKeyPress.bind(this);
this._onJoinKeyPress = this._onJoinKeyPress.bind(this);
} }
_onJoinButtonClick: () => void; _onJoinButtonClick: () => void;
@ -205,6 +208,22 @@ class Prejoin extends Component<Props, State> {
this.props.joinConference(); this.props.joinConference();
} }
_onJoinKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onJoinKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onJoinButtonClick();
}
}
_onToggleButtonClick: () => void; _onToggleButtonClick: () => void;
/** /**
@ -283,6 +302,40 @@ class Prejoin extends Component<Props, State> {
this._onDropdownClose(); this._onDropdownClose();
} }
_showDialogKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_showDialogKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._showDialog();
}
}
_onJoinConferenceWithoutAudioKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onJoinConferenceWithoutAudioKeyPress(e) {
if (this.props.joinConferenceWithoutAudio
&& (e.key === ' '
|| e.key === 'Enter')) {
e.preventDefault();
this.props.joinConferenceWithoutAudio();
}
}
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
* *
@ -305,7 +358,8 @@ class Prejoin extends Component<Props, State> {
visibleButtons visibleButtons
} = this.props; } = this.props;
const { _closeDialog, _onDropdownClose, _onJoinButtonClick, _onOptionsClick, _setName, _showDialog } = this; const { _closeDialog, _onDropdownClose, _onJoinButtonClick, _onJoinKeyPress, _showDialogKeyPress,
_onJoinConferenceWithoutAudioKeyPress, _onOptionsClick, _setName, _showDialog } = this;
const { showJoinByPhoneButtons, showError } = this.state; const { showJoinByPhoneButtons, showError } = this.state;
return ( return (
@ -322,10 +376,16 @@ class Prejoin extends Component<Props, State> {
{showJoinActions && ( {showJoinActions && (
<div className = 'prejoin-input-area-container'> <div className = 'prejoin-input-area-container'>
<div className = 'prejoin-input-area'> <div className = 'prejoin-input-area'>
<label
className = 'prejoin-input-area-label'
htmlFor = { 'Prejoin-input-field-id' } >
{ t('dialog.enterDisplayNameToJoin') }</label>
<InputField <InputField
autoComplete = { 'name' }
autoFocus = { true } autoFocus = { true }
className = { showError ? 'error' : '' } className = { showError ? 'error' : '' }
hasError = { showError } hasError = { showError }
id = { 'Prejoin-input-field-id' }
onChange = { _setName } onChange = { _setName }
onSubmit = { joinConference } onSubmit = { joinConference }
placeHolder = { t('dialog.enterDisplayName') } placeHolder = { t('dialog.enterDisplayName') }
@ -341,7 +401,10 @@ class Prejoin extends Component<Props, State> {
<div <div
className = 'prejoin-preview-dropdown-btn' className = 'prejoin-preview-dropdown-btn'
data-testid = 'prejoin.joinWithoutAudio' data-testid = 'prejoin.joinWithoutAudio'
onClick = { joinConferenceWithoutAudio }> onClick = { joinConferenceWithoutAudio }
onKeyPress = { _onJoinConferenceWithoutAudioKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon <Icon
className = 'prejoin-preview-dropdown-icon' className = 'prejoin-preview-dropdown-icon'
size = { 24 } size = { 24 }
@ -350,7 +413,10 @@ class Prejoin extends Component<Props, State> {
</div> </div>
{hasJoinByPhoneButton && <div {hasJoinByPhoneButton && <div
className = 'prejoin-preview-dropdown-btn' className = 'prejoin-preview-dropdown-btn'
onClick = { _showDialog }> onClick = { _showDialog }
onKeyPress = { _showDialogKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon <Icon
className = 'prejoin-preview-dropdown-icon' className = 'prejoin-preview-dropdown-icon'
data-testid = 'prejoin.joinByPhone' data-testid = 'prejoin.joinByPhone'
@ -363,9 +429,15 @@ class Prejoin extends Component<Props, State> {
onClose = { _onDropdownClose }> onClose = { _onDropdownClose }>
<ActionButton <ActionButton
OptionsIcon = { showJoinByPhoneButtons ? IconArrowUp : IconArrowDown } OptionsIcon = { showJoinByPhoneButtons ? IconArrowUp : IconArrowDown }
ariaDropDownLabel = { t('prejoin.joinWithoutAudio') }
ariaLabel = { t('prejoin.joinMeeting') }
ariaPressed = { showJoinByPhoneButtons }
hasOptions = { true } hasOptions = { true }
onClick = { _onJoinButtonClick } onClick = { _onJoinButtonClick }
onKeyPress = { _onJoinKeyPress }
onOptionsClick = { _onOptionsClick } onOptionsClick = { _onOptionsClick }
role = 'button'
tabIndex = { 0 }
testId = 'prejoin.joinMeeting' testId = 'prejoin.joinMeeting'
type = 'primary'> type = 'primary'>
{ t('prejoin.joinMeeting') } { t('prejoin.joinMeeting') }

View File

@ -172,9 +172,7 @@ class CountryPicker extends PureComponent<Props, State> {
* @param {Object} e - The synthetic event. * @param {Object} e - The synthetic event.
* @returns {void} * @returns {void}
*/ */
_onCountrySelectorClick(e) { _onCountrySelectorClick() {
e.stopPropagation();
this.setState({ this.setState({
isOpen: !this.setState.isOpen isOpen: !this.setState.isOpen
}); });
@ -215,7 +213,8 @@ class CountryPicker extends PureComponent<Props, State> {
* @returns {void} * @returns {void}
*/ */
_onKeyPress(e) { _onKeyPress(e) {
if (e.key === 'Enter') { if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this.props.onSubmit(); this.props.onSubmit();
} }
} }

View File

@ -1,6 +1,6 @@
// @flow // @flow
import React from 'react'; import React, { useCallback } from 'react';
import { Icon, IconArrowDown } from '../../../base/icons'; import { Icon, IconArrowDown } from '../../../base/icons';
@ -23,10 +23,18 @@ type Props = {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
function CountrySelector({ country: { code, dialCode }, onClick }: Props) { function CountrySelector({ country: { code, dialCode }, onClick }: Props) {
const onKeyPressHandler = useCallback(e => {
if (onClick && (e.key === ' ' || e.key === 'Enter')) {
e.preventDefault();
onClick();
}
}, [ onClick ]);
return ( return (
<div <div
className = 'cpick-selector' className = 'cpick-selector'
onClick = { onClick }> onClick = { onClick }
onKeyPress = { onKeyPressHandler }>
<div className = { `prejoin-dialog-flag iti-flag ${code}` } /> <div className = { `prejoin-dialog-flag iti-flag ${code}` } />
<span>{`+${dialCode}`}</span> <span>{`+${dialCode}`}</span>
<Icon <Icon

Some files were not shown because too many files have changed in this diff Show More