diff --git a/css/_utils.scss b/css/_utils.scss index 7027559c2a..05fa019f1d 100644 --- a/css/_utils.scss +++ b/css/_utils.scss @@ -41,3 +41,36 @@ display: -webkit-flex !important; display: flex !important; } + +/** + * resets default button styles, + * mostly intended to be used on interactive elements that + * differ from their default styles (e.g. ) or have custom styles + */ +.invisible-button { + background: none; + border: none; + color: inherit; + cursor: pointer; + padding: 0; +} + + +/** + * style an element the same as an + * useful on some cases where we visually have a link but it's actually a } {bottomLabel && ( - + {bottomLabel} )} diff --git a/react/features/base/ui/components/web/MultiSelect.tsx b/react/features/base/ui/components/web/MultiSelect.tsx index c8d875236d..0b712dffb5 100644 --- a/react/features/base/ui/components/web/MultiSelect.tsx +++ b/react/features/base/ui/components/web/MultiSelect.tsx @@ -14,6 +14,7 @@ interface IProps { error?: boolean; errorDialog?: JSX.Element | null; filterValue?: string; + id: string; isOpen?: boolean; items: MultiSelectItem[]; noMatchesText?: string; @@ -101,6 +102,7 @@ const MultiSelect = ({ error, errorDialog, placeholder, + id, items, filterValue, onFilterChange, @@ -145,6 +147,7 @@ const MultiSelect = ({ element. + * Necessary for screen reader users, to link the label and error to the select. + */ + id: string; + /** * Label to be displayed above the select. */ @@ -140,6 +146,7 @@ const Select = ({ className, disabled, error, + id, label, onChange, options, @@ -149,11 +156,17 @@ const Select = ({ return (
- {label && {label}} + {label && }
{ className = { styles.checkbox } disabled = { disabled } onChange = { change } /> +
- + ); }; diff --git a/react/features/base/ui/functions.web.ts b/react/features/base/ui/functions.web.ts index 3a18c181fa..52277f8caa 100644 --- a/react/features/base/ui/functions.web.ts +++ b/react/features/base/ui/functions.web.ts @@ -82,3 +82,28 @@ export function isElementInTheViewport(element?: Element): boolean { return false; } + +const enterKeyElements = [ 'select', 'textarea', 'summary', 'a' ]; + +/** + * Informs whether or not the given element does something on its own when pressing the Enter key. + * + * This is useful to correctly submit custom made "forms" that are not using the native form element, + * only when the user is not using an element that needs the enter key to work. + * Note the implementation is incomplete and should be updated as needed if more complex use cases arise + * (for example, the Tabs aria pattern is not handled). + * + * @param {Element} element - The element. + * @returns {boolean} + */ +export function operatesWithEnterKey(element: Element): boolean { + if (enterKeyElements.includes(element.tagName.toLowerCase())) { + return true; + } + + if (element.tagName.toLowerCase() === 'button' && element.getAttribute('role') === 'button') { + return true; + } + + return false; +} diff --git a/react/features/chat/components/web/ChatInput.tsx b/react/features/chat/components/web/ChatInput.tsx index 10db18de9d..d274b2b81a 100644 --- a/react/features/chat/components/web/ChatInput.tsx +++ b/react/features/chat/components/web/ChatInput.tsx @@ -135,6 +135,7 @@ class ChatInput extends Component { className = 'chat-input' icon = { this.props._areSmileysDisabled ? undefined : IconFaceSmile } iconClick = { this._toggleSmileysPanel } + id = 'chat-input-messagebox' maxRows = { 5 } onChange = { this._onMessageChange } onKeyPress = { this._onDetectSubmit } diff --git a/react/features/conference/components/web/RaisedHandsCountLabel.tsx b/react/features/conference/components/web/RaisedHandsCountLabel.tsx index 4a9901ed10..76c8f2ddb7 100644 --- a/react/features/conference/components/web/RaisedHandsCountLabel.tsx +++ b/react/features/conference/components/web/RaisedHandsCountLabel.tsx @@ -32,6 +32,7 @@ const RaisedHandsCountLabel = () => { content = { t('raisedHandsLabel') } position = { 'bottom' }>
+ { t('videothumbnail.connectionInfo') } { cursor: 'pointer', color: theme.palette.link01, transition: 'color .2s ease', + border: 0, + background: 0, + padding: 0, + display: 'inline', + fontWeight: 'bold', '&:hover': { color: theme.palette.link01Hover, @@ -714,13 +719,12 @@ const ConnectionStatsTable = ({ const _renderSaveLogs = () => ( - + type = 'button'> {t('connectionindicator.savelogs')} - + | ); @@ -732,13 +736,12 @@ const ConnectionStatsTable = ({ : 'connectionindicator.more'; return ( - + type = 'button'> {t(translationKey)} - + ); }; diff --git a/react/features/device-selection/components/DeviceSelector.web.tsx b/react/features/device-selection/components/DeviceSelector.web.tsx index 44803797bf..a1d042fae0 100644 --- a/react/features/device-selection/components/DeviceSelector.web.tsx +++ b/react/features/device-selection/components/DeviceSelector.web.tsx @@ -69,6 +69,7 @@ const useStyles = makeStyles()(theme => { const DeviceSelector = ({ devices, hasPermission, + id, isDisabled, label, onSelect, @@ -103,6 +104,7 @@ const DeviceSelector = ({ return ( diff --git a/react/features/feedback/components/FeedbackDialog.web.tsx b/react/features/feedback/components/FeedbackDialog.web.tsx index c8c07c6737..1825235588 100644 --- a/react/features/feedback/components/FeedbackDialog.web.tsx +++ b/react/features/feedback/components/FeedbackDialog.web.tsx @@ -25,7 +25,7 @@ const styles = (theme: Theme) => { rating: { display: 'flex', - flexDirection: 'column' as const, + flexDirection: 'column-reverse' as const, alignItems: 'center', justifyContent: 'center', marginTop: theme.spacing(4), @@ -316,22 +316,27 @@ class FeedbackDialog extends Component { titleKey = 'feedback.rateExperience'>
-
-

- { t(SCORES[scoreToDisplayAsSelected]) } -

-
{ scoreIcons }
+
+

+ { t('feedback.accessibilityLabel.yourChoice', { + rating: t(SCORES[scoreToDisplayAsSelected]) + }) } +

+

+ { t(SCORES[scoreToDisplayAsSelected]) } +

+
{ tabIndex = { 0 }> {avatarURL ? ( ) @@ -1105,6 +1106,20 @@ class Thumbnail extends Component { ? {video} : video)}
+ {/* put the bottom container before the top container in the dom, + because it contains the participant name that should be announced first by screen readers */} +
+ +
{ thumbnailType = { _thumbnailType } />
{_shouldDisplayTintBackground &&
} -
- -
{!_gifSrc && this._renderAvatar(styles.avatar) } { !local && (
diff --git a/react/features/gifs/components/web/GifsMenu.tsx b/react/features/gifs/components/web/GifsMenu.tsx index 5e8ed86647..4e7d05f1c8 100644 --- a/react/features/gifs/components/web/GifsMenu.tsx +++ b/react/features/gifs/components/web/GifsMenu.tsx @@ -208,6 +208,7 @@ function GifsMenu({ columns = 2, parent }: IProps) { - +

{t('addPeople.shareLink')}

diff --git a/react/features/invite/components/add-people-dialog/web/DialInNumber.tsx b/react/features/invite/components/add-people-dialog/web/DialInNumber.tsx index 03109ac3ed..612d83911d 100644 --- a/react/features/invite/components/add-people-dialog/web/DialInNumber.tsx +++ b/react/features/invite/components/add-people-dialog/web/DialInNumber.tsx @@ -44,7 +44,6 @@ class DialInNumber extends Component { // Bind event handler so it is only bound once for every instance. this._onCopyText = this._onCopyText.bind(this); - this._onCopyTextKeyPress = this._onCopyTextKeyPress.bind(this); } /** @@ -62,20 +61,6 @@ class DialInNumber extends Component { copyText(textToCopy); } - /** - * KeyPress handler for accessibility. - * - * @param {Object} e - The key event to handle. - * - * @returns {void} - */ - _onCopyTextKeyPress(e: React.KeyboardEvent) { - if (e.key === ' ' || e.key === 'Enter') { - e.preventDefault(); - this._onCopyText(); - } - } - /** * Implements React's {@link Component#render()}. * @@ -87,7 +72,7 @@ class DialInNumber extends Component { return (
-
+

{ t('info.dialInNumber') } @@ -107,16 +92,13 @@ class DialInNumber extends Component { { `${_formatConferenceIDPin(conferenceID)}#` } -

- +
); } diff --git a/react/features/invite/components/add-people-dialog/web/InviteContactsForm.tsx b/react/features/invite/components/add-people-dialog/web/InviteContactsForm.tsx index f6d0a58e38..2108408467 100644 --- a/react/features/invite/components/add-people-dialog/web/InviteContactsForm.tsx +++ b/react/features/invite/components/add-people-dialog/web/InviteContactsForm.tsx @@ -185,6 +185,7 @@ class InviteContactsForm extends AbstractAddPeopleDialog { className = { this.props.classes.formWrap } onKeyDown = { this._onKeyDown }> { return ( { <> diff --git a/react/features/polls/components/web/PollCreate.tsx b/react/features/polls/components/web/PollCreate.tsx index 84870af9c6..a76952875f 100644 --- a/react/features/polls/components/web/PollCreate.tsx +++ b/react/features/polls/components/web/PollCreate.tsx @@ -191,6 +191,7 @@ const PollCreate = ({
setAnswer(i, val) } diff --git a/react/features/prejoin/components/web/Prejoin.tsx b/react/features/prejoin/components/web/Prejoin.tsx index eae2b69f60..bcc379a83f 100644 --- a/react/features/prejoin/components/web/Prejoin.tsx +++ b/react/features/prejoin/components/web/Prejoin.tsx @@ -374,10 +374,12 @@ const Prejoin = ({ className = { classes.inputContainer } data-testid = 'prejoin.screen'> {showDisplayNameField.current ? () : ( extends AbstractButton

{ - accessibilityLabel = 'dialog.accessibilityLabel.liveStreaming'; + accessibilityLabel = 'dialog.startLiveStreaming'; + toggledAccessibilityLabel = 'dialog.stopLiveStreaming'; icon = IconSites; label = 'dialog.startLiveStreaming'; toggledLabel = 'dialog.stopLiveStreaming'; diff --git a/react/features/recording/components/LiveStream/web/StreamKeyForm.tsx b/react/features/recording/components/LiveStream/web/StreamKeyForm.tsx index 868cd38fa8..f0371cc642 100644 --- a/react/features/recording/components/LiveStream/web/StreamKeyForm.tsx +++ b/react/features/recording/components/LiveStream/web/StreamKeyForm.tsx @@ -39,20 +39,6 @@ const styles = (theme: Theme) => { */ class StreamKeyForm extends AbstractStreamKeyForm { - /** - * Initializes a new {@code StreamKeyForm} instance. - * - * @param {IProps} props - The React {@code Component} props to initialize - * the new {@code StreamKeyForm} instance with. - */ - constructor(props: IProps) { - super(props); - - // Bind event handlers so they are only bound once per instance. - this._onOpenHelp = this._onOpenHelp.bind(this); - this._onOpenHelpKeyPress = this._onOpenHelpKeyPress.bind(this); - } - /** * Implements React's {@link Component#render()}. * @@ -66,6 +52,7 @@ class StreamKeyForm extends AbstractStreamKeyForm {

{ } { this.props._liveStreaming.helpURL ? + href = { this.props._liveStreaming.helpURL } + rel = 'noopener noreferrer' + target = '_blank'> { t('liveStreaming.streamIdHelp') } : null @@ -112,33 +97,6 @@ class StreamKeyForm extends AbstractStreamKeyForm {
); } - - /** - * Opens a new tab with information on how to manually locate a YouTube - * broadcast stream key. - * - * @private - * @returns {void} - */ - _onOpenHelp() { - window.open(this.props._liveStreaming.helpURL, '_blank', 'noopener'); - } - - /** - * Opens a new tab with information on how to manually locate a YouTube - * broadcast stream key. - * - * @param {Object} e - The key event to handle. - * - * @private - * @returns {void} - */ - _onOpenHelpKeyPress(e: React.KeyboardEvent) { - if (e.key === ' ') { - e.preventDefault(); - this._onOpenHelp(); - } - } } export default translate(connect(_mapStateToProps)(withStyles(styles)(StreamKeyForm))); diff --git a/react/features/recording/components/LiveStream/web/StreamKeyPicker.tsx b/react/features/recording/components/LiveStream/web/StreamKeyPicker.tsx index 8475a61274..2fb87a02f9 100644 --- a/react/features/recording/components/LiveStream/web/StreamKeyPicker.tsx +++ b/react/features/recording/components/LiveStream/web/StreamKeyPicker.tsx @@ -100,6 +100,7 @@ class StreamKeyPicker extends PureComponent { return (
{ titleKey = { t('dialog.shareAudioTitle') }>
- { t('dialog.Cancel') } - + { t('dialog.Cancel') } + ({ t('dialog.password') }) + + ); } @@ -308,49 +221,44 @@ function PasswordSection({ if (locked) { return ( <> - { t('dialog.Remove') } + type = 'button'> + { t('dialog.Remove') } + ({ t('dialog.password') }) + { // There are cases like lobby and grant moderator when password is not available password ? <> - { t('dialog.copy') } + type = 'button'> + { t('dialog.copy') } + ({ t('dialog.password') }) + : null } {locked === LOCKED_LOCALLY && ( - {t(passwordVisible ? 'dialog.hide' : 'dialog.show')} + type = 'button'> + {t(passwordVisible ? 'dialog.hide' : 'dialog.show')} + ({ t('dialog.password') }) + )} ); } return ( - { t('info.addPassword') } + type = 'button'>{ t('info.addPassword') } ); } diff --git a/react/features/settings/components/web/MoreTab.tsx b/react/features/settings/components/web/MoreTab.tsx index 92fc42ca12..e2c0dd634f 100644 --- a/react/features/settings/components/web/MoreTab.tsx +++ b/react/features/settings/components/web/MoreTab.tsx @@ -254,6 +254,7 @@ class MoreTab extends AbstractDialogTab { return ( {label} @@ -221,7 +220,6 @@ const AudioSettingsContent = ({ isSelected = { isSelected } key = { key } length = { length } - listHeaderId = { speakerHeaderId } onClick = { _onSpeakerEntryClick }> {label} diff --git a/react/features/settings/components/web/audio/MicrophoneEntry.tsx b/react/features/settings/components/web/audio/MicrophoneEntry.tsx index 2e62d67084..61df75d3ab 100644 --- a/react/features/settings/components/web/audio/MicrophoneEntry.tsx +++ b/react/features/settings/components/web/audio/MicrophoneEntry.tsx @@ -54,8 +54,6 @@ interface IProps { length: number; - listHeaderId: string; - /** * Used to decide whether to listen to audio level changes. */ @@ -112,7 +110,6 @@ const MicrophoneEntry = ({ isSelected, length, jitsiTrack, - listHeaderId, measureAudioLevels, onClick: propsClick }: IProps) => { @@ -138,7 +135,7 @@ const MicrophoneEntry = ({ * @returns {void} */ const onKeyPress = useCallback((e: React.KeyboardEvent) => { - if (e.key === ' ') { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); propsClick(deviceId); } @@ -190,14 +187,9 @@ const MicrophoneEntry = ({ activeTrackRef.current = jitsiTrack; }, [ jitsiTrack ]); - const deviceTextId = `choose_microphone${deviceId}`; - - const labelledby = `${listHeaderId} ${deviceTextId} `; - return (
  • { * @returns {void} */ function _onKeyPress(e: React.KeyboardEvent) { - if (e.key === ' ') { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); props.onClick(props.deviceId); } @@ -135,15 +133,12 @@ const SpeakerEntry = (props: IProps) => { } } - const { children, isSelected, index, deviceId, length, listHeaderId } = props; - const deviceTextId = `choose_speaker${deviceId}`; - const labelledby = `${listHeaderId} ${deviceTextId} `; + const { children, isSelected, index, length } = props; /* eslint-disable react/jsx-no-bind */ return (
  • { role = 'radio' tabIndex = { 0 }> { virtualBackgroundSupported && } diff --git a/react/features/shared-video/components/web/SharedVideoDialog.tsx b/react/features/shared-video/components/web/SharedVideoDialog.tsx index 277f6fc495..e56b260019 100644 --- a/react/features/shared-video/components/web/SharedVideoDialog.tsx +++ b/react/features/shared-video/components/web/SharedVideoDialog.tsx @@ -84,15 +84,16 @@ class SharedVideoDialog extends AbstractSharedVideoDialog { titleKey = 'dialog.shareVideoTitle'> - { error && { t('dialog.sharedVideoDialogError') } } ); } diff --git a/react/features/toolbox/components/web/DialogPortal.ts b/react/features/toolbox/components/web/DialogPortal.ts index 8f61753738..ba4ff31832 100644 --- a/react/features/toolbox/components/web/DialogPortal.ts +++ b/react/features/toolbox/components/web/DialogPortal.ts @@ -22,6 +22,11 @@ interface IProps { */ getRef?: Function; + /** + * Function called when the portal target becomes actually visible. + */ + onVisible?: Function; + /** * Function used to get the updated size info of the container on it's resize. */ @@ -45,7 +50,7 @@ interface IProps { * * @returns {ReactElement} */ -function DialogPortal({ children, className, style, getRef, setSize, targetSelector }: IProps) { +function DialogPortal({ children, className, style, getRef, setSize, targetSelector, onVisible }: IProps) { const clientWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].clientWidth); const [ portalTarget ] = useState(() => { const portalDiv = document.createElement('div'); @@ -89,6 +94,7 @@ function DialogPortal({ children, className, style, getRef, setSize, targetSelec clearTimeout(timerRef.current); timerRef.current = window.setTimeout(() => { portalTarget.style.visibility = 'visible'; + onVisible?.(); }, 100); } }); diff --git a/react/features/toolbox/components/web/Drawer.tsx b/react/features/toolbox/components/web/Drawer.tsx index bb53208b37..90dbf07225 100644 --- a/react/features/toolbox/components/web/Drawer.tsx +++ b/react/features/toolbox/components/web/Drawer.tsx @@ -1,5 +1,5 @@ import React, { KeyboardEvent, ReactNode, useCallback } from 'react'; -import ReactFocusLock from 'react-focus-lock'; +import { FocusOn } from 'react-focus-on'; import { makeStyles } from 'tss-react/mui'; import { isElementInTheViewport } from '../../../base/ui/functions.web'; @@ -102,12 +102,7 @@ function Drawer({
    - - {children} - +
    + {children} +
    +
  • ) : null diff --git a/react/features/video-quality/components/Slider.web.tsx b/react/features/video-quality/components/Slider.web.tsx index 5a91cfe912..f64a893f3a 100644 --- a/react/features/video-quality/components/Slider.web.tsx +++ b/react/features/video-quality/components/Slider.web.tsx @@ -143,7 +143,9 @@ function Slider({ ariaLabel, max, min, onChange, step, value }: IProps) { return (
    -
      +
        {knobs.map((_, i) => (
      • { content = { t(tooltipKey) } position = { 'bottom' }>