Adds open/close animations to dialogs and modals

This commit is contained in:
Josh Perez 2021-09-29 16:59:37 -04:00 committed by GitHub
parent fc066e05df
commit b6cfe0933d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 635 additions and 224 deletions

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import React, { ComponentProps, useEffect } from 'react';
import { Globals } from '@react-spring/web';
import classNames from 'classnames';
import { AppViewType } from '../state/ducks/app';
@ -10,6 +11,7 @@ import { Install } from './Install';
import { StandaloneRegistration } from './StandaloneRegistration';
import { ThemeType } from '../types/Util';
import { usePageVisibility } from '../hooks/usePageVisibility';
import { useReducedMotion } from '../hooks/useReducedMotion';
type PropsType = {
appView: AppViewType;
@ -84,6 +86,14 @@ export const App = ({
document.body.classList.toggle('page-is-visible', isPageVisible);
}, [isPageVisible]);
// A11y settings for react-spring
const prefersReducedMotion = useReducedMotion();
useEffect(() => {
Globals.assign({
skipAnimation: prefersReducedMotion,
});
}, [prefersReducedMotion]);
return (
<div
className={classNames({

View file

@ -1,11 +1,13 @@
// Copyright 2019-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import React, { MouseEvent, useCallback } from 'react';
import { Button, ButtonVariant } from './Button';
import { LocalizerType } from '../types/Util';
import { Modal } from './Modal';
import { ModalHost } from './ModalHost';
import { Modal, ModalWindow } from './Modal';
import { Theme } from '../util/theme';
import { useAnimated } from '../hooks/useAnimated';
export type ActionSpec = {
text: string;
@ -61,15 +63,27 @@ export const ConfirmationDialog = React.memo(
title,
hasXButton,
}: Props) => {
const cancelAndClose = React.useCallback(() => {
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 cancelAndClose = useCallback(() => {
if (onCancel) {
onCancel();
}
onClose();
}, [onCancel, onClose]);
close();
}, [close, onCancel]);
const handleCancel = React.useCallback(
(e: React.MouseEvent) => {
const handleCancel = useCallback(
(e: MouseEvent) => {
if (e.target === e.currentTarget) {
cancelAndClose();
}
@ -80,40 +94,43 @@ export const ConfirmationDialog = React.memo(
const hasActions = Boolean(actions.length);
return (
<Modal
moduleClassName={moduleClassName}
i18n={i18n}
onClose={cancelAndClose}
title={title}
theme={theme}
hasXButton={hasXButton}
>
{children}
<Modal.ButtonFooter>
<Button
onClick={handleCancel}
ref={focusRef}
variant={
hasActions ? ButtonVariant.Secondary : ButtonVariant.Primary
}
<ModalHost onClose={close} theme={theme}>
{renderAnimation(
<ModalWindow
hasXButton={hasXButton}
i18n={i18n}
moduleClassName={moduleClassName}
onClose={cancelAndClose}
title={title}
>
{cancelText || i18n('confirmation-dialog--Cancel')}
</Button>
{actions.map((action, i) => (
<Button
key={action.text}
onClick={() => {
action.action();
onClose();
}}
data-action={i}
variant={getButtonVariant(action.style)}
>
{action.text}
</Button>
))}
</Modal.ButtonFooter>
</Modal>
{children}
<Modal.ButtonFooter>
<Button
onClick={handleCancel}
ref={focusRef}
variant={
hasActions ? ButtonVariant.Secondary : ButtonVariant.Primary
}
>
{cancelText || i18n('confirmation-dialog--Cancel')}
</Button>
{actions.map((action, i) => (
<Button
key={action.text}
onClick={() => {
action.action();
close();
}}
data-action={i}
variant={getButtonVariant(action.style)}
>
{action.text}
</Button>
))}
</Modal.ButtonFooter>
</ModalWindow>
)}
</ModalHost>
);
}
);

View file

@ -30,6 +30,7 @@ import { SearchInput } from './SearchInput';
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
import { assert } from '../util/assert';
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
import { useAnimated } from '../hooks/useAnimated';
export type DataPropsType = {
attachments?: Array<AttachmentType>;
@ -198,13 +199,28 @@ 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 handleBackOrClose = useCallback(() => {
if (isEditingMessage) {
setIsEditingMessage(false);
} else {
onClose();
close();
}
}, [isEditingMessage, onClose, setIsEditingMessage]);
}, [isEditingMessage, close, setIsEditingMessage]);
const rowCount = filteredConversations.length;
const getRow = (index: number): undefined | Row => {
@ -249,182 +265,188 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
{i18n('GroupV2--cannot-send')}
</ConfirmationDialog>
)}
<ModalHost onEscape={handleBackOrClose} onClose={onClose}>
<div className="module-ForwardMessageModal">
<div
className={classNames('module-ForwardMessageModal__header', {
'module-ForwardMessageModal__header--edit': isEditingMessage,
})}
>
<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"
>
&nbsp;
</button>
) : (
<button
aria-label={i18n('close')}
className="module-ForwardMessageModal__header--close"
onClick={close}
type="button"
/>
)}
<h1>{i18n('forwardMessage')}</h1>
</div>
{isEditingMessage ? (
<button
aria-label={i18n('back')}
className="module-ForwardMessageModal__header--back"
onClick={() => setIsEditingMessage(false)}
type="button"
>
&nbsp;
</button>
) : (
<button
aria-label={i18n('close')}
className="module-ForwardMessageModal__header--close"
onClick={onClose}
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}
<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}
i18n={i18n}
image={linkPreview.image}
onClose={() => removeLinkPreview()}
title={linkPreview.title}
onCloseAttachment={(attachment: AttachmentType) => {
const newAttachments = attachmentsToForward.filter(
currentAttachment => currentAttachment !== attachment
);
setAttachmentsToForward(newAttachments);
}}
/>
</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
) : null}
<div className="module-ForwardMessageModal__text-edit-area">
<CompositionInput
clearQuotedMessage={shouldNeverBeCalled}
draftText={messageBodyText}
getQuotedMessage={noop}
i18n={i18n}
onClose={focusTextEditInput}
onPickEmoji={insertEmoji}
onSetSkinTone={onSetSkinTone}
recentEmojis={recentEmojis}
skinTone={skinTone}
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}
onClose={focusTextEditInput}
onPickEmoji={insertEmoji}
onSetSkinTone={onSetSkinTone}
recentEmojis={recentEmojis}
skinTone={skinTone}
/>
</div>
</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>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
) : (
<div className="module-ForwardMessageModal__main-body">
<SearchInput
disabled={candidateConversations.length === 0}
placeholder={i18n('contactSearchPlaceholder')}
onChange={event => {
setSearchTerm(event.target.value);
}}
</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}
ref={inputRef}
value={searchTerm}
/>
) : (
<Button
aria-label={i18n('forwardMessage')}
className="module-ForwardMessageModal__send-button module-ForwardMessageModal__send-button--continue"
disabled={!hasContactsSelected}
onClick={() => setIsEditingMessage(true)}
/>
)}
{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>
);
/* 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>
</div>
</div>
)}
</ModalHost>
</>
);

View file

@ -1,7 +1,7 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useRef, useState, ReactElement, ReactNode } from 'react';
import React, { ReactElement, ReactNode, useRef, useState } from 'react';
import Measure, { ContentRect, MeasuredComponentProps } from 'react-measure';
import classNames from 'classnames';
import { noop } from 'lodash';
@ -10,6 +10,7 @@ import { LocalizerType } from '../types/Util';
import { ModalHost } from './ModalHost';
import { Theme } from '../util/theme';
import { getClassNamesFor } from '../util/getClassNamesFor';
import { useAnimated } from '../hooks/useAnimated';
import { useHasWrapped } from '../hooks/useHasWrapped';
type PropsType = {
@ -18,9 +19,12 @@ type PropsType = {
hasXButton?: boolean;
i18n: LocalizerType;
moduleClassName?: string;
noMouseClose?: boolean;
onClose?: () => void;
title?: ReactNode;
};
type ModalPropsType = PropsType & {
noMouseClose?: boolean;
theme?: Theme;
};
@ -36,8 +40,51 @@ export function Modal({
onClose = noop,
title,
theme,
}: Readonly<PropsType>): ReactElement {
}: 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
);
return (
<ModalHost noMouseClose={noMouseClose} onClose={close} theme={theme}>
{renderAnimation(
<ModalWindow
hasStickyButtons={hasStickyButtons}
hasXButton={hasXButton}
i18n={i18n}
moduleClassName={moduleClassName}
onClose={close}
title={title}
>
{children}
</ModalWindow>
)}
</ModalHost>
);
}
export function ModalWindow({
children,
hasStickyButtons,
hasXButton,
i18n,
moduleClassName,
onClose = noop,
title,
}: Readonly<PropsType>): JSX.Element {
const modalRef = useRef<HTMLDivElement | null>(null);
const bodyRef = useRef<HTMLDivElement | null>(null);
const [scrolled, setScrolled] = useState(false);
const [hasOverflow, setHasOverflow] = useState(false);
@ -56,10 +103,10 @@ export function Modal({
}
return (
<ModalHost noMouseClose={noMouseClose} onClose={onClose} theme={theme}>
<>
{/* We don't want the click event to propagate to its container node. */}
{/* eslint-disable jsx-a11y/no-static-element-interactions */}
{/* eslint-disable jsx-a11y/click-events-have-key-events */}
{/* eslint-disable-next-line max-len */}
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */}
<div
className={classNames(
getClassName(''),
@ -71,8 +118,6 @@ export function Modal({
event.stopPropagation();
}}
>
{/* eslint-enable jsx-a11y/no-static-element-interactions */}
{/* eslint-enable jsx-a11y/click-events-have-key-events */}
{hasHeader && (
<div className={getClassName('__header')}>
{hasXButton && (
@ -81,9 +126,7 @@ export function Modal({
type="button"
className={getClassName('__close-button')}
tabIndex={0}
onClick={() => {
onClose();
}}
onClick={onClose}
/>
)}
{title && (
@ -122,7 +165,7 @@ export function Modal({
)}
</Measure>
</div>
</ModalHost>
</>
);
}

37
ts/hooks/useAnimated.tsx Normal file
View file

@ -0,0 +1,37 @@
// 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';
export function useAnimated<Props extends Record<string, unknown>>(
props: UseTransitionProps,
onClose: () => unknown
): {
close: () => unknown;
renderAnimation: (children: ReactElement) => JSX.Element;
} {
const [isOpen, setIsOpen] = useState(true);
const transitions = useTransition<boolean, Props>(isOpen, {
...props,
leave: {
...props.leave,
onRest: () => onClose(),
},
config: {
duration: 200,
easing: cubicBezier(0.17, 0.17, 0, 1),
...props.config,
},
});
return {
close: () => setIsOpen(false),
renderAnimation: children =>
transitions((style, item) =>
item ? <animated.div style={style}>{children}</animated.div> : null
),
};
}

View file

@ -0,0 +1,34 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { useEffect, useState } from 'react';
function getReducedMotionQuery(): MediaQueryList {
return window.matchMedia('(prefers-reduced-motion: reduce)');
}
// Inspired by <https://github.com/infiniteluke/react-reduce-motion>.
export function useReducedMotion(): boolean {
const initialQuery = getReducedMotionQuery();
const [prefersReducedMotion, setPrefersReducedMotion] = useState(
initialQuery.matches
);
useEffect(() => {
const query = getReducedMotionQuery();
function changePreference() {
setPrefersReducedMotion(query.matches);
}
changePreference();
query.addEventListener('change', changePreference);
return () => {
query.removeEventListener('change', changePreference);
};
});
return prefersReducedMotion;
}

View file

@ -240,6 +240,153 @@
"updated": "2018-09-18T19:19:27.699Z",
"reasonDetail": "What's being eval'd is a static string"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/animated/dist/react-spring-animated.cjs.dev.js",
"line": " const instanceRef = React.useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/animated/dist/react-spring-animated.cjs.dev.js",
"line": " const observerRef = React.useRef();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/animated/dist/react-spring-animated.cjs.prod.js",
"line": " const instanceRef = React.useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/animated/dist/react-spring-animated.cjs.prod.js",
"line": " const observerRef = React.useRef();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/animated/dist/react-spring-animated.esm.js",
"line": " const instanceRef = useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/animated/dist/react-spring-animated.esm.js",
"line": " const observerRef = useRef();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/core/dist/react-spring-core.cjs.dev.js",
"line": " const layoutId = React.useRef(0);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/core/dist/react-spring-core.cjs.dev.js",
"line": " const ctrls = React.useRef([...state.ctrls]);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/core/dist/react-spring-core.cjs.dev.js",
"line": " const usedTransitions = React.useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/core/dist/react-spring-core.cjs.prod.js",
"line": " const layoutId = React.useRef(0);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/core/dist/react-spring-core.cjs.prod.js",
"line": " const ctrls = React.useRef([...state.ctrls]);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/core/dist/react-spring-core.cjs.prod.js",
"line": " const usedTransitions = React.useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/core/dist/react-spring-core.esm.js",
"line": " const layoutId = useRef(0);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/core/dist/react-spring-core.esm.js",
"line": " const ctrls = useRef([...state.ctrls]);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/core/dist/react-spring-core.esm.js",
"line": " const usedTransitions = useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/shared/dist/react-spring-shared.cjs.dev.js",
"line": " const committed = React.useRef();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/shared/dist/react-spring-shared.cjs.dev.js",
"line": " const prevRef = React.useRef();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/shared/dist/react-spring-shared.cjs.prod.js",
"line": " const committed = React.useRef();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/shared/dist/react-spring-shared.cjs.prod.js",
"line": " const prevRef = React.useRef();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/shared/dist/react-spring-shared.esm.js",
"line": " const committed = useRef();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@react-spring/shared/dist/react-spring-shared.esm.js",
"line": " const prevRef = useRef();",
"reasonCategory": "usageTrusted",
"updated": "2021-09-27T21:37:06.339Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/@sindresorhus/is/dist/index.js",