Переглянути джерело

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 4 роки тому
джерело
коміт
e9675453e1
Аккаунт користувача з таким Email не знайдено
100 змінених файлів з 1878 додано та 613 видалено
  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 Переглянути файл

32
     .dropdown-menu div[style*="transform"] {
32
     .dropdown-menu div[style*="transform"] {
33
         outline: 1px solid #455166;
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 Переглянути файл

9
         max-height: 456px;
9
         max-height: 456px;
10
         overflow: auto;
10
         overflow: auto;
11
         width: 300px;
11
         width: 300px;
12
+        &-ul {
13
+            margin:0;
14
+            padding:0;
15
+            list-style-type: none;
16
+        }
12
     }
17
     }
13
 
18
 
14
     &-header {
19
     &-header {
64
     &-speaker {
69
     &-speaker {
65
         position: relative;
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
             .audio-preview-entry {
79
             .audio-preview-entry {
69
                 background: #36383C;
80
                 background: #36383C;
70
                 margin-left: 0;
81
                 margin-left: 0;
81
             }
92
             }
82
 
93
 
83
             .audio-preview-entry-text {
94
             .audio-preview-entry-text {
84
-                max-width: 196px;
95
+                max-width: 178px;
85
             }
96
             }
86
         }
97
         }
87
 
98
 
90
         }
101
         }
91
 
102
 
92
         .audio-preview-entry-text {
103
         .audio-preview-entry-text {
93
-            max-width: 256px;
104
+            max-width: 238px;
94
         }
105
         }
95
     }
106
     }
96
 
107
 
150
         color: #1C2025;
161
         color: #1C2025;
151
         cursor: pointer;
162
         cursor: pointer;
152
         font-weight: 600;
163
         font-weight: 600;
164
+        font-size: 0.8rem;
153
         line-height: 24px;
165
         line-height: 24px;
154
-        padding: 2px 16px;
166
+        padding: 2px 8px;
155
         position: absolute;
167
         position: absolute;
156
         right: 16px;
168
         right: 16px;
157
         top: 5px;
169
         top: 5px;
162
         right: 16px;
174
         right: 16px;
163
         top: 14px;
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 Переглянути файл

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

+ 1
- 5
css/_avatar.scss Переглянути файл

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

+ 40
- 12
css/_chat.scss Переглянути файл

99
     div {
99
     div {
100
         svg {
100
         svg {
101
             cursor: pointer;
101
             cursor: pointer;
102
-            fill: white
102
+            fill: white;
103
         }
103
         }
104
     }
104
     }
105
 }
105
 }
106
 
106
 
107
+
107
 .chat-header {
108
 .chat-header {
108
     height: 70px;
109
     height: 70px;
109
     position: relative;
110
     position: relative;
110
     width: 100%;
111
     width: 100%;
111
     z-index: 1;
112
     z-index: 1;
112
     display: flex;
113
     display: flex;
113
-    justify-content: flex-end;
114
+    justify-content: space-between;
114
     padding: 16px;
115
     padding: 16px;
115
     align-items: center;
116
     align-items: center;
116
     box-sizing: border-box;
117
     box-sizing: border-box;
132
             .send-button {
133
             .send-button {
133
                 background: #1B67EC;
134
                 background: #1B67EC;
134
                 cursor: pointer;
135
                 cursor: pointer;
136
+                margin-left: 0.3rem;
135
 
137
 
136
                 @media (hover: hover) and (pointer: fine) {
138
                 @media (hover: hover) and (pointer: fine) {
137
                     &:hover {
139
                     &:hover {
188
     display: flex;
190
     display: flex;
189
     align-items: center;
191
     align-items: center;
190
     justify-content: center;
192
     justify-content: center;
191
-    height: 40px;
192
-    width: 40px;
193
+    height: 38px;
194
+    width: 38px;
195
+    margin: 2px;
193
     border-radius: 3px;
196
     border-radius: 3px;
194
 }
197
 }
195
 
198
 
226
     border: 0px none;
229
     border: 0px none;
227
     box-shadow: none;
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
 #nickname {
238
 #nickname {
231
     text-align: center;
239
     text-align: center;
234
     margin: auto 0;
242
     margin: auto 0;
235
     padding: 0 16px;
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
     input {
255
     input {
238
         height: 40px;
256
         height: 40px;
239
     }
257
     }
254
         cursor: pointer;
272
         cursor: pointer;
255
 
273
 
256
         &.disabled {
274
         &.disabled {
257
-            color: #757575;
275
+            color: #AFB6BC;
258
             background: #11336E;
276
             background: #11336E;
259
             pointer-events: none;
277
             pointer-events: none;
260
         }
278
         }
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
 .chatmessage {
335
 .chatmessage {
305
     background-color: $chatRemoteMessageBackgroundColor;
336
     background-color: $chatRemoteMessageBackgroundColor;
306
     border-radius: 0px 6px 6px 6px;
337
     border-radius: 0px 6px 6px 6px;
350
     color: #757575;
381
     color: #757575;
351
 }
382
 }
352
 
383
 
353
-.smiley {
354
-    font-size: 14pt;
355
-}
356
-
357
 #smileys {
384
 #smileys {
358
     font-size: 20pt;
385
     font-size: 20pt;
359
     margin: auto;
386
     margin: auto;
382
     box-sizing: border-box;
409
     box-sizing: border-box;
383
     background-color: rgba(0, 0, 0, .6) !important;
410
     background-color: rgba(0, 0, 0, .6) !important;
384
     height: auto;
411
     height: auto;
385
-    max-height: 0;
412
+    display: none;
386
     overflow: hidden;
413
     overflow: hidden;
387
     position: absolute;
414
     position: absolute;
388
     width: calc(#{$sidebarWidth} - 32px);
415
     width: calc(#{$sidebarWidth} - 32px);
398
     transition: max-height 0.3s;
425
     transition: max-height 0.3s;
399
 
426
 
400
     &.show-smileys {
427
     &.show-smileys {
428
+        display: flex;
401
         max-height: 500%;
429
         max-height: 500%;
402
     }
430
     }
403
 
431
 
413
 
441
 
414
 .smileyContainer {
442
 .smileyContainer {
415
     width: 40px;
443
     width: 40px;
416
-    height: 36px;
444
+    height: 40px;
417
     display: inline-block;
445
     display: inline-block;
418
     text-align: center;
446
     text-align: center;
419
 }
447
 }
509
 
537
 
510
     &-header {
538
     &-header {
511
         display: flex;
539
         display: flex;
512
-        justify-content: flex-end;
540
+        justify-content: space-between;
513
         align-items: center;
541
         align-items: center;
514
         margin: 16px;
542
         margin: 16px;
515
         width: calc(100% - 32px);
543
         width: calc(100% - 32px);

+ 4
- 1
css/_e2ee.scss Переглянути файл

8
 
8
 
9
         .read-more {
9
         .read-more {
10
             cursor: pointer;
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 Переглянути файл

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

+ 13
- 2
css/_prejoin.scss Переглянути файл

3
     &-input-area {
3
     &-input-area {
4
         margin: 0 auto;
4
         margin: 0 auto;
5
         text-align: center;
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
     &-title {
17
     &-title {
74
         z-index: 1;
83
         z-index: 1;
75
 
84
 
76
         &--warning {
85
         &--warning {
77
-            background: rgba(241, 173, 51, 0.7)
86
+            background: rgba(241, 173, 51, 1);
78
         }
87
         }
79
         &--ok {
88
         &--ok {
80
-            background: rgba(49, 183, 106, 0.7);
89
+            background: rgba(49, 183, 106, 1);
81
         }
90
         }
82
     }
91
     }
83
 
92
 
92
 
101
 
93
     &-error-desc {
102
     &-error-desc {
94
         margin-right: 4px;
103
         margin-right: 4px;
104
+        color: #fff;
105
+        font-weight: bold;
95
     }
106
     }
96
 
107
 
97
     .settings-button-container {
108
     .settings-button-container {

+ 9
- 2
css/_premeeting-screens.scss Переглянути файл

113
             cursor: pointer;
113
             cursor: pointer;
114
             color: #fff;
114
             color: #fff;
115
             display: flex;
115
             display: flex;
116
-            flex-direction: row;
116
+            flex-direction: column;
117
             font-size: 15px;
117
             font-size: 15px;
118
             font-weight: 300;
118
             font-weight: 300;
119
             justify-content: center;
119
             justify-content: center;
139
                     margin-left: 10px;
139
                     margin-left: 10px;
140
                 }
140
                 }
141
             }
141
             }
142
+            .copy-button{
143
+                width: 298px;
144
+             }
142
 
145
 
143
             .copy-meeting-text {
146
             .copy-meeting-text {
144
                 width: 266px;
147
                 width: 266px;
177
             }
180
             }
178
 
181
 
179
             &.focused {
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
         fill: transparent;
252
         fill: transparent;
250
     }
253
     }
251
 
254
 
255
+    label {
256
+        cursor: pointer;
257
+    }
258
+
252
     &:hover {
259
     &:hover {
253
         background: rgba(255, 255, 255, 0.1);
260
         background: rgba(255, 255, 255, 0.1);
254
 
261
 

+ 1
- 0
css/_recording.scss Переглянути файл

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

+ 3
- 0
css/_toolbars.scss Переглянути файл

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

+ 4
- 0
css/_video-preview.scss Переглянути файл

79
             white-space: nowrap;
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 Переглянути файл

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

+ 1
- 1
css/buttons/copy.scss Переглянути файл

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

+ 2
- 2
css/components/_input-slider.scss Переглянути файл

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
 input[type=range]:focus {
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 Переглянути файл

16
         padding: 0;
16
         padding: 0;
17
         margin: 0;
17
         margin: 0;
18
         border: none;
18
         border: none;
19
-        outline: none;
20
 
19
 
21
         -webkit-appearance: none;
20
         -webkit-appearance: none;
22
 
21
 

+ 1
- 1
css/filmstrip/_tile_view.scss Переглянути файл

7
      * see.
7
      * see.
8
      */
8
      */
9
     .active-speaker {
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
     #filmstripRemoteVideos {
13
     #filmstripRemoteVideos {

+ 1
- 1
css/filmstrip/_vertical_filmstrip_overrides.scss Переглянути файл

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

+ 2
- 2
css/modals/device-selection/_device-selection.scss Переглянути файл

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

+ 4
- 0
css/modals/feedback/_feedback.scss Переглянути файл

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 Переглянути файл

30
         overflow: hidden;
30
         overflow: hidden;
31
         text-overflow: ellipsis;
31
         text-overflow: ellipsis;
32
         white-space: nowrap;
32
         white-space: nowrap;
33
+        margin-right: 5px;
33
     }
34
     }
34
 
35
 
35
     .info-password-none,
36
     .info-password-none,
36
     .info-password-remote {
37
     .info-password-remote {
37
-        opacity: 0.5;
38
+        color: #fff;
38
     }
39
     }
39
 
40
 
40
     .info-password-input {
41
     .info-password-input {
41
         width: 100%;
42
         width: 100%;
42
-        background-color: transparent;
43
-        border: none;
43
+        background-color: #0E1624;
44
+        border-radius: 3px;
45
+        border: 2px solid #202B3D;
44
         color: inherit;
46
         color: inherit;
45
         padding-left: 0;
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
     .info-password-local {
54
     .info-password-local {
49
         user-select: text;
55
         user-select: text;

+ 2
- 1
css/modals/invite/_invite_more.scss Переглянути файл

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

+ 18
- 1
css/modals/settings/_settings.scss Переглянути файл

16
     }
16
     }
17
 
17
 
18
     .mock-atlaskit-label {
18
     .mock-atlaskit-label {
19
-        color: #56637A;
19
+        color: #b8c7e0;
20
         font-size: 12px;
20
         font-size: 12px;
21
         font-weight: 600;
21
         font-weight: 600;
22
         line-height: 1.33;
22
         line-height: 1.33;
23
         padding: 20px 0px 4px 0px;
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
     input[type="checkbox"] + svg + span {
33
     input[type="checkbox"] + svg + span {
27
         color: #9FB0CC;
34
         color: #9FB0CC;
65
     .sign-out-cta {
72
     .sign-out-cta {
66
         margin-bottom: 20px;
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 Переглянути файл

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

+ 25
- 98
css/modals/virtual-background/_virtual-background.scss Переглянути файл

7
     grid-template-columns: auto auto auto auto auto;
7
     grid-template-columns: auto auto auto auto auto;
8
     column-gap: 9px;
8
     column-gap: 9px;
9
     cursor: pointer;
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
         opacity: .5;
11
         opacity: .5;
14
         border: 2px solid #99bbf3;
12
         border: 2px solid #99bbf3;
15
         @media (min-width: 432px) and (min-width: 432px) and (max-width: 632px) {
13
         @media (min-width: 432px) and (min-width: 432px) and (max-width: 632px) {
21
             width: 56px;
19
             width: 56px;
22
         }
20
         }
23
     }
21
     }
24
-    .thumbnail {
22
+    .background-option {
25
         margin-top: 8px;
23
         margin-top: 8px;
26
         border-radius: 6px;
24
         border-radius: 6px;
27
-        object-fit: cover;
28
         height: 60px;
25
         height: 60px;
29
         width: 107px;
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
     .thumbnail:hover ~ .delete-image-icon {
38
     .thumbnail:hover ~ .delete-image-icon {
33
         display: block;
39
         display: block;
34
     }
40
     }
35
     .thumbnail-selected {
41
     .thumbnail-selected {
36
-        margin-top: 8px;
37
-        border-radius: 6px;
38
         object-fit: cover;
42
         object-fit: cover;
39
-        height: 56px;
40
-        width: 103px;
41
         border: 2px solid #246FE5;
43
         border: 2px solid #246FE5;
42
     }
44
     }
43
     .blur{
45
     .blur{
44
         box-shadow: inset 0 0 12px #000000;
46
         box-shadow: inset 0 0 12px #000000;
45
-        margin-top: 8px;
46
         background: #7E8287;
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
     .blur-selected {
50
     .blur-selected {
56
         box-shadow: inset 0 0 12px #000000;
51
         box-shadow: inset 0 0 12px #000000;
57
-        margin-top: 8px;
58
         background: #7E8287;
52
         background: #7E8287;
59
-        font-weight: bold;
60
-        height: 56px;
61
-        width: 103px;
62
-        border-radius: 6px;
63
         border: 2px solid #246FE5;
53
         border: 2px solid #246FE5;
64
-        text-align: center;
65
-        vertical-align: middle;
66
-        line-height: 60px;
54
+        padding: 0 10px;
67
     }
55
     }
68
     .slight-blur{
56
     .slight-blur{
69
         box-shadow: inset 0 0 12px #000000;
57
         box-shadow: inset 0 0 12px #000000;
70
-        margin-top: 8px;
71
         background: #A4A4A4;
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
     .slight-blur-selected{
61
     .slight-blur-selected{
81
         box-shadow: inset 0 0 12px #000000;
62
         box-shadow: inset 0 0 12px #000000;
82
-        margin-top: 8px;
83
         background: #A4A4A4;
63
         background: #A4A4A4;
84
-        font-weight: bold;
85
-        height: 56px;
86
-        width: 103px;
87
-        border-radius: 6px;
88
         border: 2px solid #246FE5;
64
         border: 2px solid #246FE5;
89
-        text-align: center;
90
-        vertical-align: middle;
91
-        line-height: 60px;
65
+        padding: 0 10px;
92
     }
66
     }
93
     .virtual-background-none {
67
     .virtual-background-none {
94
-        margin-top: 8px;
95
         background: #525252;
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
     .none-selected {
71
     .none-selected {
105
-        margin-top: 8px;
106
         background: #525252;
72
         background: #525252;
107
-        font-weight: bold;
108
-        height: 56px;
109
-        width: 103px;
110
-        border-radius: 6px;
111
         border: 2px solid #246FE5;
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
     .desktop-share{
77
     .desktop-share{
117
-        margin-top: 8px;
118
         background: #525252;
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
     .desktop-share-selected{
80
     .desktop-share-selected{
128
-        margin-top: 8px;
129
         background: #525252;
81
         background: #525252;
130
-        font-weight: bold;
131
-        height: 56px;
132
-        width: 103px;
133
-        border-radius: 6px;
134
         border: 2px solid #246FE5;
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
     @media (min-width: 432px) and (max-width: 632px) {
86
     @media (min-width: 432px) and (max-width: 632px) {
144
         font-size: 1.5vw;
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
     @media (max-width: 432px){
89
     @media (max-width: 432px){
158
-        .share-desktop-icon{
159
-            margin-top: 25%;
160
-        }
161
         font-size: 1.5vw;
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
         left: 51px
126
         left: 51px
206
     }
127
     }
207
 }
128
 }
129
+
208
 .delete-image-icon:hover {
130
 .delete-image-icon:hover {
209
     display: block;
131
     display: block;
210
 }
132
 }
211
 
133
 
212
 .thumbnail-container {
134
 .thumbnail-container {
213
     position: relative;
135
     position: relative;
136
+    &:focus-within {
137
+        .thumbnail ~ .delete-image-icon{
138
+           display: block;
139
+       }
140
+    }
214
 }
141
 }
215
 
142
 
216
 .add-background{
143
 .add-background{

+ 1
- 1
css/themes/_light.scss Переглянути файл

107
 /**
107
 /**
108
  * TODO: Replace by themed component.
108
  * TODO: Replace by themed component.
109
  */
109
  */
110
-$videoQualityActive: #4C9AFF;
110
+$videoQualityActive: #57A0ff;

+ 2
- 2
index.html Переглянути файл

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

+ 72
- 19
lang/main-de.json Переглянути файл

70
         },
70
         },
71
         "privateNotice": "Private Nachricht an {{recipient}}",
71
         "privateNotice": "Private Nachricht an {{recipient}}",
72
         "title": "Chatten",
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
     "chromeExtensionBanner": {
79
     "chromeExtensionBanner": {
76
         "installExtensionText": "Installieren Sie die Erweiterung für die Integration von Google Calendar und Office 365",
80
         "installExtensionText": "Installieren Sie die Erweiterung für die Integration von Google Calendar und Office 365",
77
         "buttonText": "Chrome-Erweiterung installieren",
81
         "buttonText": "Chrome-Erweiterung installieren",
78
-        "dontShowAgain": "Hinweis nicht mehr anzeigen"
82
+        "dontShowAgain": "Hinweis nicht mehr anzeigen",
83
+        "close": "Schließen"
79
     },
84
     },
80
     "connectingOverlay": {
85
     "connectingOverlay": {
81
         "joiningRoom": "Eine Verbindung zu Ihrem Meeting wird hergestellt…"
86
         "joiningRoom": "Eine Verbindung zu Ihrem Meeting wird hergestellt…"
204
         "e2eeLabel": "Ende-zu-Ende-Verschlüsselung aktivieren",
209
         "e2eeLabel": "Ende-zu-Ende-Verschlüsselung aktivieren",
205
         "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.",
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
         "enterDisplayName": "Bitte geben Sie hier Ihren Namen ein",
211
         "enterDisplayName": "Bitte geben Sie hier Ihren Namen ein",
212
+        "enterDisplayNameToJoin" : "Benutzername für Konferenz eingeben" ,
213
+        "embedMeeting": "Besprechung einbetten",
207
         "error": "Fehler",
214
         "error": "Fehler",
208
         "gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
215
         "gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
209
         "grantModeratorDialog": "Möchten Sie wirklich Moderationsrechte an diese Person vergeben?",
216
         "grantModeratorDialog": "Möchten Sie wirklich Moderationsrechte an diese Person vergeben?",
295
         "Share": "Teilen",
302
         "Share": "Teilen",
296
         "shareVideoLinkError": "Bitte einen gültigen YouTube-Link angeben.",
303
         "shareVideoLinkError": "Bitte einen gültigen YouTube-Link angeben.",
297
         "shareVideoTitle": "Video teilen",
304
         "shareVideoTitle": "Video teilen",
298
-        "shareYourScreen": "Bildschirm freigeben",
305
+        "shareYourScreen": "Bildschirmfreigabe ein-/ausschalten",
299
         "shareYourScreenDisabled": "Bildschirmfreigabe deaktiviert.",
306
         "shareYourScreenDisabled": "Bildschirmfreigabe deaktiviert.",
300
         "startLiveStreaming": "Livestream starten",
307
         "startLiveStreaming": "Livestream starten",
301
         "startRecording": "Aufnahme starten",
308
         "startRecording": "Aufnahme starten",
320
         "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.",
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
         "WaitingForHostTitle": "Warten auf den Beginn der Konferenz …",
328
         "WaitingForHostTitle": "Warten auf den Beginn der Konferenz …",
322
         "Yes": "Ja",
329
         "Yes": "Ja",
323
-        "yourEntireScreen": "Ganzer Bildschirm"
330
+        "yourEntireScreen": "Ganzer Bildschirm",
331
+        "remoteUserControls": "Remote Benutzersteuerung von {{username}}",
332
+        "localUserControls": "Lokale Benutzersteuerung"
324
     },
333
     },
325
     "dialOut": {
334
     "dialOut": {
326
         "statusMessage": "ist jetzt {{status}}"
335
         "statusMessage": "ist jetzt {{status}}"
340
         "slightBlur": "Hintergrund leicht unscharf",
349
         "slightBlur": "Hintergrund leicht unscharf",
341
         "removeBackground": "Hintergrund entfernen",
350
         "removeBackground": "Hintergrund entfernen",
342
         "uploadImage": "Bild hochladen",
351
         "uploadImage": "Bild hochladen",
352
+        "addBackground": "Hintergrund hinzufügen",
343
         "pleaseWait": "Bitte warten...",
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
     "feedback": {
367
     "feedback": {
347
         "average": "Durchschnittlich",
368
         "average": "Durchschnittlich",
350
         "good": "Gut",
371
         "good": "Gut",
351
         "rateExperience": "Bitte bewerten Sie diese Konferenz",
372
         "rateExperience": "Bitte bewerten Sie diese Konferenz",
352
         "veryBad": "Sehr schlecht",
373
         "veryBad": "Sehr schlecht",
353
-        "veryGood": "Sehr gut"
374
+        "veryGood": "Sehr gut",
375
+        "star": "Sterne"
354
     },
376
     },
355
     "incomingCall": {
377
     "incomingCall": {
356
         "answer": "Antworten",
378
         "answer": "Antworten",
367
         "country": "Land",
389
         "country": "Land",
368
         "dialANumber": "Um am Meeting teilzunehmen, müssen Sie eine dieser Nummern wählen und dann die PIN eingeben.",
390
         "dialANumber": "Um am Meeting teilzunehmen, müssen Sie eine dieser Nummern wählen und dann die PIN eingeben.",
369
         "dialInConferenceID": "PIN:",
391
         "dialInConferenceID": "PIN:",
392
+        "copyNumber":"Nummer kopieren",
370
         "dialInNotSupported": "Entschuldigung, leider wird das Einwählen derzeit nicht unterstützt.",
393
         "dialInNotSupported": "Entschuldigung, leider wird das Einwählen derzeit nicht unterstützt.",
371
         "dialInNumber": "Einwählen:",
394
         "dialInNumber": "Einwählen:",
372
         "dialInSummaryError": "Fehler beim Abrufen der Einwahlinformationen. Versuchen Sie es später erneut.",
395
         "dialInSummaryError": "Fehler beim Abrufen der Einwahlinformationen. Versuchen Sie es später erneut.",
404
         "support": "Support",
427
         "support": "Support",
405
         "supportMsg": "Wenn der Fehler erneut auftritt, bitte kontaktieren Sie"
428
         "supportMsg": "Wenn der Fehler erneut auftritt, bitte kontaktieren Sie"
406
     },
429
     },
430
+    "jitsiHome": "{{logo}} Logo, verlinkt zur Homepage",
407
     "keyboardShortcuts": {
431
     "keyboardShortcuts": {
408
         "focusLocal": "Lokales Video fokussieren",
432
         "focusLocal": "Lokales Video fokussieren",
409
         "focusRemote": "Auf das Video einer anderen Person fokussieren",
433
         "focusRemote": "Auf das Video einer anderen Person fokussieren",
524
         "OldElectronAPPTitle": "Sicherheitslücke!",
548
         "OldElectronAPPTitle": "Sicherheitslücke!",
525
         "oldElectronClientDescription1": "Sie scheinen eine alte Version des Jitsi-Meet-Clients zu nutzen. Diese hat bekannte Schwachstellen. Bitte aktualisieren Sie auf unsere ",
549
         "oldElectronClientDescription1": "Sie scheinen eine alte Version des Jitsi-Meet-Clients zu nutzen. Diese hat bekannte Schwachstellen. Bitte aktualisieren Sie auf unsere ",
526
         "oldElectronClientDescription2": "aktuelle Version",
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
     "participantsPane": {
565
     "participantsPane": {
530
         "headings": {
566
         "headings": {
589
         "linkCopied": "Link in die Zwischenablage kopiert",
625
         "linkCopied": "Link in die Zwischenablage kopiert",
590
         "lookGood": "Ihr Mikrofon scheint zu funktionieren.",
626
         "lookGood": "Ihr Mikrofon scheint zu funktionieren.",
591
         "or": "oder",
627
         "or": "oder",
628
+        "keyboardShortcuts" : "Tastaturkurzbefehle aktivieren",
592
         "premeeting": "Vorschau",
629
         "premeeting": "Vorschau",
593
         "showScreen": "Konferenzvorschau aktivieren",
630
         "showScreen": "Konferenzvorschau aktivieren",
594
         "startWithPhone": "Mit Telefonaudio starten",
631
         "startWithPhone": "Mit Telefonaudio starten",
612
         "ringing": "Es klingelt …"
649
         "ringing": "Es klingelt …"
613
     },
650
     },
614
     "profile": {
651
     "profile": {
652
+        "avatar": "Benutzerbild",
615
         "setDisplayNameLabel": "Anzeigename festlegen",
653
         "setDisplayNameLabel": "Anzeigename festlegen",
616
         "setEmailInput": "E-Mail eingeben",
654
         "setEmailInput": "E-Mail eingeben",
617
         "setEmailLabel": "E-Mail-Adresse für Gravatar",
655
         "setEmailLabel": "E-Mail-Adresse für Gravatar",
728
         "title": "Die Konferenz wurde unterbrochen, weil der Standby-Modus aktiviert wurde."
766
         "title": "Die Konferenz wurde unterbrochen, weil der Standby-Modus aktiviert wurde."
729
     },
767
     },
730
     "toolbar": {
768
     "toolbar": {
731
-        "accessibilityLabel": {
769
+            "accessibilityLabel": {
732
             "audioOnly": "„Nur Audio“ ein-/ausschalten",
770
             "audioOnly": "„Nur Audio“ ein-/ausschalten",
733
             "audioRoute": "Audiogerät auswählen",
771
             "audioRoute": "Audiogerät auswählen",
734
             "callQuality": "Qualitätseinstellungen",
772
             "callQuality": "Qualitätseinstellungen",
735
             "cc": "Untertitel ein-/ausschalten",
773
             "cc": "Untertitel ein-/ausschalten",
736
-            "chat": "Chatfenster ein-/ausblenden",
774
+            "chat": "Chatfenster öffnen / schließen",
737
             "document": "Geteiltes Dokument schließen",
775
             "document": "Geteiltes Dokument schließen",
738
             "download": "Unsere Apps herunterladen",
776
             "download": "Unsere Apps herunterladen",
739
             "embedMeeting": "Konferenz einbetten",
777
             "embedMeeting": "Konferenz einbetten",
740
             "feedback": "Feedback hinterlassen",
778
             "feedback": "Feedback hinterlassen",
741
             "fullScreen": "Vollbildmodus ein-/ausschalten",
779
             "fullScreen": "Vollbildmodus ein-/ausschalten",
742
             "grantModerator": "Moderationsrechte vergeben",
780
             "grantModerator": "Moderationsrechte vergeben",
743
-            "hangup": "Anruf beenden",
781
+            "hangup": "Konferenz verlassen",
744
             "help": "Hilfe",
782
             "help": "Hilfe",
745
             "invite": "Person einladen",
783
             "invite": "Person einladen",
746
             "kick": "Person entfernen",
784
             "kick": "Person entfernen",
747
             "lobbyButton": "Lobbymodus ein-/ausschalten",
785
             "lobbyButton": "Lobbymodus ein-/ausschalten",
748
             "localRecording": "Lokale Aufzeichnungssteuerelemente ein-/ausschalten",
786
             "localRecording": "Lokale Aufzeichnungssteuerelemente ein-/ausschalten",
749
             "lockRoom": "Konferenzpasswort ein-/ausschalten",
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
             "moreOptions": "Menü „Weitere Optionen“",
790
             "moreOptions": "Menü „Weitere Optionen“",
753
-            "mute": "„Audio stummschalten“ ein-/ausschalten",
791
+            "mute": "Mikrofon aktivieren / deaktivieren",
754
             "muteEveryone": "Alle stummschalten",
792
             "muteEveryone": "Alle stummschalten",
755
             "muteEveryoneElse": "Alle anderen stummschalten",
793
             "muteEveryoneElse": "Alle anderen stummschalten",
756
             "muteEveryonesVideo": "Alle Kameras ausschalten",
794
             "muteEveryonesVideo": "Alle Kameras ausschalten",
759
             "pip": "Bild-in-Bild-Modus ein-/ausschalten",
797
             "pip": "Bild-in-Bild-Modus ein-/ausschalten",
760
             "privateMessage": "Private Nachricht senden",
798
             "privateMessage": "Private Nachricht senden",
761
             "profile": "Profil bearbeiten",
799
             "profile": "Profil bearbeiten",
762
-            "raiseHand": "„Melden“ ein-/ausschalten",
800
+            "raiseHand": "Hand erheben / senken",
763
             "recording": "Aufzeichnung ein-/ausschalten",
801
             "recording": "Aufzeichnung ein-/ausschalten",
764
             "remoteMute": "Personen stummschalten",
802
             "remoteMute": "Personen stummschalten",
765
             "remoteVideoMute": "Kamera von dieser Person ausschalten",
803
             "remoteVideoMute": "Kamera von dieser Person ausschalten",
776
             "toggleCamera": "Kamera wechseln",
814
             "toggleCamera": "Kamera wechseln",
777
             "toggleFilmstrip": "Miniaturansichten ein-/ausschalten",
815
             "toggleFilmstrip": "Miniaturansichten ein-/ausschalten",
778
             "videomute": "„Video stummschalten“ ein-/ausschalten",
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
         "addPeople": "Personen zur Konferenz hinzufügen",
821
         "addPeople": "Personen zur Konferenz hinzufügen",
782
         "audioSettings": "Ton-Einstellungen",
822
         "audioSettings": "Ton-Einstellungen",
797
         "exitFullScreen": "Vollbildmodus verlassen",
837
         "exitFullScreen": "Vollbildmodus verlassen",
798
         "exitTileView": "Kachelansicht ausschalten",
838
         "exitTileView": "Kachelansicht ausschalten",
799
         "feedback": "Feedback hinterlassen",
839
         "feedback": "Feedback hinterlassen",
800
-        "hangup": "Verlassen",
840
+        "hangup": "Konferenz verlassen",
801
         "help": "Hilfe",
841
         "help": "Hilfe",
802
         "invite": "Personen einladen",
842
         "invite": "Personen einladen",
803
         "lobbyButtonDisable": "Lobbymodus deaktivieren",
843
         "lobbyButtonDisable": "Lobbymodus deaktivieren",
807
         "lowerYourHand": "Hand senken",
847
         "lowerYourHand": "Hand senken",
808
         "moreActions": "Weitere Einstellungen",
848
         "moreActions": "Weitere Einstellungen",
809
         "moreOptions": "Weitere Optionen",
849
         "moreOptions": "Weitere Optionen",
810
-        "mute": "Stummschaltung aktivieren / deaktivieren",
850
+        "mute": "Mikrofon aktivieren / deaktivieren",
811
         "muteEveryone": "Alle stummschalten",
851
         "muteEveryone": "Alle stummschalten",
812
         "muteEveryonesVideo": "Alle Kameras ausschalten",
852
         "muteEveryonesVideo": "Alle Kameras ausschalten",
813
         "noAudioSignalTitle": "Es kommt kein Input von Ihrem Mikrofon!",
853
         "noAudioSignalTitle": "Es kommt kein Input von Ihrem Mikrofon!",
822
         "pip": "Bild-in-Bild-Modus einschalten",
862
         "pip": "Bild-in-Bild-Modus einschalten",
823
         "privateMessage": "Private Nachricht senden",
863
         "privateMessage": "Private Nachricht senden",
824
         "profile": "Profil bearbeiten",
864
         "profile": "Profil bearbeiten",
825
-        "raiseHand": "Hand erheben",
865
+        "raiseHand": "Hand erheben / senken",
826
         "raiseYourHand": "Melden",
866
         "raiseYourHand": "Melden",
827
         "security": "Sicherheitsoptionen",
867
         "security": "Sicherheitsoptionen",
828
         "Settings": "Einstellungen",
868
         "Settings": "Einstellungen",
867
         "react-nativeGrantPermissions": "Wählen Sie <b><i>Erlauben</i></b>, wenn der Browser um Berechtigungen bittet.",
907
         "react-nativeGrantPermissions": "Wählen Sie <b><i>Erlauben</i></b>, wenn der Browser um Berechtigungen bittet.",
868
         "safariGrantPermissions": "Wählen Sie <b><i>OK</i></b>, wenn der Browser um Berechtigungen bittet."
908
         "safariGrantPermissions": "Wählen Sie <b><i>OK</i></b>, wenn der Browser um Berechtigungen bittet."
869
     },
909
     },
910
+    "volumeSlider": "Lautstärkeregler",
870
     "videoSIPGW": {
911
     "videoSIPGW": {
871
         "busy": "Es stehen keine freien Ressourcen zur Verfügung. Bitte versuchen Sie es später noch einmal.",
912
         "busy": "Es stehen keine freien Ressourcen zur Verfügung. Bitte versuchen Sie es später noch einmal.",
872
         "busyTitle": "Keine freien Ressourcen",
913
         "busyTitle": "Keine freien Ressourcen",
911
         "videomute": "Person hat die Kamera angehalten"
952
         "videomute": "Person hat die Kamera angehalten"
912
     },
953
     },
913
     "welcomepage": {
954
     "welcomepage": {
955
+        "addMeetingName": "Besprechungsnamen hinzufügen",
914
         "accessibilityLabel": {
956
         "accessibilityLabel": {
915
             "join": "Zum Teilnehmen tippen",
957
             "join": "Zum Teilnehmen tippen",
916
             "roomname": "Konferenzname eingeben"
958
             "roomname": "Konferenzname eingeben"
933
         "join": "ERSTELLEN / BEITRETEN",
975
         "join": "ERSTELLEN / BEITRETEN",
934
         "jitsiOnMobile": "Jitsi unterwegs – einfach unsere Apps herunterladen und Meetings von überall starten",
976
         "jitsiOnMobile": "Jitsi unterwegs – einfach unsere Apps herunterladen und Meetings von überall starten",
935
         "moderatedMessage": "Oder <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservieren Sie sich eine Konferenz-URL</a>, die nur Sie moderieren.",
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
         "privacy": "Datenschutz",
981
         "privacy": "Datenschutz",
937
         "recentList": "Verlauf",
982
         "recentList": "Verlauf",
938
         "recentListDelete": "Eintrag löschen",
983
         "recentListDelete": "Eintrag löschen",
944
         "sendFeedback": "Feedback senden",
989
         "sendFeedback": "Feedback senden",
945
         "startMeeting": "Meeting starten",
990
         "startMeeting": "Meeting starten",
946
         "terms": "AGB",
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
     "lonelyMeetingExperience": {
1002
     "lonelyMeetingExperience": {
950
         "button": "Andere einladen",
1003
         "button": "Andere einladen",

+ 60
- 17
lang/main.json Переглянути файл

70
         },
70
         },
71
         "privateNotice": "Private message to {{recipient}}",
71
         "privateNotice": "Private message to {{recipient}}",
72
         "title": "Chat",
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
     "chromeExtensionBanner": {
79
     "chromeExtensionBanner": {
76
         "installExtensionText": "Install the extension for Google Calendar and Office 365 integration",
80
         "installExtensionText": "Install the extension for Google Calendar and Office 365 integration",
77
         "buttonText": "Install Chrome Extension",
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
     "connectingOverlay": {
85
     "connectingOverlay": {
81
         "joiningRoom": "Connecting you to your meeting..."
86
         "joiningRoom": "Connecting you to your meeting..."
204
         "e2eeLabel": "Enable End-to-End Encryption",
209
         "e2eeLabel": "Enable End-to-End Encryption",
205
         "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.",
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
         "enterDisplayName": "Please enter your name here",
211
         "enterDisplayName": "Please enter your name here",
212
+        "enterDisplayNameToJoin": "Please enter your name to join",
213
+        "embedMeeting": "Embed meeting",
207
         "error": "Error",
214
         "error": "Error",
208
         "gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
215
         "gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
209
         "grantModeratorDialog": "Are you sure you want to make this participant a moderator?",
216
         "grantModeratorDialog": "Are you sure you want to make this participant a moderator?",
320
         "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.",
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
         "WaitingForHostTitle": "Waiting for the host ...",
328
         "WaitingForHostTitle": "Waiting for the host ...",
322
         "Yes": "Yes",
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
     "dialOut": {
334
     "dialOut": {
326
         "statusMessage": "is now {{status}}"
335
         "statusMessage": "is now {{status}}"
341
         "slightBlur": "Slight Blur",
350
         "slightBlur": "Slight Blur",
342
         "removeBackground": "Remove background",
351
         "removeBackground": "Remove background",
343
         "addBackground": "Add background",
352
         "addBackground": "Add background",
353
+        "pleaseWait": "Please wait...",
344
         "none": "None",
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
     "feedback": {
367
     "feedback": {
348
         "average": "Average",
368
         "average": "Average",
351
         "good": "Good",
371
         "good": "Good",
352
         "rateExperience": "Rate your meeting experience",
372
         "rateExperience": "Rate your meeting experience",
353
         "veryBad": "Very Bad",
373
         "veryBad": "Very Bad",
354
-        "veryGood": "Very Good"
374
+        "veryGood": "Very Good",
375
+        "star": "Star"
355
     },
376
     },
356
     "incomingCall": {
377
     "incomingCall": {
357
         "answer": "Answer",
378
         "answer": "Answer",
368
         "country": "Country",
389
         "country": "Country",
369
         "dialANumber": "To join your meeting, dial one of these numbers and then enter the pin.",
390
         "dialANumber": "To join your meeting, dial one of these numbers and then enter the pin.",
370
         "dialInConferenceID": "PIN:",
391
         "dialInConferenceID": "PIN:",
392
+        "copyNumber":"Copy number",
371
         "dialInNotSupported": "Sorry, dialing in is currently not supported.",
393
         "dialInNotSupported": "Sorry, dialing in is currently not supported.",
372
         "dialInNumber": "Dial-in:",
394
         "dialInNumber": "Dial-in:",
373
         "dialInSummaryError": "Error fetching dial-in info now. Please try again later.",
395
         "dialInSummaryError": "Error fetching dial-in info now. Please try again later.",
405
         "support": "Support",
427
         "support": "Support",
406
         "supportMsg": "If this keeps happening, reach out to"
428
         "supportMsg": "If this keeps happening, reach out to"
407
     },
429
     },
430
+    "jitsiHome": "{{logo}} Logo, links to  Homepage",
408
     "keyboardShortcuts": {
431
     "keyboardShortcuts": {
409
         "focusLocal": "Focus on your video",
432
         "focusLocal": "Focus on your video",
410
         "focusRemote": "Focus on another person's video",
433
         "focusRemote": "Focus on another person's video",
525
         "OldElectronAPPTitle": "Security vulnerability!",
548
         "OldElectronAPPTitle": "Security vulnerability!",
526
         "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 ",
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
         "oldElectronClientDescription2": "latest build",
550
         "oldElectronClientDescription2": "latest build",
528
-        "oldElectronClientDescription3": " now!"
551
+        "oldElectronClientDescription3": " now!",
552
+        "groupTitle": "Notifications"
529
     },
553
     },
530
     "participantsPane": {
554
     "participantsPane": {
555
+        "close": "Close",
531
         "headings": {
556
         "headings": {
532
             "lobby": "Lobby ({{count}})",
557
             "lobby": "Lobby ({{count}})",
533
             "participantsList": "Meeting participants ({{count}})"
558
             "participantsList": "Meeting participants ({{count}})"
592
         "or": "or",
617
         "or": "or",
593
         "premeeting": "Pre meeting",
618
         "premeeting": "Pre meeting",
594
         "showScreen": "Enable pre meeting screen",
619
         "showScreen": "Enable pre meeting screen",
620
+        "keyboardShortcuts" : "Enable Keyboard shortcuts",
595
         "startWithPhone": "Start with phone audio",
621
         "startWithPhone": "Start with phone audio",
596
         "screenSharingError": "Screen sharing error:",
622
         "screenSharingError": "Screen sharing error:",
597
         "videoOnlyError": "Video error:",
623
         "videoOnlyError": "Video error:",
613
         "ringing": "Ringing..."
639
         "ringing": "Ringing..."
614
     },
640
     },
615
     "profile": {
641
     "profile": {
642
+        "avatar": "avatar",
616
         "setDisplayNameLabel": "Set your display name",
643
         "setDisplayNameLabel": "Set your display name",
617
         "setEmailInput": "Enter e-mail",
644
         "setEmailInput": "Enter e-mail",
618
         "setEmailLabel": "Set your gravatar email",
645
         "setEmailLabel": "Set your gravatar email",
735
             "audioRoute": "Select the sound device",
762
             "audioRoute": "Select the sound device",
736
             "callQuality": "Manage video quality",
763
             "callQuality": "Manage video quality",
737
             "cc": "Toggle subtitles",
764
             "cc": "Toggle subtitles",
738
-            "chat": "Toggle chat window",
765
+            "chat": "Open / Close chat",
739
             "document": "Toggle shared document",
766
             "document": "Toggle shared document",
740
             "download": "Download our apps",
767
             "download": "Download our apps",
741
             "embedMeeting": "Embed meeting",
768
             "embedMeeting": "Embed meeting",
742
             "feedback": "Leave feedback",
769
             "feedback": "Leave feedback",
743
             "fullScreen": "Toggle full screen",
770
             "fullScreen": "Toggle full screen",
744
             "grantModerator": "Grant Moderator",
771
             "grantModerator": "Grant Moderator",
745
-            "hangup": "Leave the call",
772
+            "hangup": "Leave the meeting",
746
             "help": "Help",
773
             "help": "Help",
747
             "invite": "Invite people",
774
             "invite": "Invite people",
748
             "kick": "Kick participant",
775
             "kick": "Kick participant",
749
             "lobbyButton": "Enable/disable lobby mode",
776
             "lobbyButton": "Enable/disable lobby mode",
750
             "localRecording": "Toggle local recording controls",
777
             "localRecording": "Toggle local recording controls",
751
             "lockRoom": "Toggle meeting password",
778
             "lockRoom": "Toggle meeting password",
752
-            "moreActions": "Toggle more actions menu",
779
+            "moreActions": "More actions",
753
             "moreActionsMenu": "More actions menu",
780
             "moreActionsMenu": "More actions menu",
754
             "moreOptions": "Show more options",
781
             "moreOptions": "Show more options",
755
-            "mute": "Toggle mute audio",
782
+            "mute": "Mute / Unmute",
756
             "muteEveryone": "Mute everyone",
783
             "muteEveryone": "Mute everyone",
757
             "muteEveryoneElse": "Mute everyone else",
784
             "muteEveryoneElse": "Mute everyone else",
758
             "muteEveryonesVideo": "Disable everyone's camera",
785
             "muteEveryonesVideo": "Disable everyone's camera",
761
             "pip": "Toggle Picture-in-Picture mode",
788
             "pip": "Toggle Picture-in-Picture mode",
762
             "privateMessage": "Send private message",
789
             "privateMessage": "Send private message",
763
             "profile": "Edit your profile",
790
             "profile": "Edit your profile",
764
-            "raiseHand": "Toggle raise hand",
791
+            "raiseHand": "Raise / Lower your hand",
765
             "recording": "Toggle recording",
792
             "recording": "Toggle recording",
766
             "remoteMute": "Mute participant",
793
             "remoteMute": "Mute participant",
767
             "remoteVideoMute": "Disable camera of participant",
794
             "remoteVideoMute": "Disable camera of participant",
770
             "shareaudio": "Share audio",
797
             "shareaudio": "Share audio",
771
             "sharedvideo": "Toggle YouTube video sharing",
798
             "sharedvideo": "Toggle YouTube video sharing",
772
             "shareRoom": "Invite someone",
799
             "shareRoom": "Invite someone",
773
-            "shareYourScreen": "Toggle screenshare",
800
+            "shareYourScreen": "Start / Stop sharing your screen",
774
             "shortcuts": "Toggle shortcuts",
801
             "shortcuts": "Toggle shortcuts",
775
             "show": "Show on stage",
802
             "show": "Show on stage",
776
             "speakerStats": "Toggle speaker statistics",
803
             "speakerStats": "Toggle speaker statistics",
777
             "tileView": "Toggle tile view",
804
             "tileView": "Toggle tile view",
778
             "toggleCamera": "Toggle camera",
805
             "toggleCamera": "Toggle camera",
779
             "toggleFilmstrip": "Toggle filmstrip",
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
         "addPeople": "Add people to your call",
813
         "addPeople": "Add people to your call",
784
         "audioSettings": "Audio settings",
814
         "audioSettings": "Audio settings",
815
+        "videoSettings": "Video settings",
785
         "audioOnlyOff": "Disable low bandwidth mode",
816
         "audioOnlyOff": "Disable low bandwidth mode",
786
         "audioOnlyOn": "Enable low bandwidth mode",
817
         "audioOnlyOn": "Enable low bandwidth mode",
787
         "audioRoute": "Select the sound device",
818
         "audioRoute": "Select the sound device",
799
         "exitFullScreen": "Exit full screen",
830
         "exitFullScreen": "Exit full screen",
800
         "exitTileView": "Exit tile view",
831
         "exitTileView": "Exit tile view",
801
         "feedback": "Leave feedback",
832
         "feedback": "Leave feedback",
802
-        "hangup": "Leave",
833
+        "hangup": "Leave the meeting",
803
         "help": "Help",
834
         "help": "Help",
804
         "invite": "Invite people",
835
         "invite": "Invite people",
805
         "lobbyButtonDisable": "Disable lobby mode",
836
         "lobbyButtonDisable": "Disable lobby mode",
842
         "tileViewToggle": "Toggle tile view",
873
         "tileViewToggle": "Toggle tile view",
843
         "toggleCamera": "Toggle camera",
874
         "toggleCamera": "Toggle camera",
844
         "videomute": "Start / Stop camera",
875
         "videomute": "Start / Stop camera",
845
-        "videoSettings": "Video settings",
846
         "selectBackground": "Select background"
876
         "selectBackground": "Select background"
847
     },
877
     },
848
     "transcribing": {
878
     "transcribing": {
869
         "react-nativeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
899
         "react-nativeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
870
         "safariGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions."
900
         "safariGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions."
871
     },
901
     },
902
+    "volumeSlider": "Volume slider",
872
     "videoSIPGW": {
903
     "videoSIPGW": {
873
         "busy": "We're working on freeing resources. Please try again in a few minutes.",
904
         "busy": "We're working on freeing resources. Please try again in a few minutes.",
874
         "busyTitle": "The Room service is currently busy",
905
         "busyTitle": "The Room service is currently busy",
913
         "videomute": "Participant has stopped the camera"
944
         "videomute": "Participant has stopped the camera"
914
     },
945
     },
915
     "welcomepage": {
946
     "welcomepage": {
947
+        "addMeetingName": "Add Meeting name",
916
         "accessibilityLabel": {
948
         "accessibilityLabel": {
917
             "join": "Tap to join",
949
             "join": "Tap to join",
918
             "roomname": "Enter room name"
950
             "roomname": "Enter room name"
934
         "info": "Dial-in info",
966
         "info": "Dial-in info",
935
         "join": "CREATE / JOIN",
967
         "join": "CREATE / JOIN",
936
         "jitsiOnMobile": "Jitsi on mobile – download our apps and start a meeting from anywhere",
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
         "moderatedMessage": "Or <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">book a meeting URL</a> in advance where you are the only moderator.",
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
         "privacy": "Privacy",
973
         "privacy": "Privacy",
939
         "recentList": "Recent",
974
         "recentList": "Recent",
946
         "sendFeedback": "Send feedback",
981
         "sendFeedback": "Send feedback",
947
         "startMeeting": "Start meeting",
982
         "startMeeting": "Start meeting",
948
         "terms": "Terms",
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
     "lonelyMeetingExperience": {
994
     "lonelyMeetingExperience": {
952
         "button": "Invite others",
995
         "button": "Invite others",

+ 55
- 18
modules/keyboardshortcut/keyboardshortcut.js Переглянути файл

1
-/* global APP, $ */
2
-
1
+/* global APP */
2
+import { jitsiLocalStorage } from '@jitsi/js-utils';
3
 import Logger from 'jitsi-meet-logger';
3
 import Logger from 'jitsi-meet-logger';
4
 
4
 
5
 import {
5
 import {
30
 const _shortcutsHelp = new Map();
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
  * Maps keycode to character, id of popover for given function and function.
62
  * Maps keycode to character, id of popover for given function and function.
40
  */
63
  */
41
 const KeyboardShortcut = {
64
 const KeyboardShortcut = {
65
+    isPushToTalkActive: false,
66
+
42
     init() {
67
     init() {
43
         this._initGlobalShortcuts();
68
         this._initGlobalShortcuts();
44
 
69
 
45
         window.onkeyup = e => {
70
         window.onkeyup = e => {
46
-            if (!enabled) {
71
+            if (!this.getEnabled()) {
47
                 return;
72
                 return;
48
             }
73
             }
49
             const key = this._getKeyboardKey(e).toUpperCase();
74
             const key = this._getKeyboardKey(e).toUpperCase();
50
             const num = parseInt(key, 10);
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
                 if (_shortcuts.has(key)) {
78
                 if (_shortcuts.has(key)) {
56
                     _shortcuts.get(key).function(e);
79
                     _shortcuts.get(key).function(e);
57
                 } else if (!isNaN(num) && num >= 0 && num <= 9) {
80
                 } else if (!isNaN(num) && num >= 0 && num <= 9) {
58
                     APP.store.dispatch(clickOnVideo(num));
81
                     APP.store.dispatch(clickOnVideo(num));
59
                 }
82
                 }
60
-
61
             }
83
             }
62
         };
84
         };
63
 
85
 
64
         window.onkeydown = e => {
86
         window.onkeydown = e => {
65
-            if (!enabled) {
87
+            if (!this.getEnabled()) {
66
                 return;
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
                 if (this._getKeyboardKey(e).toUpperCase() === ' ') {
93
                 if (this._getKeyboardKey(e).toUpperCase() === ' ') {
72
                     if (APP.conference.isLocalAudioMuted()) {
94
                     if (APP.conference.isLocalAudioMuted()) {
73
                         sendAnalytics(createShortcutEvent(
95
                         sendAnalytics(createShortcutEvent(
75
                             PRESSED));
97
                             PRESSED));
76
                         logger.log('Talk shortcut pressed');
98
                         logger.log('Talk shortcut pressed');
77
                         APP.conference.muteAudio(false);
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
      * @param {boolean} value - the new value.
114
      * @param {boolean} value - the new value.
87
      */
115
      */
88
     enable(value) {
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
         // register SPACE shortcut in two steps to insure visibility of help
232
         // register SPACE shortcut in two steps to insure visibility of help
199
         // message
233
         // message
200
         this.registerShortcut(' ', null, () => {
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
         this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk');
242
         this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk');
206
 
243
 

+ 10
- 1
modules/translation/translation.js Переглянути файл

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

+ 42
- 5
package-lock.json Переглянути файл

15262
       }
15262
       }
15263
     },
15263
     },
15264
     "react-textarea-autosize": {
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
       "requires": {
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
     "react-transition-group": {
15284
     "react-transition-group": {
17690
       "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==",
17701
       "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==",
17691
       "dev": true
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
     "tslib": {
17709
     "tslib": {
17694
       "version": "1.9.3",
17710
       "version": "1.9.3",
17695
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
17711
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
17975
       "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz",
17991
       "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz",
17976
       "integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg=="
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
     "use-memo-one": {
18015
     "use-memo-one": {
17979
       "version": "1.1.2",
18016
       "version": "1.1.2",
17980
       "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz",
18017
       "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz",

+ 1
- 1
package.json Переглянути файл

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

+ 13
- 3
react/features/base/avatar/components/web/StatelessAvatar.js Переглянути файл

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

+ 31
- 6
react/features/base/buttons/CopyButton.js Переглянути файл

3
 import React, { useState } from 'react';
3
 import React, { useState } from 'react';
4
 
4
 
5
 import { Icon, IconCheck, IconCopy } from '../../base/icons';
5
 import { Icon, IconCheck, IconCopy } from '../../base/icons';
6
-import { translate } from '../i18n';
7
 import { copyText } from '../util';
6
 import { copyText } from '../util';
8
 
7
 
9
 
8
 
32
     /**
31
     /**
33
      * The text displayed on copy success
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
  *
44
  *
41
  * @returns {React$Element<any>}
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
     const [ isClicked, setIsClicked ] = useState(false);
48
     const [ isClicked, setIsClicked ] = useState(false);
45
     const [ isHovered, setIsHovered ] = useState(false);
49
     const [ isHovered, setIsHovered ] = useState(false);
46
 
50
 
83
         setIsHovered(false);
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
      * Renders the content of the link based on the state.
105
      * Renders the content of the link based on the state.
88
      *
106
      *
93
             return (
111
             return (
94
                 <>
112
                 <>
95
                     <div className = 'copy-button-content selected'>
113
                     <div className = 'copy-button-content selected'>
96
-                        {textOnCopySuccess}
114
+                        <span role = { 'alert' }>{ textOnCopySuccess }</span>
97
                     </div>
115
                     </div>
98
                     <Icon src = { IconCheck } />
116
                     <Icon src = { IconCheck } />
99
                 </>
117
                 </>
112
 
130
 
113
     return (
131
     return (
114
         <div
132
         <div
133
+            aria-label = { textOnHover }
115
             className = { `${className} copy-button${isClicked ? ' clicked' : ''}` }
134
             className = { `${className} copy-button${isClicked ? ' clicked' : ''}` }
135
+            id = { id }
136
+            onBlur = { onHoverOut }
116
             onClick = { onClick }
137
             onClick = { onClick }
138
+            onFocus = { onHoverIn }
139
+            onKeyPress = { onKeyPress }
117
             onMouseOut = { onHoverOut }
140
             onMouseOut = { onHoverOut }
118
-            onMouseOver = { onHoverIn }>
141
+            onMouseOver = { onHoverIn }
142
+            role = 'button'
143
+            tabIndex = { 0 }>
119
             { renderContent() }
144
             { renderContent() }
120
         </div>
145
         </div>
121
     );
146
     );
125
     className: ''
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 Переглянути файл

10
 } from '@atlaskit/modal-dialog/dist/es2019/styled/Content';
10
 } from '@atlaskit/modal-dialog/dist/es2019/styled/Content';
11
 import React from 'react';
11
 import React from 'react';
12
 
12
 
13
+import { translate } from '../../../i18n';
13
 import { Icon, IconClose } from '../../../icons';
14
 import { Icon, IconClose } from '../../../icons';
14
 
15
 
15
 const TitleIcon = ({ appearance }: { appearance?: 'danger' | 'warning' }) => {
16
 const TitleIcon = ({ appearance }: { appearance?: 'danger' | 'warning' }) => {
45
  * @class ModalHeader
46
  * @class ModalHeader
46
  * @extends {React.Component<Props>}
47
  * @extends {React.Component<Props>}
47
  */
48
  */
48
-export default class ModalHeader extends React.Component<Props> {
49
+class ModalHeader extends React.Component<Props> {
49
     static defaultProps = {
50
     static defaultProps = {
50
         isHeadingMultiline: true
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
      * Implements React's {@link Component#render()}.
84
      * Implements React's {@link Component#render()}.
55
      *
85
      *
65
             onClose,
95
             onClose,
66
             showKeyline,
96
             showKeyline,
67
             isHeadingMultiline,
97
             isHeadingMultiline,
68
-            testId
98
+            testId,
99
+            t
69
         } = this.props;
100
         } = this.props;
70
 
101
 
71
         if (!heading) {
102
         if (!heading) {
83
                         {heading}
114
                         {heading}
84
                     </TitleText>
115
                     </TitleText>
85
                 </Title>
116
                 </Title>
117
+
86
                 {
118
                 {
87
                     !hideCloseIconButton && <Icon
119
                     !hideCloseIconButton && <Icon
120
+                        ariaLabel = { t('dialog.close') }
88
                         onClick = { onClose }
121
                         onClick = { onClose }
89
-                        src = { IconClose } />
122
+                        onKeyPress = { this._onKeyPress }
123
+                        role = 'button'
124
+                        src = { IconClose }
125
+                        tabIndex = { 0 } />
90
                 }
126
                 }
91
             </Header>
127
             </Header>
92
         );
128
         );
93
     }
129
     }
94
 }
130
 }
131
+export default translate(ModalHeader);

+ 4
- 4
react/features/base/dialog/components/web/StatelessDialog.js Переглянути файл

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

+ 1
- 0
react/features/base/environment/utils.js Переглянути файл

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

+ 103
- 8
react/features/base/icons/components/Icon.js Переглянути файл

1
 // @flow
1
 // @flow
2
 
2
 
3
-import React from 'react';
3
+import React, { useCallback } from 'react';
4
 
4
 
5
 import { Container } from '../../react/base';
5
 import { Container } from '../../react/base';
6
 import { styleTypeToObject } from '../../styles';
6
 import { styleTypeToObject } from '../../styles';
10
     /**
10
     /**
11
      * Class name for the web platform, if any.
11
      * Class name for the web platform, if any.
12
      */
12
      */
13
-    className: string,
13
+    className?: string,
14
 
14
 
15
     /**
15
     /**
16
      * Color of the icon (if not provided by the style object).
16
      * Color of the icon (if not provided by the style object).
22
      */
22
      */
23
     id?: string,
23
     id?: string,
24
 
24
 
25
+    /**
26
+     * Id of the icon container
27
+     */
28
+    containerId?: string,
29
+
25
     /**
30
     /**
26
      * Function to invoke on click.
31
      * Function to invoke on click.
27
      */
32
      */
40
     /**
45
     /**
41
      * Style object to be applied.
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
 export const DEFAULT_COLOR = navigator.product === 'ReactNative' ? 'white' : undefined;
106
 export const DEFAULT_COLOR = navigator.product === 'ReactNative' ? 'white' : undefined;
47
 export const DEFAULT_SIZE = navigator.product === 'ReactNative' ? 36 : 22;
107
 export const DEFAULT_SIZE = navigator.product === 'ReactNative' ? 36 : 22;
57
         className,
117
         className,
58
         color,
118
         color,
59
         id,
119
         id,
120
+        containerId,
60
         onClick,
121
         onClick,
61
         size,
122
         size,
62
         src: IconComponent,
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
     const {
139
     const {
67
         color: styleColor,
140
         color: styleColor,
71
     const calculatedColor = color ?? styleColor ?? DEFAULT_COLOR;
144
     const calculatedColor = color ?? styleColor ?? DEFAULT_COLOR;
72
     const calculatedSize = size ?? styleSize ?? DEFAULT_SIZE;
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
     return (
156
     return (
75
         <Container
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
             onClick = { onClick }
168
             onClick = { onClick }
78
-            style = { restStyle }>
169
+            onKeyDown = { onKeyDown }
170
+            onKeyPress = { onKeyPressHandler }
171
+            role = { role }
172
+            style = { restStyle }
173
+            tabIndex = { tabIndex }>
79
             <IconComponent
174
             <IconComponent
80
                 fill = { calculatedColor }
175
                 fill = { calculatedColor }
81
                 height = { calculatedSize }
176
                 height = { calculatedSize }

+ 47
- 3
react/features/base/popover/components/Popover.web.js Переглянути файл

129
         // Bind event handlers so they are only bound once for every instance.
129
         // Bind event handlers so they are only bound once for every instance.
130
         this._onHideDialog = this._onHideDialog.bind(this);
130
         this._onHideDialog = this._onHideDialog.bind(this);
131
         this._onShowDialog = this._onShowDialog.bind(this);
131
         this._onShowDialog = this._onShowDialog.bind(this);
132
+        this._onKeyPress = this._onKeyPress.bind(this);
132
         this._drawerContainerRef = React.createRef();
133
         this._drawerContainerRef = React.createRef();
134
+        this._onEscKey = this._onEscKey.bind(this);
133
     }
135
     }
134
 
136
 
135
     /**
137
     /**
207
             <div
209
             <div
208
                 className = { className }
210
                 className = { className }
209
                 id = { id }
211
                 id = { id }
212
+                onKeyPress = { this._onKeyPress }
210
                 onMouseEnter = { this._onShowDialog }
213
                 onMouseEnter = { this._onShowDialog }
211
                 onMouseLeave = { this._onHideDialog }>
214
                 onMouseLeave = { this._onHideDialog }>
212
                 <InlineDialog
215
                 <InlineDialog
231
         this.setState({ showDialog: false });
234
         this.setState({ showDialog: false });
232
     }
235
     }
233
 
236
 
234
-    _onShowDialog: () => void;
237
+    _onShowDialog: (Object) => void;
235
 
238
 
236
     /**
239
     /**
237
      * Displays the {@code InlineDialog} and calls any registered onPopoverOpen
240
      * Displays the {@code InlineDialog} and calls any registered onPopoverOpen
238
      * callbacks.
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
      * @private
244
      * @private
242
      * @returns {void}
245
      * @returns {void}
243
      */
246
      */
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
      * Renders the React Element to be displayed in the {@code InlineDialog}.
298
      * Renders the React Element to be displayed in the {@code InlineDialog}.
257
      * Also adds padding to support moving the mouse from the trigger to the
299
      * Also adds padding to support moving the mouse from the trigger to the
264
         const { content, position } = this.props;
306
         const { content, position } = this.props;
265
 
307
 
266
         return (
308
         return (
267
-            <div className = 'popover'>
309
+            <div
310
+                className = 'popover'
311
+                onKeyDown = { this._onEscKey }>
268
                 { content }
312
                 { content }
269
                 <div className = 'popover-mouse-padding-top' />
313
                 <div className = 'popover-mouse-padding-top' />
270
                 <div className = { _mapPositionToPaddingClass(position) } />
314
                 <div className = { _mapPositionToPaddingClass(position) } />

+ 73
- 13
react/features/base/premeeting/components/web/ActionButton.js Переглянути файл

1
 // @flow
1
 // @flow
2
 
2
 
3
-import React from 'react';
3
+import React, { useCallback } from 'react';
4
 
4
 
5
 import { Icon, IconArrowDown } from '../../../icons';
5
 import { Icon, IconArrowDown } from '../../../icons';
6
 
6
 
46
      */
46
      */
47
     onClick: Function,
47
     onClick: Function,
48
 
48
 
49
+
49
     /**
50
     /**
50
      * Click handler for options.
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
     testId,
92
     testId,
67
     type = 'primary',
93
     type = 'primary',
68
     onClick,
94
     onClick,
69
-    onOptionsClick
95
+    onOptionsClick,
96
+    tabIndex,
97
+    role,
98
+    ariaPressed,
99
+    ariaLabel,
100
+    ariaDropDownLabel
70
 }: Props) {
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
     return (
118
     return (
72
         <div
119
         <div
120
+            aria-disabled = { disabled }
121
+            aria-label = { ariaLabel }
73
             className = { `action-btn ${className} ${type} ${disabled ? 'disabled' : ''}` }
122
             className = { `action-btn ${className} ${type} ${disabled ? 'disabled' : ''}` }
74
             data-testid = { testId ? testId : undefined }
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
             {children}
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
         </div>
147
         </div>
88
     );
148
     );

+ 33
- 7
react/features/base/premeeting/components/web/ConnectionStatus.js Переглянути файл

1
 // @flow
1
 // @flow
2
 
2
 
3
-import React, { useState } from 'react';
3
+import React, { useCallback, useState } from 'react';
4
 
4
 
5
 import { translate } from '../../../i18n';
5
 import { translate } from '../../../i18n';
6
 import { Icon, IconArrowDownSmall, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons';
6
 import { Icon, IconArrowDownSmall, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons';
65
         ? 'con-status-details-visible'
65
         ? 'con-status-details-visible'
66
         : 'con-status-details-hidden';
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
     return (
80
     return (
69
         <div className = 'con-status'>
81
         <div className = 'con-status'>
70
             <div className = 'con-status-container'>
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
                     <div className = { `con-status-circle ${connectionClass}` }>
87
                     <div className = { `con-status-circle ${connectionClass}` }>
73
                         <Icon
88
                         <Icon
74
                             size = { 16 }
89
                             size = { 16 }
75
                             src = { icon } />
90
                             src = { icon } />
76
                     </div>
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
                     <Icon
96
                     <Icon
97
+                        ariaDescribedBy = 'connection-status-description'
98
+                        ariaPressed = { showDetails }
79
                         className = { arrowClassName }
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
                         size = { 24 }
103
                         size = { 24 }
83
-                        src = { IconArrowDownSmall } />
104
+                        src = { IconArrowDownSmall }
105
+                        tabIndex = { 0 } />
84
                 </div>
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
             </div>
112
             </div>
87
         </div>
113
         </div>
88
     );
114
     );

+ 5
- 166
react/features/base/premeeting/components/web/CopyMeetingUrl.js Переглянути файл

2
 
2
 
3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 
4
 
5
+import CopyMeetingLinkSection
6
+    from '../../../../invite/components/add-people-dialog/web/CopyMeetingLinkSection';
5
 import { getCurrentConferenceUrl } from '../../../connection';
7
 import { getCurrentConferenceUrl } from '../../../connection';
6
 import { translate } from '../../../i18n';
8
 import { translate } from '../../../i18n';
7
-import { Icon, IconCopy, IconCheck } from '../../../icons';
8
 import { connect } from '../../../redux';
9
 import { connect } from '../../../redux';
9
-import { copyText, getDecodedURI } from '../../../util';
10
 
10
 
11
 type Props = {
11
 type Props = {
12
 
12
 
27
     _enableAutomaticUrlCopy: boolean,
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
  * Component used to copy meeting url on prejoin page.
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
      * Implements React's {@link Component#render()}.
37
      * Implements React's {@link Component#render()}.
181
      * @returns {ReactElement}
40
      * @returns {ReactElement}
182
      */
41
      */
183
     render() {
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
         return (
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
             </div>
46
             </div>
208
         );
47
         );
209
     }
48
     }

+ 5
- 1
react/features/base/premeeting/components/web/InputField.js Переглянути файл

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

+ 2
- 2
react/features/base/premeeting/components/web/PreMeetingScreen.js Переглянути файл

108
                     )}
108
                     )}
109
                     {showConferenceInfo && (
109
                     {showConferenceInfo && (
110
                         <>
110
                         <>
111
-                            <div className = 'title'>
111
+                            <h1 className = 'title'>
112
                                 { title }
112
                                 { title }
113
-                            </div>
113
+                            </h1>
114
                             {showSharingButton ? <CopyMeetingUrl /> : null}
114
                             {showSharingButton ? <CopyMeetingUrl /> : null}
115
                         </>
115
                         </>
116
                     )}
116
                     )}

+ 15
- 3
react/features/base/premeeting/components/web/ToggleButton.js Переглянути файл

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

+ 72
- 8
react/features/base/react/components/web/MeetingsList.js Переглянути файл

4
 
4
 
5
 import {
5
 import {
6
     getLocalizedDateFormatter,
6
     getLocalizedDateFormatter,
7
-    getLocalizedDurationFormatter
7
+    getLocalizedDurationFormatter,
8
+    translate
8
 } from '../../../i18n';
9
 } from '../../../i18n';
9
 import { Icon, IconTrash } from '../../../icons';
10
 import { Icon, IconTrash } from '../../../icons';
10
 
11
 
41
     /**
42
     /**
42
      * Handler for deleting an item.
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
  *
86
  *
81
  * @extends Component
87
  * @extends Component
82
  */
88
  */
83
-export default class MeetingsList extends Component<Props> {
89
+class MeetingsList extends Component<Props> {
84
     /**
90
     /**
85
      * Constructor of the MeetingsList component.
91
      * Constructor of the MeetingsList component.
86
      *
92
      *
99
      * @returns {React.ReactNode}
105
      * @returns {React.ReactNode}
100
      */
106
      */
101
     render() {
107
     render() {
102
-        const { listEmptyComponent, meetings } = this.props;
108
+        const { listEmptyComponent, meetings, t } = this.props;
103
 
109
 
104
         /**
110
         /**
105
          * If there are no recent meetings we don't want to display anything
111
          * If there are no recent meetings we don't want to display anything
107
         if (meetings) {
113
         if (meetings) {
108
             return (
114
             return (
109
                 <Container
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
                         meetings.length === 0
121
                         meetings.length === 0
113
                             ? listEmptyComponent
122
                             ? listEmptyComponent
139
         return null;
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
     _onDelete: Object => Function;
174
     _onDelete: Object => Function;
143
 
175
 
144
     /**
176
     /**
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
     _renderItem: (Object, number) => React$Node;
214
     _renderItem: (Object, number) => React$Node;
162
 
215
 
163
     /**
216
     /**
176
             title,
229
             title,
177
             url
230
             url
178
         } = meeting;
231
         } = meeting;
179
-        const { hideURL = false, onItemDelete } = this.props;
232
+        const { hideURL = false, onItemDelete, t } = this.props;
180
         const onPress = this._onPress(url);
233
         const onPress = this._onPress(url);
234
+        const onKeyPress = this._onKeyPress(url);
181
         const rootClassName
235
         const rootClassName
182
             = `item ${
236
             = `item ${
183
                 onPress ? 'with-click-handler' : 'without-click-handler'}`;
237
                 onPress ? 'with-click-handler' : 'without-click-handler'}`;
184
 
238
 
185
         return (
239
         return (
186
             <Container
240
             <Container
241
+                aria-label = { title }
187
                 className = { rootClassName }
242
                 className = { rootClassName }
188
                 key = { index }
243
                 key = { index }
189
-                onClick = { onPress }>
244
+                onClick = { onPress }
245
+                onKeyPress = { onKeyPress }
246
+                role = 'menuitem'
247
+                tabIndex = { 0 }>
190
                 <Container className = 'left-column'>
248
                 <Container className = 'left-column'>
191
                     <Text className = 'title'>
249
                     <Text className = 'title'>
192
                         { _toDateString(date) }
250
                         { _toDateString(date) }
216
                     { elementAfter || null }
274
                     { elementAfter || null }
217
 
275
 
218
                     { onItemDelete && <Icon
276
                     { onItemDelete && <Icon
277
+                        ariaLabel = { t('welcomepage.recentListDelete') }
219
                         className = 'delete-meeting'
278
                         className = 'delete-meeting'
220
                         onClick = { this._onDelete(meeting) }
279
                         onClick = { this._onDelete(meeting) }
221
-                        src = { IconTrash } />}
280
+                        onKeyPress = { this._onDeleteKeyPress(meeting) }
281
+                        role = 'button'
282
+                        src = { IconTrash }
283
+                        tabIndex = { 0 } />}
222
                 </Container>
284
                 </Container>
223
             </Container>
285
             </Container>
224
         );
286
         );
225
     }
287
     }
226
 }
288
 }
289
+
290
+export default translate(MeetingsList);

+ 5
- 1
react/features/base/react/components/web/Watermarks.js Переглянути файл

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

+ 6
- 2
react/features/base/toolbox/components/AbstractButton.js Переглянути файл

246
      * Handles clicking / pressing the button, and toggles the audio mute state
246
      * Handles clicking / pressing the button, and toggles the audio mute state
247
      * accordingly.
247
      * accordingly.
248
      *
248
      *
249
+     * @param {Object} e - Event.
249
      * @private
250
      * @private
250
      * @returns {void}
251
      * @returns {void}
251
      */
252
      */
252
-    _onClick() {
253
+    _onClick(e) {
253
         const { afterClick } = this.props;
254
         const { afterClick } = this.props;
254
 
255
 
255
         this._handleClick();
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 Переглянути файл

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

+ 7
- 15
react/features/base/toolbox/components/ToolboxItem.web.js Переглянути файл

20
     constructor(props: Props) {
20
     constructor(props: Props) {
21
         super(props);
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
      * @param {Object} event - The key event.
31
      * @param {Object} event - The key event.
33
      * @private
32
      * @private
34
      * @returns {void}
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
             event.preventDefault();
37
             event.preventDefault();
45
-            event.stopPropagation();
46
             this.props.onClick();
38
             this.props.onClick();
47
         }
39
         }
48
     }
40
     }
72
             'aria-label': this.accessibilityLabel,
64
             'aria-label': this.accessibilityLabel,
73
             className: className + (disabled ? ' disabled' : ''),
65
             className: className + (disabled ? ' disabled' : ''),
74
             onClick: disabled ? undefined : onClick,
66
             onClick: disabled ? undefined : onClick,
75
-            onKeyDown: this._onKeyDown,
67
+            onKeyPress: this._onKeyPress,
76
             tabIndex: 0,
68
             tabIndex: 0,
77
-            role: 'button'
69
+            role: showLabel ? 'menuitem' : 'button'
78
         };
70
         };
79
 
71
 
80
         const elementType = showLabel ? 'li' : 'div';
72
         const elementType = showLabel ? 'li' : 'div';

+ 34
- 1
react/features/base/toolbox/components/web/OverflowMenuItem.js Переглянути файл

74
         disabled: false
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
      * Implements React's {@link Component#render()}.
107
      * Implements React's {@link Component#render()}.
79
      *
108
      *
89
 
118
 
90
         return (
119
         return (
91
             <li
120
             <li
121
+                aria-disabled = { disabled }
92
                 aria-label = { accessibilityLabel }
122
                 aria-label = { accessibilityLabel }
93
                 className = { className }
123
                 className = { className }
94
-                onClick = { disabled ? null : onClick }>
124
+                onClick = { disabled ? null : onClick }
125
+                onKeyPress = { this._onKeyPress }
126
+                role = 'menuitem'
127
+                tabIndex = { 0 }>
95
                 <span className = 'overflow-menu-item-icon'>
128
                 <span className = 'overflow-menu-item-icon'>
96
                     <Icon
129
                     <Icon
97
                         id = { iconId }
130
                         id = { iconId }

+ 45
- 1
react/features/base/toolbox/components/web/ToolboxButtonWithIcon.js Переглянути файл

36
      * Additional styles.
36
      * Additional styles.
37
      */
37
      */
38
     styles?: Object,
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
         iconDisabled,
81
         iconDisabled,
52
         iconTooltip,
82
         iconTooltip,
53
         onIconClick,
83
         onIconClick,
54
-        styles
84
+        onIconKeyDown,
85
+        styles,
86
+        ariaLabel,
87
+        ariaHasPopup,
88
+        ariaControls,
89
+        ariaExpanded,
90
+        iconId
55
     } = props;
91
     } = props;
56
 
92
 
57
     const iconProps = {};
93
     const iconProps = {};
62
     } else {
98
     } else {
63
         iconProps.className = 'settings-button-small-icon';
99
         iconProps.className = 'settings-button-small-icon';
64
         iconProps.onClick = onIconClick;
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
                     position = 'top'>
119
                     position = 'top'>
78
                     <Icon
120
                     <Icon
79
                         { ...iconProps }
121
                         { ...iconProps }
122
+                        ariaHasPopup = { ariaHasPopup }
123
+                        ariaLabel = { ariaLabel }
80
                         size = { 9 }
124
                         size = { 9 }
81
                         src = { icon } />
125
                         src = { icon } />
82
                 </Tooltip>
126
                 </Tooltip>

+ 7
- 15
react/features/base/toolbox/components/web/ToolboxItem.js Переглянути файл

19
     constructor(props: Props) {
19
     constructor(props: Props) {
20
         super(props);
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
      * @param {Object} event - The key event.
30
      * @param {Object} event - The key event.
32
      * @private
31
      * @private
33
      * @returns {void}
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
             event.preventDefault();
36
             event.preventDefault();
44
-            event.stopPropagation();
45
             this.props.onClick();
37
             this.props.onClick();
46
         }
38
         }
47
     }
39
     }
71
             'aria-label': this.accessibilityLabel,
63
             'aria-label': this.accessibilityLabel,
72
             className: className + (disabled ? ' disabled' : ''),
64
             className: className + (disabled ? ' disabled' : ''),
73
             onClick: disabled ? undefined : onClick,
65
             onClick: disabled ? undefined : onClick,
74
-            onKeyDown: this._onKeyDown,
66
+            onKeyPress: this._onKeyPress,
75
             tabIndex: 0,
67
             tabIndex: 0,
76
-            role: 'button'
68
+            role: showLabel ? 'menuitem' : 'button'
77
         };
69
         };
78
 
70
 
79
         const elementType = showLabel ? 'li' : 'div';
71
         const elementType = showLabel ? 'li' : 'div';

+ 20
- 1
react/features/calendar-sync/components/AddMeetingUrlButton.web.js Переглянути файл

55
 
55
 
56
         // Bind event handler so it is only bound once for every instance.
56
         // Bind event handler so it is only bound once for every instance.
57
         this._onClick = this._onClick.bind(this);
57
         this._onClick = this._onClick.bind(this);
58
+        this._onKeyPress = this._onKeyPress.bind(this);
58
     }
59
     }
59
 
60
 
60
     /**
61
     /**
67
             <Tooltip content = { this.props.t('calendarSync.addMeetingURL') }>
68
             <Tooltip content = { this.props.t('calendarSync.addMeetingURL') }>
68
                 <div
69
                 <div
69
                     className = 'button add-button'
70
                     className = 'button add-button'
70
-                    onClick = { this._onClick }>
71
+                    onClick = { this._onClick }
72
+                    onKeyPress = { this._onKeyPress }
73
+                    role = 'button'>
71
                     <Icon src = { IconAdd } />
74
                     <Icon src = { IconAdd } />
72
                 </div>
75
                 </div>
73
             </Tooltip>
76
             </Tooltip>
88
 
91
 
89
         dispatch(updateCalendarEvent(eventId, calendarId));
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
 export default translate(connect()(AddMeetingUrlButton));
112
 export default translate(connect()(AddMeetingUrlButton));

+ 23
- 2
react/features/calendar-sync/components/CalendarList.web.js Переглянути файл

72
         this._getRenderListEmptyComponent
72
         this._getRenderListEmptyComponent
73
             = this._getRenderListEmptyComponent.bind(this);
73
             = this._getRenderListEmptyComponent.bind(this);
74
         this._onOpenSettings = this._onOpenSettings.bind(this);
74
         this._onOpenSettings = this._onOpenSettings.bind(this);
75
+        this._onKeyPressOpenSettings = this._onKeyPressOpenSettings.bind(this);
75
         this._onRefreshEvents = this._onRefreshEvents.bind(this);
76
         this._onRefreshEvents = this._onRefreshEvents.bind(this);
76
     }
77
     }
77
 
78
 
187
         return (
188
         return (
188
             <div className = 'meetings-list-empty'>
189
             <div className = 'meetings-list-empty'>
189
                 <div className = 'meetings-list-empty-image'>
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
                 </div>
194
                 </div>
192
                 <div className = 'description'>
195
                 <div className = 'description'>
193
                     { t('welcomepage.connectCalendarText', {
196
                     { t('welcomepage.connectCalendarText', {
197
                 </div>
200
                 </div>
198
                 <div
201
                 <div
199
                     className = 'meetings-list-empty-button'
202
                     className = 'meetings-list-empty-button'
200
-                    onClick = { this._onOpenSettings }>
203
+                    onClick = { this._onOpenSettings }
204
+                    onKeyPress = { this._onKeyPressOpenSettings }
205
+                    role = 'button'>
201
                     <Icon
206
                     <Icon
202
                         className = 'meetings-list-empty-icon'
207
                         className = 'meetings-list-empty-icon'
203
                         src = { IconPlusCalendar } />
208
                         src = { IconPlusCalendar } />
221
         this.props.dispatch(openSettingsDialog(SETTINGS_TABS.CALENDAR));
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
     _onRefreshEvents: () => void;
245
     _onRefreshEvents: () => void;
225
 
246
 
226
 
247
 

+ 20
- 1
react/features/calendar-sync/components/JoinButton.web.js Переглянути файл

45
 
45
 
46
         // Bind event handler so it is only bound once for every instance.
46
         // Bind event handler so it is only bound once for every instance.
47
         this._onClick = this._onClick.bind(this);
47
         this._onClick = this._onClick.bind(this);
48
+        this._onKeyPress = this._onKeyPress.bind(this);
48
     }
49
     }
49
 
50
 
50
     /**
51
     /**
60
                 content = { t('calendarSync.joinTooltip') }>
61
                 content = { t('calendarSync.joinTooltip') }>
61
                 <div
62
                 <div
62
                     className = 'button join-button'
63
                     className = 'button join-button'
63
-                    onClick = { this._onClick }>
64
+                    onClick = { this._onClick }
65
+                    onKeyPress = { this._onKeyPress }
66
+                    role = 'button'>
64
                     <Icon
67
                     <Icon
65
                         size = '14'
68
                         size = '14'
66
                         src = { IconAdd } />
69
                         src = { IconAdd } />
81
     _onClick(event) {
84
     _onClick(event) {
82
         this.props.onPress(event, this.props.url);
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
 export default translate(JoinButton);
105
 export default translate(JoinButton);

+ 19
- 4
react/features/calendar-sync/components/MicrosoftSignInButton.web.js Переглянути файл

2
 
2
 
3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 
4
 
5
+import { translate } from '../../base/i18n';
6
+
7
+
5
 /**
8
 /**
6
  * The type of the React {@code Component} props of
9
  * The type of the React {@code Component} props of
7
  * {@link MicrosoftSignInButton}.
10
  * {@link MicrosoftSignInButton}.
8
  */
11
  */
9
 type Props = {
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
     onClick: Function,
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
  *
32
  *
21
  * @extends Component
33
  * @extends Component
22
  */
34
  */
23
-export default class MicrosoftSignInButton extends Component<Props> {
35
+class MicrosoftSignInButton extends Component<Props> {
24
     /**
36
     /**
25
      * Implements React's {@link Component#render()}.
37
      * Implements React's {@link Component#render()}.
26
      *
38
      *
33
                 className = 'microsoft-sign-in'
45
                 className = 'microsoft-sign-in'
34
                 onClick = { this.props.onClick }>
46
                 onClick = { this.props.onClick }>
35
                 <img
47
                 <img
48
+                    alt = { this.props.t('welcomepage.logo.microsoftLogo') }
36
                     className = 'microsoft-logo'
49
                     className = 'microsoft-logo'
37
                     src = 'images/microsoftLogo.svg' />
50
                     src = 'images/microsoftLogo.svg' />
38
                 <div className = 'microsoft-cta'>
51
                 <div className = 'microsoft-cta'>
42
         );
55
         );
43
     }
56
     }
44
 }
57
 }
58
+
59
+export default translate(MicrosoftSignInButton);

+ 37
- 2
react/features/chat/components/web/Chat.js Переглянути файл

4
 
4
 
5
 import { translate } from '../../../base/i18n';
5
 import { translate } from '../../../base/i18n';
6
 import { connect } from '../../../base/redux';
6
 import { connect } from '../../../base/redux';
7
+import { toggleChat } from '../../actions.web';
7
 import AbstractChat, {
8
 import AbstractChat, {
8
     _mapStateToProps,
9
     _mapStateToProps,
9
     type Props
10
     type Props
51
         // Bind event handlers so they are only bound once for every instance.
52
         // Bind event handlers so they are only bound once for every instance.
52
         this._renderPanelContent = this._renderPanelContent.bind(this);
53
         this._renderPanelContent = this._renderPanelContent.bind(this);
53
         this._onChatInputResize = this._onChatInputResize.bind(this);
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
             this._scrollMessageContainerToBottom(false);
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
      * Implements React's {@link Component#render()}.
97
      * Implements React's {@link Component#render()}.
135
      */
153
      */
136
     _renderChatHeader() {
154
     _renderChatHeader() {
137
         return (
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
 
198
 
178
         return (
199
         return (
179
             <div
200
             <div
201
+                aria-haspopup = 'true'
180
                 className = { `sideToolbarContainer ${className}` }
202
                 className = { `sideToolbarContainer ${className}` }
181
-                id = 'sideToolbarContainer'>
203
+                id = 'sideToolbarContainer'
204
+                onKeyDown = { this._onEscClick } >
182
                 { ComponentToRender }
205
                 { ComponentToRender }
183
             </div>
206
             </div>
184
         );
207
         );
199
     }
222
     }
200
 
223
 
201
     _onSendMessage: (string) => void;
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
 export default translate(connect(_mapStateToProps)(Chat));
239
 export default translate(connect(_mapStateToProps)(Chat));

+ 25
- 5
react/features/chat/components/web/ChatDialogHeader.js Переглянути файл

1
 // @flow
1
 // @flow
2
 
2
 
3
-import React from 'react';
3
+import React, { useCallback } from 'react';
4
 
4
 
5
+import { translate } from '../../../base/i18n';
5
 import { Icon, IconClose } from '../../../base/icons';
6
 import { Icon, IconClose } from '../../../base/icons';
6
 import { connect } from '../../../base/redux';
7
 import { connect } from '../../../base/redux';
7
 import { toggleChat } from '../../actions.web';
8
 import { toggleChat } from '../../actions.web';
17
      * An optional class name.
18
      * An optional class name.
18
      */
19
      */
19
     className: string,
20
     className: string,
21
+
22
+    /**
23
+     * Invoked to obtain translated strings.
24
+     */
25
+    t: Function
20
 };
26
 };
21
 
27
 
22
 /**
28
 /**
24
  *
30
  *
25
  * @returns {React$Element<any>}
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
     return (
42
     return (
29
         <div
43
         <div
30
-            className = { className || 'chat-dialog-header' }>
44
+            className = { className || 'chat-dialog-header' }
45
+            role = 'heading'>
46
+            { t('chat.title') }
31
             <Icon
47
             <Icon
48
+                ariaLabel = { t('toolbar.closeChat') }
32
                 onClick = { onCancel }
49
                 onClick = { onCancel }
33
-                src = { IconClose } />
50
+                onKeyPress = { onKeyPressHandler }
51
+                role = 'button'
52
+                src = { IconClose }
53
+                tabIndex = { 0 } />
34
         </div>
54
         </div>
35
     );
55
     );
36
 }
56
 }
37
 
57
 
38
 const mapDispatchToProps = { onCancel: toggleChat };
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 Переглянути файл

84
         this._onSmileySelect = this._onSmileySelect.bind(this);
84
         this._onSmileySelect = this._onSmileySelect.bind(this);
85
         this._onSubmitMessage = this._onSubmitMessage.bind(this);
85
         this._onSubmitMessage = this._onSubmitMessage.bind(this);
86
         this._onToggleSmileysPanel = this._onToggleSmileysPanel.bind(this);
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
         this._setTextAreaRef = this._setTextAreaRef.bind(this);
90
         this._setTextAreaRef = this._setTextAreaRef.bind(this);
88
     }
91
     }
89
 
92
 
116
                         <div id = 'smileysarea'>
119
                         <div id = 'smileysarea'>
117
                             <div id = 'smileys'>
120
                             <div id = 'smileys'>
118
                                 <div
121
                                 <div
122
+                                    aria-expanded = { this.state.showSmileysPanel }
123
+                                    aria-haspopup = 'smileysContainer'
124
+                                    aria-label = { this.props.t('chat.smileysPanel') }
119
                                     className = 'smiley-button'
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
                                     <Icon src = { IconSmile } />
131
                                     <Icon src = { IconSmile } />
122
                                 </div>
132
                                 </div>
123
                             </div>
133
                             </div>
129
                     </div>
139
                     </div>
130
                     <div className = 'usrmsg-form'>
140
                     <div className = 'usrmsg-form'>
131
                         <TextareaAutosize
141
                         <TextareaAutosize
142
+                            autoComplete = 'off'
143
+                            autoFocus = { true }
132
                             id = 'usermsg'
144
                             id = 'usermsg'
133
-                            inputRef = { this._setTextAreaRef }
134
                             maxRows = { 5 }
145
                             maxRows = { 5 }
135
                             onChange = { this._onMessageChange }
146
                             onChange = { this._onMessageChange }
136
                             onHeightChange = { this.props.onResize }
147
                             onHeightChange = { this.props.onResize }
137
                             onKeyDown = { this._onDetectSubmit }
148
                             onKeyDown = { this._onDetectSubmit }
138
                             placeholder = { this.props.t('chat.messagebox') }
149
                             placeholder = { this.props.t('chat.messagebox') }
150
+                            ref = { this._setTextAreaRef }
151
+                            tabIndex = { 0 }
139
                             value = { this.state.message } />
152
                             value = { this.state.message } />
140
                     </div>
153
                     </div>
141
                     <div className = 'send-button-container'>
154
                     <div className = 'send-button-container'>
142
                         <div
155
                         <div
156
+                            aria-label = { this.props.t('chat.sendButton') }
143
                             className = 'send-button'
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
                             <Icon src = { IconPlane } />
162
                             <Icon src = { IconPlane } />
146
                         </div>
163
                         </div>
147
                     </div>
164
                     </div>
192
      * @returns {void}
209
      * @returns {void}
193
      */
210
      */
194
     _onDetectSubmit(event) {
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
             event.preventDefault();
215
             event.preventDefault();
216
+            event.stopPropagation();
198
 
217
 
199
             this._onSubmitMessage();
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
     _onMessageChange: (Object) => void;
238
     _onMessageChange: (Object) => void;
204
 
239
 
205
     /**
240
     /**
224
      * @returns {void}
259
      * @returns {void}
225
      */
260
      */
226
     _onSmileySelect(smileyText) {
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
         this._focus();
273
         this._focus();
233
     }
274
     }
241
      * @returns {void}
282
      * @returns {void}
242
      */
283
      */
243
     _onToggleSmileysPanel() {
284
     _onToggleSmileysPanel() {
285
+        if (this.state.showSmileysPanel) {
286
+            this._focus();
287
+        }
244
         this.setState({ showSmileysPanel: !this.state.showSmileysPanel });
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
     _setTextAreaRef: (?HTMLTextAreaElement) => void;
325
     _setTextAreaRef: (?HTMLTextAreaElement) => void;

+ 13
- 3
react/features/chat/components/web/ChatMessage.js Переглянути файл

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

+ 22
- 1
react/features/chat/components/web/DisplayNameForm.js Переглянути файл

59
         // Bind event handlers so they are only bound once for every instance.
59
         // Bind event handlers so they are only bound once for every instance.
60
         this._onDisplayNameChange = this._onDisplayNameChange.bind(this);
60
         this._onDisplayNameChange = this._onDisplayNameChange.bind(this);
61
         this._onSubmit = this._onSubmit.bind(this);
61
         this._onSubmit = this._onSubmit.bind(this);
62
+        this._onKeyPress = this._onKeyPress.bind(this);
62
     }
63
     }
63
 
64
 
64
     /**
65
     /**
74
             <div id = 'nickname'>
75
             <div id = 'nickname'>
75
                 <form onSubmit = { this._onSubmit }>
76
                 <form onSubmit = { this._onSubmit }>
76
                     <FieldTextStateless
77
                     <FieldTextStateless
78
+                        aria-describedby = 'nickname-title'
79
+                        autoComplete = 'name'
77
                         autoFocus = { true }
80
                         autoFocus = { true }
78
                         compact = { true }
81
                         compact = { true }
79
                         id = 'nickinput'
82
                         id = 'nickinput'
86
                 </form>
89
                 </form>
87
                 <div
90
                 <div
88
                     className = { `enter-chat${this.state.displayName.trim() ? '' : ' disabled'}` }
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
                     { t('chat.enter') }
96
                     { t('chat.enter') }
91
                 </div>
97
                 </div>
92
                 <KeyboardAvoider />
98
                 <KeyboardAvoider />
125
             displayName: this.state.displayName
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
 export default translate(connect()(DisplayNameForm));
151
 export default translate(connect()(DisplayNameForm));

+ 4
- 1
react/features/chat/components/web/MessageContainer.js Переглянути файл

70
 
70
 
71
         return (
71
         return (
72
             <div
72
             <div
73
+                aria-labelledby = 'chat-header'
73
                 id = 'chatconversation'
74
                 id = 'chatconversation'
74
                 onScroll = { this._onChatScroll }
75
                 onScroll = { this._onChatScroll }
75
-                ref = { this._messageListRef }>
76
+                ref = { this._messageListRef }
77
+                role = 'log'
78
+                tabIndex = { 0 }>
76
                 { messages }
79
                 { messages }
77
                 <div ref = { this._messagesListEndRef } />
80
                 <div ref = { this._messagesListEndRef } />
78
             </div>
81
             </div>

+ 38
- 2
react/features/chat/components/web/MessageRecipient.js Переглянути файл

15
  * Class to implement the displaying of the recipient of the next message.
15
  * Class to implement the displaying of the recipient of the next message.
16
  */
16
  */
17
 class MessageRecipient extends AbstractMessageRecipient<Props> {
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
      * Implements {@code PureComponent#render}.
48
      * Implements {@code PureComponent#render}.
20
      *
49
      *
30
         const { t } = this.props;
59
         const { t } = this.props;
31
 
60
 
32
         return (
61
         return (
33
-            <div id = 'chat-recipient'>
62
+            <div
63
+                id = 'chat-recipient'
64
+                role = 'alert'>
34
                 <span>
65
                 <span>
35
                     { t('chat.messageTo', {
66
                     { t('chat.messageTo', {
36
                         recipient: _privateMessageRecipient
67
                         recipient: _privateMessageRecipient
37
                     }) }
68
                     }) }
38
                 </span>
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
                     <Icon
76
                     <Icon
41
                         src = { IconCancelSelection } />
77
                         src = { IconCancelSelection } />
42
                 </div>
78
                 </div>

+ 84
- 28
react/features/chat/components/web/SmileysPanel.js Переглянути файл

23
  * @extends Component
23
  * @extends Component
24
  */
24
  */
25
 class SmileysPanel extends PureComponent<Props> {
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
      * Implements React's {@link Component#render()}.
90
      * Implements React's {@link Component#render()}.
28
      *
91
      *
30
      * @returns {ReactElement}
93
      * @returns {ReactElement}
31
      */
94
      */
32
     render() {
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
         return (
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
                 { smileyItems }
119
                 { smileyItems }
52
             </div>
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
 export default SmileysPanel;
125
 export default SmileysPanel;

+ 59
- 8
react/features/chrome-extension-banner/components/ChromeExtensionBanner.web.js Переглянути файл

107
         this._onInstallExtensionClick = this._onInstallExtensionClick.bind(this);
107
         this._onInstallExtensionClick = this._onInstallExtensionClick.bind(this);
108
         this._shouldNotRender = this._shouldNotRender.bind(this);
108
         this._shouldNotRender = this._shouldNotRender.bind(this);
109
         this._onDontShowAgainChange = this._onDontShowAgainChange.bind(this);
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
         this.setState({ closePressed: true });
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
     _onInstallExtensionClick: () => void;
190
     _onInstallExtensionClick: () => void;
173
 
191
 
174
     /**
192
     /**
182
         this.setState({ closePressed: true });
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
     _shouldNotRender: () => boolean;
219
     _shouldNotRender: () => boolean;
186
 
220
 
187
     /**
221
     /**
236
 
270
 
237
         return (
271
         return (
238
             <div className = { mainClassNames }>
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
                     <div
278
                     <div
243
-                        className = 'chrome-extension-banner__text-container'>
279
+                        className = 'chrome-extension-banner__text-container'
280
+                        id = 'chrome-extension-banner__text-container'>
244
                         { t('chromeExtensionBanner.installExtensionText') }
281
                         { t('chromeExtensionBanner.installExtensionText') }
245
                     </div>
282
                     </div>
246
                     <div
283
                     <div
284
+                        aria-label = { t('chromeExtensionBanner.close') }
247
                         className = 'chrome-extension-banner__close-container'
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
                         <Icon
290
                         <Icon
250
                             className = 'gray'
291
                             className = 'gray'
251
                             size = { 12 }
292
                             size = { 12 }
255
                 <div
296
                 <div
256
                     className = 'chrome-extension-banner__button-container'>
297
                     className = 'chrome-extension-banner__button-container'>
257
                     <div
298
                     <div
299
+                        aria-labelledby = 'chrome-extension-banner__button-text'
258
                         className = 'chrome-extension-banner__button-open-url'
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
                         <div
305
                         <div
261
-                            className = 'chrome-extension-banner__button-text'>
306
+                            className = 'chrome-extension-banner__button-text'
307
+                            id = 'chrome-extension-banner__button-text'>
262
                             { t('chromeExtensionBanner.buttonText') }
308
                             { t('chromeExtensionBanner.buttonText') }
263
                         </div>
309
                         </div>
264
                     </div>
310
                     </div>
265
                 </div>
311
                 </div>
266
                 <div className = 'chrome-extension-banner__checkbox-container'>
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
                         <input
317
                         <input
318
+                            aria-labelledby = 'chrome-extension-banner__checkbox-label'
269
                             checked = { this.state.dontShowAgainChecked }
319
                             checked = { this.state.dontShowAgainChecked }
320
+                            id = 'chrome-extension-banner__checkbox'
270
                             onChange = { this._onDontShowAgainChange }
321
                             onChange = { this._onDontShowAgainChange }
271
                             type = 'checkbox' />
322
                             type = 'checkbox' />
272
                         &nbsp;{ t('chromeExtensionBanner.dontShowAgain') }
323
                         &nbsp;{ t('chromeExtensionBanner.dontShowAgain') }

+ 15
- 3
react/features/conference/components/web/InviteMore.js Переглянути файл

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

+ 6
- 2
react/features/connection-stats/components/ConnectionStatsTable.js Переглянути файл

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

+ 1
- 0
react/features/deep-linking/components/DeepLinkingDesktopPage.web.js Переглянути файл

87
                             HIDE_DEEP_LINKING_LOGO
87
                             HIDE_DEEP_LINKING_LOGO
88
                                 ? null
88
                                 ? null
89
                                 : <img
89
                                 : <img
90
+                                    alt = { t('welcomepage.logo.logoDeepLinking') }
90
                                     className = 'logo'
91
                                     className = 'logo'
91
                                     src = 'images/logo-deep-linking.png' />
92
                                     src = 'images/logo-deep-linking.png' />
92
                         }
93
                         }

+ 2
- 0
react/features/deep-linking/components/DeepLinkingMobilePage.web.js Переглянути файл

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

+ 11
- 2
react/features/desktop-picker/components/DesktopSourcePreview.js Переглянути файл

2
 
2
 
3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 
4
 
5
+import { translate } from '../../base/i18n';
6
+
7
+
5
 /**
8
 /**
6
  * The type of the React {@code Component} props of
9
  * The type of the React {@code Component} props of
7
  * {@link DesktopSourcePreview}.
10
  * {@link DesktopSourcePreview}.
35
     /**
38
     /**
36
      * The source type of the DesktopCapturerSources to display.
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
                 onDoubleClick = { this._onDoubleClick }>
82
                 onDoubleClick = { this._onDoubleClick }>
75
                 <div className = 'desktop-source-preview-image-container'>
83
                 <div className = 'desktop-source-preview-image-container'>
76
                     <img
84
                     <img
85
+                        alt = { this.props.t('welcomepage.logo.desktopPreviewThumbnail') }
77
                         className = 'desktop-source-preview-thumbnail'
86
                         className = 'desktop-source-preview-thumbnail'
78
                         src = { this.props.source.thumbnail.toDataURL() } />
87
                         src = { this.props.source.thumbnail.toDataURL() } />
79
                 </div>
88
                 </div>
111
     }
120
     }
112
 }
121
 }
113
 
122
 
114
-export default DesktopSourcePreview;
123
+export default translate(DesktopSourcePreview);

+ 23
- 1
react/features/device-selection/components/AudioOutputPreview.js Переглянути файл

44
 
44
 
45
         this._audioElementReady = this._audioElementReady.bind(this);
45
         this._audioElementReady = this._audioElementReady.bind(this);
46
         this._onClick = this._onClick.bind(this);
46
         this._onClick = this._onClick.bind(this);
47
+        this._onKeyPress = this._onKeyPress.bind(this);
47
     }
48
     }
48
 
49
 
49
     /**
50
     /**
66
     render() {
67
     render() {
67
         return (
68
         return (
68
             <div className = 'audio-output-preview'>
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
                     { this.props.t('deviceSelection.testAudio') }
76
                     { this.props.t('deviceSelection.testAudio') }
71
                 </a>
77
                 </a>
72
                 <Audio
78
                 <Audio
105
             && this._audioElement.play();
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
      * Updates the target output device for playing the test sound.
131
      * Updates the target output device for playing the test sound.
110
      *
132
      *

+ 10
- 3
react/features/device-selection/components/DeviceSelection.js Переглянути файл

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

+ 22
- 13
react/features/device-selection/components/DeviceSelector.web.js Переглянути файл

51
     /**
51
     /**
52
      * Invoked to obtain translated strings.
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
      * @returns {ReactElement}
86
      * @returns {ReactElement}
82
      */
87
      */
83
     render() {
88
     render() {
89
+        if (this.props.hasPermission === undefined) {
90
+            return null;
91
+        }
92
+
84
         if (!this.props.hasPermission) {
93
         if (!this.props.hasPermission) {
85
             return this._renderNoPermission();
94
             return this._renderNoPermission();
86
         }
95
         }
134
     _createDropdownItem(device) {
143
     _createDropdownItem(device) {
135
         return (
144
         return (
136
             <DropdownItem
145
             <DropdownItem
146
+                data-deviceid = { device.deviceId }
147
+                isSelected = { device.deviceId === this.props.selectedDeviceId }
137
                 key = { device.deviceId }
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
                 { device.label || device.deviceId }
150
                 { device.label || device.deviceId }
146
             </DropdownItem>
151
             </DropdownItem>
147
         );
152
         );
183
                     shouldFitContainer = { true }
188
                     shouldFitContainer = { true }
184
                     trigger = { triggerText }
189
                     trigger = { triggerText }
185
                     triggerButtonProps = {{
190
                     triggerButtonProps = {{
186
-                        shouldFitContainer: true
191
+                        shouldFitContainer: true,
192
+                        id: this.props.id
187
                     }}
193
                     }}
188
                     triggerType = 'button'>
194
                     triggerType = 'button'>
189
                     <DropdownItemGroup>
195
                     <DropdownItemGroup>
199
     /**
205
     /**
200
      * Invokes the passed in callback to notify of selection changes.
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
      * @private
210
      * @private
204
      * @returns {void}
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 Переглянути файл

85
 
85
 
86
         // Bind event handlers so they are only bound once for every instance.
86
         // Bind event handlers so they are only bound once for every instance.
87
         this._onExpand = this._onExpand.bind(this);
87
         this._onExpand = this._onExpand.bind(this);
88
+        this._onExpandKeyPress = this._onExpandKeyPress.bind(this);
88
         this._onToggle = this._onToggle.bind(this);
89
         this._onToggle = this._onToggle.bind(this);
89
     }
90
     }
90
 
91
 
101
 
102
 
102
         return (
103
         return (
103
             <div id = 'e2ee-section'>
104
             <div id = 'e2ee-section'>
104
-                <p className = 'description'>
105
+                <p
106
+                    aria-live = 'polite'
107
+                    className = 'description'
108
+                    id = 'e2ee-section-description'>
105
                     { expand && description }
109
                     { expand && description }
106
                     { !expand && description.substring(0, 100) }
110
                     { !expand && description.substring(0, 100) }
107
                     { !expand && <span
111
                     { !expand && <span
112
+                        aria-controls = 'e2ee-section-description'
113
+                        aria-expanded = { expand }
108
                         className = 'read-more'
114
                         className = 'read-more'
109
-                        onClick = { this._onExpand }>
115
+                        onClick = { this._onExpand }
116
+                        onKeyPress = { this._onExpandKeyPress }
117
+                        role = 'button'
118
+                        tabIndex = { 0 }>
110
                             ... { t('dialog.readMore') }
119
                             ... { t('dialog.readMore') }
111
                     </span> }
120
                     </span> }
112
                 </p>
121
                 </p>
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
     _onToggle: () => void;
170
     _onToggle: () => void;
146
 
171
 
147
     /**
172
     /**

+ 2
- 0
react/features/embed-meeting/components/EmbedMeetingDialog.js Переглянути файл

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

+ 19
- 1
react/features/embed-meeting/components/EmbedMeetingTrigger.js Переглянути файл

36
         openEmbedDialog(EmbedMeetingDialog);
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
     return (
53
     return (
40
         <div
54
         <div
55
+            aria-label = { t('embedMeeting.title') }
41
             className = 'embed-meeting-trigger'
56
             className = 'embed-meeting-trigger'
42
-            onClick = { onClick }>
57
+            onClick = { onClick }
58
+            onKeyPress = { onKeyPress }
59
+            role = 'button'
60
+            tabIndex = { 0 }>
43
             {t('embedMeeting.title')}
61
             {t('embedMeeting.title')}
44
         </div>
62
         </div>
45
     );
63
     );

+ 18
- 5
react/features/feedback/components/FeedbackDialog.web.js Переглянути файл

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

+ 32
- 5
react/features/filmstrip/components/web/Filmstrip.js Переглянути файл

13
 import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
13
 import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
14
 import { getLocalParticipant } from '../../../base/participants';
14
 import { getLocalParticipant } from '../../../base/participants';
15
 import { connect } from '../../../base/redux';
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
 import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
18
 import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
18
 import { setFilmstripVisible } from '../../actions';
19
 import { setFilmstripVisible } from '../../actions';
19
 import { shouldRemoteVideosBeVisible } from '../../functions';
20
 import { shouldRemoteVideosBeVisible } from '../../functions';
83
      */
84
      */
84
     _visible: boolean,
85
     _visible: boolean,
85
 
86
 
87
+    /**
88
+     * Whether or not the toolbox is displayed.
89
+     */
90
+    _isToolboxVisible: Boolean,
91
+
86
     /**
92
     /**
87
      * The redux {@code dispatch} function.
93
      * The redux {@code dispatch} function.
88
      */
94
      */
114
         // Bind event handlers so they are only bound once for every instance.
120
         // Bind event handlers so they are only bound once for every instance.
115
         this._onShortcutToggleFilmstrip = this._onShortcutToggleFilmstrip.bind(this);
121
         this._onShortcutToggleFilmstrip = this._onShortcutToggleFilmstrip.bind(this);
116
         this._onToolbarToggleFilmstrip = this._onToolbarToggleFilmstrip.bind(this);
122
         this._onToolbarToggleFilmstrip = this._onToolbarToggleFilmstrip.bind(this);
123
+        this._onTabIn = this._onTabIn.bind(this);
117
     }
124
     }
118
 
125
 
119
     /**
126
     /**
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
      * Dispatches an action to change the visibility of the filmstrip.
262
      * Dispatches an action to change the visibility of the filmstrip.
243
      *
263
      *
298
         const { t } = this.props;
318
         const { t } = this.props;
299
 
319
 
300
         return (
320
         return (
301
-            <div className = 'filmstrip__toolbar'>
321
+            <div
322
+                className = 'filmstrip__toolbar'>
302
                 <button
323
                 <button
324
+                    aria-expanded = { this.props._visible }
303
                     aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }
325
                     aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }
304
                     id = 'toggleFilmstripButton'
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
                 </button>
333
                 </button>
308
             </div>
334
             </div>
309
         );
335
         );
342
         _participants: state['features/base/participants'],
368
         _participants: state['features/base/participants'],
343
         _rows: gridDimensions.rows,
369
         _rows: gridDimensions.rows,
344
         _videosClassName: videosClassName,
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 Переглянути файл

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

+ 1
- 0
react/features/google-api/components/GoogleSignInButton.web.js Переглянути файл

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

+ 3
- 1
react/features/invite/components/add-people-dialog/web/CopyMeetingLinkSection.js Переглянути файл

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

+ 22
- 1
react/features/invite/components/add-people-dialog/web/DialInNumber.js Переглянути файл

49
 
49
 
50
         // Bind event handler so it is only bound once for every instance.
50
         // Bind event handler so it is only bound once for every instance.
51
         this._onCopyText = this._onCopyText.bind(this);
51
         this._onCopyText = this._onCopyText.bind(this);
52
+        this._onCopyTextKeyPress = this._onCopyTextKeyPress.bind(this);
52
     }
53
     }
53
 
54
 
54
     _onCopyText: () => void;
55
     _onCopyText: () => void;
68
         copyText(textToCopy);
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
      * Implements React's {@link Component#render()}.
89
      * Implements React's {@link Component#render()}.
73
      *
90
      *
101
                     </span>
118
                     </span>
102
                 </div>
119
                 </div>
103
                 <a
120
                 <a
121
+                    aria-label = { t('info.copyNumber') }
104
                     className = 'dial-in-copy'
122
                     className = 'dial-in-copy'
105
-                    onClick = { this._onCopyText }>
123
+                    onClick = { this._onCopyText }
124
+                    onKeyPress = { this._onCopyTextKeyPress }
125
+                    role = 'button'
126
+                    tabIndex = { 0 }>
106
                     <Icon src = { IconCopy } />
127
                     <Icon src = { IconCopy } />
107
                 </a>
128
                 </a>
108
             </div>
129
             </div>

+ 40
- 2
react/features/invite/components/add-people-dialog/web/InviteByEmailSection.js Переглянути файл

52
         copyText(inviteText);
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
      * Toggles the email invite drawer.
70
      * Toggles the email invite drawer.
57
      *
71
      *
61
         setIsActive(!isActive);
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
      * Renders clickable elements that each open an email client
93
      * Renders clickable elements that each open an email client
66
      * containing a conference invite.
94
      * containing a conference invite.
101
                             key = { idx }
129
                             key = { idx }
102
                             position = 'top'>
130
                             position = 'top'>
103
                             <a
131
                             <a
132
+                                aria-label = { t(tooltipKey) }
104
                                 className = 'provider-icon'
133
                                 className = 'provider-icon'
105
                                 href = { url }
134
                                 href = { url }
106
                                 rel = 'noopener noreferrer'
135
                                 rel = 'noopener noreferrer'
119
         <>
148
         <>
120
             <div>
149
             <div>
121
                 <div
150
                 <div
151
+                    aria-expanded = { isActive }
152
+                    aria-label = { t('addPeople.shareInvite') }
122
                     className = { `invite-more-dialog email-container${isActive ? ' active' : ''}` }
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
                     <span>{t('addPeople.shareInvite')}</span>
158
                     <span>{t('addPeople.shareInvite')}</span>
125
                     <Icon src = { IconArrowDownSmall } />
159
                     <Icon src = { IconArrowDownSmall } />
126
                 </div>
160
                 </div>
129
                         content = { t('addPeople.copyInvite') }
163
                         content = { t('addPeople.copyInvite') }
130
                         position = 'top'>
164
                         position = 'top'>
131
                         <div
165
                         <div
166
+                            aria-label = { t('addPeople.copyInvite') }
132
                             className = 'copy-invite-icon'
167
                             className = 'copy-invite-icon'
133
-                            onClick = { _onCopyText }>
168
+                            onClick = { _onCopyText }
169
+                            onKeyPress = { _onCopyTextKeyPress }
170
+                            role = 'button'
171
+                            tabIndex = { 0 }>
134
                             <Icon src = { IconCopy } />
172
                             <Icon src = { IconCopy } />
135
                         </div>
173
                         </div>
136
                     </Tooltip>
174
                     </Tooltip>

+ 46
- 4
react/features/invite/components/add-people-dialog/web/InviteContactsForm.js Переглянути файл

76
 
76
 
77
         // Bind event handlers so they are only bound once per instance.
77
         // Bind event handlers so they are only bound once per instance.
78
         this._onClearItems = this._onClearItems.bind(this);
78
         this._onClearItems = this._onClearItems.bind(this);
79
+        this._onClearItemsKeyPress = this._onClearItemsKeyPress.bind(this);
79
         this._onItemSelected = this._onItemSelected.bind(this);
80
         this._onItemSelected = this._onItemSelected.bind(this);
80
         this._onSelectionChange = this._onSelectionChange.bind(this);
81
         this._onSelectionChange = this._onSelectionChange.bind(this);
81
         this._onSubmit = this._onSubmit.bind(this);
82
         this._onSubmit = this._onSubmit.bind(this);
83
+        this._onSubmitKeyPress = this._onSubmitKeyPress.bind(this);
82
         this._parseQueryResults = this._parseQueryResults.bind(this);
84
         this._parseQueryResults = this._parseQueryResults.bind(this);
83
         this._setMultiSelectElement = this._setMultiSelectElement.bind(this);
85
         this._setMultiSelectElement = this._setMultiSelectElement.bind(this);
84
         this._renderFooterText = this._renderFooterText.bind(this);
86
         this._renderFooterText = this._renderFooterText.bind(this);
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
     _onKeyDown: (Object) => void;
267
     _onKeyDown: (Object) => void;
250
 
268
 
251
     /**
269
     /**
425
         this.setState({ inviteItems: [] });
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
      * Renders the add/cancel actions for the form.
463
      * Renders the add/cancel actions for the form.
430
      *
464
      *
441
         return (
475
         return (
442
             <div className = { `invite-more-dialog invite-buttons${this._isAddDisabled() ? ' disabled' : ''}` }>
476
             <div className = { `invite-more-dialog invite-buttons${this._isAddDisabled() ? ' disabled' : ''}` }>
443
                 <a
477
                 <a
478
+                    aria-label = { t('dialog.Cancel') }
444
                     className = 'invite-more-dialog invite-buttons-cancel'
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
                     {t('dialog.Cancel')}
484
                     {t('dialog.Cancel')}
447
                 </a>
485
                 </a>
448
                 <a
486
                 <a
487
+                    aria-label = { t('addPeople.add') }
449
                     className = 'invite-more-dialog invite-buttons-add'
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
                     {t('addPeople.add')}
493
                     {t('addPeople.add')}
452
                 </a>
494
                 </a>
453
             </div>
495
             </div>
480
                 </span>
522
                 </span>
481
                 <span>
523
                 <span>
482
                     <a
524
                     <a
525
+                        aria-label = { supportLink }
483
                         href = { supportLink }
526
                         href = { supportLink }
484
-                        rel = 'noopener noreferrer'
485
-                        target = '_blank'>
527
+                        rel = 'noopener noreferrer'>
486
                         { t('inlineDialogFailure.support') }
528
                         { t('inlineDialogFailure.support') }
487
                     </a>
529
                     </a>
488
                 </span>
530
                 </span>

+ 3
- 1
react/features/keyboard-shortcuts/components/KeyboardShortcutsDialog.web.js Переглянути файл

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

+ 3
- 1
react/features/large-video/components/LargeVideo.web.js Переглянути файл

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

+ 3
- 1
react/features/lobby/components/web/LobbySection.js Переглянути файл

89
         return (
89
         return (
90
             <>
90
             <>
91
                 <div id = 'lobby-section'>
91
                 <div id = 'lobby-section'>
92
-                    <p className = 'description'>
92
+                    <p
93
+                        className = 'description'
94
+                        role = 'banner'>
93
                         { t('lobby.enableDialogText') }
95
                         { t('lobby.enableDialogText') }
94
                     </p>
96
                     </p>
95
                     <div className = 'control-row'>
97
                     <div className = 'control-row'>

+ 6
- 2
react/features/local-recording/components/LocalRecordingInfoDialog.js Переглянути файл

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

+ 2
- 2
react/features/notifications/components/web/Notification.js Переглянути файл

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

+ 9
- 3
react/features/notifications/components/web/NotificationsContainer.js Переглянути файл

3
 import { FlagGroup } from '@atlaskit/flag';
3
 import { FlagGroup } from '@atlaskit/flag';
4
 import React from 'react';
4
 import React from 'react';
5
 
5
 
6
+import { translate } from '../../../base/i18n';
6
 import { connect } from '../../../base/redux';
7
 import { connect } from '../../../base/redux';
7
 import AbstractNotificationsContainer, {
8
 import AbstractNotificationsContainer, {
8
     _abstractMapStateToProps,
9
     _abstractMapStateToProps,
16
     /**
17
     /**
17
      * Whether we are a SIP gateway or not.
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
         return (
48
         return (
43
             <FlagGroup
49
             <FlagGroup
44
                 id = 'notifications-container'
50
                 id = 'notifications-container'
51
+                label = { this.props.t('notify.groupTitle') }
45
                 onDismissed = { this._onDismissed }>
52
                 onDismissed = { this._onDismissed }>
46
                 { this._renderFlags() }
53
                 { this._renderFlags() }
47
             </FlagGroup>
54
             </FlagGroup>
73
                     key = { uid }
80
                     key = { uid }
74
                     onDismissed = { this._onDismissed }
81
                     onDismissed = { this._onDismissed }
75
                     uid = { uid } />
82
                     uid = { uid } />
76
-
77
             );
83
             );
78
         });
84
         });
79
     }
85
     }
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 Переглянути файл

29
 
29
 
30
         return (
30
         return (
31
             <OverlayFrame isLightOverlay = { isNetworkFailure }>
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
                     <span
37
                     <span
34
-                        className = 'reload_overlay_title'>
38
+                        className = 'reload_overlay_title'
39
+                        id = 'reload_overlay_title'
40
+                        role = 'heading'>
35
                         { t(title) }
41
                         { t(title) }
36
                     </span>
42
                     </span>
37
-                    <span className = 'reload_overlay_text'>
43
+                    <span
44
+                        className = 'reload_overlay_text'
45
+                        id = 'reload_overlay_text'>
38
                         { t(message, { seconds: timeLeft }) }
46
                         { t(message, { seconds: timeLeft }) }
39
                     </span>
47
                     </span>
40
                     { this._renderProgressBar() }
48
                     { this._renderProgressBar() }

+ 13
- 4
react/features/overlay/components/web/UserMediaPermissionsOverlay.js Переглянути файл

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

+ 1
- 1
react/features/participants-pane/components/InviteButton.js Переглянути файл

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

+ 5
- 1
react/features/participants-pane/components/MeetingParticipantItem.js Переглянути файл

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

+ 12
- 1
react/features/participants-pane/components/ParticipantsPane.js Переглянути файл

30
     const { t } = useTranslation();
30
     const { t } = useTranslation();
31
 
31
 
32
     const closePane = useCallback(() => dispatch(close(), [ dispatch ]));
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
     const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)), [ dispatch ]);
39
     const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)), [ dispatch ]);
34
 
40
 
35
     return (
41
     return (
41
                 ) }>
47
                 ) }>
42
                 <div className = 'participants_pane-content'>
48
                 <div className = 'participants_pane-content'>
43
                     <Header>
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
                     </Header>
56
                     </Header>
46
                     <Container>
57
                     <Container>
47
                         <LobbyParticipantList />
58
                         <LobbyParticipantList />

+ 75
- 3
react/features/prejoin/components/Prejoin.js Переглянути файл

183
         this._onDropdownClose = this._onDropdownClose.bind(this);
183
         this._onDropdownClose = this._onDropdownClose.bind(this);
184
         this._onOptionsClick = this._onOptionsClick.bind(this);
184
         this._onOptionsClick = this._onOptionsClick.bind(this);
185
         this._setName = this._setName.bind(this);
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
     _onJoinButtonClick: () => void;
190
     _onJoinButtonClick: () => void;
188
 
191
 
205
         this.props.joinConference();
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
     _onToggleButtonClick: () => void;
227
     _onToggleButtonClick: () => void;
209
 
228
 
210
     /**
229
     /**
283
         this._onDropdownClose();
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
      * Implements React's {@link Component#render()}.
340
      * Implements React's {@link Component#render()}.
288
      *
341
      *
305
             visibleButtons
358
             visibleButtons
306
         } = this.props;
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
         const { showJoinByPhoneButtons, showError } = this.state;
363
         const { showJoinByPhoneButtons, showError } = this.state;
310
 
364
 
311
         return (
365
         return (
322
                 {showJoinActions && (
376
                 {showJoinActions && (
323
                     <div className = 'prejoin-input-area-container'>
377
                     <div className = 'prejoin-input-area-container'>
324
                         <div className = 'prejoin-input-area'>
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
                             <InputField
383
                             <InputField
384
+                                autoComplete = { 'name' }
326
                                 autoFocus = { true }
385
                                 autoFocus = { true }
327
                                 className = { showError ? 'error' : '' }
386
                                 className = { showError ? 'error' : '' }
328
                                 hasError = { showError }
387
                                 hasError = { showError }
388
+                                id = { 'Prejoin-input-field-id' }
329
                                 onChange = { _setName }
389
                                 onChange = { _setName }
330
                                 onSubmit = { joinConference }
390
                                 onSubmit = { joinConference }
331
                                 placeHolder = { t('dialog.enterDisplayName') }
391
                                 placeHolder = { t('dialog.enterDisplayName') }
341
                                         <div
401
                                         <div
342
                                             className = 'prejoin-preview-dropdown-btn'
402
                                             className = 'prejoin-preview-dropdown-btn'
343
                                             data-testid = 'prejoin.joinWithoutAudio'
403
                                             data-testid = 'prejoin.joinWithoutAudio'
344
-                                            onClick = { joinConferenceWithoutAudio }>
404
+                                            onClick = { joinConferenceWithoutAudio }
405
+                                            onKeyPress = { _onJoinConferenceWithoutAudioKeyPress }
406
+                                            role = 'button'
407
+                                            tabIndex = { 0 }>
345
                                             <Icon
408
                                             <Icon
346
                                                 className = 'prejoin-preview-dropdown-icon'
409
                                                 className = 'prejoin-preview-dropdown-icon'
347
                                                 size = { 24 }
410
                                                 size = { 24 }
350
                                         </div>
413
                                         </div>
351
                                         {hasJoinByPhoneButton && <div
414
                                         {hasJoinByPhoneButton && <div
352
                                             className = 'prejoin-preview-dropdown-btn'
415
                                             className = 'prejoin-preview-dropdown-btn'
353
-                                            onClick = { _showDialog }>
416
+                                            onClick = { _showDialog }
417
+                                            onKeyPress = { _showDialogKeyPress }
418
+                                            role = 'button'
419
+                                            tabIndex = { 0 }>
354
                                             <Icon
420
                                             <Icon
355
                                                 className = 'prejoin-preview-dropdown-icon'
421
                                                 className = 'prejoin-preview-dropdown-icon'
356
                                                 data-testid = 'prejoin.joinByPhone'
422
                                                 data-testid = 'prejoin.joinByPhone'
363
                                     onClose = { _onDropdownClose }>
429
                                     onClose = { _onDropdownClose }>
364
                                     <ActionButton
430
                                     <ActionButton
365
                                         OptionsIcon = { showJoinByPhoneButtons ? IconArrowUp : IconArrowDown }
431
                                         OptionsIcon = { showJoinByPhoneButtons ? IconArrowUp : IconArrowDown }
432
+                                        ariaDropDownLabel = { t('prejoin.joinWithoutAudio') }
433
+                                        ariaLabel = { t('prejoin.joinMeeting') }
434
+                                        ariaPressed = { showJoinByPhoneButtons }
366
                                         hasOptions = { true }
435
                                         hasOptions = { true }
367
                                         onClick = { _onJoinButtonClick }
436
                                         onClick = { _onJoinButtonClick }
437
+                                        onKeyPress = { _onJoinKeyPress }
368
                                         onOptionsClick = { _onOptionsClick }
438
                                         onOptionsClick = { _onOptionsClick }
439
+                                        role = 'button'
440
+                                        tabIndex = { 0 }
369
                                         testId = 'prejoin.joinMeeting'
441
                                         testId = 'prejoin.joinMeeting'
370
                                         type = 'primary'>
442
                                         type = 'primary'>
371
                                         { t('prejoin.joinMeeting') }
443
                                         { t('prejoin.joinMeeting') }

+ 3
- 4
react/features/prejoin/components/country-picker/CountryPicker.js Переглянути файл

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

+ 0
- 0
react/features/prejoin/components/country-picker/CountrySelector.js Переглянути файл


Деякі файли не було показано, через те що забагато файлів було змінено

Завантаження…
Відмінити
Зберегти