From b38b22f49db022ac4b361251c1da3eed05b00d72 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Mon, 25 Oct 2021 07:58:09 -0700 Subject: [PATCH] Use focus trap for CallingLobby --- ts/components/CallingLobby.tsx | 141 +++++++++++++------------ ts/components/Tooltip.tsx | 59 ++--------- ts/components/TooltipEventWrapper.tsx | 69 ++++++++++++ ts/state/smart/TooltipEventWrapper.tsx | 29 +++++ ts/util/lint/exceptions.json | 13 +-- 5 files changed, 179 insertions(+), 132 deletions(-) create mode 100644 ts/components/TooltipEventWrapper.tsx create mode 100644 ts/state/smart/TooltipEventWrapper.tsx diff --git a/ts/components/CallingLobby.tsx b/ts/components/CallingLobby.tsx index f5d47b30f8e..bd138f99548 100644 --- a/ts/components/CallingLobby.tsx +++ b/ts/components/CallingLobby.tsx @@ -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 ( -
- {shouldShowLocalVideo ? ( -
+ ); }; diff --git a/ts/components/Tooltip.tsx b/ts/components/Tooltip.tsx index 4ccba759eb4..a2536b85a6a 100644 --- a/ts/components/Tooltip.tsx +++ b/ts/components/Tooltip.tsx @@ -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 . -const TooltipEventWrapper = React.forwardRef< - HTMLSpanElement, - EventWrapperPropsType ->(({ onHoverChanged, children }, ref) => { - const wrapperRef = React.useRef(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 ( - (ref, wrapperRef)} - > - {children} - - ); -}); +import { SmartTooltipEventWrapper } from '../state/smart/TooltipEventWrapper'; export enum TooltipPlacement { Top = 'top', @@ -97,9 +47,12 @@ export const Tooltip: React.FC = ({ {({ ref }) => ( - + {children} - + )} ; + 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 . +export const TooltipEventWrapper: React.FC = ({ + onHoverChanged, + children, + interactionMode, + innerRef, +}) => { + const wrapperRef = useRef(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 ( + (innerRef, wrapperRef)} + > + {children} + + ); +}; diff --git a/ts/state/smart/TooltipEventWrapper.tsx b/ts/state/smart/TooltipEventWrapper.tsx new file mode 100644 index 00000000000..393e5bcae60 --- /dev/null +++ b/ts/state/smart/TooltipEventWrapper.tsx @@ -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; + 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); diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 8debd745776..7ed3778bd9b 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -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(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(null);", - "reasonCategory": "usageTrusted", - "updated": "2021-07-30T16:57:33.618Z" - }, { "rule": "React-createRef", "path": "ts/components/conversation/ConversationHeader.js",