From c6eafbb8d513e7e47c8289929414198287d95b88 Mon Sep 17 00:00:00 2001 From: Josh Perez <60019601+josh-signal@users.noreply.github.com> Date: Thu, 19 Nov 2020 13:11:35 -0500 Subject: [PATCH] Fix tooltip bugs --- ACKNOWLEDGMENTS.md | 24 ---- package.json | 1 - patches/react-tooltip-lite+1.12.0.patch | 60 ---------- stylesheets/_modules.scss | 141 ++++++++++++++++++++---- ts/components/CallScreen.tsx | 3 - ts/components/CallingButton.stories.tsx | 20 +--- ts/components/CallingButton.tsx | 37 ++----- ts/components/CallingHeader.tsx | 25 +---- ts/components/CallingLobby.tsx | 13 +-- ts/components/CallingPip.tsx | 24 ++-- ts/components/InContactsIcon.tsx | 28 ++--- ts/components/IncomingCallBar.tsx | 24 ++-- ts/components/Tooltip.stories.tsx | 80 ++++++++++++++ ts/components/Tooltip.tsx | 81 ++++++++++++++ ts/util/lint/exceptions.json | 6 +- yarn.lock | 7 -- 16 files changed, 339 insertions(+), 235 deletions(-) delete mode 100644 patches/react-tooltip-lite+1.12.0.patch create mode 100644 ts/components/Tooltip.stories.tsx create mode 100644 ts/components/Tooltip.tsx diff --git a/ACKNOWLEDGMENTS.md b/ACKNOWLEDGMENTS.md index f4ff13b7b5c5..39657cac84c6 100644 --- a/ACKNOWLEDGMENTS.md +++ b/ACKNOWLEDGMENTS.md @@ -2223,30 +2223,6 @@ Signal Desktop makes use of the following open source projects. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -## react-tooltip-lite - - MIT License - - Copyright (c) 2019 Benny Sidelinger - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - ## react-virtualized The MIT License (MIT) diff --git a/package.json b/package.json index 59c34a1dc126..972824582771 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,6 @@ "react-redux": "7.1.0", "react-router-dom": "5.0.1", "react-sortable-hoc": "1.9.1", - "react-tooltip-lite": "1.12.0", "react-virtualized": "9.21.0", "read-last-lines": "1.3.0", "redux": "4.0.1", diff --git a/patches/react-tooltip-lite+1.12.0.patch b/patches/react-tooltip-lite+1.12.0.patch deleted file mode 100644 index 502a73534bfa..000000000000 --- a/patches/react-tooltip-lite+1.12.0.patch +++ /dev/null @@ -1,60 +0,0 @@ -diff --git a/node_modules/react-tooltip-lite/dist/index.js b/node_modules/react-tooltip-lite/dist/index.js -index 32ce07d..6461913 100644 ---- a/node_modules/react-tooltip-lite/dist/index.js -+++ b/node_modules/react-tooltip-lite/dist/index.js -@@ -80,7 +80,7 @@ function (_React$Component) { - - _this.state = { - showTip: false, -- hasHover: false, -+ hasHover: 0, - ignoreShow: false, - hasBeenShown: false - }; -@@ -232,7 +232,7 @@ function (_React$Component) { - var _this3 = this; - - this.setState({ -- hasHover: false -+ hasHover: 0 - }); - - if (this.state.showTip) { -@@ -250,7 +250,7 @@ function (_React$Component) { - value: function startHover() { - if (!this.state.ignoreShow) { - this.setState({ -- hasHover: true -+ hasHover: (this.state.hasHover || 0) + 1, - }); - clearTimeout(this.hoverTimeout); - this.hoverTimeout = setTimeout(this.checkHover, this.props.hoverDelay); -@@ -260,7 +260,7 @@ function (_React$Component) { - key: "endHover", - value: function endHover() { - this.setState({ -- hasHover: false -+ hasHover: Math.max((this.state.hasHover || 0) - 1, 0), - }); - clearTimeout(this.hoverTimeout); - this.hoverTimeout = setTimeout(this.checkHover, this.props.mouseOutDelay || this.props.hoverDelay); -@@ -268,7 +268,7 @@ function (_React$Component) { - }, { - key: "checkHover", - value: function checkHover() { -- this.state.hasHover ? this.showTip() : this.hideTip(); -+ this.state.hasHover > 0 ? this.showTip() : this.hideTip(); - } - }, { - key: "render", -@@ -330,7 +330,9 @@ function (_React$Component) { - props[eventToggle] = this.toggleTip; // only use hover if they don't have a toggle event - } else if (useHover && !isControlledByProps) { - props.onMouseEnter = this.startHover; -- props.onMouseLeave = tipContentHover || mouseOutDelay ? this.endHover : this.hideTip; -+ props.onMouseLeave = this.endHover; -+ props.onFocus = this.startHover; -+ props.onBlur = this.endHover; - props.onTouchStart = this.targetTouchStart; - props.onTouchEnd = this.targetTouchEnd; - diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index e15efc87a80a..aa74808d1b78 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -2782,24 +2782,34 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', } } -.module-in-contacts-icon__tooltip { - .react-tooltip-lite { +span.module-in-contacts-icon__tooltip { + /* Written in this way to add more specificity and avoid !important */ + .module-tooltip { color: $color-white; background-color: $ultramarine-ui-light; - } - .react-tooltip-lite-arrow { - border-color: $ultramarine-ui-light; - } - - @include dark-theme { - .react-tooltip-lite { - color: $color-white; - background-color: $ultramarine-ui-light; + &[data-placement='top'] { + .module-tooltip-arrow::after { + border-top-color: $ultramarine-ui-light; + } } - .react-tooltip-lite-arrow { - border-color: $ultramarine-ui-light; + &[data-placement='right'] { + .module-tooltip-arrow::after { + border-right-color: $ultramarine-ui-light; + } + } + + &[data-placement='bottom'] { + .module-tooltip-arrow::after { + border-bottom-color: $ultramarine-ui-light; + } + } + + &[data-placement='left'] { + .module-tooltip-arrow::after { + border-left-color: $ultramarine-ui-light; + } } } } @@ -6522,8 +6532,8 @@ button.module-image__border-overlay:focus { margin-top: 54px; overflow: scroll; padding: 14px; - padding-bottom: 0; width: 280px; + padding-bottom: 0; &__overlay { display: flex; @@ -10183,10 +10193,13 @@ $contact-modal-padding: 18px; } } -/* Third-party module: react-tooltip-lite */ - -.react-tooltip-lite { +.module-tooltip { border-radius: 8px; + display: inline-block; + padding: 8px 21px; + position: fixed; + text-align: center; + z-index: 999; @include light-theme { background-color: $color-gray-02; @@ -10196,14 +10209,96 @@ $contact-modal-padding: 18px; background-color: $color-gray-65; color: $color-gray-05; } -} -.react-tooltip-lite-arrow { - @include light-theme { - border-color: $color-gray-02; + .module-tooltip-arrow { + position: absolute; } - @include dark-theme { - border-color: $color-gray-65; + + .module-tooltip-arrow::after { + border: solid 6px transparent; + content: ''; + display: block; + height: 0; + margin-left: -6px; + margin-top: -6px; + position: absolute; + width: 0; + } + + &[data-placement='top'] { + margin-bottom: 12px; + + .module-tooltip-arrow { + bottom: 0; + } + + .module-tooltip-arrow::after { + bottom: -12px; + + @include light-theme { + border-top-color: $color-gray-02; + } + @include dark-theme { + border-top-color: $color-gray-65; + } + } + } + + &[data-placement='right'] { + margin-left: 12px; + + .module-tooltip-arrow { + left: 0; + } + + .module-tooltip-arrow::after { + left: -6px; + + @include light-theme { + border-right-color: $color-gray-02; + } + @include dark-theme { + border-right-color: $color-gray-65; + } + } + } + + &[data-placement='bottom'] { + margin-top: 12px; + + .module-tooltip-arrow { + top: 0; + } + + .module-tooltip-arrow::after { + top: -6px; + + @include light-theme { + border-bottom-color: $color-gray-02; + } + @include dark-theme { + border-bottom-color: $color-gray-65; + } + } + } + + &[data-placement='left'] { + margin-right: 12px; + + .module-tooltip-arrow { + right: 0; + } + + .module-tooltip-arrow::after { + right: -12px; + + @include light-theme { + border-left-color: $color-gray-02; + } + @include dark-theme { + border-left-color: $color-gray-65; + } + } } } diff --git a/ts/components/CallScreen.tsx b/ts/components/CallScreen.tsx index b76fa4aa18a4..9a9d36dfd8da 100644 --- a/ts/components/CallScreen.tsx +++ b/ts/components/CallScreen.tsx @@ -253,13 +253,11 @@ export const CallScreen: React.FC = ({ buttonType={videoButtonType} i18n={i18n} onClick={toggleVideo} - tooltipDistance={24} /> = ({ onClick={() => { hangUp({ conversationId: conversation.id }); }} - tooltipDistance={24} />
= {}): PropsType => ({ onClick: action('on-click'), tooltipDirection: select( 'tooltipDirection', - TooltipDirection, - overrideProps.tooltipDirection || TooltipDirection.DOWN - ), - tooltipDistance: number( - 'tooltipDistance', - overrideProps.tooltipDistance || 16 + TooltipPlacement, + overrideProps.tooltipDirection || TooltipPlacement.Bottom ), }); @@ -87,7 +79,7 @@ story.add('Video Disabled', () => { story.add('Tooltip right', () => { const props = createProps({ - tooltipDirection: TooltipDirection.RIGHT, + tooltipDirection: TooltipPlacement.Right, }); return ; }); diff --git a/ts/components/CallingButton.tsx b/ts/components/CallingButton.tsx index 5eac291d83e0..56ef0a7ca928 100644 --- a/ts/components/CallingButton.tsx +++ b/ts/components/CallingButton.tsx @@ -3,16 +3,9 @@ import React from 'react'; import classNames from 'classnames'; -import Tooltip from 'react-tooltip-lite'; +import { Tooltip, TooltipPlacement } from './Tooltip'; import { LocalizerType } from '../types/Util'; -export enum TooltipDirection { - UP = 'up', - RIGHT = 'right', - DOWN = 'down', - LEFT = 'left', -} - export enum CallingButtonType { AUDIO_DISABLED = 'AUDIO_DISABLED', AUDIO_OFF = 'AUDIO_OFF', @@ -27,16 +20,14 @@ export type PropsType = { buttonType: CallingButtonType; i18n: LocalizerType; onClick: () => void; - tooltipDirection?: TooltipDirection; - tooltipDistance?: number; + tooltipDirection?: TooltipPlacement; }; export const CallingButton = ({ buttonType, i18n, onClick, - tooltipDirection = TooltipDirection.DOWN, - tooltipDistance = 16, + tooltipDirection, }: PropsType): JSX.Element => { let classNameSuffix = ''; let tooltipContent = ''; @@ -69,21 +60,15 @@ export const CallingButton = ({ ); return ( - + + ); }; diff --git a/ts/components/CallingHeader.tsx b/ts/components/CallingHeader.tsx index be39101947e2..411d5cd30261 100644 --- a/ts/components/CallingHeader.tsx +++ b/ts/components/CallingHeader.tsx @@ -2,8 +2,8 @@ // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; -import Tooltip from 'react-tooltip-lite'; import { LocalizerType } from '../types/Util'; +import { Tooltip } from './Tooltip'; export type PropsType = { canPip?: boolean; @@ -34,52 +34,39 @@ export const CallingHeader = ({ {isGroupCall ? (
) : null}
- +
{canPip && (
- +
diff --git a/ts/components/CallingLobby.tsx b/ts/components/CallingLobby.tsx index 897ae5117518..d779ef888b42 100644 --- a/ts/components/CallingLobby.tsx +++ b/ts/components/CallingLobby.tsx @@ -7,11 +7,8 @@ import { SetLocalPreviewType, SetLocalVideoType, } from '../state/ducks/calling'; -import { - CallingButton, - CallingButtonType, - TooltipDirection, -} from './CallingButton'; +import { CallingButton, CallingButtonType } from './CallingButton'; +import { TooltipPlacement } from './Tooltip'; import { CallBackgroundBlur } from './CallBackgroundBlur'; import { CallingHeader } from './CallingHeader'; import { Spinner } from './Spinner'; @@ -145,15 +142,13 @@ export const CallingLobby = ({ buttonType={videoButtonType} i18n={i18n} onClick={toggleVideo} - tooltipDirection={TooltipDirection.UP} - tooltipDistance={24} + tooltipDirection={TooltipPlacement.Top} />
diff --git a/ts/components/CallingPip.tsx b/ts/components/CallingPip.tsx index d0316d0b52e7..d3d6e4714ba2 100644 --- a/ts/components/CallingPip.tsx +++ b/ts/components/CallingPip.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; -import Tooltip from 'react-tooltip-lite'; +import { Tooltip } from './Tooltip'; import { CallingPipRemoteVideo } from './CallingPipRemoteVideo'; import { LocalizerType } from '../types/Util'; import { ConversationType } from '../state/ducks/conversations'; @@ -182,27 +182,23 @@ export const CallingPip = ({ ) : null}
+ +
); diff --git a/ts/components/InContactsIcon.tsx b/ts/components/InContactsIcon.tsx index 282737d3f180..07a4de85b5b9 100644 --- a/ts/components/InContactsIcon.tsx +++ b/ts/components/InContactsIcon.tsx @@ -2,8 +2,8 @@ // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; -import Tooltip from 'react-tooltip-lite'; +import { Tooltip } from './Tooltip'; import { LocalizerType } from '../types/Util'; type PropsType = { @@ -15,22 +15,16 @@ export const InContactsIcon = (props: PropsType): JSX.Element => { /* eslint-disable jsx-a11y/no-noninteractive-tabindex */ return ( - - - + + + + + ); /* eslint-enable jsx-a11y/no-noninteractive-tabindex */ }; diff --git a/ts/components/IncomingCallBar.tsx b/ts/components/IncomingCallBar.tsx index f4b1400f061e..83236f087c7a 100644 --- a/ts/components/IncomingCallBar.tsx +++ b/ts/components/IncomingCallBar.tsx @@ -2,8 +2,8 @@ // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; -import Tooltip from 'react-tooltip-lite'; import { Avatar } from './Avatar'; +import { Tooltip } from './Tooltip'; import { ContactName } from './conversation/ContactName'; import { LocalizerType } from '../types/Util'; import { ColorType } from '../types/Colors'; @@ -41,22 +41,16 @@ const CallButton = ({ tooltipContent, }: CallButtonProps): JSX.Element => { return ( - + + ); }; diff --git a/ts/components/Tooltip.stories.tsx b/ts/components/Tooltip.stories.tsx new file mode 100644 index 000000000000..13ef7d6efbfc --- /dev/null +++ b/ts/components/Tooltip.stories.tsx @@ -0,0 +1,80 @@ +// Copyright 2020 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import * as React from 'react'; +import { storiesOf } from '@storybook/react'; +import { select } from '@storybook/addon-knobs'; + +import { Tooltip, TooltipPlacement, PropsType } from './Tooltip'; + +const createProps = (overrideProps: Partial = {}): PropsType => ({ + content: overrideProps.content || 'Hello World', + direction: select('direction', TooltipPlacement, overrideProps.direction), + sticky: overrideProps.sticky, +}); + +const story = storiesOf('Components/Tooltip', module); + +const Trigger = ( + + Trigger + +); + +story.add('Top', () => ( + + {Trigger} + +)); + +story.add('Right', () => ( + + {Trigger} + +)); + +story.add('Bottom', () => ( + + {Trigger} + +)); + +story.add('Left', () => ( + + {Trigger} + +)); + +story.add('Sticky', () => ( + + {Trigger} + +)); diff --git a/ts/components/Tooltip.tsx b/ts/components/Tooltip.tsx new file mode 100644 index 000000000000..72912a76b9e4 --- /dev/null +++ b/ts/components/Tooltip.tsx @@ -0,0 +1,81 @@ +// Copyright 2020 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; +import { Manager, Reference, Popper } from 'react-popper'; + +export enum TooltipPlacement { + Top = 'top', + Right = 'right', + Bottom = 'bottom', + Left = 'left', +} + +export type PropsType = { + content: string | JSX.Element; + direction?: TooltipPlacement; + sticky?: boolean; +}; + +export const Tooltip: React.FC = ({ + children, + content, + direction, + sticky, +}) => { + const isSticky = Boolean(sticky); + const [showTooltip, setShowTooltip] = React.useState(isSticky); + + return ( + + + {({ ref }) => ( + { + if (!isSticky) { + setShowTooltip(false); + } + }} + onFocus={() => { + if (!isSticky) { + setShowTooltip(true); + } + }} + onMouseEnter={() => { + if (!isSticky) { + setShowTooltip(true); + } + }} + onMouseLeave={() => { + if (!isSticky) { + setShowTooltip(false); + } + }} + ref={ref} + > + {children} + + )} + + + {({ arrowProps, placement, ref, style }) => + showTooltip && ( +
+ {content} +
+
+ ) + } + + + ); +}; diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 8e0a16b733e4..ade88df3038f 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -14391,7 +14391,7 @@ "rule": "React-useRef", "path": "ts/components/CallingLobby.js", "line": " const localVideoRef = react_1.default.useRef(null);", - "lineNumber": 14, + "lineNumber": 15, "reasonCategory": "usageTrusted", "updated": "2020-10-26T19:12:24.410Z", "reasonDetail": "Used to get the local video element for rendering." @@ -14400,7 +14400,7 @@ "rule": "React-useRef", "path": "ts/components/CallingLobby.tsx", "line": " const localVideoRef = React.useRef(null);", - "lineNumber": 61, + "lineNumber": 58, "reasonCategory": "usageTrusted", "updated": "2020-10-26T19:12:24.410Z", "reasonDetail": "Used to get the local video element for rendering." @@ -15176,4 +15176,4 @@ "reasonCategory": "falseMatch", "updated": "2020-09-08T23:07:22.682Z" } -] \ No newline at end of file +] diff --git a/yarn.lock b/yarn.lock index 4c31ceb18851..ab01c2599faa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13508,13 +13508,6 @@ react-textarea-autosize@^7.1.0: "@babel/runtime" "^7.1.2" prop-types "^15.6.0" -react-tooltip-lite@1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/react-tooltip-lite/-/react-tooltip-lite-1.12.0.tgz#f6cd1323cdd9f5f80dd0e71a30ef59f401dee9ba" - integrity sha512-QjDnmDmjtLNKvLY6bzUOG8W6ZDBTiE4UXugGzClOQEGvMvbkJn2GvZvLwRaxsN/GCx7589RgbGaESMiJAm+zWg== - dependencies: - prop-types "^15.5.8" - react-transition-group@^2.2.1: version "2.9.0" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"