Use focus trap for CallingLobby

This commit is contained in:
Fedor Indutny 2021-10-25 07:58:09 -07:00 committed by GitHub
parent 191bfee18c
commit b38b22f49d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 179 additions and 132 deletions

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import FocusTrap from 'focus-trap-react';
import classNames from 'classnames';
import {
SetLocalAudioType,
@ -203,83 +204,85 @@ export const CallingLobby = ({
}
return (
<div className="module-calling__container">
{shouldShowLocalVideo ? (
<video
className="module-CallingLobby__local-preview module-CallingLobby__local-preview--camera-is-on"
ref={localVideoRef}
autoPlay
/>
) : (
<CallBackgroundBlur
className="module-CallingLobby__local-preview module-CallingLobby__local-preview--camera-is-off"
avatarPath={me.avatarPath}
color={me.color}
/>
)}
<CallingHeader
i18n={i18n}
isGroupCall={isGroupCall}
participantCount={peekedParticipants.length}
showParticipantsList={showParticipantsList}
toggleParticipants={toggleParticipants}
toggleSettings={toggleSettings}
onCancel={onCallCanceled}
/>
<CallingPreCallInfo
conversation={conversation}
groupMembers={groupMembers}
i18n={i18n}
isCallFull={isCallFull}
me={me}
peekedParticipants={peekedParticipants}
ringMode={preCallInfoRingMode}
/>
<div
className={classNames(
'module-CallingLobby__camera-is-off',
`module-CallingLobby__camera-is-off--${
shouldShowLocalVideo ? 'invisible' : 'visible'
}`
<FocusTrap>
<div className="module-calling__container">
{shouldShowLocalVideo ? (
<video
className="module-CallingLobby__local-preview module-CallingLobby__local-preview--camera-is-on"
ref={localVideoRef}
autoPlay
/>
) : (
<CallBackgroundBlur
className="module-CallingLobby__local-preview module-CallingLobby__local-preview--camera-is-off"
avatarPath={me.avatarPath}
color={me.color}
/>
)}
>
{i18n('calling__your-video-is-off')}
</div>
<div className="module-calling__buttons module-calling__buttons--inline">
<CallingButton
buttonType={videoButtonType}
<CallingHeader
i18n={i18n}
onClick={toggleVideo}
tooltipDirection={TooltipPlacement.Top}
isGroupCall={isGroupCall}
participantCount={peekedParticipants.length}
showParticipantsList={showParticipantsList}
toggleParticipants={toggleParticipants}
toggleSettings={toggleSettings}
onCancel={onCallCanceled}
/>
<CallingButton
buttonType={audioButtonType}
<CallingPreCallInfo
conversation={conversation}
groupMembers={groupMembers}
i18n={i18n}
onClick={toggleAudio}
tooltipDirection={TooltipPlacement.Top}
isCallFull={isCallFull}
me={me}
peekedParticipants={peekedParticipants}
ringMode={preCallInfoRingMode}
/>
<CallingButton
buttonType={ringButtonType}
<div
className={classNames(
'module-CallingLobby__camera-is-off',
`module-CallingLobby__camera-is-off--${
shouldShowLocalVideo ? 'invisible' : 'visible'
}`
)}
>
{i18n('calling__your-video-is-off')}
</div>
<div className="module-calling__buttons module-calling__buttons--inline">
<CallingButton
buttonType={videoButtonType}
i18n={i18n}
onClick={toggleVideo}
tooltipDirection={TooltipPlacement.Top}
/>
<CallingButton
buttonType={audioButtonType}
i18n={i18n}
onClick={toggleAudio}
tooltipDirection={TooltipPlacement.Top}
/>
<CallingButton
buttonType={ringButtonType}
i18n={i18n}
isVisible={isRingButtonVisible}
onClick={toggleOutgoingRing}
tooltipDirection={TooltipPlacement.Top}
/>
</div>
<CallingLobbyJoinButton
disabled={!canJoin}
i18n={i18n}
isVisible={isRingButtonVisible}
onClick={toggleOutgoingRing}
tooltipDirection={TooltipPlacement.Top}
onClick={() => {
setIsCallConnecting(true);
onJoinCall();
}}
variant={callingLobbyJoinButtonVariant}
/>
</div>
<CallingLobbyJoinButton
disabled={!canJoin}
i18n={i18n}
onClick={() => {
setIsCallConnecting(true);
onJoinCall();
}}
variant={callingLobbyJoinButtonVariant}
/>
</div>
</FocusTrap>
);
};

View File

@ -3,62 +3,12 @@
import React from 'react';
import classNames from 'classnames';
import { noop } from 'lodash';
import { Manager, Reference, Popper } from 'react-popper';
import type { StrictModifiers } from '@popperjs/core';
import { Theme, themeClassName } from '../util/theme';
import { refMerger } from '../util/refMerger';
import { offsetDistanceModifier } from '../util/popperUtil';
type EventWrapperPropsType = {
children: React.ReactNode;
onHoverChanged: (_: boolean) => void;
};
// React doesn't reliably fire `onMouseLeave` or `onMouseOut` events if wrapping a
// disabled button. This uses native browser events to avoid that.
//
// See <https://lecstor.com/react-disabled-button-onmouseleave/>.
const TooltipEventWrapper = React.forwardRef<
HTMLSpanElement,
EventWrapperPropsType
>(({ onHoverChanged, children }, ref) => {
const wrapperRef = React.useRef<HTMLSpanElement | null>(null);
const on = React.useCallback(() => {
onHoverChanged(true);
}, [onHoverChanged]);
const off = React.useCallback(() => {
onHoverChanged(false);
}, [onHoverChanged]);
React.useEffect(() => {
const wrapperEl = wrapperRef.current;
if (!wrapperEl) {
return noop;
}
wrapperEl.addEventListener('mouseenter', on);
wrapperEl.addEventListener('mouseleave', off);
return () => {
wrapperEl.removeEventListener('mouseenter', on);
wrapperEl.removeEventListener('mouseleave', off);
};
}, [on, off]);
return (
<span
onFocus={on}
onBlur={off}
ref={refMerger<HTMLSpanElement>(ref, wrapperRef)}
>
{children}
</span>
);
});
import { SmartTooltipEventWrapper } from '../state/smart/TooltipEventWrapper';
export enum TooltipPlacement {
Top = 'top',
@ -97,9 +47,12 @@ export const Tooltip: React.FC<PropsType> = ({
<Manager>
<Reference>
{({ ref }) => (
<TooltipEventWrapper ref={ref} onHoverChanged={setIsHovering}>
<SmartTooltipEventWrapper
innerRef={ref}
onHoverChanged={setIsHovering}
>
{children}
</TooltipEventWrapper>
</SmartTooltipEventWrapper>
)}
</Reference>
<Popper

View File

@ -0,0 +1,69 @@
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { Ref, useCallback, useEffect, useRef } from 'react';
import { noop } from 'lodash';
import { refMerger } from '../util/refMerger';
import type { InteractionModeType } from '../state/ducks/conversations';
type PropsType = {
children: React.ReactNode;
interactionMode: InteractionModeType;
// Matches Popper's RefHandler type
innerRef: Ref<HTMLElement>;
onHoverChanged: (_: boolean) => void;
};
// React doesn't reliably fire `onMouseLeave` or `onMouseOut` events if wrapping a
// disabled button. This uses native browser events to avoid that.
//
// See <https://lecstor.com/react-disabled-button-onmouseleave/>.
export const TooltipEventWrapper: React.FC<PropsType> = ({
onHoverChanged,
children,
interactionMode,
innerRef,
}) => {
const wrapperRef = useRef<HTMLSpanElement | null>(null);
const on = useCallback(() => {
onHoverChanged(true);
}, [onHoverChanged]);
const off = useCallback(() => {
onHoverChanged(false);
}, [onHoverChanged]);
const onFocus = useCallback(() => {
if (interactionMode === 'keyboard') {
on();
}
}, [on, interactionMode]);
useEffect(() => {
const wrapperEl = wrapperRef.current;
if (!wrapperEl) {
return noop;
}
wrapperEl.addEventListener('mouseenter', on);
wrapperEl.addEventListener('mouseleave', off);
return () => {
wrapperEl.removeEventListener('mouseenter', on);
wrapperEl.removeEventListener('mouseleave', off);
};
}, [on, off]);
return (
<span
onFocus={onFocus}
onBlur={off}
ref={refMerger<HTMLSpanElement>(innerRef, wrapperRef)}
>
{children}
</span>
);
};

View File

@ -0,0 +1,29 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { Ref } from 'react';
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import { StateType } from '../reducer';
import { TooltipEventWrapper } from '../../components/TooltipEventWrapper';
import { getInteractionMode } from '../selectors/user';
type ExternalProps = {
// Matches Popper's RefHandler type
innerRef: Ref<HTMLElement>;
children: React.ReactNode;
onHoverChanged: (_: boolean) => void;
};
const mapStateToProps = (state: StateType, props: ExternalProps) => {
return {
...props,
interactionMode: getInteractionMode(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartTooltipEventWrapper = smart(TooltipEventWrapper);

View File

@ -12763,19 +12763,12 @@
},
{
"rule": "React-useRef",
"path": "ts/components/Tooltip.js",
"line": " const wrapperRef = react_1.default.useRef(null);",
"path": "ts/components/TooltipEventWrapper.tsx",
"line": " const wrapperRef = useRef<HTMLSpanElement | null>(null);",
"reasonCategory": "usageTrusted",
"updated": "2020-12-04T00:11:08.128Z",
"updated": "2021-10-21T16:10:14.143Z",
"reasonDetail": "Used to add (and remove) event listeners."
},
{
"rule": "React-useRef",
"path": "ts/components/Tooltip.tsx",
"line": " const wrapperRef = React.useRef<HTMLSpanElement | null>(null);",
"reasonCategory": "usageTrusted",
"updated": "2021-07-30T16:57:33.618Z"
},
{
"rule": "React-createRef",
"path": "ts/components/conversation/ConversationHeader.js",