Browse Source

feat: Make Jitsi WCAG 2.1 compliant (#8921)

* Make Jitsi WCAG 2.1 compliant

* Fixed password form keypress handling

* Added keypress handler to name form

* Removed unneccessary dom query

* Fixed mouse hove style

* Removed obsolete css rules

* accessibilty background feature

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

* fix error

* add german translation

* Fixed merge issue

* Add id prop back to device selection

* Fixed lockfile

Co-authored-by: AHMAD KADRI <52747422+ahmadkadri@users.noreply.github.com>
j8
Steffen Kolmer 3 years ago
parent
commit
e9675453e1
No account linked to committer's email address
100 changed files with 1878 additions and 613 deletions
  1. 12
    0
      css/_atlaskit_overrides.scss
  2. 22
    4
      css/_audio-preview.scss
  3. 2
    1
      css/_aui_reset.scss
  4. 1
    5
      css/_avatar.scss
  5. 40
    12
      css/_chat.scss
  6. 4
    1
      css/_e2ee.scss
  7. 3
    2
      css/_meetings_list.scss
  8. 13
    2
      css/_prejoin.scss
  9. 9
    2
      css/_premeeting-screens.scss
  10. 1
    0
      css/_recording.scss
  11. 3
    0
      css/_toolbars.scss
  12. 4
    0
      css/_video-preview.scss
  13. 1
    1
      css/_videolayout_default.scss
  14. 1
    1
      css/buttons/copy.scss
  15. 2
    2
      css/components/_input-slider.scss
  16. 0
    1
      css/filmstrip/_filmstrip_toolbar.scss
  17. 1
    1
      css/filmstrip/_tile_view.scss
  18. 1
    1
      css/filmstrip/_vertical_filmstrip_overrides.scss
  19. 2
    2
      css/modals/device-selection/_device-selection.scss
  20. 4
    0
      css/modals/feedback/_feedback.scss
  21. 9
    3
      css/modals/invite/_info.scss
  22. 2
    1
      css/modals/invite/_invite_more.scss
  23. 18
    1
      css/modals/settings/_settings.scss
  24. 1
    0
      css/modals/video-quality/_video-quality.scss
  25. 25
    98
      css/modals/virtual-background/_virtual-background.scss
  26. 1
    1
      css/themes/_light.scss
  27. 2
    2
      index.html
  28. 72
    19
      lang/main-de.json
  29. 60
    17
      lang/main.json
  30. 55
    18
      modules/keyboardshortcut/keyboardshortcut.js
  31. 10
    1
      modules/translation/translation.js
  32. 42
    5
      package-lock.json
  33. 1
    1
      package.json
  34. 13
    3
      react/features/base/avatar/components/web/StatelessAvatar.js
  35. 31
    6
      react/features/base/buttons/CopyButton.js
  36. 40
    3
      react/features/base/dialog/components/web/ModalHeader.js
  37. 4
    4
      react/features/base/dialog/components/web/StatelessDialog.js
  38. 1
    0
      react/features/base/environment/utils.js
  39. 103
    8
      react/features/base/icons/components/Icon.js
  40. 47
    3
      react/features/base/popover/components/Popover.web.js
  41. 73
    13
      react/features/base/premeeting/components/web/ActionButton.js
  42. 33
    7
      react/features/base/premeeting/components/web/ConnectionStatus.js
  43. 5
    166
      react/features/base/premeeting/components/web/CopyMeetingUrl.js
  44. 5
    1
      react/features/base/premeeting/components/web/InputField.js
  45. 2
    2
      react/features/base/premeeting/components/web/PreMeetingScreen.js
  46. 15
    3
      react/features/base/premeeting/components/web/ToggleButton.js
  47. 72
    8
      react/features/base/react/components/web/MeetingsList.js
  48. 5
    1
      react/features/base/react/components/web/Watermarks.js
  49. 6
    2
      react/features/base/toolbox/components/AbstractButton.js
  50. 4
    0
      react/features/base/toolbox/components/BetaTag.js
  51. 7
    15
      react/features/base/toolbox/components/ToolboxItem.web.js
  52. 34
    1
      react/features/base/toolbox/components/web/OverflowMenuItem.js
  53. 45
    1
      react/features/base/toolbox/components/web/ToolboxButtonWithIcon.js
  54. 7
    15
      react/features/base/toolbox/components/web/ToolboxItem.js
  55. 20
    1
      react/features/calendar-sync/components/AddMeetingUrlButton.web.js
  56. 23
    2
      react/features/calendar-sync/components/CalendarList.web.js
  57. 20
    1
      react/features/calendar-sync/components/JoinButton.web.js
  58. 19
    4
      react/features/calendar-sync/components/MicrosoftSignInButton.web.js
  59. 37
    2
      react/features/chat/components/web/Chat.js
  60. 25
    5
      react/features/chat/components/web/ChatDialogHeader.js
  61. 86
    10
      react/features/chat/components/web/ChatInput.js
  62. 13
    3
      react/features/chat/components/web/ChatMessage.js
  63. 22
    1
      react/features/chat/components/web/DisplayNameForm.js
  64. 4
    1
      react/features/chat/components/web/MessageContainer.js
  65. 38
    2
      react/features/chat/components/web/MessageRecipient.js
  66. 84
    28
      react/features/chat/components/web/SmileysPanel.js
  67. 59
    8
      react/features/chrome-extension-banner/components/ChromeExtensionBanner.web.js
  68. 15
    3
      react/features/conference/components/web/InviteMore.js
  69. 6
    2
      react/features/connection-stats/components/ConnectionStatsTable.js
  70. 1
    0
      react/features/deep-linking/components/DeepLinkingDesktopPage.web.js
  71. 2
    0
      react/features/deep-linking/components/DeepLinkingMobilePage.web.js
  72. 11
    2
      react/features/desktop-picker/components/DesktopSourcePreview.js
  73. 23
    1
      react/features/device-selection/components/AudioOutputPreview.js
  74. 10
    3
      react/features/device-selection/components/DeviceSelection.js
  75. 22
    13
      react/features/device-selection/components/DeviceSelector.web.js
  76. 27
    2
      react/features/e2ee/components/E2EESection.js
  77. 2
    0
      react/features/embed-meeting/components/EmbedMeetingDialog.js
  78. 19
    1
      react/features/embed-meeting/components/EmbedMeetingTrigger.js
  79. 18
    5
      react/features/feedback/components/FeedbackDialog.web.js
  80. 32
    5
      react/features/filmstrip/components/web/Filmstrip.js
  81. 6
    6
      react/features/filmstrip/components/web/Thumbnail.js
  82. 1
    0
      react/features/google-api/components/GoogleSignInButton.web.js
  83. 3
    1
      react/features/invite/components/add-people-dialog/web/CopyMeetingLinkSection.js
  84. 22
    1
      react/features/invite/components/add-people-dialog/web/DialInNumber.js
  85. 40
    2
      react/features/invite/components/add-people-dialog/web/InviteByEmailSection.js
  86. 46
    4
      react/features/invite/components/add-people-dialog/web/InviteContactsForm.js
  87. 3
    1
      react/features/keyboard-shortcuts/components/KeyboardShortcutsDialog.web.js
  88. 3
    1
      react/features/large-video/components/LargeVideo.web.js
  89. 3
    1
      react/features/lobby/components/web/LobbySection.js
  90. 6
    2
      react/features/local-recording/components/LocalRecordingInfoDialog.js
  91. 2
    2
      react/features/notifications/components/web/Notification.js
  92. 9
    3
      react/features/notifications/components/web/NotificationsContainer.js
  93. 11
    3
      react/features/overlay/components/web/PageReloadOverlay.js
  94. 13
    4
      react/features/overlay/components/web/UserMediaPermissionsOverlay.js
  95. 1
    1
      react/features/participants-pane/components/InviteButton.js
  96. 5
    1
      react/features/participants-pane/components/MeetingParticipantItem.js
  97. 12
    1
      react/features/participants-pane/components/ParticipantsPane.js
  98. 75
    3
      react/features/prejoin/components/Prejoin.js
  99. 3
    4
      react/features/prejoin/components/country-picker/CountryPicker.js
  100. 0
    0
      react/features/prejoin/components/country-picker/CountrySelector.js

+ 12
- 0
css/_atlaskit_overrides.scss View File

@@ -32,6 +32,18 @@
32 32
     .dropdown-menu div[style*="transform"] {
33 33
         outline: 1px solid #455166;
34 34
     }
35
+    .dropdown-menu button:not(:active):not(:hover) > span {
36
+        color: #B8C7E0;
37
+    }
38
+
39
+    /**
40
+    * Override @atlaskit/tab styling when in a modal because the
41
+    * tab text color clash with the modal backgrounds.
42
+    */
43
+    div[role="tablist"] > div:not([data-selected]):not(:hover),
44
+    label > div > span {
45
+        color: #B8C7E0 !important;
46
+    }
35 47
 }
36 48
 
37 49
 /**

+ 22
- 4
css/_audio-preview.scss View File

@@ -9,6 +9,11 @@
9 9
         max-height: 456px;
10 10
         overflow: auto;
11 11
         width: 300px;
12
+        &-ul {
13
+            margin:0;
14
+            padding:0;
15
+            list-style-type: none;
16
+        }
12 17
     }
13 18
 
14 19
     &-header {
@@ -64,7 +69,13 @@
64 69
     &-speaker {
65 70
         position: relative;
66 71
 
67
-        &:hover {
72
+        &-ul {
73
+            margin:0;
74
+            padding:0;
75
+            list-style-type: none;
76
+        }
77
+
78
+        &:hover, &:focus-within, &:focus {
68 79
             .audio-preview-entry {
69 80
                 background: #36383C;
70 81
                 margin-left: 0;
@@ -81,7 +92,7 @@
81 92
             }
82 93
 
83 94
             .audio-preview-entry-text {
84
-                max-width: 196px;
95
+                max-width: 178px;
85 96
             }
86 97
         }
87 98
 
@@ -90,7 +101,7 @@
90 101
         }
91 102
 
92 103
         .audio-preview-entry-text {
93
-            max-width: 256px;
104
+            max-width: 238px;
94 105
         }
95 106
     }
96 107
 
@@ -150,8 +161,9 @@
150 161
         color: #1C2025;
151 162
         cursor: pointer;
152 163
         font-weight: 600;
164
+        font-size: 0.8rem;
153 165
         line-height: 24px;
154
-        padding: 2px 16px;
166
+        padding: 2px 8px;
155 167
         position: absolute;
156 168
         right: 16px;
157 169
         top: 5px;
@@ -162,4 +174,10 @@
162 174
         right: 16px;
163 175
         top: 14px;
164 176
     }
177
+
178
+    // Override @atlaskit/InlineDialog container which is made with styled components
179
+    & > div:nth-child(2) {
180
+        outline: none;
181
+        padding: 0;
182
+    }
165 183
 }

+ 2
- 1
css/_aui_reset.scss View File

@@ -219,8 +219,9 @@ abbr {
219 219
 }
220 220
 
221 221
 a {
222
-  color: #3572b0;
222
+  color: #44A5FF;
223 223
   text-decoration: none;
224
+  font-weight: bold;
224 225
 }
225 226
 a:focus,
226 227
 a:hover,

+ 1
- 5
css/_avatar.scss View File

@@ -1,7 +1,7 @@
1 1
 .avatar {
2 2
     background-color: #AAA;
3 3
     border-radius: 50%;
4
-    color: rgba(255, 255, 255, 0.6);
4
+    color: rgba(255, 255, 255, 1);
5 5
     font-weight: 100;
6 6
     object-fit: cover;
7 7
 
@@ -25,10 +25,6 @@
25 25
     width: 100%;
26 26
 }
27 27
 
28
-.defaultAvatar {
29
-    opacity: 0.6
30
-}
31
-
32 28
 .avatar-badge {
33 29
     position: relative;
34 30
 

+ 40
- 12
css/_chat.scss View File

@@ -99,18 +99,19 @@
99 99
     div {
100 100
         svg {
101 101
             cursor: pointer;
102
-            fill: white
102
+            fill: white;
103 103
         }
104 104
     }
105 105
 }
106 106
 
107
+
107 108
 .chat-header {
108 109
     height: 70px;
109 110
     position: relative;
110 111
     width: 100%;
111 112
     z-index: 1;
112 113
     display: flex;
113
-    justify-content: flex-end;
114
+    justify-content: space-between;
114 115
     padding: 16px;
115 116
     align-items: center;
116 117
     box-sizing: border-box;
@@ -132,6 +133,7 @@
132 133
             .send-button {
133 134
                 background: #1B67EC;
134 135
                 cursor: pointer;
136
+                margin-left: 0.3rem;
135 137
 
136 138
                 @media (hover: hover) and (pointer: fine) {
137 139
                     &:hover {
@@ -188,8 +190,9 @@
188 190
     display: flex;
189 191
     align-items: center;
190 192
     justify-content: center;
191
-    height: 40px;
192
-    width: 40px;
193
+    height: 38px;
194
+    width: 38px;
195
+    margin: 2px;
193 196
     border-radius: 3px;
194 197
 }
195 198
 
@@ -226,6 +229,11 @@
226 229
     border: 0px none;
227 230
     box-shadow: none;
228 231
 }
232
+#usermsg:focus,
233
+#usermsg:active {
234
+    border-bottom: 1px solid white;
235
+    padding-bottom: 8px;
236
+ }
229 237
 
230 238
 #nickname {
231 239
     text-align: center;
@@ -234,6 +242,16 @@
234 242
     margin: auto 0;
235 243
     padding: 0 16px;
236 244
 
245
+    #nickname-title {
246
+        margin-bottom: 5px;
247
+        display: block;
248
+    }
249
+
250
+    label[for="nickinput"] {
251
+        > div > span {
252
+            color: #B8C7E0;
253
+        }
254
+    }
237 255
     input {
238 256
         height: 40px;
239 257
     }
@@ -254,7 +272,7 @@
254 272
         cursor: pointer;
255 273
 
256 274
         &.disabled {
257
-            color: #757575;
275
+            color: #AFB6BC;
258 276
             background: #11336E;
259 277
             pointer-events: none;
260 278
         }
@@ -301,6 +319,19 @@
301 319
     }
302 320
 }
303 321
 
322
+.sr-only {
323
+    border: 0 !important;
324
+    clip: rect(1px, 1px, 1px, 1px) !important;
325
+    clip-path: inset(50%) !important;
326
+    height: 1px !important;
327
+    margin: -1px !important;
328
+    overflow: hidden !important;
329
+    padding: 0 !important;
330
+    position: absolute !important;
331
+    width: 1px !important;
332
+    white-space: nowrap !important;
333
+}
334
+
304 335
 .chatmessage {
305 336
     background-color: $chatRemoteMessageBackgroundColor;
306 337
     border-radius: 0px 6px 6px 6px;
@@ -350,10 +381,6 @@
350 381
     color: #757575;
351 382
 }
352 383
 
353
-.smiley {
354
-    font-size: 14pt;
355
-}
356
-
357 384
 #smileys {
358 385
     font-size: 20pt;
359 386
     margin: auto;
@@ -382,7 +409,7 @@
382 409
     box-sizing: border-box;
383 410
     background-color: rgba(0, 0, 0, .6) !important;
384 411
     height: auto;
385
-    max-height: 0;
412
+    display: none;
386 413
     overflow: hidden;
387 414
     position: absolute;
388 415
     width: calc(#{$sidebarWidth} - 32px);
@@ -398,6 +425,7 @@
398 425
     transition: max-height 0.3s;
399 426
 
400 427
     &.show-smileys {
428
+        display: flex;
401 429
         max-height: 500%;
402 430
     }
403 431
 
@@ -413,7 +441,7 @@
413 441
 
414 442
 .smileyContainer {
415 443
     width: 40px;
416
-    height: 36px;
444
+    height: 40px;
417 445
     display: inline-block;
418 446
     text-align: center;
419 447
 }
@@ -509,7 +537,7 @@
509 537
 
510 538
     &-header {
511 539
         display: flex;
512
-        justify-content: flex-end;
540
+        justify-content: space-between;
513 541
         align-items: center;
514 542
         margin: 16px;
515 543
         width: calc(100% - 32px);

+ 4
- 1
css/_e2ee.scss View File

@@ -8,7 +8,10 @@
8 8
 
9 9
         .read-more {
10 10
             cursor: pointer;
11
-            opacity: .7;
11
+            opacity: .9;
12
+            color: #fff;
13
+            font-size: 0.8rem;
14
+            font-weight: bold;
12 15
         }
13 16
     }
14 17
 

+ 3
- 2
css/_meetings_list.scss View File

@@ -125,7 +125,8 @@
125 125
             cursor: pointer;
126 126
         }
127 127
 
128
-        &.with-click-handler:hover {
128
+        &.with-click-handler:hover,
129
+        &.with-click-handler:focus {
129 130
             background-color: #c7ddff;
130 131
         }
131 132
 
@@ -158,7 +159,7 @@
158 159
         }
159 160
     }
160 161
 
161
-    .item:hover {
162
+    .item:hover, .item:focus, .item:focus-within {
162 163
         .delete-meeting {
163 164
             display: block;
164 165
         }

+ 13
- 2
css/_prejoin.scss View File

@@ -3,6 +3,15 @@
3 3
     &-input-area {
4 4
         margin: 0 auto;
5 5
         text-align: center;
6
+
7
+       &-label {
8
+           display: block;
9
+           margin-bottom: 5px;
10
+           color: #ffffff;
11
+           font-weight: 300;
12
+           font-size: 15px;
13
+           line-height: 24px;
14
+       }
6 15
     }
7 16
 
8 17
     &-title {
@@ -74,10 +83,10 @@
74 83
         z-index: 1;
75 84
 
76 85
         &--warning {
77
-            background: rgba(241, 173, 51, 0.7)
86
+            background: rgba(241, 173, 51, 1);
78 87
         }
79 88
         &--ok {
80
-            background: rgba(49, 183, 106, 0.7);
89
+            background: rgba(49, 183, 106, 1);
81 90
         }
82 91
     }
83 92
 
@@ -92,6 +101,8 @@
92 101
 
93 102
     &-error-desc {
94 103
         margin-right: 4px;
104
+        color: #fff;
105
+        font-weight: bold;
95 106
     }
96 107
 
97 108
     .settings-button-container {

+ 9
- 2
css/_premeeting-screens.scss View File

@@ -113,7 +113,7 @@
113 113
             cursor: pointer;
114 114
             color: #fff;
115 115
             display: flex;
116
-            flex-direction: row;
116
+            flex-direction: column;
117 117
             font-size: 15px;
118 118
             font-weight: 300;
119 119
             justify-content: center;
@@ -139,6 +139,9 @@
139 139
                     margin-left: 10px;
140 140
                 }
141 141
             }
142
+            .copy-button{
143
+                width: 298px;
144
+             }
142 145
 
143 146
             .copy-meeting-text {
144 147
                 width: 266px;
@@ -177,7 +180,7 @@
177 180
             }
178 181
 
179 182
             &.focused {
180
-                box-shadow: 0px 0px 4px 3px #0376DA;
183
+                box-shadow: 0px 0px 1px 1.5px black, 0px 0px 1.3px 4px white;
181 184
             }
182 185
         }
183 186
     }
@@ -249,6 +252,10 @@
249 252
         fill: transparent;
250 253
     }
251 254
 
255
+    label {
256
+        cursor: pointer;
257
+    }
258
+
252 259
     &:hover {
253 260
         background: rgba(255, 255, 255, 0.1);
254 261
 

+ 1
- 0
css/_recording.scss View File

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

+ 3
- 0
css/_toolbars.scss View File

@@ -54,6 +54,7 @@
54 54
     margin-bottom: 16px;
55 55
     position: relative;
56 56
     z-index: $toolbarZ;
57
+    pointer-events: none;
57 58
 
58 59
     .button-group-center,
59 60
     .button-group-left,
@@ -103,6 +104,7 @@
103 104
     flex-direction: column;
104 105
     margin: 0 auto;
105 106
     max-width: 100%;
107
+    pointer-events: all;
106 108
 }
107 109
 
108 110
 .toolbox-content-items {
@@ -112,6 +114,7 @@
112 114
     margin: 0 auto;
113 115
     padding: 6px;
114 116
     text-align: center;
117
+    pointer-events: all;
115 118
 
116 119
     >div {
117 120
         margin-left: 8px;

+ 4
- 0
css/_video-preview.scss View File

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

+ 1
- 1
css/_videolayout_default.scss View File

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

+ 1
- 1
css/buttons/copy.scss View File

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

+ 2
- 2
css/components/_input-slider.scss View File

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

+ 0
- 1
css/filmstrip/_filmstrip_toolbar.scss View File

@@ -16,7 +16,6 @@
16 16
         padding: 0;
17 17
         margin: 0;
18 18
         border: none;
19
-        outline: none;
20 19
 
21 20
         -webkit-appearance: none;
22 21
 

+ 1
- 1
css/filmstrip/_tile_view.scss View File

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

+ 1
- 1
css/filmstrip/_vertical_filmstrip_overrides.scss View File

@@ -81,7 +81,7 @@
81 81
 
82 82
     .local-video-menu-trigger,
83 83
     .remote-video-menu-trigger {
84
-        margin-bottom: 7px;
84
+        margin-bottom: 3px;
85 85
         margin-left: $remoteVideoMenuIconMargin;
86 86
     }
87 87
 }

+ 2
- 2
css/modals/device-selection/_device-selection.scss View File

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

+ 4
- 0
css/modals/feedback/_feedback.scss View File

@@ -94,5 +94,9 @@
94 94
             };
95 95
 
96 96
         }
97
+        .star-btn:focus,
98
+        .star-btn:active {
99
+            outline: 1px solid #B8C7E0;
100
+        }
97 101
     }
98 102
 }

+ 9
- 3
css/modals/invite/_info.scss View File

@@ -30,20 +30,26 @@
30 30
         overflow: hidden;
31 31
         text-overflow: ellipsis;
32 32
         white-space: nowrap;
33
+        margin-right: 5px;
33 34
     }
34 35
 
35 36
     .info-password-none,
36 37
     .info-password-remote {
37
-        opacity: 0.5;
38
+        color: #fff;
38 39
     }
39 40
 
40 41
     .info-password-input {
41 42
         width: 100%;
42
-        background-color: transparent;
43
-        border: none;
43
+        background-color: #0E1624;
44
+        border-radius: 3px;
45
+        border: 2px solid #202B3D;
44 46
         color: inherit;
45 47
         padding-left: 0;
46 48
     }
49
+    .info-password-input:focus ,
50
+    .info-password-input:active {
51
+       border: 2px solid #B8C7E0;
52
+    }
47 53
 
48 54
     .info-password-local {
49 55
         user-select: text;

+ 2
- 1
css/modals/invite/_invite_more.scss View File

@@ -130,6 +130,7 @@
130 130
                 display: inline-block;
131 131
                 vertical-align: middle;
132 132
                 cursor: pointer;
133
+                height: 24px;
133 134
             }
134 135
         }
135 136
 
@@ -141,7 +142,7 @@
141 142
             & > a {
142 143
                 display: inline-block;
143 144
                 height: 24px;
144
-                width: 48px;
145
+                min-width: 48px;
145 146
                 border-radius: 3px;
146 147
                 text-align: center;
147 148
                 text-decoration: none;

+ 18
- 1
css/modals/settings/_settings.scss View File

@@ -16,12 +16,19 @@
16 16
     }
17 17
 
18 18
     .mock-atlaskit-label {
19
-        color: #56637A;
19
+        color: #b8c7e0;
20 20
         font-size: 12px;
21 21
         font-weight: 600;
22 22
         line-height: 1.33;
23 23
         padding: 20px 0px 4px 0px;
24 24
     }
25
+    input[type="checkbox"]:checked + svg {
26
+        --checkbox-background-color: #6492e7;
27
+        --checkbox-border-color: #6492e7;
28
+    }
29
+    input[type="checkbox"] + svg + span {
30
+        color: #b8c7e0;
31
+    }
25 32
 
26 33
     input[type="checkbox"] + svg + span {
27 34
         color: #9FB0CC;
@@ -65,4 +72,14 @@
65 72
     .sign-out-cta {
66 73
         margin-bottom: 20px;
67 74
     }
75
+
76
+    @media only screen and (max-width: $smallScreen) {
77
+        .device-selection {
78
+            display: flex;
79
+            flex-direction: column;
80
+        }
81
+        .more-tab {
82
+            flex-direction: column;
83
+        }
84
+    }
68 85
 }

+ 1
- 0
css/modals/video-quality/_video-quality.scss View File

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

+ 25
- 98
css/modals/virtual-background/_virtual-background.scss View File

@@ -7,9 +7,7 @@
7 7
     grid-template-columns: auto auto auto auto auto;
8 8
     column-gap: 9px;
9 9
     cursor: pointer;
10
-    .desktop-share:hover, .thumbnail:hover, .blur:hover, .slight-blur:hover, .virtual-background-none:hover{
11
-        height: 56px;
12
-        width: 103px;
10
+    .desktop-share:hover,  .thumbnail:hover, .blur:hover, .slight-blur:hover, .virtual-background-none:hover{
13 11
         opacity: .5;
14 12
         border: 2px solid #99bbf3;
15 13
         @media (min-width: 432px) and (min-width: 432px) and (max-width: 632px) {
@@ -21,152 +19,75 @@
21 19
             width: 56px;
22 20
         }
23 21
     }
24
-    .thumbnail {
22
+    .background-option {
25 23
         margin-top: 8px;
26 24
         border-radius: 6px;
27
-        object-fit: cover;
28 25
         height: 60px;
29 26
         width: 107px;
27
+        text-align: center;
28
+        justify-content: center;
29
+        font-weight: bold;
30
+        box-sizing: border-box;
31
+        display: flex;
32
+        align-items: center;
33
+    }
34
+    .thumbnail {
35
+        object-fit: cover;
30 36
     }
31 37
 
32 38
     .thumbnail:hover ~ .delete-image-icon {
33 39
         display: block;
34 40
     }
35 41
     .thumbnail-selected {
36
-        margin-top: 8px;
37
-        border-radius: 6px;
38 42
         object-fit: cover;
39
-        height: 56px;
40
-        width: 103px;
41 43
         border: 2px solid #246FE5;
42 44
     }
43 45
     .blur{
44 46
         box-shadow: inset 0 0 12px #000000;
45
-        margin-top: 8px;
46 47
         background: #7E8287;
47
-        font-weight: bold;
48
-        height: 60px;
49
-        width: 107px;
50
-        border-radius: 6px;
51
-        text-align: center;
52
-        vertical-align: middle;
53
-        line-height: 60px;
48
+        padding: 0 10px;
54 49
     }
55 50
     .blur-selected {
56 51
         box-shadow: inset 0 0 12px #000000;
57
-        margin-top: 8px;
58 52
         background: #7E8287;
59
-        font-weight: bold;
60
-        height: 56px;
61
-        width: 103px;
62
-        border-radius: 6px;
63 53
         border: 2px solid #246FE5;
64
-        text-align: center;
65
-        vertical-align: middle;
66
-        line-height: 60px;
54
+        padding: 0 10px;
67 55
     }
68 56
     .slight-blur{
69 57
         box-shadow: inset 0 0 12px #000000;
70
-        margin-top: 8px;
71 58
         background: #A4A4A4;
72
-        font-weight: bold;
73
-        height: 60px;
74
-        width: 107px;
75
-        border-radius: 6px;
76
-        text-align: center;
77
-        vertical-align: middle;
78
-        line-height: 60px;
59
+        padding: 0 10px;
79 60
     }
80 61
     .slight-blur-selected{
81 62
         box-shadow: inset 0 0 12px #000000;
82
-        margin-top: 8px;
83 63
         background: #A4A4A4;
84
-        font-weight: bold;
85
-        height: 56px;
86
-        width: 103px;
87
-        border-radius: 6px;
88 64
         border: 2px solid #246FE5;
89
-        text-align: center;
90
-        vertical-align: middle;
91
-        line-height: 60px;
65
+        padding: 0 10px;
92 66
     }
93 67
     .virtual-background-none {
94
-        margin-top: 8px;
95 68
         background: #525252;
96
-        font-weight: bold;
97
-        height: 60px;
98
-        width: 107px;
99
-        border-radius: 6px;
100
-        text-align: center;
101
-        vertical-align: middle;
102
-        line-height: 60px;
69
+        padding: 0 10px;
103 70
     }
104 71
     .none-selected {
105
-        margin-top: 8px;
106 72
         background: #525252;
107
-        font-weight: bold;
108
-        height: 56px;
109
-        width: 103px;
110
-        border-radius: 6px;
111 73
         border: 2px solid #246FE5;
112
-        text-align: center;
113
-        vertical-align: middle;
114
-        line-height: 60px;
74
+        padding: 0 10px;
115 75
     }
76
+
116 77
     .desktop-share{
117
-        margin-top: 8px;
118 78
         background: #525252;
119
-        font-weight: bold;
120
-        height: 60px;
121
-        width: 107px;
122
-        border-radius: 6px;
123
-        text-align: center;
124
-        vertical-align: middle;
125
-        line-height: 60px;
126 79
     }
127 80
     .desktop-share-selected{
128
-        margin-top: 8px;
129 81
         background: #525252;
130
-        font-weight: bold;
131
-        height: 56px;
132
-        width: 103px;
133
-        border-radius: 6px;
134 82
         border: 2px solid #246FE5;
135
-        text-align: center;
136
-        vertical-align: middle;
137
-        line-height: 60px;
138
-    }
139
-    .share-desktop-icon{
140
-        margin-top: 15%;
83
+        padding: 0 10px;
141 84
     }
142 85
 
143 86
     @media (min-width: 432px) and (max-width: 632px) {
144 87
         font-size: 1.5vw;
145
-        .share-desktop-icon{
146
-            margin-top: 25%;
147
-        }
148
-        .desktop-share, .virtual-background-none, .thumbnail, .blur, .slight-blur{
149
-            height: 60px;
150
-            width: 60px;
151
-        }
152
-        .desktop-share-selected, .thumbnail-selected, .none-selected, .blur-selected, .slight-blur-selected{
153
-            height: 56px;
154
-            width: 56px;
155
-        }
156 88
     }
157 89
     @media (max-width: 432px){
158
-        .share-desktop-icon{
159
-            margin-top: 25%;
160
-        }
161 90
         font-size: 1.5vw;
162
-        .desktop-share, .virtual-background-none, .thumbnail, .blur, .slight-blur{
163
-            height: 60px;
164
-            width: 60px;
165
-        }
166
-        .desktop-share-selected, .thumbnail-selected, .none-selected, .blur-selected, .slight-blur-selected{
167
-            height: 56px;
168
-            width: 56px;
169
-        }
170 91
     }
171 92
 }
172 93
 
@@ -205,12 +126,18 @@
205 126
         left: 51px
206 127
     }
207 128
 }
129
+
208 130
 .delete-image-icon:hover {
209 131
     display: block;
210 132
 }
211 133
 
212 134
 .thumbnail-container {
213 135
     position: relative;
136
+    &:focus-within {
137
+        .thumbnail ~ .delete-image-icon{
138
+           display: block;
139
+       }
140
+    }
214 141
 }
215 142
 
216 143
 .add-background{

+ 1
- 1
css/themes/_light.scss View File

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

+ 2
- 2
index.html View File

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

+ 72
- 19
lang/main-de.json View File

@@ -70,12 +70,17 @@
70 70
         },
71 71
         "privateNotice": "Private Nachricht an {{recipient}}",
72 72
         "title": "Chatten",
73
-        "you": "Sie"
73
+        "you": "Sie",
74
+        "message": "Nachricht",
75
+        "messageAccessibleTitle": "{{user}} sagt:",
76
+        "messageAccessibleTitleMe": "Ich sage:",
77
+        "smileysPanel": "Emoji-Auswahl"
74 78
     },
75 79
     "chromeExtensionBanner": {
76 80
         "installExtensionText": "Installieren Sie die Erweiterung für die Integration von Google Calendar und Office 365",
77 81
         "buttonText": "Chrome-Erweiterung installieren",
78
-        "dontShowAgain": "Hinweis nicht mehr anzeigen"
82
+        "dontShowAgain": "Hinweis nicht mehr anzeigen",
83
+        "close": "Schließen"
79 84
     },
80 85
     "connectingOverlay": {
81 86
         "joiningRoom": "Eine Verbindung zu Ihrem Meeting wird hergestellt…"
@@ -204,6 +209,8 @@
204 209
         "e2eeLabel": "Ende-zu-Ende-Verschlüsselung aktivieren",
205 210
         "e2eeWarning": "WARNUNG: Nicht alle Personen dieser Konferenz scheinen Ende-zu-Ende-Verschlüsselung zu unterstützen. Wenn Sie diese aktivieren, können die entsprechenden Personen nichts mehr sehen oder hören.",
206 211
         "enterDisplayName": "Bitte geben Sie hier Ihren Namen ein",
212
+        "enterDisplayNameToJoin" : "Benutzername für Konferenz eingeben" ,
213
+        "embedMeeting": "Besprechung einbetten",
207 214
         "error": "Fehler",
208 215
         "gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
209 216
         "grantModeratorDialog": "Möchten Sie wirklich Moderationsrechte an diese Person vergeben?",
@@ -295,7 +302,7 @@
295 302
         "Share": "Teilen",
296 303
         "shareVideoLinkError": "Bitte einen gültigen YouTube-Link angeben.",
297 304
         "shareVideoTitle": "Video teilen",
298
-        "shareYourScreen": "Bildschirm freigeben",
305
+        "shareYourScreen": "Bildschirmfreigabe ein-/ausschalten",
299 306
         "shareYourScreenDisabled": "Bildschirmfreigabe deaktiviert.",
300 307
         "startLiveStreaming": "Livestream starten",
301 308
         "startRecording": "Aufnahme starten",
@@ -320,7 +327,9 @@
320 327
         "WaitForHostMsgWOk": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Falls Sie die Konferenz leiten, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
321 328
         "WaitingForHostTitle": "Warten auf den Beginn der Konferenz …",
322 329
         "Yes": "Ja",
323
-        "yourEntireScreen": "Ganzer Bildschirm"
330
+        "yourEntireScreen": "Ganzer Bildschirm",
331
+        "remoteUserControls": "Remote Benutzersteuerung von {{username}}",
332
+        "localUserControls": "Lokale Benutzersteuerung"
324 333
     },
325 334
     "dialOut": {
326 335
         "statusMessage": "ist jetzt {{status}}"
@@ -340,8 +349,20 @@
340 349
         "slightBlur": "Hintergrund leicht unscharf",
341 350
         "removeBackground": "Hintergrund entfernen",
342 351
         "uploadImage": "Bild hochladen",
352
+        "addBackground": "Hintergrund hinzufügen",
343 353
         "pleaseWait": "Bitte warten...",
344
-        "none": "keiner"
354
+        "none": "keiner",
355
+        "uploadedImage": "Hochgeladenes Bild {{index}}",
356
+        "deleteImage": "Bild löschen",
357
+        "image1" : "Strand",
358
+        "image2" : "Weiße neutrale Wand",
359
+        "image3" : "Weißer leerer Raum",
360
+        "image4" : "Schwarze Stehlampe",
361
+        "image5" : "Berg",
362
+        "image6" : "Wald",
363
+        "image7" : "Sonnenaufgang",
364
+        "desktopShareError": "Desktop konnte nicht freigegeben werden",
365
+        "desktopShare":"Desktopfreigabe"
345 366
     },
346 367
     "feedback": {
347 368
         "average": "Durchschnittlich",
@@ -350,7 +371,8 @@
350 371
         "good": "Gut",
351 372
         "rateExperience": "Bitte bewerten Sie diese Konferenz",
352 373
         "veryBad": "Sehr schlecht",
353
-        "veryGood": "Sehr gut"
374
+        "veryGood": "Sehr gut",
375
+        "star": "Sterne"
354 376
     },
355 377
     "incomingCall": {
356 378
         "answer": "Antworten",
@@ -367,6 +389,7 @@
367 389
         "country": "Land",
368 390
         "dialANumber": "Um am Meeting teilzunehmen, müssen Sie eine dieser Nummern wählen und dann die PIN eingeben.",
369 391
         "dialInConferenceID": "PIN:",
392
+        "copyNumber":"Nummer kopieren",
370 393
         "dialInNotSupported": "Entschuldigung, leider wird das Einwählen derzeit nicht unterstützt.",
371 394
         "dialInNumber": "Einwählen:",
372 395
         "dialInSummaryError": "Fehler beim Abrufen der Einwahlinformationen. Versuchen Sie es später erneut.",
@@ -404,6 +427,7 @@
404 427
         "support": "Support",
405 428
         "supportMsg": "Wenn der Fehler erneut auftritt, bitte kontaktieren Sie"
406 429
     },
430
+    "jitsiHome": "{{logo}} Logo, verlinkt zur Homepage",
407 431
     "keyboardShortcuts": {
408 432
         "focusLocal": "Lokales Video fokussieren",
409 433
         "focusRemote": "Auf das Video einer anderen Person fokussieren",
@@ -524,7 +548,19 @@
524 548
         "OldElectronAPPTitle": "Sicherheitslücke!",
525 549
         "oldElectronClientDescription1": "Sie scheinen eine alte Version des Jitsi-Meet-Clients zu nutzen. Diese hat bekannte Schwachstellen. Bitte aktualisieren Sie auf unsere ",
526 550
         "oldElectronClientDescription2": "aktuelle Version",
527
-        "oldElectronClientDescription3": "!"
551
+        "oldElectronClientDescription3": "!",
552
+        "groupTitle": "Benachrichtigungen"
553
+    },
554
+    "participantsPane": {
555
+        "close": "Schließen",
556
+        "headings": {
557
+            "lobby": "Lobby ({{count}})",
558
+            "participantsList": "Teilnehmer ({{count}})"
559
+        },
560
+        "actions": {
561
+            "muteAll": "Alle stummschalten",
562
+            "stopVideo": "Video stoppen"
563
+        }
528 564
     },
529 565
     "participantsPane": {
530 566
         "headings": {
@@ -589,6 +625,7 @@
589 625
         "linkCopied": "Link in die Zwischenablage kopiert",
590 626
         "lookGood": "Ihr Mikrofon scheint zu funktionieren.",
591 627
         "or": "oder",
628
+        "keyboardShortcuts" : "Tastaturkurzbefehle aktivieren",
592 629
         "premeeting": "Vorschau",
593 630
         "showScreen": "Konferenzvorschau aktivieren",
594 631
         "startWithPhone": "Mit Telefonaudio starten",
@@ -612,6 +649,7 @@
612 649
         "ringing": "Es klingelt …"
613 650
     },
614 651
     "profile": {
652
+        "avatar": "Benutzerbild",
615 653
         "setDisplayNameLabel": "Anzeigename festlegen",
616 654
         "setEmailInput": "E-Mail eingeben",
617 655
         "setEmailLabel": "E-Mail-Adresse für Gravatar",
@@ -728,29 +766,29 @@
728 766
         "title": "Die Konferenz wurde unterbrochen, weil der Standby-Modus aktiviert wurde."
729 767
     },
730 768
     "toolbar": {
731
-        "accessibilityLabel": {
769
+            "accessibilityLabel": {
732 770
             "audioOnly": "„Nur Audio“ ein-/ausschalten",
733 771
             "audioRoute": "Audiogerät auswählen",
734 772
             "callQuality": "Qualitätseinstellungen",
735 773
             "cc": "Untertitel ein-/ausschalten",
736
-            "chat": "Chatfenster ein-/ausblenden",
774
+            "chat": "Chatfenster öffnen / schließen",
737 775
             "document": "Geteiltes Dokument schließen",
738 776
             "download": "Unsere Apps herunterladen",
739 777
             "embedMeeting": "Konferenz einbetten",
740 778
             "feedback": "Feedback hinterlassen",
741 779
             "fullScreen": "Vollbildmodus ein-/ausschalten",
742 780
             "grantModerator": "Moderationsrechte vergeben",
743
-            "hangup": "Anruf beenden",
781
+            "hangup": "Konferenz verlassen",
744 782
             "help": "Hilfe",
745 783
             "invite": "Person einladen",
746 784
             "kick": "Person entfernen",
747 785
             "lobbyButton": "Lobbymodus ein-/ausschalten",
748 786
             "localRecording": "Lokale Aufzeichnungssteuerelemente ein-/ausschalten",
749 787
             "lockRoom": "Konferenzpasswort ein-/ausschalten",
750
-            "moreActions": "Menü „Weitere Aktionen“ ein-/ausschalten",
751
-            "moreActionsMenu": "Menü „Weitere Aktionen“",
788
+            "moreActions": "Menü „Weitere Einstellungen“ ein-/ausschalten",
789
+            "moreActionsMenu": "Menü „Weitere Einstellungen“",
752 790
             "moreOptions": "Menü „Weitere Optionen“",
753
-            "mute": "„Audio stummschalten“ ein-/ausschalten",
791
+            "mute": "Mikrofon aktivieren / deaktivieren",
754 792
             "muteEveryone": "Alle stummschalten",
755 793
             "muteEveryoneElse": "Alle anderen stummschalten",
756 794
             "muteEveryonesVideo": "Alle Kameras ausschalten",
@@ -759,7 +797,7 @@
759 797
             "pip": "Bild-in-Bild-Modus ein-/ausschalten",
760 798
             "privateMessage": "Private Nachricht senden",
761 799
             "profile": "Profil bearbeiten",
762
-            "raiseHand": "„Melden“ ein-/ausschalten",
800
+            "raiseHand": "Hand erheben / senken",
763 801
             "recording": "Aufzeichnung ein-/ausschalten",
764 802
             "remoteMute": "Personen stummschalten",
765 803
             "remoteVideoMute": "Kamera von dieser Person ausschalten",
@@ -776,7 +814,9 @@
776 814
             "toggleCamera": "Kamera wechseln",
777 815
             "toggleFilmstrip": "Miniaturansichten ein-/ausschalten",
778 816
             "videomute": "„Video stummschalten“ ein-/ausschalten",
779
-            "selectBackground": "Hintergrund auswählen"
817
+            "selectBackground": "Hintergrund auswählen",
818
+            "expand": "Ausklappen",
819
+            "collapse": "Einklappen"
780 820
         },
781 821
         "addPeople": "Personen zur Konferenz hinzufügen",
782 822
         "audioSettings": "Ton-Einstellungen",
@@ -797,7 +837,7 @@
797 837
         "exitFullScreen": "Vollbildmodus verlassen",
798 838
         "exitTileView": "Kachelansicht ausschalten",
799 839
         "feedback": "Feedback hinterlassen",
800
-        "hangup": "Verlassen",
840
+        "hangup": "Konferenz verlassen",
801 841
         "help": "Hilfe",
802 842
         "invite": "Personen einladen",
803 843
         "lobbyButtonDisable": "Lobbymodus deaktivieren",
@@ -807,7 +847,7 @@
807 847
         "lowerYourHand": "Hand senken",
808 848
         "moreActions": "Weitere Einstellungen",
809 849
         "moreOptions": "Weitere Optionen",
810
-        "mute": "Stummschaltung aktivieren / deaktivieren",
850
+        "mute": "Mikrofon aktivieren / deaktivieren",
811 851
         "muteEveryone": "Alle stummschalten",
812 852
         "muteEveryonesVideo": "Alle Kameras ausschalten",
813 853
         "noAudioSignalTitle": "Es kommt kein Input von Ihrem Mikrofon!",
@@ -822,7 +862,7 @@
822 862
         "pip": "Bild-in-Bild-Modus einschalten",
823 863
         "privateMessage": "Private Nachricht senden",
824 864
         "profile": "Profil bearbeiten",
825
-        "raiseHand": "Hand erheben",
865
+        "raiseHand": "Hand erheben / senken",
826 866
         "raiseYourHand": "Melden",
827 867
         "security": "Sicherheitsoptionen",
828 868
         "Settings": "Einstellungen",
@@ -867,6 +907,7 @@
867 907
         "react-nativeGrantPermissions": "Wählen Sie <b><i>Erlauben</i></b>, wenn der Browser um Berechtigungen bittet.",
868 908
         "safariGrantPermissions": "Wählen Sie <b><i>OK</i></b>, wenn der Browser um Berechtigungen bittet."
869 909
     },
910
+    "volumeSlider": "Lautstärkeregler",
870 911
     "videoSIPGW": {
871 912
         "busy": "Es stehen keine freien Ressourcen zur Verfügung. Bitte versuchen Sie es später noch einmal.",
872 913
         "busyTitle": "Keine freien Ressourcen",
@@ -911,6 +952,7 @@
911 952
         "videomute": "Person hat die Kamera angehalten"
912 953
     },
913 954
     "welcomepage": {
955
+        "addMeetingName": "Besprechungsnamen hinzufügen",
914 956
         "accessibilityLabel": {
915 957
             "join": "Zum Teilnehmen tippen",
916 958
             "roomname": "Konferenzname eingeben"
@@ -933,6 +975,9 @@
933 975
         "join": "ERSTELLEN / BEITRETEN",
934 976
         "jitsiOnMobile": "Jitsi unterwegs – einfach unsere Apps herunterladen und Meetings von überall starten",
935 977
         "moderatedMessage": "Oder <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservieren Sie sich eine Konferenz-URL</a>, die nur Sie moderieren.",
978
+        "mobileDownLoadLinkIos": "iOS App Download",
979
+        "mobileDownLoadLinkAndroid": "Android App Download",
980
+        "mobileDownLoadLinkFDroid": "F-Droid App Download",
936 981
         "privacy": "Datenschutz",
937 982
         "recentList": "Verlauf",
938 983
         "recentListDelete": "Eintrag löschen",
@@ -944,7 +989,15 @@
944 989
         "sendFeedback": "Feedback senden",
945 990
         "startMeeting": "Meeting starten",
946 991
         "terms": "AGB",
947
-        "title": "Sichere, voll funktionale und komplett kostenlose Videokonferenzen"
992
+        "title": "Sichere, voll funktionale und komplett kostenlose Videokonferenzen",
993
+        "logo":{
994
+           "calendar":"Kalender Logo",
995
+           "microsoftLogo":"Microsoft Logo",
996
+           "logoDeepLinking":"Jitsi Meet Logo",
997
+           "desktopPreviewThumbnail":"Desktop-Vorschau Thumbnail",
998
+           "googleLogo":"Google Logo",
999
+           "policyLogo":"Richtlinienlogo"
1000
+        }
948 1001
     },
949 1002
     "lonelyMeetingExperience": {
950 1003
         "button": "Andere einladen",

+ 60
- 17
lang/main.json View File

@@ -70,12 +70,17 @@
70 70
         },
71 71
         "privateNotice": "Private message to {{recipient}}",
72 72
         "title": "Chat",
73
-        "you": "you"
73
+        "you": "you",
74
+        "message": "Message",
75
+        "messageAccessibleTitle": "{{user}} says:",
76
+        "messageAccessibleTitleMe": "me says:",
77
+        "smileysPanel": "Emoji panel"
74 78
     },
75 79
     "chromeExtensionBanner": {
76 80
         "installExtensionText": "Install the extension for Google Calendar and Office 365 integration",
77 81
         "buttonText": "Install Chrome Extension",
78
-        "dontShowAgain": "Don’t show me this again"
82
+        "dontShowAgain": "Don’t show me this again",
83
+        "close": "Close"
79 84
     },
80 85
     "connectingOverlay": {
81 86
         "joiningRoom": "Connecting you to your meeting..."
@@ -204,6 +209,8 @@
204 209
         "e2eeLabel": "Enable End-to-End Encryption",
205 210
         "e2eeWarning": "WARNING: Not all participants in this meeting seem to have support for End-to-End encryption. If you enable it they won't be able to see nor hear you.",
206 211
         "enterDisplayName": "Please enter your name here",
212
+        "enterDisplayNameToJoin": "Please enter your name to join",
213
+        "embedMeeting": "Embed meeting",
207 214
         "error": "Error",
208 215
         "gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
209 216
         "grantModeratorDialog": "Are you sure you want to make this participant a moderator?",
@@ -320,7 +327,9 @@
320 327
         "WaitForHostMsgWOk": "The conference <b>{{room}}</b> has not yet started. If you are the host then please press Ok to authenticate. Otherwise, please wait for the host to arrive.",
321 328
         "WaitingForHostTitle": "Waiting for the host ...",
322 329
         "Yes": "Yes",
323
-        "yourEntireScreen": "Your entire screen"
330
+        "yourEntireScreen": "Your entire screen",
331
+        "remoteUserControls": "Remote user controls of {{username}}",
332
+        "localUserControls": "Local user controls"
324 333
     },
325 334
     "dialOut": {
326 335
         "statusMessage": "is now {{status}}"
@@ -341,8 +350,19 @@
341 350
         "slightBlur": "Slight Blur",
342 351
         "removeBackground": "Remove background",
343 352
         "addBackground": "Add background",
353
+        "pleaseWait": "Please wait...",
344 354
         "none": "None",
345
-        "desktopShareError": "Could not create desktop share"
355
+        "uploadedImage": "Uploaded image {{index}}",
356
+        "deleteImage": "Delete image",
357
+        "image1" : "Beach",
358
+        "image2" : "White neutral wall",
359
+        "image3" : "White empty room",
360
+        "image4" : "Black floor lamp",
361
+        "image5" : "Mountain",
362
+        "image6" : "Forest ",
363
+        "image7" : "Sunrise",
364
+        "desktopShareError": "Could not create desktop share",
365
+        "desktopShare":"Desktop share"
346 366
     },
347 367
     "feedback": {
348 368
         "average": "Average",
@@ -351,7 +371,8 @@
351 371
         "good": "Good",
352 372
         "rateExperience": "Rate your meeting experience",
353 373
         "veryBad": "Very Bad",
354
-        "veryGood": "Very Good"
374
+        "veryGood": "Very Good",
375
+        "star": "Star"
355 376
     },
356 377
     "incomingCall": {
357 378
         "answer": "Answer",
@@ -368,6 +389,7 @@
368 389
         "country": "Country",
369 390
         "dialANumber": "To join your meeting, dial one of these numbers and then enter the pin.",
370 391
         "dialInConferenceID": "PIN:",
392
+        "copyNumber":"Copy number",
371 393
         "dialInNotSupported": "Sorry, dialing in is currently not supported.",
372 394
         "dialInNumber": "Dial-in:",
373 395
         "dialInSummaryError": "Error fetching dial-in info now. Please try again later.",
@@ -405,6 +427,7 @@
405 427
         "support": "Support",
406 428
         "supportMsg": "If this keeps happening, reach out to"
407 429
     },
430
+    "jitsiHome": "{{logo}} Logo, links to  Homepage",
408 431
     "keyboardShortcuts": {
409 432
         "focusLocal": "Focus on your video",
410 433
         "focusRemote": "Focus on another person's video",
@@ -525,9 +548,11 @@
525 548
         "OldElectronAPPTitle": "Security vulnerability!",
526 549
         "oldElectronClientDescription1": "You appear to be using an old version of the Jitsi Meet client which has known security vulnerabilities. Please make sure you update to our ",
527 550
         "oldElectronClientDescription2": "latest build",
528
-        "oldElectronClientDescription3": " now!"
551
+        "oldElectronClientDescription3": " now!",
552
+        "groupTitle": "Notifications"
529 553
     },
530 554
     "participantsPane": {
555
+        "close": "Close",
531 556
         "headings": {
532 557
             "lobby": "Lobby ({{count}})",
533 558
             "participantsList": "Meeting participants ({{count}})"
@@ -592,6 +617,7 @@
592 617
         "or": "or",
593 618
         "premeeting": "Pre meeting",
594 619
         "showScreen": "Enable pre meeting screen",
620
+        "keyboardShortcuts" : "Enable Keyboard shortcuts",
595 621
         "startWithPhone": "Start with phone audio",
596 622
         "screenSharingError": "Screen sharing error:",
597 623
         "videoOnlyError": "Video error:",
@@ -613,6 +639,7 @@
613 639
         "ringing": "Ringing..."
614 640
     },
615 641
     "profile": {
642
+        "avatar": "avatar",
616 643
         "setDisplayNameLabel": "Set your display name",
617 644
         "setEmailInput": "Enter e-mail",
618 645
         "setEmailLabel": "Set your gravatar email",
@@ -735,24 +762,24 @@
735 762
             "audioRoute": "Select the sound device",
736 763
             "callQuality": "Manage video quality",
737 764
             "cc": "Toggle subtitles",
738
-            "chat": "Toggle chat window",
765
+            "chat": "Open / Close chat",
739 766
             "document": "Toggle shared document",
740 767
             "download": "Download our apps",
741 768
             "embedMeeting": "Embed meeting",
742 769
             "feedback": "Leave feedback",
743 770
             "fullScreen": "Toggle full screen",
744 771
             "grantModerator": "Grant Moderator",
745
-            "hangup": "Leave the call",
772
+            "hangup": "Leave the meeting",
746 773
             "help": "Help",
747 774
             "invite": "Invite people",
748 775
             "kick": "Kick participant",
749 776
             "lobbyButton": "Enable/disable lobby mode",
750 777
             "localRecording": "Toggle local recording controls",
751 778
             "lockRoom": "Toggle meeting password",
752
-            "moreActions": "Toggle more actions menu",
779
+            "moreActions": "More actions",
753 780
             "moreActionsMenu": "More actions menu",
754 781
             "moreOptions": "Show more options",
755
-            "mute": "Toggle mute audio",
782
+            "mute": "Mute / Unmute",
756 783
             "muteEveryone": "Mute everyone",
757 784
             "muteEveryoneElse": "Mute everyone else",
758 785
             "muteEveryonesVideo": "Disable everyone's camera",
@@ -761,7 +788,7 @@
761 788
             "pip": "Toggle Picture-in-Picture mode",
762 789
             "privateMessage": "Send private message",
763 790
             "profile": "Edit your profile",
764
-            "raiseHand": "Toggle raise hand",
791
+            "raiseHand": "Raise / Lower your hand",
765 792
             "recording": "Toggle recording",
766 793
             "remoteMute": "Mute participant",
767 794
             "remoteVideoMute": "Disable camera of participant",
@@ -770,18 +797,22 @@
770 797
             "shareaudio": "Share audio",
771 798
             "sharedvideo": "Toggle YouTube video sharing",
772 799
             "shareRoom": "Invite someone",
773
-            "shareYourScreen": "Toggle screenshare",
800
+            "shareYourScreen": "Start / Stop sharing your screen",
774 801
             "shortcuts": "Toggle shortcuts",
775 802
             "show": "Show on stage",
776 803
             "speakerStats": "Toggle speaker statistics",
777 804
             "tileView": "Toggle tile view",
778 805
             "toggleCamera": "Toggle camera",
779 806
             "toggleFilmstrip": "Toggle filmstrip",
780
-            "videomute": "Toggle mute video",
781
-            "selectBackground": "Select Background"
807
+            "videomute": "Start / Stop camera",
808
+            "videoblur": "Toggle video blur",
809
+            "selectBackground": "Select Background",
810
+            "expand": "Expand",
811
+            "collapse": "Collapse"
782 812
         },
783 813
         "addPeople": "Add people to your call",
784 814
         "audioSettings": "Audio settings",
815
+        "videoSettings": "Video settings",
785 816
         "audioOnlyOff": "Disable low bandwidth mode",
786 817
         "audioOnlyOn": "Enable low bandwidth mode",
787 818
         "audioRoute": "Select the sound device",
@@ -799,7 +830,7 @@
799 830
         "exitFullScreen": "Exit full screen",
800 831
         "exitTileView": "Exit tile view",
801 832
         "feedback": "Leave feedback",
802
-        "hangup": "Leave",
833
+        "hangup": "Leave the meeting",
803 834
         "help": "Help",
804 835
         "invite": "Invite people",
805 836
         "lobbyButtonDisable": "Disable lobby mode",
@@ -842,7 +873,6 @@
842 873
         "tileViewToggle": "Toggle tile view",
843 874
         "toggleCamera": "Toggle camera",
844 875
         "videomute": "Start / Stop camera",
845
-        "videoSettings": "Video settings",
846 876
         "selectBackground": "Select background"
847 877
     },
848 878
     "transcribing": {
@@ -869,6 +899,7 @@
869 899
         "react-nativeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
870 900
         "safariGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions."
871 901
     },
902
+    "volumeSlider": "Volume slider",
872 903
     "videoSIPGW": {
873 904
         "busy": "We're working on freeing resources. Please try again in a few minutes.",
874 905
         "busyTitle": "The Room service is currently busy",
@@ -913,6 +944,7 @@
913 944
         "videomute": "Participant has stopped the camera"
914 945
     },
915 946
     "welcomepage": {
947
+        "addMeetingName": "Add Meeting name",
916 948
         "accessibilityLabel": {
917 949
             "join": "Tap to join",
918 950
             "roomname": "Enter room name"
@@ -934,6 +966,9 @@
934 966
         "info": "Dial-in info",
935 967
         "join": "CREATE / JOIN",
936 968
         "jitsiOnMobile": "Jitsi on mobile – download our apps and start a meeting from anywhere",
969
+        "mobileDownLoadLinkIos": "Download mobile app for iOS",
970
+        "mobileDownLoadLinkAndroid": "Download mobile app for Android",
971
+        "mobileDownLoadLinkFDroid": "Download mobile app for F-Droid",
937 972
         "moderatedMessage": "Or <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">book a meeting URL</a> in advance where you are the only moderator.",
938 973
         "privacy": "Privacy",
939 974
         "recentList": "Recent",
@@ -946,7 +981,15 @@
946 981
         "sendFeedback": "Send feedback",
947 982
         "startMeeting": "Start meeting",
948 983
         "terms": "Terms",
949
-        "title": "Secure, fully featured, and completely free video conferencing"
984
+        "title": "Secure, fully featured, and completely free video conferencing",
985
+        "logo":{
986
+           "calendar":"Calendar logo",
987
+           "microsoftLogo":"Microsoft logo",
988
+           "logoDeepLinking":"Jitsi meet logo",
989
+           "desktopPreviewThumbnail":"Desktop preview thumbnail",
990
+           "googleLogo":"Google Logo",
991
+           "policyLogo":"Policy logo"
992
+        }
950 993
     },
951 994
     "lonelyMeetingExperience": {
952 995
         "button": "Invite others",

+ 55
- 18
modules/keyboardshortcut/keyboardshortcut.js View File

@@ -1,5 +1,5 @@
1
-/* global APP, $ */
2
-
1
+/* global APP */
2
+import { jitsiLocalStorage } from '@jitsi/js-utils';
3 3
 import Logger from 'jitsi-meet-logger';
4 4
 
5 5
 import {
@@ -30,44 +30,66 @@ const _shortcuts = new Map();
30 30
 const _shortcutsHelp = new Map();
31 31
 
32 32
 /**
33
- * True if the keyboard shortcuts are enabled and false if not.
34
- * @type {boolean}
33
+ * The key used to save in local storage if keyboard shortcuts are enabled.
34
+ */
35
+const _enableShortcutsKey = 'enableShortcuts';
36
+
37
+/**
38
+ * Prefer keyboard handling of these elements over global shortcuts.
39
+ * If a button is triggered using the Spacebar it should not trigger PTT.
40
+ * If an input element is focused and M is pressed it should not mute audio.
41
+ */
42
+const _elementsBlacklist = [
43
+    'input',
44
+    'textarea',
45
+    'button',
46
+    '[role=button]',
47
+    '[role=menuitem]',
48
+    '[role=radio]',
49
+    '[role=tab]',
50
+    '[role=option]',
51
+    '[role=switch]',
52
+    '[role=range]',
53
+    '[role=log]'
54
+];
55
+
56
+/**
57
+ * An element selector for elements that have their own keyboard handling.
35 58
  */
36
-let enabled = true;
59
+const _focusedElementsSelector = `:focus:is(${_elementsBlacklist.join(',')})`;
37 60
 
38 61
 /**
39 62
  * Maps keycode to character, id of popover for given function and function.
40 63
  */
41 64
 const KeyboardShortcut = {
65
+    isPushToTalkActive: false,
66
+
42 67
     init() {
43 68
         this._initGlobalShortcuts();
44 69
 
45 70
         window.onkeyup = e => {
46
-            if (!enabled) {
71
+            if (!this.getEnabled()) {
47 72
                 return;
48 73
             }
49 74
             const key = this._getKeyboardKey(e).toUpperCase();
50 75
             const num = parseInt(key, 10);
51 76
 
52
-            if (!($(':focus').is('input[type=text]')
53
-                || $(':focus').is('input[type=password]')
54
-                || $(':focus').is('textarea'))) {
77
+            if (!document.querySelector(_focusedElementsSelector)) {
55 78
                 if (_shortcuts.has(key)) {
56 79
                     _shortcuts.get(key).function(e);
57 80
                 } else if (!isNaN(num) && num >= 0 && num <= 9) {
58 81
                     APP.store.dispatch(clickOnVideo(num));
59 82
                 }
60
-
61 83
             }
62 84
         };
63 85
 
64 86
         window.onkeydown = e => {
65
-            if (!enabled) {
87
+            if (!this.getEnabled()) {
66 88
                 return;
67 89
             }
68
-            if (!($(':focus').is('input[type=text]')
69
-                || $(':focus').is('input[type=password]')
70
-                || $(':focus').is('textarea'))) {
90
+            const focusedElement = document.querySelector(_focusedElementsSelector);
91
+
92
+            if (!focusedElement) {
71 93
                 if (this._getKeyboardKey(e).toUpperCase() === ' ') {
72 94
                     if (APP.conference.isLocalAudioMuted()) {
73 95
                         sendAnalytics(createShortcutEvent(
@@ -75,8 +97,14 @@ const KeyboardShortcut = {
75 97
                             PRESSED));
76 98
                         logger.log('Talk shortcut pressed');
77 99
                         APP.conference.muteAudio(false);
100
+                        this.isPushToTalkActive = true;
78 101
                     }
79 102
                 }
103
+            } else if (this._getKeyboardKey(e).toUpperCase() === 'ESCAPE') {
104
+                // Allow to remove focus from selected elements using ESC key.
105
+                if (focusedElement && focusedElement.blur) {
106
+                    focusedElement.blur();
107
+                }
80 108
             }
81 109
         };
82 110
     },
@@ -86,7 +114,13 @@ const KeyboardShortcut = {
86 114
      * @param {boolean} value - the new value.
87 115
      */
88 116
     enable(value) {
89
-        enabled = value;
117
+        jitsiLocalStorage.setItem(_enableShortcutsKey, value);
118
+    },
119
+
120
+    getEnabled() {
121
+        // Should be enabled if not explicitly set to false
122
+        // eslint-disable-next-line no-unneeded-ternary
123
+        return jitsiLocalStorage.getItem(_enableShortcutsKey) === 'false' ? false : true;
90 124
     },
91 125
 
92 126
     /**
@@ -198,9 +232,12 @@ const KeyboardShortcut = {
198 232
         // register SPACE shortcut in two steps to insure visibility of help
199 233
         // message
200 234
         this.registerShortcut(' ', null, () => {
201
-            sendAnalytics(createShortcutEvent('push.to.talk', RELEASED));
202
-            logger.log('Talk shortcut released');
203
-            APP.conference.muteAudio(true);
235
+            if (this.isPushToTalkActive) {
236
+                sendAnalytics(createShortcutEvent('push.to.talk', RELEASED));
237
+                logger.log('Talk shortcut released');
238
+                APP.conference.muteAudio(true);
239
+                this.isPushToTalkActive = false;
240
+            }
204 241
         });
205 242
         this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk');
206 243
 

+ 10
- 1
modules/translation/translation.js View File

@@ -1,4 +1,4 @@
1
-/* @flow */
1
+/*  @flow */
2 2
 
3 3
 import jqueryI18next from 'jquery-i18next';
4 4
 
@@ -6,6 +6,10 @@ import { i18next } from '../../react/features/base/i18n';
6 6
 
7 7
 declare var $: Function;
8 8
 
9
+type DocumentElement = {
10
+    lang: string
11
+}
12
+
9 13
 /**
10 14
  * Notifies that the {@link i18next} instance has finished its initialization.
11 15
  *
@@ -13,7 +17,12 @@ declare var $: Function;
13 17
  * @private
14 18
  */
15 19
 function _onI18nInitialized() {
20
+
21
+    const documentElement: DocumentElement
22
+        = document.documentElement || {};
23
+
16 24
     $('[data-i18n]').localize();
25
+    documentElement.lang = i18next.language;
17 26
 }
18 27
 
19 28
 /**

+ 42
- 5
package-lock.json View File

@@ -15262,12 +15262,23 @@
15262 15262
       }
15263 15263
     },
15264 15264
     "react-textarea-autosize": {
15265
-      "version": "7.1.0",
15266
-      "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-7.1.0.tgz",
15267
-      "integrity": "sha512-c2FlR/fP0qbxmlrW96SdrbgP/v0XZMTupqB90zybvmDVDutytUgPl7beU35klwcTeMepUIQEpQUn3P3bdshGPg==",
15265
+      "version": "8.3.0",
15266
+      "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.3.0.tgz",
15267
+      "integrity": "sha512-3GLWFAan2pbwBeoeNDoqGmSbrShORtgWfaWX0RJDivsUrpShh01saRM5RU/i4Zmf+whpBVEY5cA90Eq8Ub1N3w==",
15268 15268
       "requires": {
15269
-        "@babel/runtime": "^7.1.2",
15270
-        "prop-types": "^15.6.0"
15269
+        "@babel/runtime": "^7.10.2",
15270
+        "use-composed-ref": "^1.0.0",
15271
+        "use-latest": "^1.0.0"
15272
+      },
15273
+      "dependencies": {
15274
+        "@babel/runtime": {
15275
+          "version": "7.12.5",
15276
+          "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
15277
+          "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
15278
+          "requires": {
15279
+            "regenerator-runtime": "^0.13.4"
15280
+          }
15281
+        }
15271 15282
       }
15272 15283
     },
15273 15284
     "react-transition-group": {
@@ -17690,6 +17701,11 @@
17690 17701
       "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==",
17691 17702
       "dev": true
17692 17703
     },
17704
+    "ts-essentials": {
17705
+      "version": "2.0.12",
17706
+      "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-2.0.12.tgz",
17707
+      "integrity": "sha512-3IVX4nI6B5cc31/GFFE+i8ey/N2eA0CZDbo6n0yrz0zDX8ZJ8djmU1p+XRz7G3is0F3bB3pu2pAroFdAWQKU3w=="
17708
+    },
17693 17709
     "tslib": {
17694 17710
       "version": "1.9.3",
17695 17711
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
@@ -17975,6 +17991,27 @@
17975 17991
       "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz",
17976 17992
       "integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg=="
17977 17993
     },
17994
+    "use-composed-ref": {
17995
+      "version": "1.1.0",
17996
+      "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.1.0.tgz",
17997
+      "integrity": "sha512-my1lNHGWsSDAhhVAT4MKs6IjBUtG6ZG11uUqexPH9PptiIZDQOzaF4f5tEbJ2+7qvNbtXNBbU3SfmN+fXlWDhg==",
17998
+      "requires": {
17999
+        "ts-essentials": "^2.0.3"
18000
+      }
18001
+    },
18002
+    "use-isomorphic-layout-effect": {
18003
+      "version": "1.1.1",
18004
+      "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.1.tgz",
18005
+      "integrity": "sha512-L7Evj8FGcwo/wpbv/qvSfrkHFtOpCzvM5yl2KVyDJoylVuSvzphiiasmjgQPttIGBAy2WKiBNR98q8w7PiNgKQ=="
18006
+    },
18007
+    "use-latest": {
18008
+      "version": "1.2.0",
18009
+      "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.0.tgz",
18010
+      "integrity": "sha512-d2TEuG6nSLKQLAfW3By8mKr8HurOlTkul0sOpxbClIv4SQ4iOd7BYr7VIzdbktUCnv7dua/60xzd8igMU6jmyw==",
18011
+      "requires": {
18012
+        "use-isomorphic-layout-effect": "^1.0.0"
18013
+      }
18014
+    },
17978 18015
     "use-memo-one": {
17979 18016
       "version": "1.1.2",
17980 18017
       "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz",

+ 1
- 1
package.json View File

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

+ 13
- 3
react/features/base/avatar/components/web/StatelessAvatar.js View File

@@ -2,6 +2,7 @@
2 2
 
3 3
 import React from 'react';
4 4
 
5
+import { translate } from '../../../../base/i18n';
5 6
 import { Icon } from '../../../icons';
6 7
 import AbstractStatelessAvatar, { type Props as AbstractProps } from '../AbstractStatelessAvatar';
7 8
 
@@ -30,14 +31,19 @@ type Props = AbstractProps & {
30 31
     /**
31 32
      * TestId of the element, if any.
32 33
      */
33
-    testId?: string
34
+    testId?: string,
35
+
36
+    /**
37
+     * Invoked to obtain translated strings.
38
+     */
39
+    t: Function
34 40
 };
35 41
 
36 42
 /**
37 43
  * Implements a stateless avatar component that renders an avatar purely from what gets passed through
38 44
  * props.
39 45
  */
40
-export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
46
+class StatelessAvatar extends AbstractStatelessAvatar<Props> {
41 47
     /**
42 48
      * Implements {@code Component#render}.
43 49
      *
@@ -64,6 +70,7 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
64 70
             return (
65 71
                 <div className = { this._getBadgeClassName() }>
66 72
                     <img
73
+                        alt = { this.props.t('profile.avatar') }
67 74
                         className = { this._getAvatarClassName() }
68 75
                         data-testid = { this.props.testId }
69 76
                         id = { this.props.id }
@@ -88,7 +95,7 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
88 95
                         xmlnsXlink = 'http://www.w3.org/1999/xlink'>
89 96
                         <text
90 97
                             dominantBaseline = 'central'
91
-                            fill = 'rgba(255,255,255,.6)'
98
+                            fill = 'rgba(255,255,255,1)'
92 99
                             fontSize = '40pt'
93 100
                             textAnchor = 'middle'
94 101
                             x = '50'
@@ -104,6 +111,7 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
104 111
         return (
105 112
             <div className = { this._getBadgeClassName() }>
106 113
                 <img
114
+                    alt = { this.props.t('profile.avatar') }
107 115
                     className = { this._getAvatarClassName('defaultAvatar') }
108 116
                     data-testid = { this.props.testId }
109 117
                     id = { this.props.id }
@@ -157,3 +165,5 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
157 165
 
158 166
     _isIcon: (?string | ?Object) => boolean
159 167
 }
168
+
169
+export default translate(StatelessAvatar);

+ 31
- 6
react/features/base/buttons/CopyButton.js View File

@@ -3,7 +3,6 @@
3 3
 import React, { useState } from 'react';
4 4
 
5 5
 import { Icon, IconCheck, IconCopy } from '../../base/icons';
6
-import { translate } from '../i18n';
7 6
 import { copyText } from '../util';
8 7
 
9 8
 
@@ -32,7 +31,12 @@ type Props = {
32 31
     /**
33 32
      * The text displayed on copy success
34 33
      */
35
-    textOnCopySuccess: string
34
+    textOnCopySuccess: string,
35
+
36
+    /**
37
+     * The id of the button
38
+     */
39
+    id?: string,
36 40
 };
37 41
 
38 42
 /**
@@ -40,7 +44,7 @@ type Props = {
40 44
  *
41 45
  * @returns {React$Element<any>}
42 46
  */
43
-function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnCopySuccess }: Props) {
47
+function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnCopySuccess, id }: Props) {
44 48
     const [ isClicked, setIsClicked ] = useState(false);
45 49
     const [ isHovered, setIsHovered ] = useState(false);
46 50
 
@@ -83,6 +87,20 @@ function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnC
83 87
         setIsHovered(false);
84 88
     }
85 89
 
90
+    /**
91
+     * KeyPress handler for accessibility.
92
+     *
93
+     * @param {React.KeyboardEventHandler<HTMLDivElement>} e - The key event to handle.
94
+     *
95
+     * @returns {void}
96
+     */
97
+    function onKeyPress(e) {
98
+        if (onClick && (e.key === ' ' || e.key === 'Enter')) {
99
+            e.preventDefault();
100
+            onClick();
101
+        }
102
+    }
103
+
86 104
     /**
87 105
      * Renders the content of the link based on the state.
88 106
      *
@@ -93,7 +111,7 @@ function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnC
93 111
             return (
94 112
                 <>
95 113
                     <div className = 'copy-button-content selected'>
96
-                        {textOnCopySuccess}
114
+                        <span role = { 'alert' }>{ textOnCopySuccess }</span>
97 115
                     </div>
98 116
                     <Icon src = { IconCheck } />
99 117
                 </>
@@ -112,10 +130,17 @@ function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnC
112 130
 
113 131
     return (
114 132
         <div
133
+            aria-label = { textOnHover }
115 134
             className = { `${className} copy-button${isClicked ? ' clicked' : ''}` }
135
+            id = { id }
136
+            onBlur = { onHoverOut }
116 137
             onClick = { onClick }
138
+            onFocus = { onHoverIn }
139
+            onKeyPress = { onKeyPress }
117 140
             onMouseOut = { onHoverOut }
118
-            onMouseOver = { onHoverIn }>
141
+            onMouseOver = { onHoverIn }
142
+            role = 'button'
143
+            tabIndex = { 0 }>
119 144
             { renderContent() }
120 145
         </div>
121 146
     );
@@ -125,4 +150,4 @@ CopyButton.defaultProps = {
125 150
     className: ''
126 151
 };
127 152
 
128
-export default translate(CopyButton);
153
+export default CopyButton;

+ 40
- 3
react/features/base/dialog/components/web/ModalHeader.js View File

@@ -10,6 +10,7 @@ import {
10 10
 } from '@atlaskit/modal-dialog/dist/es2019/styled/Content';
11 11
 import React from 'react';
12 12
 
13
+import { translate } from '../../../i18n';
13 14
 import { Icon, IconClose } from '../../../icons';
14 15
 
15 16
 const TitleIcon = ({ appearance }: { appearance?: 'danger' | 'warning' }) => {
@@ -45,11 +46,40 @@ type Props = {
45 46
  * @class ModalHeader
46 47
  * @extends {React.Component<Props>}
47 48
  */
48
-export default class ModalHeader extends React.Component<Props> {
49
+class ModalHeader extends React.Component<Props> {
49 50
     static defaultProps = {
50 51
         isHeadingMultiline: true
51 52
     };
52 53
 
54
+    /**
55
+     * Initializes a new {@code ModalHeader} instance.
56
+     *
57
+     * @param {*} props - The read-only properties with which the new instance
58
+     * is to be initialized.
59
+     */
60
+    constructor(props) {
61
+        super(props);
62
+
63
+        // Bind event handler so it is only bound once for every instance.
64
+        this._onKeyPress = this._onKeyPress.bind(this);
65
+    }
66
+
67
+    _onKeyPress: (Object) => void;
68
+
69
+    /**
70
+     * KeyPress handler for accessibility.
71
+     *
72
+     * @param {Object} e - The key event to handle.
73
+     *
74
+     * @returns {void}
75
+     */
76
+    _onKeyPress(e) {
77
+        if (this.props.onClose && (e.key === ' ' || e.key === 'Enter')) {
78
+            e.preventDefault();
79
+            this.props.onClose();
80
+        }
81
+    }
82
+
53 83
     /**
54 84
      * Implements React's {@link Component#render()}.
55 85
      *
@@ -65,7 +95,8 @@ export default class ModalHeader extends React.Component<Props> {
65 95
             onClose,
66 96
             showKeyline,
67 97
             isHeadingMultiline,
68
-            testId
98
+            testId,
99
+            t
69 100
         } = this.props;
70 101
 
71 102
         if (!heading) {
@@ -83,12 +114,18 @@ export default class ModalHeader extends React.Component<Props> {
83 114
                         {heading}
84 115
                     </TitleText>
85 116
                 </Title>
117
+
86 118
                 {
87 119
                     !hideCloseIconButton && <Icon
120
+                        ariaLabel = { t('dialog.close') }
88 121
                         onClick = { onClose }
89
-                        src = { IconClose } />
122
+                        onKeyPress = { this._onKeyPress }
123
+                        role = 'button'
124
+                        src = { IconClose }
125
+                        tabIndex = { 0 } />
90 126
                 }
91 127
             </Header>
92 128
         );
93 129
     }
94 130
 }
131
+export default translate(ModalHeader);

+ 4
- 4
react/features/base/dialog/components/web/StatelessDialog.js View File

@@ -118,7 +118,7 @@ class StatelessDialog extends Component<Props> {
118 118
         // Bind event handlers so they are only bound once for every instance.
119 119
         this._onCancel = this._onCancel.bind(this);
120 120
         this._onDialogDismissed = this._onDialogDismissed.bind(this);
121
-        this._onKeyDown = this._onKeyDown.bind(this);
121
+        this._onKeyPress = this._onKeyPress.bind(this);
122 122
         this._onSubmit = this._onSubmit.bind(this);
123 123
         this._renderFooter = this._renderFooter.bind(this);
124 124
         this._setDialogElement = this._setDialogElement.bind(this);
@@ -159,7 +159,7 @@ class StatelessDialog extends Component<Props> {
159 159
                 shouldCloseOnEscapePress = { true }
160 160
                 width = { width || 'medium' }>
161 161
                 <div
162
-                    onKeyDown = { this._onKeyDown }
162
+                    onKeyPress = { this._onKeyPress }
163 163
                     ref = { this._setDialogElement }>
164 164
                     <form
165 165
                         className = 'modal-dialog-form'
@@ -327,7 +327,7 @@ class StatelessDialog extends Component<Props> {
327 327
         this._dialogElement = element;
328 328
     }
329 329
 
330
-    _onKeyDown: (Object) => void;
330
+    _onKeyPress: (Object) => void;
331 331
 
332 332
     /**
333 333
      * Handles 'Enter' key in the dialog to submit/hide dialog depending on
@@ -337,7 +337,7 @@ class StatelessDialog extends Component<Props> {
337 337
      * @private
338 338
      * @returns {void}
339 339
      */
340
-    _onKeyDown(event) {
340
+    _onKeyPress(event) {
341 341
         // If the event coming to the dialog has been subject to preventDefault
342 342
         // we don't handle it here.
343 343
         if (event.defaultPrevented) {

+ 1
- 0
react/features/base/environment/utils.js View File

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

+ 103
- 8
react/features/base/icons/components/Icon.js View File

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import React from 'react';
3
+import React, { useCallback } from 'react';
4 4
 
5 5
 import { Container } from '../../react/base';
6 6
 import { styleTypeToObject } from '../../styles';
@@ -10,7 +10,7 @@ type Props = {
10 10
     /**
11 11
      * Class name for the web platform, if any.
12 12
      */
13
-    className: string,
13
+    className?: string,
14 14
 
15 15
     /**
16 16
      * Color of the icon (if not provided by the style object).
@@ -22,6 +22,11 @@ type Props = {
22 22
      */
23 23
     id?: string,
24 24
 
25
+    /**
26
+     * Id of the icon container
27
+     */
28
+    containerId?: string,
29
+
25 30
     /**
26 31
      * Function to invoke on click.
27 32
      */
@@ -40,8 +45,63 @@ type Props = {
40 45
     /**
41 46
      * Style object to be applied.
42 47
      */
43
-    style?: Object
44
-};
48
+    style?: Object,
49
+
50
+    /**
51
+     * aria disabled flag for the Icon.
52
+     */
53
+    ariaDisabled?: boolean,
54
+
55
+    /**
56
+     * aria label for the Icon.
57
+     */
58
+    ariaLabel?: string,
59
+
60
+    /**
61
+     * whether the element has a popup
62
+     */
63
+    ariaHasPopup?: boolean,
64
+
65
+    /**
66
+     * whether the element has a pressed
67
+     */
68
+    ariaPressed?: boolean,
69
+
70
+    /**
71
+     * id of description label
72
+     */
73
+    ariaDescribedBy?: string,
74
+
75
+    /**
76
+     * whether the element popup is expanded
77
+     */
78
+    ariaExpanded?: boolean,
79
+
80
+    /**
81
+     * The id of the element this button icon controls
82
+     */
83
+    ariaControls?: string,
84
+
85
+      /**
86
+     * tabIndex  for the Icon.
87
+     */
88
+    tabIndex?: number,
89
+
90
+     /**
91
+     * role for the Icon.
92
+     */
93
+    role?: string,
94
+
95
+    /**
96
+     * keypress handler.
97
+     */
98
+    onKeyPress?: Function,
99
+
100
+    /**
101
+     * keydown handler.
102
+     */
103
+    onKeyDown?: Function
104
+}
45 105
 
46 106
 export const DEFAULT_COLOR = navigator.product === 'ReactNative' ? 'white' : undefined;
47 107
 export const DEFAULT_SIZE = navigator.product === 'ReactNative' ? 36 : 22;
@@ -57,11 +117,24 @@ export default function Icon(props: Props) {
57 117
         className,
58 118
         color,
59 119
         id,
120
+        containerId,
60 121
         onClick,
61 122
         size,
62 123
         src: IconComponent,
63
-        style
64
-    } = props;
124
+        style,
125
+        ariaHasPopup,
126
+        ariaLabel,
127
+        ariaDisabled,
128
+        ariaExpanded,
129
+        ariaControls,
130
+        tabIndex,
131
+        ariaPressed,
132
+        ariaDescribedBy,
133
+        role,
134
+        onKeyPress,
135
+        onKeyDown,
136
+        ...rest
137
+    }: Props = props;
65 138
 
66 139
     const {
67 140
         color: styleColor,
@@ -71,11 +144,33 @@ export default function Icon(props: Props) {
71 144
     const calculatedColor = color ?? styleColor ?? DEFAULT_COLOR;
72 145
     const calculatedSize = size ?? styleSize ?? DEFAULT_SIZE;
73 146
 
147
+    const onKeyPressHandler = useCallback(e => {
148
+        if ((e.key === 'Enter' || e.key === ' ') && onClick) {
149
+            e.preventDefault();
150
+            onClick(e);
151
+        } else if (onKeyPress) {
152
+            onKeyPress(e);
153
+        }
154
+    }, [ onClick, onKeyPress ]);
155
+
74 156
     return (
75 157
         <Container
76
-            className = { `jitsi-icon ${className}` }
158
+            { ...rest }
159
+            aria-controls = { ariaControls }
160
+            aria-describedby = { ariaDescribedBy }
161
+            aria-disabled = { ariaDisabled }
162
+            aria-expanded = { ariaExpanded }
163
+            aria-haspopup = { ariaHasPopup }
164
+            aria-label = { ariaLabel }
165
+            aria-pressed = { ariaPressed }
166
+            className = { `jitsi-icon ${className || ''}` }
167
+            id = { containerId }
77 168
             onClick = { onClick }
78
-            style = { restStyle }>
169
+            onKeyDown = { onKeyDown }
170
+            onKeyPress = { onKeyPressHandler }
171
+            role = { role }
172
+            style = { restStyle }
173
+            tabIndex = { tabIndex }>
79 174
             <IconComponent
80 175
                 fill = { calculatedColor }
81 176
                 height = { calculatedSize }

+ 47
- 3
react/features/base/popover/components/Popover.web.js View File

@@ -129,7 +129,9 @@ class Popover extends Component<Props, State> {
129 129
         // Bind event handlers so they are only bound once for every instance.
130 130
         this._onHideDialog = this._onHideDialog.bind(this);
131 131
         this._onShowDialog = this._onShowDialog.bind(this);
132
+        this._onKeyPress = this._onKeyPress.bind(this);
132 133
         this._drawerContainerRef = React.createRef();
134
+        this._onEscKey = this._onEscKey.bind(this);
133 135
     }
134 136
 
135 137
     /**
@@ -207,6 +209,7 @@ class Popover extends Component<Props, State> {
207 209
             <div
208 210
                 className = { className }
209 211
                 id = { id }
212
+                onKeyPress = { this._onKeyPress }
210 213
                 onMouseEnter = { this._onShowDialog }
211 214
                 onMouseLeave = { this._onHideDialog }>
212 215
                 <InlineDialog
@@ -231,13 +234,13 @@ class Popover extends Component<Props, State> {
231 234
         this.setState({ showDialog: false });
232 235
     }
233 236
 
234
-    _onShowDialog: () => void;
237
+    _onShowDialog: (Object) => void;
235 238
 
236 239
     /**
237 240
      * Displays the {@code InlineDialog} and calls any registered onPopoverOpen
238 241
      * callbacks.
239 242
      *
240
-     * @param {MouseEvent} event - The mouse event to intercept.
243
+     * @param {Object} event - The mouse event or the keypress event to intercept.
241 244
      * @private
242 245
      * @returns {void}
243 246
      */
@@ -252,6 +255,45 @@ class Popover extends Component<Props, State> {
252 255
         }
253 256
     }
254 257
 
258
+    _onKeyPress: (Object) => void;
259
+
260
+    /**
261
+     * KeyPress handler for accessibility.
262
+     *
263
+     * @param {Object} e - The key event to handle.
264
+     *
265
+     * @returns {void}
266
+     */
267
+    _onKeyPress(e) {
268
+        if (e.key === ' ' || e.key === 'Enter') {
269
+            e.preventDefault();
270
+            if (this.state.showDialog) {
271
+                this._onHideDialog();
272
+            } else {
273
+                this._onShowDialog(e);
274
+            }
275
+        }
276
+    }
277
+
278
+    _onEscKey: (Object) => void;
279
+
280
+    /**
281
+     * KeyPress handler for accessibility.
282
+     *
283
+     * @param {Object} e - The key event to handle.
284
+     *
285
+     * @returns {void}
286
+     */
287
+    _onEscKey(e) {
288
+        if (e.key === 'Escape') {
289
+            e.preventDefault();
290
+            e.stopPropagation();
291
+            if (this.state.showDialog) {
292
+                this._onHideDialog();
293
+            }
294
+        }
295
+    }
296
+
255 297
     /**
256 298
      * Renders the React Element to be displayed in the {@code InlineDialog}.
257 299
      * Also adds padding to support moving the mouse from the trigger to the
@@ -264,7 +306,9 @@ class Popover extends Component<Props, State> {
264 306
         const { content, position } = this.props;
265 307
 
266 308
         return (
267
-            <div className = 'popover'>
309
+            <div
310
+                className = 'popover'
311
+                onKeyDown = { this._onEscKey }>
268 312
                 { content }
269 313
                 <div className = 'popover-mouse-padding-top' />
270 314
                 <div className = { _mapPositionToPaddingClass(position) } />

+ 73
- 13
react/features/base/premeeting/components/web/ActionButton.js View File

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import React from 'react';
3
+import React, { useCallback } from 'react';
4 4
 
5 5
 import { Icon, IconArrowDown } from '../../../icons';
6 6
 
@@ -46,10 +46,36 @@ type Props = {
46 46
      */
47 47
     onClick: Function,
48 48
 
49
+
49 50
     /**
50 51
      * Click handler for options.
51 52
      */
52
-    onOptionsClick?: Function
53
+    onOptionsClick?: Function,
54
+
55
+    /**
56
+     * to navigate with the keyboard.
57
+     */
58
+    tabIndex?: number,
59
+
60
+    /**
61
+     * to give a role to the icon.
62
+     */
63
+    role?: string,
64
+
65
+    /**
66
+     * to give a aria-pressed to the icon.
67
+     */
68
+    ariaPressed?: boolean,
69
+
70
+    /**
71
+     * The Label of the current element
72
+     */
73
+    ariaLabel?: string,
74
+
75
+    /**
76
+     * The Label of the child element
77
+     */
78
+    ariaDropDownLabel?: string
53 79
 };
54 80
 
55 81
 /**
@@ -66,23 +92,57 @@ function ActionButton({
66 92
     testId,
67 93
     type = 'primary',
68 94
     onClick,
69
-    onOptionsClick
95
+    onOptionsClick,
96
+    tabIndex,
97
+    role,
98
+    ariaPressed,
99
+    ariaLabel,
100
+    ariaDropDownLabel
70 101
 }: Props) {
102
+
103
+    const onKeyPressHandler = useCallback(e => {
104
+        if (onClick && !disabled && (e.key === ' ' || e.key === 'Enter')) {
105
+            e.preventDefault();
106
+            onClick(e);
107
+        }
108
+    }, [ onClick, disabled ]);
109
+
110
+    const onOptionsKeyPressHandler = useCallback(e => {
111
+        if (onOptionsClick && !disabled && (e.key === ' ' || e.key === 'Enter')) {
112
+            e.preventDefault();
113
+            e.stopPropagation();
114
+            onOptionsClick(e);
115
+        }
116
+    }, [ onOptionsClick, disabled ]);
117
+
71 118
     return (
72 119
         <div
120
+            aria-disabled = { disabled }
121
+            aria-label = { ariaLabel }
73 122
             className = { `action-btn ${className} ${type} ${disabled ? 'disabled' : ''}` }
74 123
             data-testid = { testId ? testId : undefined }
75
-            onClick = { disabled ? undefined : onClick }>
124
+            onClick = { disabled ? undefined : onClick }
125
+            onKeyPress = { onKeyPressHandler }
126
+            role = 'button'
127
+            tabIndex = { 0 } >
76 128
             {children}
77
-            {hasOptions && <div
78
-                className = 'options'
79
-                data-testid = 'prejoin.joinOptions'
80
-                onClick = { disabled ? undefined : onOptionsClick }>
81
-                <Icon
82
-                    className = 'icon'
83
-                    size = { 14 }
84
-                    src = { OptionsIcon } />
85
-            </div>
129
+            { hasOptions
130
+                  && <div
131
+                      aria-disabled = { disabled }
132
+                      aria-haspopup = 'true'
133
+                      aria-label = { ariaDropDownLabel }
134
+                      aria-pressed = { ariaPressed }
135
+                      className = 'options'
136
+                      data-testid = 'prejoin.joinOptions'
137
+                      onClick = { disabled ? undefined : onOptionsClick }
138
+                      onKeyPress = { onOptionsKeyPressHandler }
139
+                      role = { role }
140
+                      tabIndex = { tabIndex }>
141
+                      <Icon
142
+                          className = 'icon'
143
+                          size = { 14 }
144
+                          src = { OptionsIcon } />
145
+                  </div>
86 146
             }
87 147
         </div>
88 148
     );

+ 33
- 7
react/features/base/premeeting/components/web/ConnectionStatus.js View File

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import React, { useState } from 'react';
3
+import React, { useCallback, useState } from 'react';
4 4
 
5 5
 import { translate } from '../../../i18n';
6 6
 import { Icon, IconArrowDownSmall, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons';
@@ -65,24 +65,50 @@ function ConnectionStatus({ connectionDetails, t, connectionType }: Props) {
65 65
         ? 'con-status-details-visible'
66 66
         : 'con-status-details-hidden';
67 67
 
68
+    const onToggleDetails = useCallback(e => {
69
+        e.preventDefault();
70
+        toggleDetails(!showDetails);
71
+    }, [ showDetails, toggleDetails ]);
72
+
73
+    const onKeyPressToggleDetails = useCallback(e => {
74
+        if (toggleDetails && (e.key === ' ' || e.key === 'Enter')) {
75
+            e.preventDefault();
76
+            toggleDetails(!showDetails);
77
+        }
78
+    }, [ showDetails, toggleDetails ]);
79
+
68 80
     return (
69 81
         <div className = 'con-status'>
70 82
             <div className = 'con-status-container'>
71
-                <div className = 'con-status-header'>
83
+                <div
84
+                    aria-level = { 1 }
85
+                    className = 'con-status-header'
86
+                    role = 'heading'>
72 87
                     <div className = { `con-status-circle ${connectionClass}` }>
73 88
                         <Icon
74 89
                             size = { 16 }
75 90
                             src = { icon } />
76 91
                     </div>
77
-                    <span className = 'con-status-text'>{t(connectionText)}</span>
92
+                    <span
93
+                        aria-hidden = { !showDetails }
94
+                        className = 'con-status-text'
95
+                        id = 'connection-status-description'>{t(connectionText)}</span>
78 96
                     <Icon
97
+                        ariaDescribedBy = 'connection-status-description'
98
+                        ariaPressed = { showDetails }
79 99
                         className = { arrowClassName }
80
-                        // eslint-disable-next-line react/jsx-no-bind
81
-                        onClick = { () => toggleDetails(!showDetails) }
100
+                        onClick = { onToggleDetails }
101
+                        onKeyPress = { onKeyPressToggleDetails }
102
+                        role = 'button'
82 103
                         size = { 24 }
83
-                        src = { IconArrowDownSmall } />
104
+                        src = { IconArrowDownSmall }
105
+                        tabIndex = { 0 } />
84 106
                 </div>
85
-                <div className = { `con-status-details ${detailsClassName}` }>{detailsText}</div>
107
+                <div
108
+                    aria-level = '2'
109
+                    className = { `con-status-details ${detailsClassName}` }
110
+                    role = 'heading'>
111
+                    {detailsText}</div>
86 112
             </div>
87 113
         </div>
88 114
     );

+ 5
- 166
react/features/base/premeeting/components/web/CopyMeetingUrl.js View File

@@ -2,11 +2,11 @@
2 2
 
3 3
 import React, { Component } from 'react';
4 4
 
5
+import CopyMeetingLinkSection
6
+    from '../../../../invite/components/add-people-dialog/web/CopyMeetingLinkSection';
5 7
 import { getCurrentConferenceUrl } from '../../../connection';
6 8
 import { translate } from '../../../i18n';
7
-import { Icon, IconCopy, IconCheck } from '../../../icons';
8 9
 import { connect } from '../../../redux';
9
-import { copyText, getDecodedURI } from '../../../util';
10 10
 
11 11
 type Props = {
12 12
 
@@ -27,152 +27,11 @@ type Props = {
27 27
     _enableAutomaticUrlCopy: boolean,
28 28
 };
29 29
 
30
-type State = {
31
-
32
-    /**
33
-     * If true it shows the 'copy link' message.
34
-     */
35
-    showCopyLink: boolean,
36
-
37
-    /**
38
-     * If true it shows the 'link copied' message.
39
-     */
40
-    showLinkCopied: boolean,
41
-};
42
-
43
-const COPY_TIMEOUT = 2000;
44 30
 
45 31
 /**
46 32
  * Component used to copy meeting url on prejoin page.
47 33
  */
48
-class CopyMeetingUrl extends Component<Props, State> {
49
-
50
-    /**
51
-     * Initializes a new {@code Prejoin} instance.
52
-     *
53
-     * @inheritdoc
54
-     */
55
-    constructor(props) {
56
-        super(props);
57
-
58
-        this.state = {
59
-            showCopyLink: false,
60
-            showLinkCopied: false
61
-        };
62
-        this._copyUrl = this._copyUrl.bind(this);
63
-        this._hideCopyLink = this._hideCopyLink.bind(this);
64
-        this._hideLinkCopied = this._hideLinkCopied.bind(this);
65
-        this._showCopyLink = this._showCopyLink.bind(this);
66
-        this._showLinkCopied = this._showLinkCopied.bind(this);
67
-        this._copyUrlAutomatically = this._copyUrlAutomatically.bind(this);
68
-    }
69
-
70
-    _copyUrl: () => void;
71
-
72
-    /**
73
-     * Callback invoked to copy the url to clipboard.
74
-     *
75
-     * @returns {void}
76
-     */
77
-    async _copyUrl() {
78
-        const success = await copyText(this.props.url);
79
-
80
-        if (success) {
81
-            this._showLinkCopied();
82
-            window.setTimeout(this._hideLinkCopied, COPY_TIMEOUT);
83
-        }
84
-    }
85
-
86
-    _hideLinkCopied: () => void;
87
-
88
-    /**
89
-     * Hides the 'Link copied' message.
90
-     *
91
-     * @private
92
-     * @returns {void}
93
-     */
94
-    _hideLinkCopied() {
95
-        this.setState({
96
-            showLinkCopied: false
97
-        });
98
-    }
99
-
100
-    _hideCopyLink: () => void;
101
-
102
-    /**
103
-     * Hides the 'Copy link' text.
104
-     *
105
-     * @private
106
-     * @returns {void}
107
-     */
108
-    _hideCopyLink() {
109
-        this.setState({
110
-            showCopyLink: false,
111
-            showLinkCopied: false
112
-        });
113
-    }
114
-
115
-    _showCopyLink: () => void;
116
-
117
-    /**
118
-     * Shows the dark 'Copy link' text on hover.
119
-     *
120
-     * @private
121
-     * @returns {void}
122
-     */
123
-    _showCopyLink() {
124
-        this.setState({
125
-            showCopyLink: true,
126
-            showLinkCopied: false
127
-        });
128
-    }
129
-
130
-    _showLinkCopied: () => void;
131
-
132
-    /**
133
-     * Shows the green 'Link copied' message.
134
-     *
135
-     * @private
136
-     * @returns {void}
137
-     */
138
-    _showLinkCopied() {
139
-        this.setState({
140
-            showLinkCopied: true,
141
-            showCopyLink: false
142
-        });
143
-    }
144
-
145
-    _copyUrlAutomatically: () => void;
146
-
147
-    /**
148
-     * Attempts to automatically copy invitation URL.
149
-     * Document has to be focused in order for this to work.
150
-     *
151
-     * @private
152
-     * @returns {void}
153
-     */
154
-    async _copyUrlAutomatically() {
155
-        const isCopied = await copyText(this.props.url);
156
-
157
-        if (isCopied) {
158
-            this._showLinkCopied();
159
-            window.setTimeout(this._hideLinkCopied, COPY_TIMEOUT);
160
-        }
161
-    }
162
-
163
-    /**
164
-     * Implements React's {@link Component#componentDidMount()}. Invoked
165
-     * immediately before mounting occurs.
166
-     *
167
-     * @inheritdoc
168
-     */
169
-    componentDidMount() {
170
-        const { _enableAutomaticUrlCopy } = this.props;
171
-
172
-        if (_enableAutomaticUrlCopy) {
173
-            setTimeout(this._copyUrlAutomatically, 2000);
174
-        }
175
-    }
34
+class CopyMeetingUrl extends Component<Props> {
176 35
 
177 36
     /**
178 37
      * Implements React's {@link Component#render()}.
@@ -181,29 +40,9 @@ class CopyMeetingUrl extends Component<Props, State> {
181 40
      * @returns {ReactElement}
182 41
      */
183 42
     render() {
184
-        const { showCopyLink, showLinkCopied } = this.state;
185
-        const { url, t } = this.props;
186
-        const { _copyUrl, _showCopyLink, _hideCopyLink } = this;
187
-        const src = showLinkCopied ? IconCheck : IconCopy;
188
-
189 43
         return (
190
-            <div
191
-                className = 'copy-meeting'
192
-                onMouseEnter = { _showCopyLink }
193
-                onMouseLeave = { _hideCopyLink }>
194
-                <div
195
-                    className = { `url ${showLinkCopied ? 'done' : ''}` }
196
-                    onClick = { _copyUrl } >
197
-                    <div className = 'copy-meeting-text'>
198
-                        { !showCopyLink && !showLinkCopied && getDecodedURI(url) }
199
-                        { showCopyLink && t('prejoin.copyAndShare') }
200
-                        { showLinkCopied && t('prejoin.linkCopied') }
201
-                    </div>
202
-                    <Icon
203
-                        onClick = { _copyUrl }
204
-                        size = { 24 }
205
-                        src = { src } />
206
-                </div>
44
+            <div className = 'copy-meeting'>
45
+                <CopyMeetingLinkSection url = { this.props.url } />
207 46
             </div>
208 47
         );
209 48
     }

+ 5
- 1
react/features/base/premeeting/components/web/InputField.js View File

@@ -44,7 +44,9 @@ type Props = {
44 44
     /**
45 45
      * Externally provided value.
46 46
      */
47
-    value?: string
47
+    value?: string,
48
+    id?: string,
49
+    autoComplete?: string
48 50
 };
49 51
 
50 52
 type State = {
@@ -114,9 +116,11 @@ export default class InputField extends PureComponent<Props, State> {
114 116
     render() {
115 117
         return (
116 118
             <input
119
+                autoComplete = { this.props.autoComplete }
117 120
                 autoFocus = { this.props.autoFocus }
118 121
                 className = { `field ${this.state.focused ? 'focused' : ''} ${this.props.className || ''}` }
119 122
                 data-testid = { this.props.testId ? this.props.testId : undefined }
123
+                id = { this.props.id }
120 124
                 onBlur = { this._onBlur }
121 125
                 onChange = { this._onChange }
122 126
                 onFocus = { this._onFocus }

+ 2
- 2
react/features/base/premeeting/components/web/PreMeetingScreen.js View File

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

+ 15
- 3
react/features/base/premeeting/components/web/ToggleButton.js View File

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import React from 'react';
3
+import React, { useCallback } from 'react';
4 4
 
5 5
 import { Icon, IconCheck } from '../../../icons';
6 6
 
@@ -32,10 +32,22 @@ type Props = {
32 32
 function ToggleButton({ children, isToggled, onClick }: Props) {
33 33
     const className = isToggled ? `${mainClass} ${mainClass}--toggled` : mainClass;
34 34
 
35
+    const onKeyPressHandler = useCallback(e => {
36
+        if (onClick && (e.key === ' ')) {
37
+            e.preventDefault();
38
+            onClick();
39
+        }
40
+    }, [ onClick ]);
41
+
35 42
     return (
36 43
         <div
44
+            aria-checked = { isToggled }
37 45
             className = { className }
38
-            onClick = { onClick }>
46
+            id = 'toggle-button'
47
+            onClick = { onClick }
48
+            onKeyPress = { onKeyPressHandler }
49
+            role = 'switch'
50
+            tabIndex = { 0 }>
39 51
             <div className = 'toggle-button-container'>
40 52
                 <div className = 'toggle-button-icon-container'>
41 53
                     <Icon
@@ -43,7 +55,7 @@ function ToggleButton({ children, isToggled, onClick }: Props) {
43 55
                         size = { 10 }
44 56
                         src = { IconCheck } />
45 57
                 </div>
46
-                <span>{children}</span>
58
+                <label htmlFor = 'toggle-button'>{children}</label>
47 59
             </div>
48 60
         </div>
49 61
     );

+ 72
- 8
react/features/base/react/components/web/MeetingsList.js View File

@@ -4,7 +4,8 @@ import React, { Component } from 'react';
4 4
 
5 5
 import {
6 6
     getLocalizedDateFormatter,
7
-    getLocalizedDurationFormatter
7
+    getLocalizedDurationFormatter,
8
+    translate
8 9
 } from '../../../i18n';
9 10
 import { Icon, IconTrash } from '../../../icons';
10 11
 
@@ -41,7 +42,12 @@ type Props = {
41 42
     /**
42 43
      * Handler for deleting an item.
43 44
      */
44
-    onItemDelete?: Function
45
+    onItemDelete?: Function,
46
+
47
+    /**
48
+     * Invoked to obtain translated strings.
49
+     */
50
+    t: Function
45 51
 };
46 52
 
47 53
 /**
@@ -80,7 +86,7 @@ function _toTimeString(times) {
80 86
  *
81 87
  * @extends Component
82 88
  */
83
-export default class MeetingsList extends Component<Props> {
89
+class MeetingsList extends Component<Props> {
84 90
     /**
85 91
      * Constructor of the MeetingsList component.
86 92
      *
@@ -99,7 +105,7 @@ export default class MeetingsList extends Component<Props> {
99 105
      * @returns {React.ReactNode}
100 106
      */
101 107
     render() {
102
-        const { listEmptyComponent, meetings } = this.props;
108
+        const { listEmptyComponent, meetings, t } = this.props;
103 109
 
104 110
         /**
105 111
          * If there are no recent meetings we don't want to display anything
@@ -107,7 +113,10 @@ export default class MeetingsList extends Component<Props> {
107 113
         if (meetings) {
108 114
             return (
109 115
                 <Container
110
-                    className = 'meetings-list'>
116
+                    aria-label = { t('welcomepage.recentList') }
117
+                    className = 'meetings-list'
118
+                    role = 'menu'
119
+                    tabIndex = '-1'>
111 120
                     {
112 121
                         meetings.length === 0
113 122
                             ? listEmptyComponent
@@ -139,6 +148,29 @@ export default class MeetingsList extends Component<Props> {
139 148
         return null;
140 149
     }
141 150
 
151
+    _onKeyPress: string => Function;
152
+
153
+    /**
154
+     * Returns a function that is used in the onPress callback of the items.
155
+     *
156
+     * @param {string} url - The URL of the item to navigate to.
157
+     * @private
158
+     * @returns {Function}
159
+     */
160
+    _onKeyPress(url) {
161
+        const { disabled, onPress } = this.props;
162
+
163
+        if (!disabled && url && typeof onPress === 'function') {
164
+            return e => {
165
+                if (e.key === ' ' || e.key === 'Enter') {
166
+                    onPress(url);
167
+                }
168
+            };
169
+        }
170
+
171
+        return null;
172
+    }
173
+
142 174
     _onDelete: Object => Function;
143 175
 
144 176
     /**
@@ -158,6 +190,27 @@ export default class MeetingsList extends Component<Props> {
158 190
         };
159 191
     }
160 192
 
193
+    _onDeleteKeyPress: Object => Function;
194
+
195
+    /**
196
+     * Returns a function that is used on the onDelete keypress callback.
197
+     *
198
+     * @param {Object} item - The item to be deleted.
199
+     * @private
200
+     * @returns {Function}
201
+     */
202
+    _onDeleteKeyPress(item) {
203
+        const { onItemDelete } = this.props;
204
+
205
+        return e => {
206
+            if (onItemDelete && (e.key === ' ' || e.key === 'Enter')) {
207
+                e.preventDefault();
208
+                e.stopPropagation();
209
+                onItemDelete(item);
210
+            }
211
+        };
212
+    }
213
+
161 214
     _renderItem: (Object, number) => React$Node;
162 215
 
163 216
     /**
@@ -176,17 +229,22 @@ export default class MeetingsList extends Component<Props> {
176 229
             title,
177 230
             url
178 231
         } = meeting;
179
-        const { hideURL = false, onItemDelete } = this.props;
232
+        const { hideURL = false, onItemDelete, t } = this.props;
180 233
         const onPress = this._onPress(url);
234
+        const onKeyPress = this._onKeyPress(url);
181 235
         const rootClassName
182 236
             = `item ${
183 237
                 onPress ? 'with-click-handler' : 'without-click-handler'}`;
184 238
 
185 239
         return (
186 240
             <Container
241
+                aria-label = { title }
187 242
                 className = { rootClassName }
188 243
                 key = { index }
189
-                onClick = { onPress }>
244
+                onClick = { onPress }
245
+                onKeyPress = { onKeyPress }
246
+                role = 'menuitem'
247
+                tabIndex = { 0 }>
190 248
                 <Container className = 'left-column'>
191 249
                     <Text className = 'title'>
192 250
                         { _toDateString(date) }
@@ -216,11 +274,17 @@ export default class MeetingsList extends Component<Props> {
216 274
                     { elementAfter || null }
217 275
 
218 276
                     { onItemDelete && <Icon
277
+                        ariaLabel = { t('welcomepage.recentListDelete') }
219 278
                         className = 'delete-meeting'
220 279
                         onClick = { this._onDelete(meeting) }
221
-                        src = { IconTrash } />}
280
+                        onKeyPress = { this._onDeleteKeyPress(meeting) }
281
+                        role = 'button'
282
+                        src = { IconTrash }
283
+                        tabIndex = { 0 } />}
222 284
                 </Container>
223 285
             </Container>
224 286
         );
225 287
     }
226 288
 }
289
+
290
+export default translate(MeetingsList);

+ 5
- 1
react/features/base/react/components/web/Watermarks.js View File

@@ -160,13 +160,15 @@ class Watermarks extends Component<Props, State> {
160 160
             _logoUrl,
161 161
             _showJitsiWatermark
162 162
         } = this.props;
163
+        const { t } = this.props;
163 164
         let reactElement = null;
164 165
 
165 166
         if (_showJitsiWatermark) {
166 167
             const style = {
167 168
                 backgroundImage: `url(${_logoUrl})`,
168 169
                 maxWidth: 140,
169
-                maxHeight: 70
170
+                maxHeight: 70,
171
+                position: _logoLink ? 'static' : 'absolute'
170 172
             };
171 173
 
172 174
             reactElement = (<div
@@ -176,6 +178,8 @@ class Watermarks extends Component<Props, State> {
176 178
             if (_logoLink) {
177 179
                 reactElement = (
178 180
                     <a
181
+                        aria-label = { t('jitsiHome', { logo: interfaceConfig.APP_NAME }) }
182
+                        className = 'watermark leftwatermark'
179 183
                         href = { _logoLink }
180 184
                         target = '_new'>
181 185
                         { reactElement }

+ 6
- 2
react/features/base/toolbox/components/AbstractButton.js View File

@@ -246,14 +246,18 @@ export default class AbstractButton<P: Props, S: *> extends Component<P, S> {
246 246
      * Handles clicking / pressing the button, and toggles the audio mute state
247 247
      * accordingly.
248 248
      *
249
+     * @param {Object} e - Event.
249 250
      * @private
250 251
      * @returns {void}
251 252
      */
252
-    _onClick() {
253
+    _onClick(e) {
253 254
         const { afterClick } = this.props;
254 255
 
255 256
         this._handleClick();
256
-        afterClick && afterClick();
257
+        afterClick && afterClick(e);
258
+
259
+        // blur after click to release focus from button to allow PTT.
260
+        e && e.currentTarget && e.currentTarget.blur();
257 261
     }
258 262
 
259 263
     /**

+ 4
- 0
react/features/base/toolbox/components/BetaTag.js View File

@@ -6,6 +6,10 @@ import { translate } from '../../i18n';
6 6
 import { Container, Text } from '../../react';
7 7
 
8 8
 type Props = {
9
+
10
+    /**
11
+     * Invoked to obtain translated strings.
12
+     */
9 13
     t: Function
10 14
 };
11 15
 

+ 7
- 15
react/features/base/toolbox/components/ToolboxItem.web.js View File

@@ -20,29 +20,21 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
20 20
     constructor(props: Props) {
21 21
         super(props);
22 22
 
23
-        this._onKeyDown = this._onKeyDown.bind(this);
23
+        this._onKeyPress = this._onKeyPress.bind(this);
24 24
     }
25 25
 
26
-    _onKeyDown: (Object) => void;
26
+    _onKeyPress: (Object) => void;
27 27
 
28 28
     /**
29
-     * Handles 'Enter' key on the button to trigger onClick for accessibility.
30
-     * We should be handling Space onKeyUp but it conflicts with PTT.
29
+     * Handles 'Enter' and Space key on the button to trigger onClick for accessibility.
31 30
      *
32 31
      * @param {Object} event - The key event.
33 32
      * @private
34 33
      * @returns {void}
35 34
      */
36
-    _onKeyDown(event) {
37
-        // If the event coming to the dialog has been subject to preventDefault
38
-        // we don't handle it here.
39
-        if (event.defaultPrevented) {
40
-            return;
41
-        }
42
-
43
-        if (event.key === 'Enter') {
35
+    _onKeyPress(event) {
36
+        if (event.key === 'Enter' || event.key === ' ') {
44 37
             event.preventDefault();
45
-            event.stopPropagation();
46 38
             this.props.onClick();
47 39
         }
48 40
     }
@@ -72,9 +64,9 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
72 64
             'aria-label': this.accessibilityLabel,
73 65
             className: className + (disabled ? ' disabled' : ''),
74 66
             onClick: disabled ? undefined : onClick,
75
-            onKeyDown: this._onKeyDown,
67
+            onKeyPress: this._onKeyPress,
76 68
             tabIndex: 0,
77
-            role: 'button'
69
+            role: showLabel ? 'menuitem' : 'button'
78 70
         };
79 71
 
80 72
         const elementType = showLabel ? 'li' : 'div';

+ 34
- 1
react/features/base/toolbox/components/web/OverflowMenuItem.js View File

@@ -74,6 +74,35 @@ class OverflowMenuItem extends Component<Props> {
74 74
         disabled: false
75 75
     };
76 76
 
77
+    /**
78
+     * Initializes a new {@code OverflowMenuItem} instance.
79
+     *
80
+     * @param {*} props - The read-only properties with which the new instance
81
+     * is to be initialized.
82
+     */
83
+    constructor(props: Props) {
84
+        super(props);
85
+
86
+        // Bind event handler so it is only bound once for every instance.
87
+        this._onKeyPress = this._onKeyPress.bind(this);
88
+    }
89
+
90
+    _onKeyPress: (Object) => void;
91
+
92
+    /**
93
+     * KeyPress handler for accessibility.
94
+     *
95
+     * @param {Object} e - The key event to handle.
96
+     *
97
+     * @returns {void}
98
+     */
99
+    _onKeyPress(e) {
100
+        if (!this.props.disabled && this.props.onClick && (e.key === ' ' || e.key === 'Enter')) {
101
+            e.preventDefault();
102
+            this.props.onClick();
103
+        }
104
+    }
105
+
77 106
     /**
78 107
      * Implements React's {@link Component#render()}.
79 108
      *
@@ -89,9 +118,13 @@ class OverflowMenuItem extends Component<Props> {
89 118
 
90 119
         return (
91 120
             <li
121
+                aria-disabled = { disabled }
92 122
                 aria-label = { accessibilityLabel }
93 123
                 className = { className }
94
-                onClick = { disabled ? null : onClick }>
124
+                onClick = { disabled ? null : onClick }
125
+                onKeyPress = { this._onKeyPress }
126
+                role = 'menuitem'
127
+                tabIndex = { 0 }>
95 128
                 <span className = 'overflow-menu-item-icon'>
96 129
                     <Icon
97 130
                         id = { iconId }

+ 45
- 1
react/features/base/toolbox/components/web/ToolboxButtonWithIcon.js View File

@@ -36,6 +36,36 @@ type Props = {
36 36
      * Additional styles.
37 37
      */
38 38
     styles?: Object,
39
+
40
+    /**
41
+     * aria label for the Icon.
42
+     */
43
+    ariaLabel?: string,
44
+
45
+    /**
46
+     * whether the element has a popup
47
+     */
48
+    ariaHasPopup?: boolean,
49
+
50
+    /**
51
+     * whether the element popup is expanded
52
+     */
53
+    ariaExpanded?: boolean,
54
+
55
+    /**
56
+     * The id of the element this button icon controls
57
+     */
58
+    ariaControls?: string,
59
+
60
+    /**
61
+     * keydown handler for icon.
62
+     */
63
+    onIconKeyDown?: Function,
64
+
65
+    /**
66
+     * The ID of the icon button
67
+     */
68
+    iconId: string
39 69
 };
40 70
 
41 71
 /**
@@ -51,7 +81,13 @@ export default function ToolboxButtonWithIcon(props: Props) {
51 81
         iconDisabled,
52 82
         iconTooltip,
53 83
         onIconClick,
54
-        styles
84
+        onIconKeyDown,
85
+        styles,
86
+        ariaLabel,
87
+        ariaHasPopup,
88
+        ariaControls,
89
+        ariaExpanded,
90
+        iconId
55 91
     } = props;
56 92
 
57 93
     const iconProps = {};
@@ -62,6 +98,12 @@ export default function ToolboxButtonWithIcon(props: Props) {
62 98
     } else {
63 99
         iconProps.className = 'settings-button-small-icon';
64 100
         iconProps.onClick = onIconClick;
101
+        iconProps.onKeyDown = onIconKeyDown;
102
+        iconProps.role = 'button';
103
+        iconProps.tabIndex = 0;
104
+        iconProps.ariaControls = ariaControls;
105
+        iconProps.ariaExpanded = ariaExpanded;
106
+        iconProps.containerId = iconId;
65 107
     }
66 108
 
67 109
 
@@ -77,6 +119,8 @@ export default function ToolboxButtonWithIcon(props: Props) {
77 119
                     position = 'top'>
78 120
                     <Icon
79 121
                         { ...iconProps }
122
+                        ariaHasPopup = { ariaHasPopup }
123
+                        ariaLabel = { ariaLabel }
80 124
                         size = { 9 }
81 125
                         src = { icon } />
82 126
                 </Tooltip>

+ 7
- 15
react/features/base/toolbox/components/web/ToolboxItem.js View File

@@ -19,29 +19,21 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
19 19
     constructor(props: Props) {
20 20
         super(props);
21 21
 
22
-        this._onKeyDown = this._onKeyDown.bind(this);
22
+        this._onKeyPress = this._onKeyPress.bind(this);
23 23
     }
24 24
 
25
-    _onKeyDown: (Object) => void;
25
+    _onKeyPress: (Object) => void;
26 26
 
27 27
     /**
28
-     * Handles 'Enter' key on the button to trigger onClick for accessibility.
29
-     * We should be handling Space onKeyUp but it conflicts with PTT.
28
+     * Handles 'Enter' and Space key on the button to trigger onClick for accessibility.
30 29
      *
31 30
      * @param {Object} event - The key event.
32 31
      * @private
33 32
      * @returns {void}
34 33
      */
35
-    _onKeyDown(event) {
36
-        // If the event coming to the dialog has been subject to preventDefault
37
-        // we don't handle it here.
38
-        if (event.defaultPrevented) {
39
-            return;
40
-        }
41
-
42
-        if (event.key === 'Enter') {
34
+    _onKeyPress(event) {
35
+        if (event.key === 'Enter' || event.key === ' ') {
43 36
             event.preventDefault();
44
-            event.stopPropagation();
45 37
             this.props.onClick();
46 38
         }
47 39
     }
@@ -71,9 +63,9 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
71 63
             'aria-label': this.accessibilityLabel,
72 64
             className: className + (disabled ? ' disabled' : ''),
73 65
             onClick: disabled ? undefined : onClick,
74
-            onKeyDown: this._onKeyDown,
66
+            onKeyPress: this._onKeyPress,
75 67
             tabIndex: 0,
76
-            role: 'button'
68
+            role: showLabel ? 'menuitem' : 'button'
77 69
         };
78 70
 
79 71
         const elementType = showLabel ? 'li' : 'div';

+ 20
- 1
react/features/calendar-sync/components/AddMeetingUrlButton.web.js View File

@@ -55,6 +55,7 @@ class AddMeetingUrlButton extends Component<Props> {
55 55
 
56 56
         // Bind event handler so it is only bound once for every instance.
57 57
         this._onClick = this._onClick.bind(this);
58
+        this._onKeyPress = this._onKeyPress.bind(this);
58 59
     }
59 60
 
60 61
     /**
@@ -67,7 +68,9 @@ class AddMeetingUrlButton extends Component<Props> {
67 68
             <Tooltip content = { this.props.t('calendarSync.addMeetingURL') }>
68 69
                 <div
69 70
                     className = 'button add-button'
70
-                    onClick = { this._onClick }>
71
+                    onClick = { this._onClick }
72
+                    onKeyPress = { this._onKeyPress }
73
+                    role = 'button'>
71 74
                     <Icon src = { IconAdd } />
72 75
                 </div>
73 76
             </Tooltip>
@@ -88,6 +91,22 @@ class AddMeetingUrlButton extends Component<Props> {
88 91
 
89 92
         dispatch(updateCalendarEvent(eventId, calendarId));
90 93
     }
94
+
95
+    _onKeyPress: (Object) => void;
96
+
97
+    /**
98
+     * KeyPress handler for accessibility.
99
+     *
100
+     * @param {Object} e - The key event to handle.
101
+     *
102
+     * @returns {void}
103
+     */
104
+    _onKeyPress(e) {
105
+        if (e.key === ' ' || e.key === 'Enter') {
106
+            e.preventDefault();
107
+            this._onClick();
108
+        }
109
+    }
91 110
 }
92 111
 
93 112
 export default translate(connect()(AddMeetingUrlButton));

+ 23
- 2
react/features/calendar-sync/components/CalendarList.web.js View File

@@ -72,6 +72,7 @@ class CalendarList extends AbstractPage<Props> {
72 72
         this._getRenderListEmptyComponent
73 73
             = this._getRenderListEmptyComponent.bind(this);
74 74
         this._onOpenSettings = this._onOpenSettings.bind(this);
75
+        this._onKeyPressOpenSettings = this._onKeyPressOpenSettings.bind(this);
75 76
         this._onRefreshEvents = this._onRefreshEvents.bind(this);
76 77
     }
77 78
 
@@ -187,7 +188,9 @@ class CalendarList extends AbstractPage<Props> {
187 188
         return (
188 189
             <div className = 'meetings-list-empty'>
189 190
                 <div className = 'meetings-list-empty-image'>
190
-                    <img src = './images/calendar.svg' />
191
+                    <img
192
+                        alt = { t('welcomepage.logo.calendar') }
193
+                        src = './images/calendar.svg' />
191 194
                 </div>
192 195
                 <div className = 'description'>
193 196
                     { t('welcomepage.connectCalendarText', {
@@ -197,7 +200,9 @@ class CalendarList extends AbstractPage<Props> {
197 200
                 </div>
198 201
                 <div
199 202
                     className = 'meetings-list-empty-button'
200
-                    onClick = { this._onOpenSettings }>
203
+                    onClick = { this._onOpenSettings }
204
+                    onKeyPress = { this._onKeyPressOpenSettings }
205
+                    role = 'button'>
201 206
                     <Icon
202 207
                         className = 'meetings-list-empty-icon'
203 208
                         src = { IconPlusCalendar } />
@@ -221,6 +226,22 @@ class CalendarList extends AbstractPage<Props> {
221 226
         this.props.dispatch(openSettingsDialog(SETTINGS_TABS.CALENDAR));
222 227
     }
223 228
 
229
+    _onKeyPressOpenSettings: (Object) => void;
230
+
231
+    /**
232
+     * KeyPress handler for accessibility.
233
+     *
234
+     * @param {Object} e - The key event to handle.
235
+     *
236
+     * @returns {void}
237
+     */
238
+    _onKeyPressOpenSettings(e) {
239
+        if (e.key === ' ' || e.key === 'Enter') {
240
+            e.preventDefault();
241
+            this._onOpenSettings();
242
+        }
243
+    }
244
+
224 245
     _onRefreshEvents: () => void;
225 246
 
226 247
 

+ 20
- 1
react/features/calendar-sync/components/JoinButton.web.js View File

@@ -45,6 +45,7 @@ class JoinButton extends Component<Props> {
45 45
 
46 46
         // Bind event handler so it is only bound once for every instance.
47 47
         this._onClick = this._onClick.bind(this);
48
+        this._onKeyPress = this._onKeyPress.bind(this);
48 49
     }
49 50
 
50 51
     /**
@@ -60,7 +61,9 @@ class JoinButton extends Component<Props> {
60 61
                 content = { t('calendarSync.joinTooltip') }>
61 62
                 <div
62 63
                     className = 'button join-button'
63
-                    onClick = { this._onClick }>
64
+                    onClick = { this._onClick }
65
+                    onKeyPress = { this._onKeyPress }
66
+                    role = 'button'>
64 67
                     <Icon
65 68
                         size = '14'
66 69
                         src = { IconAdd } />
@@ -81,6 +84,22 @@ class JoinButton extends Component<Props> {
81 84
     _onClick(event) {
82 85
         this.props.onPress(event, this.props.url);
83 86
     }
87
+
88
+    _onKeyPress: (Object) => void;
89
+
90
+    /**
91
+     * KeyPress handler for accessibility.
92
+     *
93
+     * @param {Object} e - The key event to handle.
94
+     *
95
+     * @returns {void}
96
+     */
97
+    _onKeyPress(e) {
98
+        if (e.key === ' ' || e.key === 'Enter') {
99
+            e.preventDefault();
100
+            this._onClick();
101
+        }
102
+    }
84 103
 }
85 104
 
86 105
 export default translate(JoinButton);

+ 19
- 4
react/features/calendar-sync/components/MicrosoftSignInButton.web.js View File

@@ -2,17 +2,29 @@
2 2
 
3 3
 import React, { Component } from 'react';
4 4
 
5
+import { translate } from '../../base/i18n';
6
+
7
+
5 8
 /**
6 9
  * The type of the React {@code Component} props of
7 10
  * {@link MicrosoftSignInButton}.
8 11
  */
9 12
 type Props = {
10 13
 
11
-    // The callback to invoke when {@code MicrosoftSignInButton} is clicked.
14
+    /**
15
+     * The callback to invoke when {@code MicrosoftSignInButton} is clicked.
16
+     */
12 17
     onClick: Function,
13 18
 
14
-    // The text to display within {@code MicrosoftSignInButton}.
15
-    text: string
19
+    /**
20
+     * The text to display within {@code MicrosoftSignInButton}.
21
+     */
22
+    text: string,
23
+
24
+    /**
25
+     * Invoked to obtain translated strings.
26
+     */
27
+    t: Function
16 28
 };
17 29
 
18 30
 /**
@@ -20,7 +32,7 @@ type Props = {
20 32
  *
21 33
  * @extends Component
22 34
  */
23
-export default class MicrosoftSignInButton extends Component<Props> {
35
+class MicrosoftSignInButton extends Component<Props> {
24 36
     /**
25 37
      * Implements React's {@link Component#render()}.
26 38
      *
@@ -33,6 +45,7 @@ export default class MicrosoftSignInButton extends Component<Props> {
33 45
                 className = 'microsoft-sign-in'
34 46
                 onClick = { this.props.onClick }>
35 47
                 <img
48
+                    alt = { this.props.t('welcomepage.logo.microsoftLogo') }
36 49
                     className = 'microsoft-logo'
37 50
                     src = 'images/microsoftLogo.svg' />
38 51
                 <div className = 'microsoft-cta'>
@@ -42,3 +55,5 @@ export default class MicrosoftSignInButton extends Component<Props> {
42 55
         );
43 56
     }
44 57
 }
58
+
59
+export default translate(MicrosoftSignInButton);

+ 37
- 2
react/features/chat/components/web/Chat.js View File

@@ -4,6 +4,7 @@ import React from 'react';
4 4
 
5 5
 import { translate } from '../../../base/i18n';
6 6
 import { connect } from '../../../base/redux';
7
+import { toggleChat } from '../../actions.web';
7 8
 import AbstractChat, {
8 9
     _mapStateToProps,
9 10
     type Props
@@ -51,6 +52,8 @@ class Chat extends AbstractChat<Props> {
51 52
         // Bind event handlers so they are only bound once for every instance.
52 53
         this._renderPanelContent = this._renderPanelContent.bind(this);
53 54
         this._onChatInputResize = this._onChatInputResize.bind(this);
55
+        this._onEscClick = this._onEscClick.bind(this);
56
+        this._onToggleChat = this._onToggleChat.bind(this);
54 57
     }
55 58
 
56 59
     /**
@@ -74,6 +77,21 @@ class Chat extends AbstractChat<Props> {
74 77
             this._scrollMessageContainerToBottom(false);
75 78
         }
76 79
     }
80
+    _onEscClick: (KeyboardEvent) => void;
81
+
82
+    /**
83
+     * Click handler for the chat sidenav.
84
+     *
85
+     * @param {KeyboardEvent} event - Esc key click to close the popup.
86
+     * @returns {void}
87
+     */
88
+    _onEscClick(event) {
89
+        if (event.key === 'Escape' && this.props._isOpen) {
90
+            event.preventDefault();
91
+            event.stopPropagation();
92
+            this._onToggleChat();
93
+        }
94
+    }
77 95
 
78 96
     /**
79 97
      * Implements React's {@link Component#render()}.
@@ -135,7 +153,10 @@ class Chat extends AbstractChat<Props> {
135 153
      */
136 154
     _renderChatHeader() {
137 155
         return (
138
-            <Header className = 'chat-header' />
156
+            <Header
157
+                className = 'chat-header'
158
+                id = 'chat-header'
159
+                onCancel = { this._onToggleChat } />
139 160
         );
140 161
     }
141 162
 
@@ -177,8 +198,10 @@ class Chat extends AbstractChat<Props> {
177 198
 
178 199
         return (
179 200
             <div
201
+                aria-haspopup = 'true'
180 202
                 className = { `sideToolbarContainer ${className}` }
181
-                id = 'sideToolbarContainer'>
203
+                id = 'sideToolbarContainer'
204
+                onKeyDown = { this._onEscClick } >
182 205
                 { ComponentToRender }
183 206
             </div>
184 207
         );
@@ -199,6 +222,18 @@ class Chat extends AbstractChat<Props> {
199 222
     }
200 223
 
201 224
     _onSendMessage: (string) => void;
225
+
226
+    _onToggleChat: () => void;
227
+
228
+    /**
229
+    * Toggles the chat window.
230
+    *
231
+    * @returns {Function}
232
+    */
233
+    _onToggleChat() {
234
+        this.props.dispatch(toggleChat());
235
+    }
236
+
202 237
 }
203 238
 
204 239
 export default translate(connect(_mapStateToProps)(Chat));

+ 25
- 5
react/features/chat/components/web/ChatDialogHeader.js View File

@@ -1,7 +1,8 @@
1 1
 // @flow
2 2
 
3
-import React from 'react';
3
+import React, { useCallback } from 'react';
4 4
 
5
+import { translate } from '../../../base/i18n';
5 6
 import { Icon, IconClose } from '../../../base/icons';
6 7
 import { connect } from '../../../base/redux';
7 8
 import { toggleChat } from '../../actions.web';
@@ -17,6 +18,11 @@ type Props = {
17 18
      * An optional class name.
18 19
      */
19 20
     className: string,
21
+
22
+    /**
23
+     * Invoked to obtain translated strings.
24
+     */
25
+    t: Function
20 26
 };
21 27
 
22 28
 /**
@@ -24,17 +30,31 @@ type Props = {
24 30
  *
25 31
  * @returns {React$Element<any>}
26 32
  */
27
-function Header({ onCancel, className }: Props) {
33
+function Header({ onCancel, className, t }: Props) {
34
+
35
+    const onKeyPressHandler = useCallback(e => {
36
+        if (onCancel && (e.key === ' ' || e.key === 'Enter')) {
37
+            e.preventDefault();
38
+            onCancel();
39
+        }
40
+    }, [ onCancel ]);
41
+
28 42
     return (
29 43
         <div
30
-            className = { className || 'chat-dialog-header' }>
44
+            className = { className || 'chat-dialog-header' }
45
+            role = 'heading'>
46
+            { t('chat.title') }
31 47
             <Icon
48
+                ariaLabel = { t('toolbar.closeChat') }
32 49
                 onClick = { onCancel }
33
-                src = { IconClose } />
50
+                onKeyPress = { onKeyPressHandler }
51
+                role = 'button'
52
+                src = { IconClose }
53
+                tabIndex = { 0 } />
34 54
         </div>
35 55
     );
36 56
 }
37 57
 
38 58
 const mapDispatchToProps = { onCancel: toggleChat };
39 59
 
40
-export default connect(null, mapDispatchToProps)(Header);
60
+export default translate(connect(null, mapDispatchToProps)(Header));

+ 86
- 10
react/features/chat/components/web/ChatInput.js View File

@@ -84,6 +84,9 @@ class ChatInput extends Component<Props, State> {
84 84
         this._onSmileySelect = this._onSmileySelect.bind(this);
85 85
         this._onSubmitMessage = this._onSubmitMessage.bind(this);
86 86
         this._onToggleSmileysPanel = this._onToggleSmileysPanel.bind(this);
87
+        this._onEscHandler = this._onEscHandler.bind(this);
88
+        this._onToggleSmileysPanelKeyPress = this._onToggleSmileysPanelKeyPress.bind(this);
89
+        this._onSubmitMessageKeyPress = this._onSubmitMessageKeyPress.bind(this);
87 90
         this._setTextAreaRef = this._setTextAreaRef.bind(this);
88 91
     }
89 92
 
@@ -116,8 +119,15 @@ class ChatInput extends Component<Props, State> {
116 119
                         <div id = 'smileysarea'>
117 120
                             <div id = 'smileys'>
118 121
                                 <div
122
+                                    aria-expanded = { this.state.showSmileysPanel }
123
+                                    aria-haspopup = 'smileysContainer'
124
+                                    aria-label = { this.props.t('chat.smileysPanel') }
119 125
                                     className = 'smiley-button'
120
-                                    onClick = { this._onToggleSmileysPanel }>
126
+                                    onClick = { this._onToggleSmileysPanel }
127
+                                    onKeyDown = { this._onEscHandler }
128
+                                    onKeyPress = { this._onToggleSmileysPanelKeyPress }
129
+                                    role = 'button'
130
+                                    tabIndex = { 0 }>
121 131
                                     <Icon src = { IconSmile } />
122 132
                                 </div>
123 133
                             </div>
@@ -129,19 +139,26 @@ class ChatInput extends Component<Props, State> {
129 139
                     </div>
130 140
                     <div className = 'usrmsg-form'>
131 141
                         <TextareaAutosize
142
+                            autoComplete = 'off'
143
+                            autoFocus = { true }
132 144
                             id = 'usermsg'
133
-                            inputRef = { this._setTextAreaRef }
134 145
                             maxRows = { 5 }
135 146
                             onChange = { this._onMessageChange }
136 147
                             onHeightChange = { this.props.onResize }
137 148
                             onKeyDown = { this._onDetectSubmit }
138 149
                             placeholder = { this.props.t('chat.messagebox') }
150
+                            ref = { this._setTextAreaRef }
151
+                            tabIndex = { 0 }
139 152
                             value = { this.state.message } />
140 153
                     </div>
141 154
                     <div className = 'send-button-container'>
142 155
                         <div
156
+                            aria-label = { this.props.t('chat.sendButton') }
143 157
                             className = 'send-button'
144
-                            onClick = { this._onSubmitMessage }>
158
+                            onClick = { this._onSubmitMessage }
159
+                            onKeyPress = { this._onSubmitMessageKeyPress }
160
+                            role = 'button'
161
+                            tabIndex = { this.state.message.trim() ? 0 : -1 } >
145 162
                             <Icon src = { IconPlane } />
146 163
                         </div>
147 164
                     </div>
@@ -192,14 +209,32 @@ class ChatInput extends Component<Props, State> {
192 209
      * @returns {void}
193 210
      */
194 211
     _onDetectSubmit(event) {
195
-        if (event.keyCode === 13
196
-            && event.shiftKey === false) {
212
+        if (event.key === 'Enter'
213
+            && event.shiftKey === false
214
+            && event.ctrlKey === false) {
197 215
             event.preventDefault();
216
+            event.stopPropagation();
198 217
 
199 218
             this._onSubmitMessage();
200 219
         }
201 220
     }
202 221
 
222
+    _onSubmitMessageKeyPress: (Object) => void;
223
+
224
+    /**
225
+     * KeyPress handler for accessibility.
226
+     *
227
+     * @param {Object} e - The key event to handle.
228
+     *
229
+     * @returns {void}
230
+     */
231
+    _onSubmitMessageKeyPress(e) {
232
+        if (e.key === ' ' || e.key === 'Enter') {
233
+            e.preventDefault();
234
+            this._onSubmitMessage();
235
+        }
236
+    }
237
+
203 238
     _onMessageChange: (Object) => void;
204 239
 
205 240
     /**
@@ -224,10 +259,16 @@ class ChatInput extends Component<Props, State> {
224 259
      * @returns {void}
225 260
      */
226 261
     _onSmileySelect(smileyText) {
227
-        this.setState({
228
-            message: `${this.state.message} ${smileyText}`,
229
-            showSmileysPanel: false
230
-        });
262
+        if (smileyText) {
263
+            this.setState({
264
+                message: `${this.state.message} ${smileyText}`,
265
+                showSmileysPanel: false
266
+            });
267
+        } else {
268
+            this.setState({
269
+                showSmileysPanel: false
270
+            });
271
+        }
231 272
 
232 273
         this._focus();
233 274
     }
@@ -241,9 +282,44 @@ class ChatInput extends Component<Props, State> {
241 282
      * @returns {void}
242 283
      */
243 284
     _onToggleSmileysPanel() {
285
+        if (this.state.showSmileysPanel) {
286
+            this._focus();
287
+        }
244 288
         this.setState({ showSmileysPanel: !this.state.showSmileysPanel });
289
+    }
245 290
 
246
-        this._focus();
291
+    _onEscHandler: (Object) => void;
292
+
293
+    /**
294
+     * KeyPress handler for accessibility.
295
+     *
296
+     * @param {Object} e - The key event to handle.
297
+     *
298
+     * @returns {void}
299
+     */
300
+    _onEscHandler(e) {
301
+        // Escape handling does not work in onKeyPress
302
+        if (this.state.showSmileysPanel && e.key === 'Escape') {
303
+            e.preventDefault();
304
+            e.stopPropagation();
305
+            this._onToggleSmileysPanel();
306
+        }
307
+    }
308
+
309
+    _onToggleSmileysPanelKeyPress: (Object) => void;
310
+
311
+    /**
312
+     * KeyPress handler for accessibility.
313
+     *
314
+     * @param {Object} e - The key event to handle.
315
+     *
316
+     * @returns {void}
317
+     */
318
+    _onToggleSmileysPanelKeyPress(e) {
319
+        if (e.key === ' ' || e.key === 'Enter') {
320
+            e.preventDefault();
321
+            this._onToggleSmileysPanel();
322
+        }
247 323
     }
248 324
 
249 325
     _setTextAreaRef: (?HTMLTextAreaElement) => void;

+ 13
- 3
react/features/chat/components/web/ChatMessage.js View File

@@ -23,7 +23,7 @@ class ChatMessage extends AbstractChatMessage<Props> {
23 23
      * @returns {ReactElement}
24 24
      */
25 25
     render() {
26
-        const { message } = this.props;
26
+        const { message, t } = this.props;
27 27
         const processedMessage = [];
28 28
 
29 29
         // content is an array of text and emoji components
@@ -38,12 +38,20 @@ class ChatMessage extends AbstractChatMessage<Props> {
38 38
         });
39 39
 
40 40
         return (
41
-            <div className = 'chatmessage-wrapper'>
41
+            <div
42
+                className = 'chatmessage-wrapper'
43
+                tabIndex = { -1 }>
42 44
                 <div className = { `chatmessage ${message.privateMessage ? 'privatemessage' : ''}` }>
43 45
                     <div className = 'replywrapper'>
44 46
                         <div className = 'messagecontent'>
45 47
                             { this.props.showDisplayName && this._renderDisplayName() }
46 48
                             <div className = 'usermessage'>
49
+                                <span className = 'sr-only'>
50
+                                    { this.props.message.displayName === this.props.message.recipient
51
+                                        ? t('chat.messageAccessibleTitleMe')
52
+                                        : t('chat.messageAccessibleTitle',
53
+                                        { user: this.props.message.displayName }) }
54
+                                </span>
47 55
                                 { processedMessage }
48 56
                             </div>
49 57
                             { message.privateMessage && this._renderPrivateNotice() }
@@ -77,7 +85,9 @@ class ChatMessage extends AbstractChatMessage<Props> {
77 85
      */
78 86
     _renderDisplayName() {
79 87
         return (
80
-            <div className = 'display-name'>
88
+            <div
89
+                aria-hidden = { true }
90
+                className = 'display-name'>
81 91
                 { this.props.message.displayName }
82 92
             </div>
83 93
         );

+ 22
- 1
react/features/chat/components/web/DisplayNameForm.js View File

@@ -59,6 +59,7 @@ class DisplayNameForm extends Component<Props, State> {
59 59
         // Bind event handlers so they are only bound once for every instance.
60 60
         this._onDisplayNameChange = this._onDisplayNameChange.bind(this);
61 61
         this._onSubmit = this._onSubmit.bind(this);
62
+        this._onKeyPress = this._onKeyPress.bind(this);
62 63
     }
63 64
 
64 65
     /**
@@ -74,6 +75,8 @@ class DisplayNameForm extends Component<Props, State> {
74 75
             <div id = 'nickname'>
75 76
                 <form onSubmit = { this._onSubmit }>
76 77
                     <FieldTextStateless
78
+                        aria-describedby = 'nickname-title'
79
+                        autoComplete = 'name'
77 80
                         autoFocus = { true }
78 81
                         compact = { true }
79 82
                         id = 'nickinput'
@@ -86,7 +89,10 @@ class DisplayNameForm extends Component<Props, State> {
86 89
                 </form>
87 90
                 <div
88 91
                     className = { `enter-chat${this.state.displayName.trim() ? '' : ' disabled'}` }
89
-                    onClick = { this._onSubmit }>
92
+                    onClick = { this._onSubmit }
93
+                    onKeyPress = { this._onKeyPress }
94
+                    role = 'button'
95
+                    tabIndex = { 0 }>
90 96
                     { t('chat.enter') }
91 97
                 </div>
92 98
                 <KeyboardAvoider />
@@ -125,6 +131,21 @@ class DisplayNameForm extends Component<Props, State> {
125 131
             displayName: this.state.displayName
126 132
         }));
127 133
     }
134
+
135
+    _onKeyPress: (Object) => void;
136
+
137
+    /**
138
+     * KeyPress handler for accessibility.
139
+     *
140
+     * @param {Object} e - The key event to handle.
141
+     *
142
+     * @returns {void}
143
+     */
144
+    _onKeyPress(e) {
145
+        if (e.key === ' ' || e.key === 'Enter') {
146
+            this._onSubmit(e);
147
+        }
148
+    }
128 149
 }
129 150
 
130 151
 export default translate(connect()(DisplayNameForm));

+ 4
- 1
react/features/chat/components/web/MessageContainer.js View File

@@ -70,9 +70,12 @@ export default class MessageContainer extends AbstractMessageContainer<Props> {
70 70
 
71 71
         return (
72 72
             <div
73
+                aria-labelledby = 'chat-header'
73 74
                 id = 'chatconversation'
74 75
                 onScroll = { this._onChatScroll }
75
-                ref = { this._messageListRef }>
76
+                ref = { this._messageListRef }
77
+                role = 'log'
78
+                tabIndex = { 0 }>
76 79
                 { messages }
77 80
                 <div ref = { this._messagesListEndRef } />
78 81
             </div>

+ 38
- 2
react/features/chat/components/web/MessageRecipient.js View File

@@ -15,6 +15,35 @@ import AbstractMessageRecipient, {
15 15
  * Class to implement the displaying of the recipient of the next message.
16 16
  */
17 17
 class MessageRecipient extends AbstractMessageRecipient<Props> {
18
+    /**
19
+     * Initializes a new {@code MessageRecipient} instance.
20
+     *
21
+     * @param {*} props - The read-only properties with which the new instance
22
+     * is to be initialized.
23
+     */
24
+    constructor(props) {
25
+        super(props);
26
+
27
+        // Bind event handler so it is only bound once for every instance.
28
+        this._onKeyPress = this._onKeyPress.bind(this);
29
+    }
30
+
31
+    _onKeyPress: (Object) => void;
32
+
33
+    /**
34
+     * KeyPress handler for accessibility.
35
+     *
36
+     * @param {Object} e - The key event to handle.
37
+     *
38
+     * @returns {void}
39
+     */
40
+    _onKeyPress(e) {
41
+        if (this.props._onRemovePrivateMessageRecipient && (e.key === ' ' || e.key === 'Enter')) {
42
+            e.preventDefault();
43
+            this.props._onRemovePrivateMessageRecipient();
44
+        }
45
+    }
46
+
18 47
     /**
19 48
      * Implements {@code PureComponent#render}.
20 49
      *
@@ -30,13 +59,20 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
30 59
         const { t } = this.props;
31 60
 
32 61
         return (
33
-            <div id = 'chat-recipient'>
62
+            <div
63
+                id = 'chat-recipient'
64
+                role = 'alert'>
34 65
                 <span>
35 66
                     { t('chat.messageTo', {
36 67
                         recipient: _privateMessageRecipient
37 68
                     }) }
38 69
                 </span>
39
-                <div onClick = { this.props._onRemovePrivateMessageRecipient }>
70
+                <div
71
+                    aria-label = { t('dialog.close') }
72
+                    onClick = { this.props._onRemovePrivateMessageRecipient }
73
+                    onKeyPress = { this._onKeyPress }
74
+                    role = 'button'
75
+                    tabIndex = { 0 }>
40 76
                     <Icon
41 77
                         src = { IconCancelSelection } />
42 78
                 </div>

+ 84
- 28
react/features/chat/components/web/SmileysPanel.js View File

@@ -23,6 +23,69 @@ type Props = {
23 23
  * @extends Component
24 24
  */
25 25
 class SmileysPanel extends PureComponent<Props> {
26
+    /**
27
+     * Initializes a new {@code SmileysPanel} instance.
28
+     *
29
+     * @param {*} props - The read-only properties with which the new instance
30
+     * is to be initialized.
31
+     */
32
+    constructor(props: Props) {
33
+        super(props);
34
+
35
+        // Bind event handler so it is only bound once for every instance.
36
+        this._onClick = this._onClick.bind(this);
37
+        this._onKeyPress = this._onKeyPress.bind(this);
38
+        this._onEscKey = this._onEscKey.bind(this);
39
+    }
40
+
41
+    _onEscKey: (Object) => void;
42
+
43
+    /**
44
+     * KeyPress handler for accessibility.
45
+     *
46
+     * @param {Object} e - The key event to handle.
47
+     *
48
+     * @returns {void}
49
+     */
50
+    _onEscKey(e) {
51
+        // Escape handling does not work in onKeyPress
52
+        if (e.key === 'Escape') {
53
+            e.preventDefault();
54
+            e.stopPropagation();
55
+            this.props.onSmileySelect();
56
+        }
57
+    }
58
+
59
+    _onKeyPress: (Object) => void;
60
+
61
+    /**
62
+     * KeyPress handler for accessibility.
63
+     *
64
+     * @param {Object} e - The key event to handle.
65
+     *
66
+     * @returns {void}
67
+     */
68
+    _onKeyPress(e) {
69
+        if (e.key === ' ') {
70
+            e.preventDefault();
71
+            this.props.onSmileySelect(e.target.id && smileys[e.target.id]);
72
+        }
73
+    }
74
+
75
+    _onClick: (Object) => void;
76
+
77
+    /**
78
+     * Click handler for to select emoji.
79
+     *
80
+     * @param {Object} e - The key event to handle.
81
+     *
82
+     * @returns {void}
83
+     */
84
+    _onClick(e) {
85
+        e.preventDefault();
86
+        this.props.onSmileySelect(e.currentTarget.id && smileys[e.currentTarget.id]);
87
+    }
88
+
26 89
     /**
27 90
      * Implements React's {@link Component#render()}.
28 91
      *
@@ -30,40 +93,33 @@ class SmileysPanel extends PureComponent<Props> {
30 93
      * @returns {ReactElement}
31 94
      */
32 95
     render() {
33
-        const smileyItems = Object.keys(smileys).map(smileyKey => {
34
-            const onSelectFunction = this._getOnSmileySelectCallback(smileyKey);
35
-
36
-            return (
37
-                <div
38
-                    className = 'smileyContainer'
39
-                    id = { smileyKey }
40
-                    key = { smileyKey }>
41
-                    <Emoji
42
-                        onClick = { onSelectFunction }
43
-                        onlyEmojiClassName = 'smiley'
44
-                        text = { smileys[smileyKey] } />
45
-                </div>
46
-            );
47
-        });
96
+        const smileyItems = Object.keys(smileys).map(smileyKey => (
97
+            <div
98
+                className = 'smileyContainer'
99
+                id = { smileyKey }
100
+                key = { smileyKey }
101
+                onClick = { this._onClick }
102
+                onKeyDown = { this._onEscKey }
103
+                onKeyPress = { this._onKeyPress }
104
+                role = 'option'
105
+                tabIndex = { 0 }>
106
+                <Emoji
107
+                    onlyEmojiClassName = 'smiley'
108
+                    text = { smileys[smileyKey] } />
109
+            </div>
110
+        ));
48 111
 
49 112
         return (
50
-            <div id = 'smileysContainer'>
113
+            <div
114
+                aria-orientation = 'horizontal'
115
+                id = 'smileysContainer'
116
+                onKeyDown = { this._onEscKey }
117
+                role = 'listbox'
118
+                tabIndex = { -1 }>
51 119
                 { smileyItems }
52 120
             </div>
53 121
         );
54 122
     }
55
-
56
-    /**
57
-     * Helper method to bind a smiley's click handler.
58
-     *
59
-     * @param {string} smileyKey - The key from the {@link smileys} object
60
-     * that should be added to the chat message.
61
-     * @private
62
-     * @returns {Function}
63
-     */
64
-    _getOnSmileySelectCallback(smileyKey) {
65
-        return () => this.props.onSmileySelect(smileys[smileyKey]);
66
-    }
67 123
 }
68 124
 
69 125
 export default SmileysPanel;

+ 59
- 8
react/features/chrome-extension-banner/components/ChromeExtensionBanner.web.js View File

@@ -107,6 +107,8 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
107 107
         this._onInstallExtensionClick = this._onInstallExtensionClick.bind(this);
108 108
         this._shouldNotRender = this._shouldNotRender.bind(this);
109 109
         this._onDontShowAgainChange = this._onDontShowAgainChange.bind(this);
110
+        this._onCloseKeyPress = this._onCloseKeyPress.bind(this);
111
+        this._onInstallExtensionKeyPress = this._onInstallExtensionKeyPress.bind(this);
110 112
     }
111 113
 
112 114
     /**
@@ -169,6 +171,22 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
169 171
         this.setState({ closePressed: true });
170 172
     }
171 173
 
174
+    _onCloseKeyPress: (Object) => void;
175
+
176
+    /**
177
+     * KeyPress handler for accessibility.
178
+     *
179
+     * @param {Object} e - The key event to handle.
180
+     *
181
+     * @returns {void}
182
+     */
183
+    _onCloseKeyPress(e) {
184
+        if (e.key === ' ' || e.key === 'Enter') {
185
+            e.preventDefault();
186
+            this._onClosePressed();
187
+        }
188
+    }
189
+
172 190
     _onInstallExtensionClick: () => void;
173 191
 
174 192
     /**
@@ -182,6 +200,22 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
182 200
         this.setState({ closePressed: true });
183 201
     }
184 202
 
203
+    _onInstallExtensionKeyPress: (Object) => void;
204
+
205
+    /**
206
+     * KeyPress handler for accessibility.
207
+     *
208
+     * @param {Object} e - The key event to handle.
209
+     *
210
+     * @returns {void}
211
+     */
212
+    _onInstallExtensionKeyPress(e) {
213
+        if (e.key === ' ' || e.key === 'Enter') {
214
+            e.preventDefault();
215
+            this._onClosePressed();
216
+        }
217
+    }
218
+
185 219
     _shouldNotRender: () => boolean;
186 220
 
187 221
     /**
@@ -236,16 +270,23 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
236 270
 
237 271
         return (
238 272
             <div className = { mainClassNames }>
239
-                <div className = 'chrome-extension-banner__container'>
240
-                    <div
241
-                        className = 'chrome-extension-banner__icon-container' />
273
+                <div
274
+                    aria-aria-describedby = 'chrome-extension-banner__text-container'
275
+                    className = 'chrome-extension-banner__container'
276
+                    role = 'banner'>
277
+                    <div className = 'chrome-extension-banner__icon-container' />
242 278
                     <div
243
-                        className = 'chrome-extension-banner__text-container'>
279
+                        className = 'chrome-extension-banner__text-container'
280
+                        id = 'chrome-extension-banner__text-container'>
244 281
                         { t('chromeExtensionBanner.installExtensionText') }
245 282
                     </div>
246 283
                     <div
284
+                        aria-label = { t('chromeExtensionBanner.close') }
247 285
                         className = 'chrome-extension-banner__close-container'
248
-                        onClick = { this._onClosePressed }>
286
+                        onClick = { this._onClosePressed }
287
+                        onKeyPress = { this._onCloseKeyPress }
288
+                        role = 'button'
289
+                        tabIndex = { 0 }>
249 290
                         <Icon
250 291
                             className = 'gray'
251 292
                             size = { 12 }
@@ -255,18 +296,28 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
255 296
                 <div
256 297
                     className = 'chrome-extension-banner__button-container'>
257 298
                     <div
299
+                        aria-labelledby = 'chrome-extension-banner__button-text'
258 300
                         className = 'chrome-extension-banner__button-open-url'
259
-                        onClick = { this._onInstallExtensionClick }>
301
+                        onClick = { this._onInstallExtensionClick }
302
+                        onKeyPress = { this._onInstallExtensionKeyPress }
303
+                        role = 'button'
304
+                        tabIndex = { 0 }>
260 305
                         <div
261
-                            className = 'chrome-extension-banner__button-text'>
306
+                            className = 'chrome-extension-banner__button-text'
307
+                            id = 'chrome-extension-banner__button-text'>
262 308
                             { t('chromeExtensionBanner.buttonText') }
263 309
                         </div>
264 310
                     </div>
265 311
                 </div>
266 312
                 <div className = 'chrome-extension-banner__checkbox-container'>
267
-                    <label className = 'chrome-extension-banner__checkbox-label'>
313
+                    <label
314
+                        className = 'chrome-extension-banner__checkbox-label'
315
+                        htmlFor = 'chrome-extension-banner__checkbox'
316
+                        id = 'chrome-extension-banner__checkbox-label'>
268 317
                         <input
318
+                            aria-labelledby = 'chrome-extension-banner__checkbox-label'
269 319
                             checked = { this.state.dontShowAgainChecked }
320
+                            id = 'chrome-extension-banner__checkbox'
270 321
                             onChange = { this._onDontShowAgainChange }
271 322
                             type = 'checkbox' />
272 323
                         &nbsp;{ t('chromeExtensionBanner.dontShowAgain') }

+ 15
- 3
react/features/conference/components/web/InviteMore.js View File

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import React from 'react';
3
+import React, { useCallback } from 'react';
4 4
 
5 5
 import { translate } from '../../../base/i18n';
6 6
 import { Icon, IconInviteMore } from '../../../base/icons';
@@ -47,16 +47,28 @@ function InviteMore({
47 47
     onClick,
48 48
     t
49 49
 }: Props) {
50
+    const onKeyPressHandler = useCallback(e => {
51
+        if (onClick && (e.key === ' ' || e.key === 'Enter')) {
52
+            e.preventDefault();
53
+            onClick();
54
+        }
55
+    }, [ onClick ]);
56
+
50 57
     return (
51 58
         _shouldShow
52 59
             ? <div className = { `invite-more-container${_toolboxVisible ? '' : ' elevated'}` }>
53 60
                 <div className = 'invite-more-content'>
54
-                    <div className = 'invite-more-header'>
61
+                    <div
62
+                        className = 'invite-more-header'
63
+                        role = 'heading'>
55 64
                         {t('addPeople.inviteMoreHeader')}
56 65
                     </div>
57 66
                     <div
58 67
                         className = 'invite-more-button'
59
-                        onClick = { onClick }>
68
+                        onClick = { onClick }
69
+                        onKeyPress = { onKeyPressHandler }
70
+                        role = 'button'
71
+                        tabIndex = { 0 }>
60 72
                         <Icon src = { IconInviteMore } />
61 73
                         <div className = 'invite-more-button-text'>
62 74
                             {t('addPeople.inviteMorePrompt')}

+ 6
- 2
react/features/connection-stats/components/ConnectionStatsTable.js View File

@@ -572,7 +572,9 @@ class ConnectionStatsTable extends Component<Props> {
572 572
             <span>
573 573
                 <a
574 574
                     className = 'savelogs link'
575
-                    onClick = { this.props.onSaveLogs } >
575
+                    onClick = { this.props.onSaveLogs }
576
+                    role = 'button'
577
+                    tabIndex = { 0 }>
576 578
                     { this.props.t('connectionindicator.savelogs') }
577 579
                 </a>
578 580
                 <span> | </span>
@@ -597,7 +599,9 @@ class ConnectionStatsTable extends Component<Props> {
597 599
         return (
598 600
             <a
599 601
                 className = 'showmore link'
600
-                onClick = { this.props.onShowMore } >
602
+                onClick = { this.props.onShowMore }
603
+                role = 'button'
604
+                tabIndex = { 0 }>
601 605
                 { this.props.t(translationKey) }
602 606
             </a>
603 607
         );

+ 1
- 0
react/features/deep-linking/components/DeepLinkingDesktopPage.web.js View File

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

+ 2
- 0
react/features/deep-linking/components/DeepLinkingMobilePage.web.js View File

@@ -119,6 +119,7 @@ class DeepLinkingMobilePage extends Component<Props> {
119 119
                         HIDE_DEEP_LINKING_LOGO
120 120
                             ? null
121 121
                             : <img
122
+                                alt = { t('welcomepage.logo.logoDeepLinking') }
122 123
                                 className = 'logo'
123 124
                                 src = 'images/logo-deep-linking.png' />
124 125
                     }
@@ -127,6 +128,7 @@ class DeepLinkingMobilePage extends Component<Props> {
127 128
                     {
128 129
                         SHOW_DEEP_LINKING_IMAGE
129 130
                             ? <img
131
+                                alt = { t('welcomepage.logo.logoDeepLinking') }
130 132
                                 className = 'image'
131 133
                                 src = 'images/deep-linking-image.png' />
132 134
                             : null

+ 11
- 2
react/features/desktop-picker/components/DesktopSourcePreview.js View File

@@ -2,6 +2,9 @@
2 2
 
3 3
 import React, { Component } from 'react';
4 4
 
5
+import { translate } from '../../base/i18n';
6
+
7
+
5 8
 /**
6 9
  * The type of the React {@code Component} props of
7 10
  * {@link DesktopSourcePreview}.
@@ -35,7 +38,12 @@ type Props = {
35 38
     /**
36 39
      * The source type of the DesktopCapturerSources to display.
37 40
      */
38
-    type: string
41
+    type: string,
42
+
43
+    /**
44
+     * Invoked to obtain translated strings.
45
+     */
46
+    t: Function
39 47
 };
40 48
 
41 49
 /**
@@ -74,6 +82,7 @@ class DesktopSourcePreview extends Component<Props> {
74 82
                 onDoubleClick = { this._onDoubleClick }>
75 83
                 <div className = 'desktop-source-preview-image-container'>
76 84
                     <img
85
+                        alt = { this.props.t('welcomepage.logo.desktopPreviewThumbnail') }
77 86
                         className = 'desktop-source-preview-thumbnail'
78 87
                         src = { this.props.source.thumbnail.toDataURL() } />
79 88
                 </div>
@@ -111,4 +120,4 @@ class DesktopSourcePreview extends Component<Props> {
111 120
     }
112 121
 }
113 122
 
114
-export default DesktopSourcePreview;
123
+export default translate(DesktopSourcePreview);

+ 23
- 1
react/features/device-selection/components/AudioOutputPreview.js View File

@@ -44,6 +44,7 @@ class AudioOutputPreview extends Component<Props> {
44 44
 
45 45
         this._audioElementReady = this._audioElementReady.bind(this);
46 46
         this._onClick = this._onClick.bind(this);
47
+        this._onKeyPress = this._onKeyPress.bind(this);
47 48
     }
48 49
 
49 50
     /**
@@ -66,7 +67,12 @@ class AudioOutputPreview extends Component<Props> {
66 67
     render() {
67 68
         return (
68 69
             <div className = 'audio-output-preview'>
69
-                <a onClick = { this._onClick }>
70
+                <a
71
+                    aria-label = { this.props.t('deviceSelection.testAudio') }
72
+                    onClick = { this._onClick }
73
+                    onKeyPress = { this._onKeyPress }
74
+                    role = 'button'
75
+                    tabIndex = { 0 }>
70 76
                     { this.props.t('deviceSelection.testAudio') }
71 77
                 </a>
72 78
                 <Audio
@@ -105,6 +111,22 @@ class AudioOutputPreview extends Component<Props> {
105 111
             && this._audioElement.play();
106 112
     }
107 113
 
114
+    _onKeyPress: (Object) => void;
115
+
116
+    /**
117
+     * KeyPress handler for accessibility.
118
+     *
119
+     * @param {Object} e - The key event to handle.
120
+     *
121
+     * @returns {void}
122
+     */
123
+    _onKeyPress(e) {
124
+        if (e.key === ' ' || e.key === 'Enter') {
125
+            e.preventDefault();
126
+            this._onClick();
127
+        }
128
+    }
129
+
108 130
     /**
109 131
      * Updates the target output device for playing the test sound.
110 132
      *

+ 10
- 3
react/features/device-selection/components/DeviceSelection.js View File

@@ -232,7 +232,9 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
232 232
                             track = { this.state.previewAudioTrack } /> }
233 233
                 </div>
234 234
                 <div className = 'device-selection-column column-selectors'>
235
-                    <div className = 'device-selectors'>
235
+                    <div
236
+                        aria-live = 'polite all'
237
+                        className = 'device-selectors'>
236 238
                         { this._renderSelectors() }
237 239
                     </div>
238 240
                     { !hideAudioOutputSelect
@@ -344,9 +346,11 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
344 346
     _renderSelector(deviceSelectorProps) {
345 347
         return (
346 348
             <div key = { deviceSelectorProps.label }>
347
-                <div className = 'device-selector-label'>
349
+                <label
350
+                    className = 'device-selector-label'
351
+                    htmlFor = { deviceSelectorProps.id }>
348 352
                     { this.props.t(deviceSelectorProps.label) }
349
-                </div>
353
+                </label>
350 354
                 <DeviceSelector { ...deviceSelectorProps } />
351 355
             </div>
352 356
         );
@@ -370,6 +374,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
370 374
                 isDisabled: this.props.disableAudioInputChange
371 375
                     || this.props.disableDeviceChange,
372 376
                 key: 'audioInput',
377
+                id: 'audioInput',
373 378
                 label: 'settings.selectMic',
374 379
                 onSelect: selectedAudioInputId =>
375 380
                     super._onChange({ selectedAudioInputId }),
@@ -385,6 +390,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
385 390
                 icon: 'icon-camera',
386 391
                 isDisabled: this.props.disableDeviceChange,
387 392
                 key: 'videoInput',
393
+                id: 'videoInput',
388 394
                 label: 'settings.selectCamera',
389 395
                 onSelect: selectedVideoInputId =>
390 396
                     super._onChange({ selectedVideoInputId }),
@@ -400,6 +406,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
400 406
                 icon: 'icon-speaker',
401 407
                 isDisabled: this.props.disableDeviceChange,
402 408
                 key: 'audioOutput',
409
+                id: 'audioOutput',
403 410
                 label: 'settings.selectAudioOutput',
404 411
                 onSelect: selectedAudioOutputId =>
405 412
                     super._onChange({ selectedAudioOutputId }),

+ 22
- 13
react/features/device-selection/components/DeviceSelector.web.js View File

@@ -51,7 +51,12 @@ type Props = {
51 51
     /**
52 52
      * Invoked to obtain translated strings.
53 53
      */
54
-    t: Function
54
+    t: Function,
55
+
56
+    /**
57
+     * The id of the dropdown element
58
+     */
59
+    id: string
55 60
 };
56 61
 
57 62
 /**
@@ -81,6 +86,10 @@ class DeviceSelector extends Component<Props> {
81 86
      * @returns {ReactElement}
82 87
      */
83 88
     render() {
89
+        if (this.props.hasPermission === undefined) {
90
+            return null;
91
+        }
92
+
84 93
         if (!this.props.hasPermission) {
85 94
             return this._renderNoPermission();
86 95
         }
@@ -134,14 +143,10 @@ class DeviceSelector extends Component<Props> {
134 143
     _createDropdownItem(device) {
135 144
         return (
136 145
             <DropdownItem
146
+                data-deviceid = { device.deviceId }
147
+                isSelected = { device.deviceId === this.props.selectedDeviceId }
137 148
                 key = { device.deviceId }
138
-                // eslint-disable-next-line react/jsx-no-bind
139
-                onClick = {
140
-                    e => {
141
-                        e.stopPropagation();
142
-                        this._onSelect(device.deviceId);
143
-                    }
144
-                }>
149
+                onClick = { this._onSelect }>
145 150
                 { device.label || device.deviceId }
146 151
             </DropdownItem>
147 152
         );
@@ -183,7 +188,8 @@ class DeviceSelector extends Component<Props> {
183 188
                     shouldFitContainer = { true }
184 189
                     trigger = { triggerText }
185 190
                     triggerButtonProps = {{
186
-                        shouldFitContainer: true
191
+                        shouldFitContainer: true,
192
+                        id: this.props.id
187 193
                     }}
188 194
                     triggerType = 'button'>
189 195
                     <DropdownItemGroup>
@@ -199,13 +205,16 @@ class DeviceSelector extends Component<Props> {
199 205
     /**
200 206
      * Invokes the passed in callback to notify of selection changes.
201 207
      *
202
-     * @param {Object} newDeviceId - Selected device id from DropdownMenu option.
208
+     * @param {Object} e - The key event to handle.
209
+     *
203 210
      * @private
204 211
      * @returns {void}
205 212
      */
206
-    _onSelect(newDeviceId) {
207
-        if (this.props.selectedDeviceId !== newDeviceId) {
208
-            this.props.onSelect(newDeviceId);
213
+    _onSelect(e) {
214
+        const deviceId = e.currentTarget.getAttribute('data-deviceid');
215
+
216
+        if (this.props.selectedDeviceId !== deviceId) {
217
+            this.props.onSelect(deviceId);
209 218
         }
210 219
     }
211 220
 

+ 27
- 2
react/features/e2ee/components/E2EESection.js View File

@@ -85,6 +85,7 @@ class E2EESection extends Component<Props, State> {
85 85
 
86 86
         // Bind event handlers so they are only bound once for every instance.
87 87
         this._onExpand = this._onExpand.bind(this);
88
+        this._onExpandKeyPress = this._onExpandKeyPress.bind(this);
88 89
         this._onToggle = this._onToggle.bind(this);
89 90
     }
90 91
 
@@ -101,12 +102,20 @@ class E2EESection extends Component<Props, State> {
101 102
 
102 103
         return (
103 104
             <div id = 'e2ee-section'>
104
-                <p className = 'description'>
105
+                <p
106
+                    aria-live = 'polite'
107
+                    className = 'description'
108
+                    id = 'e2ee-section-description'>
105 109
                     { expand && description }
106 110
                     { !expand && description.substring(0, 100) }
107 111
                     { !expand && <span
112
+                        aria-controls = 'e2ee-section-description'
113
+                        aria-expanded = { expand }
108 114
                         className = 'read-more'
109
-                        onClick = { this._onExpand }>
115
+                        onClick = { this._onExpand }
116
+                        onKeyPress = { this._onExpandKeyPress }
117
+                        role = 'button'
118
+                        tabIndex = { 0 }>
110 119
                             ... { t('dialog.readMore') }
111 120
                     </span> }
112 121
                 </p>
@@ -142,6 +151,22 @@ class E2EESection extends Component<Props, State> {
142 151
         });
143 152
     }
144 153
 
154
+    _onExpandKeyPress: (Object) => void;
155
+
156
+    /**
157
+     * KeyPress handler for accessibility.
158
+     *
159
+     * @param {Object} e - The key event to handle.
160
+     *
161
+     * @returns {void}
162
+     */
163
+    _onExpandKeyPress(e) {
164
+        if (e.key === ' ' || e.key === 'Enter') {
165
+            e.preventDefault();
166
+            this._onExpand();
167
+        }
168
+    }
169
+
145 170
     _onToggle: () => void;
146 171
 
147 172
     /**

+ 2
- 0
react/features/embed-meeting/components/EmbedMeetingDialog.js View File

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

+ 19
- 1
react/features/embed-meeting/components/EmbedMeetingTrigger.js View File

@@ -36,10 +36,28 @@ function EmbedMeetingTrigger({ t, openEmbedDialog }: Props) {
36 36
         openEmbedDialog(EmbedMeetingDialog);
37 37
     }
38 38
 
39
+    /**
40
+     * KeyPress handler for accessibility.
41
+     *
42
+     * @param {React.KeyboardEventHandler<HTMLDivElement>} e - The key event to handle.
43
+     *
44
+     * @returns {void}
45
+     */
46
+    function onKeyPress(e) {
47
+        if (e.key === ' ' || e.key === 'Enter') {
48
+            e.preventDefault();
49
+            onClick();
50
+        }
51
+    }
52
+
39 53
     return (
40 54
         <div
55
+            aria-label = { t('embedMeeting.title') }
41 56
             className = 'embed-meeting-trigger'
42
-            onClick = { onClick }>
57
+            onClick = { onClick }
58
+            onKeyPress = { onKeyPress }
59
+            role = 'button'
60
+            tabIndex = { 0 }>
43 61
             {t('embedMeeting.title')}
44 62
         </div>
45 63
     );

+ 18
- 5
react/features/feedback/components/FeedbackDialog.web.js View File

@@ -154,6 +154,12 @@ class FeedbackDialog extends Component<Props, State> {
154 154
         this._scoreClickConfigurations = SCORES.map((textKey, index) => {
155 155
             return {
156 156
                 _onClick: () => this._onScoreSelect(index),
157
+                _onKeyPres: e => {
158
+                    if (e.key === ' ' || e.key === 'Enter') {
159
+                        e.preventDefault();
160
+                        this._onScoreSelect(index);
161
+                    }
162
+                },
157 163
                 _onMouseOver: () => this._onScoreMouseOver(index)
158 164
             };
159 165
         });
@@ -200,6 +206,8 @@ class FeedbackDialog extends Component<Props, State> {
200 206
         const scoreToDisplayAsSelected
201 207
             = mousedOverScore > -1 ? mousedOverScore : score;
202 208
 
209
+        const { t } = this.props;
210
+
203 211
         const scoreIcons = this._scoreClickConfigurations.map(
204 212
             (config, index) => {
205 213
                 const isFilled = index <= scoreToDisplayAsSelected;
@@ -208,11 +216,15 @@ class FeedbackDialog extends Component<Props, State> {
208 216
                     = `star-btn ${scoreAnimationClass} ${activeClass}`;
209 217
 
210 218
                 return (
211
-                    <a
219
+                    <span
220
+                        aria-label = { t(SCORES[index]) }
212 221
                         className = { className }
213 222
                         key = { index }
214 223
                         onClick = { config._onClick }
215
-                        onMouseOver = { config._onMouseOver }>
224
+                        onKeyPress = { config._onKeyPres }
225
+                        onMouseOver = { config._onMouseOver }
226
+                        role = 'button'
227
+                        tabIndex = { 0 }>
216 228
                         { isFilled
217 229
                             ? <StarFilledIcon
218 230
                                 label = 'star-filled'
@@ -220,11 +232,10 @@ class FeedbackDialog extends Component<Props, State> {
220 232
                             : <StarIcon
221 233
                                 label = 'star'
222 234
                                 size = 'xlarge' /> }
223
-                    </a>
235
+                    </span>
224 236
                 );
225 237
             });
226 238
 
227
-        const { t } = this.props;
228 239
 
229 240
         return (
230 241
             <Dialog
@@ -234,7 +245,9 @@ class FeedbackDialog extends Component<Props, State> {
234 245
                 titleKey = 'feedback.rateExperience'>
235 246
                 <div className = 'feedback-dialog'>
236 247
                     <div className = 'rating'>
237
-                        <div className = 'star-label'>
248
+                        <div
249
+                            aria-label = { this.props.t('feedback.star') }
250
+                            className = 'star-label' >
238 251
                             <p id = 'starLabel'>
239 252
                                 { t(SCORES[scoreToDisplayAsSelected]) }
240 253
                             </p>

+ 32
- 5
react/features/filmstrip/components/web/Filmstrip.js View File

@@ -13,7 +13,8 @@ import { translate } from '../../../base/i18n';
13 13
 import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
14 14
 import { getLocalParticipant } from '../../../base/participants';
15 15
 import { connect } from '../../../base/redux';
16
-import { isButtonEnabled } from '../../../toolbox/functions.web';
16
+import { showToolbox } from '../../../toolbox/actions.web';
17
+import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
17 18
 import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
18 19
 import { setFilmstripVisible } from '../../actions';
19 20
 import { shouldRemoteVideosBeVisible } from '../../functions';
@@ -83,6 +84,11 @@ type Props = {
83 84
      */
84 85
     _visible: boolean,
85 86
 
87
+    /**
88
+     * Whether or not the toolbox is displayed.
89
+     */
90
+    _isToolboxVisible: Boolean,
91
+
86 92
     /**
87 93
      * The redux {@code dispatch} function.
88 94
      */
@@ -114,6 +120,7 @@ class Filmstrip extends Component <Props> {
114 120
         // Bind event handlers so they are only bound once for every instance.
115 121
         this._onShortcutToggleFilmstrip = this._onShortcutToggleFilmstrip.bind(this);
116 122
         this._onToolbarToggleFilmstrip = this._onToolbarToggleFilmstrip.bind(this);
123
+        this._onTabIn = this._onTabIn.bind(this);
117 124
     }
118 125
 
119 126
     /**
@@ -238,6 +245,19 @@ class Filmstrip extends Component <Props> {
238 245
         );
239 246
     }
240 247
 
248
+    _onTabIn: () => void;
249
+
250
+    /**
251
+     * Toggle the toolbar visibility when tabbing into it.
252
+     *
253
+     * @returns {void}
254
+     */
255
+    _onTabIn() {
256
+        if (!this.props._isToolboxVisible && this.props._visible) {
257
+            this.props.dispatch(showToolbox());
258
+        }
259
+    }
260
+
241 261
     /**
242 262
      * Dispatches an action to change the visibility of the filmstrip.
243 263
      *
@@ -298,12 +318,18 @@ class Filmstrip extends Component <Props> {
298 318
         const { t } = this.props;
299 319
 
300 320
         return (
301
-            <div className = 'filmstrip__toolbar'>
321
+            <div
322
+                className = 'filmstrip__toolbar'>
302 323
                 <button
324
+                    aria-expanded = { this.props._visible }
303 325
                     aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }
304 326
                     id = 'toggleFilmstripButton'
305
-                    onClick = { this._onToolbarToggleFilmstrip }>
306
-                    <Icon src = { icon } />
327
+                    onClick = { this._onToolbarToggleFilmstrip }
328
+                    onFocus = { this._onTabIn }
329
+                    tabIndex = { 0 }>
330
+                    <Icon
331
+                        aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }
332
+                        src = { icon } />
307 333
                 </button>
308 334
             </div>
309 335
         );
@@ -342,7 +368,8 @@ function _mapStateToProps(state) {
342 368
         _participants: state['features/base/participants'],
343 369
         _rows: gridDimensions.rows,
344 370
         _videosClassName: videosClassName,
345
-        _visible: visible
371
+        _visible: visible,
372
+        _isToolboxVisible: isToolboxVisible(state)
346 373
     };
347 374
 }
348 375
 

+ 6
- 6
react/features/filmstrip/components/web/Thumbnail.js View File

@@ -737,12 +737,12 @@ class Thumbnail extends Component<Props, State> {
737 737
                         participantID = { id } />
738 738
                 </div>
739 739
                 { this._renderAvatar(styles.avatar) }
740
-                <span className = 'localvideomenu'>
741
-                    <LocalVideoMenuTriggerButton />
742
-                </span>
743 740
                 <span className = 'audioindicator-container'>
744 741
                     <AudioLevelIndicator audioLevel = { audioLevel } />
745 742
                 </span>
743
+                <span className = 'localvideomenu'>
744
+                    <LocalVideoMenuTriggerButton />
745
+                </span>
746 746
             </span>
747 747
         );
748 748
     }
@@ -867,15 +867,15 @@ class Thumbnail extends Component<Props, State> {
867 867
                         className = 'presence-label'
868 868
                         participantID = { id } />
869 869
                 </div>
870
+                <span className = 'audioindicator-container'>
871
+                    <AudioLevelIndicator audioLevel = { audioLevel } />
872
+                </span>
870 873
                 <span className = 'remotevideomenu'>
871 874
                     <RemoteVideoMenuTriggerButton
872 875
                         initialVolumeValue = { volume }
873 876
                         onVolumeChange = { onVolumeChange }
874 877
                         participantID = { id } />
875 878
                 </span>
876
-                <span className = 'audioindicator-container'>
877
-                    <AudioLevelIndicator audioLevel = { audioLevel } />
878
-                </span>
879 879
             </span>
880 880
         );
881 881
     }

+ 1
- 0
react/features/google-api/components/GoogleSignInButton.web.js View File

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

+ 3
- 1
react/features/invite/components/add-people-dialog/web/CopyMeetingLinkSection.js View File

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

+ 22
- 1
react/features/invite/components/add-people-dialog/web/DialInNumber.js View File

@@ -49,6 +49,7 @@ class DialInNumber extends Component<Props> {
49 49
 
50 50
         // Bind event handler so it is only bound once for every instance.
51 51
         this._onCopyText = this._onCopyText.bind(this);
52
+        this._onCopyTextKeyPress = this._onCopyTextKeyPress.bind(this);
52 53
     }
53 54
 
54 55
     _onCopyText: () => void;
@@ -68,6 +69,22 @@ class DialInNumber extends Component<Props> {
68 69
         copyText(textToCopy);
69 70
     }
70 71
 
72
+    _onCopyTextKeyPress: (Object) => void;
73
+
74
+    /**
75
+     * KeyPress handler for accessibility.
76
+     *
77
+     * @param {Object} e - The key event to handle.
78
+     *
79
+     * @returns {void}
80
+     */
81
+    _onCopyTextKeyPress(e) {
82
+        if (e.key === ' ' || e.key === 'Enter') {
83
+            e.preventDefault();
84
+            this._onCopyText();
85
+        }
86
+    }
87
+
71 88
     /**
72 89
      * Implements React's {@link Component#render()}.
73 90
      *
@@ -101,8 +118,12 @@ class DialInNumber extends Component<Props> {
101 118
                     </span>
102 119
                 </div>
103 120
                 <a
121
+                    aria-label = { t('info.copyNumber') }
104 122
                     className = 'dial-in-copy'
105
-                    onClick = { this._onCopyText }>
123
+                    onClick = { this._onCopyText }
124
+                    onKeyPress = { this._onCopyTextKeyPress }
125
+                    role = 'button'
126
+                    tabIndex = { 0 }>
106 127
                     <Icon src = { IconCopy } />
107 128
                 </a>
108 129
             </div>

+ 40
- 2
react/features/invite/components/add-people-dialog/web/InviteByEmailSection.js View File

@@ -52,6 +52,20 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
52 52
         copyText(inviteText);
53 53
     }
54 54
 
55
+    /**
56
+     * Copies the conference invitation to the clipboard.
57
+     *
58
+     * @param {Object} e - The key event to handle.
59
+     *
60
+     * @returns {void}
61
+     */
62
+    function _onCopyTextKeyPress(e) {
63
+        if (e.key === ' ' || e.key === 'Enter') {
64
+            e.preventDefault();
65
+            copyText(inviteText);
66
+        }
67
+    }
68
+
55 69
     /**
56 70
      * Toggles the email invite drawer.
57 71
      *
@@ -61,6 +75,20 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
61 75
         setIsActive(!isActive);
62 76
     }
63 77
 
78
+    /**
79
+     * Toggles the email invite drawer.
80
+     *
81
+     * @param {Object} e - The key event to handle.
82
+     *
83
+     * @returns {void}
84
+     */
85
+    function _onToggleActiveStateKeyPress(e) {
86
+        if (e.key === ' ' || e.key === 'Enter') {
87
+            e.preventDefault();
88
+            setIsActive(!isActive);
89
+        }
90
+    }
91
+
64 92
     /**
65 93
      * Renders clickable elements that each open an email client
66 94
      * containing a conference invite.
@@ -101,6 +129,7 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
101 129
                             key = { idx }
102 130
                             position = 'top'>
103 131
                             <a
132
+                                aria-label = { t(tooltipKey) }
104 133
                                 className = 'provider-icon'
105 134
                                 href = { url }
106 135
                                 rel = 'noopener noreferrer'
@@ -119,8 +148,13 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
119 148
         <>
120 149
             <div>
121 150
                 <div
151
+                    aria-expanded = { isActive }
152
+                    aria-label = { t('addPeople.shareInvite') }
122 153
                     className = { `invite-more-dialog email-container${isActive ? ' active' : ''}` }
123
-                    onClick = { _onToggleActiveState }>
154
+                    onClick = { _onToggleActiveState }
155
+                    onKeyPress = { _onToggleActiveStateKeyPress }
156
+                    role = 'button'
157
+                    tabIndex = { 0 }>
124 158
                     <span>{t('addPeople.shareInvite')}</span>
125 159
                     <Icon src = { IconArrowDownSmall } />
126 160
                 </div>
@@ -129,8 +163,12 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
129 163
                         content = { t('addPeople.copyInvite') }
130 164
                         position = 'top'>
131 165
                         <div
166
+                            aria-label = { t('addPeople.copyInvite') }
132 167
                             className = 'copy-invite-icon'
133
-                            onClick = { _onCopyText }>
168
+                            onClick = { _onCopyText }
169
+                            onKeyPress = { _onCopyTextKeyPress }
170
+                            role = 'button'
171
+                            tabIndex = { 0 }>
134 172
                             <Icon src = { IconCopy } />
135 173
                         </div>
136 174
                     </Tooltip>

+ 46
- 4
react/features/invite/components/add-people-dialog/web/InviteContactsForm.js View File

@@ -76,9 +76,11 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
76 76
 
77 77
         // Bind event handlers so they are only bound once per instance.
78 78
         this._onClearItems = this._onClearItems.bind(this);
79
+        this._onClearItemsKeyPress = this._onClearItemsKeyPress.bind(this);
79 80
         this._onItemSelected = this._onItemSelected.bind(this);
80 81
         this._onSelectionChange = this._onSelectionChange.bind(this);
81 82
         this._onSubmit = this._onSubmit.bind(this);
83
+        this._onSubmitKeyPress = this._onSubmitKeyPress.bind(this);
82 84
         this._parseQueryResults = this._parseQueryResults.bind(this);
83 85
         this._setMultiSelectElement = this._setMultiSelectElement.bind(this);
84 86
         this._renderFooterText = this._renderFooterText.bind(this);
@@ -246,6 +248,22 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
246 248
             });
247 249
     }
248 250
 
251
+    _onSubmitKeyPress: (Object) => void;
252
+
253
+    /**
254
+     * KeyPress handler for accessibility.
255
+     *
256
+     * @param {Object} e - The key event to handle.
257
+     *
258
+     * @returns {void}
259
+     */
260
+    _onSubmitKeyPress(e) {
261
+        if (e.key === ' ' || e.key === 'Enter') {
262
+            e.preventDefault();
263
+            this._onSubmit();
264
+        }
265
+    }
266
+
249 267
     _onKeyDown: (Object) => void;
250 268
 
251 269
     /**
@@ -425,6 +443,22 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
425 443
         this.setState({ inviteItems: [] });
426 444
     }
427 445
 
446
+    _onClearItemsKeyPress: () => void;
447
+
448
+    /**
449
+     * Clears the selected items from state and form.
450
+     *
451
+     * @param {Object} e - The key event to handle.
452
+     *
453
+     * @returns {void}
454
+     */
455
+    _onClearItemsKeyPress(e) {
456
+        if (e.key === ' ' || e.key === 'Enter') {
457
+            e.preventDefault();
458
+            this._onClearItems();
459
+        }
460
+    }
461
+
428 462
     /**
429 463
      * Renders the add/cancel actions for the form.
430 464
      *
@@ -441,13 +475,21 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
441 475
         return (
442 476
             <div className = { `invite-more-dialog invite-buttons${this._isAddDisabled() ? ' disabled' : ''}` }>
443 477
                 <a
478
+                    aria-label = { t('dialog.Cancel') }
444 479
                     className = 'invite-more-dialog invite-buttons-cancel'
445
-                    onClick = { this._onClearItems }>
480
+                    onClick = { this._onClearItems }
481
+                    onKeyPress = { this._onClearItemsKeyPress }
482
+                    role = 'button'
483
+                    tabIndex = { 0 }>
446 484
                     {t('dialog.Cancel')}
447 485
                 </a>
448 486
                 <a
487
+                    aria-label = { t('addPeople.add') }
449 488
                     className = 'invite-more-dialog invite-buttons-add'
450
-                    onClick = { this._onSubmit }>
489
+                    onClick = { this._onSubmit }
490
+                    onKeyPress = { this._onSubmitKeyPress }
491
+                    role = 'button'
492
+                    tabIndex = { 0 }>
451 493
                     {t('addPeople.add')}
452 494
                 </a>
453 495
             </div>
@@ -480,9 +522,9 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
480 522
                 </span>
481 523
                 <span>
482 524
                     <a
525
+                        aria-label = { supportLink }
483 526
                         href = { supportLink }
484
-                        rel = 'noopener noreferrer'
485
-                        target = '_blank'>
527
+                        rel = 'noopener noreferrer'>
486 528
                         { t('inlineDialogFailure.support') }
487 529
                     </a>
488 530
                 </span>

+ 3
- 1
react/features/keyboard-shortcuts/components/KeyboardShortcutsDialog.web.js View File

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

+ 3
- 1
react/features/large-video/components/LargeVideo.web.js View File

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

+ 3
- 1
react/features/lobby/components/web/LobbySection.js View File

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

+ 6
- 2
react/features/local-recording/components/LocalRecordingInfoDialog.js View File

@@ -306,11 +306,15 @@ class LocalRecordingInfoDialog extends Component<Props, State> {
306 306
                 <div className = 'localrec-control-action-links'>
307 307
                     <div className = 'localrec-control-action-link'>
308 308
                         { isEngaged ? <a
309
-                            onClick = { this._onStop }>
309
+                            onClick = { this._onStop }
310
+                            role = 'button'
311
+                            tabIndex = { 0 }>
310 312
                             { t('localRecording.stop') }
311 313
                         </a>
312 314
                             : <a
313
-                                onClick = { this._onStart }>
315
+                                onClick = { this._onStart }
316
+                                role = 'button'
317
+                                tabIndex = { 0 }>
314 318
                                 { t('localRecording.start') }
315 319
                             </a>
316 320
                         }

+ 2
- 2
react/features/notifications/components/web/Notification.js View File

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

+ 9
- 3
react/features/notifications/components/web/NotificationsContainer.js View File

@@ -3,6 +3,7 @@
3 3
 import { FlagGroup } from '@atlaskit/flag';
4 4
 import React from 'react';
5 5
 
6
+import { translate } from '../../../base/i18n';
6 7
 import { connect } from '../../../base/redux';
7 8
 import AbstractNotificationsContainer, {
8 9
     _abstractMapStateToProps,
@@ -16,7 +17,12 @@ type Props = AbstractProps & {
16 17
     /**
17 18
      * Whether we are a SIP gateway or not.
18 19
      */
19
-     _iAmSipGateway: boolean
20
+     _iAmSipGateway: boolean,
21
+
22
+     /**
23
+     * Invoked to obtain translated strings.
24
+     */
25
+     t: Function
20 26
 };
21 27
 
22 28
 /**
@@ -42,6 +48,7 @@ class NotificationsContainer extends AbstractNotificationsContainer<Props> {
42 48
         return (
43 49
             <FlagGroup
44 50
                 id = 'notifications-container'
51
+                label = { this.props.t('notify.groupTitle') }
45 52
                 onDismissed = { this._onDismissed }>
46 53
                 { this._renderFlags() }
47 54
             </FlagGroup>
@@ -73,7 +80,6 @@ class NotificationsContainer extends AbstractNotificationsContainer<Props> {
73 80
                     key = { uid }
74 81
                     onDismissed = { this._onDismissed }
75 82
                     uid = { uid } />
76
-
77 83
             );
78 84
         });
79 85
     }
@@ -96,4 +102,4 @@ function _mapStateToProps(state) {
96 102
 }
97 103
 
98 104
 
99
-export default connect(_mapStateToProps)(NotificationsContainer);
105
+export default translate(connect(_mapStateToProps)(NotificationsContainer));

+ 11
- 3
react/features/overlay/components/web/PageReloadOverlay.js View File

@@ -29,12 +29,20 @@ class PageReloadOverlay extends AbstractPageReloadOverlay<Props> {
29 29
 
30 30
         return (
31 31
             <OverlayFrame isLightOverlay = { isNetworkFailure }>
32
-                <div className = 'inlay'>
32
+                <div
33
+                    aria-describedby = 'reload_overlay_text'
34
+                    aria-labelledby = 'reload_overlay_title'
35
+                    className = 'inlay'
36
+                    role = 'dialog'>
33 37
                     <span
34
-                        className = 'reload_overlay_title'>
38
+                        className = 'reload_overlay_title'
39
+                        id = 'reload_overlay_title'
40
+                        role = 'heading'>
35 41
                         { t(title) }
36 42
                     </span>
37
-                    <span className = 'reload_overlay_text'>
43
+                    <span
44
+                        className = 'reload_overlay_text'
45
+                        id = 'reload_overlay_text'>
38 46
                         { t(message, { seconds: timeLeft }) }
39 47
                     </span>
40 48
                     { this._renderProgressBar() }

+ 13
- 4
react/features/overlay/components/web/UserMediaPermissionsOverlay.js View File

@@ -30,12 +30,17 @@ class UserMediaPermissionsOverlay extends AbstractUserMediaPermissionsOverlay {
30 30
                 <div className = 'inlay'>
31 31
                     <span className = 'inlay__icon icon-microphone' />
32 32
                     <span className = 'inlay__icon icon-camera' />
33
-                    <h3 className = 'inlay__title'>
33
+                    <h3
34
+                        aria-label = { t('startupoverlay.genericTitle') }
35
+                        className = 'inlay__title'
36
+                        role = 'alert' >
34 37
                         {
35 38
                             t('startupoverlay.genericTitle')
36 39
                         }
37 40
                     </h3>
38
-                    <span className = 'inlay__text'>
41
+                    <span
42
+                        className = 'inlay__text'
43
+                        role = 'alert' >
39 44
                         {
40 45
                             translateToHTML(t,
41 46
                                 `userMedia.${browser}GrantPermissions`)
@@ -43,7 +48,9 @@ class UserMediaPermissionsOverlay extends AbstractUserMediaPermissionsOverlay {
43 48
                     </span>
44 49
                 </div>
45 50
                 <div className = 'policy overlay__policy'>
46
-                    <p className = 'policy__text'>
51
+                    <p
52
+                        className = 'policy__text'
53
+                        role = 'alert'>
47 54
                         { translateToHTML(t, 'startupoverlay.policyText') }
48 55
                     </p>
49 56
                     {
@@ -66,7 +73,9 @@ class UserMediaPermissionsOverlay extends AbstractUserMediaPermissionsOverlay {
66 73
         if (policyLogoSrc) {
67 74
             return (
68 75
                 <div className = 'policy__logo'>
69
-                    <img src = { policyLogoSrc } />
76
+                    <img
77
+                        alt = { this.props.t('welcomepage.logo.policyLogo') }
78
+                        src = { policyLogoSrc } />
70 79
                 </div>
71 80
             );
72 81
         }

+ 1
- 1
react/features/participants-pane/components/InviteButton.js View File

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

+ 5
- 1
react/features/participants-pane/components/MeetingParticipantItem.js View File

@@ -1,6 +1,7 @@
1 1
 // @flow
2 2
 
3 3
 import React from 'react';
4
+import { useTranslation } from 'react-i18next';
4 5
 import { useSelector } from 'react-redux';
5 6
 
6 7
 import { getIsParticipantAudioMuted, getIsParticipantVideoMuted } from '../../base/tracks';
@@ -38,6 +39,7 @@ export const MeetingParticipantItem = ({
38 39
     onLeave,
39 40
     participant
40 41
 }: Props) => {
42
+    const { t } = useTranslation();
41 43
     const isAudioMuted = useSelector(getIsParticipantAudioMuted(participant));
42 44
     const isVideoMuted = useSelector(getIsParticipantVideoMuted(participant));
43 45
 
@@ -49,7 +51,9 @@ export const MeetingParticipantItem = ({
49 51
             onLeave = { onLeave }
50 52
             participant = { participant }
51 53
             videoMuteState = { isVideoMuted ? MediaState.Muted : MediaState.Unmuted }>
52
-            <ParticipantActionEllipsis onClick = { onContextMenu } />
54
+            <ParticipantActionEllipsis
55
+                aria-label = { t('MeetingParticipantItem.ParticipantActionEllipsis.options') }
56
+                onClick = { onContextMenu } />
53 57
         </ParticipantItem>
54 58
     );
55 59
 };

+ 12
- 1
react/features/participants-pane/components/ParticipantsPane.js View File

@@ -30,6 +30,12 @@ export const ParticipantsPane = () => {
30 30
     const { t } = useTranslation();
31 31
 
32 32
     const closePane = useCallback(() => dispatch(close(), [ dispatch ]));
33
+    const closePaneKeyPress = useCallback(e => {
34
+        if (closePane && (e.key === ' ' || e.key === 'Enter')) {
35
+            e.preventDefault();
36
+            closePane();
37
+        }
38
+    }, [ closePane ]);
33 39
     const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)), [ dispatch ]);
34 40
 
35 41
     return (
@@ -41,7 +47,12 @@ export const ParticipantsPane = () => {
41 47
                 ) }>
42 48
                 <div className = 'participants_pane-content'>
43 49
                     <Header>
44
-                        <Close onClick = { closePane } />
50
+                        <Close
51
+                            aria-label = { t('participantsPane.close', 'Close') }
52
+                            onClick = { closePane }
53
+                            onKeyPress = { closePaneKeyPress }
54
+                            role = 'button'
55
+                            tabIndex = { 0 } />
45 56
                     </Header>
46 57
                     <Container>
47 58
                         <LobbyParticipantList />

+ 75
- 3
react/features/prejoin/components/Prejoin.js View File

@@ -183,6 +183,9 @@ class Prejoin extends Component<Props, State> {
183 183
         this._onDropdownClose = this._onDropdownClose.bind(this);
184 184
         this._onOptionsClick = this._onOptionsClick.bind(this);
185 185
         this._setName = this._setName.bind(this);
186
+        this._onJoinConferenceWithoutAudioKeyPress = this._onJoinConferenceWithoutAudioKeyPress.bind(this);
187
+        this._showDialogKeyPress = this._showDialogKeyPress.bind(this);
188
+        this._onJoinKeyPress = this._onJoinKeyPress.bind(this);
186 189
     }
187 190
     _onJoinButtonClick: () => void;
188 191
 
@@ -205,6 +208,22 @@ class Prejoin extends Component<Props, State> {
205 208
         this.props.joinConference();
206 209
     }
207 210
 
211
+    _onJoinKeyPress: (Object) => void;
212
+
213
+    /**
214
+     * KeyPress handler for accessibility.
215
+     *
216
+     * @param {Object} e - The key event to handle.
217
+     *
218
+     * @returns {void}
219
+     */
220
+    _onJoinKeyPress(e) {
221
+        if (e.key === ' ' || e.key === 'Enter') {
222
+            e.preventDefault();
223
+            this._onJoinButtonClick();
224
+        }
225
+    }
226
+
208 227
     _onToggleButtonClick: () => void;
209 228
 
210 229
     /**
@@ -283,6 +302,40 @@ class Prejoin extends Component<Props, State> {
283 302
         this._onDropdownClose();
284 303
     }
285 304
 
305
+    _showDialogKeyPress: (Object) => void;
306
+
307
+    /**
308
+     * KeyPress handler for accessibility.
309
+     *
310
+     * @param {Object} e - The key event to handle.
311
+     *
312
+     * @returns {void}
313
+     */
314
+    _showDialogKeyPress(e) {
315
+        if (e.key === ' ' || e.key === 'Enter') {
316
+            e.preventDefault();
317
+            this._showDialog();
318
+        }
319
+    }
320
+
321
+    _onJoinConferenceWithoutAudioKeyPress: (Object) => void;
322
+
323
+    /**
324
+     * KeyPress handler for accessibility.
325
+     *
326
+     * @param {Object} e - The key event to handle.
327
+     *
328
+     * @returns {void}
329
+     */
330
+    _onJoinConferenceWithoutAudioKeyPress(e) {
331
+        if (this.props.joinConferenceWithoutAudio
332
+            && (e.key === ' '
333
+                || e.key === 'Enter')) {
334
+            e.preventDefault();
335
+            this.props.joinConferenceWithoutAudio();
336
+        }
337
+    }
338
+
286 339
     /**
287 340
      * Implements React's {@link Component#render()}.
288 341
      *
@@ -305,7 +358,8 @@ class Prejoin extends Component<Props, State> {
305 358
             visibleButtons
306 359
         } = this.props;
307 360
 
308
-        const { _closeDialog, _onDropdownClose, _onJoinButtonClick, _onOptionsClick, _setName, _showDialog } = this;
361
+        const { _closeDialog, _onDropdownClose, _onJoinButtonClick, _onJoinKeyPress, _showDialogKeyPress,
362
+            _onJoinConferenceWithoutAudioKeyPress, _onOptionsClick, _setName, _showDialog } = this;
309 363
         const { showJoinByPhoneButtons, showError } = this.state;
310 364
 
311 365
         return (
@@ -322,10 +376,16 @@ class Prejoin extends Component<Props, State> {
322 376
                 {showJoinActions && (
323 377
                     <div className = 'prejoin-input-area-container'>
324 378
                         <div className = 'prejoin-input-area'>
379
+                            <label
380
+                                className = 'prejoin-input-area-label'
381
+                                htmlFor = { 'Prejoin-input-field-id' } >
382
+                                { t('dialog.enterDisplayNameToJoin') }</label>
325 383
                             <InputField
384
+                                autoComplete = { 'name' }
326 385
                                 autoFocus = { true }
327 386
                                 className = { showError ? 'error' : '' }
328 387
                                 hasError = { showError }
388
+                                id = { 'Prejoin-input-field-id' }
329 389
                                 onChange = { _setName }
330 390
                                 onSubmit = { joinConference }
331 391
                                 placeHolder = { t('dialog.enterDisplayName') }
@@ -341,7 +401,10 @@ class Prejoin extends Component<Props, State> {
341 401
                                         <div
342 402
                                             className = 'prejoin-preview-dropdown-btn'
343 403
                                             data-testid = 'prejoin.joinWithoutAudio'
344
-                                            onClick = { joinConferenceWithoutAudio }>
404
+                                            onClick = { joinConferenceWithoutAudio }
405
+                                            onKeyPress = { _onJoinConferenceWithoutAudioKeyPress }
406
+                                            role = 'button'
407
+                                            tabIndex = { 0 }>
345 408
                                             <Icon
346 409
                                                 className = 'prejoin-preview-dropdown-icon'
347 410
                                                 size = { 24 }
@@ -350,7 +413,10 @@ class Prejoin extends Component<Props, State> {
350 413
                                         </div>
351 414
                                         {hasJoinByPhoneButton && <div
352 415
                                             className = 'prejoin-preview-dropdown-btn'
353
-                                            onClick = { _showDialog }>
416
+                                            onClick = { _showDialog }
417
+                                            onKeyPress = { _showDialogKeyPress }
418
+                                            role = 'button'
419
+                                            tabIndex = { 0 }>
354 420
                                             <Icon
355 421
                                                 className = 'prejoin-preview-dropdown-icon'
356 422
                                                 data-testid = 'prejoin.joinByPhone'
@@ -363,9 +429,15 @@ class Prejoin extends Component<Props, State> {
363 429
                                     onClose = { _onDropdownClose }>
364 430
                                     <ActionButton
365 431
                                         OptionsIcon = { showJoinByPhoneButtons ? IconArrowUp : IconArrowDown }
432
+                                        ariaDropDownLabel = { t('prejoin.joinWithoutAudio') }
433
+                                        ariaLabel = { t('prejoin.joinMeeting') }
434
+                                        ariaPressed = { showJoinByPhoneButtons }
366 435
                                         hasOptions = { true }
367 436
                                         onClick = { _onJoinButtonClick }
437
+                                        onKeyPress = { _onJoinKeyPress }
368 438
                                         onOptionsClick = { _onOptionsClick }
439
+                                        role = 'button'
440
+                                        tabIndex = { 0 }
369 441
                                         testId = 'prejoin.joinMeeting'
370 442
                                         type = 'primary'>
371 443
                                         { t('prejoin.joinMeeting') }

+ 3
- 4
react/features/prejoin/components/country-picker/CountryPicker.js View File

@@ -172,9 +172,7 @@ class CountryPicker extends PureComponent<Props, State> {
172 172
      * @param {Object} e - The synthetic event.
173 173
      * @returns {void}
174 174
      */
175
-    _onCountrySelectorClick(e) {
176
-        e.stopPropagation();
177
-
175
+    _onCountrySelectorClick() {
178 176
         this.setState({
179 177
             isOpen: !this.setState.isOpen
180 178
         });
@@ -215,7 +213,8 @@ class CountryPicker extends PureComponent<Props, State> {
215 213
      * @returns {void}
216 214
      */
217 215
     _onKeyPress(e) {
218
-        if (e.key === 'Enter') {
216
+        if (e.key === ' ' || e.key === 'Enter') {
217
+            e.preventDefault();
219 218
             this.props.onSubmit();
220 219
         }
221 220
     }

+ 0
- 0
react/features/prejoin/components/country-picker/CountrySelector.js View File


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

Loading…
Cancel
Save