Преглед изворни кода

Globally improve accessibility for screen reader users (#12969)

feat(a11y): Globally improve accessibility for screen reader users
factor2
Emmanuel Pelletier пре 2 година
родитељ
комит
51a4e7daa3
No account linked to committer's email address
64 измењених фајлова са 776 додато и 428 уклоњено
  1. 33
    0
      css/_utils.scss
  2. 1
    1
      css/modals/security/_security.scss
  3. 13
    1
      lang/main.json
  4. 171
    1
      package-lock.json
  5. 1
    1
      package.json
  6. 2
    0
      react/features/authentication/components/web/LoginDialog.tsx
  7. 44
    15
      react/features/base/buttons/CopyButton.web.tsx
  8. 19
    0
      react/features/base/icons/components/Icon.tsx
  9. 10
    0
      react/features/base/label/components/web/Label.tsx
  10. 85
    26
      react/features/base/popover/components/Popover.web.tsx
  11. 1
    0
      react/features/base/react/components/web/BaseIndicator.tsx
  12. 6
    0
      react/features/base/react/components/web/MultiSelectAutocomplete.tsx
  13. 4
    49
      react/features/base/toolbox/components/web/ToolboxButtonWithPopup.tsx
  14. 1
    0
      react/features/base/tooltip/components/Tooltip.tsx
  15. 8
    6
      react/features/base/ui/components/web/BaseDialog.tsx
  16. 6
    5
      react/features/base/ui/components/web/Checkbox.tsx
  17. 29
    7
      react/features/base/ui/components/web/ContextMenuItem.tsx
  18. 11
    4
      react/features/base/ui/components/web/Dialog.tsx
  19. 11
    13
      react/features/base/ui/components/web/DialogWithTabs.tsx
  20. 18
    5
      react/features/base/ui/components/web/Input.tsx
  21. 3
    0
      react/features/base/ui/components/web/MultiSelect.tsx
  22. 17
    2
      react/features/base/ui/components/web/Select.tsx
  23. 36
    4
      react/features/base/ui/components/web/Switch.tsx
  24. 25
    0
      react/features/base/ui/functions.web.ts
  25. 1
    0
      react/features/chat/components/web/ChatInput.tsx
  26. 1
    0
      react/features/conference/components/web/RaisedHandsCountLabel.tsx
  27. 3
    1
      react/features/connection-indicator/components/web/ConnectionIndicator.tsx
  28. 11
    8
      react/features/connection-stats/components/ConnectionStatsTable.tsx
  29. 2
    0
      react/features/device-selection/components/DeviceSelector.web.tsx
  30. 1
    0
      react/features/device-selection/components/VideoDeviceSelection.web.tsx
  31. 1
    0
      react/features/display-name/components/web/DisplayNamePrompt.tsx
  32. 3
    1
      react/features/embed-meeting/components/EmbedMeetingDialog.tsx
  33. 14
    9
      react/features/feedback/components/FeedbackDialog.web.tsx
  34. 15
    12
      react/features/filmstrip/components/web/Thumbnail.tsx
  35. 1
    0
      react/features/gifs/components/web/GifsMenu.tsx
  36. 3
    6
      react/features/invite/components/add-people-dialog/web/CopyMeetingLinkSection.tsx
  37. 6
    24
      react/features/invite/components/add-people-dialog/web/DialInNumber.tsx
  38. 1
    0
      react/features/invite/components/add-people-dialog/web/InviteContactsForm.tsx
  39. 2
    0
      react/features/lobby/components/web/LobbyScreen.tsx
  40. 1
    0
      react/features/participants-pane/components/web/MeetingParticipants.tsx
  41. 2
    0
      react/features/polls/components/web/PollCreate.tsx
  42. 2
    0
      react/features/prejoin/components/web/Prejoin.tsx
  43. 0
    7
      react/features/reactions/components/web/ReactionsMenuButton.tsx
  44. 2
    1
      react/features/recording/components/LiveStream/AbstractLiveStreamButton.ts
  45. 4
    46
      react/features/recording/components/LiveStream/web/StreamKeyForm.tsx
  46. 1
    0
      react/features/recording/components/LiveStream/web/StreamKeyPicker.tsx
  47. 2
    1
      react/features/recording/components/Recording/AbstractRecordButton.ts
  48. 40
    11
      react/features/recording/components/Recording/web/StartRecordingDialogContent.tsx
  49. 1
    0
      react/features/room-lock/components/PasswordRequiredPrompt.web.tsx
  50. 1
    0
      react/features/screen-share/components/web/ShareAudioDialog.tsx
  51. 33
    125
      react/features/security/components/security-dialog/web/PasswordSection.tsx
  52. 2
    0
      react/features/settings/components/web/MoreTab.tsx
  53. 0
    2
      react/features/settings/components/web/audio/AudioSettingsContent.tsx
  54. 2
    10
      react/features/settings/components/web/audio/MicrophoneEntry.tsx
  55. 3
    8
      react/features/settings/components/web/audio/SpeakerEntry.tsx
  56. 1
    1
      react/features/settings/components/web/video/VideoSettingsContent.tsx
  57. 2
    1
      react/features/shared-video/components/web/SharedVideoDialog.tsx
  58. 7
    1
      react/features/toolbox/components/web/DialogPortal.ts
  59. 11
    9
      react/features/toolbox/components/web/Drawer.tsx
  60. 3
    1
      react/features/video-quality/components/Slider.web.tsx
  61. 1
    0
      react/features/video-quality/components/VideoQualityLabel.web.tsx
  62. 8
    2
      react/features/video-quality/components/VideoQualitySlider.web.tsx
  63. 0
    1
      react/features/virtual-background/components/UploadImageButton.tsx
  64. 27
    0
      react/features/virtual-background/components/VirtualBackgrounds.tsx

+ 33
- 0
css/_utils.scss Прегледај датотеку

@@ -41,3 +41,36 @@
41 41
     display: -webkit-flex !important;
42 42
     display: flex !important;
43 43
 }
44
+
45
+/**
46
+ * resets default button styles,
47
+ * mostly intended to be used on interactive elements that
48
+ * differ from their default styles (e.g. <a>) or have custom styles
49
+ */
50
+.invisible-button {
51
+    background: none;
52
+    border: none;
53
+    color: inherit;
54
+    cursor: pointer;
55
+    padding: 0;
56
+}
57
+
58
+
59
+/**
60
+ * style an element the same as an <a>
61
+ * useful on some cases where we visually have a link but it's actually a <button>
62
+ */
63
+.as-link {
64
+    @extend .invisible-button;
65
+
66
+    display: inline;
67
+    color: #44A5FF;
68
+    text-decoration: none;
69
+    font-weight: bold;
70
+
71
+    &:focus,
72
+    &:hover,
73
+    &:active {
74
+        text-decoration: underline;
75
+    }
76
+}

+ 1
- 1
css/modals/security/_security.scss Прегледај датотеку

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

+ 13
- 1
lang/main.json Прегледај датотеку

@@ -1,5 +1,8 @@
1 1
 {
2 2
     "addPeople": {
3
+        "accessibilityLabel": {
4
+            "meetingLink": "Meeting link: {{url}}"
5
+        },
3 6
         "add": "Invite",
4 7
         "addContacts": "Invite your contacts",
5 8
         "contacts": "contacts",
@@ -254,6 +257,8 @@
254 257
         "WaitingForHostTitle": "Waiting for the host ...",
255 258
         "Yes": "Yes",
256 259
         "accessibilityLabel": {
260
+            "Cancel": "Cancel (leave dialog)",
261
+            "Ok": "OK (save and leave dialog)",
257 262
             "close": "Close dialog",
258 263
             "liveStreaming": "Live Stream",
259 264
             "sharingTabs": "Sharing options"
@@ -459,6 +464,9 @@
459 464
         "title": "Embed this meeting"
460 465
     },
461 466
     "feedback": {
467
+        "accessibilityLabel": {
468
+            "yourChoice": "Your choice: {{rating}}"
469
+        },
462 470
         "average": "Average",
463 471
         "bad": "Bad",
464 472
         "detailsLabel": "Tell us more about it.",
@@ -1341,7 +1349,7 @@
1341 1349
         "audioOnly": "AUD",
1342 1350
         "audioOnlyExpanded": "You are in low bandwidth mode. In this mode you will receive only audio and screen sharing.",
1343 1351
         "bestPerformance": "Best performance",
1344
-        "callQuality": "Video Quality",
1352
+        "callQuality": "Video Quality (0 for best performance, 3 for highest quality)",
1345 1353
         "hd": "HD",
1346 1354
         "hdTooltip": "Viewing high definition video",
1347 1355
         "highDefinition": "High definition",
@@ -1383,6 +1391,10 @@
1383 1391
         "videomute": "Participant has stopped the camera"
1384 1392
     },
1385 1393
     "virtualBackground": {
1394
+        "accessibilityLabel": {
1395
+            "currentBackground": "Current background: {{background}}",
1396
+            "selectBackground": "Select a background"
1397
+        },
1386 1398
         "addBackground": "Add background",
1387 1399
         "apply": "Apply",
1388 1400
         "backgroundEffectError": "Failed to apply background effect.",

+ 171
- 1
package-lock.json Прегледај датотеку

@@ -71,7 +71,7 @@
71 71
         "react": "18.2.0",
72 72
         "react-dom": "18.2.0",
73 73
         "react-emoji-render": "1.2.4",
74
-        "react-focus-lock": "2.9.4",
74
+        "react-focus-on": "3.8.1",
75 75
         "react-i18next": "10.11.4",
76 76
         "react-linkify": "1.0.0-alpha",
77 77
         "react-native": "0.69.10",
@@ -6926,6 +6926,17 @@
6926 6926
         "sprintf-js": "~1.0.2"
6927 6927
       }
6928 6928
     },
6929
+    "node_modules/aria-hidden": {
6930
+      "version": "1.2.3",
6931
+      "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz",
6932
+      "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==",
6933
+      "dependencies": {
6934
+        "tslib": "^2.0.0"
6935
+      },
6936
+      "engines": {
6937
+        "node": ">=10"
6938
+      }
6939
+    },
6929 6940
     "node_modules/arr-diff": {
6930 6941
       "version": "4.0.0",
6931 6942
       "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@@ -10640,6 +10651,14 @@
10640 10651
         "url": "https://github.com/sponsors/ljharb"
10641 10652
       }
10642 10653
     },
10654
+    "node_modules/get-nonce": {
10655
+      "version": "1.0.1",
10656
+      "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
10657
+      "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
10658
+      "engines": {
10659
+        "node": ">=6"
10660
+      }
10661
+    },
10643 10662
     "node_modules/get-stream": {
10644 10663
       "version": "4.1.0",
10645 10664
       "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
@@ -15581,6 +15600,32 @@
15581 15600
         }
15582 15601
       }
15583 15602
     },
15603
+    "node_modules/react-focus-on": {
15604
+      "version": "3.8.1",
15605
+      "resolved": "https://registry.npmjs.org/react-focus-on/-/react-focus-on-3.8.1.tgz",
15606
+      "integrity": "sha512-fQcBx+SZMgXoRL+69r5+ic4bdVgqaCeKeoFPra8yhcSuL/3unWavfdirEFBGgH71K+RiocMTS6DETHcX0SlOZg==",
15607
+      "dependencies": {
15608
+        "aria-hidden": "^1.2.2",
15609
+        "react-focus-lock": "^2.9.2",
15610
+        "react-remove-scroll": "^2.5.6",
15611
+        "react-style-singleton": "^2.2.0",
15612
+        "tslib": "^2.3.1",
15613
+        "use-callback-ref": "^1.3.0",
15614
+        "use-sidecar": "^1.1.2"
15615
+      },
15616
+      "engines": {
15617
+        "node": ">=8.5.0"
15618
+      },
15619
+      "peerDependencies": {
15620
+        "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
15621
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
15622
+      },
15623
+      "peerDependenciesMeta": {
15624
+        "@types/react": {
15625
+          "optional": true
15626
+        }
15627
+      }
15628
+    },
15584 15629
     "node_modules/react-freeze": {
15585 15630
       "version": "1.0.0",
15586 15631
       "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.0.tgz",
@@ -16107,6 +16152,51 @@
16107 16152
         "node": ">=0.10.0"
16108 16153
       }
16109 16154
     },
16155
+    "node_modules/react-remove-scroll": {
16156
+      "version": "2.5.6",
16157
+      "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.6.tgz",
16158
+      "integrity": "sha512-bO856ad1uDYLefgArk559IzUNeQ6SWH4QnrevIUjH+GczV56giDfl3h0Idptf2oIKxQmd1p9BN25jleKodTALg==",
16159
+      "dependencies": {
16160
+        "react-remove-scroll-bar": "^2.3.4",
16161
+        "react-style-singleton": "^2.2.1",
16162
+        "tslib": "^2.1.0",
16163
+        "use-callback-ref": "^1.3.0",
16164
+        "use-sidecar": "^1.1.2"
16165
+      },
16166
+      "engines": {
16167
+        "node": ">=10"
16168
+      },
16169
+      "peerDependencies": {
16170
+        "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
16171
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
16172
+      },
16173
+      "peerDependenciesMeta": {
16174
+        "@types/react": {
16175
+          "optional": true
16176
+        }
16177
+      }
16178
+    },
16179
+    "node_modules/react-remove-scroll-bar": {
16180
+      "version": "2.3.4",
16181
+      "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz",
16182
+      "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==",
16183
+      "dependencies": {
16184
+        "react-style-singleton": "^2.2.1",
16185
+        "tslib": "^2.0.0"
16186
+      },
16187
+      "engines": {
16188
+        "node": ">=10"
16189
+      },
16190
+      "peerDependencies": {
16191
+        "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
16192
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
16193
+      },
16194
+      "peerDependenciesMeta": {
16195
+        "@types/react": {
16196
+          "optional": true
16197
+        }
16198
+      }
16199
+    },
16110 16200
     "node_modules/react-shallow-renderer": {
16111 16201
       "version": "16.15.0",
16112 16202
       "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz",
@@ -16119,6 +16209,28 @@
16119 16209
         "react": "^16.0.0 || ^17.0.0 || ^18.0.0"
16120 16210
       }
16121 16211
     },
16212
+    "node_modules/react-style-singleton": {
16213
+      "version": "2.2.1",
16214
+      "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
16215
+      "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
16216
+      "dependencies": {
16217
+        "get-nonce": "^1.0.0",
16218
+        "invariant": "^2.2.4",
16219
+        "tslib": "^2.0.0"
16220
+      },
16221
+      "engines": {
16222
+        "node": ">=10"
16223
+      },
16224
+      "peerDependencies": {
16225
+        "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
16226
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
16227
+      },
16228
+      "peerDependenciesMeta": {
16229
+        "@types/react": {
16230
+          "optional": true
16231
+        }
16232
+      }
16233
+    },
16122 16234
     "node_modules/react-textarea-autosize": {
16123 16235
       "version": "8.3.0",
16124 16236
       "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.3.0.tgz",
@@ -24738,6 +24850,14 @@
24738 24850
         "sprintf-js": "~1.0.2"
24739 24851
       }
24740 24852
     },
24853
+    "aria-hidden": {
24854
+      "version": "1.2.3",
24855
+      "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz",
24856
+      "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==",
24857
+      "requires": {
24858
+        "tslib": "^2.0.0"
24859
+      }
24860
+    },
24741 24861
     "arr-diff": {
24742 24862
       "version": "4.0.0",
24743 24863
       "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@@ -27571,6 +27691,11 @@
27571 27691
         "has-symbols": "^1.0.3"
27572 27692
       }
27573 27693
     },
27694
+    "get-nonce": {
27695
+      "version": "1.0.1",
27696
+      "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
27697
+      "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="
27698
+    },
27574 27699
     "get-stream": {
27575 27700
       "version": "4.1.0",
27576 27701
       "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
@@ -31287,6 +31412,20 @@
31287 31412
         "use-sidecar": "^1.1.2"
31288 31413
       }
31289 31414
     },
31415
+    "react-focus-on": {
31416
+      "version": "3.8.1",
31417
+      "resolved": "https://registry.npmjs.org/react-focus-on/-/react-focus-on-3.8.1.tgz",
31418
+      "integrity": "sha512-fQcBx+SZMgXoRL+69r5+ic4bdVgqaCeKeoFPra8yhcSuL/3unWavfdirEFBGgH71K+RiocMTS6DETHcX0SlOZg==",
31419
+      "requires": {
31420
+        "aria-hidden": "^1.2.2",
31421
+        "react-focus-lock": "^2.9.2",
31422
+        "react-remove-scroll": "^2.5.6",
31423
+        "react-style-singleton": "^2.2.0",
31424
+        "tslib": "^2.3.1",
31425
+        "use-callback-ref": "^1.3.0",
31426
+        "use-sidecar": "^1.1.2"
31427
+      }
31428
+    },
31290 31429
     "react-freeze": {
31291 31430
       "version": "1.0.0",
31292 31431
       "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.0.tgz",
@@ -31658,6 +31797,27 @@
31658 31797
       "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz",
31659 31798
       "integrity": "sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA=="
31660 31799
     },
31800
+    "react-remove-scroll": {
31801
+      "version": "2.5.6",
31802
+      "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.6.tgz",
31803
+      "integrity": "sha512-bO856ad1uDYLefgArk559IzUNeQ6SWH4QnrevIUjH+GczV56giDfl3h0Idptf2oIKxQmd1p9BN25jleKodTALg==",
31804
+      "requires": {
31805
+        "react-remove-scroll-bar": "^2.3.4",
31806
+        "react-style-singleton": "^2.2.1",
31807
+        "tslib": "^2.1.0",
31808
+        "use-callback-ref": "^1.3.0",
31809
+        "use-sidecar": "^1.1.2"
31810
+      }
31811
+    },
31812
+    "react-remove-scroll-bar": {
31813
+      "version": "2.3.4",
31814
+      "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz",
31815
+      "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==",
31816
+      "requires": {
31817
+        "react-style-singleton": "^2.2.1",
31818
+        "tslib": "^2.0.0"
31819
+      }
31820
+    },
31661 31821
     "react-shallow-renderer": {
31662 31822
       "version": "16.15.0",
31663 31823
       "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz",
@@ -31667,6 +31827,16 @@
31667 31827
         "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0"
31668 31828
       }
31669 31829
     },
31830
+    "react-style-singleton": {
31831
+      "version": "2.2.1",
31832
+      "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
31833
+      "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
31834
+      "requires": {
31835
+        "get-nonce": "^1.0.0",
31836
+        "invariant": "^2.2.4",
31837
+        "tslib": "^2.0.0"
31838
+      }
31839
+    },
31670 31840
     "react-textarea-autosize": {
31671 31841
       "version": "8.3.0",
31672 31842
       "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.3.0.tgz",

+ 1
- 1
package.json Прегледај датотеку

@@ -76,7 +76,7 @@
76 76
     "react": "18.2.0",
77 77
     "react-dom": "18.2.0",
78 78
     "react-emoji-render": "1.2.4",
79
-    "react-focus-lock": "2.9.4",
79
+    "react-focus-on": "3.8.1",
80 80
     "react-i18next": "10.11.4",
81 81
     "react-linkify": "1.0.0-alpha",
82 82
     "react-native": "0.69.10",

+ 2
- 0
react/features/authentication/components/web/LoginDialog.tsx Прегледај датотеку

@@ -268,6 +268,7 @@ class LoginDialog extends Component<IProps, IState> {
268 268
                 titleKey = { t('dialog.authenticationRequired') }>
269 269
                 <Input
270 270
                     autoFocus = { true }
271
+                    id = 'login-dialog-username'
271 272
                     label = { t('dialog.user') }
272 273
                     name = 'username'
273 274
                     onChange = { this._onUsernameChange }
@@ -277,6 +278,7 @@ class LoginDialog extends Component<IProps, IState> {
277 278
                 <br />
278 279
                 <Input
279 280
                     className = 'dialog-bottom-margin'
281
+                    id = 'login-dialog-password'
280 282
                     label = { t('dialog.userPassword') }
281 283
                     name = 'password'
282 284
                     onChange = { this._onPasswordChange }

+ 44
- 15
react/features/base/buttons/CopyButton.web.tsx Прегледај датотеку

@@ -57,6 +57,14 @@ let mounted: boolean;
57 57
 
58 58
 interface IProps {
59 59
 
60
+    /**
61
+     * The invisible text for screen readers.
62
+     *
63
+     * Intended to give the same info as `displayedText`, but can be customized to give more necessary context.
64
+     * If not given, `displayedText` will be used.
65
+     */
66
+    accessibilityText?: string;
67
+
60 68
     /**
61 69
      * Css class to apply on container.
62 70
      */
@@ -93,7 +101,15 @@ interface IProps {
93 101
  *
94 102
  * @returns {React$Element<any>}
95 103
  */
96
-function CopyButton({ className = '', displayedText, textToCopy, textOnHover, textOnCopySuccess, id }: IProps) {
104
+function CopyButton({
105
+    accessibilityText,
106
+    className = '',
107
+    displayedText,
108
+    textToCopy,
109
+    textOnHover,
110
+    textOnCopySuccess,
111
+    id
112
+}: IProps) {
97 113
     const { classes, cx } = useStyles();
98 114
     const [ isClicked, setIsClicked ] = useState(false);
99 115
     const [ isHovered, setIsHovered ] = useState(false);
@@ -196,20 +212,33 @@ function CopyButton({ className = '', displayedText, textToCopy, textOnHover, te
196 212
     }
197 213
 
198 214
     return (
199
-        <div
200
-            aria-label = { textOnHover }
201
-            className = { cx(className, classes.copyButton, isClicked ? ' clicked' : '') }
202
-            id = { id }
203
-            onBlur = { onHoverOut }
204
-            onClick = { onClick }
205
-            onFocus = { onHoverIn }
206
-            onKeyPress = { onKeyPress }
207
-            onMouseOut = { onHoverOut }
208
-            onMouseOver = { onHoverIn }
209
-            role = 'button'
210
-            tabIndex = { 0 }>
211
-            { renderContent() }
212
-        </div>
215
+        <>
216
+            <div
217
+                aria-describedby = { displayedText === textOnHover
218
+                    ? undefined
219
+                    : `${id}-sr-text` }
220
+                aria-label = { displayedText === textOnHover ? accessibilityText : textOnHover }
221
+                className = { cx(className, classes.copyButton, isClicked ? ' clicked' : '') }
222
+                id = { id }
223
+                onBlur = { onHoverOut }
224
+                onClick = { onClick }
225
+                onFocus = { onHoverIn }
226
+                onKeyPress = { onKeyPress }
227
+                onMouseOut = { onHoverOut }
228
+                onMouseOver = { onHoverIn }
229
+                role = 'button'
230
+                tabIndex = { 0 }>
231
+                { renderContent() }
232
+            </div>
233
+
234
+            { displayedText !== textOnHover && (
235
+                <span
236
+                    className = 'sr-only'
237
+                    id = { `${id}-sr-text` }>
238
+                    { accessibilityText }
239
+                </span>
240
+            )}
241
+        </>
213 242
     );
214 243
 }
215 244
 

+ 19
- 0
react/features/base/icons/components/Icon.tsx Прегледај датотеку

@@ -7,6 +7,16 @@ import { IIconProps } from './types';
7 7
 
8 8
 interface IProps extends IIconProps {
9 9
 
10
+    /**
11
+     * Optional label for screen reader users.
12
+     *
13
+     * If set, this is will add a `aria-label` attribute on the svg element,
14
+     * contrary to the aria* props which set attributes on the container element.
15
+     *
16
+     * Use this if the icon conveys meaning and is not clickable.
17
+     */
18
+    alt?: string;
19
+
10 20
     /**
11 21
      * The id of the element this button icon controls.
12 22
      */
@@ -114,6 +124,7 @@ export const DEFAULT_SIZE = navigator.product === 'ReactNative' ? 36 : 22;
114 124
  */
115 125
 export default function Icon(props: IProps) {
116 126
     const {
127
+        alt,
117 128
         className,
118 129
         color,
119 130
         id,
@@ -156,6 +167,13 @@ export default function Icon(props: IProps) {
156 167
 
157 168
     const jitsiIconClassName = calculatedColor ? 'jitsi-icon' : 'jitsi-icon jitsi-icon-default';
158 169
 
170
+    const iconProps = alt ? {
171
+        'aria-label': alt,
172
+        role: 'img'
173
+    } : {
174
+        'aria-hidden': true
175
+    };
176
+
159 177
     return (
160 178
         <Container
161 179
             { ...rest }
@@ -176,6 +194,7 @@ export default function Icon(props: IProps) {
176 194
             style = { restStyle }
177 195
             tabIndex = { tabIndex }>
178 196
             <IconComponent
197
+                { ...iconProps }
179 198
                 fill = { calculatedColor }
180 199
                 height = { calculatedSize }
181 200
                 id = { id }

+ 10
- 0
react/features/base/label/components/web/Label.tsx Прегледај датотеку

@@ -7,6 +7,14 @@ import { COLORS } from '../../constants';
7 7
 
8 8
 interface IProps {
9 9
 
10
+    /**
11
+     * Optional label for screen reader users, invisible in the UI.
12
+     *
13
+     * Note: if the text prop is set, a screen reader will first announce
14
+     * the accessibilityText, then the text.
15
+     */
16
+    accessibilityText?: string;
17
+
10 18
     /**
11 19
      * Own CSS class name.
12 20
      */
@@ -82,6 +90,7 @@ const useStyles = makeStyles()(theme => {
82 90
 });
83 91
 
84 92
 const Label = ({
93
+    accessibilityText,
85 94
     className,
86 95
     color,
87 96
     icon,
@@ -117,6 +126,7 @@ const Label = ({
117 126
                 color = { iconColor }
118 127
                 size = '16'
119 128
                 src = { icon } />}
129
+            {accessibilityText && <span className = 'sr-only'>{accessibilityText}</span>}
120 130
             {text && <span className = { icon && classes.withIcon }>{text}</span>}
121 131
         </div>
122 132
     );

+ 85
- 26
react/features/base/popover/components/Popover.web.tsx Прегледај датотеку

@@ -1,5 +1,5 @@
1 1
 import React, { Component, ReactNode } from 'react';
2
-import ReactFocusLock from 'react-focus-lock';
2
+import { FocusOn } from 'react-focus-on';
3 3
 import { connect } from 'react-redux';
4 4
 
5 5
 import { IReduxState } from '../../../app/types';
@@ -40,6 +40,16 @@ interface IProps {
40 40
      */
41 41
     disablePopover?: boolean;
42 42
 
43
+    /**
44
+     * Whether we can reach the popover element via keyboard or not when trigger is 'hover' (true by default).
45
+     *
46
+     * Only works when trigger is set to 'hover'.
47
+     *
48
+     * There are some rare cases where we want to set this to false,
49
+     * when the popover content is not necessary for screen reader users, because accessible elsewhere.
50
+     */
51
+    focusable?: boolean;
52
+
43 53
     /**
44 54
      * The id of the dom element acting as the Popover label (matches aria-labelledby).
45 55
      */
@@ -103,6 +113,14 @@ interface IState {
103 113
         position: string;
104 114
         top?: string;
105 115
     } | null;
116
+
117
+    /**
118
+     * Whether the popover should be focus locked or not.
119
+     *
120
+     * This is enabled if we notice the popover is interactive
121
+     * (trigger is click or focusable is true).
122
+     */
123
+    enableFocusLock: boolean;
106 124
 }
107 125
 
108 126
 /**
@@ -119,6 +137,7 @@ class Popover extends Component<IProps, IState> {
119 137
      */
120 138
     static defaultProps = {
121 139
         className: '',
140
+        focusable: true,
122 141
         id: '',
123 142
         trigger: 'hover'
124 143
     };
@@ -140,10 +159,12 @@ class Popover extends Component<IProps, IState> {
140 159
         super(props);
141 160
 
142 161
         this.state = {
143
-            contextMenuStyle: null
162
+            contextMenuStyle: null,
163
+            enableFocusLock: false
144 164
         };
145 165
 
146 166
         // Bind event handlers so they are only bound once for every instance.
167
+        this._enableFocusLock = this._enableFocusLock.bind(this);
147 168
         this._onHideDialog = this._onHideDialog.bind(this);
148 169
         this._onShowDialog = this._onShowDialog.bind(this);
149 170
         this._onKeyPress = this._onKeyPress.bind(this);
@@ -207,8 +228,8 @@ class Popover extends Component<IProps, IState> {
207 228
         const { children,
208 229
             className,
209 230
             content,
231
+            focusable,
210 232
             headingId,
211
-            headingLabel,
212 233
             id,
213 234
             overflowDrawer,
214 235
             visible,
@@ -242,35 +263,40 @@ class Popover extends Component<IProps, IState> {
242 263
                 onKeyPress = { this._onKeyPress }
243 264
                 { ...(trigger === 'hover' ? {
244 265
                     onMouseEnter: this._onShowDialog,
245
-                    onMouseLeave: this._onHideDialog,
246
-                    tabIndex: 0
266
+                    onMouseLeave: this._onHideDialog
247 267
                 } : {}) }
268
+                { ...(trigger === 'hover' && focusable && {
269
+                    role: 'button',
270
+                    tabIndex: 0
271
+                }) }
248 272
                 ref = { this._containerRef }>
249 273
                 { visible && (
250 274
                     <DialogPortal
251 275
                         getRef = { this._setContextMenuRef }
276
+                        onVisible = { this._isInteractive() ? this._enableFocusLock : undefined }
252 277
                         setSize = { this._setContextMenuStyle }
253 278
                         style = { this.state.contextMenuStyle }
254 279
                         targetSelector = '.popover-content'>
255
-                        <ReactFocusLock
256
-                            lockProps = {{
257
-                                role: 'dialog',
258
-                                'aria-modal': true,
259
-                                'aria-labelledby': headingId,
260
-                                'aria-label': !headingId && headingLabel ? headingLabel : undefined
261
-                            }}
280
+                        <FocusOn
281
+
282
+                            // Use the `enabled` prop instead of conditionally rendering ReactFocusOn
283
+                            // to prevent UI stutter on dialog appearance. It seems the focus guards generated annoy
284
+                            // our DialogPortal positioning calculations.
285
+                            enabled = { this.state.enableFocusLock }
262 286
                             returnFocus = {
263 287
 
264 288
                                 // If we return the focus to an element outside the viewport the page will scroll to
265 289
                                 // this element which in our case is undesirable and the element is outside of the
266
-                                // viewport on purpose (to be hidden). For example if we return the focus to the toolbox
267
-                                // when it is hidden the whole page will move up in order to show the toolbox. This is
268
-                                // usually followed up with displaying the toolbox (because now it is on focus) but
269
-                                // because of the animation the whole scenario looks like jumping large video.
290
+                                // viewport on purpose (to be hidden). For example if we return the focus to the
291
+                                // toolbox when it is hidden the whole page will move up in order to show the
292
+                                // toolbox. This is usually followed up with displaying the toolbox (because now it
293
+                                // is on focus) but because of the animation the whole scenario looks like jumping
294
+                                // large video.
270 295
                                 isElementInTheViewport
271
-                            }>
296
+                            }
297
+                            shards = { [ this._contextMenuRef ] }>
272 298
                             {this._renderContent()}
273
-                        </ReactFocusLock>
299
+                        </FocusOn>
274 300
                     </DialogPortal>
275 301
                 )}
276 302
                 { children }
@@ -361,12 +387,12 @@ class Popover extends Component<IProps, IState> {
361 387
      * @returns {void}
362 388
      */
363 389
     _onClick(event: React.MouseEvent) {
364
-        const { allowClick, trigger, visible } = this.props;
390
+        const { allowClick, trigger, focusable, visible } = this.props;
365 391
 
366 392
         if (!allowClick) {
367 393
             event.stopPropagation();
368 394
         }
369
-        if (trigger === 'click') {
395
+        if (trigger === 'click' || focusable) {
370 396
             if (visible) {
371 397
                 this._onHideDialog();
372 398
             } else {
@@ -383,7 +409,9 @@ class Popover extends Component<IProps, IState> {
383 409
      * @returns {void}
384 410
      */
385 411
     _onKeyPress(e: React.KeyboardEvent) {
386
-        if (e.key === ' ' || e.key === 'Enter') {
412
+        // first check that the element we pressed is the actual popover toggle or any of its descendant,
413
+        // otherwise pressing space or enter in any child element of the popover _dialog_ will trigger this.
414
+        if (e.currentTarget.contains(e.target as Node) && (e.key === ' ' || e.key === 'Enter')) {
387 415
             e.preventDefault();
388 416
             if (this.props.visible) {
389 417
                 this._onHideDialog();
@@ -435,18 +463,49 @@ class Popover extends Component<IProps, IState> {
435 463
      * @returns {ReactElement}
436 464
      */
437 465
     _renderContent() {
438
-        const { content, position, trigger } = this.props;
466
+        const { content, position, trigger, headingId, headingLabel } = this.props;
439 467
 
440 468
         return (
441
-            <div
442
-                className = { `popover ${trigger}` }
443
-                onKeyDown = { this._onEscKey }>
444
-                <div className = { `popover-content ${position.split('-')[0]}` }>
469
+            <div className = { `popover ${trigger}` }>
470
+                <div
471
+                    className = { `popover-content ${position.split('-')[0]}` }
472
+                    data-autofocus = { this.state.enableFocusLock }
473
+                    onKeyDown = { this._onEscKey }
474
+                    { ...(this.state.enableFocusLock && {
475
+                        'aria-modal': true,
476
+                        'aria-label': !headingId && headingLabel ? headingLabel : undefined,
477
+                        'aria-labelledby': headingId,
478
+                        role: 'dialog',
479
+                        tabIndex: -1
480
+                    }) }>
445 481
                     { content }
446 482
                 </div>
447 483
             </div>
448 484
         );
449 485
     }
486
+
487
+    /**
488
+     * Returns whether the popover is considered interactive or not.
489
+     *
490
+     * Interactive means the popover content is certainly composed of buttons, links…
491
+     * Non-interactive popovers are mostly tooltips.
492
+     *
493
+     * @private
494
+     * @returns {boolean}
495
+     */
496
+    _isInteractive() {
497
+        return this.props.trigger === 'click' || Boolean(this.props.focusable);
498
+    }
499
+
500
+    /**
501
+     * Enables the focus lock in the popover dialog.
502
+     *
503
+     * @private
504
+     * @returns {void}
505
+     */
506
+    _enableFocusLock() {
507
+        this.setState({ enableFocusLock: true });
508
+    }
450 509
 }
451 510
 
452 511
 /**

+ 1
- 0
react/features/base/react/components/web/BaseIndicator.tsx Прегледај датотеку

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

+ 6
- 0
react/features/base/react/components/web/MultiSelectAutocomplete.tsx Прегледај датотеку

@@ -24,6 +24,11 @@ interface IProps {
24 24
      */
25 25
     footer?: any;
26 26
 
27
+    /**
28
+     * Id for the included input, necessary for screen readers.
29
+     */
30
+    id: string;
31
+
27 32
     /**
28 33
      * Indicates if the component is disabled.
29 34
      */
@@ -174,6 +179,7 @@ class MultiSelectAutocomplete extends Component<IProps, IState> {
174 179
                     error = { this.state.error }
175 180
                     errorDialog = { errorDialog }
176 181
                     filterValue = { this.state.filterValue }
182
+                    id = { this.props.id }
177 183
                     isOpen = { this.state.isOpen }
178 184
                     items = { this.state.items }
179 185
                     noMatchesText = { noMatchesFound }

+ 4
- 49
react/features/base/toolbox/components/web/ToolboxButtonWithPopup.tsx Прегледај датотеку

@@ -5,21 +5,6 @@ import Popover from '../../../popover/components/Popover.web';
5 5
 
6 6
 interface IProps {
7 7
 
8
-    /**
9
-     * The id of the element this button icon controls.
10
-     */
11
-    ariaControls?: string;
12
-
13
-    /**
14
-     * Whether the element popup is expanded.
15
-     */
16
-    ariaExpanded?: boolean;
17
-
18
-    /**
19
-     * Whether the element has a popup.
20
-     */
21
-    ariaHasPopup?: boolean;
22
-
23 8
     /**
24 9
      * Aria label for the Icon.
25 10
      */
@@ -40,11 +25,6 @@ interface IProps {
40 25
      */
41 26
     iconDisabled?: boolean;
42 27
 
43
-    /**
44
-     * The ID of the icon button.
45
-     */
46
-    iconId?: string;
47
-
48 28
     /**
49 29
      * Popover close callback.
50 30
      */
@@ -84,14 +64,10 @@ interface IProps {
84 64
  */
85 65
 export default function ToolboxButtonWithPopup(props: IProps) {
86 66
     const {
87
-        ariaControls,
88
-        ariaExpanded,
89
-        ariaHasPopup,
90 67
         ariaLabel,
91 68
         children,
92 69
         icon,
93 70
         iconDisabled,
94
-        iconId,
95 71
         onPopoverClose,
96 72
         onPopoverOpen,
97 73
         popoverContent,
@@ -119,28 +95,6 @@ export default function ToolboxButtonWithPopup(props: IProps) {
119 95
         );
120 96
     }
121 97
 
122
-    const iconProps: {
123
-        ariaControls?: string;
124
-        ariaExpanded?: boolean;
125
-        className?: string;
126
-        containerId?: string;
127
-        role?: string;
128
-        tabIndex?: number;
129
-    } = {};
130
-
131
-    if (iconDisabled) {
132
-        iconProps.className
133
-            = 'settings-button-small-icon settings-button-small-icon--disabled';
134
-    } else {
135
-        iconProps.className = 'settings-button-small-icon';
136
-        iconProps.role = 'button';
137
-        iconProps.tabIndex = 0;
138
-        iconProps.ariaControls = ariaControls;
139
-        iconProps.ariaExpanded = ariaExpanded;
140
-        iconProps.containerId = iconId;
141
-    }
142
-
143
-
144 98
     return (
145 99
         <div
146 100
             className = 'settings-button-container'
@@ -155,9 +109,10 @@ export default function ToolboxButtonWithPopup(props: IProps) {
155 109
                     position = 'top'
156 110
                     visible = { visible }>
157 111
                     <Icon
158
-                        { ...iconProps }
159
-                        ariaHasPopup = { ariaHasPopup }
160
-                        ariaLabel = { ariaLabel }
112
+                        alt = { ariaLabel }
113
+                        className = { `settings-button-small-icon ${iconDisabled
114
+                            ? 'settings-button-small-icon--disabled'
115
+                            : ''}` }
161 116
                         size = { 16 }
162 117
                         src = { icon } />
163 118
                 </Popover>

+ 1
- 0
react/features/base/tooltip/components/Tooltip.tsx Прегледај датотеку

@@ -145,6 +145,7 @@ const Tooltip = ({ containerClassName, content, children, position = 'top' }: IP
145 145
             allowClick = { true }
146 146
             className = { containerClassName }
147 147
             content = { contentComponent }
148
+            focusable = { false }
148 149
             onPopoverClose = { onPopoverClose }
149 150
             onPopoverOpen = { onPopoverOpen }
150 151
             position = { position }

+ 8
- 6
react/features/base/ui/components/web/BaseDialog.tsx Прегледај датотеку

@@ -1,5 +1,5 @@
1 1
 import React, { ReactNode, useCallback, useContext, useEffect } from 'react';
2
-import FocusLock from 'react-focus-lock';
2
+import { FocusOn } from 'react-focus-on';
3 3
 import { useTranslation } from 'react-i18next';
4 4
 import { keyframes } from 'tss-react';
5 5
 import { makeStyles } from 'tss-react/mui';
@@ -183,7 +183,7 @@ const BaseDialog = ({
183 183
             <div
184 184
                 className = { classes.backdrop }
185 185
                 onClick = { onBackdropClick } />
186
-            <FocusLock
186
+            <FocusOn
187 187
                 className = { classes.focusLock }
188 188
                 returnFocus = {
189 189
 
@@ -196,14 +196,16 @@ const BaseDialog = ({
196 196
                     isElementInTheViewport
197 197
                 }>
198 198
                 <div
199
-                    aria-describedby = { description }
200
-                    aria-labelledby = { title ?? t(titleKey ?? '') }
199
+                    aria-description = { description }
200
+                    aria-label = { title ?? t(titleKey ?? '') }
201 201
                     aria-modal = { true }
202 202
                     className = { cx(classes.modal, isUnmounting && 'unmount', size, className) }
203
-                    role = 'dialog'>
203
+                    data-autofocus = { true }
204
+                    role = 'dialog'
205
+                    tabIndex = { -1 }>
204 206
                     {children}
205 207
                 </div>
206
-            </FocusLock>
208
+            </FocusOn>
207 209
         </div>
208 210
     );
209 211
 };

+ 6
- 5
react/features/base/ui/components/web/Checkbox.tsx Прегледај датотеку

@@ -156,8 +156,8 @@ const Checkbox = ({
156 156
     const isMobile = isMobileBrowser();
157 157
 
158 158
     return (
159
-        <div className = { cx(styles.formControl, isMobile && 'is-mobile', className) }>
160
-            <label className = { cx(styles.activeArea, isMobile && 'is-mobile', disabled && styles.disabled) }>
159
+        <label className = { cx(styles.formControl, isMobile && 'is-mobile', className) }>
160
+            <div className = { cx(styles.activeArea, isMobile && 'is-mobile', disabled && styles.disabled) }>
161 161
                 <input
162 162
                     checked = { checked }
163 163
                     disabled = { disabled }
@@ -165,13 +165,14 @@ const Checkbox = ({
165 165
                     onChange = { onChange }
166 166
                     type = 'checkbox' />
167 167
                 <Icon
168
+                    aria-hidden = { true }
168 169
                     className = 'checkmark'
169 170
                     color = { disabled ? theme.palette.icon03 : theme.palette.icon01 }
170 171
                     size = { 18 }
171 172
                     src = { IconCheck } />
172
-            </label>
173
-            <label>{label}</label>
174
-        </div>
173
+            </div>
174
+            <div>{label}</div>
175
+        </label>
175 176
     );
176 177
 };
177 178
 

+ 29
- 7
react/features/base/ui/components/web/ContextMenuItem.tsx Прегледај датотеку

@@ -1,4 +1,4 @@
1
-import React, { ReactNode } from 'react';
1
+import React, { ReactNode, useCallback } from 'react';
2 2
 import { useSelector } from 'react-redux';
3 3
 import { makeStyles } from 'tss-react/mui';
4 4
 
@@ -76,6 +76,9 @@ export interface IProps {
76 76
 
77 77
     /**
78 78
      * You can use this item as a tab. Defaults to button if not set.
79
+     *
80
+     * If no onClick handler is provided, we assume the context menu item is
81
+     * not interactive and no role will be set.
79 82
      */
80 83
     role?: 'tab' | 'button';
81 84
 
@@ -179,6 +182,28 @@ const ContextMenuItem = ({
179 182
     const { classes: styles, cx } = useStyles();
180 183
     const _overflowDrawer: boolean = useSelector(showOverflowDrawer);
181 184
 
185
+    const onKeyPressHandler = useCallback(e => {
186
+        // only trigger the fallback behavior (onClick) if we dont have any explicit keyboard event handler
187
+        if (onClick && !onKeyPress && !onKeyDown && (e.key === 'Enter' || e.key === ' ')) {
188
+            e.preventDefault();
189
+            onClick(e);
190
+        }
191
+
192
+        if (onKeyPress) {
193
+            onKeyPress(e);
194
+        }
195
+    }, [ onClick, onKeyPress, onKeyDown ]);
196
+
197
+    let tabIndex: undefined | 0 | -1;
198
+
199
+    if (role === 'tab') {
200
+        tabIndex = selected ? 0 : -1;
201
+    }
202
+
203
+    if (role === 'button' && !disabled) {
204
+        tabIndex = 0;
205
+    }
206
+
182 207
     return (
183 208
         <div
184 209
             aria-controls = { controls }
@@ -196,12 +221,9 @@ const ContextMenuItem = ({
196 221
             key = { text }
197 222
             onClick = { disabled ? undefined : onClick }
198 223
             onKeyDown = { disabled ? undefined : onKeyDown }
199
-            onKeyPress = { disabled ? undefined : onKeyPress }
200
-            role = { role }
201
-            tabIndex = { role === 'tab'
202
-                ? selected ? 0 : -1
203
-                : disabled ? undefined : 0
204
-            }>
224
+            onKeyPress = { disabled ? undefined : onKeyPressHandler }
225
+            role = { onClick ? role : undefined }
226
+            tabIndex = { onClick ? tabIndex : undefined }>
205 227
             {customIcon ? customIcon
206 228
                 : icon && <Icon
207 229
                     className = { styles.contextMenuItemIcon }

+ 11
- 4
react/features/base/ui/components/web/Dialog.tsx Прегледај датотеку

@@ -6,6 +6,7 @@ import { makeStyles } from 'tss-react/mui';
6 6
 import { hideDialog } from '../../../dialog/actions';
7 7
 import { IconCloseLarge } from '../../../icons/svg';
8 8
 import { withPixelLineHeight } from '../../../styles/functions.web';
9
+import { operatesWithEnterKey } from '../../functions.web';
9 10
 
10 11
 import BaseDialog, { IProps as IBaseDialogProps } from './BaseDialog';
11 12
 import Button from './Button';
@@ -108,8 +109,13 @@ const Dialog = ({
108 109
     }, [ onCancel ]);
109 110
 
110 111
     const submit = useCallback(() => {
111
-        !disableAutoHideOnSubmit && dispatch(hideDialog());
112
-        onSubmit?.();
112
+        if (onSubmit && (
113
+            (document.activeElement && !operatesWithEnterKey(document.activeElement))
114
+            || !document.activeElement
115
+        )) {
116
+            !disableAutoHideOnSubmit && dispatch(hideDialog());
117
+            onSubmit();
118
+        }
113 119
     }, [ onSubmit ]);
114 120
 
115 121
     return (
@@ -124,11 +130,11 @@ const Dialog = ({
124 130
             title = { title }
125 131
             titleKey = { titleKey }>
126 132
             <div className = { classes.header }>
127
-                <p
133
+                <h1
128 134
                     className = { classes.title }
129 135
                     id = 'dialog-title'>
130 136
                     {title ?? t(titleKey ?? '')}
131
-                </p>
137
+                </h1>
132 138
                 {!hideCloseButton && (
133 139
                     <ClickableIcon
134 140
                         accessibilityLabel = { t('dialog.accessibilityLabel.close') }
@@ -160,6 +166,7 @@ const Dialog = ({
160 166
                     accessibilityLabel = { t(ok.translationKey ?? '') }
161 167
                     disabled = { ok.disabled }
162 168
                     id = 'modal-dialog-ok-button'
169
+                    isSubmit = { true }
163 170
                     labelKey = { ok.translationKey }
164 171
                     onClick = { submit } />}
165 172
             </div>

+ 11
- 13
react/features/base/ui/components/web/DialogWithTabs.tsx Прегледај датотеку

@@ -1,5 +1,4 @@
1 1
 import React, { ComponentType, useCallback, useEffect, useMemo, useState } from 'react';
2
-import { MoveFocusInside } from 'react-focus-lock';
3 2
 import { useTranslation } from 'react-i18next';
4 3
 import { useDispatch, useSelector } from 'react-redux';
5 4
 import { makeStyles } from 'tss-react/mui';
@@ -317,20 +316,19 @@ const DialogWithTabs = ({
317 316
         <BaseDialog
318 317
             className = { cx(classes.dialog, className) }
319 318
             onClose = { onClose }
320
-            size = 'large'>
319
+            size = 'large'
320
+            titleKey = { titleKey }>
321 321
             {(!isMobile || !selectedTab) && (
322 322
                 <div
323 323
                     aria-orientation = 'vertical'
324 324
                     className = { classes.sidebar }
325 325
                     role = { isMobile ? undefined : 'tablist' }>
326 326
                     <div className = { classes.titleContainer }>
327
-                        <MoveFocusInside>
328
-                            <h2
329
-                                className = { classes.title }
330
-                                tabIndex = { -1 }>
331
-                                {t(titleKey ?? '')}
332
-                            </h2>
333
-                        </MoveFocusInside>
327
+                        <h1
328
+                            className = { classes.title }
329
+                            tabIndex = { -1 }>
330
+                            {t(titleKey ?? '')}
331
+                        </h1>
334 332
                         {isMobile && closeIcon}
335 333
                     </div>
336 334
                     {tabs.map((tab, index) => {
@@ -366,11 +364,11 @@ const DialogWithTabs = ({
366 364
                     {isMobile && (
367 365
                         <div className = { cx(classes.buttonContainer, classes.header) }>
368 366
                             <span className = { classes.backContainer }>
369
-                                <h2
367
+                                <h1
370 368
                                     className = { classes.title }
371 369
                                     tabIndex = { -1 }>
372 370
                                     {(selectedTabIndex !== null) && t(tabs[selectedTabIndex].labelKey)}
373
-                                </h2>
371
+                                </h1>
374 372
                                 <ClickableIcon
375 373
                                     accessibilityLabel = { t('dialog.Back') }
376 374
                                     icon = { IconArrowBack }
@@ -401,13 +399,13 @@ const DialogWithTabs = ({
401 399
                     <div
402 400
                         className = { cx(classes.buttonContainer, classes.footer) }>
403 401
                         <Button
404
-                            accessibilityLabel = { t('dialog.Cancel') }
402
+                            accessibilityLabel = { t('dialog.accessibilityLabel.Cancel') }
405 403
                             id = 'modal-dialog-cancel-button'
406 404
                             labelKey = { 'dialog.Cancel' }
407 405
                             onClick = { onClose }
408 406
                             type = 'tertiary' />
409 407
                         <Button
410
-                            accessibilityLabel = { t('dialog.Ok') }
408
+                            accessibilityLabel = { t('dialog.accessibilityLabel.Ok') }
411 409
                             id = 'modal-dialog-ok-button'
412 410
                             labelKey = { 'dialog.Ok' }
413 411
                             onClick = { onSubmit } />

+ 18
- 5
react/features/base/ui/components/web/Input.tsx Прегледај датотеку

@@ -15,7 +15,13 @@ interface IProps extends IInputProps {
15 15
     bottomLabel?: string;
16 16
     className?: string;
17 17
     iconClick?: () => void;
18
-    id?: string;
18
+
19
+    /**
20
+     * The id to set on the input element.
21
+     * This is required because we need it internally to tie the input to its
22
+     * info (label, error) so that screen reader users don't get lost.
23
+     */
24
+    id: string;
19 25
     maxLength?: number;
20 26
     maxRows?: number;
21 27
     maxValue?: number;
@@ -187,7 +193,11 @@ const Input = React.forwardRef<any, IProps>(({
187 193
 
188 194
     return (
189 195
         <div className = { cx(styles.inputContainer, className) }>
190
-            {label && <span className = { cx(styles.label, isMobile && 'is-mobile') }>{label}</span>}
196
+            {label && <label
197
+                className = { cx(styles.label, isMobile && 'is-mobile') }
198
+                htmlFor = { id } >
199
+                {label}
200
+            </label>}
191 201
             <div className = { styles.fieldContainer }>
192 202
                 {icon && <Icon
193 203
                     { ...(iconClick ? { tabIndex: 0 } : {}) }
@@ -203,7 +213,7 @@ const Input = React.forwardRef<any, IProps>(({
203 213
                         className = { cx(styles.input, isMobile && 'is-mobile',
204 214
                             error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
205 215
                         disabled = { disabled }
206
-                        { ...(id ? { id } : {}) }
216
+                        id = { id }
207 217
                         maxLength = { maxLength }
208 218
                         maxRows = { maxRows }
209 219
                         minRows = { minRows }
@@ -217,6 +227,7 @@ const Input = React.forwardRef<any, IProps>(({
217 227
                         value = { value } />
218 228
                 ) : (
219 229
                     <input
230
+                        aria-describedby = { bottomLabel ? `${id}-description` : undefined }
220 231
                         aria-label = { accessibilityLabel }
221 232
                         autoComplete = { autoComplete }
222 233
                         autoFocus = { autoFocus }
@@ -224,7 +235,7 @@ const Input = React.forwardRef<any, IProps>(({
224 235
                             error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
225 236
                         data-testid = { testId }
226 237
                         disabled = { disabled }
227
-                        { ...(id ? { id } : {}) }
238
+                        id = { id }
228 239
                         { ...(mode ? { inputmode: mode } : {}) }
229 240
                         { ...(type === 'number' ? { max: maxValue } : {}) }
230 241
                         maxLength = { maxLength }
@@ -249,7 +260,9 @@ const Input = React.forwardRef<any, IProps>(({
249 260
                 </button>}
250 261
             </div>
251 262
             {bottomLabel && (
252
-                <span className = { cx(styles.bottomLabel, isMobile && 'is-mobile', error && 'error') }>
263
+                <span
264
+                    className = { cx(styles.bottomLabel, isMobile && 'is-mobile', error && 'error') }
265
+                    id = { `${id}-description` }>
253 266
                     {bottomLabel}
254 267
                 </span>
255 268
             )}

+ 3
- 0
react/features/base/ui/components/web/MultiSelect.tsx Прегледај датотеку

@@ -14,6 +14,7 @@ interface IProps {
14 14
     error?: boolean;
15 15
     errorDialog?: JSX.Element | null;
16 16
     filterValue?: string;
17
+    id: string;
17 18
     isOpen?: boolean;
18 19
     items: MultiSelectItem[];
19 20
     noMatchesText?: string;
@@ -101,6 +102,7 @@ const MultiSelect = ({
101 102
     error,
102 103
     errorDialog,
103 104
     placeholder,
105
+    id,
104 106
     items,
105 107
     filterValue,
106 108
     onFilterChange,
@@ -145,6 +147,7 @@ const MultiSelect = ({
145 147
             <Input
146 148
                 autoFocus = { autoFocus }
147 149
                 disabled = { disabled }
150
+                id = { id }
148 151
                 onChange = { onFilterChange }
149 152
                 placeholder = { placeholder }
150 153
                 ref = { inputRef }

+ 17
- 2
react/features/base/ui/components/web/Select.tsx Прегледај датотеку

@@ -28,6 +28,12 @@ interface ISelectProps {
28 28
      */
29 29
     error?: boolean;
30 30
 
31
+    /**
32
+     * Id of the <select> element.
33
+     * Necessary for screen reader users, to link the label and error to the select.
34
+     */
35
+    id: string;
36
+
31 37
     /**
32 38
      * Label to be displayed above the select.
33 39
      */
@@ -140,6 +146,7 @@ const Select = ({
140 146
     className,
141 147
     disabled,
142 148
     error,
149
+    id,
143 150
     label,
144 151
     onChange,
145 152
     options,
@@ -149,11 +156,17 @@ const Select = ({
149 156
 
150 157
     return (
151 158
         <div className = { classes.container }>
152
-            {label && <span className = { cx(classes.label, isMobile && 'is-mobile') }>{label}</span>}
159
+            {label && <label
160
+                className = { cx(classes.label, isMobile && 'is-mobile') }
161
+                htmlFor = { id } >
162
+                {label}
163
+            </label>}
153 164
             <div className = { classes.selectContainer }>
154 165
                 <select
166
+                    aria-describedby = { bottomLabel ? `${id}-description` : undefined }
155 167
                     className = { cx(classes.select, isMobile && 'is-mobile', className, error && 'error') }
156 168
                     disabled = { disabled }
169
+                    id = { id }
157 170
                     onChange = { onChange }
158 171
                     value = { value }>
159 172
                     {options.map(option => (<option
@@ -167,7 +180,9 @@ const Select = ({
167 180
                     src = { IconArrowDown } />
168 181
             </div>
169 182
             {bottomLabel && (
170
-                <span className = { cx(classes.bottomLabel, isMobile && 'is-mobile', error && 'error') }>
183
+                <span
184
+                    className = { cx(classes.bottomLabel, isMobile && 'is-mobile', error && 'error') }
185
+                    id = { `${id}-description` }>
171 186
                     {bottomLabel}
172 187
                 </span>
173 188
             )}

+ 36
- 4
react/features/base/ui/components/web/Switch.tsx Прегледај датотеку

@@ -52,6 +52,7 @@ const useStyles = makeStyles()(theme => {
52 52
             width: '16px',
53 53
             height: '16px',
54 54
             position: 'absolute',
55
+            zIndex: 5,
55 56
             top: '4px',
56 57
             left: '4px',
57 58
             backgroundColor: theme.palette.ui10,
@@ -73,8 +74,38 @@ const useStyles = makeStyles()(theme => {
73 74
         },
74 75
 
75 76
         checkbox: {
76
-            height: 0,
77
-            width: 0
77
+            position: 'absolute',
78
+            zIndex: 10,
79
+            cursor: 'pointer',
80
+            left: 0,
81
+            right: 0,
82
+            top: 0,
83
+            bottom: 0,
84
+            width: '100%',
85
+            height: '100%',
86
+            opacity: 0,
87
+
88
+            '&.focus-visible + .toggle-checkbox-ring': {
89
+                outline: 0,
90
+                boxShadow: `0px 0px 0px 2px ${theme.palette.focus01}`
91
+            }
92
+        },
93
+
94
+        checkboxRing: {
95
+            position: 'absolute',
96
+            pointerEvents: 'none',
97
+            zIndex: 6,
98
+            left: 0,
99
+            right: 0,
100
+            top: 0,
101
+            bottom: 0,
102
+            width: '100%',
103
+            height: '100%',
104
+            borderRadius: '12px',
105
+
106
+            '&.is-mobile': {
107
+                borderRadius: '32px'
108
+            }
78 109
         }
79 110
     };
80 111
 });
@@ -88,7 +119,7 @@ const Switch = ({ className, id, checked, disabled, onChange }: IProps) => {
88 119
     }, []);
89 120
 
90 121
     return (
91
-        <label
122
+        <span
92 123
             className = { cx('toggle-container', styles.container, checked && styles.containerOn,
93 124
                 isMobile && 'is-mobile', disabled && 'disabled', className) }>
94 125
             <input
@@ -98,8 +129,9 @@ const Switch = ({ className, id, checked, disabled, onChange }: IProps) => {
98 129
                 className = { styles.checkbox }
99 130
                 disabled = { disabled }
100 131
                 onChange = { change } />
132
+            <div className = { cx('toggle-checkbox-ring', styles.checkboxRing, isMobile && 'is-mobile') } />
101 133
             <div className = { cx('toggle', styles.toggle, checked && styles.toggleOn, isMobile && 'is-mobile') } />
102
-        </label>
134
+        </span>
103 135
     );
104 136
 };
105 137
 

+ 25
- 0
react/features/base/ui/functions.web.ts Прегледај датотеку

@@ -82,3 +82,28 @@ export function isElementInTheViewport(element?: Element): boolean {
82 82
 
83 83
     return false;
84 84
 }
85
+
86
+const enterKeyElements = [ 'select', 'textarea', 'summary', 'a' ];
87
+
88
+/**
89
+ * Informs whether or not the given element does something on its own when pressing the Enter key.
90
+ *
91
+ * This is useful to correctly submit custom made "forms" that are not using the native form element,
92
+ * only when the user is not using an element that needs the enter key to work.
93
+ * Note the implementation is incomplete and should be updated as needed if more complex use cases arise
94
+ * (for example, the Tabs aria pattern is not handled).
95
+ *
96
+ * @param {Element} element - The element.
97
+ * @returns {boolean}
98
+ */
99
+export function operatesWithEnterKey(element: Element): boolean {
100
+    if (enterKeyElements.includes(element.tagName.toLowerCase())) {
101
+        return true;
102
+    }
103
+
104
+    if (element.tagName.toLowerCase() === 'button' && element.getAttribute('role') === 'button') {
105
+        return true;
106
+    }
107
+
108
+    return false;
109
+}

+ 1
- 0
react/features/chat/components/web/ChatInput.tsx Прегледај датотеку

@@ -135,6 +135,7 @@ class ChatInput extends Component<IProps, IState> {
135 135
                         className = 'chat-input'
136 136
                         icon = { this.props._areSmileysDisabled ? undefined : IconFaceSmile }
137 137
                         iconClick = { this._toggleSmileysPanel }
138
+                        id = 'chat-input-messagebox'
138 139
                         maxRows = { 5 }
139 140
                         onChange = { this._onMessageChange }
140 141
                         onKeyPress = { this._onDetectSubmit }

+ 1
- 0
react/features/conference/components/web/RaisedHandsCountLabel.tsx Прегледај датотеку

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

+ 3
- 1
react/features/connection-indicator/components/web/ConnectionIndicator.tsx Прегледај датотеку

@@ -347,12 +347,14 @@ class ConnectionIndicator extends AbstractConnectionIndicator<IProps, IState> {
347 347
             _connectionIndicatorInactiveDisabled,
348 348
             _videoTrack,
349 349
             classes,
350
-            iconSize
350
+            iconSize,
351
+            t
351 352
         } = this.props;
352 353
 
353 354
         return (
354 355
             <div
355 356
                 style = {{ fontSize: iconSize }}>
357
+                <span className = 'sr-only'>{ t('videothumbnail.connectionInfo') }</span>
356 358
                 <ConnectionIndicatorIcon
357 359
                     classes = { classes }
358 360
                     colorClass = { this._getConnectionColorClass() }

+ 11
- 8
react/features/connection-stats/components/ConnectionStatsTable.tsx Прегледај датотеку

@@ -259,6 +259,11 @@ const useStyles = makeStyles()(theme => {
259 259
             cursor: 'pointer',
260 260
             color: theme.palette.link01,
261 261
             transition: 'color .2s ease',
262
+            border: 0,
263
+            background: 0,
264
+            padding: 0,
265
+            display: 'inline',
266
+            fontWeight: 'bold',
262 267
 
263 268
             '&:hover': {
264 269
                 color: theme.palette.link01Hover,
@@ -714,13 +719,12 @@ const ConnectionStatsTable = ({
714 719
 
715 720
     const _renderSaveLogs = () => (
716 721
         <span>
717
-            <a
722
+            <button
718 723
                 className = { cx(classes.link, 'savelogs') }
719 724
                 onClick = { onSaveLogs }
720
-                role = 'button'
721
-                tabIndex = { 0 }>
725
+                type = 'button'>
722 726
                 {t('connectionindicator.savelogs')}
723
-            </a>
727
+            </button>
724 728
             <span> | </span>
725 729
         </span>
726 730
     );
@@ -732,13 +736,12 @@ const ConnectionStatsTable = ({
732 736
                 : 'connectionindicator.more';
733 737
 
734 738
         return (
735
-            <a
739
+            <button
736 740
                 className = { cx(classes.link, 'showmore') }
737 741
                 onClick = { onShowMore }
738
-                role = 'button'
739
-                tabIndex = { 0 }>
742
+                type = 'button'>
740 743
                 {t(translationKey)}
741
-            </a>
744
+            </button>
742 745
         );
743 746
     };
744 747
 

+ 2
- 0
react/features/device-selection/components/DeviceSelector.web.tsx Прегледај датотеку

@@ -69,6 +69,7 @@ const useStyles = makeStyles()(theme => {
69 69
 const DeviceSelector = ({
70 70
     devices,
71 71
     hasPermission,
72
+    id,
72 73
     isDisabled,
73 74
     label,
74 75
     onSelect,
@@ -103,6 +104,7 @@ const DeviceSelector = ({
103 104
 
104 105
         return (
105 106
             <Select
107
+                id = { id }
106 108
                 label = { t(label) }
107 109
                 onChange = { _onSelect }
108 110
                 options = { options.items }

+ 1
- 0
react/features/device-selection/components/VideoDeviceSelection.web.tsx Прегледај датотеку

@@ -351,6 +351,7 @@ class VideoDeviceSelection extends AbstractDialogTab<IProps, IState> {
351 351
                 bottomLabel = { parseInt(currentFramerate, 10) > SS_DEFAULT_FRAME_RATE
352 352
                     ? t('settings.desktopShareHighFpsWarning')
353 353
                     : t('settings.desktopShareWarning') }
354
+                id = 'more-framerate-select'
354 355
                 label = { t('settings.desktopShareFramerate') }
355 356
                 onChange = { this._onFramerateItemSelect }
356 357
                 options = { frameRateItems }

+ 1
- 0
react/features/display-name/components/web/DisplayNamePrompt.tsx Прегледај датотеку

@@ -58,6 +58,7 @@ class DisplayNamePrompt extends AbstractDisplayNamePrompt<IState> {
58 58
                 <Input
59 59
                     autoFocus = { true }
60 60
                     className = 'dialog-bottom-margin'
61
+                    id = 'dialog-displayName'
61 62
                     label = { this.props.t('dialog.enterDisplayName') }
62 63
                     name = 'displayName'
63 64
                     onChange = { this._onDisplayNameChange }

+ 3
- 1
react/features/embed-meeting/components/EmbedMeetingDialog.tsx Прегледај датотеку

@@ -55,13 +55,15 @@ function EmbedMeeting({ t, url }: IProps) {
55 55
             <div className = { classes.container }>
56 56
                 <Input
57 57
                     accessibilityLabel = { t('dialog.embedMeeting') }
58
+                    id = 'embed-meeting-input'
58 59
                     readOnly = { true }
59 60
                     textarea = { true }
60 61
                     value = { getEmbedCode() } />
61 62
                 <CopyButton
62
-                    aria-label = { t('addPeople.copyLink') }
63
+                    accessibilityText = { t('addPeople.copyLink') }
63 64
                     className = { classes.button }
64 65
                     displayedText = { t('dialog.copy') }
66
+                    id = 'embed-meeting-copy-button'
65 67
                     textOnCopySuccess = { t('dialog.copied') }
66 68
                     textOnHover = { t('dialog.copy') }
67 69
                     textToCopy = { getEmbedCode() } />

+ 14
- 9
react/features/feedback/components/FeedbackDialog.web.tsx Прегледај датотеку

@@ -25,7 +25,7 @@ const styles = (theme: Theme) => {
25 25
 
26 26
         rating: {
27 27
             display: 'flex',
28
-            flexDirection: 'column' as const,
28
+            flexDirection: 'column-reverse' as const,
29 29
             alignItems: 'center',
30 30
             justifyContent: 'center',
31 31
             marginTop: theme.spacing(4),
@@ -316,22 +316,27 @@ class FeedbackDialog extends Component<IProps, IState> {
316 316
                 titleKey = 'feedback.rateExperience'>
317 317
                 <div className = { classes.dialog }>
318 318
                     <div className = { classes.rating }>
319
-                        <div
320
-                            aria-label = { this.props.t('feedback.star') }
321
-                            className = { classes.ratingLabel } >
322
-                            <p id = 'starLabel'>
323
-                                { t(SCORES[scoreToDisplayAsSelected]) }
324
-                            </p>
325
-                        </div>
326 319
                         <div
327 320
                             className = { classes.stars }
328 321
                             onMouseLeave = { this._onScoreContainerMouseLeave }>
329 322
                             { scoreIcons }
330 323
                         </div>
324
+                        <div
325
+                            className = { classes.ratingLabel } >
326
+                            <p className = 'sr-only'>
327
+                                { t('feedback.accessibilityLabel.yourChoice', {
328
+                                    rating: t(SCORES[scoreToDisplayAsSelected])
329
+                                }) }
330
+                            </p>
331
+                            <p
332
+                                aria-hidden = { true }
333
+                                id = 'starLabel'>
334
+                                { t(SCORES[scoreToDisplayAsSelected]) }
335
+                            </p>
336
+                        </div>
331 337
                     </div>
332 338
                     <div className = { classes.details }>
333 339
                         <Input
334
-                            autoFocus = { true }
335 340
                             id = 'feedbackTextArea'
336 341
                             label = { t('feedback.detailsLabel') }
337 342
                             onChange = { this._onMessageChange }

+ 15
- 12
react/features/filmstrip/components/web/Thumbnail.tsx Прегледај датотеку

@@ -903,6 +903,7 @@ class Thumbnail extends Component<IProps, IState> {
903 903
                 tabIndex = { 0 }>
904 904
                 {avatarURL ? (
905 905
                     <img
906
+                        alt = ''
906 907
                         className = 'sharedVideoAvatar'
907 908
                         src = { avatarURL } />
908 909
                 )
@@ -1105,6 +1106,20 @@ class Thumbnail extends Component<IProps, IState> {
1105 1106
                     ? <span id = 'localVideoWrapper'>{video}</span>
1106 1107
                     : video)}
1107 1108
                 <div className = { classes.containerBackground } />
1109
+                {/* put the bottom container before the top container in the dom,
1110
+                because it contains the participant name that should be announced first by screen readers */}
1111
+                <div
1112
+                    className = { clsx(classes.indicatorsContainer,
1113
+                        classes.indicatorsBottomContainer,
1114
+                        _thumbnailType === THUMBNAIL_TYPE.TILE && 'tile-view-mode'
1115
+                    ) }>
1116
+                    <ThumbnailBottomIndicators
1117
+                        className = { classes.indicatorsBackground }
1118
+                        local = { local }
1119
+                        participantId = { id }
1120
+                        showStatusIndicators = { !isWhiteboardParticipant(_participant) }
1121
+                        thumbnailType = { _thumbnailType } />
1122
+                </div>
1108 1123
                 <div
1109 1124
                     className = { clsx(classes.indicatorsContainer,
1110 1125
                         classes.indicatorsTopContainer,
@@ -1122,18 +1137,6 @@ class Thumbnail extends Component<IProps, IState> {
1122 1137
                         thumbnailType = { _thumbnailType } />
1123 1138
                 </div>
1124 1139
                 {_shouldDisplayTintBackground && <div className = { classes.tintBackground } />}
1125
-                <div
1126
-                    className = { clsx(classes.indicatorsContainer,
1127
-                        classes.indicatorsBottomContainer,
1128
-                        _thumbnailType === THUMBNAIL_TYPE.TILE && 'tile-view-mode'
1129
-                    ) }>
1130
-                    <ThumbnailBottomIndicators
1131
-                        className = { classes.indicatorsBackground }
1132
-                        local = { local }
1133
-                        participantId = { id }
1134
-                        showStatusIndicators = { !isWhiteboardParticipant(_participant) }
1135
-                        thumbnailType = { _thumbnailType } />
1136
-                </div>
1137 1140
                 {!_gifSrc && this._renderAvatar(styles.avatar) }
1138 1141
                 { !local && (
1139 1142
                     <div className = 'presence-label-container'>

+ 1
- 0
react/features/gifs/components/web/GifsMenu.tsx Прегледај датотеку

@@ -208,6 +208,7 @@ function GifsMenu({ columns = 2, parent }: IProps) {
208 208
             <Input
209 209
                 autoFocus = { true }
210 210
                 className = { cx(styles.searchField, 'gif-input') }
211
+                id = 'gif-search-input'
211 212
                 onChange = { handleSearchKeyChange }
212 213
                 onKeyPress = { onInputKeyPress }
213 214
                 placeholder = { t('giphy.search') }

+ 3
- 6
react/features/invite/components/add-people-dialog/web/CopyMeetingLinkSection.tsx Прегледај датотеку

@@ -34,15 +34,12 @@ function CopyMeetingLinkSection({ url }: IProps) {
34 34
 
35 35
     return (
36 36
         <>
37
-            <label
38
-                className = { classes.label }
39
-                htmlFor = { 'copy-button-id' }
40
-                id = 'copy-button-label'>{t('addPeople.shareLink')}</label>
37
+            <p className = { classes.label }>{t('addPeople.shareLink')}</p>
41 38
             <CopyButton
42
-                aria-label = { t('addPeople.copyLink') }
39
+                accessibilityText = { t('addPeople.accessibilityLabel.meetingLink', { url: getDecodedURI(url) }) }
43 40
                 className = 'invite-more-dialog-conference-url'
44 41
                 displayedText = { getDecodedURI(url) }
45
-                id = 'copy-button-id'
42
+                id = 'add-people-copy-link-button'
46 43
                 textOnCopySuccess = { t('addPeople.linkCopied') }
47 44
                 textOnHover = { t('addPeople.copyLink') }
48 45
                 textToCopy = { url } />

+ 6
- 24
react/features/invite/components/add-people-dialog/web/DialInNumber.tsx Прегледај датотеку

@@ -44,7 +44,6 @@ class DialInNumber extends Component<IProps> {
44 44
 
45 45
         // Bind event handler so it is only bound once for every instance.
46 46
         this._onCopyText = this._onCopyText.bind(this);
47
-        this._onCopyTextKeyPress = this._onCopyTextKeyPress.bind(this);
48 47
     }
49 48
 
50 49
     /**
@@ -62,20 +61,6 @@ class DialInNumber extends Component<IProps> {
62 61
         copyText(textToCopy);
63 62
     }
64 63
 
65
-    /**
66
-     * KeyPress handler for accessibility.
67
-     *
68
-     * @param {Object} e - The key event to handle.
69
-     *
70
-     * @returns {void}
71
-     */
72
-    _onCopyTextKeyPress(e: React.KeyboardEvent) {
73
-        if (e.key === ' ' || e.key === 'Enter') {
74
-            e.preventDefault();
75
-            this._onCopyText();
76
-        }
77
-    }
78
-
79 64
     /**
80 65
      * Implements React's {@link Component#render()}.
81 66
      *
@@ -87,7 +72,7 @@ class DialInNumber extends Component<IProps> {
87 72
 
88 73
         return (
89 74
             <div className = 'dial-in-number'>
90
-                <div>
75
+                <p>
91 76
                     <span className = 'phone-number'>
92 77
                         <span className = 'info-label'>
93 78
                             { t('info.dialInNumber') }
@@ -107,16 +92,13 @@ class DialInNumber extends Component<IProps> {
107 92
                             { `${_formatConferenceIDPin(conferenceID)}#` }
108 93
                         </span>
109 94
                     </span>
110
-                </div>
111
-                <a
95
+                </p>
96
+                <button
112 97
                     aria-label = { t('info.copyNumber') }
113
-                    className = 'dial-in-copy'
114
-                    onClick = { this._onCopyText }
115
-                    onKeyPress = { this._onCopyTextKeyPress }
116
-                    role = 'button'
117
-                    tabIndex = { 0 }>
98
+                    className = 'dial-in-copy invisible-button'
99
+                    onClick = { this._onCopyText }>
118 100
                     <Icon src = { IconCopy } />
119
-                </a>
101
+                </button>
120 102
             </div>
121 103
         );
122 104
     }

+ 1
- 0
react/features/invite/components/add-people-dialog/web/InviteContactsForm.tsx Прегледај датотеку

@@ -185,6 +185,7 @@ class InviteContactsForm extends AbstractAddPeopleDialog<IProps, IState> {
185 185
                 className = { this.props.classes.formWrap }
186 186
                 onKeyDown = { this._onKeyDown }>
187 187
                 <MultiSelectAutocomplete
188
+                    id = 'invite-contacts-input'
188 189
                     isDisabled = { isMultiSelectDisabled }
189 190
                     loadingMessage = { t(loadingMessage) }
190 191
                     noMatchesFound = { t(noMatches) }

+ 2
- 0
react/features/lobby/components/web/LobbyScreen.tsx Прегледај датотеку

@@ -158,6 +158,7 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
158 158
         return (
159 159
             <Input
160 160
                 className = 'lobby-prejoin-input'
161
+                id = 'lobby-name-field'
161 162
                 onChange = { this._onChangeDisplayName }
162 163
                 placeholder = { t('lobby.nameField') }
163 164
                 testId = 'lobby.nameField'
@@ -177,6 +178,7 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
177 178
             <>
178 179
                 <Input
179 180
                     className = { `lobby-prejoin-input ${_passwordJoinFailed ? 'error' : ''}` }
181
+                    id = 'lobby-password-input'
180 182
                     onChange = { this._onChangePassword }
181 183
                     placeholder = { t('lobby.passwordField') }
182 184
                     testId = 'lobby.password'

+ 1
- 0
react/features/participants-pane/components/web/MeetingParticipants.tsx Прегледај датотеку

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

+ 2
- 0
react/features/polls/components/web/PollCreate.tsx Прегледај датотеку

@@ -191,6 +191,7 @@ const PollCreate = ({
191 191
             <div className = { classes.questionContainer }>
192 192
                 <Input
193 193
                     autoFocus = { true }
194
+                    id = 'polls-create-input'
194 195
                     label = { t('polls.create.pollQuestion') }
195 196
                     maxLength = { CHAR_LIMIT }
196 197
                     onChange = { setQuestion }
@@ -205,6 +206,7 @@ const PollCreate = ({
205 206
                         className = { classes.answer }
206 207
                         key = { i }>
207 208
                         <Input
209
+                            id = { `polls-answer-input-${i}` }
208 210
                             label = { t('polls.create.pollOption', { index: i + 1 }) }
209 211
                             maxLength = { CHAR_LIMIT }
210 212
                             onChange = { val => setAnswer(i, val) }

+ 2
- 0
react/features/prejoin/components/web/Prejoin.tsx Прегледај датотеку

@@ -374,10 +374,12 @@ const Prejoin = ({
374 374
                 className = { classes.inputContainer }
375 375
                 data-testid = 'prejoin.screen'>
376 376
                 {showDisplayNameField.current ? (<Input
377
+                    accessibilityLabel = { t('dialog.enterDisplayName') }
377 378
                     autoComplete = { 'name' }
378 379
                     autoFocus = { true }
379 380
                     className = { classes.input }
380 381
                     error = { showErrorOnJoin }
382
+                    id = 'premeeting-name-input'
381 383
                     onChange = { setName }
382 384
                     onKeyPress = { showUnsafeRoomWarning && !unsafeRoomConsent ? undefined : onInputKeyPress }
383 385
                     placeholder = { t('dialog.enterDisplayName') }

+ 0
- 7
react/features/reactions/components/web/ReactionsMenuButton.tsx Прегледај датотеку

@@ -119,9 +119,6 @@ function ReactionsMenuButton({
119 119
     if (_reactionsButtonEnabled) {
120 120
         content = (
121 121
             <ToolboxButtonWithPopup
122
-                ariaControls = 'reactions-menu-dialog'
123
-                ariaExpanded = { isOpen }
124
-                ariaHasPopup = { true }
125 122
                 ariaLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
126 123
                 onPopoverClose = { closeReactionsMenu }
127 124
                 onPopoverOpen = { openReactionsMenu }
@@ -141,13 +138,9 @@ function ReactionsMenuButton({
141 138
                     notifyMode = { notifyMode } />)
142 139
             : (
143 140
                 <ToolboxButtonWithPopup
144
-                    ariaControls = 'reactions-menu-dialog'
145
-                    ariaExpanded = { isOpen }
146
-                    ariaHasPopup = { true }
147 141
                     ariaLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
148 142
                     icon = { IconArrowUp }
149 143
                     iconDisabled = { false }
150
-                    iconId = 'reactions-menu-button'
151 144
                     onPopoverClose = { toggleReactionsMenu }
152 145
                     onPopoverOpen = { openReactionsMenu }
153 146
                     popoverContent = { reactionsMenu }

+ 2
- 1
react/features/recording/components/LiveStream/AbstractLiveStreamButton.ts Прегледај датотеку

@@ -38,7 +38,8 @@ export interface IProps extends AbstractButtonProps {
38 38
  * An abstract class of a button for starting and stopping live streaming.
39 39
  */
40 40
 export default class AbstractLiveStreamButton<P extends IProps> extends AbstractButton<P> {
41
-    accessibilityLabel = 'dialog.accessibilityLabel.liveStreaming';
41
+    accessibilityLabel = 'dialog.startLiveStreaming';
42
+    toggledAccessibilityLabel = 'dialog.stopLiveStreaming';
42 43
     icon = IconSites;
43 44
     label = 'dialog.startLiveStreaming';
44 45
     toggledLabel = 'dialog.stopLiveStreaming';

+ 4
- 46
react/features/recording/components/LiveStream/web/StreamKeyForm.tsx Прегледај датотеку

@@ -39,20 +39,6 @@ const styles = (theme: Theme) => {
39 39
  */
40 40
 class StreamKeyForm extends AbstractStreamKeyForm<IProps> {
41 41
 
42
-    /**
43
-     * Initializes a new {@code StreamKeyForm} instance.
44
-     *
45
-     * @param {IProps} props - The React {@code Component} props to initialize
46
-     * the new {@code StreamKeyForm} instance with.
47
-     */
48
-    constructor(props: IProps) {
49
-        super(props);
50
-
51
-        // Bind event handlers so they are only bound once per instance.
52
-        this._onOpenHelp = this._onOpenHelp.bind(this);
53
-        this._onOpenHelpKeyPress = this._onOpenHelpKeyPress.bind(this);
54
-    }
55
-
56 42
     /**
57 43
      * Implements React's {@link Component#render()}.
58 44
      *
@@ -66,6 +52,7 @@ class StreamKeyForm extends AbstractStreamKeyForm<IProps> {
66 52
             <div className = 'stream-key-form'>
67 53
                 <Input
68 54
                     autoFocus = { true }
55
+                    id = 'streamkey-input'
69 56
                     label = { t('dialog.streamKey') }
70 57
                     name = 'streamId'
71 58
                     onChange = { this._onInputChange }
@@ -83,12 +70,10 @@ class StreamKeyForm extends AbstractStreamKeyForm<IProps> {
83 70
                         }
84 71
                         { this.props._liveStreaming.helpURL
85 72
                             ? <a
86
-                                aria-label = { t('liveStreaming.streamIdHelp') }
87 73
                                 className = { classes.helperLink }
88
-                                onClick = { this._onOpenHelp }
89
-                                onKeyPress = { this._onOpenHelpKeyPress }
90
-                                role = 'link'
91
-                                tabIndex = { 0 }>
74
+                                href = { this.props._liveStreaming.helpURL }
75
+                                rel = 'noopener noreferrer'
76
+                                target = '_blank'>
92 77
                                 { t('liveStreaming.streamIdHelp') }
93 78
                             </a>
94 79
                             : null
@@ -112,33 +97,6 @@ class StreamKeyForm extends AbstractStreamKeyForm<IProps> {
112 97
             </div>
113 98
         );
114 99
     }
115
-
116
-    /**
117
-     * Opens a new tab with information on how to manually locate a YouTube
118
-     * broadcast stream key.
119
-     *
120
-     * @private
121
-     * @returns {void}
122
-     */
123
-    _onOpenHelp() {
124
-        window.open(this.props._liveStreaming.helpURL, '_blank', 'noopener');
125
-    }
126
-
127
-    /**
128
-     * Opens a new tab with information on how to manually locate a YouTube
129
-     * broadcast stream key.
130
-     *
131
-     * @param {Object} e - The key event to handle.
132
-     *
133
-     * @private
134
-     * @returns {void}
135
-     */
136
-    _onOpenHelpKeyPress(e: React.KeyboardEvent) {
137
-        if (e.key === ' ') {
138
-            e.preventDefault();
139
-            this._onOpenHelp();
140
-        }
141
-    }
142 100
 }
143 101
 
144 102
 export default translate(connect(_mapStateToProps)(withStyles(styles)(StreamKeyForm)));

+ 1
- 0
react/features/recording/components/LiveStream/web/StreamKeyPicker.tsx Прегледај датотеку

@@ -100,6 +100,7 @@ class StreamKeyPicker extends PureComponent<IProps> {
100 100
         return (
101 101
             <div className = 'broadcast-dropdown dropdown-menu'>
102 102
                 <Select
103
+                    id = 'streamkeypicker-select'
103 104
                     label = { t('liveStreaming.choose') }
104 105
                     onChange = { this._onSelect }
105 106
                     options = { dropdownItems }

+ 2
- 1
react/features/recording/components/Recording/AbstractRecordButton.ts Прегледај датотеку

@@ -36,7 +36,8 @@ export interface IProps extends AbstractButtonProps {
36 36
  * An abstract implementation of a button for starting and stopping recording.
37 37
  */
38 38
 export default class AbstractRecordButton<P extends IProps> extends AbstractButton<P> {
39
-    accessibilityLabel = 'toolbar.accessibilityLabel.recording';
39
+    accessibilityLabel = 'dialog.startRecording';
40
+    toggledAccessibilityLabel = 'dialog.stopRecording';
40 41
     icon = IconRecord;
41 42
     label = 'dialog.startRecording';
42 43
     toggledLabel = 'dialog.stopRecording';

+ 40
- 11
react/features/recording/components/Recording/web/StartRecordingDialogContent.tsx Прегледај датотеку

@@ -79,6 +79,7 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
79 79
                         checked = { selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE }
80 80
                         className = 'recording-switch'
81 81
                         disabled = { isValidating }
82
+                        id = 'recording-switch-jitsi'
82 83
                         onChange = { this._onRecordingServiceSwitchChange } />
83 84
                 ) : null;
84 85
 
@@ -98,12 +99,15 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
98 99
                 key = 'noIntegrationSetting'>
99 100
                 <Container className = { contentRecordingClass }>
100 101
                     <Image
102
+                        alt = ''
101 103
                         className = 'content-recording-icon'
102 104
                         src = { ICON_CLOUD } />
103 105
                 </Container>
104
-                <Text className = 'recording-title'>
106
+                <label
107
+                    className = 'recording-title'
108
+                    htmlFor = 'recording-switch-jitsi'>
105 109
                     { label }
106
-                </Text>
110
+                </label>
107 111
                 { switchContent }
108 112
             </Container>
109 113
         );
@@ -132,16 +136,20 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
132 136
                 key = 'fileSharingSetting'>
133 137
                 <Container className = 'recording-icon-container file-sharing-icon-container'>
134 138
                     <Image
139
+                        alt = ''
135 140
                         className = 'recording-file-sharing-icon'
136 141
                         src = { ICON_USERS } />
137 142
                 </Container>
138
-                <Text className = 'recording-title'>
143
+                <label
144
+                    className = 'recording-title'
145
+                    htmlFor = 'recording-switch-share'>
139 146
                     { t('recording.fileSharingdescription') }
140
-                </Text>
147
+                </label>
141 148
                 <Switch
142 149
                     checked = { sharingSetting }
143 150
                     className = 'recording-switch'
144 151
                     disabled = { isValidating }
152
+                    id = 'recording-switch-share'
145 153
                     onChange = { onSharingSettingChanged } />
146 154
             </Container>
147 155
         );
@@ -169,6 +177,7 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
169 177
                 className = 'recording-info'
170 178
                 key = 'cloudUploadInfo'>
171 179
                 <Image
180
+                    alt = ''
172 181
                     className = 'recording-info-icon'
173 182
                     src = { ICON_INFO } />
174 183
                 <Text className = 'recording-info-title'>
@@ -246,6 +255,11 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
246 255
         } = this.props;
247 256
         let content = null;
248 257
         let switchContent = null;
258
+        let labelContent = (
259
+            <Text className = 'recording-title'>
260
+                { t('recording.authDropboxText') }
261
+            </Text>
262
+        );
249 263
 
250 264
         if (isValidating) {
251 265
             content = this._renderSpinner();
@@ -281,8 +295,16 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
281 295
                         === RECORDING_TYPES.DROPBOX }
282 296
                     className = 'recording-switch'
283 297
                     disabled = { isValidating }
298
+                    id = 'recording-switch-integration'
284 299
                     onChange = { this._onDropboxSwitchChange } />
285 300
             );
301
+            labelContent = (
302
+                <label
303
+                    className = 'recording-title'
304
+                    htmlFor = 'recording-switch-integration'>
305
+                    { t('recording.authDropboxText') }
306
+                </label>
307
+            );
286 308
         }
287 309
 
288 310
         return (
@@ -293,12 +315,11 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
293 315
                     <Container
294 316
                         className = 'recording-icon-container'>
295 317
                         <Image
318
+                            alt = ''
296 319
                             className = 'recording-icon'
297 320
                             src = { DROPBOX_LOGO } />
298 321
                     </Container>
299
-                    <Text className = 'recording-title'>
300
-                        { t('recording.authDropboxText') }
301
-                    </Text>
322
+                    { labelContent }
302 323
                     { switchContent }
303 324
                 </Container>
304 325
                 <Container className = 'authorization-panel'>
@@ -338,17 +359,21 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
338 359
                         <Container
339 360
                             className = 'recording-icon-container'>
340 361
                             <Image
362
+                                alt = ''
341 363
                                 className = 'recording-icon'
342 364
                                 src = { LOCAL_RECORDING } />
343 365
                         </Container>
344
-                        <Text className = 'recording-title'>
366
+                        <label
367
+                            className = 'recording-title'
368
+                            htmlFor = 'recording-switch-local'>
345 369
                             { t('recording.saveLocalRecording') }
346
-                        </Text>
370
+                        </label>
347 371
                         <Switch
348 372
                             checked = { selectedRecordingService
349 373
                                 === RECORDING_TYPES.LOCAL }
350 374
                             className = 'recording-switch'
351 375
                             disabled = { isValidating }
376
+                            id = 'recording-switch-local'
352 377
                             onChange = { this._onLocalRecordingSwitchChange } />
353 378
                     </Container>
354 379
                 </Container>
@@ -359,16 +384,20 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
359 384
                                 <Container className = 'recording-header space-top'>
360 385
                                     <Container className = 'recording-icon-container file-sharing-icon-container'>
361 386
                                         <Image
387
+                                            alt = ''
362 388
                                             className = 'recording-file-sharing-icon'
363 389
                                             src = { ICON_USERS } />
364 390
                                     </Container>
365
-                                    <Text className = 'recording-title'>
391
+                                    <label
392
+                                        className = 'recording-title'
393
+                                        htmlFor = 'recording-switch-myself'>
366 394
                                         {t('recording.onlyRecordSelf')}
367
-                                    </Text>
395
+                                    </label>
368 396
                                     <Switch
369 397
                                         checked = { Boolean(localRecordingOnlySelf) }
370 398
                                         className = 'recording-switch'
371 399
                                         disabled = { isValidating }
400
+                                        id = 'recording-switch-myself'
372 401
                                         onChange = { onLocalRecordingSelfChange ?? EMPTY_FUNCTION } />
373 402
                                 </Container>
374 403
                             </Container>

+ 1
- 0
react/features/room-lock/components/PasswordRequiredPrompt.web.tsx Прегледај датотеку

@@ -93,6 +93,7 @@ class PasswordRequiredPrompt extends Component<IProps, IState> {
93 93
                 <Input
94 94
                     autoFocus = { true }
95 95
                     className = 'dialog-bottom-margin'
96
+                    id = 'required-password-input'
96 97
                     label = { this.props.t('dialog.passwordLabel') }
97 98
                     name = 'lockKey'
98 99
                     onChange = { this._onPasswordChanged }

+ 1
- 0
react/features/screen-share/components/web/ShareAudioDialog.tsx Прегледај датотеку

@@ -83,6 +83,7 @@ class ShareAudioDialog extends Component<IProps> {
83 83
                 titleKey = { t('dialog.shareAudioTitle') }>
84 84
                 <div className = 'share-audio-dialog'>
85 85
                     <img
86
+                        alt = ''
86 87
                         className = 'share-audio-animation'
87 88
                         src = 'images/share-audio.gif' />
88 89
                     <Checkbox

+ 33
- 125
react/features/security/components/security-dialog/web/PasswordSection.tsx Прегледај датотеку

@@ -168,67 +168,6 @@ function PasswordSection({
168 168
         copyText(password ?? '');
169 169
     }
170 170
 
171
-    /**
172
-     * Toggles whether or not the password should currently be shown as being
173
-     * edited locally.
174
-     *
175
-     * @param {Object} e - The key event to handle.
176
-     *
177
-     * @private
178
-     * @returns {void}
179
-     */
180
-    function onTogglePasswordEditStateKeyPressHandler(e: React.KeyboardEvent) {
181
-        if (e.key === ' ' || e.key === 'Enter') {
182
-            e.preventDefault();
183
-            onTogglePasswordEditState();
184
-        }
185
-    }
186
-
187
-    /**
188
-     * Method to remotely submit the password from outside of the password form.
189
-     *
190
-     * @param {Object} e - The key event to handle.
191
-     *
192
-     * @private
193
-     * @returns {void}
194
-     */
195
-    function onPasswordSaveKeyPressHandler(e: React.KeyboardEvent) {
196
-        if (e.key === ' ' || e.key === 'Enter') {
197
-            e.preventDefault();
198
-            onPasswordSave();
199
-        }
200
-    }
201
-
202
-    /**
203
-     * Callback invoked to unlock the current JitsiConference.
204
-     *
205
-     * @param {Object} e - The key event to handle.
206
-     *
207
-     * @private
208
-     * @returns {void}
209
-     */
210
-    function onPasswordRemoveKeyPressHandler(e: React.KeyboardEvent) {
211
-        if (e.key === ' ' || e.key === 'Enter') {
212
-            e.preventDefault();
213
-            onPasswordRemove();
214
-        }
215
-    }
216
-
217
-    /**
218
-     * Copies the password to the clipboard.
219
-     *
220
-     * @param {Object} e - The key event to handle.
221
-     *
222
-     * @private
223
-     * @returns {void}
224
-     */
225
-    function onPasswordCopyKeyPressHandler(e: React.KeyboardEvent) {
226
-        if (e.key === ' ' || e.key === 'Enter') {
227
-            e.preventDefault();
228
-            onPasswordCopy();
229
-        }
230
-    }
231
-
232 171
     /**
233 172
      * Callback invoked to show the current password.
234 173
      *
@@ -238,20 +177,6 @@ function PasswordSection({
238 177
         setPasswordVisible(true);
239 178
     }
240 179
 
241
-    /**
242
-     * Callback invoked to show the current password.
243
-     *
244
-     * @param {Object} e - The key event to handle.
245
-     *
246
-     * @returns {void}
247
-     */
248
-    function onPasswordShowKeyPressHandler(e: React.KeyboardEvent) {
249
-        if (e.key === ' ' || e.key === 'Enter') {
250
-            e.preventDefault();
251
-            setPasswordVisible(true);
252
-        }
253
-    }
254
-
255 180
     /**
256 181
      * Callback invoked to hide the current password.
257 182
      *
@@ -261,20 +186,6 @@ function PasswordSection({
261 186
         setPasswordVisible(false);
262 187
     }
263 188
 
264
-    /**
265
-     * Callback invoked to hide the current password.
266
-     *
267
-     * @param {Object} e - The key event to handle.
268
-     *
269
-     * @returns {void}
270
-     */
271
-    function onPasswordHideKeyPressHandler(e: React.KeyboardEvent) {
272
-        if (e.key === ' ' || e.key === 'Enter') {
273
-            e.preventDefault();
274
-            setPasswordVisible(false);
275
-        }
276
-    }
277
-
278 189
     /**
279 190
      * Method that renders the password action(s) based on the current
280 191
      * locked-status of the conference.
@@ -289,18 +200,20 @@ function PasswordSection({
289 200
         if (passwordEditEnabled) {
290 201
             return (
291 202
                 <>
292
-                    <a
293
-                        aria-label = { t('dialog.Cancel') }
203
+                    <button
204
+                        className = 'as-link'
294 205
                         onClick = { onTogglePasswordEditState }
295
-                        onKeyPress = { onTogglePasswordEditStateKeyPressHandler }
296
-                        role = 'button'
297
-                        tabIndex = { 0 }>{ t('dialog.Cancel') }</a>
298
-                    <a
299
-                        aria-label = { t('dialog.add') }
206
+                        type = 'button'>
207
+                        { t('dialog.Cancel') }
208
+                        <span className = 'sr-only'>({ t('dialog.password') })</span>
209
+                    </button>
210
+                    <button
211
+                        className = 'as-link'
300 212
                         onClick = { onPasswordSave }
301
-                        onKeyPress = { onPasswordSaveKeyPressHandler }
302
-                        role = 'button'
303
-                        tabIndex = { 0 }>{ t('dialog.add') }</a>
213
+                        type = 'button'>
214
+                        { t('dialog.add') }
215
+                        <span className = 'sr-only'>({ t('dialog.password') })</span>
216
+                    </button>
304 217
                 </>
305 218
             );
306 219
         }
@@ -308,49 +221,44 @@ function PasswordSection({
308 221
         if (locked) {
309 222
             return (
310 223
                 <>
311
-                    <a
312
-                        aria-label = { t('dialog.Remove') }
313
-                        className = 'remove-password'
224
+                    <button
225
+                        className = 'remove-password as-link'
314 226
                         onClick = { onPasswordRemove }
315
-                        onKeyPress = { onPasswordRemoveKeyPressHandler }
316
-                        role = 'button'
317
-                        tabIndex = { 0 }>{ t('dialog.Remove') }</a>
227
+                        type = 'button'>
228
+                        { t('dialog.Remove') }
229
+                        <span className = 'sr-only'>({ t('dialog.password') })</span>
230
+                    </button>
318 231
                     {
319 232
 
320 233
                         // There are cases like lobby and grant moderator when password is not available
321 234
                         password ? <>
322
-                            <a
323
-                                aria-label = { t('dialog.copy') }
324
-                                className = 'copy-password'
235
+                            <button
236
+                                className = 'copy-password as-link'
325 237
                                 onClick = { onPasswordCopy }
326
-                                onKeyPress = { onPasswordCopyKeyPressHandler }
327
-                                role = 'button'
328
-                                tabIndex = { 0 }>{ t('dialog.copy') }</a>
238
+                                type = 'button'>
239
+                                { t('dialog.copy') }
240
+                                <span className = 'sr-only'>({ t('dialog.password') })</span>
241
+                            </button>
329 242
                         </> : null
330 243
                     }
331 244
                     {locked === LOCKED_LOCALLY && (
332
-                        <a
333
-                            aria-label = { t(passwordVisible ? 'dialog.hide' : 'dialog.show') }
245
+                        <button
246
+                            className = 'as-link'
334 247
                             onClick = { passwordVisible ? onPasswordHide : onPasswordShow }
335
-                            onKeyPress = { passwordVisible
336
-                                ? onPasswordHideKeyPressHandler
337
-                                : onPasswordShowKeyPressHandler
338
-                            }
339
-                            role = 'button'
340
-                            tabIndex = { 0 }>{t(passwordVisible ? 'dialog.hide' : 'dialog.show')}</a>
248
+                            type = 'button'>
249
+                            {t(passwordVisible ? 'dialog.hide' : 'dialog.show')}
250
+                            <span className = 'sr-only'>({ t('dialog.password') })</span>
251
+                        </button>
341 252
                     )}
342 253
                 </>
343 254
             );
344 255
         }
345 256
 
346 257
         return (
347
-            <a
348
-                aria-label = { t('info.addPassword') }
349
-                className = 'add-password'
258
+            <button
259
+                className = 'add-password as-link'
350 260
                 onClick = { onTogglePasswordEditState }
351
-                onKeyPress = { onTogglePasswordEditStateKeyPressHandler }
352
-                role = 'button'
353
-                tabIndex = { 0 }>{ t('info.addPassword') }</a>
261
+                type = 'button'>{ t('info.addPassword') }</button>
354 262
         );
355 263
     }
356 264
 

+ 2
- 0
react/features/settings/components/web/MoreTab.tsx Прегледај датотеку

@@ -254,6 +254,7 @@ class MoreTab extends AbstractDialogTab<IProps, any> {
254 254
 
255 255
         return (
256 256
             <Select
257
+                id = 'more-maxStageParticipants-select'
257 258
                 label = { t('settings.maxStageParticipants') }
258 259
                 onChange = { this._onMaxStageParticipantsSelect }
259 260
                 options = { maxParticipantsItems }
@@ -286,6 +287,7 @@ class MoreTab extends AbstractDialogTab<IProps, any> {
286 287
         return (
287 288
             <Select
288 289
                 className = { classes.bottomMargin }
290
+                id = 'more-language-select'
289 291
                 label = { t('settings.language') }
290 292
                 onChange = { this._onLanguageItemSelect }
291 293
                 options = { languageItems }

+ 0
- 2
react/features/settings/components/web/audio/AudioSettingsContent.tsx Прегледај датотеку

@@ -192,7 +192,6 @@ const AudioSettingsContent = ({
192 192
                 jitsiTrack = { jitsiTrack }
193 193
                 key = { `me-${index}` }
194 194
                 length = { length }
195
-                listHeaderId = { microphoneHeaderId }
196 195
                 measureAudioLevels = { measureAudioLevels }
197 196
                 onClick = { _onMicrophoneEntryClick }>
198 197
                 {label}
@@ -221,7 +220,6 @@ const AudioSettingsContent = ({
221 220
                 isSelected = { isSelected }
222 221
                 key = { key }
223 222
                 length = { length }
224
-                listHeaderId = { speakerHeaderId }
225 223
                 onClick = { _onSpeakerEntryClick }>
226 224
                 {label}
227 225
             </SpeakerEntry>

+ 2
- 10
react/features/settings/components/web/audio/MicrophoneEntry.tsx Прегледај датотеку

@@ -54,8 +54,6 @@ interface IProps {
54 54
     length: number;
55 55
 
56 56
 
57
-    listHeaderId: string;
58
-
59 57
     /**
60 58
     * Used to decide whether to listen to audio level changes.
61 59
     */
@@ -112,7 +110,6 @@ const MicrophoneEntry = ({
112 110
     isSelected,
113 111
     length,
114 112
     jitsiTrack,
115
-    listHeaderId,
116 113
     measureAudioLevels,
117 114
     onClick: propsClick
118 115
 }: IProps) => {
@@ -138,7 +135,7 @@ const MicrophoneEntry = ({
138 135
      * @returns {void}
139 136
      */
140 137
     const onKeyPress = useCallback((e: React.KeyboardEvent) => {
141
-        if (e.key === ' ') {
138
+        if (e.key === 'Enter' || e.key === ' ') {
142 139
             e.preventDefault();
143 140
             propsClick(deviceId);
144 141
         }
@@ -190,14 +187,9 @@ const MicrophoneEntry = ({
190 187
         activeTrackRef.current = jitsiTrack;
191 188
     }, [ jitsiTrack ]);
192 189
 
193
-    const deviceTextId = `choose_microphone${deviceId}`;
194
-
195
-    const labelledby = `${listHeaderId} ${deviceTextId} `;
196
-
197 190
     return (
198 191
         <li
199 192
             aria-checked = { isSelected }
200
-            aria-labelledby = { labelledby }
201 193
             aria-posinset = { index }
202 194
             aria-setsize = { length }
203 195
             className = { classes.container }
@@ -206,7 +198,7 @@ const MicrophoneEntry = ({
206 198
             role = 'radio'
207 199
             tabIndex = { 0 }>
208 200
             <ContextMenuItem
209
-                accessibilityLabel = ''
201
+                accessibilityLabel = { children }
210 202
                 icon = { isSelected ? IconCheck : undefined }
211 203
                 overflowType = { TEXT_OVERFLOW_TYPES.SCROLL_ON_HOVER }
212 204
                 selected = { isSelected }

+ 3
- 8
react/features/settings/components/web/audio/SpeakerEntry.tsx Прегледај датотеку

@@ -39,8 +39,6 @@ interface IProps {
39 39
      */
40 40
     length: number;
41 41
 
42
-    listHeaderId: string;
43
-
44 42
     /**
45 43
      * Click handler for the component.
46 44
      */
@@ -111,7 +109,7 @@ const SpeakerEntry = (props: IProps) => {
111 109
      * @returns {void}
112 110
      */
113 111
     function _onKeyPress(e: React.KeyboardEvent) {
114
-        if (e.key === ' ') {
112
+        if (e.key === 'Enter' || e.key === ' ') {
115 113
             e.preventDefault();
116 114
             props.onClick(props.deviceId);
117 115
         }
@@ -135,15 +133,12 @@ const SpeakerEntry = (props: IProps) => {
135 133
         }
136 134
     }
137 135
 
138
-    const { children, isSelected, index, deviceId, length, listHeaderId } = props;
139
-    const deviceTextId = `choose_speaker${deviceId}`;
140
-    const labelledby = `${listHeaderId} ${deviceTextId} `;
136
+    const { children, isSelected, index, length } = props;
141 137
 
142 138
     /* eslint-disable react/jsx-no-bind */
143 139
     return (
144 140
         <li
145 141
             aria-checked = { isSelected }
146
-            aria-labelledby = { labelledby }
147 142
             aria-posinset = { index }
148 143
             aria-setsize = { length }
149 144
             className = { classes.container }
@@ -152,7 +147,7 @@ const SpeakerEntry = (props: IProps) => {
152 147
             role = 'radio'
153 148
             tabIndex = { 0 }>
154 149
             <ContextMenuItem
155
-                accessibilityLabel = ''
150
+                accessibilityLabel = { children }
156 151
                 icon = { isSelected ? IconCheck : undefined }
157 152
                 overflowType = { TEXT_OVERFLOW_TYPES.SCROLL_ON_HOVER }
158 153
                 selected = { isSelected }

+ 1
- 1
react/features/settings/components/web/video/VideoSettingsContent.tsx Прегледај датотеку

@@ -302,7 +302,7 @@ const VideoSettingsContent = ({
302 302
             </ContextMenuItemGroup>
303 303
             <ContextMenuItemGroup>
304 304
                 { virtualBackgroundSupported && <ContextMenuItem
305
-                    accessibilityLabel = 'virtualBackground.title'
305
+                    accessibilityLabel = { t('virtualBackground.title') }
306 306
                     icon = { IconImage }
307 307
                     onClick = { selectBackground }
308 308
                     text = { t('virtualBackground.title') } /> }

+ 2
- 1
react/features/shared-video/components/web/SharedVideoDialog.tsx Прегледај датотеку

@@ -84,15 +84,16 @@ class SharedVideoDialog extends AbstractSharedVideoDialog<any> {
84 84
                 titleKey = 'dialog.shareVideoTitle'>
85 85
                 <Input
86 86
                     autoFocus = { true }
87
+                    bottomLabel = { error && t('dialog.sharedVideoDialogError') }
87 88
                     className = 'dialog-bottom-margin'
88 89
                     error = { error }
90
+                    id = 'shared-video-url-input'
89 91
                     label = { t('dialog.videoLink') }
90 92
                     name = 'sharedVideoUrl'
91 93
                     onChange = { this._onChange }
92 94
                     placeholder = { t('dialog.sharedVideoLinkPlaceholder') }
93 95
                     type = 'text'
94 96
                     value = { this.state.value } />
95
-                { error && <span className = 'shared-video-dialog-error'>{ t('dialog.sharedVideoDialogError') }</span> }
96 97
             </Dialog>
97 98
         );
98 99
     }

+ 7
- 1
react/features/toolbox/components/web/DialogPortal.ts Прегледај датотеку

@@ -22,6 +22,11 @@ interface IProps {
22 22
      */
23 23
     getRef?: Function;
24 24
 
25
+    /**
26
+     * Function called when the portal target becomes actually visible.
27
+     */
28
+    onVisible?: Function;
29
+
25 30
     /**
26 31
      * Function used to get the updated size info of the container on it's resize.
27 32
      */
@@ -45,7 +50,7 @@ interface IProps {
45 50
  *
46 51
  * @returns {ReactElement}
47 52
  */
48
-function DialogPortal({ children, className, style, getRef, setSize, targetSelector }: IProps) {
53
+function DialogPortal({ children, className, style, getRef, setSize, targetSelector, onVisible }: IProps) {
49 54
     const clientWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].clientWidth);
50 55
     const [ portalTarget ] = useState(() => {
51 56
         const portalDiv = document.createElement('div');
@@ -89,6 +94,7 @@ function DialogPortal({ children, className, style, getRef, setSize, targetSelec
89 94
                 clearTimeout(timerRef.current);
90 95
                 timerRef.current = window.setTimeout(() => {
91 96
                     portalTarget.style.visibility = 'visible';
97
+                    onVisible?.();
92 98
                 }, 100);
93 99
             }
94 100
         });

+ 11
- 9
react/features/toolbox/components/web/Drawer.tsx Прегледај датотеку

@@ -1,5 +1,5 @@
1 1
 import React, { KeyboardEvent, ReactNode, useCallback } from 'react';
2
-import ReactFocusLock from 'react-focus-lock';
2
+import { FocusOn } from 'react-focus-on';
3 3
 import { makeStyles } from 'tss-react/mui';
4 4
 
5 5
 import { isElementInTheViewport } from '../../../base/ui/functions.web';
@@ -102,12 +102,7 @@ function Drawer({
102 102
                 <div
103 103
                     className = { `drawer-menu ${styles.drawer} ${className}` }
104 104
                     onClick = { handleInsideClick }>
105
-                    <ReactFocusLock
106
-                        lockProps = {{
107
-                            role: 'dialog',
108
-                            'aria-modal': true,
109
-                            'aria-labelledby': `#${headingId}`
110
-                        }}
105
+                    <FocusOn
111 106
                         returnFocus = {
112 107
 
113 108
                             // If we return the focus to an element outside the viewport the page will scroll to
@@ -118,8 +113,15 @@ function Drawer({
118 113
                             // because of the animation the whole scenario looks like jumping large video.
119 114
                             isElementInTheViewport
120 115
                         }>
121
-                        {children}
122
-                    </ReactFocusLock>
116
+                        <div
117
+                            aria-labelledby = { headingId ? `#${headingId}` : undefined }
118
+                            aria-modal = { true }
119
+                            data-autofocus = { true }
120
+                            role = 'dialog'
121
+                            tabIndex = { -1 }>
122
+                            {children}
123
+                        </div>
124
+                    </FocusOn>
123 125
                 </div>
124 126
             </div>
125 127
         ) : null

+ 3
- 1
react/features/video-quality/components/Slider.web.tsx Прегледај датотеку

@@ -143,7 +143,9 @@ function Slider({ ariaLabel, max, min, onChange, step, value }: IProps) {
143 143
 
144 144
     return (
145 145
         <div className = { classes.sliderContainer }>
146
-            <ul className = { cx('empty-list', classes.knobContainer) }>
146
+            <ul
147
+                aria-hidden = { true }
148
+                className = { cx('empty-list', classes.knobContainer) }>
147 149
                 {knobs.map((_, i) => (
148 150
                     <li
149 151
                         className = { classes.knob }

+ 1
- 0
react/features/video-quality/components/VideoQualityLabel.web.tsx Прегледај датотеку

@@ -81,6 +81,7 @@ export class VideoQualityLabel extends AbstractVideoQualityLabel<IProps> {
81 81
                 content = { t(tooltipKey) }
82 82
                 position = { 'bottom' }>
83 83
                 <Label
84
+                    accessibilityText = { t(tooltipKey) }
84 85
                     className = { className }
85 86
                     color = { COLORS.white }
86 87
                     icon = { icon }

+ 8
- 2
react/features/video-quality/components/VideoQualitySlider.web.tsx Прегледај датотеку

@@ -187,9 +187,15 @@ class VideoQualitySlider extends Component<IProps> {
187 187
 
188 188
         return (
189 189
             <div className = { clsx('video-quality-dialog', classes.dialog) }>
190
-                <div className = { classes.dialogDetails }>{t('videoStatus.adjustFor')}</div>
190
+                <div
191
+                    aria-hidden = { true }
192
+                    className = { classes.dialogDetails }>
193
+                    {t('videoStatus.adjustFor')}
194
+                </div>
191 195
                 <div className = { classes.dialogContents }>
192
-                    <div className = { classes.sliderDescription }>
196
+                    <div
197
+                        aria-hidden = { true }
198
+                        className = { classes.sliderDescription }>
193 199
                         <span>{t('videoStatus.bestPerformance')}</span>
194 200
                         <span>{t('videoStatus.highestQuality')}</span>
195 201
                     </div>

+ 0
- 1
react/features/virtual-background/components/UploadImageButton.tsx Прегледај датотеку

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

+ 27
- 0
react/features/virtual-background/components/VirtualBackgrounds.tsx Прегледај датотеку

@@ -360,6 +360,24 @@ function VirtualBackgrounds({
360 360
         await setPreviewIsLoaded(loaded);
361 361
     }, []);
362 362
 
363
+    // create a full list of {backgroundId: backgroundLabel} to easily retrieve label of selected background
364
+    const labelsMap: Record<string, string> = {
365
+        none: t('virtualBackground.none'),
366
+        'slight-blur': t('virtualBackground.slightBlur'),
367
+        blur: t('virtualBackground.blur'),
368
+        ..._images.reduce<Record<string, string>>((acc, image) => {
369
+            acc[image.id] = image.tooltip ? t(`virtualBackground.${image.tooltip}`) : '';
370
+
371
+            return acc;
372
+        }, {}),
373
+        ...storedImages.reduce<Record<string, string>>((acc, image, index) => {
374
+            acc[image.id] = t('virtualBackground.uploadedImage', { index: index + 1 });
375
+
376
+            return acc;
377
+        }, {})
378
+    };
379
+    const currentBackgroundLabel = labelsMap[selectedThumbnail] || labelsMap.none;
380
+
363 381
     return (
364 382
         <>
365 383
             <VirtualBackgroundPreview
@@ -372,6 +390,13 @@ function VirtualBackgrounds({
372 390
                 </div>
373 391
             ) : (
374 392
                 <div className = { classes.container }>
393
+                    <span
394
+                        className = 'sr-only'
395
+                        id = 'virtual-background-current-info'>
396
+                        { t('virtualBackground.accessibilityLabel.currentBackground', {
397
+                            background: currentBackgroundLabel
398
+                        }) }
399
+                    </span>
375 400
                     {_showUploadButton
376 401
                     && <UploadImageButton
377 402
                         setLoading = { setLoading }
@@ -380,6 +405,8 @@ function VirtualBackgrounds({
380 405
                         showLabel = { previewIsLoaded }
381 406
                         storedImages = { storedImages } />}
382 407
                     <div
408
+                        aria-describedby = 'virtual-background-current-info'
409
+                        aria-label = { t('virtualBackground.accessibilityLabel.selectBackground') }
383 410
                         className = { classes.thumbnailContainer }
384 411
                         role = 'radiogroup'
385 412
                         tabIndex = { -1 }>

Loading…
Откажи
Сачувај