Animates ModalHost overlay
This commit is contained in:
parent
cfc5407d03
commit
d0e8fbd5a6
9 changed files with 340 additions and 292 deletions
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
<ModalHost onClose={close} theme={theme}>
|
||||
{renderAnimation(
|
||||
<ModalHost onClose={close} theme={theme} overlayStyles={overlayStyles}>
|
||||
<animated.div style={modalStyles}>
|
||||
<ModalWindow
|
||||
hasXButton={hasXButton}
|
||||
i18n={i18n}
|
||||
|
@ -129,7 +123,7 @@ export const ConfirmationDialog = React.memo(
|
|||
))}
|
||||
</Modal.ButtonFooter>
|
||||
</ModalWindow>
|
||||
)}
|
||||
</animated.div>
|
||||
</ModalHost>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<PropsType> = ({
|
|||
[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<PropsType> = ({
|
|||
{i18n('GroupV2--cannot-send')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
<ModalHost onEscape={handleBackOrClose} onClose={close}>
|
||||
{renderAnimation(
|
||||
<div className="module-ForwardMessageModal">
|
||||
<div
|
||||
className={classNames('module-ForwardMessageModal__header', {
|
||||
'module-ForwardMessageModal__header--edit': isEditingMessage,
|
||||
})}
|
||||
>
|
||||
{isEditingMessage ? (
|
||||
<button
|
||||
aria-label={i18n('back')}
|
||||
className="module-ForwardMessageModal__header--back"
|
||||
onClick={() => setIsEditingMessage(false)}
|
||||
type="button"
|
||||
>
|
||||
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
aria-label={i18n('close')}
|
||||
className="module-ForwardMessageModal__header--close"
|
||||
onClick={close}
|
||||
type="button"
|
||||
/>
|
||||
)}
|
||||
<h1>{i18n('forwardMessage')}</h1>
|
||||
</div>
|
||||
<ModalHost
|
||||
onEscape={handleBackOrClose}
|
||||
onClose={close}
|
||||
overlayStyles={overlayStyles}
|
||||
>
|
||||
<animated.div
|
||||
className="module-ForwardMessageModal"
|
||||
style={modalStyles}
|
||||
>
|
||||
<div
|
||||
className={classNames('module-ForwardMessageModal__header', {
|
||||
'module-ForwardMessageModal__header--edit': isEditingMessage,
|
||||
})}
|
||||
>
|
||||
{isEditingMessage ? (
|
||||
<div className="module-ForwardMessageModal__main-body">
|
||||
{linkPreview ? (
|
||||
<div className="module-ForwardMessageModal--link-preview">
|
||||
<StagedLinkPreview
|
||||
date={linkPreview.date || null}
|
||||
description={linkPreview.description || ''}
|
||||
domain={linkPreview.url}
|
||||
i18n={i18n}
|
||||
image={linkPreview.image}
|
||||
onClose={() => removeLinkPreview()}
|
||||
title={linkPreview.title}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{attachmentsToForward && attachmentsToForward.length ? (
|
||||
<AttachmentList
|
||||
attachments={attachmentsToForward}
|
||||
<button
|
||||
aria-label={i18n('back')}
|
||||
className="module-ForwardMessageModal__header--back"
|
||||
onClick={() => setIsEditingMessage(false)}
|
||||
type="button"
|
||||
>
|
||||
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
aria-label={i18n('close')}
|
||||
className="module-ForwardMessageModal__header--close"
|
||||
onClick={close}
|
||||
type="button"
|
||||
/>
|
||||
)}
|
||||
<h1>{i18n('forwardMessage')}</h1>
|
||||
</div>
|
||||
{isEditingMessage ? (
|
||||
<div className="module-ForwardMessageModal__main-body">
|
||||
{linkPreview ? (
|
||||
<div className="module-ForwardMessageModal--link-preview">
|
||||
<StagedLinkPreview
|
||||
date={linkPreview.date || null}
|
||||
description={linkPreview.description || ''}
|
||||
domain={linkPreview.url}
|
||||
i18n={i18n}
|
||||
onCloseAttachment={(attachment: AttachmentType) => {
|
||||
const newAttachments = attachmentsToForward.filter(
|
||||
currentAttachment => currentAttachment !== attachment
|
||||
);
|
||||
setAttachmentsToForward(newAttachments);
|
||||
}}
|
||||
image={linkPreview.image}
|
||||
onClose={() => removeLinkPreview()}
|
||||
title={linkPreview.title}
|
||||
/>
|
||||
) : null}
|
||||
<div className="module-ForwardMessageModal__text-edit-area">
|
||||
<CompositionInput
|
||||
clearQuotedMessage={shouldNeverBeCalled}
|
||||
draftText={messageBodyText}
|
||||
getQuotedMessage={noop}
|
||||
</div>
|
||||
) : null}
|
||||
{attachmentsToForward && attachmentsToForward.length ? (
|
||||
<AttachmentList
|
||||
attachments={attachmentsToForward}
|
||||
i18n={i18n}
|
||||
onCloseAttachment={(attachment: AttachmentType) => {
|
||||
const newAttachments = attachmentsToForward.filter(
|
||||
currentAttachment => currentAttachment !== attachment
|
||||
);
|
||||
setAttachmentsToForward(newAttachments);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div className="module-ForwardMessageModal__text-edit-area">
|
||||
<CompositionInput
|
||||
clearQuotedMessage={shouldNeverBeCalled}
|
||||
draftText={messageBodyText}
|
||||
getQuotedMessage={noop}
|
||||
i18n={i18n}
|
||||
inputApi={inputApiRef}
|
||||
large
|
||||
moduleClassName="module-ForwardMessageModal__input"
|
||||
onEditorStateChange={(
|
||||
messageText,
|
||||
bodyRanges,
|
||||
caretLocation
|
||||
) => {
|
||||
setMessageBodyText(messageText);
|
||||
onEditorStateChange(messageText, bodyRanges, caretLocation);
|
||||
}}
|
||||
onPickEmoji={onPickEmoji}
|
||||
onSubmit={forwardMessage}
|
||||
onTextTooLong={onTextTooLong}
|
||||
/>
|
||||
<div className="module-ForwardMessageModal__emoji">
|
||||
<EmojiButton
|
||||
i18n={i18n}
|
||||
inputApi={inputApiRef}
|
||||
large
|
||||
moduleClassName="module-ForwardMessageModal__input"
|
||||
onEditorStateChange={(
|
||||
messageText,
|
||||
bodyRanges,
|
||||
caretLocation
|
||||
) => {
|
||||
setMessageBodyText(messageText);
|
||||
onEditorStateChange(
|
||||
messageText,
|
||||
bodyRanges,
|
||||
caretLocation
|
||||
);
|
||||
}}
|
||||
onPickEmoji={onPickEmoji}
|
||||
onSubmit={forwardMessage}
|
||||
onTextTooLong={onTextTooLong}
|
||||
onClose={focusTextEditInput}
|
||||
onPickEmoji={insertEmoji}
|
||||
onSetSkinTone={onSetSkinTone}
|
||||
recentEmojis={recentEmojis}
|
||||
skinTone={skinTone}
|
||||
/>
|
||||
<div className="module-ForwardMessageModal__emoji">
|
||||
<EmojiButton
|
||||
i18n={i18n}
|
||||
onClose={focusTextEditInput}
|
||||
onPickEmoji={insertEmoji}
|
||||
onSetSkinTone={onSetSkinTone}
|
||||
recentEmojis={recentEmojis}
|
||||
skinTone={skinTone}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="module-ForwardMessageModal__main-body">
|
||||
<SearchInput
|
||||
disabled={candidateConversations.length === 0}
|
||||
placeholder={i18n('contactSearchPlaceholder')}
|
||||
onChange={event => {
|
||||
setSearchTerm(event.target.value);
|
||||
}}
|
||||
ref={inputRef}
|
||||
value={searchTerm}
|
||||
/>
|
||||
{candidateConversations.length ? (
|
||||
<Measure bounds>
|
||||
{({ 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 (
|
||||
<div
|
||||
className="module-ForwardMessageModal__list-wrapper"
|
||||
ref={measureRef}
|
||||
>
|
||||
<ConversationList
|
||||
dimensions={contentRect.bounds}
|
||||
getRow={getRow}
|
||||
i18n={i18n}
|
||||
onClickArchiveButton={shouldNeverBeCalled}
|
||||
onClickContactCheckbox={(
|
||||
conversationId: string,
|
||||
disabledReason:
|
||||
| undefined
|
||||
| ContactCheckboxDisabledReason
|
||||
) => {
|
||||
if (
|
||||
disabledReason !==
|
||||
ContactCheckboxDisabledReason.MaximumContactsSelected
|
||||
) {
|
||||
toggleSelectedConversation(conversationId);
|
||||
}
|
||||
}}
|
||||
onSelectConversation={shouldNeverBeCalled}
|
||||
renderMessageSearchResult={() => {
|
||||
shouldNeverBeCalled();
|
||||
return <div />;
|
||||
}}
|
||||
rowCount={rowCount}
|
||||
shouldRecomputeRowHeights={false}
|
||||
showChooseGroupMembers={shouldNeverBeCalled}
|
||||
startNewConversationFromPhoneNumber={
|
||||
shouldNeverBeCalled
|
||||
</div>
|
||||
) : (
|
||||
<div className="module-ForwardMessageModal__main-body">
|
||||
<SearchInput
|
||||
disabled={candidateConversations.length === 0}
|
||||
placeholder={i18n('contactSearchPlaceholder')}
|
||||
onChange={event => {
|
||||
setSearchTerm(event.target.value);
|
||||
}}
|
||||
ref={inputRef}
|
||||
value={searchTerm}
|
||||
/>
|
||||
{candidateConversations.length ? (
|
||||
<Measure bounds>
|
||||
{({ 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 (
|
||||
<div
|
||||
className="module-ForwardMessageModal__list-wrapper"
|
||||
ref={measureRef}
|
||||
>
|
||||
<ConversationList
|
||||
dimensions={contentRect.bounds}
|
||||
getRow={getRow}
|
||||
i18n={i18n}
|
||||
onClickArchiveButton={shouldNeverBeCalled}
|
||||
onClickContactCheckbox={(
|
||||
conversationId: string,
|
||||
disabledReason:
|
||||
| undefined
|
||||
| ContactCheckboxDisabledReason
|
||||
) => {
|
||||
if (
|
||||
disabledReason !==
|
||||
ContactCheckboxDisabledReason.MaximumContactsSelected
|
||||
) {
|
||||
toggleSelectedConversation(conversationId);
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
/* eslint-enable jsx-a11y/no-static-element-interactions */
|
||||
}}
|
||||
</Measure>
|
||||
) : (
|
||||
<div className="module-ForwardMessageModal__no-candidate-contacts">
|
||||
{i18n('noContactsFound')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="module-ForwardMessageModal__footer">
|
||||
<div>
|
||||
{Boolean(selectedContacts.length) &&
|
||||
selectedContacts.map(contact => contact.title).join(', ')}
|
||||
</div>
|
||||
<div>
|
||||
{isEditingMessage || !isMessageEditable ? (
|
||||
<Button
|
||||
aria-label={i18n('ForwardMessageModal--continue')}
|
||||
className="module-ForwardMessageModal__send-button module-ForwardMessageModal__send-button--forward"
|
||||
disabled={!canForwardMessage}
|
||||
onClick={forwardMessage}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
aria-label={i18n('forwardMessage')}
|
||||
className="module-ForwardMessageModal__send-button module-ForwardMessageModal__send-button--continue"
|
||||
disabled={!hasContactsSelected}
|
||||
onClick={() => setIsEditingMessage(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}}
|
||||
onSelectConversation={shouldNeverBeCalled}
|
||||
renderMessageSearchResult={() => {
|
||||
shouldNeverBeCalled();
|
||||
return <div />;
|
||||
}}
|
||||
rowCount={rowCount}
|
||||
shouldRecomputeRowHeights={false}
|
||||
showChooseGroupMembers={shouldNeverBeCalled}
|
||||
startNewConversationFromPhoneNumber={
|
||||
shouldNeverBeCalled
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
/* eslint-enable jsx-a11y/no-static-element-interactions */
|
||||
}}
|
||||
</Measure>
|
||||
) : (
|
||||
<div className="module-ForwardMessageModal__no-candidate-contacts">
|
||||
{i18n('noContactsFound')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="module-ForwardMessageModal__footer">
|
||||
<div>
|
||||
{Boolean(selectedContacts.length) &&
|
||||
selectedContacts.map(contact => contact.title).join(', ')}
|
||||
</div>
|
||||
<div>
|
||||
{isEditingMessage || !isMessageEditable ? (
|
||||
<Button
|
||||
aria-label={i18n('ForwardMessageModal--continue')}
|
||||
className="module-ForwardMessageModal__send-button module-ForwardMessageModal__send-button--forward"
|
||||
disabled={!canForwardMessage}
|
||||
onClick={forwardMessage}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
aria-label={i18n('forwardMessage')}
|
||||
className="module-ForwardMessageModal__send-button module-ForwardMessageModal__send-button--continue"
|
||||
disabled={!hasContactsSelected}
|
||||
onClick={() => setIsEditingMessage(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</animated.div>
|
||||
</ModalHost>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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<ModalPropsType>): 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 (
|
||||
<ModalHost noMouseClose={noMouseClose} onClose={close} theme={theme}>
|
||||
{renderAnimation(
|
||||
<ModalHost
|
||||
noMouseClose={noMouseClose}
|
||||
onClose={close}
|
||||
overlayStyles={overlayStyles}
|
||||
theme={theme}
|
||||
>
|
||||
<animated.div style={modalStyles}>
|
||||
<ModalWindow
|
||||
hasStickyButtons={hasStickyButtons}
|
||||
hasXButton={hasXButton}
|
||||
|
@ -70,7 +69,7 @@ export function Modal({
|
|||
>
|
||||
{children}
|
||||
</ModalWindow>
|
||||
)}
|
||||
</animated.div>
|
||||
</ModalHost>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 noMouseClose?: boolean;
|
||||
readonly onEscape?: () => unknown;
|
||||
readonly onClose: () => unknown;
|
||||
readonly children: React.ReactElement;
|
||||
readonly noMouseClose?: boolean;
|
||||
readonly onClose: () => unknown;
|
||||
readonly onEscape?: () => unknown;
|
||||
readonly overlayStyles?: SpringValues<ModalConfigType>;
|
||||
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<HTMLElement | null>(null);
|
||||
const [isMouseDown, setIsMouseDown] = React.useState(false);
|
||||
|
||||
|
@ -64,16 +74,18 @@ export const ModalHost = React.memo(
|
|||
allowOutsideClick: false,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
role="presentation"
|
||||
className={classNames(
|
||||
'module-modal-host__overlay',
|
||||
theme ? themeClassName(theme) : undefined
|
||||
)}
|
||||
onMouseDown={noMouseClose ? undefined : handleMouseDown}
|
||||
onMouseUp={noMouseClose ? undefined : handleMouseUp}
|
||||
>
|
||||
{children}
|
||||
<div>
|
||||
<animated.div
|
||||
role="presentation"
|
||||
className={classNames(
|
||||
'module-modal-host__overlay',
|
||||
theme ? themeClassName(theme) : undefined
|
||||
)}
|
||||
onMouseDown={noMouseClose ? undefined : handleMouseDown}
|
||||
onMouseUp={noMouseClose ? undefined : handleMouseUp}
|
||||
style={overlayStyles}
|
||||
/>
|
||||
<div className="module-modal-host__container">{children}</div>
|
||||
</div>
|
||||
</FocusTrap>,
|
||||
root
|
||||
|
|
|
@ -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 extends Record<string, unknown>>(
|
||||
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<ModalConfigType>;
|
||||
overlayStyles: SpringValues<ModalConfigType>;
|
||||
} {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const transitions = useTransition<boolean, Props>(isOpen, {
|
||||
...props,
|
||||
leave: {
|
||||
...props.leave,
|
||||
onRest: () => onClose(),
|
||||
const modalRef = useSpringRef();
|
||||
|
||||
const modalStyles = useSpring({
|
||||
from: getFrom(isOpen),
|
||||
to: getTo(isOpen),
|
||||
onRest: () => {
|
||||
if (!isOpen) {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
config: {
|
||||
duration: 200,
|
||||
easing: cubicBezier(0.17, 0.17, 0, 1),
|
||||
...props.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: {
|
||||
clamp: true,
|
||||
friction: 22,
|
||||
tension: 360,
|
||||
},
|
||||
ref: overlayRef,
|
||||
});
|
||||
|
||||
useChain(isOpen ? [overlayRef, modalRef] : [modalRef, overlayRef]);
|
||||
|
||||
return {
|
||||
close: () => setIsOpen(false),
|
||||
renderAnimation: children =>
|
||||
transitions((style, item) =>
|
||||
item ? <animated.div style={style}>{children}</animated.div> : null
|
||||
),
|
||||
overlayStyles,
|
||||
modalStyles,
|
||||
};
|
||||
}
|
||||
|
|
70
yarn.lock
70
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==
|
||||
"@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.0"
|
||||
"@react-spring/types" "~9.2.0"
|
||||
"@react-spring/shared" "~9.2.6-beta.0"
|
||||
"@react-spring/types" "~9.2.6-beta.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==
|
||||
"@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.0"
|
||||
"@react-spring/shared" "~9.2.0"
|
||||
"@react-spring/types" "~9.2.0"
|
||||
"@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.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/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.0":
|
||||
version "9.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.2.4.tgz#f9cc66ac5308a77293330a18518e34121f4008c1"
|
||||
integrity sha512-ZEr4l2BxmyFRUvRA2VCkPfCJii4E7cGkwbjmTBx1EmcGrOnde/V2eF5dxqCTY3k35QuCegkrWe0coRJVkh8q2Q==
|
||||
"@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.0"
|
||||
"@react-spring/types" "~9.2.0"
|
||||
"@react-spring/rafz" "~9.2.6-beta.0"
|
||||
"@react-spring/types" "~9.2.6-beta.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/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.4":
|
||||
version "9.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.2.4.tgz#c6d5464a954bfd0d7bc90117050f796a95ebfa08"
|
||||
integrity sha512-vtPvOalLFvuju/MDBtoSnCyt0xXSL6Amyv82fljOuWPl1yGd4M1WteijnYL9Zlriljl0a3oXcPunAVYTD9dbDQ==
|
||||
"@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.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"
|
||||
"@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"
|
||||
|
|
Loading…
Add table
Reference in a new issue