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"] {
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;
overflow: auto;
width: 300px;
&-ul {
margin:0;
padding:0;
list-style-type: none;
}
}
&-header {
@ -64,7 +69,13 @@
&-speaker {
position: relative;
&:hover {
&-ul {
margin:0;
padding:0;
list-style-type: none;
}
&:hover, &:focus-within, &:focus {
.audio-preview-entry {
background: #36383C;
margin-left: 0;
@ -81,7 +92,7 @@
}
.audio-preview-entry-text {
max-width: 196px;
max-width: 178px;
}
}
@ -90,7 +101,7 @@
}
.audio-preview-entry-text {
max-width: 256px;
max-width: 238px;
}
}
@ -150,8 +161,9 @@
color: #1C2025;
cursor: pointer;
font-weight: 600;
font-size: 0.8rem;
line-height: 24px;
padding: 2px 16px;
padding: 2px 8px;
position: absolute;
right: 16px;
top: 5px;
@ -162,4 +174,10 @@
right: 16px;
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 {
color: #3572b0;
color: #44A5FF;
text-decoration: none;
font-weight: bold;
}
a:focus,
a:hover,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -113,7 +113,7 @@
cursor: pointer;
color: #fff;
display: flex;
flex-direction: row;
flex-direction: column;
font-size: 15px;
font-weight: 300;
justify-content: center;
@ -139,6 +139,9 @@
margin-left: 10px;
}
}
.copy-button{
width: 298px;
}
.copy-meeting-text {
width: 266px;
@ -177,7 +180,7 @@
}
&.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;
}
label {
cursor: pointer;
}
&:hover {
background: rgba(255, 255, 255, 0.1);

View File

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

View File

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

View File

@ -79,4 +79,8 @@
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;
z-index: $zindex2;
width: 18px;
height: 13px;
height: 18px;
color: #FFF;
font-size: 10pt;
margin-right: $remoteVideoMenuIconMargin;

View File

@ -3,7 +3,7 @@
justify-content: space-between;
align-items: center;
padding: 8px 8px 8px 16px;
margin-top: 8px;
margin-top: 5px;
width: calc(100% - 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 {
outline: none;
outline: 1px solid white !important;
}
/**

View File

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

View File

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

View File

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

View File

@ -103,7 +103,7 @@
font-size: 14px;
a {
color: #2684FF;
color: #6FB1EA;
cursor: pointer;
text-decoration: none;
}
@ -119,7 +119,7 @@
height: 8px;
.audio-input-preview-level {
background: #4C9AFF;
background: #75B1FF;
border-radius: 5px;
height: 100%;
-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;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 5px;
}
.info-password-none,
.info-password-remote {
opacity: 0.5;
color: #fff;
}
.info-password-input {
width: 100%;
background-color: transparent;
border: none;
background-color: #0E1624;
border-radius: 3px;
border: 2px solid #202B3D;
color: inherit;
padding-left: 0;
}
.info-password-input:focus ,
.info-password-input:active {
border: 2px solid #B8C7E0;
}
.info-password-local {
user-select: text;

View File

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

View File

@ -16,12 +16,19 @@
}
.mock-atlaskit-label {
color: #56637A;
color: #b8c7e0;
font-size: 12px;
font-weight: 600;
line-height: 1.33;
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 {
color: #9FB0CC;
@ -65,4 +72,14 @@
.sign-out-cta {
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 {
color: $videoQualityActive;
font-weight: bold;
&::before {
background: $videoQualityActive;

View File

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

View File

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

View File

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

View File

@ -70,12 +70,17 @@
},
"privateNotice": "Private Nachricht an {{recipient}}",
"title": "Chatten",
"you": "Sie"
"you": "Sie",
"message": "Nachricht",
"messageAccessibleTitle": "{{user}} sagt:",
"messageAccessibleTitleMe": "Ich sage:",
"smileysPanel": "Emoji-Auswahl"
},
"chromeExtensionBanner": {
"installExtensionText": "Installieren Sie die Erweiterung für die Integration von Google Calendar und Office 365",
"buttonText": "Chrome-Erweiterung installieren",
"dontShowAgain": "Hinweis nicht mehr anzeigen"
"dontShowAgain": "Hinweis nicht mehr anzeigen",
"close": "Schließen"
},
"connectingOverlay": {
"joiningRoom": "Eine Verbindung zu Ihrem Meeting wird hergestellt…"
@ -204,6 +209,8 @@
"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.",
"enterDisplayName": "Bitte geben Sie hier Ihren Namen ein",
"enterDisplayNameToJoin" : "Benutzername für Konferenz eingeben" ,
"embedMeeting": "Besprechung einbetten",
"error": "Fehler",
"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?",
@ -295,7 +302,7 @@
"Share": "Teilen",
"shareVideoLinkError": "Bitte einen gültigen YouTube-Link angeben.",
"shareVideoTitle": "Video teilen",
"shareYourScreen": "Bildschirm freigeben",
"shareYourScreen": "Bildschirmfreigabe ein-/ausschalten",
"shareYourScreenDisabled": "Bildschirmfreigabe deaktiviert.",
"startLiveStreaming": "Livestream 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.",
"WaitingForHostTitle": "Warten auf den Beginn der Konferenz …",
"Yes": "Ja",
"yourEntireScreen": "Ganzer Bildschirm"
"yourEntireScreen": "Ganzer Bildschirm",
"remoteUserControls": "Remote Benutzersteuerung von {{username}}",
"localUserControls": "Lokale Benutzersteuerung"
},
"dialOut": {
"statusMessage": "ist jetzt {{status}}"
@ -340,8 +349,20 @@
"slightBlur": "Hintergrund leicht unscharf",
"removeBackground": "Hintergrund entfernen",
"uploadImage": "Bild hochladen",
"addBackground": "Hintergrund hinzufügen",
"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": {
"average": "Durchschnittlich",
@ -350,7 +371,8 @@
"good": "Gut",
"rateExperience": "Bitte bewerten Sie diese Konferenz",
"veryBad": "Sehr schlecht",
"veryGood": "Sehr gut"
"veryGood": "Sehr gut",
"star": "Sterne"
},
"incomingCall": {
"answer": "Antworten",
@ -367,6 +389,7 @@
"country": "Land",
"dialANumber": "Um am Meeting teilzunehmen, müssen Sie eine dieser Nummern wählen und dann die PIN eingeben.",
"dialInConferenceID": "PIN:",
"copyNumber":"Nummer kopieren",
"dialInNotSupported": "Entschuldigung, leider wird das Einwählen derzeit nicht unterstützt.",
"dialInNumber": "Einwählen:",
"dialInSummaryError": "Fehler beim Abrufen der Einwahlinformationen. Versuchen Sie es später erneut.",
@ -404,6 +427,7 @@
"support": "Support",
"supportMsg": "Wenn der Fehler erneut auftritt, bitte kontaktieren Sie"
},
"jitsiHome": "{{logo}} Logo, verlinkt zur Homepage",
"keyboardShortcuts": {
"focusLocal": "Lokales Video fokussieren",
"focusRemote": "Auf das Video einer anderen Person fokussieren",
@ -524,7 +548,19 @@
"OldElectronAPPTitle": "Sicherheitslücke!",
"oldElectronClientDescription1": "Sie scheinen eine alte Version des Jitsi-Meet-Clients zu nutzen. Diese hat bekannte Schwachstellen. Bitte aktualisieren Sie auf unsere ",
"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": {
"headings": {
@ -589,6 +625,7 @@
"linkCopied": "Link in die Zwischenablage kopiert",
"lookGood": "Ihr Mikrofon scheint zu funktionieren.",
"or": "oder",
"keyboardShortcuts" : "Tastaturkurzbefehle aktivieren",
"premeeting": "Vorschau",
"showScreen": "Konferenzvorschau aktivieren",
"startWithPhone": "Mit Telefonaudio starten",
@ -612,6 +649,7 @@
"ringing": "Es klingelt …"
},
"profile": {
"avatar": "Benutzerbild",
"setDisplayNameLabel": "Anzeigename festlegen",
"setEmailInput": "E-Mail eingeben",
"setEmailLabel": "E-Mail-Adresse für Gravatar",
@ -728,29 +766,29 @@
"title": "Die Konferenz wurde unterbrochen, weil der Standby-Modus aktiviert wurde."
},
"toolbar": {
"accessibilityLabel": {
"accessibilityLabel": {
"audioOnly": "„Nur Audio“ ein-/ausschalten",
"audioRoute": "Audiogerät auswählen",
"callQuality": "Qualitätseinstellungen",
"cc": "Untertitel ein-/ausschalten",
"chat": "Chatfenster ein-/ausblenden",
"chat": "Chatfenster öffnen / schließen",
"document": "Geteiltes Dokument schließen",
"download": "Unsere Apps herunterladen",
"embedMeeting": "Konferenz einbetten",
"feedback": "Feedback hinterlassen",
"fullScreen": "Vollbildmodus ein-/ausschalten",
"grantModerator": "Moderationsrechte vergeben",
"hangup": "Anruf beenden",
"hangup": "Konferenz verlassen",
"help": "Hilfe",
"invite": "Person einladen",
"kick": "Person entfernen",
"lobbyButton": "Lobbymodus ein-/ausschalten",
"localRecording": "Lokale Aufzeichnungssteuerelemente ein-/ausschalten",
"lockRoom": "Konferenzpasswort ein-/ausschalten",
"moreActions": "Menü „Weitere Aktionen“ ein-/ausschalten",
"moreActionsMenu": "Menü „Weitere Aktionen“",
"moreActions": "Menü „Weitere Einstellungen“ ein-/ausschalten",
"moreActionsMenu": "Menü „Weitere Einstellungen“",
"moreOptions": "Menü „Weitere Optionen“",
"mute": "„Audio stummschalten“ ein-/ausschalten",
"mute": "Mikrofon aktivieren / deaktivieren",
"muteEveryone": "Alle stummschalten",
"muteEveryoneElse": "Alle anderen stummschalten",
"muteEveryonesVideo": "Alle Kameras ausschalten",
@ -759,7 +797,7 @@
"pip": "Bild-in-Bild-Modus ein-/ausschalten",
"privateMessage": "Private Nachricht senden",
"profile": "Profil bearbeiten",
"raiseHand": "„Melden“ ein-/ausschalten",
"raiseHand": "Hand erheben / senken",
"recording": "Aufzeichnung ein-/ausschalten",
"remoteMute": "Personen stummschalten",
"remoteVideoMute": "Kamera von dieser Person ausschalten",
@ -776,7 +814,9 @@
"toggleCamera": "Kamera wechseln",
"toggleFilmstrip": "Miniaturansichten 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",
"audioSettings": "Ton-Einstellungen",
@ -797,7 +837,7 @@
"exitFullScreen": "Vollbildmodus verlassen",
"exitTileView": "Kachelansicht ausschalten",
"feedback": "Feedback hinterlassen",
"hangup": "Verlassen",
"hangup": "Konferenz verlassen",
"help": "Hilfe",
"invite": "Personen einladen",
"lobbyButtonDisable": "Lobbymodus deaktivieren",
@ -807,7 +847,7 @@
"lowerYourHand": "Hand senken",
"moreActions": "Weitere Einstellungen",
"moreOptions": "Weitere Optionen",
"mute": "Stummschaltung aktivieren / deaktivieren",
"mute": "Mikrofon aktivieren / deaktivieren",
"muteEveryone": "Alle stummschalten",
"muteEveryonesVideo": "Alle Kameras ausschalten",
"noAudioSignalTitle": "Es kommt kein Input von Ihrem Mikrofon!",
@ -822,7 +862,7 @@
"pip": "Bild-in-Bild-Modus einschalten",
"privateMessage": "Private Nachricht senden",
"profile": "Profil bearbeiten",
"raiseHand": "Hand erheben",
"raiseHand": "Hand erheben / senken",
"raiseYourHand": "Melden",
"security": "Sicherheitsoptionen",
"Settings": "Einstellungen",
@ -867,6 +907,7 @@
"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."
},
"volumeSlider": "Lautstärkeregler",
"videoSIPGW": {
"busy": "Es stehen keine freien Ressourcen zur Verfügung. Bitte versuchen Sie es später noch einmal.",
"busyTitle": "Keine freien Ressourcen",
@ -911,6 +952,7 @@
"videomute": "Person hat die Kamera angehalten"
},
"welcomepage": {
"addMeetingName": "Besprechungsnamen hinzufügen",
"accessibilityLabel": {
"join": "Zum Teilnehmen tippen",
"roomname": "Konferenzname eingeben"
@ -933,6 +975,9 @@
"join": "ERSTELLEN / BEITRETEN",
"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.",
"mobileDownLoadLinkIos": "iOS App Download",
"mobileDownLoadLinkAndroid": "Android App Download",
"mobileDownLoadLinkFDroid": "F-Droid App Download",
"privacy": "Datenschutz",
"recentList": "Verlauf",
"recentListDelete": "Eintrag löschen",
@ -944,7 +989,15 @@
"sendFeedback": "Feedback senden",
"startMeeting": "Meeting starten",
"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": {
"button": "Andere einladen",

View File

@ -70,12 +70,17 @@
},
"privateNotice": "Private message to {{recipient}}",
"title": "Chat",
"you": "you"
"you": "you",
"message": "Message",
"messageAccessibleTitle": "{{user}} says:",
"messageAccessibleTitleMe": "me says:",
"smileysPanel": "Emoji panel"
},
"chromeExtensionBanner": {
"installExtensionText": "Install the extension for Google Calendar and Office 365 integration",
"buttonText": "Install Chrome Extension",
"dontShowAgain": "Dont show me this again"
"dontShowAgain": "Dont show me this again",
"close": "Close"
},
"connectingOverlay": {
"joiningRoom": "Connecting you to your meeting..."
@ -204,6 +209,8 @@
"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.",
"enterDisplayName": "Please enter your name here",
"enterDisplayNameToJoin": "Please enter your name to join",
"embedMeeting": "Embed meeting",
"error": "Error",
"gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
"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.",
"WaitingForHostTitle": "Waiting for the host ...",
"Yes": "Yes",
"yourEntireScreen": "Your entire screen"
"yourEntireScreen": "Your entire screen",
"remoteUserControls": "Remote user controls of {{username}}",
"localUserControls": "Local user controls"
},
"dialOut": {
"statusMessage": "is now {{status}}"
@ -341,8 +350,19 @@
"slightBlur": "Slight Blur",
"removeBackground": "Remove background",
"addBackground": "Add background",
"pleaseWait": "Please wait...",
"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": {
"average": "Average",
@ -351,7 +371,8 @@
"good": "Good",
"rateExperience": "Rate your meeting experience",
"veryBad": "Very Bad",
"veryGood": "Very Good"
"veryGood": "Very Good",
"star": "Star"
},
"incomingCall": {
"answer": "Answer",
@ -368,6 +389,7 @@
"country": "Country",
"dialANumber": "To join your meeting, dial one of these numbers and then enter the pin.",
"dialInConferenceID": "PIN:",
"copyNumber":"Copy number",
"dialInNotSupported": "Sorry, dialing in is currently not supported.",
"dialInNumber": "Dial-in:",
"dialInSummaryError": "Error fetching dial-in info now. Please try again later.",
@ -405,6 +427,7 @@
"support": "Support",
"supportMsg": "If this keeps happening, reach out to"
},
"jitsiHome": "{{logo}} Logo, links to Homepage",
"keyboardShortcuts": {
"focusLocal": "Focus on your video",
"focusRemote": "Focus on another person's video",
@ -525,9 +548,11 @@
"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 ",
"oldElectronClientDescription2": "latest build",
"oldElectronClientDescription3": " now!"
"oldElectronClientDescription3": " now!",
"groupTitle": "Notifications"
},
"participantsPane": {
"close": "Close",
"headings": {
"lobby": "Lobby ({{count}})",
"participantsList": "Meeting participants ({{count}})"
@ -592,6 +617,7 @@
"or": "or",
"premeeting": "Pre meeting",
"showScreen": "Enable pre meeting screen",
"keyboardShortcuts" : "Enable Keyboard shortcuts",
"startWithPhone": "Start with phone audio",
"screenSharingError": "Screen sharing error:",
"videoOnlyError": "Video error:",
@ -613,6 +639,7 @@
"ringing": "Ringing..."
},
"profile": {
"avatar": "avatar",
"setDisplayNameLabel": "Set your display name",
"setEmailInput": "Enter e-mail",
"setEmailLabel": "Set your gravatar email",
@ -735,24 +762,24 @@
"audioRoute": "Select the sound device",
"callQuality": "Manage video quality",
"cc": "Toggle subtitles",
"chat": "Toggle chat window",
"chat": "Open / Close chat",
"document": "Toggle shared document",
"download": "Download our apps",
"embedMeeting": "Embed meeting",
"feedback": "Leave feedback",
"fullScreen": "Toggle full screen",
"grantModerator": "Grant Moderator",
"hangup": "Leave the call",
"hangup": "Leave the meeting",
"help": "Help",
"invite": "Invite people",
"kick": "Kick participant",
"lobbyButton": "Enable/disable lobby mode",
"localRecording": "Toggle local recording controls",
"lockRoom": "Toggle meeting password",
"moreActions": "Toggle more actions menu",
"moreActions": "More actions",
"moreActionsMenu": "More actions menu",
"moreOptions": "Show more options",
"mute": "Toggle mute audio",
"mute": "Mute / Unmute",
"muteEveryone": "Mute everyone",
"muteEveryoneElse": "Mute everyone else",
"muteEveryonesVideo": "Disable everyone's camera",
@ -761,7 +788,7 @@
"pip": "Toggle Picture-in-Picture mode",
"privateMessage": "Send private message",
"profile": "Edit your profile",
"raiseHand": "Toggle raise hand",
"raiseHand": "Raise / Lower your hand",
"recording": "Toggle recording",
"remoteMute": "Mute participant",
"remoteVideoMute": "Disable camera of participant",
@ -770,18 +797,22 @@
"shareaudio": "Share audio",
"sharedvideo": "Toggle YouTube video sharing",
"shareRoom": "Invite someone",
"shareYourScreen": "Toggle screenshare",
"shareYourScreen": "Start / Stop sharing your screen",
"shortcuts": "Toggle shortcuts",
"show": "Show on stage",
"speakerStats": "Toggle speaker statistics",
"tileView": "Toggle tile view",
"toggleCamera": "Toggle camera",
"toggleFilmstrip": "Toggle filmstrip",
"videomute": "Toggle mute video",
"selectBackground": "Select Background"
"videomute": "Start / Stop camera",
"videoblur": "Toggle video blur",
"selectBackground": "Select Background",
"expand": "Expand",
"collapse": "Collapse"
},
"addPeople": "Add people to your call",
"audioSettings": "Audio settings",
"videoSettings": "Video settings",
"audioOnlyOff": "Disable low bandwidth mode",
"audioOnlyOn": "Enable low bandwidth mode",
"audioRoute": "Select the sound device",
@ -799,7 +830,7 @@
"exitFullScreen": "Exit full screen",
"exitTileView": "Exit tile view",
"feedback": "Leave feedback",
"hangup": "Leave",
"hangup": "Leave the meeting",
"help": "Help",
"invite": "Invite people",
"lobbyButtonDisable": "Disable lobby mode",
@ -842,7 +873,6 @@
"tileViewToggle": "Toggle tile view",
"toggleCamera": "Toggle camera",
"videomute": "Start / Stop camera",
"videoSettings": "Video settings",
"selectBackground": "Select background"
},
"transcribing": {
@ -869,6 +899,7 @@
"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."
},
"volumeSlider": "Volume slider",
"videoSIPGW": {
"busy": "We're working on freeing resources. Please try again in a few minutes.",
"busyTitle": "The Room service is currently busy",
@ -913,6 +944,7 @@
"videomute": "Participant has stopped the camera"
},
"welcomepage": {
"addMeetingName": "Add Meeting name",
"accessibilityLabel": {
"join": "Tap to join",
"roomname": "Enter room name"
@ -934,6 +966,9 @@
"info": "Dial-in info",
"join": "CREATE / JOIN",
"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.",
"privacy": "Privacy",
"recentList": "Recent",
@ -946,7 +981,15 @@
"sendFeedback": "Send feedback",
"startMeeting": "Start meeting",
"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": {
"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 {
@ -30,44 +30,66 @@ const _shortcuts = new Map();
const _shortcutsHelp = new Map();
/**
* True if the keyboard shortcuts are enabled and false if not.
* @type {boolean}
* The key used to save in local storage if keyboard shortcuts are enabled.
*/
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.
*/
const KeyboardShortcut = {
isPushToTalkActive: false,
init() {
this._initGlobalShortcuts();
window.onkeyup = e => {
if (!enabled) {
if (!this.getEnabled()) {
return;
}
const key = this._getKeyboardKey(e).toUpperCase();
const num = parseInt(key, 10);
if (!($(':focus').is('input[type=text]')
|| $(':focus').is('input[type=password]')
|| $(':focus').is('textarea'))) {
if (!document.querySelector(_focusedElementsSelector)) {
if (_shortcuts.has(key)) {
_shortcuts.get(key).function(e);
} else if (!isNaN(num) && num >= 0 && num <= 9) {
APP.store.dispatch(clickOnVideo(num));
}
}
};
window.onkeydown = e => {
if (!enabled) {
if (!this.getEnabled()) {
return;
}
if (!($(':focus').is('input[type=text]')
|| $(':focus').is('input[type=password]')
|| $(':focus').is('textarea'))) {
const focusedElement = document.querySelector(_focusedElementsSelector);
if (!focusedElement) {
if (this._getKeyboardKey(e).toUpperCase() === ' ') {
if (APP.conference.isLocalAudioMuted()) {
sendAnalytics(createShortcutEvent(
@ -75,8 +97,14 @@ const KeyboardShortcut = {
PRESSED));
logger.log('Talk shortcut pressed');
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.
*/
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
// message
this.registerShortcut(' ', null, () => {
sendAnalytics(createShortcutEvent('push.to.talk', RELEASED));
logger.log('Talk shortcut released');
APP.conference.muteAudio(true);
if (this.isPushToTalkActive) {
sendAnalytics(createShortcutEvent('push.to.talk', RELEASED));
logger.log('Talk shortcut released');
APP.conference.muteAudio(true);
this.isPushToTalkActive = false;
}
});
this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk');

View File

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

47
package-lock.json generated
View File

@ -15262,12 +15262,23 @@
}
},
"react-textarea-autosize": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-7.1.0.tgz",
"integrity": "sha512-c2FlR/fP0qbxmlrW96SdrbgP/v0XZMTupqB90zybvmDVDutytUgPl7beU35klwcTeMepUIQEpQUn3P3bdshGPg==",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.3.0.tgz",
"integrity": "sha512-3GLWFAan2pbwBeoeNDoqGmSbrShORtgWfaWX0RJDivsUrpShh01saRM5RU/i4Zmf+whpBVEY5cA90Eq8Ub1N3w==",
"requires": {
"@babel/runtime": "^7.1.2",
"prop-types": "^15.6.0"
"@babel/runtime": "^7.10.2",
"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": {
@ -17690,6 +17701,11 @@
"integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==",
"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": {
"version": "1.9.3",
"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",
"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": {
"version": "1.1.2",
"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-youtube-iframe": "1.2.3",
"react-redux": "7.1.0",
"react-textarea-autosize": "7.1.0",
"react-textarea-autosize": "8.3.0",
"react-transition-group": "2.4.0",
"react-youtube": "7.13.1",
"redux": "4.0.4",

View File

@ -2,6 +2,7 @@
import React from 'react';
import { translate } from '../../../../base/i18n';
import { Icon } from '../../../icons';
import AbstractStatelessAvatar, { type Props as AbstractProps } from '../AbstractStatelessAvatar';
@ -30,14 +31,19 @@ type Props = AbstractProps & {
/**
* 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
* props.
*/
export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
class StatelessAvatar extends AbstractStatelessAvatar<Props> {
/**
* Implements {@code Component#render}.
*
@ -64,6 +70,7 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
return (
<div className = { this._getBadgeClassName() }>
<img
alt = { this.props.t('profile.avatar') }
className = { this._getAvatarClassName() }
data-testid = { this.props.testId }
id = { this.props.id }
@ -88,7 +95,7 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
xmlnsXlink = 'http://www.w3.org/1999/xlink'>
<text
dominantBaseline = 'central'
fill = 'rgba(255,255,255,.6)'
fill = 'rgba(255,255,255,1)'
fontSize = '40pt'
textAnchor = 'middle'
x = '50'
@ -104,6 +111,7 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
return (
<div className = { this._getBadgeClassName() }>
<img
alt = { this.props.t('profile.avatar') }
className = { this._getAvatarClassName('defaultAvatar') }
data-testid = { this.props.testId }
id = { this.props.id }
@ -157,3 +165,5 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
_isIcon: (?string | ?Object) => boolean
}
export default translate(StatelessAvatar);

View File

@ -3,7 +3,6 @@
import React, { useState } from 'react';
import { Icon, IconCheck, IconCopy } from '../../base/icons';
import { translate } from '../i18n';
import { copyText } from '../util';
@ -32,7 +31,12 @@ type Props = {
/**
* 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>}
*/
function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnCopySuccess }: Props) {
function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnCopySuccess, id }: Props) {
const [ isClicked, setIsClicked ] = useState(false);
const [ isHovered, setIsHovered ] = useState(false);
@ -83,6 +87,20 @@ function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnC
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.
*
@ -93,7 +111,7 @@ function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnC
return (
<>
<div className = 'copy-button-content selected'>
{textOnCopySuccess}
<span role = { 'alert' }>{ textOnCopySuccess }</span>
</div>
<Icon src = { IconCheck } />
</>
@ -112,10 +130,17 @@ function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnC
return (
<div
aria-label = { textOnHover }
className = { `${className} copy-button${isClicked ? ' clicked' : ''}` }
id = { id }
onBlur = { onHoverOut }
onClick = { onClick }
onFocus = { onHoverIn }
onKeyPress = { onKeyPress }
onMouseOut = { onHoverOut }
onMouseOver = { onHoverIn }>
onMouseOver = { onHoverIn }
role = 'button'
tabIndex = { 0 }>
{ renderContent() }
</div>
);
@ -125,4 +150,4 @@ CopyButton.defaultProps = {
className: ''
};
export default translate(CopyButton);
export default CopyButton;

View File

@ -10,6 +10,7 @@ import {
} from '@atlaskit/modal-dialog/dist/es2019/styled/Content';
import React from 'react';
import { translate } from '../../../i18n';
import { Icon, IconClose } from '../../../icons';
const TitleIcon = ({ appearance }: { appearance?: 'danger' | 'warning' }) => {
@ -45,11 +46,40 @@ type Props = {
* @class ModalHeader
* @extends {React.Component<Props>}
*/
export default class ModalHeader extends React.Component<Props> {
class ModalHeader extends React.Component<Props> {
static defaultProps = {
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()}.
*
@ -65,7 +95,8 @@ export default class ModalHeader extends React.Component<Props> {
onClose,
showKeyline,
isHeadingMultiline,
testId
testId,
t
} = this.props;
if (!heading) {
@ -83,12 +114,18 @@ export default class ModalHeader extends React.Component<Props> {
{heading}
</TitleText>
</Title>
{
!hideCloseIconButton && <Icon
ariaLabel = { t('dialog.close') }
onClick = { onClose }
src = { IconClose } />
onKeyPress = { this._onKeyPress }
role = 'button'
src = { IconClose }
tabIndex = { 0 } />
}
</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.
this._onCancel = this._onCancel.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._renderFooter = this._renderFooter.bind(this);
this._setDialogElement = this._setDialogElement.bind(this);
@ -159,7 +159,7 @@ class StatelessDialog extends Component<Props> {
shouldCloseOnEscapePress = { true }
width = { width || 'medium' }>
<div
onKeyDown = { this._onKeyDown }
onKeyPress = { this._onKeyPress }
ref = { this._setDialogElement }>
<form
className = 'modal-dialog-form'
@ -327,7 +327,7 @@ class StatelessDialog extends Component<Props> {
this._dialogElement = element;
}
_onKeyDown: (Object) => void;
_onKeyPress: (Object) => void;
/**
* Handles 'Enter' key in the dialog to submit/hide dialog depending on
@ -337,7 +337,7 @@ class StatelessDialog extends Component<Props> {
* @private
* @returns {void}
*/
_onKeyDown(event) {
_onKeyPress(event) {
// If the event coming to the dialog has been subject to preventDefault
// we don't handle it here.
if (event.defaultPrevented) {

View File

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

View File

@ -1,6 +1,6 @@
// @flow
import React from 'react';
import React, { useCallback } from 'react';
import { Container } from '../../react/base';
import { styleTypeToObject } from '../../styles';
@ -10,7 +10,7 @@ type Props = {
/**
* Class name for the web platform, if any.
*/
className: string,
className?: string,
/**
* Color of the icon (if not provided by the style object).
@ -22,6 +22,11 @@ type Props = {
*/
id?: string,
/**
* Id of the icon container
*/
containerId?: string,
/**
* Function to invoke on click.
*/
@ -40,8 +45,63 @@ type Props = {
/**
* 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_SIZE = navigator.product === 'ReactNative' ? 36 : 22;
@ -57,11 +117,24 @@ export default function Icon(props: Props) {
className,
color,
id,
containerId,
onClick,
size,
src: IconComponent,
style
} = props;
style,
ariaHasPopup,
ariaLabel,
ariaDisabled,
ariaExpanded,
ariaControls,
tabIndex,
ariaPressed,
ariaDescribedBy,
role,
onKeyPress,
onKeyDown,
...rest
}: Props = props;
const {
color: styleColor,
@ -71,11 +144,33 @@ export default function Icon(props: Props) {
const calculatedColor = color ?? styleColor ?? DEFAULT_COLOR;
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 (
<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 }
style = { restStyle }>
onKeyDown = { onKeyDown }
onKeyPress = { onKeyPressHandler }
role = { role }
style = { restStyle }
tabIndex = { tabIndex }>
<IconComponent
fill = { calculatedColor }
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.
this._onHideDialog = this._onHideDialog.bind(this);
this._onShowDialog = this._onShowDialog.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
this._drawerContainerRef = React.createRef();
this._onEscKey = this._onEscKey.bind(this);
}
/**
@ -207,6 +209,7 @@ class Popover extends Component<Props, State> {
<div
className = { className }
id = { id }
onKeyPress = { this._onKeyPress }
onMouseEnter = { this._onShowDialog }
onMouseLeave = { this._onHideDialog }>
<InlineDialog
@ -231,13 +234,13 @@ class Popover extends Component<Props, State> {
this.setState({ showDialog: false });
}
_onShowDialog: () => void;
_onShowDialog: (Object) => void;
/**
* Displays the {@code InlineDialog} and calls any registered onPopoverOpen
* callbacks.
*
* @param {MouseEvent} event - The mouse event to intercept.
* @param {Object} event - The mouse event or the keypress event to intercept.
* @private
* @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}.
* 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;
return (
<div className = 'popover'>
<div
className = 'popover'
onKeyDown = { this._onEscKey }>
{ content }
<div className = 'popover-mouse-padding-top' />
<div className = { _mapPositionToPaddingClass(position) } />

View File

@ -1,6 +1,6 @@
// @flow
import React from 'react';
import React, { useCallback } from 'react';
import { Icon, IconArrowDown } from '../../../icons';
@ -46,10 +46,36 @@ type Props = {
*/
onClick: Function,
/**
* 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,
type = 'primary',
onClick,
onOptionsClick
onOptionsClick,
tabIndex,
role,
ariaPressed,
ariaLabel,
ariaDropDownLabel
}: 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 (
<div
aria-disabled = { disabled }
aria-label = { ariaLabel }
className = { `action-btn ${className} ${type} ${disabled ? 'disabled' : ''}` }
data-testid = { testId ? testId : undefined }
onClick = { disabled ? undefined : onClick }>
onClick = { disabled ? undefined : onClick }
onKeyPress = { onKeyPressHandler }
role = 'button'
tabIndex = { 0 } >
{children}
{hasOptions && <div
className = 'options'
data-testid = 'prejoin.joinOptions'
onClick = { disabled ? undefined : onOptionsClick }>
<Icon
className = 'icon'
size = { 14 }
src = { OptionsIcon } />
</div>
{ hasOptions
&& <div
aria-disabled = { disabled }
aria-haspopup = 'true'
aria-label = { ariaDropDownLabel }
aria-pressed = { ariaPressed }
className = 'options'
data-testid = 'prejoin.joinOptions'
onClick = { disabled ? undefined : onOptionsClick }
onKeyPress = { onOptionsKeyPressHandler }
role = { role }
tabIndex = { tabIndex }>
<Icon
className = 'icon'
size = { 14 }
src = { OptionsIcon } />
</div>
}
</div>
);

View File

@ -1,6 +1,6 @@
// @flow
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import { translate } from '../../../i18n';
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-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 (
<div className = 'con-status'>
<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}` }>
<Icon
size = { 16 }
src = { icon } />
</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
ariaDescribedBy = 'connection-status-description'
ariaPressed = { showDetails }
className = { arrowClassName }
// eslint-disable-next-line react/jsx-no-bind
onClick = { () => toggleDetails(!showDetails) }
onClick = { onToggleDetails }
onKeyPress = { onKeyPressToggleDetails }
role = 'button'
size = { 24 }
src = { IconArrowDownSmall } />
src = { IconArrowDownSmall }
tabIndex = { 0 } />
</div>
<div className = { `con-status-details ${detailsClassName}` }>{detailsText}</div>
<div
aria-level = '2'
className = { `con-status-details ${detailsClassName}` }
role = 'heading'>
{detailsText}</div>
</div>
</div>
);

View File

@ -2,11 +2,11 @@
import React, { Component } from 'react';
import CopyMeetingLinkSection
from '../../../../invite/components/add-people-dialog/web/CopyMeetingLinkSection';
import { getCurrentConferenceUrl } from '../../../connection';
import { translate } from '../../../i18n';
import { Icon, IconCopy, IconCheck } from '../../../icons';
import { connect } from '../../../redux';
import { copyText, getDecodedURI } from '../../../util';
type Props = {
@ -27,152 +27,11 @@ type Props = {
_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.
*/
class CopyMeetingUrl extends Component<Props, State> {
/**
* 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);
}
}
class CopyMeetingUrl extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
@ -181,29 +40,9 @@ class CopyMeetingUrl extends Component<Props, State> {
* @returns {ReactElement}
*/
render() {
const { showCopyLink, showLinkCopied } = this.state;
const { url, t } = this.props;
const { _copyUrl, _showCopyLink, _hideCopyLink } = this;
const src = showLinkCopied ? IconCheck : IconCopy;
return (
<div
className = 'copy-meeting'
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 className = 'copy-meeting'>
<CopyMeetingLinkSection url = { this.props.url } />
</div>
);
}

View File

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

View File

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

View File

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

View File

@ -4,7 +4,8 @@ import React, { Component } from 'react';
import {
getLocalizedDateFormatter,
getLocalizedDurationFormatter
getLocalizedDurationFormatter,
translate
} from '../../../i18n';
import { Icon, IconTrash } from '../../../icons';
@ -41,7 +42,12 @@ type Props = {
/**
* 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
*/
export default class MeetingsList extends Component<Props> {
class MeetingsList extends Component<Props> {
/**
* Constructor of the MeetingsList component.
*
@ -99,7 +105,7 @@ export default class MeetingsList extends Component<Props> {
* @returns {React.ReactNode}
*/
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
@ -107,7 +113,10 @@ export default class MeetingsList extends Component<Props> {
if (meetings) {
return (
<Container
className = 'meetings-list'>
aria-label = { t('welcomepage.recentList') }
className = 'meetings-list'
role = 'menu'
tabIndex = '-1'>
{
meetings.length === 0
? listEmptyComponent
@ -139,6 +148,29 @@ export default class MeetingsList extends Component<Props> {
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;
/**
@ -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;
/**
@ -176,17 +229,22 @@ export default class MeetingsList extends Component<Props> {
title,
url
} = meeting;
const { hideURL = false, onItemDelete } = this.props;
const { hideURL = false, onItemDelete, t } = this.props;
const onPress = this._onPress(url);
const onKeyPress = this._onKeyPress(url);
const rootClassName
= `item ${
onPress ? 'with-click-handler' : 'without-click-handler'}`;
return (
<Container
aria-label = { title }
className = { rootClassName }
key = { index }
onClick = { onPress }>
onClick = { onPress }
onKeyPress = { onKeyPress }
role = 'menuitem'
tabIndex = { 0 }>
<Container className = 'left-column'>
<Text className = 'title'>
{ _toDateString(date) }
@ -216,11 +274,17 @@ export default class MeetingsList extends Component<Props> {
{ elementAfter || null }
{ onItemDelete && <Icon
ariaLabel = { t('welcomepage.recentListDelete') }
className = 'delete-meeting'
onClick = { this._onDelete(meeting) }
src = { IconTrash } />}
onKeyPress = { this._onDeleteKeyPress(meeting) }
role = 'button'
src = { IconTrash }
tabIndex = { 0 } />}
</Container>
</Container>
);
}
}
export default translate(MeetingsList);

View File

@ -160,13 +160,15 @@ class Watermarks extends Component<Props, State> {
_logoUrl,
_showJitsiWatermark
} = this.props;
const { t } = this.props;
let reactElement = null;
if (_showJitsiWatermark) {
const style = {
backgroundImage: `url(${_logoUrl})`,
maxWidth: 140,
maxHeight: 70
maxHeight: 70,
position: _logoLink ? 'static' : 'absolute'
};
reactElement = (<div
@ -176,6 +178,8 @@ class Watermarks extends Component<Props, State> {
if (_logoLink) {
reactElement = (
<a
aria-label = { t('jitsiHome', { logo: interfaceConfig.APP_NAME }) }
className = 'watermark leftwatermark'
href = { _logoLink }
target = '_new'>
{ 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
* accordingly.
*
* @param {Object} e - Event.
* @private
* @returns {void}
*/
_onClick() {
_onClick(e) {
const { afterClick } = this.props;
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';
type Props = {
/**
* Invoked to obtain translated strings.
*/
t: Function
};

View File

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

View File

@ -74,6 +74,35 @@ class OverflowMenuItem extends Component<Props> {
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()}.
*
@ -89,9 +118,13 @@ class OverflowMenuItem extends Component<Props> {
return (
<li
aria-disabled = { disabled }
aria-label = { accessibilityLabel }
className = { className }
onClick = { disabled ? null : onClick }>
onClick = { disabled ? null : onClick }
onKeyPress = { this._onKeyPress }
role = 'menuitem'
tabIndex = { 0 }>
<span className = 'overflow-menu-item-icon'>
<Icon
id = { iconId }

View File

@ -36,6 +36,36 @@ type Props = {
* Additional styles.
*/
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,
iconTooltip,
onIconClick,
styles
onIconKeyDown,
styles,
ariaLabel,
ariaHasPopup,
ariaControls,
ariaExpanded,
iconId
} = props;
const iconProps = {};
@ -62,6 +98,12 @@ export default function ToolboxButtonWithIcon(props: Props) {
} else {
iconProps.className = 'settings-button-small-icon';
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'>
<Icon
{ ...iconProps }
ariaHasPopup = { ariaHasPopup }
ariaLabel = { ariaLabel }
size = { 9 }
src = { icon } />
</Tooltip>

View File

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

View File

@ -72,6 +72,7 @@ class CalendarList extends AbstractPage<Props> {
this._getRenderListEmptyComponent
= this._getRenderListEmptyComponent.bind(this);
this._onOpenSettings = this._onOpenSettings.bind(this);
this._onKeyPressOpenSettings = this._onKeyPressOpenSettings.bind(this);
this._onRefreshEvents = this._onRefreshEvents.bind(this);
}
@ -187,7 +188,9 @@ class CalendarList extends AbstractPage<Props> {
return (
<div className = 'meetings-list-empty'>
<div className = 'meetings-list-empty-image'>
<img src = './images/calendar.svg' />
<img
alt = { t('welcomepage.logo.calendar') }
src = './images/calendar.svg' />
</div>
<div className = 'description'>
{ t('welcomepage.connectCalendarText', {
@ -197,7 +200,9 @@ class CalendarList extends AbstractPage<Props> {
</div>
<div
className = 'meetings-list-empty-button'
onClick = { this._onOpenSettings }>
onClick = { this._onOpenSettings }
onKeyPress = { this._onKeyPressOpenSettings }
role = 'button'>
<Icon
className = 'meetings-list-empty-icon'
src = { IconPlusCalendar } />
@ -221,6 +226,22 @@ class CalendarList extends AbstractPage<Props> {
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;

View File

@ -45,6 +45,7 @@ class JoinButton extends Component<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);
}
/**
@ -60,7 +61,9 @@ class JoinButton extends Component<Props> {
content = { t('calendarSync.joinTooltip') }>
<div
className = 'button join-button'
onClick = { this._onClick }>
onClick = { this._onClick }
onKeyPress = { this._onKeyPress }
role = 'button'>
<Icon
size = '14'
src = { IconAdd } />
@ -81,6 +84,22 @@ class JoinButton extends Component<Props> {
_onClick(event) {
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);

View File

@ -2,17 +2,29 @@
import React, { Component } from 'react';
import { translate } from '../../base/i18n';
/**
* The type of the React {@code Component} props of
* {@link MicrosoftSignInButton}.
*/
type Props = {
// The callback to invoke when {@code MicrosoftSignInButton} is clicked.
/**
* The callback to invoke when {@code MicrosoftSignInButton} is clicked.
*/
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
*/
export default class MicrosoftSignInButton extends Component<Props> {
class MicrosoftSignInButton extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
@ -33,6 +45,7 @@ export default class MicrosoftSignInButton extends Component<Props> {
className = 'microsoft-sign-in'
onClick = { this.props.onClick }>
<img
alt = { this.props.t('welcomepage.logo.microsoftLogo') }
className = 'microsoft-logo'
src = 'images/microsoftLogo.svg' />
<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 { connect } from '../../../base/redux';
import { toggleChat } from '../../actions.web';
import AbstractChat, {
_mapStateToProps,
type Props
@ -51,6 +52,8 @@ class Chat extends AbstractChat<Props> {
// Bind event handlers so they are only bound once for every instance.
this._renderPanelContent = this._renderPanelContent.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);
}
}
_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()}.
@ -135,7 +153,10 @@ class Chat extends AbstractChat<Props> {
*/
_renderChatHeader() {
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 (
<div
aria-haspopup = 'true'
className = { `sideToolbarContainer ${className}` }
id = 'sideToolbarContainer'>
id = 'sideToolbarContainer'
onKeyDown = { this._onEscClick } >
{ ComponentToRender }
</div>
);
@ -199,6 +222,18 @@ class Chat extends AbstractChat<Props> {
}
_onSendMessage: (string) => void;
_onToggleChat: () => void;
/**
* Toggles the chat window.
*
* @returns {Function}
*/
_onToggleChat() {
this.props.dispatch(toggleChat());
}
}
export default translate(connect(_mapStateToProps)(Chat));

View File

@ -1,7 +1,8 @@
// @flow
import React from 'react';
import React, { useCallback } from 'react';
import { translate } from '../../../base/i18n';
import { Icon, IconClose } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { toggleChat } from '../../actions.web';
@ -17,6 +18,11 @@ type Props = {
* An optional class name.
*/
className: string,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
@ -24,17 +30,31 @@ type Props = {
*
* @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 (
<div
className = { className || 'chat-dialog-header' }>
className = { className || 'chat-dialog-header' }
role = 'heading'>
{ t('chat.title') }
<Icon
ariaLabel = { t('toolbar.closeChat') }
onClick = { onCancel }
src = { IconClose } />
onKeyPress = { onKeyPressHandler }
role = 'button'
src = { IconClose }
tabIndex = { 0 } />
</div>
);
}
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._onSubmitMessage = this._onSubmitMessage.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);
}
@ -116,8 +119,15 @@ class ChatInput extends Component<Props, State> {
<div id = 'smileysarea'>
<div id = 'smileys'>
<div
aria-expanded = { this.state.showSmileysPanel }
aria-haspopup = 'smileysContainer'
aria-label = { this.props.t('chat.smileysPanel') }
className = 'smiley-button'
onClick = { this._onToggleSmileysPanel }>
onClick = { this._onToggleSmileysPanel }
onKeyDown = { this._onEscHandler }
onKeyPress = { this._onToggleSmileysPanelKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon src = { IconSmile } />
</div>
</div>
@ -129,19 +139,26 @@ class ChatInput extends Component<Props, State> {
</div>
<div className = 'usrmsg-form'>
<TextareaAutosize
autoComplete = 'off'
autoFocus = { true }
id = 'usermsg'
inputRef = { this._setTextAreaRef }
maxRows = { 5 }
onChange = { this._onMessageChange }
onHeightChange = { this.props.onResize }
onKeyDown = { this._onDetectSubmit }
placeholder = { this.props.t('chat.messagebox') }
ref = { this._setTextAreaRef }
tabIndex = { 0 }
value = { this.state.message } />
</div>
<div className = 'send-button-container'>
<div
aria-label = { this.props.t('chat.sendButton') }
className = 'send-button'
onClick = { this._onSubmitMessage }>
onClick = { this._onSubmitMessage }
onKeyPress = { this._onSubmitMessageKeyPress }
role = 'button'
tabIndex = { this.state.message.trim() ? 0 : -1 } >
<Icon src = { IconPlane } />
</div>
</div>
@ -192,14 +209,32 @@ class ChatInput extends Component<Props, State> {
* @returns {void}
*/
_onDetectSubmit(event) {
if (event.keyCode === 13
&& event.shiftKey === false) {
if (event.key === 'Enter'
&& event.shiftKey === false
&& event.ctrlKey === false) {
event.preventDefault();
event.stopPropagation();
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;
/**
@ -224,10 +259,16 @@ class ChatInput extends Component<Props, State> {
* @returns {void}
*/
_onSmileySelect(smileyText) {
this.setState({
message: `${this.state.message} ${smileyText}`,
showSmileysPanel: false
});
if (smileyText) {
this.setState({
message: `${this.state.message} ${smileyText}`,
showSmileysPanel: false
});
} else {
this.setState({
showSmileysPanel: false
});
}
this._focus();
}
@ -241,9 +282,44 @@ class ChatInput extends Component<Props, State> {
* @returns {void}
*/
_onToggleSmileysPanel() {
if (this.state.showSmileysPanel) {
this._focus();
}
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;

View File

@ -23,7 +23,7 @@ class ChatMessage extends AbstractChatMessage<Props> {
* @returns {ReactElement}
*/
render() {
const { message } = this.props;
const { message, t } = this.props;
const processedMessage = [];
// content is an array of text and emoji components
@ -38,12 +38,20 @@ class ChatMessage extends AbstractChatMessage<Props> {
});
return (
<div className = 'chatmessage-wrapper'>
<div
className = 'chatmessage-wrapper'
tabIndex = { -1 }>
<div className = { `chatmessage ${message.privateMessage ? 'privatemessage' : ''}` }>
<div className = 'replywrapper'>
<div className = 'messagecontent'>
{ this.props.showDisplayName && this._renderDisplayName() }
<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 }
</div>
{ message.privateMessage && this._renderPrivateNotice() }
@ -77,7 +85,9 @@ class ChatMessage extends AbstractChatMessage<Props> {
*/
_renderDisplayName() {
return (
<div className = 'display-name'>
<div
aria-hidden = { true }
className = 'display-name'>
{ this.props.message.displayName }
</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.
this._onDisplayNameChange = this._onDisplayNameChange.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'>
<form onSubmit = { this._onSubmit }>
<FieldTextStateless
aria-describedby = 'nickname-title'
autoComplete = 'name'
autoFocus = { true }
compact = { true }
id = 'nickinput'
@ -86,7 +89,10 @@ class DisplayNameForm extends Component<Props, State> {
</form>
<div
className = { `enter-chat${this.state.displayName.trim() ? '' : ' disabled'}` }
onClick = { this._onSubmit }>
onClick = { this._onSubmit }
onKeyPress = { this._onKeyPress }
role = 'button'
tabIndex = { 0 }>
{ t('chat.enter') }
</div>
<KeyboardAvoider />
@ -125,6 +131,21 @@ class DisplayNameForm extends Component<Props, State> {
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));

View File

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

View File

@ -15,6 +15,35 @@ import AbstractMessageRecipient, {
* Class to implement the displaying of the recipient of the next message.
*/
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}.
*
@ -30,13 +59,20 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
const { t } = this.props;
return (
<div id = 'chat-recipient'>
<div
id = 'chat-recipient'
role = 'alert'>
<span>
{ t('chat.messageTo', {
recipient: _privateMessageRecipient
}) }
</span>
<div onClick = { this.props._onRemovePrivateMessageRecipient }>
<div
aria-label = { t('dialog.close') }
onClick = { this.props._onRemovePrivateMessageRecipient }
onKeyPress = { this._onKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon
src = { IconCancelSelection } />
</div>

View File

@ -23,6 +23,69 @@ type Props = {
* @extends Component
*/
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()}.
*
@ -30,40 +93,33 @@ class SmileysPanel extends PureComponent<Props> {
* @returns {ReactElement}
*/
render() {
const smileyItems = Object.keys(smileys).map(smileyKey => {
const onSelectFunction = this._getOnSmileySelectCallback(smileyKey);
return (
<div
className = 'smileyContainer'
id = { smileyKey }
key = { smileyKey }>
<Emoji
onClick = { onSelectFunction }
onlyEmojiClassName = 'smiley'
text = { smileys[smileyKey] } />
</div>
);
});
const smileyItems = Object.keys(smileys).map(smileyKey => (
<div
className = 'smileyContainer'
id = { smileyKey }
key = { smileyKey }
onClick = { this._onClick }
onKeyDown = { this._onEscKey }
onKeyPress = { this._onKeyPress }
role = 'option'
tabIndex = { 0 }>
<Emoji
onlyEmojiClassName = 'smiley'
text = { smileys[smileyKey] } />
</div>
));
return (
<div id = 'smileysContainer'>
<div
aria-orientation = 'horizontal'
id = 'smileysContainer'
onKeyDown = { this._onEscKey }
role = 'listbox'
tabIndex = { -1 }>
{ smileyItems }
</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;

View File

@ -107,6 +107,8 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
this._onInstallExtensionClick = this._onInstallExtensionClick.bind(this);
this._shouldNotRender = this._shouldNotRender.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 });
}
_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;
/**
@ -182,6 +200,22 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
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;
/**
@ -236,16 +270,23 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
return (
<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
className = 'chrome-extension-banner__icon-container' />
<div
className = 'chrome-extension-banner__text-container'>
className = 'chrome-extension-banner__text-container'
id = 'chrome-extension-banner__text-container'>
{ t('chromeExtensionBanner.installExtensionText') }
</div>
<div
aria-label = { t('chromeExtensionBanner.close') }
className = 'chrome-extension-banner__close-container'
onClick = { this._onClosePressed }>
onClick = { this._onClosePressed }
onKeyPress = { this._onCloseKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon
className = 'gray'
size = { 12 }
@ -255,18 +296,28 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
<div
className = 'chrome-extension-banner__button-container'>
<div
aria-labelledby = 'chrome-extension-banner__button-text'
className = 'chrome-extension-banner__button-open-url'
onClick = { this._onInstallExtensionClick }>
onClick = { this._onInstallExtensionClick }
onKeyPress = { this._onInstallExtensionKeyPress }
role = 'button'
tabIndex = { 0 }>
<div
className = 'chrome-extension-banner__button-text'>
className = 'chrome-extension-banner__button-text'
id = 'chrome-extension-banner__button-text'>
{ t('chromeExtensionBanner.buttonText') }
</div>
</div>
</div>
<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
aria-labelledby = 'chrome-extension-banner__checkbox-label'
checked = { this.state.dontShowAgainChecked }
id = 'chrome-extension-banner__checkbox'
onChange = { this._onDontShowAgainChange }
type = 'checkbox' />
&nbsp;{ t('chromeExtensionBanner.dontShowAgain') }

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,9 @@
import React, { Component } from 'react';
import { translate } from '../../base/i18n';
/**
* The type of the React {@code Component} props of
* {@link DesktopSourcePreview}.
@ -35,7 +38,12 @@ type Props = {
/**
* 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 }>
<div className = 'desktop-source-preview-image-container'>
<img
alt = { this.props.t('welcomepage.logo.desktopPreviewThumbnail') }
className = 'desktop-source-preview-thumbnail'
src = { this.props.source.thumbnail.toDataURL() } />
</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._onClick = this._onClick.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
}
/**
@ -66,7 +67,12 @@ class AudioOutputPreview extends Component<Props> {
render() {
return (
<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') }
</a>
<Audio
@ -105,6 +111,22 @@ class AudioOutputPreview extends Component<Props> {
&& 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.
*

View File

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

View File

@ -51,7 +51,12 @@ type Props = {
/**
* 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}
*/
render() {
if (this.props.hasPermission === undefined) {
return null;
}
if (!this.props.hasPermission) {
return this._renderNoPermission();
}
@ -134,14 +143,10 @@ class DeviceSelector extends Component<Props> {
_createDropdownItem(device) {
return (
<DropdownItem
data-deviceid = { device.deviceId }
isSelected = { device.deviceId === this.props.selectedDeviceId }
key = { device.deviceId }
// eslint-disable-next-line react/jsx-no-bind
onClick = {
e => {
e.stopPropagation();
this._onSelect(device.deviceId);
}
}>
onClick = { this._onSelect }>
{ device.label || device.deviceId }
</DropdownItem>
);
@ -183,7 +188,8 @@ class DeviceSelector extends Component<Props> {
shouldFitContainer = { true }
trigger = { triggerText }
triggerButtonProps = {{
shouldFitContainer: true
shouldFitContainer: true,
id: this.props.id
}}
triggerType = 'button'>
<DropdownItemGroup>
@ -199,13 +205,16 @@ class DeviceSelector extends Component<Props> {
/**
* 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
* @returns {void}
*/
_onSelect(newDeviceId) {
if (this.props.selectedDeviceId !== newDeviceId) {
this.props.onSelect(newDeviceId);
_onSelect(e) {
const deviceId = e.currentTarget.getAttribute('data-deviceid');
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.
this._onExpand = this._onExpand.bind(this);
this._onExpandKeyPress = this._onExpandKeyPress.bind(this);
this._onToggle = this._onToggle.bind(this);
}
@ -101,12 +102,20 @@ class E2EESection extends Component<Props, State> {
return (
<div id = 'e2ee-section'>
<p className = 'description'>
<p
aria-live = 'polite'
className = 'description'
id = 'e2ee-section-description'>
{ expand && description }
{ !expand && description.substring(0, 100) }
{ !expand && <span
aria-controls = 'e2ee-section-description'
aria-expanded = { expand }
className = 'read-more'
onClick = { this._onExpand }>
onClick = { this._onExpand }
onKeyPress = { this._onExpandKeyPress }
role = 'button'
tabIndex = { 0 }>
... { t('dialog.readMore') }
</span> }
</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;
/**

View File

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

View File

@ -36,10 +36,28 @@ function EmbedMeetingTrigger({ t, openEmbedDialog }: Props) {
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 (
<div
aria-label = { t('embedMeeting.title') }
className = 'embed-meeting-trigger'
onClick = { onClick }>
onClick = { onClick }
onKeyPress = { onKeyPress }
role = 'button'
tabIndex = { 0 }>
{t('embedMeeting.title')}
</div>
);

View File

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

View File

@ -13,7 +13,8 @@ import { translate } from '../../../base/i18n';
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
import { getLocalParticipant } from '../../../base/participants';
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 { setFilmstripVisible } from '../../actions';
import { shouldRemoteVideosBeVisible } from '../../functions';
@ -83,6 +84,11 @@ type Props = {
*/
_visible: boolean,
/**
* Whether or not the toolbox is displayed.
*/
_isToolboxVisible: Boolean,
/**
* 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.
this._onShortcutToggleFilmstrip = this._onShortcutToggleFilmstrip.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.
*
@ -298,12 +318,18 @@ class Filmstrip extends Component <Props> {
const { t } = this.props;
return (
<div className = 'filmstrip__toolbar'>
<div
className = 'filmstrip__toolbar'>
<button
aria-expanded = { this.props._visible }
aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }
id = 'toggleFilmstripButton'
onClick = { this._onToolbarToggleFilmstrip }>
<Icon src = { icon } />
onClick = { this._onToolbarToggleFilmstrip }
onFocus = { this._onTabIn }
tabIndex = { 0 }>
<Icon
aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }
src = { icon } />
</button>
</div>
);
@ -342,7 +368,8 @@ function _mapStateToProps(state) {
_participants: state['features/base/participants'],
_rows: gridDimensions.rows,
_videosClassName: videosClassName,
_visible: visible
_visible: visible,
_isToolboxVisible: isToolboxVisible(state)
};
}

View File

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

View File

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

View File

@ -28,10 +28,12 @@ type Props = {
function CopyMeetingLinkSection({ t, url }: Props) {
return (
<>
<span>{t('addPeople.shareLink')}</span>
<label htmlFor = { 'copy-button-id' }>{t('addPeople.shareLink')}</label>
<CopyButton
aria-label = { t('addPeople.copyLink') }
className = 'invite-more-dialog-conference-url'
displayedText = { getDecodedURI(url) }
id = 'copy-button-id'
textOnCopySuccess = { t('addPeople.linkCopied') }
textOnHover = { t('addPeople.copyLink') }
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.
this._onCopyText = this._onCopyText.bind(this);
this._onCopyTextKeyPress = this._onCopyTextKeyPress.bind(this);
}
_onCopyText: () => void;
@ -68,6 +69,22 @@ class DialInNumber extends Component<Props> {
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()}.
*
@ -101,8 +118,12 @@ class DialInNumber extends Component<Props> {
</span>
</div>
<a
aria-label = { t('info.copyNumber') }
className = 'dial-in-copy'
onClick = { this._onCopyText }>
onClick = { this._onCopyText }
onKeyPress = { this._onCopyTextKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon src = { IconCopy } />
</a>
</div>

View File

@ -52,6 +52,20 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
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.
*
@ -61,6 +75,20 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
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
* containing a conference invite.
@ -101,6 +129,7 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
key = { idx }
position = 'top'>
<a
aria-label = { t(tooltipKey) }
className = 'provider-icon'
href = { url }
rel = 'noopener noreferrer'
@ -119,8 +148,13 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
<>
<div>
<div
aria-expanded = { isActive }
aria-label = { t('addPeople.shareInvite') }
className = { `invite-more-dialog email-container${isActive ? ' active' : ''}` }
onClick = { _onToggleActiveState }>
onClick = { _onToggleActiveState }
onKeyPress = { _onToggleActiveStateKeyPress }
role = 'button'
tabIndex = { 0 }>
<span>{t('addPeople.shareInvite')}</span>
<Icon src = { IconArrowDownSmall } />
</div>
@ -129,8 +163,12 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
content = { t('addPeople.copyInvite') }
position = 'top'>
<div
aria-label = { t('addPeople.copyInvite') }
className = 'copy-invite-icon'
onClick = { _onCopyText }>
onClick = { _onCopyText }
onKeyPress = { _onCopyTextKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon src = { IconCopy } />
</div>
</Tooltip>

View File

@ -76,9 +76,11 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
// Bind event handlers so they are only bound once per instance.
this._onClearItems = this._onClearItems.bind(this);
this._onClearItemsKeyPress = this._onClearItemsKeyPress.bind(this);
this._onItemSelected = this._onItemSelected.bind(this);
this._onSelectionChange = this._onSelectionChange.bind(this);
this._onSubmit = this._onSubmit.bind(this);
this._onSubmitKeyPress = this._onSubmitKeyPress.bind(this);
this._parseQueryResults = this._parseQueryResults.bind(this);
this._setMultiSelectElement = this._setMultiSelectElement.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;
/**
@ -425,6 +443,22 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
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.
*
@ -441,13 +475,21 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
return (
<div className = { `invite-more-dialog invite-buttons${this._isAddDisabled() ? ' disabled' : ''}` }>
<a
aria-label = { t('dialog.Cancel') }
className = 'invite-more-dialog invite-buttons-cancel'
onClick = { this._onClearItems }>
onClick = { this._onClearItems }
onKeyPress = { this._onClearItemsKeyPress }
role = 'button'
tabIndex = { 0 }>
{t('dialog.Cancel')}
</a>
<a
aria-label = { t('addPeople.add') }
className = 'invite-more-dialog invite-buttons-add'
onClick = { this._onSubmit }>
onClick = { this._onSubmit }
onKeyPress = { this._onSubmitKeyPress }
role = 'button'
tabIndex = { 0 }>
{t('addPeople.add')}
</a>
</div>
@ -480,9 +522,9 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
</span>
<span>
<a
aria-label = { supportLink }
href = { supportLink }
rel = 'noopener noreferrer'
target = '_blank'>
rel = 'noopener noreferrer'>
{ t('inlineDialogFailure.support') }
</a>
</span>

View File

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

View File

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

View File

@ -89,7 +89,9 @@ class LobbySection extends PureComponent<Props, State> {
return (
<>
<div id = 'lobby-section'>
<p className = 'description'>
<p
className = 'description'
role = 'banner'>
{ t('lobby.enableDialogText') }
</p>
<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-link'>
{ isEngaged ? <a
onClick = { this._onStop }>
onClick = { this._onStop }
role = 'button'
tabIndex = { 0 }>
{ t('localRecording.stop') }
</a>
: <a
onClick = { this._onStart }>
onClick = { this._onStart }
role = 'button'
tabIndex = { 0 }>
{ t('localRecording.start') }
</a>
}

View File

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

View File

@ -3,6 +3,7 @@
import { FlagGroup } from '@atlaskit/flag';
import React from 'react';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractNotificationsContainer, {
_abstractMapStateToProps,
@ -16,7 +17,12 @@ type Props = AbstractProps & {
/**
* 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 (
<FlagGroup
id = 'notifications-container'
label = { this.props.t('notify.groupTitle') }
onDismissed = { this._onDismissed }>
{ this._renderFlags() }
</FlagGroup>
@ -73,7 +80,6 @@ class NotificationsContainer extends AbstractNotificationsContainer<Props> {
key = { uid }
onDismissed = { this._onDismissed }
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 (
<OverlayFrame isLightOverlay = { isNetworkFailure }>
<div className = 'inlay'>
<div
aria-describedby = 'reload_overlay_text'
aria-labelledby = 'reload_overlay_title'
className = 'inlay'
role = 'dialog'>
<span
className = 'reload_overlay_title'>
className = 'reload_overlay_title'
id = 'reload_overlay_title'
role = 'heading'>
{ t(title) }
</span>
<span className = 'reload_overlay_text'>
<span
className = 'reload_overlay_text'
id = 'reload_overlay_text'>
{ t(message, { seconds: timeLeft }) }
</span>
{ this._renderProgressBar() }

View File

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

View File

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

View File

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

View File

@ -30,6 +30,12 @@ export const ParticipantsPane = () => {
const { t } = useTranslation();
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 ]);
return (
@ -41,7 +47,12 @@ export const ParticipantsPane = () => {
) }>
<div className = 'participants_pane-content'>
<Header>
<Close onClick = { closePane } />
<Close
aria-label = { t('participantsPane.close', 'Close') }
onClick = { closePane }
onKeyPress = { closePaneKeyPress }
role = 'button'
tabIndex = { 0 } />
</Header>
<Container>
<LobbyParticipantList />

View File

@ -183,6 +183,9 @@ class Prejoin extends Component<Props, State> {
this._onDropdownClose = this._onDropdownClose.bind(this);
this._onOptionsClick = this._onOptionsClick.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;
@ -205,6 +208,22 @@ class Prejoin extends Component<Props, State> {
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;
/**
@ -283,6 +302,40 @@ class Prejoin extends Component<Props, State> {
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()}.
*
@ -305,7 +358,8 @@ class Prejoin extends Component<Props, State> {
visibleButtons
} = 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;
return (
@ -322,10 +376,16 @@ class Prejoin extends Component<Props, State> {
{showJoinActions && (
<div className = 'prejoin-input-area-container'>
<div className = 'prejoin-input-area'>
<label
className = 'prejoin-input-area-label'
htmlFor = { 'Prejoin-input-field-id' } >
{ t('dialog.enterDisplayNameToJoin') }</label>
<InputField
autoComplete = { 'name' }
autoFocus = { true }
className = { showError ? 'error' : '' }
hasError = { showError }
id = { 'Prejoin-input-field-id' }
onChange = { _setName }
onSubmit = { joinConference }
placeHolder = { t('dialog.enterDisplayName') }
@ -341,7 +401,10 @@ class Prejoin extends Component<Props, State> {
<div
className = 'prejoin-preview-dropdown-btn'
data-testid = 'prejoin.joinWithoutAudio'
onClick = { joinConferenceWithoutAudio }>
onClick = { joinConferenceWithoutAudio }
onKeyPress = { _onJoinConferenceWithoutAudioKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon
className = 'prejoin-preview-dropdown-icon'
size = { 24 }
@ -350,7 +413,10 @@ class Prejoin extends Component<Props, State> {
</div>
{hasJoinByPhoneButton && <div
className = 'prejoin-preview-dropdown-btn'
onClick = { _showDialog }>
onClick = { _showDialog }
onKeyPress = { _showDialogKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon
className = 'prejoin-preview-dropdown-icon'
data-testid = 'prejoin.joinByPhone'
@ -363,9 +429,15 @@ class Prejoin extends Component<Props, State> {
onClose = { _onDropdownClose }>
<ActionButton
OptionsIcon = { showJoinByPhoneButtons ? IconArrowUp : IconArrowDown }
ariaDropDownLabel = { t('prejoin.joinWithoutAudio') }
ariaLabel = { t('prejoin.joinMeeting') }
ariaPressed = { showJoinByPhoneButtons }
hasOptions = { true }
onClick = { _onJoinButtonClick }
onKeyPress = { _onJoinKeyPress }
onOptionsClick = { _onOptionsClick }
role = 'button'
tabIndex = { 0 }
testId = 'prejoin.joinMeeting'
type = 'primary'>
{ t('prejoin.joinMeeting') }

View File

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

View File

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

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