Compare commits

..

21 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
08e2b4a2dd sdk(android/ios): update version to 8.2.2 2023-07-17 15:31:25 +02:00
Saúl Ibarra Corretgé
7baa684e17 chore(rn,deps) react-native@0.69.11 2023-07-17 15:29:56 +02:00
Saúl Ibarra Corretgé
02ed9714f9 fix(android) fix React-Native POM file when publishing
For some reason the packaging mode changed from AAR to POM after 0.68,
and dependencies are now marked optional, when they are not.

Fixes: https://github.com/jitsi/jitsi-meet/issues/13566
2023-07-17 15:29:56 +02:00
Calin-Teodor
c5f91d31e4 sdk(android/ios): update version to 8.2.1 2023-07-14 11:56:25 +03:00
Calinteodor
febb50cd2c feat(base/flags): created flag to control unsafe room warning (#13560)
* feat(base/flags): created flag to control unsafe room warning
2023-07-14 11:44:34 +03:00
Calin-Teodor
879e73c11a feat(android/ios): updated app/sdk versions 2023-06-22 10:09:39 +03:00
Jaya Allamsetty
3ae18be21f fix(lastn) Update lastN on virtual screenshare updates.
Fixes https://github.com/jitsi/jitsi-meet/issues/13448.
2023-06-21 12:30:16 -04:00
Calinteodor
9f5dbb21a7 feat(base/media): fixed movement inside zoomed screenshare (#13476)
* feat(base/media): fixed movement inside zoomed screenshare
2023-06-21 11:59:03 +03:00
Mihaela Dumitru
2d14990b9e chore(deps) update excalidraw to fix load issues with bigger whiteboards (#13474) 2023-06-20 17:43:22 +03:00
Calin-Teodor
169c8ecb62 sdk(react-native-sdk): added generated folders to gitignore 2023-06-20 16:23:19 +03:00
Horatiu Muresan
d608cf40f5 fix(prejoin) Check for valid url for prejoin (#13468)
- `getPropertyValue` calls `parseUrlParam` with the connection URL from store, which is not yet defined
2023-06-19 15:52:38 +03:00
Emmanuel Pelletier
51a4e7daa3 Globally improve accessibility for screen reader users (#12969)
feat(a11y): Globally improve accessibility for screen reader users
2023-06-19 14:34:41 +03:00
arunnadesh
7538bfc713 fix(AudioTrack) fix currentMuted
Co-authored-by: Arun Nadesh <arun.raveendran@hg.ninjavan.co>
2023-06-19 09:51:46 +02:00
Saúl Ibarra Corretgé
48e1f443ea fix(password) use the numeric input mode when only digits are required
Fixes: https://github.com/jitsi/brave-tracker/issues/101
2023-06-16 15:50:27 +02:00
Robert Pintilii
2292ebe762 fix(transcriptions) Open correct settings tab (#13460) 2023-06-15 16:02:12 +03:00
Hristo Terezov
5425b52615 fix(horizontal-filmstrip): JS error.
Fixes the following JS error which prevents the whole page from
rendering:
TypeError: Cannot read properties of null (reading 'offsetHeight')
2023-06-14 18:28:32 -05:00
Hristo Terezov
74f605e045 fix(screenLock): Improve.
- Add debug logs.
 - Re-request wake lock if it is released by the OS because of page
visibility.
2023-06-14 11:15:37 -05:00
Alexander Bigga
1918566581 fix(lang) update German translation 2023-06-13 11:21:36 -05:00
Saúl Ibarra Corretgé
ee8ba6696d fix(full-screen) drop no longer needed checks
The vendored prefix on Firefox was removed on version 64.

We still need the vendored version for Safari since the prefix got
dropped in 16.4.
2023-06-12 13:55:45 +02:00
Saúl Ibarra Corretgé
15df3cb11e fix(toolbox) drop unneeded checks
These are web files, no need to check if APP is undefined.
2023-06-12 13:55:45 +02:00
Robert Pintilii
b77db024f5 fix(settings-dialog) On mobile open on the correct tab (#13443) 2023-06-12 13:55:32 +03:00
92 changed files with 1231 additions and 746 deletions

12
.gitignore vendored
View File

@@ -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

View File

@@ -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.2

View File

@@ -29,6 +29,10 @@ if [[ $MVN_HTTP == 1 ]]; then
# Push React Native
echo "Pushing React Native ${RN_VERSION} to the Maven repo"
pushd ${THIS_DIR}/../../node_modules/react-native/android/com/facebook/react/react-native/${RN_VERSION}
cat react-native-${RN_VERSION}.pom \
| sed "s#<packaging>pom</packaging>#<packaging>aar</packaging>#" \
| sed "/<optional>/d" \
> react-native-${RN_VERSION}-fixed.pom
mvn \
deploy:deploy-file \
-Durl=${MVN_REPO} \
@@ -36,7 +40,7 @@ if [[ $MVN_HTTP == 1 ]]; then
-Dfile=react-native-${RN_VERSION}-release.aar \
-Dpackaging=aar \
-DgeneratePom=false \
-DpomFile=react-native-${RN_VERSION}.pom || true
-DpomFile=react-native-${RN_VERSION}-fixed.pom || true
popd
# Push JSC
echo "Pushing JSC ${JSC_VERSION} to the Maven repo"
@@ -55,13 +59,17 @@ else
if [[ ! -d ${MVN_REPO}/com/facebook/react/react-native/${RN_VERSION} ]]; then
echo "Pushing React Native ${RN_VERSION} to the Maven repo"
pushd ${THIS_DIR}/../../node_modules/react-native/android/com/facebook/react/react-native/${RN_VERSION}
cat react-native-${RN_VERSION}.pom \
| sed "s#<packaging>pom</packaging>#<packaging>aar</packaging>#" \
| sed "/<optional>/d" \
> react-native-${RN_VERSION}-fixed.pom
mvn \
deploy:deploy-file \
-Durl=${MVN_REPO} \
-Dfile=react-native-${RN_VERSION}-release.aar \
-Dpackaging=aar \
-DgeneratePom=false \
-DpomFile=react-native-${RN_VERSION}.pom
-DpomFile=react-native-${RN_VERSION}-fixed.pom
popd
fi

View File

@@ -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;
}
}

View File

@@ -21,7 +21,7 @@
&-actions {
margin-top: 10px;
a {
button {
cursor: pointer;
text-decoration: none;
font-size: 14px;

View File

@@ -14,14 +14,14 @@ PODS:
- CocoaLumberjack/Core (= 3.7.2)
- CocoaLumberjack/Core (3.7.2)
- DoubleConversion (1.1.6)
- FBLazyVector (0.69.10)
- FBReactNativeSpec (0.69.10):
- FBLazyVector (0.69.11)
- FBReactNativeSpec (0.69.11):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTRequired (= 0.69.10)
- RCTTypeSafety (= 0.69.10)
- React-Core (= 0.69.10)
- React-jsi (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- RCTRequired (= 0.69.11)
- RCTTypeSafety (= 0.69.11)
- React-Core (= 0.69.11)
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- Firebase/Analytics (8.15.0):
- Firebase/Core
- Firebase/Core (8.15.0):
@@ -164,203 +164,203 @@ PODS:
- DoubleConversion
- fmt (~> 6.2.1)
- glog
- RCTRequired (0.69.10)
- RCTTypeSafety (0.69.10):
- FBLazyVector (= 0.69.10)
- RCTRequired (= 0.69.10)
- React-Core (= 0.69.10)
- React (0.69.10):
- React-Core (= 0.69.10)
- React-Core/DevSupport (= 0.69.10)
- React-Core/RCTWebSocket (= 0.69.10)
- React-RCTActionSheet (= 0.69.10)
- React-RCTAnimation (= 0.69.10)
- React-RCTBlob (= 0.69.10)
- React-RCTImage (= 0.69.10)
- React-RCTLinking (= 0.69.10)
- React-RCTNetwork (= 0.69.10)
- React-RCTSettings (= 0.69.10)
- React-RCTText (= 0.69.10)
- React-RCTVibration (= 0.69.10)
- React-bridging (0.69.10):
- RCTRequired (0.69.11)
- RCTTypeSafety (0.69.11):
- FBLazyVector (= 0.69.11)
- RCTRequired (= 0.69.11)
- React-Core (= 0.69.11)
- React (0.69.11):
- React-Core (= 0.69.11)
- React-Core/DevSupport (= 0.69.11)
- React-Core/RCTWebSocket (= 0.69.11)
- React-RCTActionSheet (= 0.69.11)
- React-RCTAnimation (= 0.69.11)
- React-RCTBlob (= 0.69.11)
- React-RCTImage (= 0.69.11)
- React-RCTLinking (= 0.69.11)
- React-RCTNetwork (= 0.69.11)
- React-RCTSettings (= 0.69.11)
- React-RCTText (= 0.69.11)
- React-RCTVibration (= 0.69.11)
- React-bridging (0.69.11):
- RCT-Folly (= 2021.06.28.00-v2)
- React-jsi (= 0.69.10)
- React-callinvoker (0.69.10)
- React-Codegen (0.69.10):
- FBReactNativeSpec (= 0.69.10)
- React-jsi (= 0.69.11)
- React-callinvoker (0.69.11)
- React-Codegen (0.69.11):
- FBReactNativeSpec (= 0.69.11)
- RCT-Folly (= 2021.06.28.00-v2)
- RCTRequired (= 0.69.10)
- RCTTypeSafety (= 0.69.10)
- React-Core (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- React-Core (0.69.10):
- RCTRequired (= 0.69.11)
- RCTTypeSafety (= 0.69.11)
- React-Core (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-Core (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default (= 0.69.10)
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-Core/Default (= 0.69.11)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/CoreModulesHeaders (0.69.10):
- React-Core/CoreModulesHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/Default (0.69.10):
- React-Core/Default (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/DevSupport (0.69.10):
- React-Core/DevSupport (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default (= 0.69.10)
- React-Core/RCTWebSocket (= 0.69.10)
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-jsinspector (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-Core/Default (= 0.69.11)
- React-Core/RCTWebSocket (= 0.69.11)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-jsinspector (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTActionSheetHeaders (0.69.10):
- React-Core/RCTActionSheetHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTAnimationHeaders (0.69.10):
- React-Core/RCTAnimationHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTBlobHeaders (0.69.10):
- React-Core/RCTBlobHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTImageHeaders (0.69.10):
- React-Core/RCTImageHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTLinkingHeaders (0.69.10):
- React-Core/RCTLinkingHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTNetworkHeaders (0.69.10):
- React-Core/RCTNetworkHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTSettingsHeaders (0.69.10):
- React-Core/RCTSettingsHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTTextHeaders (0.69.10):
- React-Core/RCTTextHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTVibrationHeaders (0.69.10):
- React-Core/RCTVibrationHeaders (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-Core/RCTWebSocket (0.69.10):
- React-Core/RCTWebSocket (0.69.11):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default (= 0.69.10)
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsiexecutor (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-Core/Default (= 0.69.11)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- Yoga
- React-CoreModules (0.69.10):
- React-CoreModules (0.69.11):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.69.10)
- React-Codegen (= 0.69.10)
- React-Core/CoreModulesHeaders (= 0.69.10)
- React-jsi (= 0.69.10)
- React-RCTImage (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- React-cxxreact (0.69.10):
- RCTTypeSafety (= 0.69.11)
- React-Codegen (= 0.69.11)
- React-Core/CoreModulesHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- React-RCTImage (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-cxxreact (0.69.11):
- boost (= 1.76.0)
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-callinvoker (= 0.69.10)
- React-jsi (= 0.69.10)
- React-jsinspector (= 0.69.10)
- React-logger (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-runtimeexecutor (= 0.69.10)
- React-jsi (0.69.10):
- React-callinvoker (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsinspector (= 0.69.11)
- React-logger (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-runtimeexecutor (= 0.69.11)
- React-jsi (0.69.11):
- boost (= 1.76.0)
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-jsi/Default (= 0.69.10)
- React-jsi/Default (0.69.10):
- React-jsi/Default (= 0.69.11)
- React-jsi/Default (0.69.11):
- boost (= 1.76.0)
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-jsiexecutor (0.69.10):
- React-jsiexecutor (0.69.11):
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-jsinspector (0.69.10)
- React-logger (0.69.10):
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-jsinspector (0.69.11)
- React-logger (0.69.11):
- glog
- react-native-background-timer (2.4.1):
- React-Core
@@ -395,72 +395,72 @@ PODS:
- React-Core
- react-native-webview (11.15.1):
- React-Core
- React-perflogger (0.69.10)
- React-RCTActionSheet (0.69.10):
- React-Core/RCTActionSheetHeaders (= 0.69.10)
- React-RCTAnimation (0.69.10):
- React-perflogger (0.69.11)
- React-RCTActionSheet (0.69.11):
- React-Core/RCTActionSheetHeaders (= 0.69.11)
- React-RCTAnimation (0.69.11):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.69.10)
- React-Codegen (= 0.69.10)
- React-Core/RCTAnimationHeaders (= 0.69.10)
- React-jsi (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- React-RCTBlob (0.69.10):
- RCTTypeSafety (= 0.69.11)
- React-Codegen (= 0.69.11)
- React-Core/RCTAnimationHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-RCTBlob (0.69.11):
- RCT-Folly (= 2021.06.28.00-v2)
- React-Codegen (= 0.69.10)
- React-Core/RCTBlobHeaders (= 0.69.10)
- React-Core/RCTWebSocket (= 0.69.10)
- React-jsi (= 0.69.10)
- React-RCTNetwork (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- React-RCTImage (0.69.10):
- React-Codegen (= 0.69.11)
- React-Core/RCTBlobHeaders (= 0.69.11)
- React-Core/RCTWebSocket (= 0.69.11)
- React-jsi (= 0.69.11)
- React-RCTNetwork (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-RCTImage (0.69.11):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.69.10)
- React-Codegen (= 0.69.10)
- React-Core/RCTImageHeaders (= 0.69.10)
- React-jsi (= 0.69.10)
- React-RCTNetwork (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- React-RCTLinking (0.69.10):
- React-Codegen (= 0.69.10)
- React-Core/RCTLinkingHeaders (= 0.69.10)
- React-jsi (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- React-RCTNetwork (0.69.10):
- RCTTypeSafety (= 0.69.11)
- React-Codegen (= 0.69.11)
- React-Core/RCTImageHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- React-RCTNetwork (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-RCTLinking (0.69.11):
- React-Codegen (= 0.69.11)
- React-Core/RCTLinkingHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-RCTNetwork (0.69.11):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.69.10)
- React-Codegen (= 0.69.10)
- React-Core/RCTNetworkHeaders (= 0.69.10)
- React-jsi (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- React-RCTSettings (0.69.10):
- RCTTypeSafety (= 0.69.11)
- React-Codegen (= 0.69.11)
- React-Core/RCTNetworkHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-RCTSettings (0.69.11):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.69.10)
- React-Codegen (= 0.69.10)
- React-Core/RCTSettingsHeaders (= 0.69.10)
- React-jsi (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- React-RCTText (0.69.10):
- React-Core/RCTTextHeaders (= 0.69.10)
- React-RCTVibration (0.69.10):
- RCTTypeSafety (= 0.69.11)
- React-Codegen (= 0.69.11)
- React-Core/RCTSettingsHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-RCTText (0.69.11):
- React-Core/RCTTextHeaders (= 0.69.11)
- React-RCTVibration (0.69.11):
- RCT-Folly (= 2021.06.28.00-v2)
- React-Codegen (= 0.69.10)
- React-Core/RCTVibrationHeaders (= 0.69.10)
- React-jsi (= 0.69.10)
- ReactCommon/turbomodule/core (= 0.69.10)
- React-runtimeexecutor (0.69.10):
- React-jsi (= 0.69.10)
- ReactCommon/turbomodule/core (0.69.10):
- React-Codegen (= 0.69.11)
- React-Core/RCTVibrationHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-runtimeexecutor (0.69.11):
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (0.69.11):
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-bridging (= 0.69.10)
- React-callinvoker (= 0.69.10)
- React-Core (= 0.69.10)
- React-cxxreact (= 0.69.10)
- React-jsi (= 0.69.10)
- React-logger (= 0.69.10)
- React-perflogger (= 0.69.10)
- React-bridging (= 0.69.11)
- React-callinvoker (= 0.69.11)
- React-Core (= 0.69.11)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-logger (= 0.69.11)
- React-perflogger (= 0.69.11)
- RNCalendarEvents (2.2.0):
- React
- RNCAsyncStorage (1.17.3):
@@ -706,8 +706,8 @@ SPEC CHECKSUMS:
boost: a7c83b31436843459a1961bfd74b96033dc77234
CocoaLumberjack: b7e05132ff94f6ae4dfa9d5bce9141893a21d9da
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: a8af91c2b5a0029d12ff6b32e428863d63c48991
FBReactNativeSpec: ec5e878f6452a3de5430e0b2324a4d4ae6ac63f6
FBLazyVector: 5c0975e66853436589eae7542f4b956c7e2ef465
FBReactNativeSpec: bb062293e84c33200005312d1807d8cb94a0d66a
Firebase: 5f8193dff4b5b7c5d5ef72ae54bb76c08e2b841d
FirebaseAnalytics: 7761cbadb00a717d8d0939363eb46041526474fa
FirebaseCore: 5743c5785c074a794d35f2fff7ecc254a91e08b1
@@ -732,19 +732,19 @@ SPEC CHECKSUMS:
PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef
PromisesSwift: cf9eb58666a43bbe007302226e510b16c1e10959
RCT-Folly: b9d9fe1fc70114b751c076104e52f3b1b5e5a95a
RCTRequired: 3581db0757e7ff9be10718a56b3d79b6a6bd3bdf
RCTTypeSafety: ce13e630c48340401ebfb28710959913f74b8b36
React: cca8f2b7cce018f79847ca79847fa367b206e8a1
React-bridging: b893643f09d3964afba6c347e00dd86cf10691e5
React-callinvoker: 9ac7cba30428eddf7a06d1253f8e7561b5c97334
React-Codegen: 65ff9fbddf8a17a6d4f495f71d365288f934a93a
React-Core: 550b694774bc778b5c7bf7608fc12a484e01ec05
React-CoreModules: c332d5b416cb3ccf972e7af79d496498a700e073
React-cxxreact: c5c4106bfd2d0cee80b848e33b7ff4e35a721b16
React-jsi: 6ff3fb9b9764a499c959e0096c0d384fa2b4beef
React-jsiexecutor: 388f1c99404c848141d7ea162f61233d04829ede
React-jsinspector: a4463b3411b8b9b37153255ef694a84c77ba3c7f
React-logger: 2a0497622cbabc47fb769d97620952df14c1f814
RCTRequired: 8e9a57dddc8f8e9e816c67c2d2537271a997137a
RCTTypeSafety: 2b19e268e2036a2c2f6db6deb1ac03e28b1d607a
React: f9478e6390f177ee6b67b87a3c6afea42b39523e
React-bridging: d405ecd3ff80e1d0a4059a11063eaa9ed7a00c58
React-callinvoker: c8ffa61f3f06f486ba6647769fc98f19e25d165a
React-Codegen: 73acfdac1495b91ad5efdd3ab005568263c5def6
React-Core: 7b7c75af4b73fe0ed4e5c3cdb7d79979e81148dc
React-CoreModules: cd6e7efb38162884f08c7afa16fffaf15ff28ae4
React-cxxreact: 51157cc600c9f436a7e623913a03b775305ef86c
React-jsi: 3eeb345c4828d7b132fd38064a305f31b46d4ec3
React-jsiexecutor: 5813455a4a908fb7284aa13307a9e0386e93b0bb
React-jsinspector: 9ca5bf73ed0a195397e45fdbcd507cf7d503c428
React-logger: 700340e325f21ba2a2d6413a61ef14268c7360aa
react-native-background-timer: 17ea5e06803401a379ebf1f20505b793ac44d0fe
react-native-get-random-values: 30b3f74ca34e30e2e480de48e4add2706a40ac8f
react-native-keep-awake: afad8a51dfef9fe9655a6344771be32c8596d774
@@ -757,18 +757,18 @@ SPEC CHECKSUMS:
react-native-video: bb6f12a7198db53b261fefb5d609dc77417acc8b
react-native-webrtc: 2702afae1e59882b423e6077768ca0d1e6fc42ed
react-native-webview: ea4899a1056c782afa96dd082179a66cbebf5504
React-perflogger: bc57c4a953c1ec913b0d984cf4f2b9842a12bde0
React-RCTActionSheet: 3efa3546119a1050f6c34a461b386dd9e36eaf0b
React-RCTAnimation: e58fb9f1adf7b38af329881ea2740f43ffeea854
React-RCTBlob: d2238645553c3ec787324268c0676148d86e6cc4
React-RCTImage: e6d7c9ab978cae99364fcc96b9238fc7740a13da
React-RCTLinking: 329e88ce217dad464ef34b5d0c40b3ceaac6c9ec
React-RCTNetwork: c8967f2382aac31761ddb750fee53fa34cf7a4ee
React-RCTSettings: 8a825b4b5ea58f6713a7c97eea6cc82e9895188b
React-RCTText: ffcaac5c66bc065f2ccf79b6fe34585adb9e589b
React-RCTVibration: 0039c986626b78242401931bb23c803935fae9d1
React-runtimeexecutor: 5ebf1ddaa706bf2986123f22d2cad905443c2c5f
ReactCommon: 65754b8932ea80272714988268bbfb9f303264a5
React-perflogger: fdee2a0c512167ae4c19c4e230ccf6aa66a6aff0
React-RCTActionSheet: 1cf5fef4e372f1c877969710a51bea4bb25e78fe
React-RCTAnimation: 73816e3acd1f5e3f00166fc7eedb34f6b112f734
React-RCTBlob: 6976c838fb14a1daf75d7c8bb23bae9cbbf726bb
React-RCTImage: ab8a7498f215117f32271698591e4bd932dcf812
React-RCTLinking: e8e78aed2744ab9946cc8ba5716b4938c2efb1e0
React-RCTNetwork: 796f5aed4d932655d292bdc6b40f9502dcdb9542
React-RCTSettings: 7e1cd2a384b45c90caf67464572abe3833b9da3b
React-RCTText: fd6162890828f0761e03c59058fa23c3a21b2e10
React-RCTVibration: 302cfd5cc33669d7abdb7ec6790123baba66e62e
React-runtimeexecutor: 59407514818b2afbb1d7507e4e1ac834d24b0fbd
ReactCommon: b8487da74723562d7368dab27135fd182f00a91c
RNCalendarEvents: 7e65eb4a94f53c1744d1e275f7fafcfaa619f7a3
RNCAsyncStorage: 005c0e2f09575360f142d0d1f1f15e4ec575b1af
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
@@ -780,8 +780,8 @@ SPEC CHECKSUMS:
RNSound: 27e8268bdb0a1f191f219a33267f7e0445e8d62f
RNSVG: f3b60aeeaa81960e2e0536c3a9eef50b667ef3a9
RNWatch: dae6c858a2051dbdcfb00b9a86cf4d90400263b4
Yoga: d24d6184b6b85f742536bd93bd07d69d7b9bb4c1
Yoga: 7f5ad94937ba3fc58c151ad1b7bbada2c275b28e
PODFILE CHECKSUM: e3579df5272b8b697c9fdc0e55aa0845b189c4dd
COCOAPODS: 1.11.3
COCOAPODS: 1.12.1

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>99.0.0</string>
<string>8.2.2</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>

View File

@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>99.0.0</string>
<string>8.2.2</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>

View File

@@ -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": {

View File

@@ -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.",

View File

@@ -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;

199
package-lock.json generated
View File

@@ -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,10 +71,10 @@
"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",
"react-native": "0.69.11",
"react-native-background-timer": "2.4.1",
"react-native-calendar-events": "2.2.0",
"react-native-callstats": "3.73.7",
@@ -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",
@@ -15620,9 +15665,10 @@
}
},
"node_modules/react-native": {
"version": "0.69.10",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.69.10.tgz",
"integrity": "sha512-zBsJmFsjyx9zC0JDRH6F6hib3BWbIu65EYPsJBQMDHMSGJ9fL7C6ZV49O7Abockn/dbCHhIWpiSyCuda/bhV8g==",
"version": "0.69.11",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.69.11.tgz",
"integrity": "sha512-lYSzyDicrE5FLag+Vaq1XmPscQFKFqiUCsDMNkgd+wW9139u3hDEnbEWnXfM/kgF3vrKQUfyLMwfkuLV8EQfug==",
"deprecated": "Issues and pull requests filed against this version are not supported. See the React Native release support policy to learn more: https://github.com/reactwg/react-native-releases#releases-support-policy",
"dependencies": {
"@jest/create-cache-key-function": "^27.0.1",
"@react-native-community/cli": "^8.0.4",
@@ -16107,6 +16153,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 +16210,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 +22010,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 +24851,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 +27692,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 +31413,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",
@@ -31316,9 +31456,9 @@
}
},
"react-native": {
"version": "0.69.10",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.69.10.tgz",
"integrity": "sha512-zBsJmFsjyx9zC0JDRH6F6hib3BWbIu65EYPsJBQMDHMSGJ9fL7C6ZV49O7Abockn/dbCHhIWpiSyCuda/bhV8g==",
"version": "0.69.11",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.69.11.tgz",
"integrity": "sha512-lYSzyDicrE5FLag+Vaq1XmPscQFKFqiUCsDMNkgd+wW9139u3hDEnbEWnXfM/kgF3vrKQUfyLMwfkuLV8EQfug==",
"requires": {
"@jest/create-cache-key-function": "^27.0.1",
"@react-native-community/cli": "^8.0.4",
@@ -31658,6 +31798,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 +31828,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",

View File

@@ -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,10 +76,10 @@
"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",
"react-native": "0.69.11",
"react-native-background-timer": "2.4.1",
"react-native-calendar-events": "2.2.0",
"react-native-callstats": "3.73.7",

View File

@@ -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;

View File

@@ -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 }

View File

@@ -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>
)}
</>
);
}

View File

@@ -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;
}

View File

@@ -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).

View File

@@ -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 }

View File

@@ -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>
);

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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 });
}
}
/**

View File

@@ -103,6 +103,7 @@ const BaseIndicator = ({
className = { className }
id = { id }>
<Icon
alt = { t(tooltipKey) }
className = { iconClassName }
color = { iconColor }
id = { iconId }

View File

@@ -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 }

View File

@@ -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>

View File

@@ -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 }

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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 }

View File

@@ -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>

View File

@@ -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 } />

View File

@@ -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>
)}

View File

@@ -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 }

View File

@@ -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>
)}

View File

@@ -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>
);
};

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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 }

View File

@@ -32,6 +32,7 @@ const RaisedHandsCountLabel = () => {
content = { t('raisedHandsLabel') }
position = { 'bottom' }>
<Label
accessibilityText = { t('raisedHandsLabel') }
className = { styles.label }
icon = { IconRaiseHand }
iconColor = { theme.palette.icon04 }

View File

@@ -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() }

View File

@@ -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>
);
};

View File

@@ -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 }

View File

@@ -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 }

View File

@@ -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 }

View File

@@ -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() } />

View File

@@ -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 }

View File

@@ -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'>

View File

@@ -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') }

View File

@@ -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 } />

View File

@@ -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>
);
}

View File

@@ -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) }

View File

@@ -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'

View File

@@ -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)
};
}

View File

@@ -127,6 +127,7 @@ function MeetingParticipants({
<Input
className = { styles.search }
clearable = { true }
id = 'participants-search-input'
onChange = { setSearchString }
placeholder = { t('participantsPane.search') }
value = { searchString } />

View File

@@ -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) }

View File

@@ -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') }

View File

@@ -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));
}

View File

@@ -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 }

View File

@@ -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';

View File

@@ -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)));

View File

@@ -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 }

View File

@@ -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';

View File

@@ -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>

View File

@@ -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 }

View File

@@ -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

View File

@@ -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 }

View File

@@ -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>
);
}

View File

@@ -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

View File

@@ -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 }

View File

@@ -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));
}
}

View File

@@ -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>

View File

@@ -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 }

View File

@@ -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 }

View File

@@ -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') } /> }

View File

@@ -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>
);
}

View File

@@ -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 (

View File

@@ -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);
}
});

View File

@@ -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

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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 }

View File

@@ -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 }

View File

@@ -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>

View File

@@ -122,7 +122,6 @@ function UploadImageButton({
return (
<>
{showLabel && <label
aria-label = { t('virtualBackground.uploadImage') }
className = { classes.label }
htmlFor = 'file-upload'
onKeyPress = { uploadImageKeyPress }

View File

@@ -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 }>