Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove react-transition-group dependency #2391

Merged
merged 34 commits into from
Jan 28, 2025
Merged
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d543d87
🛫
r100-stack Jan 6, 2025
182b4a8
🛫
r100-stack Jan 6, 2025
c2fe3b4
modified repo for the new year (2025)
smmr-dn Jan 6, 2025
1e43853
revert App.tsx
smmr-dn Jan 6, 2025
f2dd813
📈
r100-stack Jan 6, 2025
87a1663
Merge remote-tracking branch 'origin/uyen/modified-new-year-repo' int…
r100-stack Jan 6, 2025
2880bad
Merge branch 'r/themeprovider-children-state-loss-fix' into r/react-t…
r100-stack Jan 6, 2025
eb344b3
Merge branch 'main' into r/react-transition-group-removal
r100-stack Jan 8, 2025
0454c60
Dialog unit test fixes
r100-stack Jan 9, 2025
0a4bc2e
Merge branch 'main' into r/react-transition-group-removal
r100-stack Jan 9, 2025
e28fc35
Fix toast
r100-stack Jan 9, 2025
eff3128
Add JS animations
r100-stack Jan 10, 2025
22e06cd
Fix unit tests
r100-stack Jan 10, 2025
96ff004
Merge branch 'main' into r/react-transition-group-removal
r100-stack Jan 21, 2025
26a1172
Comments
r100-stack Jan 21, 2025
99c9bd6
Separate hook
r100-stack Jan 21, 2025
8b891a7
Testing without mocks
r100-stack Jan 21, 2025
6942306
Cleaner custom hook
r100-stack Jan 21, 2025
76bede6
Cleanup
r100-stack Jan 21, 2025
4197180
`animate?.()`
r100-stack Jan 21, 2025
1455427
Merge branch 'main' into r/react-transition-group-removal
r100-stack Jan 22, 2025
cbac390
Remove duplicate animateOutToRef
r100-stack Jan 22, 2025
b4876c8
onExit called before onClose
r100-stack Jan 23, 2025
8112a64
Merge branch 'main' into r/react-transition-group-removal
r100-stack Jan 23, 2025
630e7c9
Leftover
r100-stack Jan 23, 2025
ec56aaa
Fix Dialog subcomponents cannot self exist
r100-stack Jan 23, 2025
4344c56
Fix a few more tests
r100-stack Jan 23, 2025
14b84cf
Separate DialogMain context
r100-stack Jan 24, 2025
8ee4f16
Minimize diff
r100-stack Jan 24, 2025
de24f21
Leftover
r100-stack Jan 24, 2025
e4a0688
Changeset
r100-stack Jan 24, 2025
e33e1a1
Copyright
r100-stack Jan 24, 2025
0afd50e
Merge branch 'main' into r/react-transition-group-removal
r100-stack Jan 28, 2025
24783fb
Comments
r100-stack Jan 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rude-books-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@itwin/itwinui-css': minor
---

Animation related classes are now deprecated: `.iui-enter`, `.iui-enter-active`, `.iui-exit`, `.iui-exit-active`.
7 changes: 7 additions & 0 deletions .changeset/tidy-geckos-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@itwin/itwinui-react': minor
---

Removed dependency on `react-transition-group`. Notable changes in components:
* `useToaster`: Animations have been reworked to directly use the web animations API.
* `Dialog` and `Modal`: Exit animations have been temporarily removed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/react-workshop/src/Toasts.test.ts
Original file line number Diff line number Diff line change
@@ -20,6 +20,9 @@ describe('Toasts', () => {
cy.get('#ladle-root').within(() => {
cy.get('button').first().click();
});

// Wait for entry animation to complete
cy.wait(240);
cy.compareSnapshot(testName);
});
});
31 changes: 0 additions & 31 deletions packages/itwinui-css/src/mixins.scss
Original file line number Diff line number Diff line change
@@ -41,37 +41,6 @@
}
}

/// Classes for react-transition-group
/// Used for expand/collapse transitions. Needs height/width to be set in JS.
@mixin iui-transition-group {
$transition-rule:
opacity var(--iui-duration-1) ease-out,
width var(--iui-duration-1) ease-out,
height var(--iui-duration-1) ease-out;

&.iui-enter {
opacity: 0;
}

&.iui-enter-active {
opacity: 1;
@media (prefers-reduced-motion: no-preference) {
transition: $transition-rule;
}
}

&.iui-exit {
opacity: 1;
}

&.iui-exit-active {
opacity: 0;
@media (prefers-reduced-motion: no-preference) {
transition: $transition-rule;
}
}
}

@mixin safari-only {
@supports (-apple-pay-button-style: inherit) {
@content;
Original file line number Diff line number Diff line change
@@ -162,13 +162,6 @@ $iui-side-navigation-icon-margins: calc(1.5 * var(--iui-size-m));
background-color: var(--iui-color-background);
border-inline-end: 1px solid var(--iui-color-border);

@include mixins.iui-transition-group;

&.iui-enter-active,
&.iui-exit-active {
display: flex;
}

&-content {
padding-block: 0 var(--iui-size-s);
padding-inline: var(--iui-size-s);
1 change: 0 additions & 1 deletion packages/itwinui-css/src/table/base.scss
Original file line number Diff line number Diff line change
@@ -211,7 +211,6 @@
border-inline-end: 1px solid transparent;
border-block-end: 1px solid var(--iui-color-border);
flex-shrink: 0;
@include mixins.iui-transition-group;
}

// #region Selection
4 changes: 1 addition & 3 deletions packages/itwinui-react/package.json
Original file line number Diff line number Diff line change
@@ -109,8 +109,7 @@
"@tanstack/react-virtual": "^3.8.2",
"classnames": "^2.3.2",
"jotai": "^2.8.0",
"react-table": "^7.8.0",
"react-transition-group": "^4.4.5"
"react-table": "^7.8.0"
},
"devDependencies": {
"@swc/cli": "^0.5.1",
@@ -121,7 +120,6 @@
"@types/node": "*",
"@types/react": "*",
"@types/react-dom": "*",
"@types/react-transition-group": "^4.4.10",
"@vitest/coverage-v8": "^1.2.1",
"eslint": "^8",
"eslint-config-prettier": "^8.8.0",
7 changes: 0 additions & 7 deletions packages/itwinui-react/src/core/Dialog/Dialog.test.tsx
Original file line number Diff line number Diff line change
@@ -177,13 +177,6 @@ it('should not stay in the DOM when isOpen=false', () => {

rerender(<Component isOpen={false} />);

// Should be there in the DOM until the exit animation is finished
dialogWrapper = container.querySelector('.iui-dialog-wrapper') as HTMLElement;
expect(dialogWrapper).toBeTruthy();

// Since timeout for the exit animation is 600ms
act(() => vi.advanceTimersByTime(600));

dialogWrapper = container.querySelector('.iui-dialog-wrapper') as HTMLElement;
expect(dialogWrapper).toBeFalsy();
});
61 changes: 29 additions & 32 deletions packages/itwinui-react/src/core/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -13,14 +13,13 @@ import { DialogButtonBar } from './DialogButtonBar.js';
import { DialogMain } from './DialogMain.js';
import { useMergedRefs, Box, Portal } from '../../utils/index.js';
import type { PolymorphicForwardRefComponent } from '../../utils/index.js';
import { Transition } from 'react-transition-group';

type DialogProps = {
/**
* Dialog content.
*/
children: React.ReactNode;
} & Omit<DialogContextProps, 'dialogRootRef'>;
} & DialogContextProps;

const DialogComponent = React.forwardRef((props, ref) => {
const {
@@ -42,37 +41,35 @@ const DialogComponent = React.forwardRef((props, ref) => {
} = props;

const dialogRootRef = React.useRef<HTMLDivElement>(null);
const mergedRefs = useMergedRefs(ref, dialogRootRef);

return (
<Transition in={isOpen} timeout={{ exit: 600 }} mountOnEnter unmountOnExit>
<DialogContext.Provider
value={{
isOpen,
onClose,
closeOnEsc,
closeOnExternalClick,
isDismissible,
preventDocumentScroll,
trapFocus,
setFocus,
isDraggable,
isResizable,
relativeTo,
dialogRootRef,
placement,
}}
>
<Portal portal={portal}>
<Box
className={cx('iui-dialog-wrapper', className)}
data-iui-relative={relativeTo === 'container'}
ref={useMergedRefs(ref, dialogRootRef)}
{...rest}
/>
</Portal>
</DialogContext.Provider>
</Transition>
);
return isOpen ? (
<DialogContext.Provider
value={{
isOpen,
onClose,
closeOnEsc,
closeOnExternalClick,
isDismissible,
preventDocumentScroll,
trapFocus,
setFocus,
isDraggable,
isResizable,
relativeTo,
placement,
}}
>
<Portal portal={portal}>
<Box
className={cx('iui-dialog-wrapper', className)}
data-iui-relative={relativeTo === 'container'}
ref={mergedRefs}
{...rest}
/>
</Portal>
</DialogContext.Provider>
) : null;
}) as PolymorphicForwardRefComponent<'div', DialogProps>;
if (process.env.NODE_ENV === 'development') {
DialogComponent.displayName = 'Dialog';
7 changes: 5 additions & 2 deletions packages/itwinui-react/src/core/Dialog/DialogBackdrop.tsx
Original file line number Diff line number Diff line change
@@ -7,8 +7,8 @@ import { Backdrop } from '../Backdrop/Backdrop.js';
import type { BackdropProps } from '../Backdrop/Backdrop.js';
import { useMergedRefs } from '../../utils/index.js';
import type { PolymorphicForwardRefComponent } from '../../utils/index.js';
import { useDialogContext } from './DialogContext.js';
import type { DialogContextProps } from './DialogContext.js';
import { useDialogContext, type DialogContextProps } from './DialogContext.js';
import { useDialogMainContext } from './DialogMainContext.js';
import cx from 'classnames';

type DialogBackdropProps = BackdropProps &
@@ -25,6 +25,8 @@ type DialogBackdropProps = BackdropProps &
*/
export const DialogBackdrop = React.forwardRef((props, ref) => {
const dialogContext = useDialogContext();
const dialogMainContext = useDialogMainContext();

const {
isVisible = dialogContext.isOpen,
isDismissible = dialogContext.isDismissible,
@@ -47,6 +49,7 @@ export const DialogBackdrop = React.forwardRef((props, ref) => {
return;
}
if (isDismissible && closeOnExternalClick && onClose) {
dialogMainContext?.beforeClose();
onClose(event);
}
onMouseDown?.(event);
76 changes: 44 additions & 32 deletions packages/itwinui-react/src/core/Dialog/DialogMain.tsx
Original file line number Diff line number Diff line change
@@ -16,9 +16,9 @@ import {
import type { PolymorphicForwardRefComponent } from '../../utils/index.js';
import { useDialogContext } from './DialogContext.js';
import type { DialogContextProps } from './DialogContext.js';
import { Transition } from 'react-transition-group';
import { DialogDragContext } from './DialogDragContext.js';
import { useDragAndDrop } from '../../utils/hooks/useDragAndDrop.js';
import { DialogMainContext } from './DialogMainContext.js';

export type DialogMainProps = {
/**
@@ -73,14 +73,16 @@ export const DialogMain = React.forwardRef((props, ref) => {
...rest
} = props;

const [style, setStyle] = React.useState<React.CSSProperties>();
const { dialogRootRef } = dialogContext;

const dialogRef = React.useRef<HTMLDivElement>(null);
const hasBeenResized = React.useRef(false);
const previousFocusedElement = React.useRef<HTMLElement | null>();

const [style, setStyle] = React.useState<React.CSSProperties>();
const hasBeenResized = React.useRef(false);

const originalBodyOverflow = React.useRef('');
React.useEffect(() => {
useLayoutEffect(() => {
if (isOpen) {
originalBodyOverflow.current = document.body.style.overflow;
}
@@ -107,7 +109,7 @@ export const DialogMain = React.forwardRef((props, ref) => {
return () => {
ownerDocument.body.style.overflow = originalBodyOverflow.current;
};
}, [isOpen, preventDocumentScroll]);
}, [dialogRef, isOpen, preventDocumentScroll]);

const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.altKey) {
@@ -116,14 +118,15 @@ export const DialogMain = React.forwardRef((props, ref) => {
// Prevents React from resetting its properties
event.persist();
if (isDismissible && closeOnEsc && event.key === 'Escape' && onClose) {
beforeClose();
onClose(event);
}
onKeyDown?.(event);
};

const { onPointerDown, transform } = useDragAndDrop(
dialogRef,
dialogContext.dialogRootRef,
dialogRootRef,
isDraggable,
);
const handlePointerDown = React.useCallback(
@@ -149,7 +152,7 @@ export const DialogMain = React.forwardRef((props, ref) => {
insetBlockStart: dialogRef.current?.offsetTop,
transform: `translate(${translateX}px,${translateY}px)`,
}));
}, [isDraggable, isOpen]);
}, [dialogRef, isDraggable, isOpen]);

const setResizeStyle = React.useCallback((newStyle: React.CSSProperties) => {
setStyle((oldStyle) => ({
@@ -158,6 +161,35 @@ export const DialogMain = React.forwardRef((props, ref) => {
}));
}, []);

/** Focuses dialog when opened. */
const onEnter = React.useCallback(() => {
previousFocusedElement.current = dialogRef.current?.ownerDocument
.activeElement as HTMLElement;
if (setFocus) {
dialogRef.current?.focus({ preventScroll: true });
}
}, [dialogRef, previousFocusedElement, setFocus]);

/** Brings back focus to the previously focused element when closed. */
const beforeClose = React.useCallback(() => {
if (
dialogRef.current?.contains(
dialogRef.current?.ownerDocument.activeElement,
)
) {
previousFocusedElement.current?.focus();
}
}, [dialogRef, previousFocusedElement]);

const mountRef = React.useCallback(
(element: HTMLElement | null) => {
if (element) {
onEnter();
}
},
[onEnter],
);

const content = (
<Box
className={cx(
@@ -171,7 +203,7 @@ export const DialogMain = React.forwardRef((props, ref) => {
className,
)}
role='dialog'
ref={useMergedRefs(dialogRef, ref)}
ref={useMergedRefs(dialogRef, mountRef, ref)}
onKeyDown={handleKeyDown}
tabIndex={-1}
data-iui-placement={placement}
@@ -187,7 +219,7 @@ export const DialogMain = React.forwardRef((props, ref) => {
{isResizable && (
<Resizer
elementRef={dialogRef}
containerRef={dialogContext.dialogRootRef}
containerRef={dialogRootRef}
onResizeStart={() => {
if (!hasBeenResized.current) {
hasBeenResized.current = true;
@@ -204,34 +236,14 @@ export const DialogMain = React.forwardRef((props, ref) => {
);

return (
<Transition
in={isOpen}
appear={true}
timeout={{ exit: 600 }}
// Focuses dialog when opened
onEntered={() => {
previousFocusedElement.current = dialogRef.current?.ownerDocument
.activeElement as HTMLElement;
setFocus && dialogRef.current?.focus({ preventScroll: true });
}}
// Brings back focus to the previously focused element when closed
onExit={() => {
if (
dialogRef.current?.contains(
dialogRef.current?.ownerDocument.activeElement,
)
) {
previousFocusedElement.current?.focus();
}
}}
unmountOnExit={true}
nodeRef={dialogRef}
<DialogMainContext.Provider
value={React.useMemo(() => ({ beforeClose }), [beforeClose])}
>
<DialogDragContext.Provider value={{ onPointerDown: handlePointerDown }}>
{trapFocus && <FocusTrap>{content}</FocusTrap>}
{!trapFocus && content}
</DialogDragContext.Provider>
</Transition>
</DialogMainContext.Provider>
);
}) as PolymorphicForwardRefComponent<'div', DialogMainProps>;
if (process.env.NODE_ENV === 'development') {
16 changes: 16 additions & 0 deletions packages/itwinui-react/src/core/Dialog/DialogMainContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import * as React from 'react';

type DialogMainContextProps = {
beforeClose: () => void;
};

export const DialogMainContext =
React.createContext<DialogMainContextProps | null>(null);

export const useDialogMainContext = () => {
return React.useContext(DialogMainContext);
};
13 changes: 12 additions & 1 deletion packages/itwinui-react/src/core/Dialog/DialogTitleBar.tsx
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import { SvgClose, mergeEventHandlers, Box } from '../../utils/index.js';
import type { PolymorphicForwardRefComponent } from '../../utils/index.js';
import { IconButton } from '../Buttons/IconButton.js';
import { useDialogContext } from './DialogContext.js';
import { useDialogMainContext } from './DialogMainContext.js';
import type { DialogContextProps } from './DialogContext.js';
import { DialogTitleBarTitle } from './DialogTitleBarTitle.js';
import { useDialogDragContext } from './DialogDragContext.js';
@@ -43,6 +44,8 @@ type DialogTitleBarProps = {
export const DialogTitleBar = Object.assign(
React.forwardRef((props, ref) => {
const dialogContext = useDialogContext();
const dialogMainContext = useDialogMainContext();

const {
children,
titleText,
@@ -56,6 +59,14 @@ export const DialogTitleBar = Object.assign(

const { onPointerDown } = useDialogDragContext();

const onClick = React.useCallback(
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
dialogMainContext?.beforeClose();
onClose?.(e);
},
[dialogMainContext, onClose],
);

return (
<Box
className={cx('iui-dialog-title-bar', className, {
@@ -74,7 +85,7 @@ export const DialogTitleBar = Object.assign(
<IconButton
size='small'
styleType='borderless'
onClick={onClose}
onClick={onClick}
aria-label='Close'
data-iui-shift='right'
>
Original file line number Diff line number Diff line change
@@ -4,12 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as React from 'react';
import cx from 'classnames';
import {
WithCSSTransition,
SvgChevronRight,
Box,
useControlledState,
} from '../../utils/index.js';
import { SvgChevronRight, Box, useControlledState } from '../../utils/index.js';
import type { PolymorphicForwardRefComponent } from '../../utils/index.js';
import { IconButton } from '../Buttons/IconButton.js';

@@ -45,12 +40,12 @@ type SideNavigationProps = {
*/
onExpanderClick?: () => void;
/**
* Submenu to show supplemental info assicated to the main item.
* Submenu to show supplemental info associated to the main item.
*
* Should be used with the `isSubmenuOpen` props from both `SideNavigation` and `SidenavButton`.
* @example
* <SideNavigation
* // ...
* //
* submenu={(
* <SidenavSubmenu>
* <SidenavSubmenuHeader>Documents</SidenavSubmenuHeader>
@@ -178,16 +173,7 @@ export const SideNavigation = React.forwardRef((props, forwardedRef) => {
{expanderPlacement === 'bottom' && ExpandButton}
</Box>

{submenu && (
<WithCSSTransition
in={isSubmenuOpen}
dimension='width'
timeout={200}
classNames='iui'
>
{submenu}
</WithCSSTransition>
)}
{submenu && isSubmenuOpen ? submenu : null}
</Box>
</SidenavExpandedContext.Provider>
);
224 changes: 157 additions & 67 deletions packages/itwinui-react/src/core/Toast/Toast.tsx
Original file line number Diff line number Diff line change
@@ -3,7 +3,6 @@
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import * as React from 'react';
import { Transition } from 'react-transition-group';
import cx from 'classnames';
import {
getWindow,
@@ -13,6 +12,7 @@ import {
useSafeContext,
ButtonBase,
useMediaQuery,
useLatestRef,
} from '../../utils/index.js';
import type { PolymorphicForwardRefComponent } from '../../utils/index.js';
import { IconButton } from '../Buttons/IconButton.js';
@@ -115,8 +115,6 @@ export const Toast = (props: ToastProps) => {
const thisElement = React.useRef<HTMLDivElement>(null);
const [margin, setMargin] = React.useState(0);

const motionOk = useMediaQuery('(prefers-reduced-motion: no-preference)');

const marginStyle = () => {
if (placementPosition === 'top') {
return { marginBlockEnd: margin };
@@ -172,73 +170,36 @@ export const Toast = (props: ToastProps) => {
}
};

const calculateOutAnimation = (node: HTMLElement) => {
// calculation translate x and y pixels.
let translateX = 0;
let translateY = 0;
if (animateOutTo && node) {
const { x: startX, y: startY } = node.getBoundingClientRect(); // current element
const { x: endX, y: endY } = animateOutTo.getBoundingClientRect(); // anchor point
translateX = endX - startX;
translateY = endY - startY;
}
return { translateX, translateY };
};
const shouldBeMounted = useAnimateToastBasedOnVisibility(isVisible, {
thisElement,
animateOutTo,
onRemove,
});

return (
<Transition
timeout={{ enter: 240, exit: animateOutTo ? 400 : 120 }}
in={isVisible}
appear={true}
unmountOnExit={true}
onEnter={(node: HTMLElement) => {
if (motionOk) {
node.style.transform = 'translateY(15%)';
node.style.transitionTimingFunction = 'ease';
}
}}
onEntered={(node: HTMLElement) => {
if (motionOk) {
node.style.transform = 'translateY(0)';
}
}}
onExiting={(node) => {
if (motionOk) {
const { translateX, translateY } = calculateOutAnimation(node);
node.style.transform = animateOutTo
? `scale(0.9) translate(${translateX}px,${translateY}px)`
: `scale(0.9)`;
node.style.opacity = '0';
node.style.transitionDuration = animateOutTo ? '400ms' : '120ms';
node.style.transitionTimingFunction = 'cubic-bezier(0.4, 0, 1, 1)';
}
return shouldBeMounted ? (
<Box
ref={thisElement}
className='iui-toast-all'
style={{
height,
...marginStyle(),
}}
onExited={onRemove}
>
<Box
ref={thisElement}
className='iui-toast-all'
style={{
height,
...marginStyle(),
}}
>
<div ref={onRef}>
<ToastPresentation
as='div'
category={category}
content={content}
link={link}
type={type}
hasCloseButton={hasCloseButton}
onClose={close}
{...domProps?.toastProps}
contentProps={domProps?.contentProps}
/>
</div>
</Box>
</Transition>
);
<div ref={onRef}>
<ToastPresentation
as='div'
category={category}
content={content}
link={link}
type={type}
hasCloseButton={hasCloseButton}
onClose={close}
{...domProps?.toastProps}
contentProps={domProps?.contentProps}
/>
</div>
</Box>
) : null;
};

export type ToastPresentationProps = Omit<
@@ -308,3 +269,132 @@ export const ToastPresentation = React.forwardRef((props, forwardedRef) => {
</Box>
);
}) as PolymorphicForwardRefComponent<'div', ToastPresentationProps>;

/**
* Animates in and out the toast based on `isVisible`.
* Returns `shouldBeMounted`. It takes into account the animations (e.g. exit animations are finished before unmounting)
*/
const useAnimateToastBasedOnVisibility = (
isVisible: ToastProps['isVisible'],
args: {
thisElement: React.RefObject<HTMLDivElement | null>;
animateOutTo: ToastProps['animateOutTo'];
onRemove: ToastProps['onRemove'];
},
) => {
const { thisElement, animateOutTo, onRemove } = args;
const [shouldBeMounted, setShouldBeMounted] = React.useState(isVisible);

const motionOk = useMediaQuery('(prefers-reduced-motion: no-preference)');
const onRemoveRef = useLatestRef(onRemove);

const [prevIsVisible, setPrevIsVisible] = React.useState<
typeof isVisible | undefined
>(undefined);

React.useEffect(() => {
// if isVisible prop is changed, animate in or out.
if (prevIsVisible !== isVisible) {
setPrevIsVisible(isVisible);

if (isVisible) {
safeAnimateIn();
} else {
safeAnimateOut();
}
}

function calculateOutAnimation(node: HTMLElement) {
// calculation translate x and y pixels.
let translateX = 0;
let translateY = 0;
if (animateOutTo && node) {
const { x: startX, y: startY } = node.getBoundingClientRect(); // current element
const { x: endX, y: endY } = animateOutTo.getBoundingClientRect(); // anchor point
translateX = endX - startX;
translateY = endY - startY;
}
return { translateX, translateY };
}

function safeAnimateIn() {
setShouldBeMounted(true);

// Mount *before* handling dialog entry.
queueMicrotask(() => {
animateIn();
});
}

function safeAnimateOut() {
if (!motionOk) {
setShouldBeMounted(false);
onRemoveRef.current?.();
} else {
const animation = animateOut();

// Unmount *after* handling dialog exit.
animation?.addEventListener('finish', () => {
setShouldBeMounted(false);
onRemoveRef.current?.();
});
}
}

function animateIn() {
if (!motionOk) {
return;
}

thisElement.current?.animate?.(
[{ transform: 'translateY(15%)' }, { transform: 'translateY(0)' }],
{
duration: 240,
fill: 'forwards',
},
);
}

function animateOut() {
if (thisElement.current == null || !motionOk) {
return;
}

const { translateX, translateY } = calculateOutAnimation(
thisElement.current,
);

const animationDuration = animateOutTo ? 400 : 120;

const animation = thisElement.current?.animate?.(
[
{
transform: animateOutTo
? `scale(0.9) translate(${translateX}px,${translateY}px)`
: `scale(0.9)`,
opacity: 0,
transitionDuration: `${animationDuration}ms`,
transitionTimingFunction: 'cubic-bezier(0.4, 0, 1, 1)',
},
],
{
duration: animationDuration,
iterations: 1,
fill: 'forwards',
},
);

return animation;
}
}, [
isVisible,
prevIsVisible,
animateOutTo,
motionOk,
thisElement,
setShouldBeMounted,
onRemoveRef,
]);

return shouldBeMounted;
};

This file was deleted.

1 change: 0 additions & 1 deletion packages/itwinui-react/src/utils/components/index.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,6 @@ export * from './FocusTrap.js';
export * from './InputContainer.js';
export * from './InputFlexContainer.js';
export * from './InputWithIcon.js';
export * from './WithCSSTransition.js';
export * from './MiddleTextTruncation.js';
export * from './AutoclearingHiddenLiveRegion.js';
export * from './Box.js';
45 changes: 0 additions & 45 deletions pnpm-lock.yaml