Animates ModalHost overlay
This commit is contained in:
parent
cfc5407d03
commit
d0e8fbd5a6
9 changed files with 340 additions and 292 deletions
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue