From d0e8fbd5a637a8f9492c8aa07155dc0a694a6f23 Mon Sep 17 00:00:00 2001 From: Josh Perez <60019601+josh-signal@users.noreply.github.com> Date: Thu, 14 Oct 2021 12:52:42 -0400 Subject: [PATCH] Animates ModalHost overlay --- package.json | 2 +- stylesheets/_modules.scss | 16 +- ts/components/ConfirmationDialog.tsx | 22 +- ts/components/ForwardMessageModal.tsx | 368 +++++++++++++------------- ts/components/Lightbox.tsx | 6 + ts/components/Modal.tsx | 33 ++- ts/components/ModalHost.tsx | 38 ++- ts/hooks/useAnimated.tsx | 73 +++-- yarn.lock | 88 +++--- 9 files changed, 347 insertions(+), 299 deletions(-) diff --git a/package.json b/package.json index 70e6bc471e6..07d91613a93 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ }, "dependencies": { "@popperjs/core": "2.9.2", - "@react-spring/web": "9.2.4", + "@react-spring/web": "9.2.6", "@signalapp/signal-client": "0.9.5", "@sindresorhus/is": "0.8.0", "abort-controller": "3.0.0", diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 084dd35dabe..f732bcf34b4 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -8725,22 +8725,26 @@ button.module-image__border-overlay:focus { .module-modal-host__overlay { background: $color-black-alpha-40; - position: absolute; - height: 100vh; - width: 100vw; - left: 0; + position: absolute; top: 0; - + width: 100vw; z-index: 2; +} +.module-modal-host__container { display: flex; flex-direction: column; + height: 100vh; justify-content: center; - + left: 0; overflow: hidden; padding: 20px; + position: absolute; + top: 0; + width: 100vw; + z-index: 2; } // Module: GroupV2 Join Dialog diff --git a/ts/components/ConfirmationDialog.tsx b/ts/components/ConfirmationDialog.tsx index 07e34c988e4..a598cd93f21 100644 --- a/ts/components/ConfirmationDialog.tsx +++ b/ts/components/ConfirmationDialog.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import React, { MouseEvent, useCallback } from 'react'; +import { animated } from '@react-spring/web'; import { Button, ButtonVariant } from './Button'; import { LocalizerType } from '../types/Util'; import { ModalHost } from './ModalHost'; @@ -63,17 +64,10 @@ export const ConfirmationDialog = React.memo( title, hasXButton, }: Props) => { - const { close, renderAnimation } = useAnimated( - { - from: { opacity: 0, transform: 'scale(0.25)' }, - enter: { opacity: 1, transform: 'scale(1)' }, - leave: { opacity: 0, onRest: () => onClose() }, - config: { - duration: 150, - }, - }, - onClose - ); + const { close, overlayStyles, modalStyles } = useAnimated(onClose, { + getFrom: () => ({ opacity: 0, transform: 'scale(0.25)' }), + getTo: isOpen => ({ opacity: isOpen ? 1 : 0, transform: 'scale(1)' }), + }); const cancelAndClose = useCallback(() => { if (onCancel) { @@ -94,8 +88,8 @@ export const ConfirmationDialog = React.memo( const hasActions = Boolean(actions.length); return ( - - {renderAnimation( + + - )} + ); } diff --git a/ts/components/ForwardMessageModal.tsx b/ts/components/ForwardMessageModal.tsx index 0de436740d7..4bbedd1c2ca 100644 --- a/ts/components/ForwardMessageModal.tsx +++ b/ts/components/ForwardMessageModal.tsx @@ -11,6 +11,7 @@ import React, { } from 'react'; import Measure, { MeasuredComponentProps } from 'react-measure'; import { noop } from 'lodash'; +import { animated } from '@react-spring/web'; import classNames from 'classnames'; import { AttachmentList } from './conversation/AttachmentList'; @@ -199,20 +200,16 @@ export const ForwardMessageModal: FunctionComponent = ({ [contactLookup, selectedContacts, setSelectedContacts] ); - const { close, renderAnimation } = useAnimated( - { - from: { opacity: 0, transform: 'translateY(48px)' }, - enter: { opacity: 1, transform: 'translateY(0px)' }, - leave: { - opacity: 0, - transform: 'translateY(48px)', - }, - config: { - duration: 200, - }, - }, - onClose - ); + const { close, modalStyles, overlayStyles } = useAnimated(onClose, { + getFrom: () => ({ opacity: 0, transform: 'translateY(48px)' }), + getTo: isOpen => + isOpen + ? { opacity: 1, transform: 'translateY(0px)' } + : { + opacity: 0, + transform: 'translateY(48px)', + }, + }); const handleBackOrClose = useCallback(() => { if (isEditingMessage) { @@ -265,188 +262,189 @@ export const ForwardMessageModal: FunctionComponent = ({ {i18n('GroupV2--cannot-send')} )} - - {renderAnimation( -
-
- {isEditingMessage ? ( - - ) : ( -
+ + +
{isEditingMessage ? ( -
- {linkPreview ? ( -
- removeLinkPreview()} - title={linkPreview.title} - /> -
- ) : null} - {attachmentsToForward && attachmentsToForward.length ? ( - setIsEditingMessage(false)} + type="button" + > +   + + ) : ( +
+ {isEditingMessage ? ( +
+ {linkPreview ? ( +
+ { - const newAttachments = attachmentsToForward.filter( - currentAttachment => currentAttachment !== attachment - ); - setAttachmentsToForward(newAttachments); - }} + image={linkPreview.image} + onClose={() => removeLinkPreview()} + title={linkPreview.title} /> - ) : null} -
- + ) : null} + {attachmentsToForward && attachmentsToForward.length ? ( + { + const newAttachments = attachmentsToForward.filter( + currentAttachment => currentAttachment !== attachment + ); + setAttachmentsToForward(newAttachments); + }} + /> + ) : null} +
+ { + setMessageBodyText(messageText); + onEditorStateChange(messageText, bodyRanges, caretLocation); + }} + onPickEmoji={onPickEmoji} + onSubmit={forwardMessage} + onTextTooLong={onTextTooLong} + /> +
+ { - setMessageBodyText(messageText); - onEditorStateChange( - messageText, - bodyRanges, - caretLocation - ); - }} - onPickEmoji={onPickEmoji} - onSubmit={forwardMessage} - onTextTooLong={onTextTooLong} + onClose={focusTextEditInput} + onPickEmoji={insertEmoji} + onSetSkinTone={onSetSkinTone} + recentEmojis={recentEmojis} + skinTone={skinTone} /> -
- -
- ) : ( -
- { - setSearchTerm(event.target.value); +
+ ) : ( +
+ { + setSearchTerm(event.target.value); + }} + ref={inputRef} + value={searchTerm} + /> + {candidateConversations.length ? ( + + {({ contentRect, measureRef }: MeasuredComponentProps) => { + // We disable this ESLint rule because we're capturing a bubbled + // keydown event. See [this note in the jsx-a11y docs][0]. + // + // [0]: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/c275964f52c35775208bd00cb612c6f82e42e34f/docs/rules/no-static-element-interactions.md#case-the-event-handler-is-only-being-used-to-capture-bubbled-events + /* eslint-disable jsx-a11y/no-static-element-interactions */ + return ( +
+ { + if ( + disabledReason !== + ContactCheckboxDisabledReason.MaximumContactsSelected + ) { + toggleSelectedConversation(conversationId); + } + }} + onSelectConversation={shouldNeverBeCalled} + renderMessageSearchResult={() => { + shouldNeverBeCalled(); + return
; + }} + rowCount={rowCount} + shouldRecomputeRowHeights={false} + showChooseGroupMembers={shouldNeverBeCalled} + startNewConversationFromPhoneNumber={ + shouldNeverBeCalled + } + /> +
+ ); + /* eslint-enable jsx-a11y/no-static-element-interactions */ }} - ref={inputRef} - value={searchTerm} + + ) : ( +
+ {i18n('noContactsFound')} +
+ )} +
+ )} +
+
+ {Boolean(selectedContacts.length) && + selectedContacts.map(contact => contact.title).join(', ')} +
+
+ {isEditingMessage || !isMessageEditable ? ( +
+ ) : ( +
- )} + ); diff --git a/ts/components/Lightbox.tsx b/ts/components/Lightbox.tsx index a87d0248a59..1ff7d6482a8 100644 --- a/ts/components/Lightbox.tsx +++ b/ts/components/Lightbox.tsx @@ -47,6 +47,12 @@ const INITIAL_IMAGE_TRANSFORM = { scale: 1, translateX: 0, translateY: 0, + config: { + clamp: true, + friction: 20, + mass: 0.5, + tension: 350, + }, }; export function Lightbox({ diff --git a/ts/components/Modal.tsx b/ts/components/Modal.tsx index df16c44878d..29a014c4089 100644 --- a/ts/components/Modal.tsx +++ b/ts/components/Modal.tsx @@ -5,6 +5,7 @@ import React, { ReactElement, ReactNode, useRef, useState } from 'react'; import Measure, { ContentRect, MeasuredComponentProps } from 'react-measure'; import classNames from 'classnames'; import { noop } from 'lodash'; +import { animated } from '@react-spring/web'; import { LocalizerType } from '../types/Util'; import { ModalHost } from './ModalHost'; @@ -42,24 +43,22 @@ export function Modal({ title, theme, }: Readonly): ReactElement { - const { close, renderAnimation } = useAnimated( - { - from: { opacity: 0, transform: 'translateY(48px)' }, - enter: { opacity: 1, transform: 'translateY(0px)' }, - leave: { - opacity: 0, - transform: 'translateY(48px)', - }, - config: { - duration: 200, - }, - }, - onClose - ); + const { close, modalStyles, overlayStyles } = useAnimated(onClose, { + getFrom: () => ({ opacity: 0, transform: 'translateY(48px)' }), + getTo: isOpen => + isOpen + ? { opacity: 1, transform: 'translateY(0px)' } + : { opacity: 0, transform: 'translateY(48px)' }, + }); return ( - - {renderAnimation( + + {children} - )} + ); } diff --git a/ts/components/ModalHost.tsx b/ts/components/ModalHost.tsx index 465a75becde..966dfffe151 100644 --- a/ts/components/ModalHost.tsx +++ b/ts/components/ModalHost.tsx @@ -5,20 +5,30 @@ import React, { useEffect } from 'react'; import classNames from 'classnames'; import { createPortal } from 'react-dom'; import FocusTrap from 'focus-trap-react'; +import { SpringValues, animated } from '@react-spring/web'; +import type { ModalConfigType } from '../hooks/useAnimated'; import { Theme, themeClassName } from '../util/theme'; import { useEscapeHandling } from '../hooks/useEscapeHandling'; export type PropsType = { + readonly children: React.ReactElement; readonly noMouseClose?: boolean; - readonly onEscape?: () => unknown; readonly onClose: () => unknown; - readonly children: React.ReactElement; + readonly onEscape?: () => unknown; + readonly overlayStyles?: SpringValues; readonly theme?: Theme; }; export const ModalHost = React.memo( - ({ onEscape, onClose, children, noMouseClose, theme }: PropsType) => { + ({ + children, + noMouseClose, + onClose, + onEscape, + theme, + overlayStyles, + }: PropsType) => { const [root, setRoot] = React.useState(null); const [isMouseDown, setIsMouseDown] = React.useState(false); @@ -64,16 +74,18 @@ export const ModalHost = React.memo( allowOutsideClick: false, }} > -
- {children} +
+ +
{children}
, root diff --git a/ts/hooks/useAnimated.tsx b/ts/hooks/useAnimated.tsx index 48f484d20d2..e0d15df0962 100644 --- a/ts/hooks/useAnimated.tsx +++ b/ts/hooks/useAnimated.tsx @@ -1,37 +1,72 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React, { useState, ReactElement } from 'react'; -import { animated, useTransition, UseTransitionProps } from '@react-spring/web'; -import cubicBezier from 'bezier-easing'; +import { useState } from 'react'; +import { + SpringValues, + useChain, + useSpring, + useSpringRef, +} from '@react-spring/web'; -export function useAnimated>( - props: UseTransitionProps, - onClose: () => unknown +export type ModalConfigType = { + opacity: number; + transform?: string; +}; + +export function useAnimated( + onClose: () => unknown, + { + getFrom, + getTo, + }: { + getFrom: (isOpen: boolean) => ModalConfigType; + getTo: (isOpen: boolean) => ModalConfigType; + } ): { close: () => unknown; - renderAnimation: (children: ReactElement) => JSX.Element; + modalStyles: SpringValues; + overlayStyles: SpringValues; } { const [isOpen, setIsOpen] = useState(true); - const transitions = useTransition(isOpen, { - ...props, - leave: { - ...props.leave, - onRest: () => onClose(), + const modalRef = useSpringRef(); + + const modalStyles = useSpring({ + from: getFrom(isOpen), + to: getTo(isOpen), + onRest: () => { + if (!isOpen) { + onClose(); + } + }, + config: { + clamp: true, + friction: 20, + mass: 0.5, + tension: 350, }, + ref: modalRef, + }); + + const overlayRef = useSpringRef(); + + const overlayStyles = useSpring({ + from: { opacity: 0 }, + to: { opacity: isOpen ? 1 : 0 }, config: { - duration: 200, - easing: cubicBezier(0.17, 0.17, 0, 1), - ...props.config, + clamp: true, + friction: 22, + tension: 360, }, + ref: overlayRef, }); + useChain(isOpen ? [overlayRef, modalRef] : [modalRef, overlayRef]); + return { close: () => setIsOpen(false), - renderAnimation: children => - transitions((style, item) => - item ? {children} : null - ), + overlayStyles, + modalStyles, }; } diff --git a/yarn.lock b/yarn.lock index cf7b6535ee7..bdf01558a15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1671,50 +1671,50 @@ react-lifecycles-compat "^3.0.4" warning "^3.0.0" -"@react-spring/animated@~9.2.0": - version "9.2.4" - resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.2.4.tgz#062ecc0fdfef89f2541a42d8500428b70035f879" - integrity sha512-AfV6ZM8pCCAT29GY5C8/1bOPjZrv/7kD0vedjiE/tEYvNDwg9GlscrvsTViWR2XykJoYrDfdkYArrldWpsCJ5g== - dependencies: - "@react-spring/shared" "~9.2.0" - "@react-spring/types" "~9.2.0" - -"@react-spring/core@~9.2.0": - version "9.2.4" - resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.2.4.tgz#275a4a065e3a315a4f5fb28c9a6f62ce718c25d6" - integrity sha512-R+PwyfsjiuYCWqaTTfCpYpRmsP0h87RNm7uxC1Uxy7QAHUfHEm2sAHn+AdHPwq/MbVwDssVT8C5yf2WGcqiXGg== - dependencies: - "@react-spring/animated" "~9.2.0" - "@react-spring/shared" "~9.2.0" - "@react-spring/types" "~9.2.0" - -"@react-spring/rafz@~9.2.0": - version "9.2.4" - resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.2.4.tgz#44793e9adc14dd0dcd1573d094368af11a89d73a" - integrity sha512-SOKf9eue+vAX+DGo7kWYNl9i9J3gPUlQjifIcV9Bzw9h3i30wPOOP0TjS7iMG/kLp2cdHQYDNFte6nt23VAZkQ== - -"@react-spring/shared@~9.2.0": - version "9.2.4" - resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.2.4.tgz#f9cc66ac5308a77293330a18518e34121f4008c1" - integrity sha512-ZEr4l2BxmyFRUvRA2VCkPfCJii4E7cGkwbjmTBx1EmcGrOnde/V2eF5dxqCTY3k35QuCegkrWe0coRJVkh8q2Q== - dependencies: - "@react-spring/rafz" "~9.2.0" - "@react-spring/types" "~9.2.0" - -"@react-spring/types@~9.2.0": - version "9.2.4" - resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.2.4.tgz#2365ce9d761f548a9adcb2cd68714bf26765a5de" - integrity sha512-zHUXrWO8nweUN/ISjrjqU7GgXXvoEbFca1CgiE0TY0H/dqJb3l+Rhx8ecPVNYimzFg3ZZ1/T0egpLop8SOv4aA== - -"@react-spring/web@9.2.4": - version "9.2.4" - resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.2.4.tgz#c6d5464a954bfd0d7bc90117050f796a95ebfa08" - integrity sha512-vtPvOalLFvuju/MDBtoSnCyt0xXSL6Amyv82fljOuWPl1yGd4M1WteijnYL9Zlriljl0a3oXcPunAVYTD9dbDQ== - dependencies: - "@react-spring/animated" "~9.2.0" - "@react-spring/core" "~9.2.0" - "@react-spring/shared" "~9.2.0" - "@react-spring/types" "~9.2.0" +"@react-spring/animated@~9.2.6-beta.0": + version "9.2.6" + resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.2.6.tgz#58f30fb75d8bfb7ccbc156cfd6b974a8f3dfd54e" + integrity sha512-xjL6nmixYNDvnpTs1FFMsMfSC0tURwPCU3b2jWNriYGLfwZ7c/TcyaEZA7yiNnmdFnuR3f3Z27AqIgaFC083Cw== + dependencies: + "@react-spring/shared" "~9.2.6-beta.0" + "@react-spring/types" "~9.2.6-beta.0" + +"@react-spring/core@~9.2.6-beta.0": + version "9.2.6" + resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.2.6.tgz#ae22338fe55d070caf03abb4293b5519ba620d93" + integrity sha512-uPHUxmu+w6mHJrfQTMtmGJ8iZEwiVxz9kH7dRyk69bkZJt9z+w0Oj3UF4J3VcECZsbm3HRhN2ogXSAaqGjwhQw== + dependencies: + "@react-spring/animated" "~9.2.6-beta.0" + "@react-spring/shared" "~9.2.6-beta.0" + "@react-spring/types" "~9.2.6-beta.0" + +"@react-spring/rafz@~9.2.6-beta.0": + version "9.2.6" + resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.2.6.tgz#d97484003875bf5fb5e6ec22dee97cc208363e48" + integrity sha512-62SivLKEpo7EfHPkxO5J3g9Cr9LF6+1A1RVOMJhkcpEYtbdbmma/d63Xp8qpMPEpk7uuWxaTb6jjyxW33pW3sg== + +"@react-spring/shared@~9.2.6-beta.0": + version "9.2.6" + resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.2.6.tgz#2c84e62cc0cfbbbbeb5546acd46c1f4b248bc562" + integrity sha512-Qrm9fopKG/RxZ3Rw+4euhrpnB3uXSyiON9skHbcBfmkkzagpkUR66MX1YLrhHw0UchcZuSDnXs0Lonzt1rpWag== + dependencies: + "@react-spring/rafz" "~9.2.6-beta.0" + "@react-spring/types" "~9.2.6-beta.0" + +"@react-spring/types@~9.2.6-beta.0": + version "9.2.6" + resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.2.6.tgz#f60722fcf9f8492ae16d0bdc47f0ea3c2a16d2cf" + integrity sha512-l7mCw182DtDMnCI8CB9orgTAEoFZRtdQ6aS6YeEAqYcy3nQZPmPggIHH9DxyLw7n7vBPRSzu9gCvUMgXKpTflg== + +"@react-spring/web@9.2.6": + version "9.2.6" + resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.2.6.tgz#c4fba69e1b1b43bd1d6a62346530cfb07f2be09b" + integrity sha512-0HkRsEYR/CO3Uw46FWDWaF2wg2rUXcWE2R9AoZXthEYLUn5w9uE1mf2Jel7BxBxWGQ73owkqSQv+klA1Hb+ViQ== + dependencies: + "@react-spring/animated" "~9.2.6-beta.0" + "@react-spring/core" "~9.2.6-beta.0" + "@react-spring/shared" "~9.2.6-beta.0" + "@react-spring/types" "~9.2.6-beta.0" "@signalapp/signal-client@0.9.5": version "0.9.5"