mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-09 08:10:18 +00:00
Compare commits
18 Commits
7333
...
android-sd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5f91d31e4 | ||
|
|
febb50cd2c | ||
|
|
879e73c11a | ||
|
|
3ae18be21f | ||
|
|
9f5dbb21a7 | ||
|
|
2d14990b9e | ||
|
|
169c8ecb62 | ||
|
|
d608cf40f5 | ||
|
|
51a4e7daa3 | ||
|
|
7538bfc713 | ||
|
|
48e1f443ea | ||
|
|
2292ebe762 | ||
|
|
5425b52615 | ||
|
|
74f605e045 | ||
|
|
1918566581 | ||
|
|
ee8ba6696d | ||
|
|
15df3cb11e | ||
|
|
b77db024f5 |
12
.gitignore
vendored
12
.gitignore
vendored
@@ -94,3 +94,15 @@ twa/*.aab
|
||||
twa/assetlinks.json
|
||||
|
||||
tsconfig.json
|
||||
|
||||
# React Native SDK
|
||||
#
|
||||
react-native-sdk/android/src
|
||||
react-native-sdk/images
|
||||
react-native-sdk/ios
|
||||
react-native-sdk/lang
|
||||
react-native-sdk/modules
|
||||
react-native-sdk/node_modules
|
||||
react-native-sdk/react
|
||||
react-native-sdk/service
|
||||
react-native-sdk/sounds
|
||||
|
||||
@@ -26,5 +26,5 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.bundle.enableUncompressedNativeLibs=false
|
||||
|
||||
appVersion=99.0.0
|
||||
sdkVersion=99.0.0
|
||||
appVersion=23.2.0
|
||||
sdkVersion=8.2.1
|
||||
|
||||
@@ -41,3 +41,36 @@
|
||||
display: -webkit-flex !important;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* resets default button styles,
|
||||
* mostly intended to be used on interactive elements that
|
||||
* differ from their default styles (e.g. <a>) or have custom styles
|
||||
*/
|
||||
.invisible-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* style an element the same as an <a>
|
||||
* useful on some cases where we visually have a link but it's actually a <button>
|
||||
*/
|
||||
.as-link {
|
||||
@extend .invisible-button;
|
||||
|
||||
display: inline;
|
||||
color: #44A5FF;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
|
||||
&:focus,
|
||||
&:hover,
|
||||
&:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
&-actions {
|
||||
margin-top: 10px;
|
||||
a {
|
||||
button {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>99.0.0</string>
|
||||
<string>23.2.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>99.0.0</string>
|
||||
<string>23.2.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>99.0.0</string>
|
||||
<string>23.2.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>99.0.0</string>
|
||||
<string>23.2.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CLKComplicationPrincipalClass</key>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>99.0.0</string>
|
||||
<string>8.2.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>99.0.0</string>
|
||||
<string>8.2.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -39,6 +39,18 @@
|
||||
"audioOnly": {
|
||||
"audioOnly": "Geringe Bandbreite"
|
||||
},
|
||||
"bandwidthSettings": {
|
||||
"assumedBandwidthBps": "z.B. 10000000 für 10 Mbps",
|
||||
"assumedBandwidthBpsWarning": "Höhere Werte können zu Netzwerk-Problemen führen.",
|
||||
"customValue": "spezifischer Wert",
|
||||
"customValueEffect": "setzt den Wert in bps",
|
||||
"leaveEmpty": "leer lassen",
|
||||
"leaveEmptyEffect": "aktiviert die automatische Abschätzung",
|
||||
"possibleValues": "Mögliche Werte",
|
||||
"setAssumedBandwidthBps": "Angenommene Bandbreite (bps)",
|
||||
"title": "Einstellungen Bandbreite",
|
||||
"zeroEffect": "schaltet Video aus"
|
||||
},
|
||||
"breakoutRooms": {
|
||||
"actions": {
|
||||
"add": "Breakout-Raum hinzufügen",
|
||||
@@ -156,6 +168,7 @@
|
||||
"localport_plural": "Lokale Ports:",
|
||||
"maxEnabledResolution": "max. senden",
|
||||
"more": "Mehr anzeigen",
|
||||
"no": "Nein",
|
||||
"packetloss": "Paketverlust:",
|
||||
"participant_id": "Personen-ID:",
|
||||
"quality": {
|
||||
@@ -174,7 +187,8 @@
|
||||
"status": "Verbindung:",
|
||||
"transport": "Protokoll:",
|
||||
"transport_plural": "Protokolle:",
|
||||
"video_ssrc": "Video-SSRC:"
|
||||
"video_ssrc": "Video-SSRC:",
|
||||
"yes": "Ja"
|
||||
},
|
||||
"dateUtils": {
|
||||
"earlier": "Früher",
|
||||
@@ -673,6 +687,7 @@
|
||||
"connectedTwoMembers": "{{first}} und {{second}} nehmen am Meeting teil",
|
||||
"dataChannelClosed": "Schlechte Videoqualität",
|
||||
"dataChannelClosedDescription": "Die Steuerungsverbindung (Bridge Channel) wurde unterbrochen, daher ist die Videoqulität auf die schlechteste Stufe limitiert.",
|
||||
"disabledIframe": "Die Einbettung ist nur für Demo-Zwecke vorgesehen. Diese Konferenz wird in {{timeout}} Minuten beendet.",
|
||||
"disconnected": "getrennt",
|
||||
"displayNotifications": "Benachrichtigungen anzeigen für",
|
||||
"dontRemindMe": "Nicht erinnern",
|
||||
@@ -867,9 +882,11 @@
|
||||
"lookGood": "Ihr Mikrofon scheint zu funktionieren.",
|
||||
"or": "oder",
|
||||
"premeeting": "Vorschau",
|
||||
"proceedAnyway": "Trotzdem fortsetzen",
|
||||
"screenSharingError": "Fehler bei Bildschirmfreigabe:",
|
||||
"showScreen": "Konferenzvorschau aktivieren",
|
||||
"startWithPhone": "Mit Telefonaudio starten",
|
||||
"unsafeRoomConsent": "Ich verstehe das Risiko und möchte der Konferenz beitreten",
|
||||
"videoOnlyError": "Videofehler:",
|
||||
"videoTrackError": "Videotrack konnte nicht erstellt werden.",
|
||||
"viewAllNumbers": "alle Nummern anzeigen"
|
||||
@@ -971,8 +988,14 @@
|
||||
"security": {
|
||||
"about": "Sie können Ihre Konferenz mit einem Passwort sichern. Teilnehmer müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
|
||||
"aboutReadOnly": "Mit Moderationsrechten kann die Konferenz mit einem Passwort gesichert werden. Personen müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
|
||||
"insecureRoomNameWarning": "Der Raumname ist unsicher. Unerwünschte Teilnehmer könnten Ihrer Konferenz beitreten",
|
||||
"title": "Sicherheitsoptionen"
|
||||
"insecureRoomNameWarningNative": "Der Raumname ist unsicher. Unerwünschte Teilnehmer könnten Ihrer Konferenz beitreten. {{recommendAction}} Lernen Sie mehr über die Absicherung Ihrer Konferenz ",
|
||||
"insecureRoomNameWarningWeb": "Der Raumname ist unsicher. Unerwünschte Teilnehmer könnten Ihrer Konferenz beitreten {{recommendAction}} Lernen Sie <a href=\"{{securityUrl}}\" rel=\"security\" target=\"_blank\">hier</a> mehr über die Absicherung Ihrer Konferenz.",
|
||||
"title": "Sicherheitsoptionen",
|
||||
"unsafeRoomActions": {
|
||||
"meeting": "Erwägen Sie die Absicherung Ihrer Konferenz über den Sicherheits-Button.",
|
||||
"prejoin": "Erwägen Sie einen einzigartigeren Raumnamen zu wählen.",
|
||||
"welcome": "Erwägen Sie einen einzigartigeren Raumnamen zu wählen oder wählen Sie einen der Vorschläge."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"audio": "Audio",
|
||||
@@ -1140,6 +1163,7 @@
|
||||
"muteEveryoneElse": "Alle anderen stummschalten",
|
||||
"muteEveryoneElsesVideoStream": "Alle anderen Kameras ausschalten",
|
||||
"muteEveryonesVideoStream": "Alle Kameras ausschalten",
|
||||
"muteGUMPending": "Verbinde Ihr Mikrofon",
|
||||
"noiseSuppression": "Rauschunterdrückung",
|
||||
"openChat": "Chat öffnen",
|
||||
"participants": "Anwesende",
|
||||
@@ -1147,6 +1171,7 @@
|
||||
"privateMessage": "Private Nachricht senden",
|
||||
"profile": "Profil bearbeiten",
|
||||
"raiseHand": "Hand heben",
|
||||
"reactions": "Interaktionen",
|
||||
"reactionsMenu": "Interaktionsmenü öffnen / schließen",
|
||||
"recording": "Aufzeichnung ein-/ausschalten",
|
||||
"remoteMute": "Personen stummschalten",
|
||||
@@ -1172,6 +1197,7 @@
|
||||
"unmute": "Stummschaltung aufheben",
|
||||
"videoblur": "Unscharfer Hintergrund ein-/ausschalten",
|
||||
"videomute": "„Video stummschalten“ ein-/ausschalten",
|
||||
"videomuteGUMPending": "Verbinde Ihre Kamera",
|
||||
"videounmute": "Kamera einschalten"
|
||||
},
|
||||
"addPeople": "Personen zur Konferenz hinzufügen",
|
||||
@@ -1222,6 +1248,7 @@
|
||||
"mute": "Stummschalten",
|
||||
"muteEveryone": "Alle stummschalten",
|
||||
"muteEveryonesVideo": "Alle Kameras ausschalten",
|
||||
"muteGUMPending": "Verbinde Ihre Kamera",
|
||||
"noAudioSignalDesc": "Wenn Sie das Gerät nicht absichtlich über die Systemeinstellungen oder die Hardware stumm geschaltet haben, sollten Sie einen Wechsel des Geräts in Erwägung ziehen.",
|
||||
"noAudioSignalDescSuggestion": "Wenn Sie das Gerät nicht absichtlich über die Systemeinstellungen oder die Hardware stummgeschaltet haben, sollten Sie einen Wechsel auf das vorgeschlagene Gerät in Erwägung ziehen.",
|
||||
"noAudioSignalDialInDesc": "Sie können sich auch über die Einwahlnummer einwählen:",
|
||||
@@ -1244,6 +1271,7 @@
|
||||
"reactionLike": "Daumen hoch senden",
|
||||
"reactionSilence": "Stille senden",
|
||||
"reactionSurprised": "Überrascht senden",
|
||||
"reactions": "Interaktionen",
|
||||
"security": "Sicherheitsoptionen",
|
||||
"selectBackground": "Hintergrund auswählen",
|
||||
"shareRoom": "Person einladen",
|
||||
@@ -1266,6 +1294,7 @@
|
||||
"unmute": "Stummschaltung aufheben",
|
||||
"videoSettings": "Kameraeinstellungen",
|
||||
"videomute": "Kamera stoppen",
|
||||
"videomuteGUMPending": "Verbinde Ihre Kamera",
|
||||
"videounmute": "Kamera einschalten"
|
||||
},
|
||||
"transcribing": {
|
||||
@@ -1377,7 +1406,14 @@
|
||||
"webAssemblyWarning": "WebAssembly wird nicht unterstützt",
|
||||
"webAssemblyWarningDescription": "WebAssembly ist deaktiviert oder wird in diesem Browser nicht unterstützt"
|
||||
},
|
||||
"visitorsLabel": "Anzahl Gäste: {{count}}",
|
||||
"visitors": {
|
||||
"chatIndicator": "(Gast)",
|
||||
"labelTooltip": "Anzahl Gäste: {{count}}",
|
||||
"notification": {
|
||||
"description": "Bitte melden Sie sich um teilzunehmen",
|
||||
"title": "Sie sind Gast in der Konferenz"
|
||||
}
|
||||
},
|
||||
"volumeSlider": "Lautstärkeregler",
|
||||
"welcomepage": {
|
||||
"accessibilityLabel": {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"addPeople": {
|
||||
"accessibilityLabel": {
|
||||
"meetingLink": "Meeting link: {{url}}"
|
||||
},
|
||||
"add": "Invite",
|
||||
"addContacts": "Invite your contacts",
|
||||
"contacts": "contacts",
|
||||
@@ -254,6 +257,8 @@
|
||||
"WaitingForHostTitle": "Waiting for the host ...",
|
||||
"Yes": "Yes",
|
||||
"accessibilityLabel": {
|
||||
"Cancel": "Cancel (leave dialog)",
|
||||
"Ok": "OK (save and leave dialog)",
|
||||
"close": "Close dialog",
|
||||
"liveStreaming": "Live Stream",
|
||||
"sharingTabs": "Sharing options"
|
||||
@@ -459,6 +464,9 @@
|
||||
"title": "Embed this meeting"
|
||||
},
|
||||
"feedback": {
|
||||
"accessibilityLabel": {
|
||||
"yourChoice": "Your choice: {{rating}}"
|
||||
},
|
||||
"average": "Average",
|
||||
"bad": "Bad",
|
||||
"detailsLabel": "Tell us more about it.",
|
||||
@@ -1341,7 +1349,7 @@
|
||||
"audioOnly": "AUD",
|
||||
"audioOnlyExpanded": "You are in low bandwidth mode. In this mode you will receive only audio and screen sharing.",
|
||||
"bestPerformance": "Best performance",
|
||||
"callQuality": "Video Quality",
|
||||
"callQuality": "Video Quality (0 for best performance, 3 for highest quality)",
|
||||
"hd": "HD",
|
||||
"hdTooltip": "Viewing high definition video",
|
||||
"highDefinition": "High definition",
|
||||
@@ -1383,6 +1391,10 @@
|
||||
"videomute": "Participant has stopped the camera"
|
||||
},
|
||||
"virtualBackground": {
|
||||
"accessibilityLabel": {
|
||||
"currentBackground": "Current background: {{background}}",
|
||||
"selectBackground": "Select a background"
|
||||
},
|
||||
"addBackground": "Add background",
|
||||
"apply": "Apply",
|
||||
"backgroundEffectError": "Failed to apply background effect.",
|
||||
|
||||
@@ -15,7 +15,7 @@ const Filmstrip = {
|
||||
// horizontal film strip mode for calculating how tall large video
|
||||
// display should be.
|
||||
if (isFilmstripVisible(APP.store) && !interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
return document.querySelector('.filmstrip').offsetHeight;
|
||||
return document.querySelector('.filmstrip')?.offsetHeight ?? 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
184
package-lock.json
generated
184
package-lock.json
generated
@@ -17,7 +17,7 @@
|
||||
"@giphy/react-components": "6.8.1",
|
||||
"@giphy/react-native-sdk": "2.3.0",
|
||||
"@hapi/bourne": "2.0.0",
|
||||
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.13/jitsi-excalidraw-0.0.13.tgz",
|
||||
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.14/jitsi-excalidraw-0.0.14.tgz",
|
||||
"@jitsi/js-utils": "2.0.5",
|
||||
"@jitsi/logger": "2.0.0",
|
||||
"@jitsi/rnnoise-wasm": "0.1.0",
|
||||
@@ -71,7 +71,7 @@
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-emoji-render": "1.2.4",
|
||||
"react-focus-lock": "2.9.4",
|
||||
"react-focus-on": "3.8.1",
|
||||
"react-i18next": "10.11.4",
|
||||
"react-linkify": "1.0.0-alpha",
|
||||
"react-native": "0.69.10",
|
||||
@@ -3100,9 +3100,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jitsi/excalidraw": {
|
||||
"version": "0.0.13",
|
||||
"resolved": "https://github.com/jitsi/excalidraw/releases/download/v0.0.13/jitsi-excalidraw-0.0.13.tgz",
|
||||
"integrity": "sha512-GcH+KwBTuE+3bdf73lS2X+TpVp/QFyXBHps8jntSWjz5UOfmXhF4SAoUe+550eVCHiZex78AaLaVflR34Lv0VA==",
|
||||
"version": "0.0.14",
|
||||
"resolved": "https://github.com/jitsi/excalidraw/releases/download/v0.0.14/jitsi-excalidraw-0.0.14.tgz",
|
||||
"integrity": "sha512-iK7p7i6qJFOkjTVZhWDvurDW1u+eMoOhAVgpab9CZEqCTX+W4Ih4AOPrUpf+mjaAHK5XqmQZSc5nVEpMg+xIGQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.2 || ^18.2.0",
|
||||
@@ -6926,6 +6926,17 @@
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/aria-hidden": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz",
|
||||
"integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/arr-diff": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
|
||||
@@ -10640,6 +10651,14 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-nonce": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
|
||||
@@ -15581,6 +15600,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-focus-on": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/react-focus-on/-/react-focus-on-3.8.1.tgz",
|
||||
"integrity": "sha512-fQcBx+SZMgXoRL+69r5+ic4bdVgqaCeKeoFPra8yhcSuL/3unWavfdirEFBGgH71K+RiocMTS6DETHcX0SlOZg==",
|
||||
"dependencies": {
|
||||
"aria-hidden": "^1.2.2",
|
||||
"react-focus-lock": "^2.9.2",
|
||||
"react-remove-scroll": "^2.5.6",
|
||||
"react-style-singleton": "^2.2.0",
|
||||
"tslib": "^2.3.1",
|
||||
"use-callback-ref": "^1.3.0",
|
||||
"use-sidecar": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-freeze": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.0.tgz",
|
||||
@@ -16107,6 +16152,51 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-remove-scroll": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.6.tgz",
|
||||
"integrity": "sha512-bO856ad1uDYLefgArk559IzUNeQ6SWH4QnrevIUjH+GczV56giDfl3h0Idptf2oIKxQmd1p9BN25jleKodTALg==",
|
||||
"dependencies": {
|
||||
"react-remove-scroll-bar": "^2.3.4",
|
||||
"react-style-singleton": "^2.2.1",
|
||||
"tslib": "^2.1.0",
|
||||
"use-callback-ref": "^1.3.0",
|
||||
"use-sidecar": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-remove-scroll-bar": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz",
|
||||
"integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==",
|
||||
"dependencies": {
|
||||
"react-style-singleton": "^2.2.1",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-shallow-renderer": {
|
||||
"version": "16.15.0",
|
||||
"resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz",
|
||||
@@ -16119,6 +16209,28 @@
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
|
||||
"integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
|
||||
"dependencies": {
|
||||
"get-nonce": "^1.0.0",
|
||||
"invariant": "^2.2.4",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-textarea-autosize": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.3.0.tgz",
|
||||
@@ -21897,8 +22009,8 @@
|
||||
"dev": true
|
||||
},
|
||||
"@jitsi/excalidraw": {
|
||||
"version": "https://github.com/jitsi/excalidraw/releases/download/v0.0.13/jitsi-excalidraw-0.0.13.tgz",
|
||||
"integrity": "sha512-GcH+KwBTuE+3bdf73lS2X+TpVp/QFyXBHps8jntSWjz5UOfmXhF4SAoUe+550eVCHiZex78AaLaVflR34Lv0VA=="
|
||||
"version": "https://github.com/jitsi/excalidraw/releases/download/v0.0.14/jitsi-excalidraw-0.0.14.tgz",
|
||||
"integrity": "sha512-iK7p7i6qJFOkjTVZhWDvurDW1u+eMoOhAVgpab9CZEqCTX+W4Ih4AOPrUpf+mjaAHK5XqmQZSc5nVEpMg+xIGQ=="
|
||||
},
|
||||
"@jitsi/js-utils": {
|
||||
"version": "2.0.5",
|
||||
@@ -24738,6 +24850,14 @@
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"aria-hidden": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz",
|
||||
"integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==",
|
||||
"requires": {
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"arr-diff": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
|
||||
@@ -27571,6 +27691,11 @@
|
||||
"has-symbols": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"get-nonce": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
|
||||
@@ -31287,6 +31412,20 @@
|
||||
"use-sidecar": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"react-focus-on": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/react-focus-on/-/react-focus-on-3.8.1.tgz",
|
||||
"integrity": "sha512-fQcBx+SZMgXoRL+69r5+ic4bdVgqaCeKeoFPra8yhcSuL/3unWavfdirEFBGgH71K+RiocMTS6DETHcX0SlOZg==",
|
||||
"requires": {
|
||||
"aria-hidden": "^1.2.2",
|
||||
"react-focus-lock": "^2.9.2",
|
||||
"react-remove-scroll": "^2.5.6",
|
||||
"react-style-singleton": "^2.2.0",
|
||||
"tslib": "^2.3.1",
|
||||
"use-callback-ref": "^1.3.0",
|
||||
"use-sidecar": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"react-freeze": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.0.tgz",
|
||||
@@ -31658,6 +31797,27 @@
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz",
|
||||
"integrity": "sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA=="
|
||||
},
|
||||
"react-remove-scroll": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.6.tgz",
|
||||
"integrity": "sha512-bO856ad1uDYLefgArk559IzUNeQ6SWH4QnrevIUjH+GczV56giDfl3h0Idptf2oIKxQmd1p9BN25jleKodTALg==",
|
||||
"requires": {
|
||||
"react-remove-scroll-bar": "^2.3.4",
|
||||
"react-style-singleton": "^2.2.1",
|
||||
"tslib": "^2.1.0",
|
||||
"use-callback-ref": "^1.3.0",
|
||||
"use-sidecar": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"react-remove-scroll-bar": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz",
|
||||
"integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==",
|
||||
"requires": {
|
||||
"react-style-singleton": "^2.2.1",
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"react-shallow-renderer": {
|
||||
"version": "16.15.0",
|
||||
"resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz",
|
||||
@@ -31667,6 +31827,16 @@
|
||||
"react-is": "^16.12.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"react-style-singleton": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
|
||||
"integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
|
||||
"requires": {
|
||||
"get-nonce": "^1.0.0",
|
||||
"invariant": "^2.2.4",
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"react-textarea-autosize": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.3.0.tgz",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@giphy/react-components": "6.8.1",
|
||||
"@giphy/react-native-sdk": "2.3.0",
|
||||
"@hapi/bourne": "2.0.0",
|
||||
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.13/jitsi-excalidraw-0.0.13.tgz",
|
||||
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.14/jitsi-excalidraw-0.0.14.tgz",
|
||||
"@jitsi/js-utils": "2.0.5",
|
||||
"@jitsi/logger": "2.0.0",
|
||||
"@jitsi/rnnoise-wasm": "0.1.0",
|
||||
@@ -76,7 +76,7 @@
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-emoji-render": "1.2.4",
|
||||
"react-focus-lock": "2.9.4",
|
||||
"react-focus-on": "3.8.1",
|
||||
"react-i18next": "10.11.4",
|
||||
"react-linkify": "1.0.0-alpha",
|
||||
"react-native": "0.69.10",
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
import {
|
||||
createFakeConfig,
|
||||
restoreConfig
|
||||
} from '../base/config/functions';
|
||||
import { connect, disconnect, setLocationURL } from '../base/connection/actions';
|
||||
} from '../base/config/functions.native';
|
||||
import { connect, disconnect, setLocationURL } from '../base/connection/actions.native';
|
||||
import { loadConfig } from '../base/lib-jitsi-meet/functions.native';
|
||||
import { createDesiredLocalTracks } from '../base/tracks/actions';
|
||||
import { createDesiredLocalTracks } from '../base/tracks/actions.native';
|
||||
import isInsecureRoomName from '../base/util/isInsecureRoomName';
|
||||
import { parseURLParams } from '../base/util/parseURLParams';
|
||||
import {
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
} from '../mobile/navigation/rootNavigationContainerRef';
|
||||
import { screen } from '../mobile/navigation/routes';
|
||||
import { clearNotifications } from '../notifications/actions';
|
||||
import { isUnsafeRoomWarningEnabled } from '../prejoin/functions';
|
||||
|
||||
import { addTrackStateToURL, getDefaultURL } from './functions.native';
|
||||
import logger from './logger';
|
||||
@@ -137,7 +138,7 @@ export function appNavigate(uri?: string, options: IReloadNowOptions = {}) {
|
||||
dispatch(setRoom(room));
|
||||
|
||||
if (room) {
|
||||
if (isInsecureRoomName(room)) {
|
||||
if (isUnsafeRoomWarningEnabled(getState()) && isInsecureRoomName(room)) {
|
||||
navigateRoot(screen.unsafeRoomWarning);
|
||||
|
||||
return;
|
||||
|
||||
@@ -268,6 +268,7 @@ class LoginDialog extends Component<IProps, IState> {
|
||||
titleKey = { t('dialog.authenticationRequired') }>
|
||||
<Input
|
||||
autoFocus = { true }
|
||||
id = 'login-dialog-username'
|
||||
label = { t('dialog.user') }
|
||||
name = 'username'
|
||||
onChange = { this._onUsernameChange }
|
||||
@@ -277,6 +278,7 @@ class LoginDialog extends Component<IProps, IState> {
|
||||
<br />
|
||||
<Input
|
||||
className = 'dialog-bottom-margin'
|
||||
id = 'login-dialog-password'
|
||||
label = { t('dialog.userPassword') }
|
||||
name = 'password'
|
||||
onChange = { this._onPasswordChange }
|
||||
|
||||
@@ -57,6 +57,14 @@ let mounted: boolean;
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The invisible text for screen readers.
|
||||
*
|
||||
* Intended to give the same info as `displayedText`, but can be customized to give more necessary context.
|
||||
* If not given, `displayedText` will be used.
|
||||
*/
|
||||
accessibilityText?: string;
|
||||
|
||||
/**
|
||||
* Css class to apply on container.
|
||||
*/
|
||||
@@ -93,7 +101,15 @@ interface IProps {
|
||||
*
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
function CopyButton({ className = '', displayedText, textToCopy, textOnHover, textOnCopySuccess, id }: IProps) {
|
||||
function CopyButton({
|
||||
accessibilityText,
|
||||
className = '',
|
||||
displayedText,
|
||||
textToCopy,
|
||||
textOnHover,
|
||||
textOnCopySuccess,
|
||||
id
|
||||
}: IProps) {
|
||||
const { classes, cx } = useStyles();
|
||||
const [ isClicked, setIsClicked ] = useState(false);
|
||||
const [ isHovered, setIsHovered ] = useState(false);
|
||||
@@ -196,20 +212,33 @@ function CopyButton({ className = '', displayedText, textToCopy, textOnHover, te
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-label = { textOnHover }
|
||||
className = { cx(className, classes.copyButton, isClicked ? ' clicked' : '') }
|
||||
id = { id }
|
||||
onBlur = { onHoverOut }
|
||||
onClick = { onClick }
|
||||
onFocus = { onHoverIn }
|
||||
onKeyPress = { onKeyPress }
|
||||
onMouseOut = { onHoverOut }
|
||||
onMouseOver = { onHoverIn }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
{ renderContent() }
|
||||
</div>
|
||||
<>
|
||||
<div
|
||||
aria-describedby = { displayedText === textOnHover
|
||||
? undefined
|
||||
: `${id}-sr-text` }
|
||||
aria-label = { displayedText === textOnHover ? accessibilityText : textOnHover }
|
||||
className = { cx(className, classes.copyButton, isClicked ? ' clicked' : '') }
|
||||
id = { id }
|
||||
onBlur = { onHoverOut }
|
||||
onClick = { onClick }
|
||||
onFocus = { onHoverIn }
|
||||
onKeyPress = { onKeyPress }
|
||||
onMouseOut = { onHoverOut }
|
||||
onMouseOver = { onHoverIn }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
{ renderContent() }
|
||||
</div>
|
||||
|
||||
{ displayedText !== textOnHover && (
|
||||
<span
|
||||
className = 'sr-only'
|
||||
id = { `${id}-sr-text` }>
|
||||
{ accessibilityText }
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,15 +19,68 @@ let screenLock: WakeLockSentinel | undefined;
|
||||
/**
|
||||
* Releases the screen lock.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function releaseScreenLock() {
|
||||
if (screenLock) {
|
||||
if (!screenLock.released) {
|
||||
logger.debug('Releasing wake lock.');
|
||||
|
||||
try {
|
||||
await screenLock.release();
|
||||
} catch (e) {
|
||||
logger.error(`Error while releasing the screen wake lock: ${e}.`);
|
||||
}
|
||||
}
|
||||
screenLock.removeEventListener('release', onWakeLockReleased);
|
||||
screenLock = undefined;
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a new screen wake lock.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function releaseScreenLock() {
|
||||
if (screenLock) {
|
||||
screenLock.release();
|
||||
screenLock = undefined;
|
||||
function requestWakeLock() {
|
||||
if (navigator.wakeLock?.request) {
|
||||
navigator.wakeLock.request('screen')
|
||||
.then(lock => {
|
||||
screenLock = lock;
|
||||
screenLock.addEventListener('release', onWakeLockReleased);
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
logger.debug('Wake lock created.');
|
||||
})
|
||||
.catch(e => {
|
||||
logger.error(`Error while requesting wake lock for screen: ${e}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Page visibility change handler that re-requests the wake lock if it has been released by the OS.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
async function handleVisibilityChange() {
|
||||
if (screenLock?.released && document.visibilityState === 'visible') {
|
||||
// The screen lock have been released by the OS because of document visibility change. Lets try to request the
|
||||
// wake lock again.
|
||||
await releaseScreenLock();
|
||||
requestWakeLock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wake lock released handler.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function onWakeLockReleased() {
|
||||
logger.debug('Wake lock released');
|
||||
}
|
||||
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const { dispatch, getState } = store;
|
||||
const { enableForcedReload } = getState()['features/base/config'];
|
||||
@@ -43,15 +96,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
dispatch(setSkipPrejoinOnReload(false));
|
||||
}
|
||||
|
||||
if (navigator.wakeLock?.request) {
|
||||
navigator.wakeLock.request('screen')
|
||||
.then(lock => {
|
||||
screenLock = lock;
|
||||
})
|
||||
.catch(e => {
|
||||
logger.error(`Error while requesting wake lock for screen: ${e}`);
|
||||
});
|
||||
}
|
||||
requestWakeLock();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -243,6 +243,12 @@ export const TOOLBOX_ALWAYS_VISIBLE = 'toolbox.alwaysVisible';
|
||||
*/
|
||||
export const TOOLBOX_ENABLED = 'toolbox.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if the unsafe room warning should be enabled.
|
||||
* Default: disabled (false).
|
||||
*/
|
||||
export const UNSAFE_ROOM_WARNING = 'unsaferoomwarning.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if the video mute button should be displayed.
|
||||
* Default: enabled (true).
|
||||
|
||||
@@ -7,6 +7,16 @@ import { IIconProps } from './types';
|
||||
|
||||
interface IProps extends IIconProps {
|
||||
|
||||
/**
|
||||
* Optional label for screen reader users.
|
||||
*
|
||||
* If set, this is will add a `aria-label` attribute on the svg element,
|
||||
* contrary to the aria* props which set attributes on the container element.
|
||||
*
|
||||
* Use this if the icon conveys meaning and is not clickable.
|
||||
*/
|
||||
alt?: string;
|
||||
|
||||
/**
|
||||
* The id of the element this button icon controls.
|
||||
*/
|
||||
@@ -114,6 +124,7 @@ export const DEFAULT_SIZE = navigator.product === 'ReactNative' ? 36 : 22;
|
||||
*/
|
||||
export default function Icon(props: IProps) {
|
||||
const {
|
||||
alt,
|
||||
className,
|
||||
color,
|
||||
id,
|
||||
@@ -156,6 +167,13 @@ export default function Icon(props: IProps) {
|
||||
|
||||
const jitsiIconClassName = calculatedColor ? 'jitsi-icon' : 'jitsi-icon jitsi-icon-default';
|
||||
|
||||
const iconProps = alt ? {
|
||||
'aria-label': alt,
|
||||
role: 'img'
|
||||
} : {
|
||||
'aria-hidden': true
|
||||
};
|
||||
|
||||
return (
|
||||
<Container
|
||||
{ ...rest }
|
||||
@@ -176,6 +194,7 @@ export default function Icon(props: IProps) {
|
||||
style = { restStyle }
|
||||
tabIndex = { tabIndex }>
|
||||
<IconComponent
|
||||
{ ...iconProps }
|
||||
fill = { calculatedColor }
|
||||
height = { calculatedSize }
|
||||
id = { id }
|
||||
|
||||
@@ -7,6 +7,14 @@ import { COLORS } from '../../constants';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Optional label for screen reader users, invisible in the UI.
|
||||
*
|
||||
* Note: if the text prop is set, a screen reader will first announce
|
||||
* the accessibilityText, then the text.
|
||||
*/
|
||||
accessibilityText?: string;
|
||||
|
||||
/**
|
||||
* Own CSS class name.
|
||||
*/
|
||||
@@ -82,6 +90,7 @@ const useStyles = makeStyles()(theme => {
|
||||
});
|
||||
|
||||
const Label = ({
|
||||
accessibilityText,
|
||||
className,
|
||||
color,
|
||||
icon,
|
||||
@@ -117,6 +126,7 @@ const Label = ({
|
||||
color = { iconColor }
|
||||
size = '16'
|
||||
src = { icon } />}
|
||||
{accessibilityText && <span className = 'sr-only'>{accessibilityText}</span>}
|
||||
{text && <span className = { icon && classes.withIcon }>{text}</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,10 @@ import debounce from 'lodash/debounce';
|
||||
import { IStore } from '../../app/types';
|
||||
import { SET_FILMSTRIP_ENABLED } from '../../filmstrip/actionTypes';
|
||||
import { APP_STATE_CHANGED } from '../../mobile/background/actionTypes';
|
||||
import { SET_CAR_MODE } from '../../video-layout/actionTypes';
|
||||
import {
|
||||
SET_CAR_MODE,
|
||||
VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED
|
||||
} from '../../video-layout/actionTypes';
|
||||
import { SET_AUDIO_ONLY } from '../audio-only/actionTypes';
|
||||
import { CONFERENCE_JOINED } from '../conference/actionTypes';
|
||||
import { getParticipantById } from '../participants/functions';
|
||||
@@ -81,6 +84,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
case SET_AUDIO_ONLY:
|
||||
case SET_CAR_MODE:
|
||||
case SET_FILMSTRIP_ENABLED:
|
||||
case VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED:
|
||||
_updateLastN(store);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -597,7 +597,8 @@ class VideoTransform extends Component<IProps, IState> {
|
||||
this._onGesture('scale', scale);
|
||||
}
|
||||
} else if (gestureState.numberActiveTouches === 1
|
||||
&& isNaN(this.initialDistance ?? 0)
|
||||
&& (this.initialDistance === undefined
|
||||
|| isNaN(this.initialDistance))
|
||||
&& this._didMove(gestureState)) {
|
||||
// this is a move event
|
||||
const position = this._getTouchPosition(evt);
|
||||
|
||||
@@ -152,7 +152,7 @@ class AudioTrack extends Component<IProps> {
|
||||
const currentMuted = this._ref.muted;
|
||||
const nextMuted = nextProps._muted;
|
||||
|
||||
if (typeof nextMuted === 'boolean' && currentMuted !== nextVolume) {
|
||||
if (typeof nextMuted === 'boolean' && currentMuted !== nextMuted) {
|
||||
this._ref.muted = nextMuted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { Component, ReactNode } from 'react';
|
||||
import ReactFocusLock from 'react-focus-lock';
|
||||
import { FocusOn } from 'react-focus-on';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
@@ -40,6 +40,16 @@ interface IProps {
|
||||
*/
|
||||
disablePopover?: boolean;
|
||||
|
||||
/**
|
||||
* Whether we can reach the popover element via keyboard or not when trigger is 'hover' (true by default).
|
||||
*
|
||||
* Only works when trigger is set to 'hover'.
|
||||
*
|
||||
* There are some rare cases where we want to set this to false,
|
||||
* when the popover content is not necessary for screen reader users, because accessible elsewhere.
|
||||
*/
|
||||
focusable?: boolean;
|
||||
|
||||
/**
|
||||
* The id of the dom element acting as the Popover label (matches aria-labelledby).
|
||||
*/
|
||||
@@ -103,6 +113,14 @@ interface IState {
|
||||
position: string;
|
||||
top?: string;
|
||||
} | null;
|
||||
|
||||
/**
|
||||
* Whether the popover should be focus locked or not.
|
||||
*
|
||||
* This is enabled if we notice the popover is interactive
|
||||
* (trigger is click or focusable is true).
|
||||
*/
|
||||
enableFocusLock: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,6 +137,7 @@ class Popover extends Component<IProps, IState> {
|
||||
*/
|
||||
static defaultProps = {
|
||||
className: '',
|
||||
focusable: true,
|
||||
id: '',
|
||||
trigger: 'hover'
|
||||
};
|
||||
@@ -140,10 +159,12 @@ class Popover extends Component<IProps, IState> {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
contextMenuStyle: null
|
||||
contextMenuStyle: null,
|
||||
enableFocusLock: false
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._enableFocusLock = this._enableFocusLock.bind(this);
|
||||
this._onHideDialog = this._onHideDialog.bind(this);
|
||||
this._onShowDialog = this._onShowDialog.bind(this);
|
||||
this._onKeyPress = this._onKeyPress.bind(this);
|
||||
@@ -207,8 +228,8 @@ class Popover extends Component<IProps, IState> {
|
||||
const { children,
|
||||
className,
|
||||
content,
|
||||
focusable,
|
||||
headingId,
|
||||
headingLabel,
|
||||
id,
|
||||
overflowDrawer,
|
||||
visible,
|
||||
@@ -242,35 +263,40 @@ class Popover extends Component<IProps, IState> {
|
||||
onKeyPress = { this._onKeyPress }
|
||||
{ ...(trigger === 'hover' ? {
|
||||
onMouseEnter: this._onShowDialog,
|
||||
onMouseLeave: this._onHideDialog,
|
||||
tabIndex: 0
|
||||
onMouseLeave: this._onHideDialog
|
||||
} : {}) }
|
||||
{ ...(trigger === 'hover' && focusable && {
|
||||
role: 'button',
|
||||
tabIndex: 0
|
||||
}) }
|
||||
ref = { this._containerRef }>
|
||||
{ visible && (
|
||||
<DialogPortal
|
||||
getRef = { this._setContextMenuRef }
|
||||
onVisible = { this._isInteractive() ? this._enableFocusLock : undefined }
|
||||
setSize = { this._setContextMenuStyle }
|
||||
style = { this.state.contextMenuStyle }
|
||||
targetSelector = '.popover-content'>
|
||||
<ReactFocusLock
|
||||
lockProps = {{
|
||||
role: 'dialog',
|
||||
'aria-modal': true,
|
||||
'aria-labelledby': headingId,
|
||||
'aria-label': !headingId && headingLabel ? headingLabel : undefined
|
||||
}}
|
||||
<FocusOn
|
||||
|
||||
// Use the `enabled` prop instead of conditionally rendering ReactFocusOn
|
||||
// to prevent UI stutter on dialog appearance. It seems the focus guards generated annoy
|
||||
// our DialogPortal positioning calculations.
|
||||
enabled = { this.state.enableFocusLock }
|
||||
returnFocus = {
|
||||
|
||||
// If we return the focus to an element outside the viewport the page will scroll to
|
||||
// this element which in our case is undesirable and the element is outside of the
|
||||
// viewport on purpose (to be hidden). For example if we return the focus to the toolbox
|
||||
// when it is hidden the whole page will move up in order to show the toolbox. This is
|
||||
// usually followed up with displaying the toolbox (because now it is on focus) but
|
||||
// because of the animation the whole scenario looks like jumping large video.
|
||||
// viewport on purpose (to be hidden). For example if we return the focus to the
|
||||
// toolbox when it is hidden the whole page will move up in order to show the
|
||||
// toolbox. This is usually followed up with displaying the toolbox (because now it
|
||||
// is on focus) but because of the animation the whole scenario looks like jumping
|
||||
// large video.
|
||||
isElementInTheViewport
|
||||
}>
|
||||
}
|
||||
shards = { [ this._contextMenuRef ] }>
|
||||
{this._renderContent()}
|
||||
</ReactFocusLock>
|
||||
</FocusOn>
|
||||
</DialogPortal>
|
||||
)}
|
||||
{ children }
|
||||
@@ -361,12 +387,12 @@ class Popover extends Component<IProps, IState> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick(event: React.MouseEvent) {
|
||||
const { allowClick, trigger, visible } = this.props;
|
||||
const { allowClick, trigger, focusable, visible } = this.props;
|
||||
|
||||
if (!allowClick) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
if (trigger === 'click') {
|
||||
if (trigger === 'click' || focusable) {
|
||||
if (visible) {
|
||||
this._onHideDialog();
|
||||
} else {
|
||||
@@ -383,7 +409,9 @@ class Popover extends Component<IProps, IState> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyPress(e: React.KeyboardEvent) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
// first check that the element we pressed is the actual popover toggle or any of its descendant,
|
||||
// otherwise pressing space or enter in any child element of the popover _dialog_ will trigger this.
|
||||
if (e.currentTarget.contains(e.target as Node) && (e.key === ' ' || e.key === 'Enter')) {
|
||||
e.preventDefault();
|
||||
if (this.props.visible) {
|
||||
this._onHideDialog();
|
||||
@@ -435,18 +463,49 @@ class Popover extends Component<IProps, IState> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderContent() {
|
||||
const { content, position, trigger } = this.props;
|
||||
const { content, position, trigger, headingId, headingLabel } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { `popover ${trigger}` }
|
||||
onKeyDown = { this._onEscKey }>
|
||||
<div className = { `popover-content ${position.split('-')[0]}` }>
|
||||
<div className = { `popover ${trigger}` }>
|
||||
<div
|
||||
className = { `popover-content ${position.split('-')[0]}` }
|
||||
data-autofocus = { this.state.enableFocusLock }
|
||||
onKeyDown = { this._onEscKey }
|
||||
{ ...(this.state.enableFocusLock && {
|
||||
'aria-modal': true,
|
||||
'aria-label': !headingId && headingLabel ? headingLabel : undefined,
|
||||
'aria-labelledby': headingId,
|
||||
role: 'dialog',
|
||||
tabIndex: -1
|
||||
}) }>
|
||||
{ content }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the popover is considered interactive or not.
|
||||
*
|
||||
* Interactive means the popover content is certainly composed of buttons, links…
|
||||
* Non-interactive popovers are mostly tooltips.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isInteractive() {
|
||||
return this.props.trigger === 'click' || Boolean(this.props.focusable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the focus lock in the popover dialog.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_enableFocusLock() {
|
||||
this.setState({ enableFocusLock: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -103,6 +103,7 @@ const BaseIndicator = ({
|
||||
className = { className }
|
||||
id = { id }>
|
||||
<Icon
|
||||
alt = { t(tooltipKey) }
|
||||
className = { iconClassName }
|
||||
color = { iconColor }
|
||||
id = { iconId }
|
||||
|
||||
@@ -24,6 +24,11 @@ interface IProps {
|
||||
*/
|
||||
footer?: any;
|
||||
|
||||
/**
|
||||
* Id for the included input, necessary for screen readers.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Indicates if the component is disabled.
|
||||
*/
|
||||
@@ -174,6 +179,7 @@ class MultiSelectAutocomplete extends Component<IProps, IState> {
|
||||
error = { this.state.error }
|
||||
errorDialog = { errorDialog }
|
||||
filterValue = { this.state.filterValue }
|
||||
id = { this.props.id }
|
||||
isOpen = { this.state.isOpen }
|
||||
items = { this.state.items }
|
||||
noMatchesText = { noMatchesFound }
|
||||
|
||||
@@ -5,21 +5,6 @@ import Popover from '../../../popover/components/Popover.web';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The id of the element this button icon controls.
|
||||
*/
|
||||
ariaControls?: string;
|
||||
|
||||
/**
|
||||
* Whether the element popup is expanded.
|
||||
*/
|
||||
ariaExpanded?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the element has a popup.
|
||||
*/
|
||||
ariaHasPopup?: boolean;
|
||||
|
||||
/**
|
||||
* Aria label for the Icon.
|
||||
*/
|
||||
@@ -40,11 +25,6 @@ interface IProps {
|
||||
*/
|
||||
iconDisabled?: boolean;
|
||||
|
||||
/**
|
||||
* The ID of the icon button.
|
||||
*/
|
||||
iconId?: string;
|
||||
|
||||
/**
|
||||
* Popover close callback.
|
||||
*/
|
||||
@@ -84,14 +64,10 @@ interface IProps {
|
||||
*/
|
||||
export default function ToolboxButtonWithPopup(props: IProps) {
|
||||
const {
|
||||
ariaControls,
|
||||
ariaExpanded,
|
||||
ariaHasPopup,
|
||||
ariaLabel,
|
||||
children,
|
||||
icon,
|
||||
iconDisabled,
|
||||
iconId,
|
||||
onPopoverClose,
|
||||
onPopoverOpen,
|
||||
popoverContent,
|
||||
@@ -119,28 +95,6 @@ export default function ToolboxButtonWithPopup(props: IProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const iconProps: {
|
||||
ariaControls?: string;
|
||||
ariaExpanded?: boolean;
|
||||
className?: string;
|
||||
containerId?: string;
|
||||
role?: string;
|
||||
tabIndex?: number;
|
||||
} = {};
|
||||
|
||||
if (iconDisabled) {
|
||||
iconProps.className
|
||||
= 'settings-button-small-icon settings-button-small-icon--disabled';
|
||||
} else {
|
||||
iconProps.className = 'settings-button-small-icon';
|
||||
iconProps.role = 'button';
|
||||
iconProps.tabIndex = 0;
|
||||
iconProps.ariaControls = ariaControls;
|
||||
iconProps.ariaExpanded = ariaExpanded;
|
||||
iconProps.containerId = iconId;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-button-container'
|
||||
@@ -155,9 +109,10 @@ export default function ToolboxButtonWithPopup(props: IProps) {
|
||||
position = 'top'
|
||||
visible = { visible }>
|
||||
<Icon
|
||||
{ ...iconProps }
|
||||
ariaHasPopup = { ariaHasPopup }
|
||||
ariaLabel = { ariaLabel }
|
||||
alt = { ariaLabel }
|
||||
className = { `settings-button-small-icon ${iconDisabled
|
||||
? 'settings-button-small-icon--disabled'
|
||||
: ''}` }
|
||||
size = { 16 }
|
||||
src = { icon } />
|
||||
</Popover>
|
||||
|
||||
@@ -145,6 +145,7 @@ const Tooltip = ({ containerClassName, content, children, position = 'top' }: IP
|
||||
allowClick = { true }
|
||||
className = { containerClassName }
|
||||
content = { contentComponent }
|
||||
focusable = { false }
|
||||
onPopoverClose = { onPopoverClose }
|
||||
onPopoverOpen = { onPopoverOpen }
|
||||
position = { position }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { ReactNode, useCallback, useContext, useEffect } from 'react';
|
||||
import FocusLock from 'react-focus-lock';
|
||||
import { FocusOn } from 'react-focus-on';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { keyframes } from 'tss-react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
@@ -183,7 +183,7 @@ const BaseDialog = ({
|
||||
<div
|
||||
className = { classes.backdrop }
|
||||
onClick = { onBackdropClick } />
|
||||
<FocusLock
|
||||
<FocusOn
|
||||
className = { classes.focusLock }
|
||||
returnFocus = {
|
||||
|
||||
@@ -196,14 +196,16 @@ const BaseDialog = ({
|
||||
isElementInTheViewport
|
||||
}>
|
||||
<div
|
||||
aria-describedby = { description }
|
||||
aria-labelledby = { title ?? t(titleKey ?? '') }
|
||||
aria-description = { description }
|
||||
aria-label = { title ?? t(titleKey ?? '') }
|
||||
aria-modal = { true }
|
||||
className = { cx(classes.modal, isUnmounting && 'unmount', size, className) }
|
||||
role = 'dialog'>
|
||||
data-autofocus = { true }
|
||||
role = 'dialog'
|
||||
tabIndex = { -1 }>
|
||||
{children}
|
||||
</div>
|
||||
</FocusLock>
|
||||
</FocusOn>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -156,8 +156,8 @@ const Checkbox = ({
|
||||
const isMobile = isMobileBrowser();
|
||||
|
||||
return (
|
||||
<div className = { cx(styles.formControl, isMobile && 'is-mobile', className) }>
|
||||
<label className = { cx(styles.activeArea, isMobile && 'is-mobile', disabled && styles.disabled) }>
|
||||
<label className = { cx(styles.formControl, isMobile && 'is-mobile', className) }>
|
||||
<div className = { cx(styles.activeArea, isMobile && 'is-mobile', disabled && styles.disabled) }>
|
||||
<input
|
||||
checked = { checked }
|
||||
disabled = { disabled }
|
||||
@@ -165,13 +165,14 @@ const Checkbox = ({
|
||||
onChange = { onChange }
|
||||
type = 'checkbox' />
|
||||
<Icon
|
||||
aria-hidden = { true }
|
||||
className = 'checkmark'
|
||||
color = { disabled ? theme.palette.icon03 : theme.palette.icon01 }
|
||||
size = { 18 }
|
||||
src = { IconCheck } />
|
||||
</label>
|
||||
<label>{label}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>{label}</div>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { ReactNode, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
@@ -76,6 +76,9 @@ export interface IProps {
|
||||
|
||||
/**
|
||||
* You can use this item as a tab. Defaults to button if not set.
|
||||
*
|
||||
* If no onClick handler is provided, we assume the context menu item is
|
||||
* not interactive and no role will be set.
|
||||
*/
|
||||
role?: 'tab' | 'button';
|
||||
|
||||
@@ -179,6 +182,28 @@ const ContextMenuItem = ({
|
||||
const { classes: styles, cx } = useStyles();
|
||||
const _overflowDrawer: boolean = useSelector(showOverflowDrawer);
|
||||
|
||||
const onKeyPressHandler = useCallback(e => {
|
||||
// only trigger the fallback behavior (onClick) if we dont have any explicit keyboard event handler
|
||||
if (onClick && !onKeyPress && !onKeyDown && (e.key === 'Enter' || e.key === ' ')) {
|
||||
e.preventDefault();
|
||||
onClick(e);
|
||||
}
|
||||
|
||||
if (onKeyPress) {
|
||||
onKeyPress(e);
|
||||
}
|
||||
}, [ onClick, onKeyPress, onKeyDown ]);
|
||||
|
||||
let tabIndex: undefined | 0 | -1;
|
||||
|
||||
if (role === 'tab') {
|
||||
tabIndex = selected ? 0 : -1;
|
||||
}
|
||||
|
||||
if (role === 'button' && !disabled) {
|
||||
tabIndex = 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-controls = { controls }
|
||||
@@ -196,12 +221,9 @@ const ContextMenuItem = ({
|
||||
key = { text }
|
||||
onClick = { disabled ? undefined : onClick }
|
||||
onKeyDown = { disabled ? undefined : onKeyDown }
|
||||
onKeyPress = { disabled ? undefined : onKeyPress }
|
||||
role = { role }
|
||||
tabIndex = { role === 'tab'
|
||||
? selected ? 0 : -1
|
||||
: disabled ? undefined : 0
|
||||
}>
|
||||
onKeyPress = { disabled ? undefined : onKeyPressHandler }
|
||||
role = { onClick ? role : undefined }
|
||||
tabIndex = { onClick ? tabIndex : undefined }>
|
||||
{customIcon ? customIcon
|
||||
: icon && <Icon
|
||||
className = { styles.contextMenuItemIcon }
|
||||
|
||||
@@ -6,6 +6,7 @@ import { makeStyles } from 'tss-react/mui';
|
||||
import { hideDialog } from '../../../dialog/actions';
|
||||
import { IconCloseLarge } from '../../../icons/svg';
|
||||
import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
import { operatesWithEnterKey } from '../../functions.web';
|
||||
|
||||
import BaseDialog, { IProps as IBaseDialogProps } from './BaseDialog';
|
||||
import Button from './Button';
|
||||
@@ -108,8 +109,13 @@ const Dialog = ({
|
||||
}, [ onCancel ]);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
!disableAutoHideOnSubmit && dispatch(hideDialog());
|
||||
onSubmit?.();
|
||||
if (onSubmit && (
|
||||
(document.activeElement && !operatesWithEnterKey(document.activeElement))
|
||||
|| !document.activeElement
|
||||
)) {
|
||||
!disableAutoHideOnSubmit && dispatch(hideDialog());
|
||||
onSubmit();
|
||||
}
|
||||
}, [ onSubmit ]);
|
||||
|
||||
return (
|
||||
@@ -124,11 +130,11 @@ const Dialog = ({
|
||||
title = { title }
|
||||
titleKey = { titleKey }>
|
||||
<div className = { classes.header }>
|
||||
<p
|
||||
<h1
|
||||
className = { classes.title }
|
||||
id = 'dialog-title'>
|
||||
{title ?? t(titleKey ?? '')}
|
||||
</p>
|
||||
</h1>
|
||||
{!hideCloseButton && (
|
||||
<ClickableIcon
|
||||
accessibilityLabel = { t('dialog.accessibilityLabel.close') }
|
||||
@@ -160,6 +166,7 @@ const Dialog = ({
|
||||
accessibilityLabel = { t(ok.translationKey ?? '') }
|
||||
disabled = { ok.disabled }
|
||||
id = 'modal-dialog-ok-button'
|
||||
isSubmit = { true }
|
||||
labelKey = { ok.translationKey }
|
||||
onClick = { submit } />}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { ComponentType, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { MoveFocusInside } from 'react-focus-lock';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
@@ -187,7 +186,7 @@ const DialogWithTabs = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobile) {
|
||||
setSelectedTab(undefined);
|
||||
setSelectedTab(defaultTab);
|
||||
} else {
|
||||
setSelectedTab(defaultTab ?? tabs[0].name);
|
||||
}
|
||||
@@ -317,20 +316,19 @@ const DialogWithTabs = ({
|
||||
<BaseDialog
|
||||
className = { cx(classes.dialog, className) }
|
||||
onClose = { onClose }
|
||||
size = 'large'>
|
||||
size = 'large'
|
||||
titleKey = { titleKey }>
|
||||
{(!isMobile || !selectedTab) && (
|
||||
<div
|
||||
aria-orientation = 'vertical'
|
||||
className = { classes.sidebar }
|
||||
role = { isMobile ? undefined : 'tablist' }>
|
||||
<div className = { classes.titleContainer }>
|
||||
<MoveFocusInside>
|
||||
<h2
|
||||
className = { classes.title }
|
||||
tabIndex = { -1 }>
|
||||
{t(titleKey ?? '')}
|
||||
</h2>
|
||||
</MoveFocusInside>
|
||||
<h1
|
||||
className = { classes.title }
|
||||
tabIndex = { -1 }>
|
||||
{t(titleKey ?? '')}
|
||||
</h1>
|
||||
{isMobile && closeIcon}
|
||||
</div>
|
||||
{tabs.map((tab, index) => {
|
||||
@@ -366,11 +364,11 @@ const DialogWithTabs = ({
|
||||
{isMobile && (
|
||||
<div className = { cx(classes.buttonContainer, classes.header) }>
|
||||
<span className = { classes.backContainer }>
|
||||
<h2
|
||||
<h1
|
||||
className = { classes.title }
|
||||
tabIndex = { -1 }>
|
||||
{(selectedTabIndex !== null) && t(tabs[selectedTabIndex].labelKey)}
|
||||
</h2>
|
||||
</h1>
|
||||
<ClickableIcon
|
||||
accessibilityLabel = { t('dialog.Back') }
|
||||
icon = { IconArrowBack }
|
||||
@@ -401,13 +399,13 @@ const DialogWithTabs = ({
|
||||
<div
|
||||
className = { cx(classes.buttonContainer, classes.footer) }>
|
||||
<Button
|
||||
accessibilityLabel = { t('dialog.Cancel') }
|
||||
accessibilityLabel = { t('dialog.accessibilityLabel.Cancel') }
|
||||
id = 'modal-dialog-cancel-button'
|
||||
labelKey = { 'dialog.Cancel' }
|
||||
onClick = { onClose }
|
||||
type = 'tertiary' />
|
||||
<Button
|
||||
accessibilityLabel = { t('dialog.Ok') }
|
||||
accessibilityLabel = { t('dialog.accessibilityLabel.Ok') }
|
||||
id = 'modal-dialog-ok-button'
|
||||
labelKey = { 'dialog.Ok' }
|
||||
onClick = { onSubmit } />
|
||||
|
||||
@@ -15,12 +15,19 @@ interface IProps extends IInputProps {
|
||||
bottomLabel?: string;
|
||||
className?: string;
|
||||
iconClick?: () => void;
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* The id to set on the input element.
|
||||
* This is required because we need it internally to tie the input to its
|
||||
* info (label, error) so that screen reader users don't get lost.
|
||||
*/
|
||||
id: string;
|
||||
maxLength?: number;
|
||||
maxRows?: number;
|
||||
maxValue?: number;
|
||||
minRows?: number;
|
||||
minValue?: number;
|
||||
mode?: 'text' | 'none' | 'decimal' | 'numeric' | 'tel' | 'search' | ' email' | 'url';
|
||||
name?: string;
|
||||
onBlur?: (e: any) => void;
|
||||
onFocus?: (event: React.FocusEvent) => void;
|
||||
@@ -162,6 +169,7 @@ const Input = React.forwardRef<any, IProps>(({
|
||||
maxRows,
|
||||
minValue,
|
||||
minRows,
|
||||
mode,
|
||||
name,
|
||||
onBlur,
|
||||
onChange,
|
||||
@@ -185,7 +193,11 @@ const Input = React.forwardRef<any, IProps>(({
|
||||
|
||||
return (
|
||||
<div className = { cx(styles.inputContainer, className) }>
|
||||
{label && <span className = { cx(styles.label, isMobile && 'is-mobile') }>{label}</span>}
|
||||
{label && <label
|
||||
className = { cx(styles.label, isMobile && 'is-mobile') }
|
||||
htmlFor = { id } >
|
||||
{label}
|
||||
</label>}
|
||||
<div className = { styles.fieldContainer }>
|
||||
{icon && <Icon
|
||||
{ ...(iconClick ? { tabIndex: 0 } : {}) }
|
||||
@@ -201,7 +213,7 @@ const Input = React.forwardRef<any, IProps>(({
|
||||
className = { cx(styles.input, isMobile && 'is-mobile',
|
||||
error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
|
||||
disabled = { disabled }
|
||||
{ ...(id ? { id } : {}) }
|
||||
id = { id }
|
||||
maxLength = { maxLength }
|
||||
maxRows = { maxRows }
|
||||
minRows = { minRows }
|
||||
@@ -215,6 +227,7 @@ const Input = React.forwardRef<any, IProps>(({
|
||||
value = { value } />
|
||||
) : (
|
||||
<input
|
||||
aria-describedby = { bottomLabel ? `${id}-description` : undefined }
|
||||
aria-label = { accessibilityLabel }
|
||||
autoComplete = { autoComplete }
|
||||
autoFocus = { autoFocus }
|
||||
@@ -222,7 +235,8 @@ const Input = React.forwardRef<any, IProps>(({
|
||||
error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
|
||||
data-testid = { testId }
|
||||
disabled = { disabled }
|
||||
{ ...(id ? { id } : {}) }
|
||||
id = { id }
|
||||
{ ...(mode ? { inputmode: mode } : {}) }
|
||||
{ ...(type === 'number' ? { max: maxValue } : {}) }
|
||||
maxLength = { maxLength }
|
||||
{ ...(type === 'number' ? { min: minValue } : {}) }
|
||||
@@ -246,7 +260,9 @@ const Input = React.forwardRef<any, IProps>(({
|
||||
</button>}
|
||||
</div>
|
||||
{bottomLabel && (
|
||||
<span className = { cx(styles.bottomLabel, isMobile && 'is-mobile', error && 'error') }>
|
||||
<span
|
||||
className = { cx(styles.bottomLabel, isMobile && 'is-mobile', error && 'error') }
|
||||
id = { `${id}-description` }>
|
||||
{bottomLabel}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -14,6 +14,7 @@ interface IProps {
|
||||
error?: boolean;
|
||||
errorDialog?: JSX.Element | null;
|
||||
filterValue?: string;
|
||||
id: string;
|
||||
isOpen?: boolean;
|
||||
items: MultiSelectItem[];
|
||||
noMatchesText?: string;
|
||||
@@ -101,6 +102,7 @@ const MultiSelect = ({
|
||||
error,
|
||||
errorDialog,
|
||||
placeholder,
|
||||
id,
|
||||
items,
|
||||
filterValue,
|
||||
onFilterChange,
|
||||
@@ -145,6 +147,7 @@ const MultiSelect = ({
|
||||
<Input
|
||||
autoFocus = { autoFocus }
|
||||
disabled = { disabled }
|
||||
id = { id }
|
||||
onChange = { onFilterChange }
|
||||
placeholder = { placeholder }
|
||||
ref = { inputRef }
|
||||
|
||||
@@ -28,6 +28,12 @@ interface ISelectProps {
|
||||
*/
|
||||
error?: boolean;
|
||||
|
||||
/**
|
||||
* Id of the <select> element.
|
||||
* Necessary for screen reader users, to link the label and error to the select.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Label to be displayed above the select.
|
||||
*/
|
||||
@@ -140,6 +146,7 @@ const Select = ({
|
||||
className,
|
||||
disabled,
|
||||
error,
|
||||
id,
|
||||
label,
|
||||
onChange,
|
||||
options,
|
||||
@@ -149,11 +156,17 @@ const Select = ({
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
{label && <span className = { cx(classes.label, isMobile && 'is-mobile') }>{label}</span>}
|
||||
{label && <label
|
||||
className = { cx(classes.label, isMobile && 'is-mobile') }
|
||||
htmlFor = { id } >
|
||||
{label}
|
||||
</label>}
|
||||
<div className = { classes.selectContainer }>
|
||||
<select
|
||||
aria-describedby = { bottomLabel ? `${id}-description` : undefined }
|
||||
className = { cx(classes.select, isMobile && 'is-mobile', className, error && 'error') }
|
||||
disabled = { disabled }
|
||||
id = { id }
|
||||
onChange = { onChange }
|
||||
value = { value }>
|
||||
{options.map(option => (<option
|
||||
@@ -167,7 +180,9 @@ const Select = ({
|
||||
src = { IconArrowDown } />
|
||||
</div>
|
||||
{bottomLabel && (
|
||||
<span className = { cx(classes.bottomLabel, isMobile && 'is-mobile', error && 'error') }>
|
||||
<span
|
||||
className = { cx(classes.bottomLabel, isMobile && 'is-mobile', error && 'error') }
|
||||
id = { `${id}-description` }>
|
||||
{bottomLabel}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -52,6 +52,7 @@ const useStyles = makeStyles()(theme => {
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
position: 'absolute',
|
||||
zIndex: 5,
|
||||
top: '4px',
|
||||
left: '4px',
|
||||
backgroundColor: theme.palette.ui10,
|
||||
@@ -73,8 +74,38 @@ const useStyles = makeStyles()(theme => {
|
||||
},
|
||||
|
||||
checkbox: {
|
||||
height: 0,
|
||||
width: 0
|
||||
position: 'absolute',
|
||||
zIndex: 10,
|
||||
cursor: 'pointer',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
opacity: 0,
|
||||
|
||||
'&.focus-visible + .toggle-checkbox-ring': {
|
||||
outline: 0,
|
||||
boxShadow: `0px 0px 0px 2px ${theme.palette.focus01}`
|
||||
}
|
||||
},
|
||||
|
||||
checkboxRing: {
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 6,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '12px',
|
||||
|
||||
'&.is-mobile': {
|
||||
borderRadius: '32px'
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -88,7 +119,7 @@ const Switch = ({ className, id, checked, disabled, onChange }: IProps) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<label
|
||||
<span
|
||||
className = { cx('toggle-container', styles.container, checked && styles.containerOn,
|
||||
isMobile && 'is-mobile', disabled && 'disabled', className) }>
|
||||
<input
|
||||
@@ -98,8 +129,9 @@ const Switch = ({ className, id, checked, disabled, onChange }: IProps) => {
|
||||
className = { styles.checkbox }
|
||||
disabled = { disabled }
|
||||
onChange = { change } />
|
||||
<div className = { cx('toggle-checkbox-ring', styles.checkboxRing, isMobile && 'is-mobile') } />
|
||||
<div className = { cx('toggle', styles.toggle, checked && styles.toggleOn, isMobile && 'is-mobile') } />
|
||||
</label>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -82,3 +82,28 @@ export function isElementInTheViewport(element?: Element): boolean {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const enterKeyElements = [ 'select', 'textarea', 'summary', 'a' ];
|
||||
|
||||
/**
|
||||
* Informs whether or not the given element does something on its own when pressing the Enter key.
|
||||
*
|
||||
* This is useful to correctly submit custom made "forms" that are not using the native form element,
|
||||
* only when the user is not using an element that needs the enter key to work.
|
||||
* Note the implementation is incomplete and should be updated as needed if more complex use cases arise
|
||||
* (for example, the Tabs aria pattern is not handled).
|
||||
*
|
||||
* @param {Element} element - The element.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function operatesWithEnterKey(element: Element): boolean {
|
||||
if (enterKeyElements.includes(element.tagName.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (element.tagName.toLowerCase() === 'button' && element.getAttribute('role') === 'button') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ export function parseURLParams(
|
||||
url: URL | string,
|
||||
dontParse = false,
|
||||
source = 'hash') {
|
||||
if (!url) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (typeof url === 'string') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
url = new URL(url);
|
||||
|
||||
@@ -135,6 +135,7 @@ class ChatInput extends Component<IProps, IState> {
|
||||
className = 'chat-input'
|
||||
icon = { this.props._areSmileysDisabled ? undefined : IconFaceSmile }
|
||||
iconClick = { this._toggleSmileysPanel }
|
||||
id = 'chat-input-messagebox'
|
||||
maxRows = { 5 }
|
||||
onChange = { this._onMessageChange }
|
||||
onKeyPress = { this._onDetectSubmit }
|
||||
|
||||
@@ -32,6 +32,7 @@ const RaisedHandsCountLabel = () => {
|
||||
content = { t('raisedHandsLabel') }
|
||||
position = { 'bottom' }>
|
||||
<Label
|
||||
accessibilityText = { t('raisedHandsLabel') }
|
||||
className = { styles.label }
|
||||
icon = { IconRaiseHand }
|
||||
iconColor = { theme.palette.icon04 }
|
||||
|
||||
@@ -347,12 +347,14 @@ class ConnectionIndicator extends AbstractConnectionIndicator<IProps, IState> {
|
||||
_connectionIndicatorInactiveDisabled,
|
||||
_videoTrack,
|
||||
classes,
|
||||
iconSize
|
||||
iconSize,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
style = {{ fontSize: iconSize }}>
|
||||
<span className = 'sr-only'>{ t('videothumbnail.connectionInfo') }</span>
|
||||
<ConnectionIndicatorIcon
|
||||
classes = { classes }
|
||||
colorClass = { this._getConnectionColorClass() }
|
||||
|
||||
@@ -259,6 +259,11 @@ const useStyles = makeStyles()(theme => {
|
||||
cursor: 'pointer',
|
||||
color: theme.palette.link01,
|
||||
transition: 'color .2s ease',
|
||||
border: 0,
|
||||
background: 0,
|
||||
padding: 0,
|
||||
display: 'inline',
|
||||
fontWeight: 'bold',
|
||||
|
||||
'&:hover': {
|
||||
color: theme.palette.link01Hover,
|
||||
@@ -714,13 +719,12 @@ const ConnectionStatsTable = ({
|
||||
|
||||
const _renderSaveLogs = () => (
|
||||
<span>
|
||||
<a
|
||||
<button
|
||||
className = { cx(classes.link, 'savelogs') }
|
||||
onClick = { onSaveLogs }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
type = 'button'>
|
||||
{t('connectionindicator.savelogs')}
|
||||
</a>
|
||||
</button>
|
||||
<span> | </span>
|
||||
</span>
|
||||
);
|
||||
@@ -732,13 +736,12 @@ const ConnectionStatsTable = ({
|
||||
: 'connectionindicator.more';
|
||||
|
||||
return (
|
||||
<a
|
||||
<button
|
||||
className = { cx(classes.link, 'showmore') }
|
||||
onClick = { onShowMore }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
type = 'button'>
|
||||
{t(translationKey)}
|
||||
</a>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ const useStyles = makeStyles()(theme => {
|
||||
const DeviceSelector = ({
|
||||
devices,
|
||||
hasPermission,
|
||||
id,
|
||||
isDisabled,
|
||||
label,
|
||||
onSelect,
|
||||
@@ -103,6 +104,7 @@ const DeviceSelector = ({
|
||||
|
||||
return (
|
||||
<Select
|
||||
id = { id }
|
||||
label = { t(label) }
|
||||
onChange = { _onSelect }
|
||||
options = { options.items }
|
||||
|
||||
@@ -351,6 +351,7 @@ class VideoDeviceSelection extends AbstractDialogTab<IProps, IState> {
|
||||
bottomLabel = { parseInt(currentFramerate, 10) > SS_DEFAULT_FRAME_RATE
|
||||
? t('settings.desktopShareHighFpsWarning')
|
||||
: t('settings.desktopShareWarning') }
|
||||
id = 'more-framerate-select'
|
||||
label = { t('settings.desktopShareFramerate') }
|
||||
onChange = { this._onFramerateItemSelect }
|
||||
options = { frameRateItems }
|
||||
|
||||
@@ -58,6 +58,7 @@ class DisplayNamePrompt extends AbstractDisplayNamePrompt<IState> {
|
||||
<Input
|
||||
autoFocus = { true }
|
||||
className = 'dialog-bottom-margin'
|
||||
id = 'dialog-displayName'
|
||||
label = { this.props.t('dialog.enterDisplayName') }
|
||||
name = 'displayName'
|
||||
onChange = { this._onDisplayNameChange }
|
||||
|
||||
@@ -55,13 +55,15 @@ function EmbedMeeting({ t, url }: IProps) {
|
||||
<div className = { classes.container }>
|
||||
<Input
|
||||
accessibilityLabel = { t('dialog.embedMeeting') }
|
||||
id = 'embed-meeting-input'
|
||||
readOnly = { true }
|
||||
textarea = { true }
|
||||
value = { getEmbedCode() } />
|
||||
<CopyButton
|
||||
aria-label = { t('addPeople.copyLink') }
|
||||
accessibilityText = { t('addPeople.copyLink') }
|
||||
className = { classes.button }
|
||||
displayedText = { t('dialog.copy') }
|
||||
id = 'embed-meeting-copy-button'
|
||||
textOnCopySuccess = { t('dialog.copied') }
|
||||
textOnHover = { t('dialog.copy') }
|
||||
textToCopy = { getEmbedCode() } />
|
||||
|
||||
@@ -25,7 +25,7 @@ const styles = (theme: Theme) => {
|
||||
|
||||
rating: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
flexDirection: 'column-reverse' as const,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: theme.spacing(4),
|
||||
@@ -316,22 +316,27 @@ class FeedbackDialog extends Component<IProps, IState> {
|
||||
titleKey = 'feedback.rateExperience'>
|
||||
<div className = { classes.dialog }>
|
||||
<div className = { classes.rating }>
|
||||
<div
|
||||
aria-label = { this.props.t('feedback.star') }
|
||||
className = { classes.ratingLabel } >
|
||||
<p id = 'starLabel'>
|
||||
{ t(SCORES[scoreToDisplayAsSelected]) }
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className = { classes.stars }
|
||||
onMouseLeave = { this._onScoreContainerMouseLeave }>
|
||||
{ scoreIcons }
|
||||
</div>
|
||||
<div
|
||||
className = { classes.ratingLabel } >
|
||||
<p className = 'sr-only'>
|
||||
{ t('feedback.accessibilityLabel.yourChoice', {
|
||||
rating: t(SCORES[scoreToDisplayAsSelected])
|
||||
}) }
|
||||
</p>
|
||||
<p
|
||||
aria-hidden = { true }
|
||||
id = 'starLabel'>
|
||||
{ t(SCORES[scoreToDisplayAsSelected]) }
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className = { classes.details }>
|
||||
<Input
|
||||
autoFocus = { true }
|
||||
id = 'feedbackTextArea'
|
||||
label = { t('feedback.detailsLabel') }
|
||||
onChange = { this._onMessageChange }
|
||||
|
||||
@@ -903,6 +903,7 @@ class Thumbnail extends Component<IProps, IState> {
|
||||
tabIndex = { 0 }>
|
||||
{avatarURL ? (
|
||||
<img
|
||||
alt = ''
|
||||
className = 'sharedVideoAvatar'
|
||||
src = { avatarURL } />
|
||||
)
|
||||
@@ -1105,6 +1106,20 @@ class Thumbnail extends Component<IProps, IState> {
|
||||
? <span id = 'localVideoWrapper'>{video}</span>
|
||||
: video)}
|
||||
<div className = { classes.containerBackground } />
|
||||
{/* put the bottom container before the top container in the dom,
|
||||
because it contains the participant name that should be announced first by screen readers */}
|
||||
<div
|
||||
className = { clsx(classes.indicatorsContainer,
|
||||
classes.indicatorsBottomContainer,
|
||||
_thumbnailType === THUMBNAIL_TYPE.TILE && 'tile-view-mode'
|
||||
) }>
|
||||
<ThumbnailBottomIndicators
|
||||
className = { classes.indicatorsBackground }
|
||||
local = { local }
|
||||
participantId = { id }
|
||||
showStatusIndicators = { !isWhiteboardParticipant(_participant) }
|
||||
thumbnailType = { _thumbnailType } />
|
||||
</div>
|
||||
<div
|
||||
className = { clsx(classes.indicatorsContainer,
|
||||
classes.indicatorsTopContainer,
|
||||
@@ -1122,18 +1137,6 @@ class Thumbnail extends Component<IProps, IState> {
|
||||
thumbnailType = { _thumbnailType } />
|
||||
</div>
|
||||
{_shouldDisplayTintBackground && <div className = { classes.tintBackground } />}
|
||||
<div
|
||||
className = { clsx(classes.indicatorsContainer,
|
||||
classes.indicatorsBottomContainer,
|
||||
_thumbnailType === THUMBNAIL_TYPE.TILE && 'tile-view-mode'
|
||||
) }>
|
||||
<ThumbnailBottomIndicators
|
||||
className = { classes.indicatorsBackground }
|
||||
local = { local }
|
||||
participantId = { id }
|
||||
showStatusIndicators = { !isWhiteboardParticipant(_participant) }
|
||||
thumbnailType = { _thumbnailType } />
|
||||
</div>
|
||||
{!_gifSrc && this._renderAvatar(styles.avatar) }
|
||||
{ !local && (
|
||||
<div className = 'presence-label-container'>
|
||||
|
||||
@@ -208,6 +208,7 @@ function GifsMenu({ columns = 2, parent }: IProps) {
|
||||
<Input
|
||||
autoFocus = { true }
|
||||
className = { cx(styles.searchField, 'gif-input') }
|
||||
id = 'gif-search-input'
|
||||
onChange = { handleSearchKeyChange }
|
||||
onKeyPress = { onInputKeyPress }
|
||||
placeholder = { t('giphy.search') }
|
||||
|
||||
@@ -34,15 +34,12 @@ function CopyMeetingLinkSection({ url }: IProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
className = { classes.label }
|
||||
htmlFor = { 'copy-button-id' }
|
||||
id = 'copy-button-label'>{t('addPeople.shareLink')}</label>
|
||||
<p className = { classes.label }>{t('addPeople.shareLink')}</p>
|
||||
<CopyButton
|
||||
aria-label = { t('addPeople.copyLink') }
|
||||
accessibilityText = { t('addPeople.accessibilityLabel.meetingLink', { url: getDecodedURI(url) }) }
|
||||
className = 'invite-more-dialog-conference-url'
|
||||
displayedText = { getDecodedURI(url) }
|
||||
id = 'copy-button-id'
|
||||
id = 'add-people-copy-link-button'
|
||||
textOnCopySuccess = { t('addPeople.linkCopied') }
|
||||
textOnHover = { t('addPeople.copyLink') }
|
||||
textToCopy = { url } />
|
||||
|
||||
@@ -44,7 +44,6 @@ class DialInNumber extends Component<IProps> {
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onCopyText = this._onCopyText.bind(this);
|
||||
this._onCopyTextKeyPress = this._onCopyTextKeyPress.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,20 +61,6 @@ class DialInNumber extends Component<IProps> {
|
||||
copyText(textToCopy);
|
||||
}
|
||||
|
||||
/**
|
||||
* KeyPress handler for accessibility.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCopyTextKeyPress(e: React.KeyboardEvent) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this._onCopyText();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
@@ -87,7 +72,7 @@ class DialInNumber extends Component<IProps> {
|
||||
|
||||
return (
|
||||
<div className = 'dial-in-number'>
|
||||
<div>
|
||||
<p>
|
||||
<span className = 'phone-number'>
|
||||
<span className = 'info-label'>
|
||||
{ t('info.dialInNumber') }
|
||||
@@ -107,16 +92,13 @@ class DialInNumber extends Component<IProps> {
|
||||
{ `${_formatConferenceIDPin(conferenceID)}#` }
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<a
|
||||
</p>
|
||||
<button
|
||||
aria-label = { t('info.copyNumber') }
|
||||
className = 'dial-in-copy'
|
||||
onClick = { this._onCopyText }
|
||||
onKeyPress = { this._onCopyTextKeyPress }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
className = 'dial-in-copy invisible-button'
|
||||
onClick = { this._onCopyText }>
|
||||
<Icon src = { IconCopy } />
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -185,6 +185,7 @@ class InviteContactsForm extends AbstractAddPeopleDialog<IProps, IState> {
|
||||
className = { this.props.classes.formWrap }
|
||||
onKeyDown = { this._onKeyDown }>
|
||||
<MultiSelectAutocomplete
|
||||
id = 'invite-contacts-input'
|
||||
isDisabled = { isMultiSelectDisabled }
|
||||
loadingMessage = { t(loadingMessage) }
|
||||
noMatchesFound = { t(noMatches) }
|
||||
|
||||
@@ -158,6 +158,7 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
|
||||
return (
|
||||
<Input
|
||||
className = 'lobby-prejoin-input'
|
||||
id = 'lobby-name-field'
|
||||
onChange = { this._onChangeDisplayName }
|
||||
placeholder = { t('lobby.nameField') }
|
||||
testId = 'lobby.nameField'
|
||||
@@ -177,6 +178,7 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
|
||||
<>
|
||||
<Input
|
||||
className = { `lobby-prejoin-input ${_passwordJoinFailed ? 'error' : ''}` }
|
||||
id = 'lobby-password-input'
|
||||
onChange = { this._onChangePassword }
|
||||
placeholder = { t('lobby.passwordField') }
|
||||
testId = 'lobby.password'
|
||||
|
||||
@@ -8,6 +8,7 @@ import { IReduxState, IStore } from '../../../app/types';
|
||||
import DialInSummary from '../../../invite/components/dial-in-summary/native/DialInSummary';
|
||||
import Prejoin from '../../../prejoin/components/native/Prejoin';
|
||||
import UnsafeRoomWarning from '../../../prejoin/components/native/UnsafeRoomWarning';
|
||||
import { isUnsafeRoomWarningEnabled } from '../../../prejoin/functions';
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
import WelcomePage from '../../../welcome/components/WelcomePage';
|
||||
@@ -39,6 +40,11 @@ interface IProps {
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Is unsafe room warning available?
|
||||
*/
|
||||
isUnsafeRoomWarningAvailable: boolean;
|
||||
|
||||
/**
|
||||
* Is welcome page available?
|
||||
*/
|
||||
@@ -46,7 +52,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
|
||||
const RootNavigationContainer = ({ dispatch, isWelcomePageAvailable }: IProps) => {
|
||||
const RootNavigationContainer = ({ dispatch, isUnsafeRoomWarningAvailable, isWelcomePageAvailable }: IProps) => {
|
||||
const initialRouteName = isWelcomePageAvailable
|
||||
? screen.welcome.main : screen.connecting;
|
||||
const onReady = useCallback(() => {
|
||||
@@ -92,10 +98,13 @@ const RootNavigationContainer = ({ dispatch, isWelcomePageAvailable }: IProps) =
|
||||
component = { Prejoin }
|
||||
name = { screen.preJoin }
|
||||
options = { preJoinScreenOptions } />
|
||||
<RootStack.Screen
|
||||
component = { UnsafeRoomWarning }
|
||||
name = { screen.unsafeRoomWarning }
|
||||
options = { unsafeMeetingScreenOptions } />
|
||||
{
|
||||
isUnsafeRoomWarningAvailable
|
||||
&& <RootStack.Screen
|
||||
component = { UnsafeRoomWarning }
|
||||
name = { screen.unsafeRoomWarning }
|
||||
options = { unsafeMeetingScreenOptions } />
|
||||
}
|
||||
<RootStack.Screen
|
||||
component = { ConferenceNavigationContainer }
|
||||
name = { screen.conference.root }
|
||||
@@ -113,6 +122,7 @@ const RootNavigationContainer = ({ dispatch, isWelcomePageAvailable }: IProps) =
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
isUnsafeRoomWarningAvailable: isUnsafeRoomWarningEnabled(state),
|
||||
isWelcomePageAvailable: isWelcomePageEnabled(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ function MeetingParticipants({
|
||||
<Input
|
||||
className = { styles.search }
|
||||
clearable = { true }
|
||||
id = 'participants-search-input'
|
||||
onChange = { setSearchString }
|
||||
placeholder = { t('participantsPane.search') }
|
||||
value = { searchString } />
|
||||
|
||||
@@ -191,6 +191,7 @@ const PollCreate = ({
|
||||
<div className = { classes.questionContainer }>
|
||||
<Input
|
||||
autoFocus = { true }
|
||||
id = 'polls-create-input'
|
||||
label = { t('polls.create.pollQuestion') }
|
||||
maxLength = { CHAR_LIMIT }
|
||||
onChange = { setQuestion }
|
||||
@@ -205,6 +206,7 @@ const PollCreate = ({
|
||||
className = { classes.answer }
|
||||
key = { i }>
|
||||
<Input
|
||||
id = { `polls-answer-input-${i}` }
|
||||
label = { t('polls.create.pollOption', { index: i + 1 }) }
|
||||
maxLength = { CHAR_LIMIT }
|
||||
onChange = { val => setAnswer(i, val) }
|
||||
|
||||
@@ -374,10 +374,12 @@ const Prejoin = ({
|
||||
className = { classes.inputContainer }
|
||||
data-testid = 'prejoin.screen'>
|
||||
{showDisplayNameField.current ? (<Input
|
||||
accessibilityLabel = { t('dialog.enterDisplayName') }
|
||||
autoComplete = { 'name' }
|
||||
autoFocus = { true }
|
||||
className = { classes.input }
|
||||
error = { showErrorOnJoin }
|
||||
id = 'premeeting-name-input'
|
||||
onChange = { setName }
|
||||
onKeyPress = { showUnsafeRoomWarning && !unsafeRoomConsent ? undefined : onInputKeyPress }
|
||||
placeholder = { t('dialog.enterDisplayName') }
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import { getRoomName } from '../base/conference/functions';
|
||||
import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions';
|
||||
import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions.any';
|
||||
import { UNSAFE_ROOM_WARNING } from '../base/flags/constants';
|
||||
import { getFeatureFlag } from '../base/flags/functions';
|
||||
import { isAudioMuted, isVideoMutedByUser } from '../base/media/functions';
|
||||
import { getLobbyConfig } from '../lobby/functions';
|
||||
|
||||
|
||||
/**
|
||||
* Selector for the visibility of the 'join by phone' button.
|
||||
*
|
||||
@@ -169,3 +172,14 @@ export function shouldAutoKnock(state: IReduxState): boolean {
|
||||
|| autoKnock || (iAmRecorder && iAmSipGateway))
|
||||
&& !state['features/lobby'].knocking);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the unsafe room warning flag is enabled.
|
||||
*
|
||||
* @param {IReduxState} stateful - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isUnsafeRoomWarningEnabled(stateful: IReduxState): boolean {
|
||||
return Boolean(navigator.product === 'ReactNative'
|
||||
&& getFeatureFlag(stateful, UNSAFE_ROOM_WARNING, true));
|
||||
}
|
||||
|
||||
@@ -119,9 +119,6 @@ function ReactionsMenuButton({
|
||||
if (_reactionsButtonEnabled) {
|
||||
content = (
|
||||
<ToolboxButtonWithPopup
|
||||
ariaControls = 'reactions-menu-dialog'
|
||||
ariaExpanded = { isOpen }
|
||||
ariaHasPopup = { true }
|
||||
ariaLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
|
||||
onPopoverClose = { closeReactionsMenu }
|
||||
onPopoverOpen = { openReactionsMenu }
|
||||
@@ -141,13 +138,9 @@ function ReactionsMenuButton({
|
||||
notifyMode = { notifyMode } />)
|
||||
: (
|
||||
<ToolboxButtonWithPopup
|
||||
ariaControls = 'reactions-menu-dialog'
|
||||
ariaExpanded = { isOpen }
|
||||
ariaHasPopup = { true }
|
||||
ariaLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
|
||||
icon = { IconArrowUp }
|
||||
iconDisabled = { false }
|
||||
iconId = 'reactions-menu-button'
|
||||
onPopoverClose = { toggleReactionsMenu }
|
||||
onPopoverOpen = { openReactionsMenu }
|
||||
popoverContent = { reactionsMenu }
|
||||
|
||||
@@ -38,7 +38,8 @@ export interface IProps extends AbstractButtonProps {
|
||||
* An abstract class of a button for starting and stopping live streaming.
|
||||
*/
|
||||
export default class AbstractLiveStreamButton<P extends IProps> extends AbstractButton<P> {
|
||||
accessibilityLabel = 'dialog.accessibilityLabel.liveStreaming';
|
||||
accessibilityLabel = 'dialog.startLiveStreaming';
|
||||
toggledAccessibilityLabel = 'dialog.stopLiveStreaming';
|
||||
icon = IconSites;
|
||||
label = 'dialog.startLiveStreaming';
|
||||
toggledLabel = 'dialog.stopLiveStreaming';
|
||||
|
||||
@@ -39,20 +39,6 @@ const styles = (theme: Theme) => {
|
||||
*/
|
||||
class StreamKeyForm extends AbstractStreamKeyForm<IProps> {
|
||||
|
||||
/**
|
||||
* Initializes a new {@code StreamKeyForm} instance.
|
||||
*
|
||||
* @param {IProps} props - The React {@code Component} props to initialize
|
||||
* the new {@code StreamKeyForm} instance with.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onOpenHelp = this._onOpenHelp.bind(this);
|
||||
this._onOpenHelpKeyPress = this._onOpenHelpKeyPress.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
@@ -66,6 +52,7 @@ class StreamKeyForm extends AbstractStreamKeyForm<IProps> {
|
||||
<div className = 'stream-key-form'>
|
||||
<Input
|
||||
autoFocus = { true }
|
||||
id = 'streamkey-input'
|
||||
label = { t('dialog.streamKey') }
|
||||
name = 'streamId'
|
||||
onChange = { this._onInputChange }
|
||||
@@ -83,12 +70,10 @@ class StreamKeyForm extends AbstractStreamKeyForm<IProps> {
|
||||
}
|
||||
{ this.props._liveStreaming.helpURL
|
||||
? <a
|
||||
aria-label = { t('liveStreaming.streamIdHelp') }
|
||||
className = { classes.helperLink }
|
||||
onClick = { this._onOpenHelp }
|
||||
onKeyPress = { this._onOpenHelpKeyPress }
|
||||
role = 'link'
|
||||
tabIndex = { 0 }>
|
||||
href = { this.props._liveStreaming.helpURL }
|
||||
rel = 'noopener noreferrer'
|
||||
target = '_blank'>
|
||||
{ t('liveStreaming.streamIdHelp') }
|
||||
</a>
|
||||
: null
|
||||
@@ -112,33 +97,6 @@ class StreamKeyForm extends AbstractStreamKeyForm<IProps> {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a new tab with information on how to manually locate a YouTube
|
||||
* broadcast stream key.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onOpenHelp() {
|
||||
window.open(this.props._liveStreaming.helpURL, '_blank', 'noopener');
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a new tab with information on how to manually locate a YouTube
|
||||
* broadcast stream key.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onOpenHelpKeyPress(e: React.KeyboardEvent) {
|
||||
if (e.key === ' ') {
|
||||
e.preventDefault();
|
||||
this._onOpenHelp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(withStyles(styles)(StreamKeyForm)));
|
||||
|
||||
@@ -100,6 +100,7 @@ class StreamKeyPicker extends PureComponent<IProps> {
|
||||
return (
|
||||
<div className = 'broadcast-dropdown dropdown-menu'>
|
||||
<Select
|
||||
id = 'streamkeypicker-select'
|
||||
label = { t('liveStreaming.choose') }
|
||||
onChange = { this._onSelect }
|
||||
options = { dropdownItems }
|
||||
|
||||
@@ -36,7 +36,8 @@ export interface IProps extends AbstractButtonProps {
|
||||
* An abstract implementation of a button for starting and stopping recording.
|
||||
*/
|
||||
export default class AbstractRecordButton<P extends IProps> extends AbstractButton<P> {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.recording';
|
||||
accessibilityLabel = 'dialog.startRecording';
|
||||
toggledAccessibilityLabel = 'dialog.stopRecording';
|
||||
icon = IconRecord;
|
||||
label = 'dialog.startRecording';
|
||||
toggledLabel = 'dialog.stopRecording';
|
||||
|
||||
@@ -79,6 +79,7 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
|
||||
checked = { selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE }
|
||||
className = 'recording-switch'
|
||||
disabled = { isValidating }
|
||||
id = 'recording-switch-jitsi'
|
||||
onChange = { this._onRecordingServiceSwitchChange } />
|
||||
) : null;
|
||||
|
||||
@@ -98,12 +99,15 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
|
||||
key = 'noIntegrationSetting'>
|
||||
<Container className = { contentRecordingClass }>
|
||||
<Image
|
||||
alt = ''
|
||||
className = 'content-recording-icon'
|
||||
src = { ICON_CLOUD } />
|
||||
</Container>
|
||||
<Text className = 'recording-title'>
|
||||
<label
|
||||
className = 'recording-title'
|
||||
htmlFor = 'recording-switch-jitsi'>
|
||||
{ label }
|
||||
</Text>
|
||||
</label>
|
||||
{ switchContent }
|
||||
</Container>
|
||||
);
|
||||
@@ -132,16 +136,20 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
|
||||
key = 'fileSharingSetting'>
|
||||
<Container className = 'recording-icon-container file-sharing-icon-container'>
|
||||
<Image
|
||||
alt = ''
|
||||
className = 'recording-file-sharing-icon'
|
||||
src = { ICON_USERS } />
|
||||
</Container>
|
||||
<Text className = 'recording-title'>
|
||||
<label
|
||||
className = 'recording-title'
|
||||
htmlFor = 'recording-switch-share'>
|
||||
{ t('recording.fileSharingdescription') }
|
||||
</Text>
|
||||
</label>
|
||||
<Switch
|
||||
checked = { sharingSetting }
|
||||
className = 'recording-switch'
|
||||
disabled = { isValidating }
|
||||
id = 'recording-switch-share'
|
||||
onChange = { onSharingSettingChanged } />
|
||||
</Container>
|
||||
);
|
||||
@@ -169,6 +177,7 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
|
||||
className = 'recording-info'
|
||||
key = 'cloudUploadInfo'>
|
||||
<Image
|
||||
alt = ''
|
||||
className = 'recording-info-icon'
|
||||
src = { ICON_INFO } />
|
||||
<Text className = 'recording-info-title'>
|
||||
@@ -246,6 +255,11 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
|
||||
} = this.props;
|
||||
let content = null;
|
||||
let switchContent = null;
|
||||
let labelContent = (
|
||||
<Text className = 'recording-title'>
|
||||
{ t('recording.authDropboxText') }
|
||||
</Text>
|
||||
);
|
||||
|
||||
if (isValidating) {
|
||||
content = this._renderSpinner();
|
||||
@@ -281,8 +295,16 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
|
||||
=== RECORDING_TYPES.DROPBOX }
|
||||
className = 'recording-switch'
|
||||
disabled = { isValidating }
|
||||
id = 'recording-switch-integration'
|
||||
onChange = { this._onDropboxSwitchChange } />
|
||||
);
|
||||
labelContent = (
|
||||
<label
|
||||
className = 'recording-title'
|
||||
htmlFor = 'recording-switch-integration'>
|
||||
{ t('recording.authDropboxText') }
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -293,12 +315,11 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
|
||||
<Container
|
||||
className = 'recording-icon-container'>
|
||||
<Image
|
||||
alt = ''
|
||||
className = 'recording-icon'
|
||||
src = { DROPBOX_LOGO } />
|
||||
</Container>
|
||||
<Text className = 'recording-title'>
|
||||
{ t('recording.authDropboxText') }
|
||||
</Text>
|
||||
{ labelContent }
|
||||
{ switchContent }
|
||||
</Container>
|
||||
<Container className = 'authorization-panel'>
|
||||
@@ -338,17 +359,21 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
|
||||
<Container
|
||||
className = 'recording-icon-container'>
|
||||
<Image
|
||||
alt = ''
|
||||
className = 'recording-icon'
|
||||
src = { LOCAL_RECORDING } />
|
||||
</Container>
|
||||
<Text className = 'recording-title'>
|
||||
<label
|
||||
className = 'recording-title'
|
||||
htmlFor = 'recording-switch-local'>
|
||||
{ t('recording.saveLocalRecording') }
|
||||
</Text>
|
||||
</label>
|
||||
<Switch
|
||||
checked = { selectedRecordingService
|
||||
=== RECORDING_TYPES.LOCAL }
|
||||
className = 'recording-switch'
|
||||
disabled = { isValidating }
|
||||
id = 'recording-switch-local'
|
||||
onChange = { this._onLocalRecordingSwitchChange } />
|
||||
</Container>
|
||||
</Container>
|
||||
@@ -359,16 +384,20 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
|
||||
<Container className = 'recording-header space-top'>
|
||||
<Container className = 'recording-icon-container file-sharing-icon-container'>
|
||||
<Image
|
||||
alt = ''
|
||||
className = 'recording-file-sharing-icon'
|
||||
src = { ICON_USERS } />
|
||||
</Container>
|
||||
<Text className = 'recording-title'>
|
||||
<label
|
||||
className = 'recording-title'
|
||||
htmlFor = 'recording-switch-myself'>
|
||||
{t('recording.onlyRecordSelf')}
|
||||
</Text>
|
||||
</label>
|
||||
<Switch
|
||||
checked = { Boolean(localRecordingOnlySelf) }
|
||||
className = 'recording-switch'
|
||||
disabled = { isValidating }
|
||||
id = 'recording-switch-myself'
|
||||
onChange = { onLocalRecordingSelfChange ?? EMPTY_FUNCTION } />
|
||||
</Container>
|
||||
</Container>
|
||||
|
||||
@@ -93,6 +93,7 @@ class PasswordRequiredPrompt extends Component<IProps, IState> {
|
||||
<Input
|
||||
autoFocus = { true }
|
||||
className = 'dialog-bottom-margin'
|
||||
id = 'required-password-input'
|
||||
label = { this.props.t('dialog.passwordLabel') }
|
||||
name = 'lockKey'
|
||||
onChange = { this._onPasswordChanged }
|
||||
|
||||
@@ -83,6 +83,7 @@ class ShareAudioDialog extends Component<IProps> {
|
||||
titleKey = { t('dialog.shareAudioTitle') }>
|
||||
<div className = 'share-audio-dialog'>
|
||||
<img
|
||||
alt = ''
|
||||
className = 'share-audio-animation'
|
||||
src = 'images/share-audio.gif' />
|
||||
<Checkbox
|
||||
|
||||
@@ -140,22 +140,29 @@ class PasswordForm extends Component<IProps, IState> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderPasswordField() {
|
||||
if (this.props.editEnabled) {
|
||||
let placeHolderText = this.props.t('dialog.password');
|
||||
const {
|
||||
editEnabled,
|
||||
passwordNumberOfDigits,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
if (this.props.passwordNumberOfDigits) {
|
||||
if (editEnabled) {
|
||||
let placeHolderText = t('dialog.password');
|
||||
|
||||
if (passwordNumberOfDigits) {
|
||||
placeHolderText = this.props.t('passwordDigitsOnly', {
|
||||
number: this.props.passwordNumberOfDigits });
|
||||
number: passwordNumberOfDigits });
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'info-password-form'>
|
||||
<Input
|
||||
accessibilityLabel = { this.props.t('info.addPassword') }
|
||||
accessibilityLabel = { t('info.addPassword') }
|
||||
autoFocus = { true }
|
||||
id = 'info-password-input'
|
||||
maxLength = { this.props.passwordNumberOfDigits }
|
||||
maxLength = { passwordNumberOfDigits }
|
||||
mode = { passwordNumberOfDigits ? 'numeric' : undefined }
|
||||
onChange = { this._onEnteredPasswordChange }
|
||||
onKeyPress = { this._onKeyPress }
|
||||
placeholder = { placeHolderText }
|
||||
|
||||
@@ -168,67 +168,6 @@ function PasswordSection({
|
||||
copyText(password ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles whether or not the password should currently be shown as being
|
||||
* edited locally.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function onTogglePasswordEditStateKeyPressHandler(e: React.KeyboardEvent) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
onTogglePasswordEditState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to remotely submit the password from outside of the password form.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function onPasswordSaveKeyPressHandler(e: React.KeyboardEvent) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
onPasswordSave();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to unlock the current JitsiConference.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function onPasswordRemoveKeyPressHandler(e: React.KeyboardEvent) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
onPasswordRemove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the password to the clipboard.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function onPasswordCopyKeyPressHandler(e: React.KeyboardEvent) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
onPasswordCopy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to show the current password.
|
||||
*
|
||||
@@ -238,20 +177,6 @@ function PasswordSection({
|
||||
setPasswordVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to show the current password.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function onPasswordShowKeyPressHandler(e: React.KeyboardEvent) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
setPasswordVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to hide the current password.
|
||||
*
|
||||
@@ -261,20 +186,6 @@ function PasswordSection({
|
||||
setPasswordVisible(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to hide the current password.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function onPasswordHideKeyPressHandler(e: React.KeyboardEvent) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
setPasswordVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that renders the password action(s) based on the current
|
||||
* locked-status of the conference.
|
||||
@@ -289,18 +200,20 @@ function PasswordSection({
|
||||
if (passwordEditEnabled) {
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
aria-label = { t('dialog.Cancel') }
|
||||
<button
|
||||
className = 'as-link'
|
||||
onClick = { onTogglePasswordEditState }
|
||||
onKeyPress = { onTogglePasswordEditStateKeyPressHandler }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>{ t('dialog.Cancel') }</a>
|
||||
<a
|
||||
aria-label = { t('dialog.add') }
|
||||
type = 'button'>
|
||||
{ t('dialog.Cancel') }
|
||||
<span className = 'sr-only'>({ t('dialog.password') })</span>
|
||||
</button>
|
||||
<button
|
||||
className = 'as-link'
|
||||
onClick = { onPasswordSave }
|
||||
onKeyPress = { onPasswordSaveKeyPressHandler }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>{ t('dialog.add') }</a>
|
||||
type = 'button'>
|
||||
{ t('dialog.add') }
|
||||
<span className = 'sr-only'>({ t('dialog.password') })</span>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -308,49 +221,44 @@ function PasswordSection({
|
||||
if (locked) {
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
aria-label = { t('dialog.Remove') }
|
||||
className = 'remove-password'
|
||||
<button
|
||||
className = 'remove-password as-link'
|
||||
onClick = { onPasswordRemove }
|
||||
onKeyPress = { onPasswordRemoveKeyPressHandler }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>{ t('dialog.Remove') }</a>
|
||||
type = 'button'>
|
||||
{ t('dialog.Remove') }
|
||||
<span className = 'sr-only'>({ t('dialog.password') })</span>
|
||||
</button>
|
||||
{
|
||||
|
||||
// There are cases like lobby and grant moderator when password is not available
|
||||
password ? <>
|
||||
<a
|
||||
aria-label = { t('dialog.copy') }
|
||||
className = 'copy-password'
|
||||
<button
|
||||
className = 'copy-password as-link'
|
||||
onClick = { onPasswordCopy }
|
||||
onKeyPress = { onPasswordCopyKeyPressHandler }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>{ t('dialog.copy') }</a>
|
||||
type = 'button'>
|
||||
{ t('dialog.copy') }
|
||||
<span className = 'sr-only'>({ t('dialog.password') })</span>
|
||||
</button>
|
||||
</> : null
|
||||
}
|
||||
{locked === LOCKED_LOCALLY && (
|
||||
<a
|
||||
aria-label = { t(passwordVisible ? 'dialog.hide' : 'dialog.show') }
|
||||
<button
|
||||
className = 'as-link'
|
||||
onClick = { passwordVisible ? onPasswordHide : onPasswordShow }
|
||||
onKeyPress = { passwordVisible
|
||||
? onPasswordHideKeyPressHandler
|
||||
: onPasswordShowKeyPressHandler
|
||||
}
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>{t(passwordVisible ? 'dialog.hide' : 'dialog.show')}</a>
|
||||
type = 'button'>
|
||||
{t(passwordVisible ? 'dialog.hide' : 'dialog.show')}
|
||||
<span className = 'sr-only'>({ t('dialog.password') })</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
aria-label = { t('info.addPassword') }
|
||||
className = 'add-password'
|
||||
<button
|
||||
className = 'add-password as-link'
|
||||
onClick = { onTogglePasswordEditState }
|
||||
onKeyPress = { onTogglePasswordEditStateKeyPressHandler }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>{ t('info.addPassword') }</a>
|
||||
type = 'button'>{ t('info.addPassword') }</button>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ export function openLogoutDialog(onLogout: Function) {
|
||||
* welcome page or not.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function openSettingsDialog(defaultTab: string, isDisplayedOnWelcomePage?: boolean) {
|
||||
export function openSettingsDialog(defaultTab?: string, isDisplayedOnWelcomePage?: boolean) {
|
||||
return openDialog(SettingsDialog, {
|
||||
defaultTab,
|
||||
isDisplayedOnWelcomePage
|
||||
|
||||
@@ -254,6 +254,7 @@ class MoreTab extends AbstractDialogTab<IProps, any> {
|
||||
|
||||
return (
|
||||
<Select
|
||||
id = 'more-maxStageParticipants-select'
|
||||
label = { t('settings.maxStageParticipants') }
|
||||
onChange = { this._onMaxStageParticipantsSelect }
|
||||
options = { maxParticipantsItems }
|
||||
@@ -286,6 +287,7 @@ class MoreTab extends AbstractDialogTab<IProps, any> {
|
||||
return (
|
||||
<Select
|
||||
className = { classes.bottomMargin }
|
||||
id = 'more-language-select'
|
||||
label = { t('settings.language') }
|
||||
onChange = { this._onLanguageItemSelect }
|
||||
options = { languageItems }
|
||||
|
||||
@@ -6,7 +6,6 @@ import { translate } from '../../../base/i18n/functions';
|
||||
import { IconGear } from '../../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
||||
import { openSettingsDialog } from '../../actions';
|
||||
import { SETTINGS_TABS } from '../../constants';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link SettingsButton}.
|
||||
@@ -41,10 +40,10 @@ class SettingsButton extends AbstractButton<IProps> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { defaultTab = SETTINGS_TABS.AUDIO, dispatch, isDisplayedOnWelcomePage = false } = this.props;
|
||||
const { dispatch, isDisplayedOnWelcomePage = false } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent('settings'));
|
||||
dispatch(openSettingsDialog(defaultTab, isDisplayedOnWelcomePage));
|
||||
dispatch(openSettingsDialog(undefined, isDisplayedOnWelcomePage));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +192,6 @@ const AudioSettingsContent = ({
|
||||
jitsiTrack = { jitsiTrack }
|
||||
key = { `me-${index}` }
|
||||
length = { length }
|
||||
listHeaderId = { microphoneHeaderId }
|
||||
measureAudioLevels = { measureAudioLevels }
|
||||
onClick = { _onMicrophoneEntryClick }>
|
||||
{label}
|
||||
@@ -221,7 +220,6 @@ const AudioSettingsContent = ({
|
||||
isSelected = { isSelected }
|
||||
key = { key }
|
||||
length = { length }
|
||||
listHeaderId = { speakerHeaderId }
|
||||
onClick = { _onSpeakerEntryClick }>
|
||||
{label}
|
||||
</SpeakerEntry>
|
||||
|
||||
@@ -54,8 +54,6 @@ interface IProps {
|
||||
length: number;
|
||||
|
||||
|
||||
listHeaderId: string;
|
||||
|
||||
/**
|
||||
* Used to decide whether to listen to audio level changes.
|
||||
*/
|
||||
@@ -112,7 +110,6 @@ const MicrophoneEntry = ({
|
||||
isSelected,
|
||||
length,
|
||||
jitsiTrack,
|
||||
listHeaderId,
|
||||
measureAudioLevels,
|
||||
onClick: propsClick
|
||||
}: IProps) => {
|
||||
@@ -138,7 +135,7 @@ const MicrophoneEntry = ({
|
||||
* @returns {void}
|
||||
*/
|
||||
const onKeyPress = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === ' ') {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
propsClick(deviceId);
|
||||
}
|
||||
@@ -190,14 +187,9 @@ const MicrophoneEntry = ({
|
||||
activeTrackRef.current = jitsiTrack;
|
||||
}, [ jitsiTrack ]);
|
||||
|
||||
const deviceTextId = `choose_microphone${deviceId}`;
|
||||
|
||||
const labelledby = `${listHeaderId} ${deviceTextId} `;
|
||||
|
||||
return (
|
||||
<li
|
||||
aria-checked = { isSelected }
|
||||
aria-labelledby = { labelledby }
|
||||
aria-posinset = { index }
|
||||
aria-setsize = { length }
|
||||
className = { classes.container }
|
||||
@@ -206,7 +198,7 @@ const MicrophoneEntry = ({
|
||||
role = 'radio'
|
||||
tabIndex = { 0 }>
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = ''
|
||||
accessibilityLabel = { children }
|
||||
icon = { isSelected ? IconCheck : undefined }
|
||||
overflowType = { TEXT_OVERFLOW_TYPES.SCROLL_ON_HOVER }
|
||||
selected = { isSelected }
|
||||
|
||||
@@ -39,8 +39,6 @@ interface IProps {
|
||||
*/
|
||||
length: number;
|
||||
|
||||
listHeaderId: string;
|
||||
|
||||
/**
|
||||
* Click handler for the component.
|
||||
*/
|
||||
@@ -111,7 +109,7 @@ const SpeakerEntry = (props: IProps) => {
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onKeyPress(e: React.KeyboardEvent) {
|
||||
if (e.key === ' ') {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
props.onClick(props.deviceId);
|
||||
}
|
||||
@@ -135,15 +133,12 @@ const SpeakerEntry = (props: IProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
const { children, isSelected, index, deviceId, length, listHeaderId } = props;
|
||||
const deviceTextId = `choose_speaker${deviceId}`;
|
||||
const labelledby = `${listHeaderId} ${deviceTextId} `;
|
||||
const { children, isSelected, index, length } = props;
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
return (
|
||||
<li
|
||||
aria-checked = { isSelected }
|
||||
aria-labelledby = { labelledby }
|
||||
aria-posinset = { index }
|
||||
aria-setsize = { length }
|
||||
className = { classes.container }
|
||||
@@ -152,7 +147,7 @@ const SpeakerEntry = (props: IProps) => {
|
||||
role = 'radio'
|
||||
tabIndex = { 0 }>
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = ''
|
||||
accessibilityLabel = { children }
|
||||
icon = { isSelected ? IconCheck : undefined }
|
||||
overflowType = { TEXT_OVERFLOW_TYPES.SCROLL_ON_HOVER }
|
||||
selected = { isSelected }
|
||||
|
||||
@@ -302,7 +302,7 @@ const VideoSettingsContent = ({
|
||||
</ContextMenuItemGroup>
|
||||
<ContextMenuItemGroup>
|
||||
{ virtualBackgroundSupported && <ContextMenuItem
|
||||
accessibilityLabel = 'virtualBackground.title'
|
||||
accessibilityLabel = { t('virtualBackground.title') }
|
||||
icon = { IconImage }
|
||||
onClick = { selectBackground }
|
||||
text = { t('virtualBackground.title') } /> }
|
||||
|
||||
@@ -84,15 +84,16 @@ class SharedVideoDialog extends AbstractSharedVideoDialog<any> {
|
||||
titleKey = 'dialog.shareVideoTitle'>
|
||||
<Input
|
||||
autoFocus = { true }
|
||||
bottomLabel = { error && t('dialog.sharedVideoDialogError') }
|
||||
className = 'dialog-bottom-margin'
|
||||
error = { error }
|
||||
id = 'shared-video-url-input'
|
||||
label = { t('dialog.videoLink') }
|
||||
name = 'sharedVideoUrl'
|
||||
onChange = { this._onChange }
|
||||
placeholder = { t('dialog.sharedVideoLinkPlaceholder') }
|
||||
type = 'text'
|
||||
value = { this.state.value } />
|
||||
{ error && <span className = 'shared-video-dialog-error'>{ t('dialog.sharedVideoDialogError') }</span> }
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ const LanguageSelectorDialog = (props: IAbstractLanguageSelectorDialogProps) =>
|
||||
}, [ language ]);
|
||||
|
||||
const onSourceLanguageClick = useCallback(() => {
|
||||
dispatch(openSettingsDialog(SETTINGS_TABS.PROFILE, false));
|
||||
dispatch(openSettingsDialog(SETTINGS_TABS.MORE, false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -22,6 +22,11 @@ interface IProps {
|
||||
*/
|
||||
getRef?: Function;
|
||||
|
||||
/**
|
||||
* Function called when the portal target becomes actually visible.
|
||||
*/
|
||||
onVisible?: Function;
|
||||
|
||||
/**
|
||||
* Function used to get the updated size info of the container on it's resize.
|
||||
*/
|
||||
@@ -45,7 +50,7 @@ interface IProps {
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function DialogPortal({ children, className, style, getRef, setSize, targetSelector }: IProps) {
|
||||
function DialogPortal({ children, className, style, getRef, setSize, targetSelector, onVisible }: IProps) {
|
||||
const clientWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].clientWidth);
|
||||
const [ portalTarget ] = useState(() => {
|
||||
const portalDiv = document.createElement('div');
|
||||
@@ -89,6 +94,7 @@ function DialogPortal({ children, className, style, getRef, setSize, targetSelec
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = window.setTimeout(() => {
|
||||
portalTarget.style.visibility = 'visible';
|
||||
onVisible?.();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { KeyboardEvent, ReactNode, useCallback } from 'react';
|
||||
import ReactFocusLock from 'react-focus-lock';
|
||||
import { FocusOn } from 'react-focus-on';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { isElementInTheViewport } from '../../../base/ui/functions.web';
|
||||
@@ -102,12 +102,7 @@ function Drawer({
|
||||
<div
|
||||
className = { `drawer-menu ${styles.drawer} ${className}` }
|
||||
onClick = { handleInsideClick }>
|
||||
<ReactFocusLock
|
||||
lockProps = {{
|
||||
role: 'dialog',
|
||||
'aria-modal': true,
|
||||
'aria-labelledby': `#${headingId}`
|
||||
}}
|
||||
<FocusOn
|
||||
returnFocus = {
|
||||
|
||||
// If we return the focus to an element outside the viewport the page will scroll to
|
||||
@@ -118,8 +113,15 @@ function Drawer({
|
||||
// because of the animation the whole scenario looks like jumping large video.
|
||||
isElementInTheViewport
|
||||
}>
|
||||
{children}
|
||||
</ReactFocusLock>
|
||||
<div
|
||||
aria-labelledby = { headingId ? `#${headingId}` : undefined }
|
||||
aria-modal = { true }
|
||||
data-autofocus = { true }
|
||||
role = 'dialog'
|
||||
tabIndex = { -1 }>
|
||||
{children}
|
||||
</div>
|
||||
</FocusOn>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
SET_TOOLBOX_TIMEOUT
|
||||
} from './actionTypes';
|
||||
|
||||
import './subscriber';
|
||||
import './subscriber.web';
|
||||
|
||||
/**
|
||||
* Middleware which intercepts Toolbox actions to handle changes to the
|
||||
@@ -18,7 +18,6 @@ import './subscriber';
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
switch (action.type) {
|
||||
case CLEAR_TOOLBOX_TIMEOUT: {
|
||||
const { timeoutID } = store.getState()['features/toolbox'];
|
||||
@@ -45,7 +44,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
});
|
||||
|
||||
type DocumentElement = {
|
||||
mozRequestFullScreen?: Function;
|
||||
requestFullscreen?: Function;
|
||||
webkitRequestFullscreen?: Function;
|
||||
};
|
||||
@@ -63,34 +61,26 @@ type DocumentElement = {
|
||||
function _setFullScreen(next: Function, action: AnyAction) {
|
||||
const result = next(action);
|
||||
|
||||
if (typeof APP === 'object') {
|
||||
const { fullScreen } = action;
|
||||
const { fullScreen } = action;
|
||||
|
||||
if (fullScreen) {
|
||||
const documentElement: DocumentElement
|
||||
= document.documentElement || {};
|
||||
if (fullScreen) {
|
||||
const documentElement: DocumentElement
|
||||
= document.documentElement || {};
|
||||
|
||||
if (typeof documentElement.requestFullscreen === 'function') {
|
||||
documentElement.requestFullscreen();
|
||||
} else if (
|
||||
typeof documentElement.mozRequestFullScreen === 'function') {
|
||||
documentElement.mozRequestFullScreen();
|
||||
} else if (
|
||||
typeof documentElement.webkitRequestFullscreen === 'function') {
|
||||
documentElement.webkitRequestFullscreen();
|
||||
}
|
||||
|
||||
return result;
|
||||
if (typeof documentElement.requestFullscreen === 'function') {
|
||||
documentElement.requestFullscreen();
|
||||
} else if (
|
||||
typeof documentElement.webkitRequestFullscreen === 'function') {
|
||||
documentElement.webkitRequestFullscreen();
|
||||
}
|
||||
|
||||
if (typeof document.exitFullscreen === 'function') {
|
||||
document.exitFullscreen();
|
||||
return result;
|
||||
}
|
||||
|
||||
} else if (typeof document.mozCancelFullScreen === 'function') {
|
||||
document.mozCancelFullScreen();
|
||||
} else if (typeof document.webkitExitFullscreen === 'function') {
|
||||
document.webkitExitFullscreen();
|
||||
}
|
||||
if (typeof document.exitFullscreen === 'function') {
|
||||
document.exitFullscreen();
|
||||
} else if (typeof document.webkitExitFullscreen === 'function') {
|
||||
document.webkitExitFullscreen();
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -15,10 +15,6 @@ import { isAudioMuteButtonDisabled } from './functions.any';
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ (state: IReduxState) => isAudioMuteButtonDisabled(state),
|
||||
/* listener */ (disabled: boolean, store: IStore, previousDisabled: boolean) => {
|
||||
if (typeof APP !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (disabled !== previousDisabled) {
|
||||
APP.API.notifyAudioAvailabilityChanged(!disabled);
|
||||
}
|
||||
@@ -143,7 +143,9 @@ function Slider({ ariaLabel, max, min, onChange, step, value }: IProps) {
|
||||
|
||||
return (
|
||||
<div className = { classes.sliderContainer }>
|
||||
<ul className = { cx('empty-list', classes.knobContainer) }>
|
||||
<ul
|
||||
aria-hidden = { true }
|
||||
className = { cx('empty-list', classes.knobContainer) }>
|
||||
{knobs.map((_, i) => (
|
||||
<li
|
||||
className = { classes.knob }
|
||||
|
||||
@@ -81,6 +81,7 @@ export class VideoQualityLabel extends AbstractVideoQualityLabel<IProps> {
|
||||
content = { t(tooltipKey) }
|
||||
position = { 'bottom' }>
|
||||
<Label
|
||||
accessibilityText = { t(tooltipKey) }
|
||||
className = { className }
|
||||
color = { COLORS.white }
|
||||
icon = { icon }
|
||||
|
||||
@@ -187,9 +187,15 @@ class VideoQualitySlider extends Component<IProps> {
|
||||
|
||||
return (
|
||||
<div className = { clsx('video-quality-dialog', classes.dialog) }>
|
||||
<div className = { classes.dialogDetails }>{t('videoStatus.adjustFor')}</div>
|
||||
<div
|
||||
aria-hidden = { true }
|
||||
className = { classes.dialogDetails }>
|
||||
{t('videoStatus.adjustFor')}
|
||||
</div>
|
||||
<div className = { classes.dialogContents }>
|
||||
<div className = { classes.sliderDescription }>
|
||||
<div
|
||||
aria-hidden = { true }
|
||||
className = { classes.sliderDescription }>
|
||||
<span>{t('videoStatus.bestPerformance')}</span>
|
||||
<span>{t('videoStatus.highestQuality')}</span>
|
||||
</div>
|
||||
|
||||
@@ -122,7 +122,6 @@ function UploadImageButton({
|
||||
return (
|
||||
<>
|
||||
{showLabel && <label
|
||||
aria-label = { t('virtualBackground.uploadImage') }
|
||||
className = { classes.label }
|
||||
htmlFor = 'file-upload'
|
||||
onKeyPress = { uploadImageKeyPress }
|
||||
|
||||
@@ -360,6 +360,24 @@ function VirtualBackgrounds({
|
||||
await setPreviewIsLoaded(loaded);
|
||||
}, []);
|
||||
|
||||
// create a full list of {backgroundId: backgroundLabel} to easily retrieve label of selected background
|
||||
const labelsMap: Record<string, string> = {
|
||||
none: t('virtualBackground.none'),
|
||||
'slight-blur': t('virtualBackground.slightBlur'),
|
||||
blur: t('virtualBackground.blur'),
|
||||
..._images.reduce<Record<string, string>>((acc, image) => {
|
||||
acc[image.id] = image.tooltip ? t(`virtualBackground.${image.tooltip}`) : '';
|
||||
|
||||
return acc;
|
||||
}, {}),
|
||||
...storedImages.reduce<Record<string, string>>((acc, image, index) => {
|
||||
acc[image.id] = t('virtualBackground.uploadedImage', { index: index + 1 });
|
||||
|
||||
return acc;
|
||||
}, {})
|
||||
};
|
||||
const currentBackgroundLabel = labelsMap[selectedThumbnail] || labelsMap.none;
|
||||
|
||||
return (
|
||||
<>
|
||||
<VirtualBackgroundPreview
|
||||
@@ -372,6 +390,13 @@ function VirtualBackgrounds({
|
||||
</div>
|
||||
) : (
|
||||
<div className = { classes.container }>
|
||||
<span
|
||||
className = 'sr-only'
|
||||
id = 'virtual-background-current-info'>
|
||||
{ t('virtualBackground.accessibilityLabel.currentBackground', {
|
||||
background: currentBackgroundLabel
|
||||
}) }
|
||||
</span>
|
||||
{_showUploadButton
|
||||
&& <UploadImageButton
|
||||
setLoading = { setLoading }
|
||||
@@ -380,6 +405,8 @@ function VirtualBackgrounds({
|
||||
showLabel = { previewIsLoaded }
|
||||
storedImages = { storedImages } />}
|
||||
<div
|
||||
aria-describedby = 'virtual-background-current-info'
|
||||
aria-label = { t('virtualBackground.accessibilityLabel.selectBackground') }
|
||||
className = { classes.thumbnailContainer }
|
||||
role = 'radiogroup'
|
||||
tabIndex = { -1 }>
|
||||
|
||||
Reference in New Issue
Block a user