feat: Make Jitsi WCAG 2.1 compliant (#8921)
* Make Jitsi WCAG 2.1 compliant * Fixed password form keypress handling * Added keypress handler to name form * Removed unneccessary dom query * Fixed mouse hove style * Removed obsolete css rules * accessibilty background feature * Merge remote-tracking branch 'upstream/master' into nic/fix/merge-conflicts * fix error * add german translation * Fixed merge issue * Add id prop back to device selection * Fixed lockfile Co-authored-by: AHMAD KADRI <52747422+ahmadkadri@users.noreply.github.com>
This commit is contained in:
parent
76f1fe8457
commit
e9675453e1
|
@ -32,6 +32,18 @@
|
||||||
.dropdown-menu div[style*="transform"] {
|
.dropdown-menu div[style*="transform"] {
|
||||||
outline: 1px solid #455166;
|
outline: 1px solid #455166;
|
||||||
}
|
}
|
||||||
|
.dropdown-menu button:not(:active):not(:hover) > span {
|
||||||
|
color: #B8C7E0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override @atlaskit/tab styling when in a modal because the
|
||||||
|
* tab text color clash with the modal backgrounds.
|
||||||
|
*/
|
||||||
|
div[role="tablist"] > div:not([data-selected]):not(:hover),
|
||||||
|
label > div > span {
|
||||||
|
color: #B8C7E0 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -9,6 +9,11 @@
|
||||||
max-height: 456px;
|
max-height: 456px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
&-ul {
|
||||||
|
margin:0;
|
||||||
|
padding:0;
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-header {
|
&-header {
|
||||||
|
@ -64,7 +69,13 @@
|
||||||
&-speaker {
|
&-speaker {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:hover {
|
&-ul {
|
||||||
|
margin:0;
|
||||||
|
padding:0;
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &:focus-within, &:focus {
|
||||||
.audio-preview-entry {
|
.audio-preview-entry {
|
||||||
background: #36383C;
|
background: #36383C;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
@ -81,7 +92,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-preview-entry-text {
|
.audio-preview-entry-text {
|
||||||
max-width: 196px;
|
max-width: 178px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +101,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-preview-entry-text {
|
.audio-preview-entry-text {
|
||||||
max-width: 256px;
|
max-width: 238px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,8 +161,9 @@
|
||||||
color: #1C2025;
|
color: #1C2025;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
font-size: 0.8rem;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
padding: 2px 16px;
|
padding: 2px 8px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
|
@ -162,4 +174,10 @@
|
||||||
right: 16px;
|
right: 16px;
|
||||||
top: 14px;
|
top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override @atlaskit/InlineDialog container which is made with styled components
|
||||||
|
& > div:nth-child(2) {
|
||||||
|
outline: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -219,8 +219,9 @@ abbr {
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #3572b0;
|
color: #44A5FF;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
a:focus,
|
a:focus,
|
||||||
a:hover,
|
a:hover,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
.avatar {
|
.avatar {
|
||||||
background-color: #AAA;
|
background-color: #AAA;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
color: rgba(255, 255, 255, 0.6);
|
color: rgba(255, 255, 255, 1);
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|
||||||
|
@ -25,10 +25,6 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.defaultAvatar {
|
|
||||||
opacity: 0.6
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-badge {
|
.avatar-badge {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
|
|
@ -99,18 +99,19 @@
|
||||||
div {
|
div {
|
||||||
svg {
|
svg {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
fill: white
|
fill: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.chat-header {
|
.chat-header {
|
||||||
height: 70px;
|
height: 70px;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -132,6 +133,7 @@
|
||||||
.send-button {
|
.send-button {
|
||||||
background: #1B67EC;
|
background: #1B67EC;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin-left: 0.3rem;
|
||||||
|
|
||||||
@media (hover: hover) and (pointer: fine) {
|
@media (hover: hover) and (pointer: fine) {
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -188,8 +190,9 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 40px;
|
height: 38px;
|
||||||
width: 40px;
|
width: 38px;
|
||||||
|
margin: 2px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,6 +229,11 @@
|
||||||
border: 0px none;
|
border: 0px none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
#usermsg:focus,
|
||||||
|
#usermsg:active {
|
||||||
|
border-bottom: 1px solid white;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
#nickname {
|
#nickname {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -234,6 +242,16 @@
|
||||||
margin: auto 0;
|
margin: auto 0;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
|
|
||||||
|
#nickname-title {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
label[for="nickinput"] {
|
||||||
|
> div > span {
|
||||||
|
color: #B8C7E0;
|
||||||
|
}
|
||||||
|
}
|
||||||
input {
|
input {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
@ -254,7 +272,7 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
color: #757575;
|
color: #AFB6BC;
|
||||||
background: #11336E;
|
background: #11336E;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
@ -301,6 +319,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
border: 0 !important;
|
||||||
|
clip: rect(1px, 1px, 1px, 1px) !important;
|
||||||
|
clip-path: inset(50%) !important;
|
||||||
|
height: 1px !important;
|
||||||
|
margin: -1px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
position: absolute !important;
|
||||||
|
width: 1px !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
.chatmessage {
|
.chatmessage {
|
||||||
background-color: $chatRemoteMessageBackgroundColor;
|
background-color: $chatRemoteMessageBackgroundColor;
|
||||||
border-radius: 0px 6px 6px 6px;
|
border-radius: 0px 6px 6px 6px;
|
||||||
|
@ -350,10 +381,6 @@
|
||||||
color: #757575;
|
color: #757575;
|
||||||
}
|
}
|
||||||
|
|
||||||
.smiley {
|
|
||||||
font-size: 14pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
#smileys {
|
#smileys {
|
||||||
font-size: 20pt;
|
font-size: 20pt;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
@ -382,7 +409,7 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: rgba(0, 0, 0, .6) !important;
|
background-color: rgba(0, 0, 0, .6) !important;
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 0;
|
display: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: calc(#{$sidebarWidth} - 32px);
|
width: calc(#{$sidebarWidth} - 32px);
|
||||||
|
@ -398,6 +425,7 @@
|
||||||
transition: max-height 0.3s;
|
transition: max-height 0.3s;
|
||||||
|
|
||||||
&.show-smileys {
|
&.show-smileys {
|
||||||
|
display: flex;
|
||||||
max-height: 500%;
|
max-height: 500%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -413,7 +441,7 @@
|
||||||
|
|
||||||
.smileyContainer {
|
.smileyContainer {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 36px;
|
height: 40px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@ -509,7 +537,7 @@
|
||||||
|
|
||||||
&-header {
|
&-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
width: calc(100% - 32px);
|
width: calc(100% - 32px);
|
||||||
|
|
|
@ -8,7 +8,10 @@
|
||||||
|
|
||||||
.read-more {
|
.read-more {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: .7;
|
opacity: .9;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -125,7 +125,8 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.with-click-handler:hover {
|
&.with-click-handler:hover,
|
||||||
|
&.with-click-handler:focus {
|
||||||
background-color: #c7ddff;
|
background-color: #c7ddff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,7 +159,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.item:hover {
|
.item:hover, .item:focus, .item:focus-within {
|
||||||
.delete-meeting {
|
.delete-meeting {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,15 @@
|
||||||
&-input-area {
|
&-input-area {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
&-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-title {
|
&-title {
|
||||||
|
@ -74,10 +83,10 @@
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
&--warning {
|
&--warning {
|
||||||
background: rgba(241, 173, 51, 0.7)
|
background: rgba(241, 173, 51, 1);
|
||||||
}
|
}
|
||||||
&--ok {
|
&--ok {
|
||||||
background: rgba(49, 183, 106, 0.7);
|
background: rgba(49, 183, 106, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,6 +101,8 @@
|
||||||
|
|
||||||
&-error-desc {
|
&-error-desc {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-button-container {
|
.settings-button-container {
|
||||||
|
|
|
@ -113,7 +113,7 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -139,6 +139,9 @@
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.copy-button{
|
||||||
|
width: 298px;
|
||||||
|
}
|
||||||
|
|
||||||
.copy-meeting-text {
|
.copy-meeting-text {
|
||||||
width: 266px;
|
width: 266px;
|
||||||
|
@ -177,7 +180,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.focused {
|
&.focused {
|
||||||
box-shadow: 0px 0px 4px 3px #0376DA;
|
box-shadow: 0px 0px 1px 1.5px black, 0px 0px 1.3px 4px white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -249,6 +252,10 @@
|
||||||
fill: transparent;
|
fill: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
|
|
@ -105,6 +105,7 @@
|
||||||
|
|
||||||
.helper-link {
|
.helper-link {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|
|
@ -54,6 +54,7 @@
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: $toolbarZ;
|
z-index: $toolbarZ;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
.button-group-center,
|
.button-group-center,
|
||||||
.button-group-left,
|
.button-group-left,
|
||||||
|
@ -103,6 +104,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbox-content-items {
|
.toolbox-content-items {
|
||||||
|
@ -112,6 +114,7 @@
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
pointer-events: all;
|
||||||
|
|
||||||
>div {
|
>div {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
|
|
@ -79,4 +79,8 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Override @atlaskit/InlineDialog container which is made with styled components
|
||||||
|
& > div:nth-child(2) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -428,7 +428,7 @@
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: $zindex2;
|
z-index: $zindex2;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 13px;
|
height: 18px;
|
||||||
color: #FFF;
|
color: #FFF;
|
||||||
font-size: 10pt;
|
font-size: 10pt;
|
||||||
margin-right: $remoteVideoMenuIconMargin;
|
margin-right: $remoteVideoMenuIconMargin;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 8px 8px 16px;
|
padding: 8px 8px 8px 16px;
|
||||||
margin-top: 8px;
|
margin-top: 5px;
|
||||||
width: calc(100% - 24px);
|
width: calc(100% - 24px);
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
|
||||||
|
|
|
@ -9,10 +9,10 @@ input[type=range]{
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable the default focus styles for webkit range inputs (sliders).
|
* Show focus for keyboard accessibility.
|
||||||
*/
|
*/
|
||||||
input[type=range]:focus {
|
input[type=range]:focus {
|
||||||
outline: none;
|
outline: 1px solid white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
|
||||||
|
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
* see.
|
* see.
|
||||||
*/
|
*/
|
||||||
.active-speaker {
|
.active-speaker {
|
||||||
box-shadow: 0 0 5px 3px $videoThumbnailSelected
|
box-shadow: 0px 0px 1px 1.5px black, 0px 0px 1.3px 4px $videoThumbnailSelected;
|
||||||
}
|
}
|
||||||
|
|
||||||
#filmstripRemoteVideos {
|
#filmstripRemoteVideos {
|
||||||
|
|
|
@ -81,7 +81,7 @@
|
||||||
|
|
||||||
.local-video-menu-trigger,
|
.local-video-menu-trigger,
|
||||||
.remote-video-menu-trigger {
|
.remote-video-menu-trigger {
|
||||||
margin-bottom: 7px;
|
margin-bottom: 3px;
|
||||||
margin-left: $remoteVideoMenuIconMargin;
|
margin-left: $remoteVideoMenuIconMargin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,7 +103,7 @@
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #2684FF;
|
color: #6FB1EA;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
@ -119,7 +119,7 @@
|
||||||
height: 8px;
|
height: 8px;
|
||||||
|
|
||||||
.audio-input-preview-level {
|
.audio-input-preview-level {
|
||||||
background: #4C9AFF;
|
background: #75B1FF;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
-webkit-transition: width .1s ease-in-out;
|
-webkit-transition: width .1s ease-in-out;
|
||||||
|
|
|
@ -94,5 +94,9 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
.star-btn:focus,
|
||||||
|
.star-btn:active {
|
||||||
|
outline: 1px solid #B8C7E0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,20 +30,26 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-password-none,
|
.info-password-none,
|
||||||
.info-password-remote {
|
.info-password-remote {
|
||||||
opacity: 0.5;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-password-input {
|
.info-password-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: transparent;
|
background-color: #0E1624;
|
||||||
border: none;
|
border-radius: 3px;
|
||||||
|
border: 2px solid #202B3D;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
.info-password-input:focus ,
|
||||||
|
.info-password-input:active {
|
||||||
|
border: 2px solid #B8C7E0;
|
||||||
|
}
|
||||||
|
|
||||||
.info-password-local {
|
.info-password-local {
|
||||||
user-select: text;
|
user-select: text;
|
||||||
|
|
|
@ -130,6 +130,7 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
height: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,7 +142,7 @@
|
||||||
& > a {
|
& > a {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
width: 48px;
|
min-width: 48px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
|
@ -16,12 +16,19 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.mock-atlaskit-label {
|
.mock-atlaskit-label {
|
||||||
color: #56637A;
|
color: #b8c7e0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.33;
|
line-height: 1.33;
|
||||||
padding: 20px 0px 4px 0px;
|
padding: 20px 0px 4px 0px;
|
||||||
}
|
}
|
||||||
|
input[type="checkbox"]:checked + svg {
|
||||||
|
--checkbox-background-color: #6492e7;
|
||||||
|
--checkbox-border-color: #6492e7;
|
||||||
|
}
|
||||||
|
input[type="checkbox"] + svg + span {
|
||||||
|
color: #b8c7e0;
|
||||||
|
}
|
||||||
|
|
||||||
input[type="checkbox"] + svg + span {
|
input[type="checkbox"] + svg + span {
|
||||||
color: #9FB0CC;
|
color: #9FB0CC;
|
||||||
|
@ -65,4 +72,14 @@
|
||||||
.sign-out-cta {
|
.sign-out-cta {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $smallScreen) {
|
||||||
|
.device-selection {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.more-tab {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,6 +86,7 @@
|
||||||
|
|
||||||
.video-quality-dialog-label-container.active {
|
.video-quality-dialog-label-container.active {
|
||||||
color: $videoQualityActive;
|
color: $videoQualityActive;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
background: $videoQualityActive;
|
background: $videoQualityActive;
|
||||||
|
|
|
@ -7,9 +7,7 @@
|
||||||
grid-template-columns: auto auto auto auto auto;
|
grid-template-columns: auto auto auto auto auto;
|
||||||
column-gap: 9px;
|
column-gap: 9px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
.desktop-share:hover, .thumbnail:hover, .blur:hover, .slight-blur:hover, .virtual-background-none:hover{
|
.desktop-share:hover, .thumbnail:hover, .blur:hover, .slight-blur:hover, .virtual-background-none:hover{
|
||||||
height: 56px;
|
|
||||||
width: 103px;
|
|
||||||
opacity: .5;
|
opacity: .5;
|
||||||
border: 2px solid #99bbf3;
|
border: 2px solid #99bbf3;
|
||||||
@media (min-width: 432px) and (min-width: 432px) and (max-width: 632px) {
|
@media (min-width: 432px) and (min-width: 432px) and (max-width: 632px) {
|
||||||
|
@ -21,152 +19,75 @@
|
||||||
width: 56px;
|
width: 56px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.thumbnail {
|
.background-option {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
object-fit: cover;
|
|
||||||
height: 60px;
|
height: 60px;
|
||||||
width: 107px;
|
width: 107px;
|
||||||
|
text-align: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.thumbnail {
|
||||||
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnail:hover ~ .delete-image-icon {
|
.thumbnail:hover ~ .delete-image-icon {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.thumbnail-selected {
|
.thumbnail-selected {
|
||||||
margin-top: 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
height: 56px;
|
|
||||||
width: 103px;
|
|
||||||
border: 2px solid #246FE5;
|
border: 2px solid #246FE5;
|
||||||
}
|
}
|
||||||
.blur{
|
.blur{
|
||||||
box-shadow: inset 0 0 12px #000000;
|
box-shadow: inset 0 0 12px #000000;
|
||||||
margin-top: 8px;
|
|
||||||
background: #7E8287;
|
background: #7E8287;
|
||||||
font-weight: bold;
|
padding: 0 10px;
|
||||||
height: 60px;
|
|
||||||
width: 107px;
|
|
||||||
border-radius: 6px;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 60px;
|
|
||||||
}
|
}
|
||||||
.blur-selected {
|
.blur-selected {
|
||||||
box-shadow: inset 0 0 12px #000000;
|
box-shadow: inset 0 0 12px #000000;
|
||||||
margin-top: 8px;
|
|
||||||
background: #7E8287;
|
background: #7E8287;
|
||||||
font-weight: bold;
|
|
||||||
height: 56px;
|
|
||||||
width: 103px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 2px solid #246FE5;
|
border: 2px solid #246FE5;
|
||||||
text-align: center;
|
padding: 0 10px;
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 60px;
|
|
||||||
}
|
}
|
||||||
.slight-blur{
|
.slight-blur{
|
||||||
box-shadow: inset 0 0 12px #000000;
|
box-shadow: inset 0 0 12px #000000;
|
||||||
margin-top: 8px;
|
|
||||||
background: #A4A4A4;
|
background: #A4A4A4;
|
||||||
font-weight: bold;
|
padding: 0 10px;
|
||||||
height: 60px;
|
|
||||||
width: 107px;
|
|
||||||
border-radius: 6px;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 60px;
|
|
||||||
}
|
}
|
||||||
.slight-blur-selected{
|
.slight-blur-selected{
|
||||||
box-shadow: inset 0 0 12px #000000;
|
box-shadow: inset 0 0 12px #000000;
|
||||||
margin-top: 8px;
|
|
||||||
background: #A4A4A4;
|
background: #A4A4A4;
|
||||||
font-weight: bold;
|
|
||||||
height: 56px;
|
|
||||||
width: 103px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 2px solid #246FE5;
|
border: 2px solid #246FE5;
|
||||||
text-align: center;
|
padding: 0 10px;
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 60px;
|
|
||||||
}
|
}
|
||||||
.virtual-background-none {
|
.virtual-background-none {
|
||||||
margin-top: 8px;
|
|
||||||
background: #525252;
|
background: #525252;
|
||||||
font-weight: bold;
|
padding: 0 10px;
|
||||||
height: 60px;
|
|
||||||
width: 107px;
|
|
||||||
border-radius: 6px;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 60px;
|
|
||||||
}
|
}
|
||||||
.none-selected {
|
.none-selected {
|
||||||
margin-top: 8px;
|
|
||||||
background: #525252;
|
background: #525252;
|
||||||
font-weight: bold;
|
|
||||||
height: 56px;
|
|
||||||
width: 103px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 2px solid #246FE5;
|
border: 2px solid #246FE5;
|
||||||
text-align: center;
|
padding: 0 10px;
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 60px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.desktop-share{
|
.desktop-share{
|
||||||
margin-top: 8px;
|
|
||||||
background: #525252;
|
background: #525252;
|
||||||
font-weight: bold;
|
|
||||||
height: 60px;
|
|
||||||
width: 107px;
|
|
||||||
border-radius: 6px;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 60px;
|
|
||||||
}
|
}
|
||||||
.desktop-share-selected{
|
.desktop-share-selected{
|
||||||
margin-top: 8px;
|
|
||||||
background: #525252;
|
background: #525252;
|
||||||
font-weight: bold;
|
|
||||||
height: 56px;
|
|
||||||
width: 103px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 2px solid #246FE5;
|
border: 2px solid #246FE5;
|
||||||
text-align: center;
|
padding: 0 10px;
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 60px;
|
|
||||||
}
|
|
||||||
.share-desktop-icon{
|
|
||||||
margin-top: 15%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 432px) and (max-width: 632px) {
|
@media (min-width: 432px) and (max-width: 632px) {
|
||||||
font-size: 1.5vw;
|
font-size: 1.5vw;
|
||||||
.share-desktop-icon{
|
|
||||||
margin-top: 25%;
|
|
||||||
}
|
|
||||||
.desktop-share, .virtual-background-none, .thumbnail, .blur, .slight-blur{
|
|
||||||
height: 60px;
|
|
||||||
width: 60px;
|
|
||||||
}
|
|
||||||
.desktop-share-selected, .thumbnail-selected, .none-selected, .blur-selected, .slight-blur-selected{
|
|
||||||
height: 56px;
|
|
||||||
width: 56px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@media (max-width: 432px){
|
@media (max-width: 432px){
|
||||||
.share-desktop-icon{
|
|
||||||
margin-top: 25%;
|
|
||||||
}
|
|
||||||
font-size: 1.5vw;
|
font-size: 1.5vw;
|
||||||
.desktop-share, .virtual-background-none, .thumbnail, .blur, .slight-blur{
|
|
||||||
height: 60px;
|
|
||||||
width: 60px;
|
|
||||||
}
|
|
||||||
.desktop-share-selected, .thumbnail-selected, .none-selected, .blur-selected, .slight-blur-selected{
|
|
||||||
height: 56px;
|
|
||||||
width: 56px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,12 +126,18 @@
|
||||||
left: 51px
|
left: 51px
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-image-icon:hover {
|
.delete-image-icon:hover {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnail-container {
|
.thumbnail-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
&:focus-within {
|
||||||
|
.thumbnail ~ .delete-image-icon{
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-background{
|
.add-background{
|
||||||
|
|
|
@ -107,4 +107,4 @@ $selectActiveItemBg: darken($controlBackground, 20%);
|
||||||
/**
|
/**
|
||||||
* TODO: Replace by themed component.
|
* TODO: Replace by themed component.
|
||||||
*/
|
*/
|
||||||
$videoQualityActive: #4C9AFF;
|
$videoQualityActive: #57A0ff;
|
||||||
|
|
|
@ -186,10 +186,10 @@
|
||||||
<!--#include virtual="static/settingsToolbarAdditionalContent.html" -->
|
<!--#include virtual="static/settingsToolbarAdditionalContent.html" -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript aria-hidden="true">
|
||||||
<div>JavaScript is disabled. </br>For this site to work you have to enable JavaScript.</div>
|
<div>JavaScript is disabled. </br>For this site to work you have to enable JavaScript.</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
<!--#include virtual="body.html" -->
|
<!--#include virtual="body.html" -->
|
||||||
<div id="react"></div>
|
<div id="react" role="main"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -70,12 +70,17 @@
|
||||||
},
|
},
|
||||||
"privateNotice": "Private Nachricht an {{recipient}}",
|
"privateNotice": "Private Nachricht an {{recipient}}",
|
||||||
"title": "Chatten",
|
"title": "Chatten",
|
||||||
"you": "Sie"
|
"you": "Sie",
|
||||||
|
"message": "Nachricht",
|
||||||
|
"messageAccessibleTitle": "{{user}} sagt:",
|
||||||
|
"messageAccessibleTitleMe": "Ich sage:",
|
||||||
|
"smileysPanel": "Emoji-Auswahl"
|
||||||
},
|
},
|
||||||
"chromeExtensionBanner": {
|
"chromeExtensionBanner": {
|
||||||
"installExtensionText": "Installieren Sie die Erweiterung für die Integration von Google Calendar und Office 365",
|
"installExtensionText": "Installieren Sie die Erweiterung für die Integration von Google Calendar und Office 365",
|
||||||
"buttonText": "Chrome-Erweiterung installieren",
|
"buttonText": "Chrome-Erweiterung installieren",
|
||||||
"dontShowAgain": "Hinweis nicht mehr anzeigen"
|
"dontShowAgain": "Hinweis nicht mehr anzeigen",
|
||||||
|
"close": "Schließen"
|
||||||
},
|
},
|
||||||
"connectingOverlay": {
|
"connectingOverlay": {
|
||||||
"joiningRoom": "Eine Verbindung zu Ihrem Meeting wird hergestellt…"
|
"joiningRoom": "Eine Verbindung zu Ihrem Meeting wird hergestellt…"
|
||||||
|
@ -204,6 +209,8 @@
|
||||||
"e2eeLabel": "Ende-zu-Ende-Verschlüsselung aktivieren",
|
"e2eeLabel": "Ende-zu-Ende-Verschlüsselung aktivieren",
|
||||||
"e2eeWarning": "WARNUNG: Nicht alle Personen dieser Konferenz scheinen Ende-zu-Ende-Verschlüsselung zu unterstützen. Wenn Sie diese aktivieren, können die entsprechenden Personen nichts mehr sehen oder hören.",
|
"e2eeWarning": "WARNUNG: Nicht alle Personen dieser Konferenz scheinen Ende-zu-Ende-Verschlüsselung zu unterstützen. Wenn Sie diese aktivieren, können die entsprechenden Personen nichts mehr sehen oder hören.",
|
||||||
"enterDisplayName": "Bitte geben Sie hier Ihren Namen ein",
|
"enterDisplayName": "Bitte geben Sie hier Ihren Namen ein",
|
||||||
|
"enterDisplayNameToJoin" : "Benutzername für Konferenz eingeben" ,
|
||||||
|
"embedMeeting": "Besprechung einbetten",
|
||||||
"error": "Fehler",
|
"error": "Fehler",
|
||||||
"gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
|
"gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
|
||||||
"grantModeratorDialog": "Möchten Sie wirklich Moderationsrechte an diese Person vergeben?",
|
"grantModeratorDialog": "Möchten Sie wirklich Moderationsrechte an diese Person vergeben?",
|
||||||
|
@ -295,7 +302,7 @@
|
||||||
"Share": "Teilen",
|
"Share": "Teilen",
|
||||||
"shareVideoLinkError": "Bitte einen gültigen YouTube-Link angeben.",
|
"shareVideoLinkError": "Bitte einen gültigen YouTube-Link angeben.",
|
||||||
"shareVideoTitle": "Video teilen",
|
"shareVideoTitle": "Video teilen",
|
||||||
"shareYourScreen": "Bildschirm freigeben",
|
"shareYourScreen": "Bildschirmfreigabe ein-/ausschalten",
|
||||||
"shareYourScreenDisabled": "Bildschirmfreigabe deaktiviert.",
|
"shareYourScreenDisabled": "Bildschirmfreigabe deaktiviert.",
|
||||||
"startLiveStreaming": "Livestream starten",
|
"startLiveStreaming": "Livestream starten",
|
||||||
"startRecording": "Aufnahme starten",
|
"startRecording": "Aufnahme starten",
|
||||||
|
@ -320,7 +327,9 @@
|
||||||
"WaitForHostMsgWOk": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Falls Sie die Konferenz leiten, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
|
"WaitForHostMsgWOk": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Falls Sie die Konferenz leiten, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
|
||||||
"WaitingForHostTitle": "Warten auf den Beginn der Konferenz …",
|
"WaitingForHostTitle": "Warten auf den Beginn der Konferenz …",
|
||||||
"Yes": "Ja",
|
"Yes": "Ja",
|
||||||
"yourEntireScreen": "Ganzer Bildschirm"
|
"yourEntireScreen": "Ganzer Bildschirm",
|
||||||
|
"remoteUserControls": "Remote Benutzersteuerung von {{username}}",
|
||||||
|
"localUserControls": "Lokale Benutzersteuerung"
|
||||||
},
|
},
|
||||||
"dialOut": {
|
"dialOut": {
|
||||||
"statusMessage": "ist jetzt {{status}}"
|
"statusMessage": "ist jetzt {{status}}"
|
||||||
|
@ -340,8 +349,20 @@
|
||||||
"slightBlur": "Hintergrund leicht unscharf",
|
"slightBlur": "Hintergrund leicht unscharf",
|
||||||
"removeBackground": "Hintergrund entfernen",
|
"removeBackground": "Hintergrund entfernen",
|
||||||
"uploadImage": "Bild hochladen",
|
"uploadImage": "Bild hochladen",
|
||||||
|
"addBackground": "Hintergrund hinzufügen",
|
||||||
"pleaseWait": "Bitte warten...",
|
"pleaseWait": "Bitte warten...",
|
||||||
"none": "keiner"
|
"none": "keiner",
|
||||||
|
"uploadedImage": "Hochgeladenes Bild {{index}}",
|
||||||
|
"deleteImage": "Bild löschen",
|
||||||
|
"image1" : "Strand",
|
||||||
|
"image2" : "Weiße neutrale Wand",
|
||||||
|
"image3" : "Weißer leerer Raum",
|
||||||
|
"image4" : "Schwarze Stehlampe",
|
||||||
|
"image5" : "Berg",
|
||||||
|
"image6" : "Wald",
|
||||||
|
"image7" : "Sonnenaufgang",
|
||||||
|
"desktopShareError": "Desktop konnte nicht freigegeben werden",
|
||||||
|
"desktopShare":"Desktopfreigabe"
|
||||||
},
|
},
|
||||||
"feedback": {
|
"feedback": {
|
||||||
"average": "Durchschnittlich",
|
"average": "Durchschnittlich",
|
||||||
|
@ -350,7 +371,8 @@
|
||||||
"good": "Gut",
|
"good": "Gut",
|
||||||
"rateExperience": "Bitte bewerten Sie diese Konferenz",
|
"rateExperience": "Bitte bewerten Sie diese Konferenz",
|
||||||
"veryBad": "Sehr schlecht",
|
"veryBad": "Sehr schlecht",
|
||||||
"veryGood": "Sehr gut"
|
"veryGood": "Sehr gut",
|
||||||
|
"star": "Sterne"
|
||||||
},
|
},
|
||||||
"incomingCall": {
|
"incomingCall": {
|
||||||
"answer": "Antworten",
|
"answer": "Antworten",
|
||||||
|
@ -367,6 +389,7 @@
|
||||||
"country": "Land",
|
"country": "Land",
|
||||||
"dialANumber": "Um am Meeting teilzunehmen, müssen Sie eine dieser Nummern wählen und dann die PIN eingeben.",
|
"dialANumber": "Um am Meeting teilzunehmen, müssen Sie eine dieser Nummern wählen und dann die PIN eingeben.",
|
||||||
"dialInConferenceID": "PIN:",
|
"dialInConferenceID": "PIN:",
|
||||||
|
"copyNumber":"Nummer kopieren",
|
||||||
"dialInNotSupported": "Entschuldigung, leider wird das Einwählen derzeit nicht unterstützt.",
|
"dialInNotSupported": "Entschuldigung, leider wird das Einwählen derzeit nicht unterstützt.",
|
||||||
"dialInNumber": "Einwählen:",
|
"dialInNumber": "Einwählen:",
|
||||||
"dialInSummaryError": "Fehler beim Abrufen der Einwahlinformationen. Versuchen Sie es später erneut.",
|
"dialInSummaryError": "Fehler beim Abrufen der Einwahlinformationen. Versuchen Sie es später erneut.",
|
||||||
|
@ -404,6 +427,7 @@
|
||||||
"support": "Support",
|
"support": "Support",
|
||||||
"supportMsg": "Wenn der Fehler erneut auftritt, bitte kontaktieren Sie"
|
"supportMsg": "Wenn der Fehler erneut auftritt, bitte kontaktieren Sie"
|
||||||
},
|
},
|
||||||
|
"jitsiHome": "{{logo}} Logo, verlinkt zur Homepage",
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"focusLocal": "Lokales Video fokussieren",
|
"focusLocal": "Lokales Video fokussieren",
|
||||||
"focusRemote": "Auf das Video einer anderen Person fokussieren",
|
"focusRemote": "Auf das Video einer anderen Person fokussieren",
|
||||||
|
@ -524,7 +548,19 @@
|
||||||
"OldElectronAPPTitle": "Sicherheitslücke!",
|
"OldElectronAPPTitle": "Sicherheitslücke!",
|
||||||
"oldElectronClientDescription1": "Sie scheinen eine alte Version des Jitsi-Meet-Clients zu nutzen. Diese hat bekannte Schwachstellen. Bitte aktualisieren Sie auf unsere ",
|
"oldElectronClientDescription1": "Sie scheinen eine alte Version des Jitsi-Meet-Clients zu nutzen. Diese hat bekannte Schwachstellen. Bitte aktualisieren Sie auf unsere ",
|
||||||
"oldElectronClientDescription2": "aktuelle Version",
|
"oldElectronClientDescription2": "aktuelle Version",
|
||||||
"oldElectronClientDescription3": "!"
|
"oldElectronClientDescription3": "!",
|
||||||
|
"groupTitle": "Benachrichtigungen"
|
||||||
|
},
|
||||||
|
"participantsPane": {
|
||||||
|
"close": "Schließen",
|
||||||
|
"headings": {
|
||||||
|
"lobby": "Lobby ({{count}})",
|
||||||
|
"participantsList": "Teilnehmer ({{count}})"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"muteAll": "Alle stummschalten",
|
||||||
|
"stopVideo": "Video stoppen"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"participantsPane": {
|
"participantsPane": {
|
||||||
"headings": {
|
"headings": {
|
||||||
|
@ -589,6 +625,7 @@
|
||||||
"linkCopied": "Link in die Zwischenablage kopiert",
|
"linkCopied": "Link in die Zwischenablage kopiert",
|
||||||
"lookGood": "Ihr Mikrofon scheint zu funktionieren.",
|
"lookGood": "Ihr Mikrofon scheint zu funktionieren.",
|
||||||
"or": "oder",
|
"or": "oder",
|
||||||
|
"keyboardShortcuts" : "Tastaturkurzbefehle aktivieren",
|
||||||
"premeeting": "Vorschau",
|
"premeeting": "Vorschau",
|
||||||
"showScreen": "Konferenzvorschau aktivieren",
|
"showScreen": "Konferenzvorschau aktivieren",
|
||||||
"startWithPhone": "Mit Telefonaudio starten",
|
"startWithPhone": "Mit Telefonaudio starten",
|
||||||
|
@ -612,6 +649,7 @@
|
||||||
"ringing": "Es klingelt …"
|
"ringing": "Es klingelt …"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
|
"avatar": "Benutzerbild",
|
||||||
"setDisplayNameLabel": "Anzeigename festlegen",
|
"setDisplayNameLabel": "Anzeigename festlegen",
|
||||||
"setEmailInput": "E-Mail eingeben",
|
"setEmailInput": "E-Mail eingeben",
|
||||||
"setEmailLabel": "E-Mail-Adresse für Gravatar",
|
"setEmailLabel": "E-Mail-Adresse für Gravatar",
|
||||||
|
@ -728,29 +766,29 @@
|
||||||
"title": "Die Konferenz wurde unterbrochen, weil der Standby-Modus aktiviert wurde."
|
"title": "Die Konferenz wurde unterbrochen, weil der Standby-Modus aktiviert wurde."
|
||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"accessibilityLabel": {
|
"accessibilityLabel": {
|
||||||
"audioOnly": "„Nur Audio“ ein-/ausschalten",
|
"audioOnly": "„Nur Audio“ ein-/ausschalten",
|
||||||
"audioRoute": "Audiogerät auswählen",
|
"audioRoute": "Audiogerät auswählen",
|
||||||
"callQuality": "Qualitätseinstellungen",
|
"callQuality": "Qualitätseinstellungen",
|
||||||
"cc": "Untertitel ein-/ausschalten",
|
"cc": "Untertitel ein-/ausschalten",
|
||||||
"chat": "Chatfenster ein-/ausblenden",
|
"chat": "Chatfenster öffnen / schließen",
|
||||||
"document": "Geteiltes Dokument schließen",
|
"document": "Geteiltes Dokument schließen",
|
||||||
"download": "Unsere Apps herunterladen",
|
"download": "Unsere Apps herunterladen",
|
||||||
"embedMeeting": "Konferenz einbetten",
|
"embedMeeting": "Konferenz einbetten",
|
||||||
"feedback": "Feedback hinterlassen",
|
"feedback": "Feedback hinterlassen",
|
||||||
"fullScreen": "Vollbildmodus ein-/ausschalten",
|
"fullScreen": "Vollbildmodus ein-/ausschalten",
|
||||||
"grantModerator": "Moderationsrechte vergeben",
|
"grantModerator": "Moderationsrechte vergeben",
|
||||||
"hangup": "Anruf beenden",
|
"hangup": "Konferenz verlassen",
|
||||||
"help": "Hilfe",
|
"help": "Hilfe",
|
||||||
"invite": "Person einladen",
|
"invite": "Person einladen",
|
||||||
"kick": "Person entfernen",
|
"kick": "Person entfernen",
|
||||||
"lobbyButton": "Lobbymodus ein-/ausschalten",
|
"lobbyButton": "Lobbymodus ein-/ausschalten",
|
||||||
"localRecording": "Lokale Aufzeichnungssteuerelemente ein-/ausschalten",
|
"localRecording": "Lokale Aufzeichnungssteuerelemente ein-/ausschalten",
|
||||||
"lockRoom": "Konferenzpasswort ein-/ausschalten",
|
"lockRoom": "Konferenzpasswort ein-/ausschalten",
|
||||||
"moreActions": "Menü „Weitere Aktionen“ ein-/ausschalten",
|
"moreActions": "Menü „Weitere Einstellungen“ ein-/ausschalten",
|
||||||
"moreActionsMenu": "Menü „Weitere Aktionen“",
|
"moreActionsMenu": "Menü „Weitere Einstellungen“",
|
||||||
"moreOptions": "Menü „Weitere Optionen“",
|
"moreOptions": "Menü „Weitere Optionen“",
|
||||||
"mute": "„Audio stummschalten“ ein-/ausschalten",
|
"mute": "Mikrofon aktivieren / deaktivieren",
|
||||||
"muteEveryone": "Alle stummschalten",
|
"muteEveryone": "Alle stummschalten",
|
||||||
"muteEveryoneElse": "Alle anderen stummschalten",
|
"muteEveryoneElse": "Alle anderen stummschalten",
|
||||||
"muteEveryonesVideo": "Alle Kameras ausschalten",
|
"muteEveryonesVideo": "Alle Kameras ausschalten",
|
||||||
|
@ -759,7 +797,7 @@
|
||||||
"pip": "Bild-in-Bild-Modus ein-/ausschalten",
|
"pip": "Bild-in-Bild-Modus ein-/ausschalten",
|
||||||
"privateMessage": "Private Nachricht senden",
|
"privateMessage": "Private Nachricht senden",
|
||||||
"profile": "Profil bearbeiten",
|
"profile": "Profil bearbeiten",
|
||||||
"raiseHand": "„Melden“ ein-/ausschalten",
|
"raiseHand": "Hand erheben / senken",
|
||||||
"recording": "Aufzeichnung ein-/ausschalten",
|
"recording": "Aufzeichnung ein-/ausschalten",
|
||||||
"remoteMute": "Personen stummschalten",
|
"remoteMute": "Personen stummschalten",
|
||||||
"remoteVideoMute": "Kamera von dieser Person ausschalten",
|
"remoteVideoMute": "Kamera von dieser Person ausschalten",
|
||||||
|
@ -776,7 +814,9 @@
|
||||||
"toggleCamera": "Kamera wechseln",
|
"toggleCamera": "Kamera wechseln",
|
||||||
"toggleFilmstrip": "Miniaturansichten ein-/ausschalten",
|
"toggleFilmstrip": "Miniaturansichten ein-/ausschalten",
|
||||||
"videomute": "„Video stummschalten“ ein-/ausschalten",
|
"videomute": "„Video stummschalten“ ein-/ausschalten",
|
||||||
"selectBackground": "Hintergrund auswählen"
|
"selectBackground": "Hintergrund auswählen",
|
||||||
|
"expand": "Ausklappen",
|
||||||
|
"collapse": "Einklappen"
|
||||||
},
|
},
|
||||||
"addPeople": "Personen zur Konferenz hinzufügen",
|
"addPeople": "Personen zur Konferenz hinzufügen",
|
||||||
"audioSettings": "Ton-Einstellungen",
|
"audioSettings": "Ton-Einstellungen",
|
||||||
|
@ -797,7 +837,7 @@
|
||||||
"exitFullScreen": "Vollbildmodus verlassen",
|
"exitFullScreen": "Vollbildmodus verlassen",
|
||||||
"exitTileView": "Kachelansicht ausschalten",
|
"exitTileView": "Kachelansicht ausschalten",
|
||||||
"feedback": "Feedback hinterlassen",
|
"feedback": "Feedback hinterlassen",
|
||||||
"hangup": "Verlassen",
|
"hangup": "Konferenz verlassen",
|
||||||
"help": "Hilfe",
|
"help": "Hilfe",
|
||||||
"invite": "Personen einladen",
|
"invite": "Personen einladen",
|
||||||
"lobbyButtonDisable": "Lobbymodus deaktivieren",
|
"lobbyButtonDisable": "Lobbymodus deaktivieren",
|
||||||
|
@ -807,7 +847,7 @@
|
||||||
"lowerYourHand": "Hand senken",
|
"lowerYourHand": "Hand senken",
|
||||||
"moreActions": "Weitere Einstellungen",
|
"moreActions": "Weitere Einstellungen",
|
||||||
"moreOptions": "Weitere Optionen",
|
"moreOptions": "Weitere Optionen",
|
||||||
"mute": "Stummschaltung aktivieren / deaktivieren",
|
"mute": "Mikrofon aktivieren / deaktivieren",
|
||||||
"muteEveryone": "Alle stummschalten",
|
"muteEveryone": "Alle stummschalten",
|
||||||
"muteEveryonesVideo": "Alle Kameras ausschalten",
|
"muteEveryonesVideo": "Alle Kameras ausschalten",
|
||||||
"noAudioSignalTitle": "Es kommt kein Input von Ihrem Mikrofon!",
|
"noAudioSignalTitle": "Es kommt kein Input von Ihrem Mikrofon!",
|
||||||
|
@ -822,7 +862,7 @@
|
||||||
"pip": "Bild-in-Bild-Modus einschalten",
|
"pip": "Bild-in-Bild-Modus einschalten",
|
||||||
"privateMessage": "Private Nachricht senden",
|
"privateMessage": "Private Nachricht senden",
|
||||||
"profile": "Profil bearbeiten",
|
"profile": "Profil bearbeiten",
|
||||||
"raiseHand": "Hand erheben",
|
"raiseHand": "Hand erheben / senken",
|
||||||
"raiseYourHand": "Melden",
|
"raiseYourHand": "Melden",
|
||||||
"security": "Sicherheitsoptionen",
|
"security": "Sicherheitsoptionen",
|
||||||
"Settings": "Einstellungen",
|
"Settings": "Einstellungen",
|
||||||
|
@ -867,6 +907,7 @@
|
||||||
"react-nativeGrantPermissions": "Wählen Sie <b><i>Erlauben</i></b>, wenn der Browser um Berechtigungen bittet.",
|
"react-nativeGrantPermissions": "Wählen Sie <b><i>Erlauben</i></b>, wenn der Browser um Berechtigungen bittet.",
|
||||||
"safariGrantPermissions": "Wählen Sie <b><i>OK</i></b>, wenn der Browser um Berechtigungen bittet."
|
"safariGrantPermissions": "Wählen Sie <b><i>OK</i></b>, wenn der Browser um Berechtigungen bittet."
|
||||||
},
|
},
|
||||||
|
"volumeSlider": "Lautstärkeregler",
|
||||||
"videoSIPGW": {
|
"videoSIPGW": {
|
||||||
"busy": "Es stehen keine freien Ressourcen zur Verfügung. Bitte versuchen Sie es später noch einmal.",
|
"busy": "Es stehen keine freien Ressourcen zur Verfügung. Bitte versuchen Sie es später noch einmal.",
|
||||||
"busyTitle": "Keine freien Ressourcen",
|
"busyTitle": "Keine freien Ressourcen",
|
||||||
|
@ -911,6 +952,7 @@
|
||||||
"videomute": "Person hat die Kamera angehalten"
|
"videomute": "Person hat die Kamera angehalten"
|
||||||
},
|
},
|
||||||
"welcomepage": {
|
"welcomepage": {
|
||||||
|
"addMeetingName": "Besprechungsnamen hinzufügen",
|
||||||
"accessibilityLabel": {
|
"accessibilityLabel": {
|
||||||
"join": "Zum Teilnehmen tippen",
|
"join": "Zum Teilnehmen tippen",
|
||||||
"roomname": "Konferenzname eingeben"
|
"roomname": "Konferenzname eingeben"
|
||||||
|
@ -933,6 +975,9 @@
|
||||||
"join": "ERSTELLEN / BEITRETEN",
|
"join": "ERSTELLEN / BEITRETEN",
|
||||||
"jitsiOnMobile": "Jitsi unterwegs – einfach unsere Apps herunterladen und Meetings von überall starten",
|
"jitsiOnMobile": "Jitsi unterwegs – einfach unsere Apps herunterladen und Meetings von überall starten",
|
||||||
"moderatedMessage": "Oder <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservieren Sie sich eine Konferenz-URL</a>, die nur Sie moderieren.",
|
"moderatedMessage": "Oder <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservieren Sie sich eine Konferenz-URL</a>, die nur Sie moderieren.",
|
||||||
|
"mobileDownLoadLinkIos": "iOS App Download",
|
||||||
|
"mobileDownLoadLinkAndroid": "Android App Download",
|
||||||
|
"mobileDownLoadLinkFDroid": "F-Droid App Download",
|
||||||
"privacy": "Datenschutz",
|
"privacy": "Datenschutz",
|
||||||
"recentList": "Verlauf",
|
"recentList": "Verlauf",
|
||||||
"recentListDelete": "Eintrag löschen",
|
"recentListDelete": "Eintrag löschen",
|
||||||
|
@ -944,7 +989,15 @@
|
||||||
"sendFeedback": "Feedback senden",
|
"sendFeedback": "Feedback senden",
|
||||||
"startMeeting": "Meeting starten",
|
"startMeeting": "Meeting starten",
|
||||||
"terms": "AGB",
|
"terms": "AGB",
|
||||||
"title": "Sichere, voll funktionale und komplett kostenlose Videokonferenzen"
|
"title": "Sichere, voll funktionale und komplett kostenlose Videokonferenzen",
|
||||||
|
"logo":{
|
||||||
|
"calendar":"Kalender Logo",
|
||||||
|
"microsoftLogo":"Microsoft Logo",
|
||||||
|
"logoDeepLinking":"Jitsi Meet Logo",
|
||||||
|
"desktopPreviewThumbnail":"Desktop-Vorschau Thumbnail",
|
||||||
|
"googleLogo":"Google Logo",
|
||||||
|
"policyLogo":"Richtlinienlogo"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"lonelyMeetingExperience": {
|
"lonelyMeetingExperience": {
|
||||||
"button": "Andere einladen",
|
"button": "Andere einladen",
|
||||||
|
|
|
@ -70,12 +70,17 @@
|
||||||
},
|
},
|
||||||
"privateNotice": "Private message to {{recipient}}",
|
"privateNotice": "Private message to {{recipient}}",
|
||||||
"title": "Chat",
|
"title": "Chat",
|
||||||
"you": "you"
|
"you": "you",
|
||||||
|
"message": "Message",
|
||||||
|
"messageAccessibleTitle": "{{user}} says:",
|
||||||
|
"messageAccessibleTitleMe": "me says:",
|
||||||
|
"smileysPanel": "Emoji panel"
|
||||||
},
|
},
|
||||||
"chromeExtensionBanner": {
|
"chromeExtensionBanner": {
|
||||||
"installExtensionText": "Install the extension for Google Calendar and Office 365 integration",
|
"installExtensionText": "Install the extension for Google Calendar and Office 365 integration",
|
||||||
"buttonText": "Install Chrome Extension",
|
"buttonText": "Install Chrome Extension",
|
||||||
"dontShowAgain": "Don’t show me this again"
|
"dontShowAgain": "Don’t show me this again",
|
||||||
|
"close": "Close"
|
||||||
},
|
},
|
||||||
"connectingOverlay": {
|
"connectingOverlay": {
|
||||||
"joiningRoom": "Connecting you to your meeting..."
|
"joiningRoom": "Connecting you to your meeting..."
|
||||||
|
@ -204,6 +209,8 @@
|
||||||
"e2eeLabel": "Enable End-to-End Encryption",
|
"e2eeLabel": "Enable End-to-End Encryption",
|
||||||
"e2eeWarning": "WARNING: Not all participants in this meeting seem to have support for End-to-End encryption. If you enable it they won't be able to see nor hear you.",
|
"e2eeWarning": "WARNING: Not all participants in this meeting seem to have support for End-to-End encryption. If you enable it they won't be able to see nor hear you.",
|
||||||
"enterDisplayName": "Please enter your name here",
|
"enterDisplayName": "Please enter your name here",
|
||||||
|
"enterDisplayNameToJoin": "Please enter your name to join",
|
||||||
|
"embedMeeting": "Embed meeting",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
|
"gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
|
||||||
"grantModeratorDialog": "Are you sure you want to make this participant a moderator?",
|
"grantModeratorDialog": "Are you sure you want to make this participant a moderator?",
|
||||||
|
@ -320,7 +327,9 @@
|
||||||
"WaitForHostMsgWOk": "The conference <b>{{room}}</b> has not yet started. If you are the host then please press Ok to authenticate. Otherwise, please wait for the host to arrive.",
|
"WaitForHostMsgWOk": "The conference <b>{{room}}</b> has not yet started. If you are the host then please press Ok to authenticate. Otherwise, please wait for the host to arrive.",
|
||||||
"WaitingForHostTitle": "Waiting for the host ...",
|
"WaitingForHostTitle": "Waiting for the host ...",
|
||||||
"Yes": "Yes",
|
"Yes": "Yes",
|
||||||
"yourEntireScreen": "Your entire screen"
|
"yourEntireScreen": "Your entire screen",
|
||||||
|
"remoteUserControls": "Remote user controls of {{username}}",
|
||||||
|
"localUserControls": "Local user controls"
|
||||||
},
|
},
|
||||||
"dialOut": {
|
"dialOut": {
|
||||||
"statusMessage": "is now {{status}}"
|
"statusMessage": "is now {{status}}"
|
||||||
|
@ -341,8 +350,19 @@
|
||||||
"slightBlur": "Slight Blur",
|
"slightBlur": "Slight Blur",
|
||||||
"removeBackground": "Remove background",
|
"removeBackground": "Remove background",
|
||||||
"addBackground": "Add background",
|
"addBackground": "Add background",
|
||||||
|
"pleaseWait": "Please wait...",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"desktopShareError": "Could not create desktop share"
|
"uploadedImage": "Uploaded image {{index}}",
|
||||||
|
"deleteImage": "Delete image",
|
||||||
|
"image1" : "Beach",
|
||||||
|
"image2" : "White neutral wall",
|
||||||
|
"image3" : "White empty room",
|
||||||
|
"image4" : "Black floor lamp",
|
||||||
|
"image5" : "Mountain",
|
||||||
|
"image6" : "Forest ",
|
||||||
|
"image7" : "Sunrise",
|
||||||
|
"desktopShareError": "Could not create desktop share",
|
||||||
|
"desktopShare":"Desktop share"
|
||||||
},
|
},
|
||||||
"feedback": {
|
"feedback": {
|
||||||
"average": "Average",
|
"average": "Average",
|
||||||
|
@ -351,7 +371,8 @@
|
||||||
"good": "Good",
|
"good": "Good",
|
||||||
"rateExperience": "Rate your meeting experience",
|
"rateExperience": "Rate your meeting experience",
|
||||||
"veryBad": "Very Bad",
|
"veryBad": "Very Bad",
|
||||||
"veryGood": "Very Good"
|
"veryGood": "Very Good",
|
||||||
|
"star": "Star"
|
||||||
},
|
},
|
||||||
"incomingCall": {
|
"incomingCall": {
|
||||||
"answer": "Answer",
|
"answer": "Answer",
|
||||||
|
@ -368,6 +389,7 @@
|
||||||
"country": "Country",
|
"country": "Country",
|
||||||
"dialANumber": "To join your meeting, dial one of these numbers and then enter the pin.",
|
"dialANumber": "To join your meeting, dial one of these numbers and then enter the pin.",
|
||||||
"dialInConferenceID": "PIN:",
|
"dialInConferenceID": "PIN:",
|
||||||
|
"copyNumber":"Copy number",
|
||||||
"dialInNotSupported": "Sorry, dialing in is currently not supported.",
|
"dialInNotSupported": "Sorry, dialing in is currently not supported.",
|
||||||
"dialInNumber": "Dial-in:",
|
"dialInNumber": "Dial-in:",
|
||||||
"dialInSummaryError": "Error fetching dial-in info now. Please try again later.",
|
"dialInSummaryError": "Error fetching dial-in info now. Please try again later.",
|
||||||
|
@ -405,6 +427,7 @@
|
||||||
"support": "Support",
|
"support": "Support",
|
||||||
"supportMsg": "If this keeps happening, reach out to"
|
"supportMsg": "If this keeps happening, reach out to"
|
||||||
},
|
},
|
||||||
|
"jitsiHome": "{{logo}} Logo, links to Homepage",
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"focusLocal": "Focus on your video",
|
"focusLocal": "Focus on your video",
|
||||||
"focusRemote": "Focus on another person's video",
|
"focusRemote": "Focus on another person's video",
|
||||||
|
@ -525,9 +548,11 @@
|
||||||
"OldElectronAPPTitle": "Security vulnerability!",
|
"OldElectronAPPTitle": "Security vulnerability!",
|
||||||
"oldElectronClientDescription1": "You appear to be using an old version of the Jitsi Meet client which has known security vulnerabilities. Please make sure you update to our ",
|
"oldElectronClientDescription1": "You appear to be using an old version of the Jitsi Meet client which has known security vulnerabilities. Please make sure you update to our ",
|
||||||
"oldElectronClientDescription2": "latest build",
|
"oldElectronClientDescription2": "latest build",
|
||||||
"oldElectronClientDescription3": " now!"
|
"oldElectronClientDescription3": " now!",
|
||||||
|
"groupTitle": "Notifications"
|
||||||
},
|
},
|
||||||
"participantsPane": {
|
"participantsPane": {
|
||||||
|
"close": "Close",
|
||||||
"headings": {
|
"headings": {
|
||||||
"lobby": "Lobby ({{count}})",
|
"lobby": "Lobby ({{count}})",
|
||||||
"participantsList": "Meeting participants ({{count}})"
|
"participantsList": "Meeting participants ({{count}})"
|
||||||
|
@ -592,6 +617,7 @@
|
||||||
"or": "or",
|
"or": "or",
|
||||||
"premeeting": "Pre meeting",
|
"premeeting": "Pre meeting",
|
||||||
"showScreen": "Enable pre meeting screen",
|
"showScreen": "Enable pre meeting screen",
|
||||||
|
"keyboardShortcuts" : "Enable Keyboard shortcuts",
|
||||||
"startWithPhone": "Start with phone audio",
|
"startWithPhone": "Start with phone audio",
|
||||||
"screenSharingError": "Screen sharing error:",
|
"screenSharingError": "Screen sharing error:",
|
||||||
"videoOnlyError": "Video error:",
|
"videoOnlyError": "Video error:",
|
||||||
|
@ -613,6 +639,7 @@
|
||||||
"ringing": "Ringing..."
|
"ringing": "Ringing..."
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
|
"avatar": "avatar",
|
||||||
"setDisplayNameLabel": "Set your display name",
|
"setDisplayNameLabel": "Set your display name",
|
||||||
"setEmailInput": "Enter e-mail",
|
"setEmailInput": "Enter e-mail",
|
||||||
"setEmailLabel": "Set your gravatar email",
|
"setEmailLabel": "Set your gravatar email",
|
||||||
|
@ -735,24 +762,24 @@
|
||||||
"audioRoute": "Select the sound device",
|
"audioRoute": "Select the sound device",
|
||||||
"callQuality": "Manage video quality",
|
"callQuality": "Manage video quality",
|
||||||
"cc": "Toggle subtitles",
|
"cc": "Toggle subtitles",
|
||||||
"chat": "Toggle chat window",
|
"chat": "Open / Close chat",
|
||||||
"document": "Toggle shared document",
|
"document": "Toggle shared document",
|
||||||
"download": "Download our apps",
|
"download": "Download our apps",
|
||||||
"embedMeeting": "Embed meeting",
|
"embedMeeting": "Embed meeting",
|
||||||
"feedback": "Leave feedback",
|
"feedback": "Leave feedback",
|
||||||
"fullScreen": "Toggle full screen",
|
"fullScreen": "Toggle full screen",
|
||||||
"grantModerator": "Grant Moderator",
|
"grantModerator": "Grant Moderator",
|
||||||
"hangup": "Leave the call",
|
"hangup": "Leave the meeting",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
"invite": "Invite people",
|
"invite": "Invite people",
|
||||||
"kick": "Kick participant",
|
"kick": "Kick participant",
|
||||||
"lobbyButton": "Enable/disable lobby mode",
|
"lobbyButton": "Enable/disable lobby mode",
|
||||||
"localRecording": "Toggle local recording controls",
|
"localRecording": "Toggle local recording controls",
|
||||||
"lockRoom": "Toggle meeting password",
|
"lockRoom": "Toggle meeting password",
|
||||||
"moreActions": "Toggle more actions menu",
|
"moreActions": "More actions",
|
||||||
"moreActionsMenu": "More actions menu",
|
"moreActionsMenu": "More actions menu",
|
||||||
"moreOptions": "Show more options",
|
"moreOptions": "Show more options",
|
||||||
"mute": "Toggle mute audio",
|
"mute": "Mute / Unmute",
|
||||||
"muteEveryone": "Mute everyone",
|
"muteEveryone": "Mute everyone",
|
||||||
"muteEveryoneElse": "Mute everyone else",
|
"muteEveryoneElse": "Mute everyone else",
|
||||||
"muteEveryonesVideo": "Disable everyone's camera",
|
"muteEveryonesVideo": "Disable everyone's camera",
|
||||||
|
@ -761,7 +788,7 @@
|
||||||
"pip": "Toggle Picture-in-Picture mode",
|
"pip": "Toggle Picture-in-Picture mode",
|
||||||
"privateMessage": "Send private message",
|
"privateMessage": "Send private message",
|
||||||
"profile": "Edit your profile",
|
"profile": "Edit your profile",
|
||||||
"raiseHand": "Toggle raise hand",
|
"raiseHand": "Raise / Lower your hand",
|
||||||
"recording": "Toggle recording",
|
"recording": "Toggle recording",
|
||||||
"remoteMute": "Mute participant",
|
"remoteMute": "Mute participant",
|
||||||
"remoteVideoMute": "Disable camera of participant",
|
"remoteVideoMute": "Disable camera of participant",
|
||||||
|
@ -770,18 +797,22 @@
|
||||||
"shareaudio": "Share audio",
|
"shareaudio": "Share audio",
|
||||||
"sharedvideo": "Toggle YouTube video sharing",
|
"sharedvideo": "Toggle YouTube video sharing",
|
||||||
"shareRoom": "Invite someone",
|
"shareRoom": "Invite someone",
|
||||||
"shareYourScreen": "Toggle screenshare",
|
"shareYourScreen": "Start / Stop sharing your screen",
|
||||||
"shortcuts": "Toggle shortcuts",
|
"shortcuts": "Toggle shortcuts",
|
||||||
"show": "Show on stage",
|
"show": "Show on stage",
|
||||||
"speakerStats": "Toggle speaker statistics",
|
"speakerStats": "Toggle speaker statistics",
|
||||||
"tileView": "Toggle tile view",
|
"tileView": "Toggle tile view",
|
||||||
"toggleCamera": "Toggle camera",
|
"toggleCamera": "Toggle camera",
|
||||||
"toggleFilmstrip": "Toggle filmstrip",
|
"toggleFilmstrip": "Toggle filmstrip",
|
||||||
"videomute": "Toggle mute video",
|
"videomute": "Start / Stop camera",
|
||||||
"selectBackground": "Select Background"
|
"videoblur": "Toggle video blur",
|
||||||
|
"selectBackground": "Select Background",
|
||||||
|
"expand": "Expand",
|
||||||
|
"collapse": "Collapse"
|
||||||
},
|
},
|
||||||
"addPeople": "Add people to your call",
|
"addPeople": "Add people to your call",
|
||||||
"audioSettings": "Audio settings",
|
"audioSettings": "Audio settings",
|
||||||
|
"videoSettings": "Video settings",
|
||||||
"audioOnlyOff": "Disable low bandwidth mode",
|
"audioOnlyOff": "Disable low bandwidth mode",
|
||||||
"audioOnlyOn": "Enable low bandwidth mode",
|
"audioOnlyOn": "Enable low bandwidth mode",
|
||||||
"audioRoute": "Select the sound device",
|
"audioRoute": "Select the sound device",
|
||||||
|
@ -799,7 +830,7 @@
|
||||||
"exitFullScreen": "Exit full screen",
|
"exitFullScreen": "Exit full screen",
|
||||||
"exitTileView": "Exit tile view",
|
"exitTileView": "Exit tile view",
|
||||||
"feedback": "Leave feedback",
|
"feedback": "Leave feedback",
|
||||||
"hangup": "Leave",
|
"hangup": "Leave the meeting",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
"invite": "Invite people",
|
"invite": "Invite people",
|
||||||
"lobbyButtonDisable": "Disable lobby mode",
|
"lobbyButtonDisable": "Disable lobby mode",
|
||||||
|
@ -842,7 +873,6 @@
|
||||||
"tileViewToggle": "Toggle tile view",
|
"tileViewToggle": "Toggle tile view",
|
||||||
"toggleCamera": "Toggle camera",
|
"toggleCamera": "Toggle camera",
|
||||||
"videomute": "Start / Stop camera",
|
"videomute": "Start / Stop camera",
|
||||||
"videoSettings": "Video settings",
|
|
||||||
"selectBackground": "Select background"
|
"selectBackground": "Select background"
|
||||||
},
|
},
|
||||||
"transcribing": {
|
"transcribing": {
|
||||||
|
@ -869,6 +899,7 @@
|
||||||
"react-nativeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
|
"react-nativeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
|
||||||
"safariGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions."
|
"safariGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions."
|
||||||
},
|
},
|
||||||
|
"volumeSlider": "Volume slider",
|
||||||
"videoSIPGW": {
|
"videoSIPGW": {
|
||||||
"busy": "We're working on freeing resources. Please try again in a few minutes.",
|
"busy": "We're working on freeing resources. Please try again in a few minutes.",
|
||||||
"busyTitle": "The Room service is currently busy",
|
"busyTitle": "The Room service is currently busy",
|
||||||
|
@ -913,6 +944,7 @@
|
||||||
"videomute": "Participant has stopped the camera"
|
"videomute": "Participant has stopped the camera"
|
||||||
},
|
},
|
||||||
"welcomepage": {
|
"welcomepage": {
|
||||||
|
"addMeetingName": "Add Meeting name",
|
||||||
"accessibilityLabel": {
|
"accessibilityLabel": {
|
||||||
"join": "Tap to join",
|
"join": "Tap to join",
|
||||||
"roomname": "Enter room name"
|
"roomname": "Enter room name"
|
||||||
|
@ -934,6 +966,9 @@
|
||||||
"info": "Dial-in info",
|
"info": "Dial-in info",
|
||||||
"join": "CREATE / JOIN",
|
"join": "CREATE / JOIN",
|
||||||
"jitsiOnMobile": "Jitsi on mobile – download our apps and start a meeting from anywhere",
|
"jitsiOnMobile": "Jitsi on mobile – download our apps and start a meeting from anywhere",
|
||||||
|
"mobileDownLoadLinkIos": "Download mobile app for iOS",
|
||||||
|
"mobileDownLoadLinkAndroid": "Download mobile app for Android",
|
||||||
|
"mobileDownLoadLinkFDroid": "Download mobile app for F-Droid",
|
||||||
"moderatedMessage": "Or <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">book a meeting URL</a> in advance where you are the only moderator.",
|
"moderatedMessage": "Or <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">book a meeting URL</a> in advance where you are the only moderator.",
|
||||||
"privacy": "Privacy",
|
"privacy": "Privacy",
|
||||||
"recentList": "Recent",
|
"recentList": "Recent",
|
||||||
|
@ -946,7 +981,15 @@
|
||||||
"sendFeedback": "Send feedback",
|
"sendFeedback": "Send feedback",
|
||||||
"startMeeting": "Start meeting",
|
"startMeeting": "Start meeting",
|
||||||
"terms": "Terms",
|
"terms": "Terms",
|
||||||
"title": "Secure, fully featured, and completely free video conferencing"
|
"title": "Secure, fully featured, and completely free video conferencing",
|
||||||
|
"logo":{
|
||||||
|
"calendar":"Calendar logo",
|
||||||
|
"microsoftLogo":"Microsoft logo",
|
||||||
|
"logoDeepLinking":"Jitsi meet logo",
|
||||||
|
"desktopPreviewThumbnail":"Desktop preview thumbnail",
|
||||||
|
"googleLogo":"Google Logo",
|
||||||
|
"policyLogo":"Policy logo"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"lonelyMeetingExperience": {
|
"lonelyMeetingExperience": {
|
||||||
"button": "Invite others",
|
"button": "Invite others",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/* global APP, $ */
|
/* global APP */
|
||||||
|
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||||
import Logger from 'jitsi-meet-logger';
|
import Logger from 'jitsi-meet-logger';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -30,44 +30,66 @@ const _shortcuts = new Map();
|
||||||
const _shortcutsHelp = new Map();
|
const _shortcutsHelp = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if the keyboard shortcuts are enabled and false if not.
|
* The key used to save in local storage if keyboard shortcuts are enabled.
|
||||||
* @type {boolean}
|
|
||||||
*/
|
*/
|
||||||
let enabled = true;
|
const _enableShortcutsKey = 'enableShortcuts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefer keyboard handling of these elements over global shortcuts.
|
||||||
|
* If a button is triggered using the Spacebar it should not trigger PTT.
|
||||||
|
* If an input element is focused and M is pressed it should not mute audio.
|
||||||
|
*/
|
||||||
|
const _elementsBlacklist = [
|
||||||
|
'input',
|
||||||
|
'textarea',
|
||||||
|
'button',
|
||||||
|
'[role=button]',
|
||||||
|
'[role=menuitem]',
|
||||||
|
'[role=radio]',
|
||||||
|
'[role=tab]',
|
||||||
|
'[role=option]',
|
||||||
|
'[role=switch]',
|
||||||
|
'[role=range]',
|
||||||
|
'[role=log]'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An element selector for elements that have their own keyboard handling.
|
||||||
|
*/
|
||||||
|
const _focusedElementsSelector = `:focus:is(${_elementsBlacklist.join(',')})`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps keycode to character, id of popover for given function and function.
|
* Maps keycode to character, id of popover for given function and function.
|
||||||
*/
|
*/
|
||||||
const KeyboardShortcut = {
|
const KeyboardShortcut = {
|
||||||
|
isPushToTalkActive: false,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._initGlobalShortcuts();
|
this._initGlobalShortcuts();
|
||||||
|
|
||||||
window.onkeyup = e => {
|
window.onkeyup = e => {
|
||||||
if (!enabled) {
|
if (!this.getEnabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const key = this._getKeyboardKey(e).toUpperCase();
|
const key = this._getKeyboardKey(e).toUpperCase();
|
||||||
const num = parseInt(key, 10);
|
const num = parseInt(key, 10);
|
||||||
|
|
||||||
if (!($(':focus').is('input[type=text]')
|
if (!document.querySelector(_focusedElementsSelector)) {
|
||||||
|| $(':focus').is('input[type=password]')
|
|
||||||
|| $(':focus').is('textarea'))) {
|
|
||||||
if (_shortcuts.has(key)) {
|
if (_shortcuts.has(key)) {
|
||||||
_shortcuts.get(key).function(e);
|
_shortcuts.get(key).function(e);
|
||||||
} else if (!isNaN(num) && num >= 0 && num <= 9) {
|
} else if (!isNaN(num) && num >= 0 && num <= 9) {
|
||||||
APP.store.dispatch(clickOnVideo(num));
|
APP.store.dispatch(clickOnVideo(num));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.onkeydown = e => {
|
window.onkeydown = e => {
|
||||||
if (!enabled) {
|
if (!this.getEnabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!($(':focus').is('input[type=text]')
|
const focusedElement = document.querySelector(_focusedElementsSelector);
|
||||||
|| $(':focus').is('input[type=password]')
|
|
||||||
|| $(':focus').is('textarea'))) {
|
if (!focusedElement) {
|
||||||
if (this._getKeyboardKey(e).toUpperCase() === ' ') {
|
if (this._getKeyboardKey(e).toUpperCase() === ' ') {
|
||||||
if (APP.conference.isLocalAudioMuted()) {
|
if (APP.conference.isLocalAudioMuted()) {
|
||||||
sendAnalytics(createShortcutEvent(
|
sendAnalytics(createShortcutEvent(
|
||||||
|
@ -75,8 +97,14 @@ const KeyboardShortcut = {
|
||||||
PRESSED));
|
PRESSED));
|
||||||
logger.log('Talk shortcut pressed');
|
logger.log('Talk shortcut pressed');
|
||||||
APP.conference.muteAudio(false);
|
APP.conference.muteAudio(false);
|
||||||
|
this.isPushToTalkActive = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (this._getKeyboardKey(e).toUpperCase() === 'ESCAPE') {
|
||||||
|
// Allow to remove focus from selected elements using ESC key.
|
||||||
|
if (focusedElement && focusedElement.blur) {
|
||||||
|
focusedElement.blur();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -86,7 +114,13 @@ const KeyboardShortcut = {
|
||||||
* @param {boolean} value - the new value.
|
* @param {boolean} value - the new value.
|
||||||
*/
|
*/
|
||||||
enable(value) {
|
enable(value) {
|
||||||
enabled = value;
|
jitsiLocalStorage.setItem(_enableShortcutsKey, value);
|
||||||
|
},
|
||||||
|
|
||||||
|
getEnabled() {
|
||||||
|
// Should be enabled if not explicitly set to false
|
||||||
|
// eslint-disable-next-line no-unneeded-ternary
|
||||||
|
return jitsiLocalStorage.getItem(_enableShortcutsKey) === 'false' ? false : true;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -198,9 +232,12 @@ const KeyboardShortcut = {
|
||||||
// register SPACE shortcut in two steps to insure visibility of help
|
// register SPACE shortcut in two steps to insure visibility of help
|
||||||
// message
|
// message
|
||||||
this.registerShortcut(' ', null, () => {
|
this.registerShortcut(' ', null, () => {
|
||||||
sendAnalytics(createShortcutEvent('push.to.talk', RELEASED));
|
if (this.isPushToTalkActive) {
|
||||||
logger.log('Talk shortcut released');
|
sendAnalytics(createShortcutEvent('push.to.talk', RELEASED));
|
||||||
APP.conference.muteAudio(true);
|
logger.log('Talk shortcut released');
|
||||||
|
APP.conference.muteAudio(true);
|
||||||
|
this.isPushToTalkActive = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk');
|
this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk');
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
import jqueryI18next from 'jquery-i18next';
|
import jqueryI18next from 'jquery-i18next';
|
||||||
|
|
||||||
|
@ -6,6 +6,10 @@ import { i18next } from '../../react/features/base/i18n';
|
||||||
|
|
||||||
declare var $: Function;
|
declare var $: Function;
|
||||||
|
|
||||||
|
type DocumentElement = {
|
||||||
|
lang: string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies that the {@link i18next} instance has finished its initialization.
|
* Notifies that the {@link i18next} instance has finished its initialization.
|
||||||
*
|
*
|
||||||
|
@ -13,7 +17,12 @@ declare var $: Function;
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
function _onI18nInitialized() {
|
function _onI18nInitialized() {
|
||||||
|
|
||||||
|
const documentElement: DocumentElement
|
||||||
|
= document.documentElement || {};
|
||||||
|
|
||||||
$('[data-i18n]').localize();
|
$('[data-i18n]').localize();
|
||||||
|
documentElement.lang = i18next.language;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -15262,12 +15262,23 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-textarea-autosize": {
|
"react-textarea-autosize": {
|
||||||
"version": "7.1.0",
|
"version": "8.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.3.0.tgz",
|
||||||
"integrity": "sha512-c2FlR/fP0qbxmlrW96SdrbgP/v0XZMTupqB90zybvmDVDutytUgPl7beU35klwcTeMepUIQEpQUn3P3bdshGPg==",
|
"integrity": "sha512-3GLWFAan2pbwBeoeNDoqGmSbrShORtgWfaWX0RJDivsUrpShh01saRM5RU/i4Zmf+whpBVEY5cA90Eq8Ub1N3w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.1.2",
|
"@babel/runtime": "^7.10.2",
|
||||||
"prop-types": "^15.6.0"
|
"use-composed-ref": "^1.0.0",
|
||||||
|
"use-latest": "^1.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": {
|
||||||
|
"version": "7.12.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
|
||||||
|
"integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
|
||||||
|
"requires": {
|
||||||
|
"regenerator-runtime": "^0.13.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-transition-group": {
|
"react-transition-group": {
|
||||||
|
@ -17690,6 +17701,11 @@
|
||||||
"integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==",
|
"integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"ts-essentials": {
|
||||||
|
"version": "2.0.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-2.0.12.tgz",
|
||||||
|
"integrity": "sha512-3IVX4nI6B5cc31/GFFE+i8ey/N2eA0CZDbo6n0yrz0zDX8ZJ8djmU1p+XRz7G3is0F3bB3pu2pAroFdAWQKU3w=="
|
||||||
|
},
|
||||||
"tslib": {
|
"tslib": {
|
||||||
"version": "1.9.3",
|
"version": "1.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
|
||||||
|
@ -17975,6 +17991,27 @@
|
||||||
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz",
|
||||||
"integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg=="
|
"integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg=="
|
||||||
},
|
},
|
||||||
|
"use-composed-ref": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-my1lNHGWsSDAhhVAT4MKs6IjBUtG6ZG11uUqexPH9PptiIZDQOzaF4f5tEbJ2+7qvNbtXNBbU3SfmN+fXlWDhg==",
|
||||||
|
"requires": {
|
||||||
|
"ts-essentials": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"use-isomorphic-layout-effect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-L7Evj8FGcwo/wpbv/qvSfrkHFtOpCzvM5yl2KVyDJoylVuSvzphiiasmjgQPttIGBAy2WKiBNR98q8w7PiNgKQ=="
|
||||||
|
},
|
||||||
|
"use-latest": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-d2TEuG6nSLKQLAfW3By8mKr8HurOlTkul0sOpxbClIv4SQ4iOd7BYr7VIzdbktUCnv7dua/60xzd8igMU6jmyw==",
|
||||||
|
"requires": {
|
||||||
|
"use-isomorphic-layout-effect": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"use-memo-one": {
|
"use-memo-one": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz",
|
||||||
|
|
|
@ -91,7 +91,7 @@
|
||||||
"react-native-webview": "11.0.2",
|
"react-native-webview": "11.0.2",
|
||||||
"react-native-youtube-iframe": "1.2.3",
|
"react-native-youtube-iframe": "1.2.3",
|
||||||
"react-redux": "7.1.0",
|
"react-redux": "7.1.0",
|
||||||
"react-textarea-autosize": "7.1.0",
|
"react-textarea-autosize": "8.3.0",
|
||||||
"react-transition-group": "2.4.0",
|
"react-transition-group": "2.4.0",
|
||||||
"react-youtube": "7.13.1",
|
"react-youtube": "7.13.1",
|
||||||
"redux": "4.0.4",
|
"redux": "4.0.4",
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { translate } from '../../../../base/i18n';
|
||||||
import { Icon } from '../../../icons';
|
import { Icon } from '../../../icons';
|
||||||
import AbstractStatelessAvatar, { type Props as AbstractProps } from '../AbstractStatelessAvatar';
|
import AbstractStatelessAvatar, { type Props as AbstractProps } from '../AbstractStatelessAvatar';
|
||||||
|
|
||||||
|
@ -30,14 +31,19 @@ type Props = AbstractProps & {
|
||||||
/**
|
/**
|
||||||
* TestId of the element, if any.
|
* TestId of the element, if any.
|
||||||
*/
|
*/
|
||||||
testId?: string
|
testId?: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked to obtain translated strings.
|
||||||
|
*/
|
||||||
|
t: Function
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements a stateless avatar component that renders an avatar purely from what gets passed through
|
* Implements a stateless avatar component that renders an avatar purely from what gets passed through
|
||||||
* props.
|
* props.
|
||||||
*/
|
*/
|
||||||
export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||||
/**
|
/**
|
||||||
* Implements {@code Component#render}.
|
* Implements {@code Component#render}.
|
||||||
*
|
*
|
||||||
|
@ -64,6 +70,7 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||||
return (
|
return (
|
||||||
<div className = { this._getBadgeClassName() }>
|
<div className = { this._getBadgeClassName() }>
|
||||||
<img
|
<img
|
||||||
|
alt = { this.props.t('profile.avatar') }
|
||||||
className = { this._getAvatarClassName() }
|
className = { this._getAvatarClassName() }
|
||||||
data-testid = { this.props.testId }
|
data-testid = { this.props.testId }
|
||||||
id = { this.props.id }
|
id = { this.props.id }
|
||||||
|
@ -88,7 +95,7 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||||
xmlnsXlink = 'http://www.w3.org/1999/xlink'>
|
xmlnsXlink = 'http://www.w3.org/1999/xlink'>
|
||||||
<text
|
<text
|
||||||
dominantBaseline = 'central'
|
dominantBaseline = 'central'
|
||||||
fill = 'rgba(255,255,255,.6)'
|
fill = 'rgba(255,255,255,1)'
|
||||||
fontSize = '40pt'
|
fontSize = '40pt'
|
||||||
textAnchor = 'middle'
|
textAnchor = 'middle'
|
||||||
x = '50'
|
x = '50'
|
||||||
|
@ -104,6 +111,7 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||||
return (
|
return (
|
||||||
<div className = { this._getBadgeClassName() }>
|
<div className = { this._getBadgeClassName() }>
|
||||||
<img
|
<img
|
||||||
|
alt = { this.props.t('profile.avatar') }
|
||||||
className = { this._getAvatarClassName('defaultAvatar') }
|
className = { this._getAvatarClassName('defaultAvatar') }
|
||||||
data-testid = { this.props.testId }
|
data-testid = { this.props.testId }
|
||||||
id = { this.props.id }
|
id = { this.props.id }
|
||||||
|
@ -157,3 +165,5 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||||
|
|
||||||
_isIcon: (?string | ?Object) => boolean
|
_isIcon: (?string | ?Object) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default translate(StatelessAvatar);
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { Icon, IconCheck, IconCopy } from '../../base/icons';
|
import { Icon, IconCheck, IconCopy } from '../../base/icons';
|
||||||
import { translate } from '../i18n';
|
|
||||||
import { copyText } from '../util';
|
import { copyText } from '../util';
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,7 +31,12 @@ type Props = {
|
||||||
/**
|
/**
|
||||||
* The text displayed on copy success
|
* The text displayed on copy success
|
||||||
*/
|
*/
|
||||||
textOnCopySuccess: string
|
textOnCopySuccess: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the button
|
||||||
|
*/
|
||||||
|
id?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,7 +44,7 @@ type Props = {
|
||||||
*
|
*
|
||||||
* @returns {React$Element<any>}
|
* @returns {React$Element<any>}
|
||||||
*/
|
*/
|
||||||
function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnCopySuccess }: Props) {
|
function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnCopySuccess, id }: Props) {
|
||||||
const [ isClicked, setIsClicked ] = useState(false);
|
const [ isClicked, setIsClicked ] = useState(false);
|
||||||
const [ isHovered, setIsHovered ] = useState(false);
|
const [ isHovered, setIsHovered ] = useState(false);
|
||||||
|
|
||||||
|
@ -83,6 +87,20 @@ function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnC
|
||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {React.KeyboardEventHandler<HTMLDivElement>} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function onKeyPress(e) {
|
||||||
|
if (onClick && (e.key === ' ' || e.key === 'Enter')) {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the content of the link based on the state.
|
* Renders the content of the link based on the state.
|
||||||
*
|
*
|
||||||
|
@ -93,7 +111,7 @@ function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnC
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className = 'copy-button-content selected'>
|
<div className = 'copy-button-content selected'>
|
||||||
{textOnCopySuccess}
|
<span role = { 'alert' }>{ textOnCopySuccess }</span>
|
||||||
</div>
|
</div>
|
||||||
<Icon src = { IconCheck } />
|
<Icon src = { IconCheck } />
|
||||||
</>
|
</>
|
||||||
|
@ -112,10 +130,17 @@ function CopyButton({ className, displayedText, textToCopy, textOnHover, textOnC
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
aria-label = { textOnHover }
|
||||||
className = { `${className} copy-button${isClicked ? ' clicked' : ''}` }
|
className = { `${className} copy-button${isClicked ? ' clicked' : ''}` }
|
||||||
|
id = { id }
|
||||||
|
onBlur = { onHoverOut }
|
||||||
onClick = { onClick }
|
onClick = { onClick }
|
||||||
|
onFocus = { onHoverIn }
|
||||||
|
onKeyPress = { onKeyPress }
|
||||||
onMouseOut = { onHoverOut }
|
onMouseOut = { onHoverOut }
|
||||||
onMouseOver = { onHoverIn }>
|
onMouseOver = { onHoverIn }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
{ renderContent() }
|
{ renderContent() }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -125,4 +150,4 @@ CopyButton.defaultProps = {
|
||||||
className: ''
|
className: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
export default translate(CopyButton);
|
export default CopyButton;
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
} from '@atlaskit/modal-dialog/dist/es2019/styled/Content';
|
} from '@atlaskit/modal-dialog/dist/es2019/styled/Content';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { translate } from '../../../i18n';
|
||||||
import { Icon, IconClose } from '../../../icons';
|
import { Icon, IconClose } from '../../../icons';
|
||||||
|
|
||||||
const TitleIcon = ({ appearance }: { appearance?: 'danger' | 'warning' }) => {
|
const TitleIcon = ({ appearance }: { appearance?: 'danger' | 'warning' }) => {
|
||||||
|
@ -45,11 +46,40 @@ type Props = {
|
||||||
* @class ModalHeader
|
* @class ModalHeader
|
||||||
* @extends {React.Component<Props>}
|
* @extends {React.Component<Props>}
|
||||||
*/
|
*/
|
||||||
export default class ModalHeader extends React.Component<Props> {
|
class ModalHeader extends React.Component<Props> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
isHeadingMultiline: true
|
isHeadingMultiline: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new {@code ModalHeader} instance.
|
||||||
|
*
|
||||||
|
* @param {*} props - The read-only properties with which the new instance
|
||||||
|
* is to be initialized.
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// Bind event handler so it is only bound once for every instance.
|
||||||
|
this._onKeyPress = this._onKeyPress.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onKeyPress: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onKeyPress(e) {
|
||||||
|
if (this.props.onClose && (e.key === ' ' || e.key === 'Enter')) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -65,7 +95,8 @@ export default class ModalHeader extends React.Component<Props> {
|
||||||
onClose,
|
onClose,
|
||||||
showKeyline,
|
showKeyline,
|
||||||
isHeadingMultiline,
|
isHeadingMultiline,
|
||||||
testId
|
testId,
|
||||||
|
t
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!heading) {
|
if (!heading) {
|
||||||
|
@ -83,12 +114,18 @@ export default class ModalHeader extends React.Component<Props> {
|
||||||
{heading}
|
{heading}
|
||||||
</TitleText>
|
</TitleText>
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
{
|
{
|
||||||
!hideCloseIconButton && <Icon
|
!hideCloseIconButton && <Icon
|
||||||
|
ariaLabel = { t('dialog.close') }
|
||||||
onClick = { onClose }
|
onClick = { onClose }
|
||||||
src = { IconClose } />
|
onKeyPress = { this._onKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
src = { IconClose }
|
||||||
|
tabIndex = { 0 } />
|
||||||
}
|
}
|
||||||
</Header>
|
</Header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export default translate(ModalHeader);
|
||||||
|
|
|
@ -118,7 +118,7 @@ class StatelessDialog extends Component<Props> {
|
||||||
// Bind event handlers so they are only bound once for every instance.
|
// Bind event handlers so they are only bound once for every instance.
|
||||||
this._onCancel = this._onCancel.bind(this);
|
this._onCancel = this._onCancel.bind(this);
|
||||||
this._onDialogDismissed = this._onDialogDismissed.bind(this);
|
this._onDialogDismissed = this._onDialogDismissed.bind(this);
|
||||||
this._onKeyDown = this._onKeyDown.bind(this);
|
this._onKeyPress = this._onKeyPress.bind(this);
|
||||||
this._onSubmit = this._onSubmit.bind(this);
|
this._onSubmit = this._onSubmit.bind(this);
|
||||||
this._renderFooter = this._renderFooter.bind(this);
|
this._renderFooter = this._renderFooter.bind(this);
|
||||||
this._setDialogElement = this._setDialogElement.bind(this);
|
this._setDialogElement = this._setDialogElement.bind(this);
|
||||||
|
@ -159,7 +159,7 @@ class StatelessDialog extends Component<Props> {
|
||||||
shouldCloseOnEscapePress = { true }
|
shouldCloseOnEscapePress = { true }
|
||||||
width = { width || 'medium' }>
|
width = { width || 'medium' }>
|
||||||
<div
|
<div
|
||||||
onKeyDown = { this._onKeyDown }
|
onKeyPress = { this._onKeyPress }
|
||||||
ref = { this._setDialogElement }>
|
ref = { this._setDialogElement }>
|
||||||
<form
|
<form
|
||||||
className = 'modal-dialog-form'
|
className = 'modal-dialog-form'
|
||||||
|
@ -327,7 +327,7 @@ class StatelessDialog extends Component<Props> {
|
||||||
this._dialogElement = element;
|
this._dialogElement = element;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onKeyDown: (Object) => void;
|
_onKeyPress: (Object) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles 'Enter' key in the dialog to submit/hide dialog depending on
|
* Handles 'Enter' key in the dialog to submit/hide dialog depending on
|
||||||
|
@ -337,7 +337,7 @@ class StatelessDialog extends Component<Props> {
|
||||||
* @private
|
* @private
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onKeyDown(event) {
|
_onKeyPress(event) {
|
||||||
// If the event coming to the dialog has been subject to preventDefault
|
// If the event coming to the dialog has been subject to preventDefault
|
||||||
// we don't handle it here.
|
// we don't handle it here.
|
||||||
if (event.defaultPrevented) {
|
if (event.defaultPrevented) {
|
||||||
|
|
|
@ -33,6 +33,7 @@ export function checkChromeExtensionsInstalled(config: Object = {}) {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
|
|
||||||
img.src = `chrome-extension://${info.id}/${info.path}`;
|
img.src = `chrome-extension://${info.id}/${info.path}`;
|
||||||
|
img.setAttribute('aria-hidden', 'true');
|
||||||
img.onload = function() {
|
img.onload = function() {
|
||||||
resolve(true);
|
resolve(true);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { Container } from '../../react/base';
|
import { Container } from '../../react/base';
|
||||||
import { styleTypeToObject } from '../../styles';
|
import { styleTypeToObject } from '../../styles';
|
||||||
|
@ -10,7 +10,7 @@ type Props = {
|
||||||
/**
|
/**
|
||||||
* Class name for the web platform, if any.
|
* Class name for the web platform, if any.
|
||||||
*/
|
*/
|
||||||
className: string,
|
className?: string,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Color of the icon (if not provided by the style object).
|
* Color of the icon (if not provided by the style object).
|
||||||
|
@ -22,6 +22,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
id?: string,
|
id?: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Id of the icon container
|
||||||
|
*/
|
||||||
|
containerId?: string,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to invoke on click.
|
* Function to invoke on click.
|
||||||
*/
|
*/
|
||||||
|
@ -40,8 +45,63 @@ type Props = {
|
||||||
/**
|
/**
|
||||||
* Style object to be applied.
|
* Style object to be applied.
|
||||||
*/
|
*/
|
||||||
style?: Object
|
style?: Object,
|
||||||
};
|
|
||||||
|
/**
|
||||||
|
* aria disabled flag for the Icon.
|
||||||
|
*/
|
||||||
|
ariaDisabled?: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* aria label for the Icon.
|
||||||
|
*/
|
||||||
|
ariaLabel?: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* whether the element has a popup
|
||||||
|
*/
|
||||||
|
ariaHasPopup?: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* whether the element has a pressed
|
||||||
|
*/
|
||||||
|
ariaPressed?: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* id of description label
|
||||||
|
*/
|
||||||
|
ariaDescribedBy?: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* whether the element popup is expanded
|
||||||
|
*/
|
||||||
|
ariaExpanded?: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the element this button icon controls
|
||||||
|
*/
|
||||||
|
ariaControls?: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tabIndex for the Icon.
|
||||||
|
*/
|
||||||
|
tabIndex?: number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* role for the Icon.
|
||||||
|
*/
|
||||||
|
role?: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* keypress handler.
|
||||||
|
*/
|
||||||
|
onKeyPress?: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* keydown handler.
|
||||||
|
*/
|
||||||
|
onKeyDown?: Function
|
||||||
|
}
|
||||||
|
|
||||||
export const DEFAULT_COLOR = navigator.product === 'ReactNative' ? 'white' : undefined;
|
export const DEFAULT_COLOR = navigator.product === 'ReactNative' ? 'white' : undefined;
|
||||||
export const DEFAULT_SIZE = navigator.product === 'ReactNative' ? 36 : 22;
|
export const DEFAULT_SIZE = navigator.product === 'ReactNative' ? 36 : 22;
|
||||||
|
@ -57,11 +117,24 @@ export default function Icon(props: Props) {
|
||||||
className,
|
className,
|
||||||
color,
|
color,
|
||||||
id,
|
id,
|
||||||
|
containerId,
|
||||||
onClick,
|
onClick,
|
||||||
size,
|
size,
|
||||||
src: IconComponent,
|
src: IconComponent,
|
||||||
style
|
style,
|
||||||
} = props;
|
ariaHasPopup,
|
||||||
|
ariaLabel,
|
||||||
|
ariaDisabled,
|
||||||
|
ariaExpanded,
|
||||||
|
ariaControls,
|
||||||
|
tabIndex,
|
||||||
|
ariaPressed,
|
||||||
|
ariaDescribedBy,
|
||||||
|
role,
|
||||||
|
onKeyPress,
|
||||||
|
onKeyDown,
|
||||||
|
...rest
|
||||||
|
}: Props = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
color: styleColor,
|
color: styleColor,
|
||||||
|
@ -71,11 +144,33 @@ export default function Icon(props: Props) {
|
||||||
const calculatedColor = color ?? styleColor ?? DEFAULT_COLOR;
|
const calculatedColor = color ?? styleColor ?? DEFAULT_COLOR;
|
||||||
const calculatedSize = size ?? styleSize ?? DEFAULT_SIZE;
|
const calculatedSize = size ?? styleSize ?? DEFAULT_SIZE;
|
||||||
|
|
||||||
|
const onKeyPressHandler = useCallback(e => {
|
||||||
|
if ((e.key === 'Enter' || e.key === ' ') && onClick) {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick(e);
|
||||||
|
} else if (onKeyPress) {
|
||||||
|
onKeyPress(e);
|
||||||
|
}
|
||||||
|
}, [ onClick, onKeyPress ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
className = { `jitsi-icon ${className}` }
|
{ ...rest }
|
||||||
|
aria-controls = { ariaControls }
|
||||||
|
aria-describedby = { ariaDescribedBy }
|
||||||
|
aria-disabled = { ariaDisabled }
|
||||||
|
aria-expanded = { ariaExpanded }
|
||||||
|
aria-haspopup = { ariaHasPopup }
|
||||||
|
aria-label = { ariaLabel }
|
||||||
|
aria-pressed = { ariaPressed }
|
||||||
|
className = { `jitsi-icon ${className || ''}` }
|
||||||
|
id = { containerId }
|
||||||
onClick = { onClick }
|
onClick = { onClick }
|
||||||
style = { restStyle }>
|
onKeyDown = { onKeyDown }
|
||||||
|
onKeyPress = { onKeyPressHandler }
|
||||||
|
role = { role }
|
||||||
|
style = { restStyle }
|
||||||
|
tabIndex = { tabIndex }>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
fill = { calculatedColor }
|
fill = { calculatedColor }
|
||||||
height = { calculatedSize }
|
height = { calculatedSize }
|
||||||
|
|
|
@ -129,7 +129,9 @@ class Popover extends Component<Props, State> {
|
||||||
// Bind event handlers so they are only bound once for every instance.
|
// Bind event handlers so they are only bound once for every instance.
|
||||||
this._onHideDialog = this._onHideDialog.bind(this);
|
this._onHideDialog = this._onHideDialog.bind(this);
|
||||||
this._onShowDialog = this._onShowDialog.bind(this);
|
this._onShowDialog = this._onShowDialog.bind(this);
|
||||||
|
this._onKeyPress = this._onKeyPress.bind(this);
|
||||||
this._drawerContainerRef = React.createRef();
|
this._drawerContainerRef = React.createRef();
|
||||||
|
this._onEscKey = this._onEscKey.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -207,6 +209,7 @@ class Popover extends Component<Props, State> {
|
||||||
<div
|
<div
|
||||||
className = { className }
|
className = { className }
|
||||||
id = { id }
|
id = { id }
|
||||||
|
onKeyPress = { this._onKeyPress }
|
||||||
onMouseEnter = { this._onShowDialog }
|
onMouseEnter = { this._onShowDialog }
|
||||||
onMouseLeave = { this._onHideDialog }>
|
onMouseLeave = { this._onHideDialog }>
|
||||||
<InlineDialog
|
<InlineDialog
|
||||||
|
@ -231,13 +234,13 @@ class Popover extends Component<Props, State> {
|
||||||
this.setState({ showDialog: false });
|
this.setState({ showDialog: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
_onShowDialog: () => void;
|
_onShowDialog: (Object) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the {@code InlineDialog} and calls any registered onPopoverOpen
|
* Displays the {@code InlineDialog} and calls any registered onPopoverOpen
|
||||||
* callbacks.
|
* callbacks.
|
||||||
*
|
*
|
||||||
* @param {MouseEvent} event - The mouse event to intercept.
|
* @param {Object} event - The mouse event or the keypress event to intercept.
|
||||||
* @private
|
* @private
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
|
@ -252,6 +255,45 @@ class Popover extends Component<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onKeyPress: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onKeyPress(e) {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.state.showDialog) {
|
||||||
|
this._onHideDialog();
|
||||||
|
} else {
|
||||||
|
this._onShowDialog(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onEscKey: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onEscKey(e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (this.state.showDialog) {
|
||||||
|
this._onHideDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the React Element to be displayed in the {@code InlineDialog}.
|
* Renders the React Element to be displayed in the {@code InlineDialog}.
|
||||||
* Also adds padding to support moving the mouse from the trigger to the
|
* Also adds padding to support moving the mouse from the trigger to the
|
||||||
|
@ -264,7 +306,9 @@ class Popover extends Component<Props, State> {
|
||||||
const { content, position } = this.props;
|
const { content, position } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className = 'popover'>
|
<div
|
||||||
|
className = 'popover'
|
||||||
|
onKeyDown = { this._onEscKey }>
|
||||||
{ content }
|
{ content }
|
||||||
<div className = 'popover-mouse-padding-top' />
|
<div className = 'popover-mouse-padding-top' />
|
||||||
<div className = { _mapPositionToPaddingClass(position) } />
|
<div className = { _mapPositionToPaddingClass(position) } />
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { Icon, IconArrowDown } from '../../../icons';
|
import { Icon, IconArrowDown } from '../../../icons';
|
||||||
|
|
||||||
|
@ -46,10 +46,36 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
onClick: Function,
|
onClick: Function,
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Click handler for options.
|
* Click handler for options.
|
||||||
*/
|
*/
|
||||||
onOptionsClick?: Function
|
onOptionsClick?: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* to navigate with the keyboard.
|
||||||
|
*/
|
||||||
|
tabIndex?: number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* to give a role to the icon.
|
||||||
|
*/
|
||||||
|
role?: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* to give a aria-pressed to the icon.
|
||||||
|
*/
|
||||||
|
ariaPressed?: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Label of the current element
|
||||||
|
*/
|
||||||
|
ariaLabel?: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Label of the child element
|
||||||
|
*/
|
||||||
|
ariaDropDownLabel?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,23 +92,57 @@ function ActionButton({
|
||||||
testId,
|
testId,
|
||||||
type = 'primary',
|
type = 'primary',
|
||||||
onClick,
|
onClick,
|
||||||
onOptionsClick
|
onOptionsClick,
|
||||||
|
tabIndex,
|
||||||
|
role,
|
||||||
|
ariaPressed,
|
||||||
|
ariaLabel,
|
||||||
|
ariaDropDownLabel
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
|
||||||
|
const onKeyPressHandler = useCallback(e => {
|
||||||
|
if (onClick && !disabled && (e.key === ' ' || e.key === 'Enter')) {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick(e);
|
||||||
|
}
|
||||||
|
}, [ onClick, disabled ]);
|
||||||
|
|
||||||
|
const onOptionsKeyPressHandler = useCallback(e => {
|
||||||
|
if (onOptionsClick && !disabled && (e.key === ' ' || e.key === 'Enter')) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onOptionsClick(e);
|
||||||
|
}
|
||||||
|
}, [ onOptionsClick, disabled ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
aria-disabled = { disabled }
|
||||||
|
aria-label = { ariaLabel }
|
||||||
className = { `action-btn ${className} ${type} ${disabled ? 'disabled' : ''}` }
|
className = { `action-btn ${className} ${type} ${disabled ? 'disabled' : ''}` }
|
||||||
data-testid = { testId ? testId : undefined }
|
data-testid = { testId ? testId : undefined }
|
||||||
onClick = { disabled ? undefined : onClick }>
|
onClick = { disabled ? undefined : onClick }
|
||||||
|
onKeyPress = { onKeyPressHandler }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 } >
|
||||||
{children}
|
{children}
|
||||||
{hasOptions && <div
|
{ hasOptions
|
||||||
className = 'options'
|
&& <div
|
||||||
data-testid = 'prejoin.joinOptions'
|
aria-disabled = { disabled }
|
||||||
onClick = { disabled ? undefined : onOptionsClick }>
|
aria-haspopup = 'true'
|
||||||
<Icon
|
aria-label = { ariaDropDownLabel }
|
||||||
className = 'icon'
|
aria-pressed = { ariaPressed }
|
||||||
size = { 14 }
|
className = 'options'
|
||||||
src = { OptionsIcon } />
|
data-testid = 'prejoin.joinOptions'
|
||||||
</div>
|
onClick = { disabled ? undefined : onOptionsClick }
|
||||||
|
onKeyPress = { onOptionsKeyPressHandler }
|
||||||
|
role = { role }
|
||||||
|
tabIndex = { tabIndex }>
|
||||||
|
<Icon
|
||||||
|
className = 'icon'
|
||||||
|
size = { 14 }
|
||||||
|
src = { OptionsIcon } />
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { translate } from '../../../i18n';
|
import { translate } from '../../../i18n';
|
||||||
import { Icon, IconArrowDownSmall, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons';
|
import { Icon, IconArrowDownSmall, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons';
|
||||||
|
@ -65,24 +65,50 @@ function ConnectionStatus({ connectionDetails, t, connectionType }: Props) {
|
||||||
? 'con-status-details-visible'
|
? 'con-status-details-visible'
|
||||||
: 'con-status-details-hidden';
|
: 'con-status-details-hidden';
|
||||||
|
|
||||||
|
const onToggleDetails = useCallback(e => {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleDetails(!showDetails);
|
||||||
|
}, [ showDetails, toggleDetails ]);
|
||||||
|
|
||||||
|
const onKeyPressToggleDetails = useCallback(e => {
|
||||||
|
if (toggleDetails && (e.key === ' ' || e.key === 'Enter')) {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleDetails(!showDetails);
|
||||||
|
}
|
||||||
|
}, [ showDetails, toggleDetails ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className = 'con-status'>
|
<div className = 'con-status'>
|
||||||
<div className = 'con-status-container'>
|
<div className = 'con-status-container'>
|
||||||
<div className = 'con-status-header'>
|
<div
|
||||||
|
aria-level = { 1 }
|
||||||
|
className = 'con-status-header'
|
||||||
|
role = 'heading'>
|
||||||
<div className = { `con-status-circle ${connectionClass}` }>
|
<div className = { `con-status-circle ${connectionClass}` }>
|
||||||
<Icon
|
<Icon
|
||||||
size = { 16 }
|
size = { 16 }
|
||||||
src = { icon } />
|
src = { icon } />
|
||||||
</div>
|
</div>
|
||||||
<span className = 'con-status-text'>{t(connectionText)}</span>
|
<span
|
||||||
|
aria-hidden = { !showDetails }
|
||||||
|
className = 'con-status-text'
|
||||||
|
id = 'connection-status-description'>{t(connectionText)}</span>
|
||||||
<Icon
|
<Icon
|
||||||
|
ariaDescribedBy = 'connection-status-description'
|
||||||
|
ariaPressed = { showDetails }
|
||||||
className = { arrowClassName }
|
className = { arrowClassName }
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
onClick = { onToggleDetails }
|
||||||
onClick = { () => toggleDetails(!showDetails) }
|
onKeyPress = { onKeyPressToggleDetails }
|
||||||
|
role = 'button'
|
||||||
size = { 24 }
|
size = { 24 }
|
||||||
src = { IconArrowDownSmall } />
|
src = { IconArrowDownSmall }
|
||||||
|
tabIndex = { 0 } />
|
||||||
</div>
|
</div>
|
||||||
<div className = { `con-status-details ${detailsClassName}` }>{detailsText}</div>
|
<div
|
||||||
|
aria-level = '2'
|
||||||
|
className = { `con-status-details ${detailsClassName}` }
|
||||||
|
role = 'heading'>
|
||||||
|
{detailsText}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import CopyMeetingLinkSection
|
||||||
|
from '../../../../invite/components/add-people-dialog/web/CopyMeetingLinkSection';
|
||||||
import { getCurrentConferenceUrl } from '../../../connection';
|
import { getCurrentConferenceUrl } from '../../../connection';
|
||||||
import { translate } from '../../../i18n';
|
import { translate } from '../../../i18n';
|
||||||
import { Icon, IconCopy, IconCheck } from '../../../icons';
|
|
||||||
import { connect } from '../../../redux';
|
import { connect } from '../../../redux';
|
||||||
import { copyText, getDecodedURI } from '../../../util';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
|
@ -27,152 +27,11 @@ type Props = {
|
||||||
_enableAutomaticUrlCopy: boolean,
|
_enableAutomaticUrlCopy: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If true it shows the 'copy link' message.
|
|
||||||
*/
|
|
||||||
showCopyLink: boolean,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If true it shows the 'link copied' message.
|
|
||||||
*/
|
|
||||||
showLinkCopied: boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
const COPY_TIMEOUT = 2000;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component used to copy meeting url on prejoin page.
|
* Component used to copy meeting url on prejoin page.
|
||||||
*/
|
*/
|
||||||
class CopyMeetingUrl extends Component<Props, State> {
|
class CopyMeetingUrl extends Component<Props> {
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes a new {@code Prejoin} instance.
|
|
||||||
*
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
showCopyLink: false,
|
|
||||||
showLinkCopied: false
|
|
||||||
};
|
|
||||||
this._copyUrl = this._copyUrl.bind(this);
|
|
||||||
this._hideCopyLink = this._hideCopyLink.bind(this);
|
|
||||||
this._hideLinkCopied = this._hideLinkCopied.bind(this);
|
|
||||||
this._showCopyLink = this._showCopyLink.bind(this);
|
|
||||||
this._showLinkCopied = this._showLinkCopied.bind(this);
|
|
||||||
this._copyUrlAutomatically = this._copyUrlAutomatically.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
_copyUrl: () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback invoked to copy the url to clipboard.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
async _copyUrl() {
|
|
||||||
const success = await copyText(this.props.url);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
this._showLinkCopied();
|
|
||||||
window.setTimeout(this._hideLinkCopied, COPY_TIMEOUT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_hideLinkCopied: () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides the 'Link copied' message.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
_hideLinkCopied() {
|
|
||||||
this.setState({
|
|
||||||
showLinkCopied: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_hideCopyLink: () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides the 'Copy link' text.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
_hideCopyLink() {
|
|
||||||
this.setState({
|
|
||||||
showCopyLink: false,
|
|
||||||
showLinkCopied: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_showCopyLink: () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the dark 'Copy link' text on hover.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
_showCopyLink() {
|
|
||||||
this.setState({
|
|
||||||
showCopyLink: true,
|
|
||||||
showLinkCopied: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_showLinkCopied: () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the green 'Link copied' message.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
_showLinkCopied() {
|
|
||||||
this.setState({
|
|
||||||
showLinkCopied: true,
|
|
||||||
showCopyLink: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_copyUrlAutomatically: () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to automatically copy invitation URL.
|
|
||||||
* Document has to be focused in order for this to work.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
async _copyUrlAutomatically() {
|
|
||||||
const isCopied = await copyText(this.props.url);
|
|
||||||
|
|
||||||
if (isCopied) {
|
|
||||||
this._showLinkCopied();
|
|
||||||
window.setTimeout(this._hideLinkCopied, COPY_TIMEOUT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements React's {@link Component#componentDidMount()}. Invoked
|
|
||||||
* immediately before mounting occurs.
|
|
||||||
*
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
componentDidMount() {
|
|
||||||
const { _enableAutomaticUrlCopy } = this.props;
|
|
||||||
|
|
||||||
if (_enableAutomaticUrlCopy) {
|
|
||||||
setTimeout(this._copyUrlAutomatically, 2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
|
@ -181,29 +40,9 @@ class CopyMeetingUrl extends Component<Props, State> {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { showCopyLink, showLinkCopied } = this.state;
|
|
||||||
const { url, t } = this.props;
|
|
||||||
const { _copyUrl, _showCopyLink, _hideCopyLink } = this;
|
|
||||||
const src = showLinkCopied ? IconCheck : IconCopy;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className = 'copy-meeting'>
|
||||||
className = 'copy-meeting'
|
<CopyMeetingLinkSection url = { this.props.url } />
|
||||||
onMouseEnter = { _showCopyLink }
|
|
||||||
onMouseLeave = { _hideCopyLink }>
|
|
||||||
<div
|
|
||||||
className = { `url ${showLinkCopied ? 'done' : ''}` }
|
|
||||||
onClick = { _copyUrl } >
|
|
||||||
<div className = 'copy-meeting-text'>
|
|
||||||
{ !showCopyLink && !showLinkCopied && getDecodedURI(url) }
|
|
||||||
{ showCopyLink && t('prejoin.copyAndShare') }
|
|
||||||
{ showLinkCopied && t('prejoin.linkCopied') }
|
|
||||||
</div>
|
|
||||||
<Icon
|
|
||||||
onClick = { _copyUrl }
|
|
||||||
size = { 24 }
|
|
||||||
src = { src } />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,9 @@ type Props = {
|
||||||
/**
|
/**
|
||||||
* Externally provided value.
|
* Externally provided value.
|
||||||
*/
|
*/
|
||||||
value?: string
|
value?: string,
|
||||||
|
id?: string,
|
||||||
|
autoComplete?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
@ -114,9 +116,11 @@ export default class InputField extends PureComponent<Props, State> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
|
autoComplete = { this.props.autoComplete }
|
||||||
autoFocus = { this.props.autoFocus }
|
autoFocus = { this.props.autoFocus }
|
||||||
className = { `field ${this.state.focused ? 'focused' : ''} ${this.props.className || ''}` }
|
className = { `field ${this.state.focused ? 'focused' : ''} ${this.props.className || ''}` }
|
||||||
data-testid = { this.props.testId ? this.props.testId : undefined }
|
data-testid = { this.props.testId ? this.props.testId : undefined }
|
||||||
|
id = { this.props.id }
|
||||||
onBlur = { this._onBlur }
|
onBlur = { this._onBlur }
|
||||||
onChange = { this._onChange }
|
onChange = { this._onChange }
|
||||||
onFocus = { this._onFocus }
|
onFocus = { this._onFocus }
|
||||||
|
|
|
@ -108,9 +108,9 @@ export default class PreMeetingScreen extends PureComponent<Props> {
|
||||||
)}
|
)}
|
||||||
{showConferenceInfo && (
|
{showConferenceInfo && (
|
||||||
<>
|
<>
|
||||||
<div className = 'title'>
|
<h1 className = 'title'>
|
||||||
{ title }
|
{ title }
|
||||||
</div>
|
</h1>
|
||||||
{showSharingButton ? <CopyMeetingUrl /> : null}
|
{showSharingButton ? <CopyMeetingUrl /> : null}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { Icon, IconCheck } from '../../../icons';
|
import { Icon, IconCheck } from '../../../icons';
|
||||||
|
|
||||||
|
@ -32,10 +32,22 @@ type Props = {
|
||||||
function ToggleButton({ children, isToggled, onClick }: Props) {
|
function ToggleButton({ children, isToggled, onClick }: Props) {
|
||||||
const className = isToggled ? `${mainClass} ${mainClass}--toggled` : mainClass;
|
const className = isToggled ? `${mainClass} ${mainClass}--toggled` : mainClass;
|
||||||
|
|
||||||
|
const onKeyPressHandler = useCallback(e => {
|
||||||
|
if (onClick && (e.key === ' ')) {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
}, [ onClick ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
aria-checked = { isToggled }
|
||||||
className = { className }
|
className = { className }
|
||||||
onClick = { onClick }>
|
id = 'toggle-button'
|
||||||
|
onClick = { onClick }
|
||||||
|
onKeyPress = { onKeyPressHandler }
|
||||||
|
role = 'switch'
|
||||||
|
tabIndex = { 0 }>
|
||||||
<div className = 'toggle-button-container'>
|
<div className = 'toggle-button-container'>
|
||||||
<div className = 'toggle-button-icon-container'>
|
<div className = 'toggle-button-icon-container'>
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -43,7 +55,7 @@ function ToggleButton({ children, isToggled, onClick }: Props) {
|
||||||
size = { 10 }
|
size = { 10 }
|
||||||
src = { IconCheck } />
|
src = { IconCheck } />
|
||||||
</div>
|
</div>
|
||||||
<span>{children}</span>
|
<label htmlFor = 'toggle-button'>{children}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,7 +4,8 @@ import React, { Component } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getLocalizedDateFormatter,
|
getLocalizedDateFormatter,
|
||||||
getLocalizedDurationFormatter
|
getLocalizedDurationFormatter,
|
||||||
|
translate
|
||||||
} from '../../../i18n';
|
} from '../../../i18n';
|
||||||
import { Icon, IconTrash } from '../../../icons';
|
import { Icon, IconTrash } from '../../../icons';
|
||||||
|
|
||||||
|
@ -41,7 +42,12 @@ type Props = {
|
||||||
/**
|
/**
|
||||||
* Handler for deleting an item.
|
* Handler for deleting an item.
|
||||||
*/
|
*/
|
||||||
onItemDelete?: Function
|
onItemDelete?: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked to obtain translated strings.
|
||||||
|
*/
|
||||||
|
t: Function
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -80,7 +86,7 @@ function _toTimeString(times) {
|
||||||
*
|
*
|
||||||
* @extends Component
|
* @extends Component
|
||||||
*/
|
*/
|
||||||
export default class MeetingsList extends Component<Props> {
|
class MeetingsList extends Component<Props> {
|
||||||
/**
|
/**
|
||||||
* Constructor of the MeetingsList component.
|
* Constructor of the MeetingsList component.
|
||||||
*
|
*
|
||||||
|
@ -99,7 +105,7 @@ export default class MeetingsList extends Component<Props> {
|
||||||
* @returns {React.ReactNode}
|
* @returns {React.ReactNode}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { listEmptyComponent, meetings } = this.props;
|
const { listEmptyComponent, meetings, t } = this.props;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If there are no recent meetings we don't want to display anything
|
* If there are no recent meetings we don't want to display anything
|
||||||
|
@ -107,7 +113,10 @@ export default class MeetingsList extends Component<Props> {
|
||||||
if (meetings) {
|
if (meetings) {
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
className = 'meetings-list'>
|
aria-label = { t('welcomepage.recentList') }
|
||||||
|
className = 'meetings-list'
|
||||||
|
role = 'menu'
|
||||||
|
tabIndex = '-1'>
|
||||||
{
|
{
|
||||||
meetings.length === 0
|
meetings.length === 0
|
||||||
? listEmptyComponent
|
? listEmptyComponent
|
||||||
|
@ -139,6 +148,29 @@ export default class MeetingsList extends Component<Props> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onKeyPress: string => Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a function that is used in the onPress callback of the items.
|
||||||
|
*
|
||||||
|
* @param {string} url - The URL of the item to navigate to.
|
||||||
|
* @private
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
_onKeyPress(url) {
|
||||||
|
const { disabled, onPress } = this.props;
|
||||||
|
|
||||||
|
if (!disabled && url && typeof onPress === 'function') {
|
||||||
|
return e => {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
onPress(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
_onDelete: Object => Function;
|
_onDelete: Object => Function;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -158,6 +190,27 @@ export default class MeetingsList extends Component<Props> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onDeleteKeyPress: Object => Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a function that is used on the onDelete keypress callback.
|
||||||
|
*
|
||||||
|
* @param {Object} item - The item to be deleted.
|
||||||
|
* @private
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
_onDeleteKeyPress(item) {
|
||||||
|
const { onItemDelete } = this.props;
|
||||||
|
|
||||||
|
return e => {
|
||||||
|
if (onItemDelete && (e.key === ' ' || e.key === 'Enter')) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onItemDelete(item);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
_renderItem: (Object, number) => React$Node;
|
_renderItem: (Object, number) => React$Node;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -176,17 +229,22 @@ export default class MeetingsList extends Component<Props> {
|
||||||
title,
|
title,
|
||||||
url
|
url
|
||||||
} = meeting;
|
} = meeting;
|
||||||
const { hideURL = false, onItemDelete } = this.props;
|
const { hideURL = false, onItemDelete, t } = this.props;
|
||||||
const onPress = this._onPress(url);
|
const onPress = this._onPress(url);
|
||||||
|
const onKeyPress = this._onKeyPress(url);
|
||||||
const rootClassName
|
const rootClassName
|
||||||
= `item ${
|
= `item ${
|
||||||
onPress ? 'with-click-handler' : 'without-click-handler'}`;
|
onPress ? 'with-click-handler' : 'without-click-handler'}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
|
aria-label = { title }
|
||||||
className = { rootClassName }
|
className = { rootClassName }
|
||||||
key = { index }
|
key = { index }
|
||||||
onClick = { onPress }>
|
onClick = { onPress }
|
||||||
|
onKeyPress = { onKeyPress }
|
||||||
|
role = 'menuitem'
|
||||||
|
tabIndex = { 0 }>
|
||||||
<Container className = 'left-column'>
|
<Container className = 'left-column'>
|
||||||
<Text className = 'title'>
|
<Text className = 'title'>
|
||||||
{ _toDateString(date) }
|
{ _toDateString(date) }
|
||||||
|
@ -216,11 +274,17 @@ export default class MeetingsList extends Component<Props> {
|
||||||
{ elementAfter || null }
|
{ elementAfter || null }
|
||||||
|
|
||||||
{ onItemDelete && <Icon
|
{ onItemDelete && <Icon
|
||||||
|
ariaLabel = { t('welcomepage.recentListDelete') }
|
||||||
className = 'delete-meeting'
|
className = 'delete-meeting'
|
||||||
onClick = { this._onDelete(meeting) }
|
onClick = { this._onDelete(meeting) }
|
||||||
src = { IconTrash } />}
|
onKeyPress = { this._onDeleteKeyPress(meeting) }
|
||||||
|
role = 'button'
|
||||||
|
src = { IconTrash }
|
||||||
|
tabIndex = { 0 } />}
|
||||||
</Container>
|
</Container>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default translate(MeetingsList);
|
||||||
|
|
|
@ -160,13 +160,15 @@ class Watermarks extends Component<Props, State> {
|
||||||
_logoUrl,
|
_logoUrl,
|
||||||
_showJitsiWatermark
|
_showJitsiWatermark
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const { t } = this.props;
|
||||||
let reactElement = null;
|
let reactElement = null;
|
||||||
|
|
||||||
if (_showJitsiWatermark) {
|
if (_showJitsiWatermark) {
|
||||||
const style = {
|
const style = {
|
||||||
backgroundImage: `url(${_logoUrl})`,
|
backgroundImage: `url(${_logoUrl})`,
|
||||||
maxWidth: 140,
|
maxWidth: 140,
|
||||||
maxHeight: 70
|
maxHeight: 70,
|
||||||
|
position: _logoLink ? 'static' : 'absolute'
|
||||||
};
|
};
|
||||||
|
|
||||||
reactElement = (<div
|
reactElement = (<div
|
||||||
|
@ -176,6 +178,8 @@ class Watermarks extends Component<Props, State> {
|
||||||
if (_logoLink) {
|
if (_logoLink) {
|
||||||
reactElement = (
|
reactElement = (
|
||||||
<a
|
<a
|
||||||
|
aria-label = { t('jitsiHome', { logo: interfaceConfig.APP_NAME }) }
|
||||||
|
className = 'watermark leftwatermark'
|
||||||
href = { _logoLink }
|
href = { _logoLink }
|
||||||
target = '_new'>
|
target = '_new'>
|
||||||
{ reactElement }
|
{ reactElement }
|
||||||
|
|
|
@ -246,14 +246,18 @@ export default class AbstractButton<P: Props, S: *> extends Component<P, S> {
|
||||||
* Handles clicking / pressing the button, and toggles the audio mute state
|
* Handles clicking / pressing the button, and toggles the audio mute state
|
||||||
* accordingly.
|
* accordingly.
|
||||||
*
|
*
|
||||||
|
* @param {Object} e - Event.
|
||||||
* @private
|
* @private
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onClick() {
|
_onClick(e) {
|
||||||
const { afterClick } = this.props;
|
const { afterClick } = this.props;
|
||||||
|
|
||||||
this._handleClick();
|
this._handleClick();
|
||||||
afterClick && afterClick();
|
afterClick && afterClick(e);
|
||||||
|
|
||||||
|
// blur after click to release focus from button to allow PTT.
|
||||||
|
e && e.currentTarget && e.currentTarget.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,6 +6,10 @@ import { translate } from '../../i18n';
|
||||||
import { Container, Text } from '../../react';
|
import { Container, Text } from '../../react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked to obtain translated strings.
|
||||||
|
*/
|
||||||
t: Function
|
t: Function
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,29 +20,21 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this._onKeyDown = this._onKeyDown.bind(this);
|
this._onKeyPress = this._onKeyPress.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onKeyDown: (Object) => void;
|
_onKeyPress: (Object) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles 'Enter' key on the button to trigger onClick for accessibility.
|
* Handles 'Enter' and Space key on the button to trigger onClick for accessibility.
|
||||||
* We should be handling Space onKeyUp but it conflicts with PTT.
|
|
||||||
*
|
*
|
||||||
* @param {Object} event - The key event.
|
* @param {Object} event - The key event.
|
||||||
* @private
|
* @private
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onKeyDown(event) {
|
_onKeyPress(event) {
|
||||||
// If the event coming to the dialog has been subject to preventDefault
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
// we don't handle it here.
|
|
||||||
if (event.defaultPrevented) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
|
||||||
this.props.onClick();
|
this.props.onClick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,9 +64,9 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
|
||||||
'aria-label': this.accessibilityLabel,
|
'aria-label': this.accessibilityLabel,
|
||||||
className: className + (disabled ? ' disabled' : ''),
|
className: className + (disabled ? ' disabled' : ''),
|
||||||
onClick: disabled ? undefined : onClick,
|
onClick: disabled ? undefined : onClick,
|
||||||
onKeyDown: this._onKeyDown,
|
onKeyPress: this._onKeyPress,
|
||||||
tabIndex: 0,
|
tabIndex: 0,
|
||||||
role: 'button'
|
role: showLabel ? 'menuitem' : 'button'
|
||||||
};
|
};
|
||||||
|
|
||||||
const elementType = showLabel ? 'li' : 'div';
|
const elementType = showLabel ? 'li' : 'div';
|
||||||
|
|
|
@ -74,6 +74,35 @@ class OverflowMenuItem extends Component<Props> {
|
||||||
disabled: false
|
disabled: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new {@code OverflowMenuItem} instance.
|
||||||
|
*
|
||||||
|
* @param {*} props - The read-only properties with which the new instance
|
||||||
|
* is to be initialized.
|
||||||
|
*/
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// Bind event handler so it is only bound once for every instance.
|
||||||
|
this._onKeyPress = this._onKeyPress.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onKeyPress: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onKeyPress(e) {
|
||||||
|
if (!this.props.disabled && this.props.onClick && (e.key === ' ' || e.key === 'Enter')) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -89,9 +118,13 @@ class OverflowMenuItem extends Component<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
|
aria-disabled = { disabled }
|
||||||
aria-label = { accessibilityLabel }
|
aria-label = { accessibilityLabel }
|
||||||
className = { className }
|
className = { className }
|
||||||
onClick = { disabled ? null : onClick }>
|
onClick = { disabled ? null : onClick }
|
||||||
|
onKeyPress = { this._onKeyPress }
|
||||||
|
role = 'menuitem'
|
||||||
|
tabIndex = { 0 }>
|
||||||
<span className = 'overflow-menu-item-icon'>
|
<span className = 'overflow-menu-item-icon'>
|
||||||
<Icon
|
<Icon
|
||||||
id = { iconId }
|
id = { iconId }
|
||||||
|
|
|
@ -36,6 +36,36 @@ type Props = {
|
||||||
* Additional styles.
|
* Additional styles.
|
||||||
*/
|
*/
|
||||||
styles?: Object,
|
styles?: Object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* aria label for the Icon.
|
||||||
|
*/
|
||||||
|
ariaLabel?: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* whether the element has a popup
|
||||||
|
*/
|
||||||
|
ariaHasPopup?: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* whether the element popup is expanded
|
||||||
|
*/
|
||||||
|
ariaExpanded?: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the element this button icon controls
|
||||||
|
*/
|
||||||
|
ariaControls?: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* keydown handler for icon.
|
||||||
|
*/
|
||||||
|
onIconKeyDown?: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the icon button
|
||||||
|
*/
|
||||||
|
iconId: string
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -51,7 +81,13 @@ export default function ToolboxButtonWithIcon(props: Props) {
|
||||||
iconDisabled,
|
iconDisabled,
|
||||||
iconTooltip,
|
iconTooltip,
|
||||||
onIconClick,
|
onIconClick,
|
||||||
styles
|
onIconKeyDown,
|
||||||
|
styles,
|
||||||
|
ariaLabel,
|
||||||
|
ariaHasPopup,
|
||||||
|
ariaControls,
|
||||||
|
ariaExpanded,
|
||||||
|
iconId
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const iconProps = {};
|
const iconProps = {};
|
||||||
|
@ -62,6 +98,12 @@ export default function ToolboxButtonWithIcon(props: Props) {
|
||||||
} else {
|
} else {
|
||||||
iconProps.className = 'settings-button-small-icon';
|
iconProps.className = 'settings-button-small-icon';
|
||||||
iconProps.onClick = onIconClick;
|
iconProps.onClick = onIconClick;
|
||||||
|
iconProps.onKeyDown = onIconKeyDown;
|
||||||
|
iconProps.role = 'button';
|
||||||
|
iconProps.tabIndex = 0;
|
||||||
|
iconProps.ariaControls = ariaControls;
|
||||||
|
iconProps.ariaExpanded = ariaExpanded;
|
||||||
|
iconProps.containerId = iconId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,6 +119,8 @@ export default function ToolboxButtonWithIcon(props: Props) {
|
||||||
position = 'top'>
|
position = 'top'>
|
||||||
<Icon
|
<Icon
|
||||||
{ ...iconProps }
|
{ ...iconProps }
|
||||||
|
ariaHasPopup = { ariaHasPopup }
|
||||||
|
ariaLabel = { ariaLabel }
|
||||||
size = { 9 }
|
size = { 9 }
|
||||||
src = { icon } />
|
src = { icon } />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -19,29 +19,21 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this._onKeyDown = this._onKeyDown.bind(this);
|
this._onKeyPress = this._onKeyPress.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onKeyDown: (Object) => void;
|
_onKeyPress: (Object) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles 'Enter' key on the button to trigger onClick for accessibility.
|
* Handles 'Enter' and Space key on the button to trigger onClick for accessibility.
|
||||||
* We should be handling Space onKeyUp but it conflicts with PTT.
|
|
||||||
*
|
*
|
||||||
* @param {Object} event - The key event.
|
* @param {Object} event - The key event.
|
||||||
* @private
|
* @private
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onKeyDown(event) {
|
_onKeyPress(event) {
|
||||||
// If the event coming to the dialog has been subject to preventDefault
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
// we don't handle it here.
|
|
||||||
if (event.defaultPrevented) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
|
||||||
this.props.onClick();
|
this.props.onClick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,9 +63,9 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
|
||||||
'aria-label': this.accessibilityLabel,
|
'aria-label': this.accessibilityLabel,
|
||||||
className: className + (disabled ? ' disabled' : ''),
|
className: className + (disabled ? ' disabled' : ''),
|
||||||
onClick: disabled ? undefined : onClick,
|
onClick: disabled ? undefined : onClick,
|
||||||
onKeyDown: this._onKeyDown,
|
onKeyPress: this._onKeyPress,
|
||||||
tabIndex: 0,
|
tabIndex: 0,
|
||||||
role: 'button'
|
role: showLabel ? 'menuitem' : 'button'
|
||||||
};
|
};
|
||||||
|
|
||||||
const elementType = showLabel ? 'li' : 'div';
|
const elementType = showLabel ? 'li' : 'div';
|
||||||
|
|
|
@ -55,6 +55,7 @@ class AddMeetingUrlButton extends Component<Props> {
|
||||||
|
|
||||||
// Bind event handler so it is only bound once for every instance.
|
// Bind event handler so it is only bound once for every instance.
|
||||||
this._onClick = this._onClick.bind(this);
|
this._onClick = this._onClick.bind(this);
|
||||||
|
this._onKeyPress = this._onKeyPress.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -67,7 +68,9 @@ class AddMeetingUrlButton extends Component<Props> {
|
||||||
<Tooltip content = { this.props.t('calendarSync.addMeetingURL') }>
|
<Tooltip content = { this.props.t('calendarSync.addMeetingURL') }>
|
||||||
<div
|
<div
|
||||||
className = 'button add-button'
|
className = 'button add-button'
|
||||||
onClick = { this._onClick }>
|
onClick = { this._onClick }
|
||||||
|
onKeyPress = { this._onKeyPress }
|
||||||
|
role = 'button'>
|
||||||
<Icon src = { IconAdd } />
|
<Icon src = { IconAdd } />
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -88,6 +91,22 @@ class AddMeetingUrlButton extends Component<Props> {
|
||||||
|
|
||||||
dispatch(updateCalendarEvent(eventId, calendarId));
|
dispatch(updateCalendarEvent(eventId, calendarId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onKeyPress: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onKeyPress(e) {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this._onClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate(connect()(AddMeetingUrlButton));
|
export default translate(connect()(AddMeetingUrlButton));
|
||||||
|
|
|
@ -72,6 +72,7 @@ class CalendarList extends AbstractPage<Props> {
|
||||||
this._getRenderListEmptyComponent
|
this._getRenderListEmptyComponent
|
||||||
= this._getRenderListEmptyComponent.bind(this);
|
= this._getRenderListEmptyComponent.bind(this);
|
||||||
this._onOpenSettings = this._onOpenSettings.bind(this);
|
this._onOpenSettings = this._onOpenSettings.bind(this);
|
||||||
|
this._onKeyPressOpenSettings = this._onKeyPressOpenSettings.bind(this);
|
||||||
this._onRefreshEvents = this._onRefreshEvents.bind(this);
|
this._onRefreshEvents = this._onRefreshEvents.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,7 +188,9 @@ class CalendarList extends AbstractPage<Props> {
|
||||||
return (
|
return (
|
||||||
<div className = 'meetings-list-empty'>
|
<div className = 'meetings-list-empty'>
|
||||||
<div className = 'meetings-list-empty-image'>
|
<div className = 'meetings-list-empty-image'>
|
||||||
<img src = './images/calendar.svg' />
|
<img
|
||||||
|
alt = { t('welcomepage.logo.calendar') }
|
||||||
|
src = './images/calendar.svg' />
|
||||||
</div>
|
</div>
|
||||||
<div className = 'description'>
|
<div className = 'description'>
|
||||||
{ t('welcomepage.connectCalendarText', {
|
{ t('welcomepage.connectCalendarText', {
|
||||||
|
@ -197,7 +200,9 @@ class CalendarList extends AbstractPage<Props> {
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className = 'meetings-list-empty-button'
|
className = 'meetings-list-empty-button'
|
||||||
onClick = { this._onOpenSettings }>
|
onClick = { this._onOpenSettings }
|
||||||
|
onKeyPress = { this._onKeyPressOpenSettings }
|
||||||
|
role = 'button'>
|
||||||
<Icon
|
<Icon
|
||||||
className = 'meetings-list-empty-icon'
|
className = 'meetings-list-empty-icon'
|
||||||
src = { IconPlusCalendar } />
|
src = { IconPlusCalendar } />
|
||||||
|
@ -221,6 +226,22 @@ class CalendarList extends AbstractPage<Props> {
|
||||||
this.props.dispatch(openSettingsDialog(SETTINGS_TABS.CALENDAR));
|
this.props.dispatch(openSettingsDialog(SETTINGS_TABS.CALENDAR));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onKeyPressOpenSettings: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onKeyPressOpenSettings(e) {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this._onOpenSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_onRefreshEvents: () => void;
|
_onRefreshEvents: () => void;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ class JoinButton extends Component<Props> {
|
||||||
|
|
||||||
// Bind event handler so it is only bound once for every instance.
|
// Bind event handler so it is only bound once for every instance.
|
||||||
this._onClick = this._onClick.bind(this);
|
this._onClick = this._onClick.bind(this);
|
||||||
|
this._onKeyPress = this._onKeyPress.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -60,7 +61,9 @@ class JoinButton extends Component<Props> {
|
||||||
content = { t('calendarSync.joinTooltip') }>
|
content = { t('calendarSync.joinTooltip') }>
|
||||||
<div
|
<div
|
||||||
className = 'button join-button'
|
className = 'button join-button'
|
||||||
onClick = { this._onClick }>
|
onClick = { this._onClick }
|
||||||
|
onKeyPress = { this._onKeyPress }
|
||||||
|
role = 'button'>
|
||||||
<Icon
|
<Icon
|
||||||
size = '14'
|
size = '14'
|
||||||
src = { IconAdd } />
|
src = { IconAdd } />
|
||||||
|
@ -81,6 +84,22 @@ class JoinButton extends Component<Props> {
|
||||||
_onClick(event) {
|
_onClick(event) {
|
||||||
this.props.onPress(event, this.props.url);
|
this.props.onPress(event, this.props.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onKeyPress: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onKeyPress(e) {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this._onClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate(JoinButton);
|
export default translate(JoinButton);
|
||||||
|
|
|
@ -2,17 +2,29 @@
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { translate } from '../../base/i18n';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the React {@code Component} props of
|
* The type of the React {@code Component} props of
|
||||||
* {@link MicrosoftSignInButton}.
|
* {@link MicrosoftSignInButton}.
|
||||||
*/
|
*/
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
// The callback to invoke when {@code MicrosoftSignInButton} is clicked.
|
/**
|
||||||
|
* The callback to invoke when {@code MicrosoftSignInButton} is clicked.
|
||||||
|
*/
|
||||||
onClick: Function,
|
onClick: Function,
|
||||||
|
|
||||||
// The text to display within {@code MicrosoftSignInButton}.
|
/**
|
||||||
text: string
|
* The text to display within {@code MicrosoftSignInButton}.
|
||||||
|
*/
|
||||||
|
text: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked to obtain translated strings.
|
||||||
|
*/
|
||||||
|
t: Function
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -20,7 +32,7 @@ type Props = {
|
||||||
*
|
*
|
||||||
* @extends Component
|
* @extends Component
|
||||||
*/
|
*/
|
||||||
export default class MicrosoftSignInButton extends Component<Props> {
|
class MicrosoftSignInButton extends Component<Props> {
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -33,6 +45,7 @@ export default class MicrosoftSignInButton extends Component<Props> {
|
||||||
className = 'microsoft-sign-in'
|
className = 'microsoft-sign-in'
|
||||||
onClick = { this.props.onClick }>
|
onClick = { this.props.onClick }>
|
||||||
<img
|
<img
|
||||||
|
alt = { this.props.t('welcomepage.logo.microsoftLogo') }
|
||||||
className = 'microsoft-logo'
|
className = 'microsoft-logo'
|
||||||
src = 'images/microsoftLogo.svg' />
|
src = 'images/microsoftLogo.svg' />
|
||||||
<div className = 'microsoft-cta'>
|
<div className = 'microsoft-cta'>
|
||||||
|
@ -42,3 +55,5 @@ export default class MicrosoftSignInButton extends Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default translate(MicrosoftSignInButton);
|
||||||
|
|
|
@ -4,6 +4,7 @@ import React from 'react';
|
||||||
|
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
|
import { toggleChat } from '../../actions.web';
|
||||||
import AbstractChat, {
|
import AbstractChat, {
|
||||||
_mapStateToProps,
|
_mapStateToProps,
|
||||||
type Props
|
type Props
|
||||||
|
@ -51,6 +52,8 @@ class Chat extends AbstractChat<Props> {
|
||||||
// Bind event handlers so they are only bound once for every instance.
|
// Bind event handlers so they are only bound once for every instance.
|
||||||
this._renderPanelContent = this._renderPanelContent.bind(this);
|
this._renderPanelContent = this._renderPanelContent.bind(this);
|
||||||
this._onChatInputResize = this._onChatInputResize.bind(this);
|
this._onChatInputResize = this._onChatInputResize.bind(this);
|
||||||
|
this._onEscClick = this._onEscClick.bind(this);
|
||||||
|
this._onToggleChat = this._onToggleChat.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -74,6 +77,21 @@ class Chat extends AbstractChat<Props> {
|
||||||
this._scrollMessageContainerToBottom(false);
|
this._scrollMessageContainerToBottom(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_onEscClick: (KeyboardEvent) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click handler for the chat sidenav.
|
||||||
|
*
|
||||||
|
* @param {KeyboardEvent} event - Esc key click to close the popup.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onEscClick(event) {
|
||||||
|
if (event.key === 'Escape' && this.props._isOpen) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this._onToggleChat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
|
@ -135,7 +153,10 @@ class Chat extends AbstractChat<Props> {
|
||||||
*/
|
*/
|
||||||
_renderChatHeader() {
|
_renderChatHeader() {
|
||||||
return (
|
return (
|
||||||
<Header className = 'chat-header' />
|
<Header
|
||||||
|
className = 'chat-header'
|
||||||
|
id = 'chat-header'
|
||||||
|
onCancel = { this._onToggleChat } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,8 +198,10 @@ class Chat extends AbstractChat<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
aria-haspopup = 'true'
|
||||||
className = { `sideToolbarContainer ${className}` }
|
className = { `sideToolbarContainer ${className}` }
|
||||||
id = 'sideToolbarContainer'>
|
id = 'sideToolbarContainer'
|
||||||
|
onKeyDown = { this._onEscClick } >
|
||||||
{ ComponentToRender }
|
{ ComponentToRender }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -199,6 +222,18 @@ class Chat extends AbstractChat<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSendMessage: (string) => void;
|
_onSendMessage: (string) => void;
|
||||||
|
|
||||||
|
_onToggleChat: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the chat window.
|
||||||
|
*
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
_onToggleChat() {
|
||||||
|
this.props.dispatch(toggleChat());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate(connect(_mapStateToProps)(Chat));
|
export default translate(connect(_mapStateToProps)(Chat));
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
import { Icon, IconClose } from '../../../base/icons';
|
import { Icon, IconClose } from '../../../base/icons';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
import { toggleChat } from '../../actions.web';
|
import { toggleChat } from '../../actions.web';
|
||||||
|
@ -17,6 +18,11 @@ type Props = {
|
||||||
* An optional class name.
|
* An optional class name.
|
||||||
*/
|
*/
|
||||||
className: string,
|
className: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked to obtain translated strings.
|
||||||
|
*/
|
||||||
|
t: Function
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,17 +30,31 @@ type Props = {
|
||||||
*
|
*
|
||||||
* @returns {React$Element<any>}
|
* @returns {React$Element<any>}
|
||||||
*/
|
*/
|
||||||
function Header({ onCancel, className }: Props) {
|
function Header({ onCancel, className, t }: Props) {
|
||||||
|
|
||||||
|
const onKeyPressHandler = useCallback(e => {
|
||||||
|
if (onCancel && (e.key === ' ' || e.key === 'Enter')) {
|
||||||
|
e.preventDefault();
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
}, [ onCancel ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className = { className || 'chat-dialog-header' }>
|
className = { className || 'chat-dialog-header' }
|
||||||
|
role = 'heading'>
|
||||||
|
{ t('chat.title') }
|
||||||
<Icon
|
<Icon
|
||||||
|
ariaLabel = { t('toolbar.closeChat') }
|
||||||
onClick = { onCancel }
|
onClick = { onCancel }
|
||||||
src = { IconClose } />
|
onKeyPress = { onKeyPressHandler }
|
||||||
|
role = 'button'
|
||||||
|
src = { IconClose }
|
||||||
|
tabIndex = { 0 } />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = { onCancel: toggleChat };
|
const mapDispatchToProps = { onCancel: toggleChat };
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(Header);
|
export default translate(connect(null, mapDispatchToProps)(Header));
|
||||||
|
|
|
@ -84,6 +84,9 @@ class ChatInput extends Component<Props, State> {
|
||||||
this._onSmileySelect = this._onSmileySelect.bind(this);
|
this._onSmileySelect = this._onSmileySelect.bind(this);
|
||||||
this._onSubmitMessage = this._onSubmitMessage.bind(this);
|
this._onSubmitMessage = this._onSubmitMessage.bind(this);
|
||||||
this._onToggleSmileysPanel = this._onToggleSmileysPanel.bind(this);
|
this._onToggleSmileysPanel = this._onToggleSmileysPanel.bind(this);
|
||||||
|
this._onEscHandler = this._onEscHandler.bind(this);
|
||||||
|
this._onToggleSmileysPanelKeyPress = this._onToggleSmileysPanelKeyPress.bind(this);
|
||||||
|
this._onSubmitMessageKeyPress = this._onSubmitMessageKeyPress.bind(this);
|
||||||
this._setTextAreaRef = this._setTextAreaRef.bind(this);
|
this._setTextAreaRef = this._setTextAreaRef.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,8 +119,15 @@ class ChatInput extends Component<Props, State> {
|
||||||
<div id = 'smileysarea'>
|
<div id = 'smileysarea'>
|
||||||
<div id = 'smileys'>
|
<div id = 'smileys'>
|
||||||
<div
|
<div
|
||||||
|
aria-expanded = { this.state.showSmileysPanel }
|
||||||
|
aria-haspopup = 'smileysContainer'
|
||||||
|
aria-label = { this.props.t('chat.smileysPanel') }
|
||||||
className = 'smiley-button'
|
className = 'smiley-button'
|
||||||
onClick = { this._onToggleSmileysPanel }>
|
onClick = { this._onToggleSmileysPanel }
|
||||||
|
onKeyDown = { this._onEscHandler }
|
||||||
|
onKeyPress = { this._onToggleSmileysPanelKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
<Icon src = { IconSmile } />
|
<Icon src = { IconSmile } />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -129,19 +139,26 @@ class ChatInput extends Component<Props, State> {
|
||||||
</div>
|
</div>
|
||||||
<div className = 'usrmsg-form'>
|
<div className = 'usrmsg-form'>
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
|
autoComplete = 'off'
|
||||||
|
autoFocus = { true }
|
||||||
id = 'usermsg'
|
id = 'usermsg'
|
||||||
inputRef = { this._setTextAreaRef }
|
|
||||||
maxRows = { 5 }
|
maxRows = { 5 }
|
||||||
onChange = { this._onMessageChange }
|
onChange = { this._onMessageChange }
|
||||||
onHeightChange = { this.props.onResize }
|
onHeightChange = { this.props.onResize }
|
||||||
onKeyDown = { this._onDetectSubmit }
|
onKeyDown = { this._onDetectSubmit }
|
||||||
placeholder = { this.props.t('chat.messagebox') }
|
placeholder = { this.props.t('chat.messagebox') }
|
||||||
|
ref = { this._setTextAreaRef }
|
||||||
|
tabIndex = { 0 }
|
||||||
value = { this.state.message } />
|
value = { this.state.message } />
|
||||||
</div>
|
</div>
|
||||||
<div className = 'send-button-container'>
|
<div className = 'send-button-container'>
|
||||||
<div
|
<div
|
||||||
|
aria-label = { this.props.t('chat.sendButton') }
|
||||||
className = 'send-button'
|
className = 'send-button'
|
||||||
onClick = { this._onSubmitMessage }>
|
onClick = { this._onSubmitMessage }
|
||||||
|
onKeyPress = { this._onSubmitMessageKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { this.state.message.trim() ? 0 : -1 } >
|
||||||
<Icon src = { IconPlane } />
|
<Icon src = { IconPlane } />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -192,14 +209,32 @@ class ChatInput extends Component<Props, State> {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onDetectSubmit(event) {
|
_onDetectSubmit(event) {
|
||||||
if (event.keyCode === 13
|
if (event.key === 'Enter'
|
||||||
&& event.shiftKey === false) {
|
&& event.shiftKey === false
|
||||||
|
&& event.ctrlKey === false) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
this._onSubmitMessage();
|
this._onSubmitMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onSubmitMessageKeyPress: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onSubmitMessageKeyPress(e) {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this._onSubmitMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_onMessageChange: (Object) => void;
|
_onMessageChange: (Object) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -224,10 +259,16 @@ class ChatInput extends Component<Props, State> {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onSmileySelect(smileyText) {
|
_onSmileySelect(smileyText) {
|
||||||
this.setState({
|
if (smileyText) {
|
||||||
message: `${this.state.message} ${smileyText}`,
|
this.setState({
|
||||||
showSmileysPanel: false
|
message: `${this.state.message} ${smileyText}`,
|
||||||
});
|
showSmileysPanel: false
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
showSmileysPanel: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this._focus();
|
this._focus();
|
||||||
}
|
}
|
||||||
|
@ -241,9 +282,44 @@ class ChatInput extends Component<Props, State> {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onToggleSmileysPanel() {
|
_onToggleSmileysPanel() {
|
||||||
|
if (this.state.showSmileysPanel) {
|
||||||
|
this._focus();
|
||||||
|
}
|
||||||
this.setState({ showSmileysPanel: !this.state.showSmileysPanel });
|
this.setState({ showSmileysPanel: !this.state.showSmileysPanel });
|
||||||
|
}
|
||||||
|
|
||||||
this._focus();
|
_onEscHandler: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onEscHandler(e) {
|
||||||
|
// Escape handling does not work in onKeyPress
|
||||||
|
if (this.state.showSmileysPanel && e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this._onToggleSmileysPanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onToggleSmileysPanelKeyPress: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onToggleSmileysPanelKeyPress(e) {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this._onToggleSmileysPanel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_setTextAreaRef: (?HTMLTextAreaElement) => void;
|
_setTextAreaRef: (?HTMLTextAreaElement) => void;
|
||||||
|
|
|
@ -23,7 +23,7 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { message } = this.props;
|
const { message, t } = this.props;
|
||||||
const processedMessage = [];
|
const processedMessage = [];
|
||||||
|
|
||||||
// content is an array of text and emoji components
|
// content is an array of text and emoji components
|
||||||
|
@ -38,12 +38,20 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className = 'chatmessage-wrapper'>
|
<div
|
||||||
|
className = 'chatmessage-wrapper'
|
||||||
|
tabIndex = { -1 }>
|
||||||
<div className = { `chatmessage ${message.privateMessage ? 'privatemessage' : ''}` }>
|
<div className = { `chatmessage ${message.privateMessage ? 'privatemessage' : ''}` }>
|
||||||
<div className = 'replywrapper'>
|
<div className = 'replywrapper'>
|
||||||
<div className = 'messagecontent'>
|
<div className = 'messagecontent'>
|
||||||
{ this.props.showDisplayName && this._renderDisplayName() }
|
{ this.props.showDisplayName && this._renderDisplayName() }
|
||||||
<div className = 'usermessage'>
|
<div className = 'usermessage'>
|
||||||
|
<span className = 'sr-only'>
|
||||||
|
{ this.props.message.displayName === this.props.message.recipient
|
||||||
|
? t('chat.messageAccessibleTitleMe')
|
||||||
|
: t('chat.messageAccessibleTitle',
|
||||||
|
{ user: this.props.message.displayName }) }
|
||||||
|
</span>
|
||||||
{ processedMessage }
|
{ processedMessage }
|
||||||
</div>
|
</div>
|
||||||
{ message.privateMessage && this._renderPrivateNotice() }
|
{ message.privateMessage && this._renderPrivateNotice() }
|
||||||
|
@ -77,7 +85,9 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
||||||
*/
|
*/
|
||||||
_renderDisplayName() {
|
_renderDisplayName() {
|
||||||
return (
|
return (
|
||||||
<div className = 'display-name'>
|
<div
|
||||||
|
aria-hidden = { true }
|
||||||
|
className = 'display-name'>
|
||||||
{ this.props.message.displayName }
|
{ this.props.message.displayName }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -59,6 +59,7 @@ class DisplayNameForm extends Component<Props, State> {
|
||||||
// Bind event handlers so they are only bound once for every instance.
|
// Bind event handlers so they are only bound once for every instance.
|
||||||
this._onDisplayNameChange = this._onDisplayNameChange.bind(this);
|
this._onDisplayNameChange = this._onDisplayNameChange.bind(this);
|
||||||
this._onSubmit = this._onSubmit.bind(this);
|
this._onSubmit = this._onSubmit.bind(this);
|
||||||
|
this._onKeyPress = this._onKeyPress.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -74,6 +75,8 @@ class DisplayNameForm extends Component<Props, State> {
|
||||||
<div id = 'nickname'>
|
<div id = 'nickname'>
|
||||||
<form onSubmit = { this._onSubmit }>
|
<form onSubmit = { this._onSubmit }>
|
||||||
<FieldTextStateless
|
<FieldTextStateless
|
||||||
|
aria-describedby = 'nickname-title'
|
||||||
|
autoComplete = 'name'
|
||||||
autoFocus = { true }
|
autoFocus = { true }
|
||||||
compact = { true }
|
compact = { true }
|
||||||
id = 'nickinput'
|
id = 'nickinput'
|
||||||
|
@ -86,7 +89,10 @@ class DisplayNameForm extends Component<Props, State> {
|
||||||
</form>
|
</form>
|
||||||
<div
|
<div
|
||||||
className = { `enter-chat${this.state.displayName.trim() ? '' : ' disabled'}` }
|
className = { `enter-chat${this.state.displayName.trim() ? '' : ' disabled'}` }
|
||||||
onClick = { this._onSubmit }>
|
onClick = { this._onSubmit }
|
||||||
|
onKeyPress = { this._onKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
{ t('chat.enter') }
|
{ t('chat.enter') }
|
||||||
</div>
|
</div>
|
||||||
<KeyboardAvoider />
|
<KeyboardAvoider />
|
||||||
|
@ -125,6 +131,21 @@ class DisplayNameForm extends Component<Props, State> {
|
||||||
displayName: this.state.displayName
|
displayName: this.state.displayName
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onKeyPress: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onKeyPress(e) {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
this._onSubmit(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate(connect()(DisplayNameForm));
|
export default translate(connect()(DisplayNameForm));
|
||||||
|
|
|
@ -70,9 +70,12 @@ export default class MessageContainer extends AbstractMessageContainer<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
aria-labelledby = 'chat-header'
|
||||||
id = 'chatconversation'
|
id = 'chatconversation'
|
||||||
onScroll = { this._onChatScroll }
|
onScroll = { this._onChatScroll }
|
||||||
ref = { this._messageListRef }>
|
ref = { this._messageListRef }
|
||||||
|
role = 'log'
|
||||||
|
tabIndex = { 0 }>
|
||||||
{ messages }
|
{ messages }
|
||||||
<div ref = { this._messagesListEndRef } />
|
<div ref = { this._messagesListEndRef } />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,6 +15,35 @@ import AbstractMessageRecipient, {
|
||||||
* Class to implement the displaying of the recipient of the next message.
|
* Class to implement the displaying of the recipient of the next message.
|
||||||
*/
|
*/
|
||||||
class MessageRecipient extends AbstractMessageRecipient<Props> {
|
class MessageRecipient extends AbstractMessageRecipient<Props> {
|
||||||
|
/**
|
||||||
|
* Initializes a new {@code MessageRecipient} instance.
|
||||||
|
*
|
||||||
|
* @param {*} props - The read-only properties with which the new instance
|
||||||
|
* is to be initialized.
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// Bind event handler so it is only bound once for every instance.
|
||||||
|
this._onKeyPress = this._onKeyPress.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onKeyPress: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onKeyPress(e) {
|
||||||
|
if (this.props._onRemovePrivateMessageRecipient && (e.key === ' ' || e.key === 'Enter')) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props._onRemovePrivateMessageRecipient();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements {@code PureComponent#render}.
|
* Implements {@code PureComponent#render}.
|
||||||
*
|
*
|
||||||
|
@ -30,13 +59,20 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id = 'chat-recipient'>
|
<div
|
||||||
|
id = 'chat-recipient'
|
||||||
|
role = 'alert'>
|
||||||
<span>
|
<span>
|
||||||
{ t('chat.messageTo', {
|
{ t('chat.messageTo', {
|
||||||
recipient: _privateMessageRecipient
|
recipient: _privateMessageRecipient
|
||||||
}) }
|
}) }
|
||||||
</span>
|
</span>
|
||||||
<div onClick = { this.props._onRemovePrivateMessageRecipient }>
|
<div
|
||||||
|
aria-label = { t('dialog.close') }
|
||||||
|
onClick = { this.props._onRemovePrivateMessageRecipient }
|
||||||
|
onKeyPress = { this._onKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
<Icon
|
<Icon
|
||||||
src = { IconCancelSelection } />
|
src = { IconCancelSelection } />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -23,6 +23,69 @@ type Props = {
|
||||||
* @extends Component
|
* @extends Component
|
||||||
*/
|
*/
|
||||||
class SmileysPanel extends PureComponent<Props> {
|
class SmileysPanel extends PureComponent<Props> {
|
||||||
|
/**
|
||||||
|
* Initializes a new {@code SmileysPanel} instance.
|
||||||
|
*
|
||||||
|
* @param {*} props - The read-only properties with which the new instance
|
||||||
|
* is to be initialized.
|
||||||
|
*/
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// Bind event handler so it is only bound once for every instance.
|
||||||
|
this._onClick = this._onClick.bind(this);
|
||||||
|
this._onKeyPress = this._onKeyPress.bind(this);
|
||||||
|
this._onEscKey = this._onEscKey.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onEscKey: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onEscKey(e) {
|
||||||
|
// Escape handling does not work in onKeyPress
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.props.onSmileySelect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onKeyPress: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onKeyPress(e) {
|
||||||
|
if (e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onSmileySelect(e.target.id && smileys[e.target.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onClick: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click handler for to select emoji.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onSmileySelect(e.currentTarget.id && smileys[e.currentTarget.id]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -30,40 +93,33 @@ class SmileysPanel extends PureComponent<Props> {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const smileyItems = Object.keys(smileys).map(smileyKey => {
|
const smileyItems = Object.keys(smileys).map(smileyKey => (
|
||||||
const onSelectFunction = this._getOnSmileySelectCallback(smileyKey);
|
<div
|
||||||
|
className = 'smileyContainer'
|
||||||
return (
|
id = { smileyKey }
|
||||||
<div
|
key = { smileyKey }
|
||||||
className = 'smileyContainer'
|
onClick = { this._onClick }
|
||||||
id = { smileyKey }
|
onKeyDown = { this._onEscKey }
|
||||||
key = { smileyKey }>
|
onKeyPress = { this._onKeyPress }
|
||||||
<Emoji
|
role = 'option'
|
||||||
onClick = { onSelectFunction }
|
tabIndex = { 0 }>
|
||||||
onlyEmojiClassName = 'smiley'
|
<Emoji
|
||||||
text = { smileys[smileyKey] } />
|
onlyEmojiClassName = 'smiley'
|
||||||
</div>
|
text = { smileys[smileyKey] } />
|
||||||
);
|
</div>
|
||||||
});
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id = 'smileysContainer'>
|
<div
|
||||||
|
aria-orientation = 'horizontal'
|
||||||
|
id = 'smileysContainer'
|
||||||
|
onKeyDown = { this._onEscKey }
|
||||||
|
role = 'listbox'
|
||||||
|
tabIndex = { -1 }>
|
||||||
{ smileyItems }
|
{ smileyItems }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper method to bind a smiley's click handler.
|
|
||||||
*
|
|
||||||
* @param {string} smileyKey - The key from the {@link smileys} object
|
|
||||||
* that should be added to the chat message.
|
|
||||||
* @private
|
|
||||||
* @returns {Function}
|
|
||||||
*/
|
|
||||||
_getOnSmileySelectCallback(smileyKey) {
|
|
||||||
return () => this.props.onSmileySelect(smileys[smileyKey]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SmileysPanel;
|
export default SmileysPanel;
|
||||||
|
|
|
@ -107,6 +107,8 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
|
||||||
this._onInstallExtensionClick = this._onInstallExtensionClick.bind(this);
|
this._onInstallExtensionClick = this._onInstallExtensionClick.bind(this);
|
||||||
this._shouldNotRender = this._shouldNotRender.bind(this);
|
this._shouldNotRender = this._shouldNotRender.bind(this);
|
||||||
this._onDontShowAgainChange = this._onDontShowAgainChange.bind(this);
|
this._onDontShowAgainChange = this._onDontShowAgainChange.bind(this);
|
||||||
|
this._onCloseKeyPress = this._onCloseKeyPress.bind(this);
|
||||||
|
this._onInstallExtensionKeyPress = this._onInstallExtensionKeyPress.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -169,6 +171,22 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
|
||||||
this.setState({ closePressed: true });
|
this.setState({ closePressed: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onCloseKeyPress: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onCloseKeyPress(e) {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this._onClosePressed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_onInstallExtensionClick: () => void;
|
_onInstallExtensionClick: () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -182,6 +200,22 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
|
||||||
this.setState({ closePressed: true });
|
this.setState({ closePressed: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onInstallExtensionKeyPress: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onInstallExtensionKeyPress(e) {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this._onClosePressed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_shouldNotRender: () => boolean;
|
_shouldNotRender: () => boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -236,16 +270,23 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className = { mainClassNames }>
|
<div className = { mainClassNames }>
|
||||||
<div className = 'chrome-extension-banner__container'>
|
<div
|
||||||
|
aria-aria-describedby = 'chrome-extension-banner__text-container'
|
||||||
|
className = 'chrome-extension-banner__container'
|
||||||
|
role = 'banner'>
|
||||||
|
<div className = 'chrome-extension-banner__icon-container' />
|
||||||
<div
|
<div
|
||||||
className = 'chrome-extension-banner__icon-container' />
|
className = 'chrome-extension-banner__text-container'
|
||||||
<div
|
id = 'chrome-extension-banner__text-container'>
|
||||||
className = 'chrome-extension-banner__text-container'>
|
|
||||||
{ t('chromeExtensionBanner.installExtensionText') }
|
{ t('chromeExtensionBanner.installExtensionText') }
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
aria-label = { t('chromeExtensionBanner.close') }
|
||||||
className = 'chrome-extension-banner__close-container'
|
className = 'chrome-extension-banner__close-container'
|
||||||
onClick = { this._onClosePressed }>
|
onClick = { this._onClosePressed }
|
||||||
|
onKeyPress = { this._onCloseKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
<Icon
|
<Icon
|
||||||
className = 'gray'
|
className = 'gray'
|
||||||
size = { 12 }
|
size = { 12 }
|
||||||
|
@ -255,18 +296,28 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
|
||||||
<div
|
<div
|
||||||
className = 'chrome-extension-banner__button-container'>
|
className = 'chrome-extension-banner__button-container'>
|
||||||
<div
|
<div
|
||||||
|
aria-labelledby = 'chrome-extension-banner__button-text'
|
||||||
className = 'chrome-extension-banner__button-open-url'
|
className = 'chrome-extension-banner__button-open-url'
|
||||||
onClick = { this._onInstallExtensionClick }>
|
onClick = { this._onInstallExtensionClick }
|
||||||
|
onKeyPress = { this._onInstallExtensionKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
<div
|
<div
|
||||||
className = 'chrome-extension-banner__button-text'>
|
className = 'chrome-extension-banner__button-text'
|
||||||
|
id = 'chrome-extension-banner__button-text'>
|
||||||
{ t('chromeExtensionBanner.buttonText') }
|
{ t('chromeExtensionBanner.buttonText') }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className = 'chrome-extension-banner__checkbox-container'>
|
<div className = 'chrome-extension-banner__checkbox-container'>
|
||||||
<label className = 'chrome-extension-banner__checkbox-label'>
|
<label
|
||||||
|
className = 'chrome-extension-banner__checkbox-label'
|
||||||
|
htmlFor = 'chrome-extension-banner__checkbox'
|
||||||
|
id = 'chrome-extension-banner__checkbox-label'>
|
||||||
<input
|
<input
|
||||||
|
aria-labelledby = 'chrome-extension-banner__checkbox-label'
|
||||||
checked = { this.state.dontShowAgainChecked }
|
checked = { this.state.dontShowAgainChecked }
|
||||||
|
id = 'chrome-extension-banner__checkbox'
|
||||||
onChange = { this._onDontShowAgainChange }
|
onChange = { this._onDontShowAgainChange }
|
||||||
type = 'checkbox' />
|
type = 'checkbox' />
|
||||||
{ t('chromeExtensionBanner.dontShowAgain') }
|
{ t('chromeExtensionBanner.dontShowAgain') }
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
import { Icon, IconInviteMore } from '../../../base/icons';
|
import { Icon, IconInviteMore } from '../../../base/icons';
|
||||||
|
@ -47,16 +47,28 @@ function InviteMore({
|
||||||
onClick,
|
onClick,
|
||||||
t
|
t
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const onKeyPressHandler = useCallback(e => {
|
||||||
|
if (onClick && (e.key === ' ' || e.key === 'Enter')) {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
}, [ onClick ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
_shouldShow
|
_shouldShow
|
||||||
? <div className = { `invite-more-container${_toolboxVisible ? '' : ' elevated'}` }>
|
? <div className = { `invite-more-container${_toolboxVisible ? '' : ' elevated'}` }>
|
||||||
<div className = 'invite-more-content'>
|
<div className = 'invite-more-content'>
|
||||||
<div className = 'invite-more-header'>
|
<div
|
||||||
|
className = 'invite-more-header'
|
||||||
|
role = 'heading'>
|
||||||
{t('addPeople.inviteMoreHeader')}
|
{t('addPeople.inviteMoreHeader')}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className = 'invite-more-button'
|
className = 'invite-more-button'
|
||||||
onClick = { onClick }>
|
onClick = { onClick }
|
||||||
|
onKeyPress = { onKeyPressHandler }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
<Icon src = { IconInviteMore } />
|
<Icon src = { IconInviteMore } />
|
||||||
<div className = 'invite-more-button-text'>
|
<div className = 'invite-more-button-text'>
|
||||||
{t('addPeople.inviteMorePrompt')}
|
{t('addPeople.inviteMorePrompt')}
|
||||||
|
|
|
@ -572,7 +572,9 @@ class ConnectionStatsTable extends Component<Props> {
|
||||||
<span>
|
<span>
|
||||||
<a
|
<a
|
||||||
className = 'savelogs link'
|
className = 'savelogs link'
|
||||||
onClick = { this.props.onSaveLogs } >
|
onClick = { this.props.onSaveLogs }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
{ this.props.t('connectionindicator.savelogs') }
|
{ this.props.t('connectionindicator.savelogs') }
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
|
@ -597,7 +599,9 @@ class ConnectionStatsTable extends Component<Props> {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className = 'showmore link'
|
className = 'showmore link'
|
||||||
onClick = { this.props.onShowMore } >
|
onClick = { this.props.onShowMore }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
{ this.props.t(translationKey) }
|
{ this.props.t(translationKey) }
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
|
@ -87,6 +87,7 @@ class DeepLinkingDesktopPage<P : Props> extends Component<P> {
|
||||||
HIDE_DEEP_LINKING_LOGO
|
HIDE_DEEP_LINKING_LOGO
|
||||||
? null
|
? null
|
||||||
: <img
|
: <img
|
||||||
|
alt = { t('welcomepage.logo.logoDeepLinking') }
|
||||||
className = 'logo'
|
className = 'logo'
|
||||||
src = 'images/logo-deep-linking.png' />
|
src = 'images/logo-deep-linking.png' />
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,6 +119,7 @@ class DeepLinkingMobilePage extends Component<Props> {
|
||||||
HIDE_DEEP_LINKING_LOGO
|
HIDE_DEEP_LINKING_LOGO
|
||||||
? null
|
? null
|
||||||
: <img
|
: <img
|
||||||
|
alt = { t('welcomepage.logo.logoDeepLinking') }
|
||||||
className = 'logo'
|
className = 'logo'
|
||||||
src = 'images/logo-deep-linking.png' />
|
src = 'images/logo-deep-linking.png' />
|
||||||
}
|
}
|
||||||
|
@ -127,6 +128,7 @@ class DeepLinkingMobilePage extends Component<Props> {
|
||||||
{
|
{
|
||||||
SHOW_DEEP_LINKING_IMAGE
|
SHOW_DEEP_LINKING_IMAGE
|
||||||
? <img
|
? <img
|
||||||
|
alt = { t('welcomepage.logo.logoDeepLinking') }
|
||||||
className = 'image'
|
className = 'image'
|
||||||
src = 'images/deep-linking-image.png' />
|
src = 'images/deep-linking-image.png' />
|
||||||
: null
|
: null
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { translate } from '../../base/i18n';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the React {@code Component} props of
|
* The type of the React {@code Component} props of
|
||||||
* {@link DesktopSourcePreview}.
|
* {@link DesktopSourcePreview}.
|
||||||
|
@ -35,7 +38,12 @@ type Props = {
|
||||||
/**
|
/**
|
||||||
* The source type of the DesktopCapturerSources to display.
|
* The source type of the DesktopCapturerSources to display.
|
||||||
*/
|
*/
|
||||||
type: string
|
type: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked to obtain translated strings.
|
||||||
|
*/
|
||||||
|
t: Function
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -74,6 +82,7 @@ class DesktopSourcePreview extends Component<Props> {
|
||||||
onDoubleClick = { this._onDoubleClick }>
|
onDoubleClick = { this._onDoubleClick }>
|
||||||
<div className = 'desktop-source-preview-image-container'>
|
<div className = 'desktop-source-preview-image-container'>
|
||||||
<img
|
<img
|
||||||
|
alt = { this.props.t('welcomepage.logo.desktopPreviewThumbnail') }
|
||||||
className = 'desktop-source-preview-thumbnail'
|
className = 'desktop-source-preview-thumbnail'
|
||||||
src = { this.props.source.thumbnail.toDataURL() } />
|
src = { this.props.source.thumbnail.toDataURL() } />
|
||||||
</div>
|
</div>
|
||||||
|
@ -111,4 +120,4 @@ class DesktopSourcePreview extends Component<Props> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DesktopSourcePreview;
|
export default translate(DesktopSourcePreview);
|
||||||
|
|
|
@ -44,6 +44,7 @@ class AudioOutputPreview extends Component<Props> {
|
||||||
|
|
||||||
this._audioElementReady = this._audioElementReady.bind(this);
|
this._audioElementReady = this._audioElementReady.bind(this);
|
||||||
this._onClick = this._onClick.bind(this);
|
this._onClick = this._onClick.bind(this);
|
||||||
|
this._onKeyPress = this._onKeyPress.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,7 +67,12 @@ class AudioOutputPreview extends Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className = 'audio-output-preview'>
|
<div className = 'audio-output-preview'>
|
||||||
<a onClick = { this._onClick }>
|
<a
|
||||||
|
aria-label = { this.props.t('deviceSelection.testAudio') }
|
||||||
|
onClick = { this._onClick }
|
||||||
|
onKeyPress = { this._onKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
{ this.props.t('deviceSelection.testAudio') }
|
{ this.props.t('deviceSelection.testAudio') }
|
||||||
</a>
|
</a>
|
||||||
<Audio
|
<Audio
|
||||||
|
@ -105,6 +111,22 @@ class AudioOutputPreview extends Component<Props> {
|
||||||
&& this._audioElement.play();
|
&& this._audioElement.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onKeyPress: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onKeyPress(e) {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this._onClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the target output device for playing the test sound.
|
* Updates the target output device for playing the test sound.
|
||||||
*
|
*
|
||||||
|
|
|
@ -232,7 +232,9 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
|
||||||
track = { this.state.previewAudioTrack } /> }
|
track = { this.state.previewAudioTrack } /> }
|
||||||
</div>
|
</div>
|
||||||
<div className = 'device-selection-column column-selectors'>
|
<div className = 'device-selection-column column-selectors'>
|
||||||
<div className = 'device-selectors'>
|
<div
|
||||||
|
aria-live = 'polite all'
|
||||||
|
className = 'device-selectors'>
|
||||||
{ this._renderSelectors() }
|
{ this._renderSelectors() }
|
||||||
</div>
|
</div>
|
||||||
{ !hideAudioOutputSelect
|
{ !hideAudioOutputSelect
|
||||||
|
@ -344,9 +346,11 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
|
||||||
_renderSelector(deviceSelectorProps) {
|
_renderSelector(deviceSelectorProps) {
|
||||||
return (
|
return (
|
||||||
<div key = { deviceSelectorProps.label }>
|
<div key = { deviceSelectorProps.label }>
|
||||||
<div className = 'device-selector-label'>
|
<label
|
||||||
|
className = 'device-selector-label'
|
||||||
|
htmlFor = { deviceSelectorProps.id }>
|
||||||
{ this.props.t(deviceSelectorProps.label) }
|
{ this.props.t(deviceSelectorProps.label) }
|
||||||
</div>
|
</label>
|
||||||
<DeviceSelector { ...deviceSelectorProps } />
|
<DeviceSelector { ...deviceSelectorProps } />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -370,6 +374,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
|
||||||
isDisabled: this.props.disableAudioInputChange
|
isDisabled: this.props.disableAudioInputChange
|
||||||
|| this.props.disableDeviceChange,
|
|| this.props.disableDeviceChange,
|
||||||
key: 'audioInput',
|
key: 'audioInput',
|
||||||
|
id: 'audioInput',
|
||||||
label: 'settings.selectMic',
|
label: 'settings.selectMic',
|
||||||
onSelect: selectedAudioInputId =>
|
onSelect: selectedAudioInputId =>
|
||||||
super._onChange({ selectedAudioInputId }),
|
super._onChange({ selectedAudioInputId }),
|
||||||
|
@ -385,6 +390,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
|
||||||
icon: 'icon-camera',
|
icon: 'icon-camera',
|
||||||
isDisabled: this.props.disableDeviceChange,
|
isDisabled: this.props.disableDeviceChange,
|
||||||
key: 'videoInput',
|
key: 'videoInput',
|
||||||
|
id: 'videoInput',
|
||||||
label: 'settings.selectCamera',
|
label: 'settings.selectCamera',
|
||||||
onSelect: selectedVideoInputId =>
|
onSelect: selectedVideoInputId =>
|
||||||
super._onChange({ selectedVideoInputId }),
|
super._onChange({ selectedVideoInputId }),
|
||||||
|
@ -400,6 +406,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
|
||||||
icon: 'icon-speaker',
|
icon: 'icon-speaker',
|
||||||
isDisabled: this.props.disableDeviceChange,
|
isDisabled: this.props.disableDeviceChange,
|
||||||
key: 'audioOutput',
|
key: 'audioOutput',
|
||||||
|
id: 'audioOutput',
|
||||||
label: 'settings.selectAudioOutput',
|
label: 'settings.selectAudioOutput',
|
||||||
onSelect: selectedAudioOutputId =>
|
onSelect: selectedAudioOutputId =>
|
||||||
super._onChange({ selectedAudioOutputId }),
|
super._onChange({ selectedAudioOutputId }),
|
||||||
|
|
|
@ -51,7 +51,12 @@ type Props = {
|
||||||
/**
|
/**
|
||||||
* Invoked to obtain translated strings.
|
* Invoked to obtain translated strings.
|
||||||
*/
|
*/
|
||||||
t: Function
|
t: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the dropdown element
|
||||||
|
*/
|
||||||
|
id: string
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -81,6 +86,10 @@ class DeviceSelector extends Component<Props> {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
|
if (this.props.hasPermission === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.props.hasPermission) {
|
if (!this.props.hasPermission) {
|
||||||
return this._renderNoPermission();
|
return this._renderNoPermission();
|
||||||
}
|
}
|
||||||
|
@ -134,14 +143,10 @@ class DeviceSelector extends Component<Props> {
|
||||||
_createDropdownItem(device) {
|
_createDropdownItem(device) {
|
||||||
return (
|
return (
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
|
data-deviceid = { device.deviceId }
|
||||||
|
isSelected = { device.deviceId === this.props.selectedDeviceId }
|
||||||
key = { device.deviceId }
|
key = { device.deviceId }
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
onClick = { this._onSelect }>
|
||||||
onClick = {
|
|
||||||
e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
this._onSelect(device.deviceId);
|
|
||||||
}
|
|
||||||
}>
|
|
||||||
{ device.label || device.deviceId }
|
{ device.label || device.deviceId }
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
);
|
);
|
||||||
|
@ -183,7 +188,8 @@ class DeviceSelector extends Component<Props> {
|
||||||
shouldFitContainer = { true }
|
shouldFitContainer = { true }
|
||||||
trigger = { triggerText }
|
trigger = { triggerText }
|
||||||
triggerButtonProps = {{
|
triggerButtonProps = {{
|
||||||
shouldFitContainer: true
|
shouldFitContainer: true,
|
||||||
|
id: this.props.id
|
||||||
}}
|
}}
|
||||||
triggerType = 'button'>
|
triggerType = 'button'>
|
||||||
<DropdownItemGroup>
|
<DropdownItemGroup>
|
||||||
|
@ -199,13 +205,16 @@ class DeviceSelector extends Component<Props> {
|
||||||
/**
|
/**
|
||||||
* Invokes the passed in callback to notify of selection changes.
|
* Invokes the passed in callback to notify of selection changes.
|
||||||
*
|
*
|
||||||
* @param {Object} newDeviceId - Selected device id from DropdownMenu option.
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
* @private
|
* @private
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onSelect(newDeviceId) {
|
_onSelect(e) {
|
||||||
if (this.props.selectedDeviceId !== newDeviceId) {
|
const deviceId = e.currentTarget.getAttribute('data-deviceid');
|
||||||
this.props.onSelect(newDeviceId);
|
|
||||||
|
if (this.props.selectedDeviceId !== deviceId) {
|
||||||
|
this.props.onSelect(deviceId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -85,6 +85,7 @@ class E2EESection extends Component<Props, State> {
|
||||||
|
|
||||||
// Bind event handlers so they are only bound once for every instance.
|
// Bind event handlers so they are only bound once for every instance.
|
||||||
this._onExpand = this._onExpand.bind(this);
|
this._onExpand = this._onExpand.bind(this);
|
||||||
|
this._onExpandKeyPress = this._onExpandKeyPress.bind(this);
|
||||||
this._onToggle = this._onToggle.bind(this);
|
this._onToggle = this._onToggle.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,12 +102,20 @@ class E2EESection extends Component<Props, State> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id = 'e2ee-section'>
|
<div id = 'e2ee-section'>
|
||||||
<p className = 'description'>
|
<p
|
||||||
|
aria-live = 'polite'
|
||||||
|
className = 'description'
|
||||||
|
id = 'e2ee-section-description'>
|
||||||
{ expand && description }
|
{ expand && description }
|
||||||
{ !expand && description.substring(0, 100) }
|
{ !expand && description.substring(0, 100) }
|
||||||
{ !expand && <span
|
{ !expand && <span
|
||||||
|
aria-controls = 'e2ee-section-description'
|
||||||
|
aria-expanded = { expand }
|
||||||
className = 'read-more'
|
className = 'read-more'
|
||||||
onClick = { this._onExpand }>
|
onClick = { this._onExpand }
|
||||||
|
onKeyPress = { this._onExpandKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
... { t('dialog.readMore') }
|
... { t('dialog.readMore') }
|
||||||
</span> }
|
</span> }
|
||||||
</p>
|
</p>
|
||||||
|
@ -142,6 +151,22 @@ class E2EESection extends Component<Props, State> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onExpandKeyPress: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onExpandKeyPress(e) {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this._onExpand();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_onToggle: () => void;
|
_onToggle: () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -44,10 +44,12 @@ function EmbedMeeting({ t, url }: Props) {
|
||||||
width = 'small'>
|
width = 'small'>
|
||||||
<div className = 'embed-meeting-dialog'>
|
<div className = 'embed-meeting-dialog'>
|
||||||
<textarea
|
<textarea
|
||||||
|
aria-label = { t('dialog.embedMeeting') }
|
||||||
className = 'embed-meeting-code'
|
className = 'embed-meeting-code'
|
||||||
readOnly = { true }
|
readOnly = { true }
|
||||||
value = { getEmbedCode() } />
|
value = { getEmbedCode() } />
|
||||||
<CopyButton
|
<CopyButton
|
||||||
|
aria-label = { t('addPeople.copyLink') }
|
||||||
className = 'embed-meeting-copy'
|
className = 'embed-meeting-copy'
|
||||||
displayedText = { t('dialog.copy') }
|
displayedText = { t('dialog.copy') }
|
||||||
textOnCopySuccess = { t('dialog.copied') }
|
textOnCopySuccess = { t('dialog.copied') }
|
||||||
|
|
|
@ -36,10 +36,28 @@ function EmbedMeetingTrigger({ t, openEmbedDialog }: Props) {
|
||||||
openEmbedDialog(EmbedMeetingDialog);
|
openEmbedDialog(EmbedMeetingDialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {React.KeyboardEventHandler<HTMLDivElement>} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function onKeyPress(e) {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
aria-label = { t('embedMeeting.title') }
|
||||||
className = 'embed-meeting-trigger'
|
className = 'embed-meeting-trigger'
|
||||||
onClick = { onClick }>
|
onClick = { onClick }
|
||||||
|
onKeyPress = { onKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
{t('embedMeeting.title')}
|
{t('embedMeeting.title')}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -154,6 +154,12 @@ class FeedbackDialog extends Component<Props, State> {
|
||||||
this._scoreClickConfigurations = SCORES.map((textKey, index) => {
|
this._scoreClickConfigurations = SCORES.map((textKey, index) => {
|
||||||
return {
|
return {
|
||||||
_onClick: () => this._onScoreSelect(index),
|
_onClick: () => this._onScoreSelect(index),
|
||||||
|
_onKeyPres: e => {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this._onScoreSelect(index);
|
||||||
|
}
|
||||||
|
},
|
||||||
_onMouseOver: () => this._onScoreMouseOver(index)
|
_onMouseOver: () => this._onScoreMouseOver(index)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -200,6 +206,8 @@ class FeedbackDialog extends Component<Props, State> {
|
||||||
const scoreToDisplayAsSelected
|
const scoreToDisplayAsSelected
|
||||||
= mousedOverScore > -1 ? mousedOverScore : score;
|
= mousedOverScore > -1 ? mousedOverScore : score;
|
||||||
|
|
||||||
|
const { t } = this.props;
|
||||||
|
|
||||||
const scoreIcons = this._scoreClickConfigurations.map(
|
const scoreIcons = this._scoreClickConfigurations.map(
|
||||||
(config, index) => {
|
(config, index) => {
|
||||||
const isFilled = index <= scoreToDisplayAsSelected;
|
const isFilled = index <= scoreToDisplayAsSelected;
|
||||||
|
@ -208,11 +216,15 @@ class FeedbackDialog extends Component<Props, State> {
|
||||||
= `star-btn ${scoreAnimationClass} ${activeClass}`;
|
= `star-btn ${scoreAnimationClass} ${activeClass}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<span
|
||||||
|
aria-label = { t(SCORES[index]) }
|
||||||
className = { className }
|
className = { className }
|
||||||
key = { index }
|
key = { index }
|
||||||
onClick = { config._onClick }
|
onClick = { config._onClick }
|
||||||
onMouseOver = { config._onMouseOver }>
|
onKeyPress = { config._onKeyPres }
|
||||||
|
onMouseOver = { config._onMouseOver }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
{ isFilled
|
{ isFilled
|
||||||
? <StarFilledIcon
|
? <StarFilledIcon
|
||||||
label = 'star-filled'
|
label = 'star-filled'
|
||||||
|
@ -220,11 +232,10 @@ class FeedbackDialog extends Component<Props, State> {
|
||||||
: <StarIcon
|
: <StarIcon
|
||||||
label = 'star'
|
label = 'star'
|
||||||
size = 'xlarge' /> }
|
size = 'xlarge' /> }
|
||||||
</a>
|
</span>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
@ -234,7 +245,9 @@ class FeedbackDialog extends Component<Props, State> {
|
||||||
titleKey = 'feedback.rateExperience'>
|
titleKey = 'feedback.rateExperience'>
|
||||||
<div className = 'feedback-dialog'>
|
<div className = 'feedback-dialog'>
|
||||||
<div className = 'rating'>
|
<div className = 'rating'>
|
||||||
<div className = 'star-label'>
|
<div
|
||||||
|
aria-label = { this.props.t('feedback.star') }
|
||||||
|
className = 'star-label' >
|
||||||
<p id = 'starLabel'>
|
<p id = 'starLabel'>
|
||||||
{ t(SCORES[scoreToDisplayAsSelected]) }
|
{ t(SCORES[scoreToDisplayAsSelected]) }
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -13,7 +13,8 @@ import { translate } from '../../../base/i18n';
|
||||||
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
|
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
|
||||||
import { getLocalParticipant } from '../../../base/participants';
|
import { getLocalParticipant } from '../../../base/participants';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
import { isButtonEnabled } from '../../../toolbox/functions.web';
|
import { showToolbox } from '../../../toolbox/actions.web';
|
||||||
|
import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
|
||||||
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
|
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
|
||||||
import { setFilmstripVisible } from '../../actions';
|
import { setFilmstripVisible } from '../../actions';
|
||||||
import { shouldRemoteVideosBeVisible } from '../../functions';
|
import { shouldRemoteVideosBeVisible } from '../../functions';
|
||||||
|
@ -83,6 +84,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
_visible: boolean,
|
_visible: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the toolbox is displayed.
|
||||||
|
*/
|
||||||
|
_isToolboxVisible: Boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The redux {@code dispatch} function.
|
* The redux {@code dispatch} function.
|
||||||
*/
|
*/
|
||||||
|
@ -114,6 +120,7 @@ class Filmstrip extends Component <Props> {
|
||||||
// Bind event handlers so they are only bound once for every instance.
|
// Bind event handlers so they are only bound once for every instance.
|
||||||
this._onShortcutToggleFilmstrip = this._onShortcutToggleFilmstrip.bind(this);
|
this._onShortcutToggleFilmstrip = this._onShortcutToggleFilmstrip.bind(this);
|
||||||
this._onToolbarToggleFilmstrip = this._onToolbarToggleFilmstrip.bind(this);
|
this._onToolbarToggleFilmstrip = this._onToolbarToggleFilmstrip.bind(this);
|
||||||
|
this._onTabIn = this._onTabIn.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -238,6 +245,19 @@ class Filmstrip extends Component <Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onTabIn: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the toolbar visibility when tabbing into it.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onTabIn() {
|
||||||
|
if (!this.props._isToolboxVisible && this.props._visible) {
|
||||||
|
this.props.dispatch(showToolbox());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatches an action to change the visibility of the filmstrip.
|
* Dispatches an action to change the visibility of the filmstrip.
|
||||||
*
|
*
|
||||||
|
@ -298,12 +318,18 @@ class Filmstrip extends Component <Props> {
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className = 'filmstrip__toolbar'>
|
<div
|
||||||
|
className = 'filmstrip__toolbar'>
|
||||||
<button
|
<button
|
||||||
|
aria-expanded = { this.props._visible }
|
||||||
aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }
|
aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }
|
||||||
id = 'toggleFilmstripButton'
|
id = 'toggleFilmstripButton'
|
||||||
onClick = { this._onToolbarToggleFilmstrip }>
|
onClick = { this._onToolbarToggleFilmstrip }
|
||||||
<Icon src = { icon } />
|
onFocus = { this._onTabIn }
|
||||||
|
tabIndex = { 0 }>
|
||||||
|
<Icon
|
||||||
|
aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }
|
||||||
|
src = { icon } />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -342,7 +368,8 @@ function _mapStateToProps(state) {
|
||||||
_participants: state['features/base/participants'],
|
_participants: state['features/base/participants'],
|
||||||
_rows: gridDimensions.rows,
|
_rows: gridDimensions.rows,
|
||||||
_videosClassName: videosClassName,
|
_videosClassName: videosClassName,
|
||||||
_visible: visible
|
_visible: visible,
|
||||||
|
_isToolboxVisible: isToolboxVisible(state)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -737,12 +737,12 @@ class Thumbnail extends Component<Props, State> {
|
||||||
participantID = { id } />
|
participantID = { id } />
|
||||||
</div>
|
</div>
|
||||||
{ this._renderAvatar(styles.avatar) }
|
{ this._renderAvatar(styles.avatar) }
|
||||||
<span className = 'localvideomenu'>
|
|
||||||
<LocalVideoMenuTriggerButton />
|
|
||||||
</span>
|
|
||||||
<span className = 'audioindicator-container'>
|
<span className = 'audioindicator-container'>
|
||||||
<AudioLevelIndicator audioLevel = { audioLevel } />
|
<AudioLevelIndicator audioLevel = { audioLevel } />
|
||||||
</span>
|
</span>
|
||||||
|
<span className = 'localvideomenu'>
|
||||||
|
<LocalVideoMenuTriggerButton />
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -867,15 +867,15 @@ class Thumbnail extends Component<Props, State> {
|
||||||
className = 'presence-label'
|
className = 'presence-label'
|
||||||
participantID = { id } />
|
participantID = { id } />
|
||||||
</div>
|
</div>
|
||||||
|
<span className = 'audioindicator-container'>
|
||||||
|
<AudioLevelIndicator audioLevel = { audioLevel } />
|
||||||
|
</span>
|
||||||
<span className = 'remotevideomenu'>
|
<span className = 'remotevideomenu'>
|
||||||
<RemoteVideoMenuTriggerButton
|
<RemoteVideoMenuTriggerButton
|
||||||
initialVolumeValue = { volume }
|
initialVolumeValue = { volume }
|
||||||
onVolumeChange = { onVolumeChange }
|
onVolumeChange = { onVolumeChange }
|
||||||
participantID = { id } />
|
participantID = { id } />
|
||||||
</span>
|
</span>
|
||||||
<span className = 'audioindicator-container'>
|
|
||||||
<AudioLevelIndicator audioLevel = { audioLevel } />
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ class GoogleSignInButton extends AbstractGoogleSignInButton {
|
||||||
className = 'google-sign-in'
|
className = 'google-sign-in'
|
||||||
onClick = { this.props.onClick }>
|
onClick = { this.props.onClick }>
|
||||||
<img
|
<img
|
||||||
|
alt = { t('welcomepage.logo.googleLogo') }
|
||||||
className = 'google-logo'
|
className = 'google-logo'
|
||||||
src = 'images/googleLogo.svg' />
|
src = 'images/googleLogo.svg' />
|
||||||
<div className = 'google-cta'>
|
<div className = 'google-cta'>
|
||||||
|
|
|
@ -28,10 +28,12 @@ type Props = {
|
||||||
function CopyMeetingLinkSection({ t, url }: Props) {
|
function CopyMeetingLinkSection({ t, url }: Props) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span>{t('addPeople.shareLink')}</span>
|
<label htmlFor = { 'copy-button-id' }>{t('addPeople.shareLink')}</label>
|
||||||
<CopyButton
|
<CopyButton
|
||||||
|
aria-label = { t('addPeople.copyLink') }
|
||||||
className = 'invite-more-dialog-conference-url'
|
className = 'invite-more-dialog-conference-url'
|
||||||
displayedText = { getDecodedURI(url) }
|
displayedText = { getDecodedURI(url) }
|
||||||
|
id = 'copy-button-id'
|
||||||
textOnCopySuccess = { t('addPeople.linkCopied') }
|
textOnCopySuccess = { t('addPeople.linkCopied') }
|
||||||
textOnHover = { t('addPeople.copyLink') }
|
textOnHover = { t('addPeople.copyLink') }
|
||||||
textToCopy = { url } />
|
textToCopy = { url } />
|
||||||
|
|
|
@ -49,6 +49,7 @@ class DialInNumber extends Component<Props> {
|
||||||
|
|
||||||
// Bind event handler so it is only bound once for every instance.
|
// Bind event handler so it is only bound once for every instance.
|
||||||
this._onCopyText = this._onCopyText.bind(this);
|
this._onCopyText = this._onCopyText.bind(this);
|
||||||
|
this._onCopyTextKeyPress = this._onCopyTextKeyPress.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCopyText: () => void;
|
_onCopyText: () => void;
|
||||||
|
@ -68,6 +69,22 @@ class DialInNumber extends Component<Props> {
|
||||||
copyText(textToCopy);
|
copyText(textToCopy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onCopyTextKeyPress: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onCopyTextKeyPress(e) {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this._onCopyText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -101,8 +118,12 @@ class DialInNumber extends Component<Props> {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
|
aria-label = { t('info.copyNumber') }
|
||||||
className = 'dial-in-copy'
|
className = 'dial-in-copy'
|
||||||
onClick = { this._onCopyText }>
|
onClick = { this._onCopyText }
|
||||||
|
onKeyPress = { this._onCopyTextKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
<Icon src = { IconCopy } />
|
<Icon src = { IconCopy } />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -52,6 +52,20 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
|
||||||
copyText(inviteText);
|
copyText(inviteText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the conference invitation to the clipboard.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function _onCopyTextKeyPress(e) {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
copyText(inviteText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles the email invite drawer.
|
* Toggles the email invite drawer.
|
||||||
*
|
*
|
||||||
|
@ -61,6 +75,20 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
|
||||||
setIsActive(!isActive);
|
setIsActive(!isActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the email invite drawer.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function _onToggleActiveStateKeyPress(e) {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsActive(!isActive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders clickable elements that each open an email client
|
* Renders clickable elements that each open an email client
|
||||||
* containing a conference invite.
|
* containing a conference invite.
|
||||||
|
@ -101,6 +129,7 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
|
||||||
key = { idx }
|
key = { idx }
|
||||||
position = 'top'>
|
position = 'top'>
|
||||||
<a
|
<a
|
||||||
|
aria-label = { t(tooltipKey) }
|
||||||
className = 'provider-icon'
|
className = 'provider-icon'
|
||||||
href = { url }
|
href = { url }
|
||||||
rel = 'noopener noreferrer'
|
rel = 'noopener noreferrer'
|
||||||
|
@ -119,8 +148,13 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
|
aria-expanded = { isActive }
|
||||||
|
aria-label = { t('addPeople.shareInvite') }
|
||||||
className = { `invite-more-dialog email-container${isActive ? ' active' : ''}` }
|
className = { `invite-more-dialog email-container${isActive ? ' active' : ''}` }
|
||||||
onClick = { _onToggleActiveState }>
|
onClick = { _onToggleActiveState }
|
||||||
|
onKeyPress = { _onToggleActiveStateKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
<span>{t('addPeople.shareInvite')}</span>
|
<span>{t('addPeople.shareInvite')}</span>
|
||||||
<Icon src = { IconArrowDownSmall } />
|
<Icon src = { IconArrowDownSmall } />
|
||||||
</div>
|
</div>
|
||||||
|
@ -129,8 +163,12 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
|
||||||
content = { t('addPeople.copyInvite') }
|
content = { t('addPeople.copyInvite') }
|
||||||
position = 'top'>
|
position = 'top'>
|
||||||
<div
|
<div
|
||||||
|
aria-label = { t('addPeople.copyInvite') }
|
||||||
className = 'copy-invite-icon'
|
className = 'copy-invite-icon'
|
||||||
onClick = { _onCopyText }>
|
onClick = { _onCopyText }
|
||||||
|
onKeyPress = { _onCopyTextKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
<Icon src = { IconCopy } />
|
<Icon src = { IconCopy } />
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -76,9 +76,11 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||||
|
|
||||||
// Bind event handlers so they are only bound once per instance.
|
// Bind event handlers so they are only bound once per instance.
|
||||||
this._onClearItems = this._onClearItems.bind(this);
|
this._onClearItems = this._onClearItems.bind(this);
|
||||||
|
this._onClearItemsKeyPress = this._onClearItemsKeyPress.bind(this);
|
||||||
this._onItemSelected = this._onItemSelected.bind(this);
|
this._onItemSelected = this._onItemSelected.bind(this);
|
||||||
this._onSelectionChange = this._onSelectionChange.bind(this);
|
this._onSelectionChange = this._onSelectionChange.bind(this);
|
||||||
this._onSubmit = this._onSubmit.bind(this);
|
this._onSubmit = this._onSubmit.bind(this);
|
||||||
|
this._onSubmitKeyPress = this._onSubmitKeyPress.bind(this);
|
||||||
this._parseQueryResults = this._parseQueryResults.bind(this);
|
this._parseQueryResults = this._parseQueryResults.bind(this);
|
||||||
this._setMultiSelectElement = this._setMultiSelectElement.bind(this);
|
this._setMultiSelectElement = this._setMultiSelectElement.bind(this);
|
||||||
this._renderFooterText = this._renderFooterText.bind(this);
|
this._renderFooterText = this._renderFooterText.bind(this);
|
||||||
|
@ -246,6 +248,22 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onSubmitKeyPress: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onSubmitKeyPress(e) {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this._onSubmit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_onKeyDown: (Object) => void;
|
_onKeyDown: (Object) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -425,6 +443,22 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||||
this.setState({ inviteItems: [] });
|
this.setState({ inviteItems: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onClearItemsKeyPress: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the selected items from state and form.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onClearItemsKeyPress(e) {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this._onClearItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the add/cancel actions for the form.
|
* Renders the add/cancel actions for the form.
|
||||||
*
|
*
|
||||||
|
@ -441,13 +475,21 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||||
return (
|
return (
|
||||||
<div className = { `invite-more-dialog invite-buttons${this._isAddDisabled() ? ' disabled' : ''}` }>
|
<div className = { `invite-more-dialog invite-buttons${this._isAddDisabled() ? ' disabled' : ''}` }>
|
||||||
<a
|
<a
|
||||||
|
aria-label = { t('dialog.Cancel') }
|
||||||
className = 'invite-more-dialog invite-buttons-cancel'
|
className = 'invite-more-dialog invite-buttons-cancel'
|
||||||
onClick = { this._onClearItems }>
|
onClick = { this._onClearItems }
|
||||||
|
onKeyPress = { this._onClearItemsKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
{t('dialog.Cancel')}
|
{t('dialog.Cancel')}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
|
aria-label = { t('addPeople.add') }
|
||||||
className = 'invite-more-dialog invite-buttons-add'
|
className = 'invite-more-dialog invite-buttons-add'
|
||||||
onClick = { this._onSubmit }>
|
onClick = { this._onSubmit }
|
||||||
|
onKeyPress = { this._onSubmitKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
{t('addPeople.add')}
|
{t('addPeople.add')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -480,9 +522,9 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<a
|
<a
|
||||||
|
aria-label = { supportLink }
|
||||||
href = { supportLink }
|
href = { supportLink }
|
||||||
rel = 'noopener noreferrer'
|
rel = 'noopener noreferrer'>
|
||||||
target = '_blank'>
|
|
||||||
{ t('inlineDialogFailure.support') }
|
{ t('inlineDialogFailure.support') }
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -71,7 +71,9 @@ class KeyboardShortcutsDialog extends Component<Props> {
|
||||||
<li
|
<li
|
||||||
className = 'shortcuts-list__item'
|
className = 'shortcuts-list__item'
|
||||||
key = { keyboardKey }>
|
key = { keyboardKey }>
|
||||||
<span className = 'shortcuts-list__description'>
|
<span
|
||||||
|
aria-label = { this.props.t(translationKey) }
|
||||||
|
className = 'shortcuts-list__description'>
|
||||||
{ this.props.t(translationKey) }
|
{ this.props.t(translationKey) }
|
||||||
</span>
|
</span>
|
||||||
<span className = 'item-action'>
|
<span className = 'item-action'>
|
||||||
|
|
|
@ -102,7 +102,9 @@ class LargeVideo extends Component<Props> {
|
||||||
* another container for the background and the
|
* another container for the background and the
|
||||||
* largeVideoWrapper in order to hide/show them.
|
* largeVideoWrapper in order to hide/show them.
|
||||||
*/}
|
*/}
|
||||||
<div id = 'largeVideoWrapper'>
|
<div
|
||||||
|
id = 'largeVideoWrapper'
|
||||||
|
role = 'figure' >
|
||||||
<video
|
<video
|
||||||
autoPlay = { !_noAutoPlayVideo }
|
autoPlay = { !_noAutoPlayVideo }
|
||||||
id = 'largeVideo'
|
id = 'largeVideo'
|
||||||
|
|
|
@ -89,7 +89,9 @@ class LobbySection extends PureComponent<Props, State> {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div id = 'lobby-section'>
|
<div id = 'lobby-section'>
|
||||||
<p className = 'description'>
|
<p
|
||||||
|
className = 'description'
|
||||||
|
role = 'banner'>
|
||||||
{ t('lobby.enableDialogText') }
|
{ t('lobby.enableDialogText') }
|
||||||
</p>
|
</p>
|
||||||
<div className = 'control-row'>
|
<div className = 'control-row'>
|
||||||
|
|
|
@ -306,11 +306,15 @@ class LocalRecordingInfoDialog extends Component<Props, State> {
|
||||||
<div className = 'localrec-control-action-links'>
|
<div className = 'localrec-control-action-links'>
|
||||||
<div className = 'localrec-control-action-link'>
|
<div className = 'localrec-control-action-link'>
|
||||||
{ isEngaged ? <a
|
{ isEngaged ? <a
|
||||||
onClick = { this._onStop }>
|
onClick = { this._onStop }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
{ t('localRecording.stop') }
|
{ t('localRecording.stop') }
|
||||||
</a>
|
</a>
|
||||||
: <a
|
: <a
|
||||||
onClick = { this._onStart }>
|
onClick = { this._onStart }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
{ t('localRecording.start') }
|
{ t('localRecording.start') }
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,9 +81,9 @@ class Notification extends AbstractNotification<Props> {
|
||||||
|
|
||||||
// the id is used for testing the UI
|
// the id is used for testing the UI
|
||||||
return (
|
return (
|
||||||
<div data-testid = { this._getDescriptionKey() } >
|
<p data-testid = { this._getDescriptionKey() } >
|
||||||
{ description }
|
{ description }
|
||||||
</div>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import { FlagGroup } from '@atlaskit/flag';
|
import { FlagGroup } from '@atlaskit/flag';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
import AbstractNotificationsContainer, {
|
import AbstractNotificationsContainer, {
|
||||||
_abstractMapStateToProps,
|
_abstractMapStateToProps,
|
||||||
|
@ -16,7 +17,12 @@ type Props = AbstractProps & {
|
||||||
/**
|
/**
|
||||||
* Whether we are a SIP gateway or not.
|
* Whether we are a SIP gateway or not.
|
||||||
*/
|
*/
|
||||||
_iAmSipGateway: boolean
|
_iAmSipGateway: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked to obtain translated strings.
|
||||||
|
*/
|
||||||
|
t: Function
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,6 +48,7 @@ class NotificationsContainer extends AbstractNotificationsContainer<Props> {
|
||||||
return (
|
return (
|
||||||
<FlagGroup
|
<FlagGroup
|
||||||
id = 'notifications-container'
|
id = 'notifications-container'
|
||||||
|
label = { this.props.t('notify.groupTitle') }
|
||||||
onDismissed = { this._onDismissed }>
|
onDismissed = { this._onDismissed }>
|
||||||
{ this._renderFlags() }
|
{ this._renderFlags() }
|
||||||
</FlagGroup>
|
</FlagGroup>
|
||||||
|
@ -73,7 +80,6 @@ class NotificationsContainer extends AbstractNotificationsContainer<Props> {
|
||||||
key = { uid }
|
key = { uid }
|
||||||
onDismissed = { this._onDismissed }
|
onDismissed = { this._onDismissed }
|
||||||
uid = { uid } />
|
uid = { uid } />
|
||||||
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -96,4 +102,4 @@ function _mapStateToProps(state) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default connect(_mapStateToProps)(NotificationsContainer);
|
export default translate(connect(_mapStateToProps)(NotificationsContainer));
|
||||||
|
|
|
@ -29,12 +29,20 @@ class PageReloadOverlay extends AbstractPageReloadOverlay<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OverlayFrame isLightOverlay = { isNetworkFailure }>
|
<OverlayFrame isLightOverlay = { isNetworkFailure }>
|
||||||
<div className = 'inlay'>
|
<div
|
||||||
|
aria-describedby = 'reload_overlay_text'
|
||||||
|
aria-labelledby = 'reload_overlay_title'
|
||||||
|
className = 'inlay'
|
||||||
|
role = 'dialog'>
|
||||||
<span
|
<span
|
||||||
className = 'reload_overlay_title'>
|
className = 'reload_overlay_title'
|
||||||
|
id = 'reload_overlay_title'
|
||||||
|
role = 'heading'>
|
||||||
{ t(title) }
|
{ t(title) }
|
||||||
</span>
|
</span>
|
||||||
<span className = 'reload_overlay_text'>
|
<span
|
||||||
|
className = 'reload_overlay_text'
|
||||||
|
id = 'reload_overlay_text'>
|
||||||
{ t(message, { seconds: timeLeft }) }
|
{ t(message, { seconds: timeLeft }) }
|
||||||
</span>
|
</span>
|
||||||
{ this._renderProgressBar() }
|
{ this._renderProgressBar() }
|
||||||
|
|
|
@ -30,12 +30,17 @@ class UserMediaPermissionsOverlay extends AbstractUserMediaPermissionsOverlay {
|
||||||
<div className = 'inlay'>
|
<div className = 'inlay'>
|
||||||
<span className = 'inlay__icon icon-microphone' />
|
<span className = 'inlay__icon icon-microphone' />
|
||||||
<span className = 'inlay__icon icon-camera' />
|
<span className = 'inlay__icon icon-camera' />
|
||||||
<h3 className = 'inlay__title'>
|
<h3
|
||||||
|
aria-label = { t('startupoverlay.genericTitle') }
|
||||||
|
className = 'inlay__title'
|
||||||
|
role = 'alert' >
|
||||||
{
|
{
|
||||||
t('startupoverlay.genericTitle')
|
t('startupoverlay.genericTitle')
|
||||||
}
|
}
|
||||||
</h3>
|
</h3>
|
||||||
<span className = 'inlay__text'>
|
<span
|
||||||
|
className = 'inlay__text'
|
||||||
|
role = 'alert' >
|
||||||
{
|
{
|
||||||
translateToHTML(t,
|
translateToHTML(t,
|
||||||
`userMedia.${browser}GrantPermissions`)
|
`userMedia.${browser}GrantPermissions`)
|
||||||
|
@ -43,7 +48,9 @@ class UserMediaPermissionsOverlay extends AbstractUserMediaPermissionsOverlay {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className = 'policy overlay__policy'>
|
<div className = 'policy overlay__policy'>
|
||||||
<p className = 'policy__text'>
|
<p
|
||||||
|
className = 'policy__text'
|
||||||
|
role = 'alert'>
|
||||||
{ translateToHTML(t, 'startupoverlay.policyText') }
|
{ translateToHTML(t, 'startupoverlay.policyText') }
|
||||||
</p>
|
</p>
|
||||||
{
|
{
|
||||||
|
@ -66,7 +73,9 @@ class UserMediaPermissionsOverlay extends AbstractUserMediaPermissionsOverlay {
|
||||||
if (policyLogoSrc) {
|
if (policyLogoSrc) {
|
||||||
return (
|
return (
|
||||||
<div className = 'policy__logo'>
|
<div className = 'policy__logo'>
|
||||||
<img src = { policyLogoSrc } />
|
<img
|
||||||
|
alt = { this.props.t('welcomepage.logo.policyLogo') }
|
||||||
|
src = { policyLogoSrc } />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ export const InviteButton = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ParticipantInviteButton
|
<ParticipantInviteButton
|
||||||
aria-label = { t('toolbar.accessibilityLabel.invite') }
|
aria-label = { t('participantsPane.actions.invite') }
|
||||||
onClick = { onInvite }>
|
onClick = { onInvite }>
|
||||||
<Icon
|
<Icon
|
||||||
size = { 20 }
|
size = { 20 }
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { getIsParticipantAudioMuted, getIsParticipantVideoMuted } from '../../base/tracks';
|
import { getIsParticipantAudioMuted, getIsParticipantVideoMuted } from '../../base/tracks';
|
||||||
|
@ -38,6 +39,7 @@ export const MeetingParticipantItem = ({
|
||||||
onLeave,
|
onLeave,
|
||||||
participant
|
participant
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const isAudioMuted = useSelector(getIsParticipantAudioMuted(participant));
|
const isAudioMuted = useSelector(getIsParticipantAudioMuted(participant));
|
||||||
const isVideoMuted = useSelector(getIsParticipantVideoMuted(participant));
|
const isVideoMuted = useSelector(getIsParticipantVideoMuted(participant));
|
||||||
|
|
||||||
|
@ -49,7 +51,9 @@ export const MeetingParticipantItem = ({
|
||||||
onLeave = { onLeave }
|
onLeave = { onLeave }
|
||||||
participant = { participant }
|
participant = { participant }
|
||||||
videoMuteState = { isVideoMuted ? MediaState.Muted : MediaState.Unmuted }>
|
videoMuteState = { isVideoMuted ? MediaState.Muted : MediaState.Unmuted }>
|
||||||
<ParticipantActionEllipsis onClick = { onContextMenu } />
|
<ParticipantActionEllipsis
|
||||||
|
aria-label = { t('MeetingParticipantItem.ParticipantActionEllipsis.options') }
|
||||||
|
onClick = { onContextMenu } />
|
||||||
</ParticipantItem>
|
</ParticipantItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -30,6 +30,12 @@ export const ParticipantsPane = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const closePane = useCallback(() => dispatch(close(), [ dispatch ]));
|
const closePane = useCallback(() => dispatch(close(), [ dispatch ]));
|
||||||
|
const closePaneKeyPress = useCallback(e => {
|
||||||
|
if (closePane && (e.key === ' ' || e.key === 'Enter')) {
|
||||||
|
e.preventDefault();
|
||||||
|
closePane();
|
||||||
|
}
|
||||||
|
}, [ closePane ]);
|
||||||
const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)), [ dispatch ]);
|
const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)), [ dispatch ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -41,7 +47,12 @@ export const ParticipantsPane = () => {
|
||||||
) }>
|
) }>
|
||||||
<div className = 'participants_pane-content'>
|
<div className = 'participants_pane-content'>
|
||||||
<Header>
|
<Header>
|
||||||
<Close onClick = { closePane } />
|
<Close
|
||||||
|
aria-label = { t('participantsPane.close', 'Close') }
|
||||||
|
onClick = { closePane }
|
||||||
|
onKeyPress = { closePaneKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 } />
|
||||||
</Header>
|
</Header>
|
||||||
<Container>
|
<Container>
|
||||||
<LobbyParticipantList />
|
<LobbyParticipantList />
|
||||||
|
|
|
@ -183,6 +183,9 @@ class Prejoin extends Component<Props, State> {
|
||||||
this._onDropdownClose = this._onDropdownClose.bind(this);
|
this._onDropdownClose = this._onDropdownClose.bind(this);
|
||||||
this._onOptionsClick = this._onOptionsClick.bind(this);
|
this._onOptionsClick = this._onOptionsClick.bind(this);
|
||||||
this._setName = this._setName.bind(this);
|
this._setName = this._setName.bind(this);
|
||||||
|
this._onJoinConferenceWithoutAudioKeyPress = this._onJoinConferenceWithoutAudioKeyPress.bind(this);
|
||||||
|
this._showDialogKeyPress = this._showDialogKeyPress.bind(this);
|
||||||
|
this._onJoinKeyPress = this._onJoinKeyPress.bind(this);
|
||||||
}
|
}
|
||||||
_onJoinButtonClick: () => void;
|
_onJoinButtonClick: () => void;
|
||||||
|
|
||||||
|
@ -205,6 +208,22 @@ class Prejoin extends Component<Props, State> {
|
||||||
this.props.joinConference();
|
this.props.joinConference();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onJoinKeyPress: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onJoinKeyPress(e) {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this._onJoinButtonClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_onToggleButtonClick: () => void;
|
_onToggleButtonClick: () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -283,6 +302,40 @@ class Prejoin extends Component<Props, State> {
|
||||||
this._onDropdownClose();
|
this._onDropdownClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_showDialogKeyPress: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_showDialogKeyPress(e) {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this._showDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onJoinConferenceWithoutAudioKeyPress: (Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPress handler for accessibility.
|
||||||
|
*
|
||||||
|
* @param {Object} e - The key event to handle.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onJoinConferenceWithoutAudioKeyPress(e) {
|
||||||
|
if (this.props.joinConferenceWithoutAudio
|
||||||
|
&& (e.key === ' '
|
||||||
|
|| e.key === 'Enter')) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.joinConferenceWithoutAudio();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -305,7 +358,8 @@ class Prejoin extends Component<Props, State> {
|
||||||
visibleButtons
|
visibleButtons
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { _closeDialog, _onDropdownClose, _onJoinButtonClick, _onOptionsClick, _setName, _showDialog } = this;
|
const { _closeDialog, _onDropdownClose, _onJoinButtonClick, _onJoinKeyPress, _showDialogKeyPress,
|
||||||
|
_onJoinConferenceWithoutAudioKeyPress, _onOptionsClick, _setName, _showDialog } = this;
|
||||||
const { showJoinByPhoneButtons, showError } = this.state;
|
const { showJoinByPhoneButtons, showError } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -322,10 +376,16 @@ class Prejoin extends Component<Props, State> {
|
||||||
{showJoinActions && (
|
{showJoinActions && (
|
||||||
<div className = 'prejoin-input-area-container'>
|
<div className = 'prejoin-input-area-container'>
|
||||||
<div className = 'prejoin-input-area'>
|
<div className = 'prejoin-input-area'>
|
||||||
|
<label
|
||||||
|
className = 'prejoin-input-area-label'
|
||||||
|
htmlFor = { 'Prejoin-input-field-id' } >
|
||||||
|
{ t('dialog.enterDisplayNameToJoin') }</label>
|
||||||
<InputField
|
<InputField
|
||||||
|
autoComplete = { 'name' }
|
||||||
autoFocus = { true }
|
autoFocus = { true }
|
||||||
className = { showError ? 'error' : '' }
|
className = { showError ? 'error' : '' }
|
||||||
hasError = { showError }
|
hasError = { showError }
|
||||||
|
id = { 'Prejoin-input-field-id' }
|
||||||
onChange = { _setName }
|
onChange = { _setName }
|
||||||
onSubmit = { joinConference }
|
onSubmit = { joinConference }
|
||||||
placeHolder = { t('dialog.enterDisplayName') }
|
placeHolder = { t('dialog.enterDisplayName') }
|
||||||
|
@ -341,7 +401,10 @@ class Prejoin extends Component<Props, State> {
|
||||||
<div
|
<div
|
||||||
className = 'prejoin-preview-dropdown-btn'
|
className = 'prejoin-preview-dropdown-btn'
|
||||||
data-testid = 'prejoin.joinWithoutAudio'
|
data-testid = 'prejoin.joinWithoutAudio'
|
||||||
onClick = { joinConferenceWithoutAudio }>
|
onClick = { joinConferenceWithoutAudio }
|
||||||
|
onKeyPress = { _onJoinConferenceWithoutAudioKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
<Icon
|
<Icon
|
||||||
className = 'prejoin-preview-dropdown-icon'
|
className = 'prejoin-preview-dropdown-icon'
|
||||||
size = { 24 }
|
size = { 24 }
|
||||||
|
@ -350,7 +413,10 @@ class Prejoin extends Component<Props, State> {
|
||||||
</div>
|
</div>
|
||||||
{hasJoinByPhoneButton && <div
|
{hasJoinByPhoneButton && <div
|
||||||
className = 'prejoin-preview-dropdown-btn'
|
className = 'prejoin-preview-dropdown-btn'
|
||||||
onClick = { _showDialog }>
|
onClick = { _showDialog }
|
||||||
|
onKeyPress = { _showDialogKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
<Icon
|
<Icon
|
||||||
className = 'prejoin-preview-dropdown-icon'
|
className = 'prejoin-preview-dropdown-icon'
|
||||||
data-testid = 'prejoin.joinByPhone'
|
data-testid = 'prejoin.joinByPhone'
|
||||||
|
@ -363,9 +429,15 @@ class Prejoin extends Component<Props, State> {
|
||||||
onClose = { _onDropdownClose }>
|
onClose = { _onDropdownClose }>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
OptionsIcon = { showJoinByPhoneButtons ? IconArrowUp : IconArrowDown }
|
OptionsIcon = { showJoinByPhoneButtons ? IconArrowUp : IconArrowDown }
|
||||||
|
ariaDropDownLabel = { t('prejoin.joinWithoutAudio') }
|
||||||
|
ariaLabel = { t('prejoin.joinMeeting') }
|
||||||
|
ariaPressed = { showJoinByPhoneButtons }
|
||||||
hasOptions = { true }
|
hasOptions = { true }
|
||||||
onClick = { _onJoinButtonClick }
|
onClick = { _onJoinButtonClick }
|
||||||
|
onKeyPress = { _onJoinKeyPress }
|
||||||
onOptionsClick = { _onOptionsClick }
|
onOptionsClick = { _onOptionsClick }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }
|
||||||
testId = 'prejoin.joinMeeting'
|
testId = 'prejoin.joinMeeting'
|
||||||
type = 'primary'>
|
type = 'primary'>
|
||||||
{ t('prejoin.joinMeeting') }
|
{ t('prejoin.joinMeeting') }
|
||||||
|
|
|
@ -172,9 +172,7 @@ class CountryPicker extends PureComponent<Props, State> {
|
||||||
* @param {Object} e - The synthetic event.
|
* @param {Object} e - The synthetic event.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onCountrySelectorClick(e) {
|
_onCountrySelectorClick() {
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isOpen: !this.setState.isOpen
|
isOpen: !this.setState.isOpen
|
||||||
});
|
});
|
||||||
|
@ -215,7 +213,8 @@ class CountryPicker extends PureComponent<Props, State> {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onKeyPress(e) {
|
_onKeyPress(e) {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
this.props.onSubmit();
|
this.props.onSubmit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { Icon, IconArrowDown } from '../../../base/icons';
|
import { Icon, IconArrowDown } from '../../../base/icons';
|
||||||
|
|
||||||
|
@ -23,10 +23,18 @@ type Props = {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
function CountrySelector({ country: { code, dialCode }, onClick }: Props) {
|
function CountrySelector({ country: { code, dialCode }, onClick }: Props) {
|
||||||
|
const onKeyPressHandler = useCallback(e => {
|
||||||
|
if (onClick && (e.key === ' ' || e.key === 'Enter')) {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
}, [ onClick ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className = 'cpick-selector'
|
className = 'cpick-selector'
|
||||||
onClick = { onClick }>
|
onClick = { onClick }
|
||||||
|
onKeyPress = { onKeyPressHandler }>
|
||||||
<div className = { `prejoin-dialog-flag iti-flag ${code}` } />
|
<div className = { `prejoin-dialog-flag iti-flag ${code}` } />
|
||||||
<span>{`+${dialCode}`}</span>
|
<span>{`+${dialCode}`}</span>
|
||||||
<Icon
|
<Icon
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue