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 // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React from 'react';
import FocusTrap from 'focus-trap-react';
import classNames from 'classnames'; import classNames from 'classnames';
import { import {
SetLocalAudioType, SetLocalAudioType,
@ -203,6 +204,7 @@ export const CallingLobby = ({
} }
return ( return (
<FocusTrap>
<div className="module-calling__container"> <div className="module-calling__container">
{shouldShowLocalVideo ? ( {shouldShowLocalVideo ? (
<video <video
@ -281,5 +283,6 @@ export const CallingLobby = ({
variant={callingLobbyJoinButtonVariant} variant={callingLobbyJoinButtonVariant}
/> />
</div> </div>
</FocusTrap>
); );
}; };

View file

@ -3,62 +3,12 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { noop } from 'lodash';
import { Manager, Reference, Popper } from 'react-popper'; import { Manager, Reference, Popper } from 'react-popper';
import type { StrictModifiers } from '@popperjs/core'; import type { StrictModifiers } from '@popperjs/core';
import { Theme, themeClassName } from '../util/theme'; import { Theme, themeClassName } from '../util/theme';
import { refMerger } from '../util/refMerger';
import { offsetDistanceModifier } from '../util/popperUtil'; import { offsetDistanceModifier } from '../util/popperUtil';
type EventWrapperPropsType = { import { SmartTooltipEventWrapper } from '../state/smart/TooltipEventWrapper';
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>
);
});
export enum TooltipPlacement { export enum TooltipPlacement {
Top = 'top', Top = 'top',
@ -97,9 +47,12 @@ export const Tooltip: React.FC<PropsType> = ({
<Manager> <Manager>
<Reference> <Reference>
{({ ref }) => ( {({ ref }) => (
<TooltipEventWrapper ref={ref} onHoverChanged={setIsHovering}> <SmartTooltipEventWrapper
innerRef={ref}
onHoverChanged={setIsHovering}
>
{children} {children}
</TooltipEventWrapper> </SmartTooltipEventWrapper>
)} )}
</Reference> </Reference>
<Popper <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", "rule": "React-useRef",
"path": "ts/components/Tooltip.js", "path": "ts/components/TooltipEventWrapper.tsx",
"line": " const wrapperRef = react_1.default.useRef(null);", "line": " const wrapperRef = useRef<HTMLSpanElement | null>(null);",
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-12-04T00:11:08.128Z", "updated": "2021-10-21T16:10:14.143Z",
"reasonDetail": "Used to add (and remove) event listeners." "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", "rule": "React-createRef",
"path": "ts/components/conversation/ConversationHeader.js", "path": "ts/components/conversation/ConversationHeader.js",