diff --git a/.changeset/rude-books-drum.md b/.changeset/rude-books-drum.md
new file mode 100644
index 00000000000..fcfd47bf4c8
--- /dev/null
+++ b/.changeset/rude-books-drum.md
@@ -0,0 +1,5 @@
+---
+'@itwin/itwinui-css': minor
+---
+
+Animation related classes are now deprecated: `.iui-enter`, `.iui-enter-active`, `.iui-exit`, `.iui-exit-active`.
diff --git a/.changeset/tidy-geckos-check.md b/.changeset/tidy-geckos-check.md
new file mode 100644
index 00000000000..94b7ffa1aae
--- /dev/null
+++ b/.changeset/tidy-geckos-check.md
@@ -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.
diff --git a/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Anchor To Button.png b/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Anchor To Button.png
index c161869e995..dab2e502d5f 100755
Binary files a/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Anchor To Button.png and b/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Anchor To Button.png differ
diff --git a/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Informational.png b/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Informational.png
index fca6446f4b4..0f9f8909fc3 100755
Binary files a/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Informational.png and b/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Informational.png differ
diff --git a/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Negative.png b/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Negative.png
index ba4acdb53c4..10227735908 100755
Binary files a/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Negative.png and b/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Negative.png differ
diff --git a/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Position Changed.png b/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Position Changed.png
index c78324e3b0d..22492d02136 100755
Binary files a/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Position Changed.png and b/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Position Changed.png differ
diff --git a/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Positive.png b/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Positive.png
index c161869e995..dab2e502d5f 100755
Binary files a/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Positive.png and b/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Positive.png differ
diff --git a/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Warning.png b/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Warning.png
index d78059f129c..53c490d12ff 100755
Binary files a/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Warning.png and b/apps/react-workshop/cypress-visual-screenshots/baseline/Toasts.test.ts-Warning.png differ
diff --git a/apps/react-workshop/src/Toasts.test.ts b/apps/react-workshop/src/Toasts.test.ts
index 27b5cdcae46..7a73060b77d 100644
--- a/apps/react-workshop/src/Toasts.test.ts
+++ b/apps/react-workshop/src/Toasts.test.ts
@@ -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);
});
});
diff --git a/packages/itwinui-css/src/mixins.scss b/packages/itwinui-css/src/mixins.scss
index 33f5013925b..9db20b4dfbd 100644
--- a/packages/itwinui-css/src/mixins.scss
+++ b/packages/itwinui-css/src/mixins.scss
@@ -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;
diff --git a/packages/itwinui-css/src/side-navigation/side-navigation.scss b/packages/itwinui-css/src/side-navigation/side-navigation.scss
index 0a05487aa9b..eb02ebb47ba 100644
--- a/packages/itwinui-css/src/side-navigation/side-navigation.scss
+++ b/packages/itwinui-css/src/side-navigation/side-navigation.scss
@@ -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);
diff --git a/packages/itwinui-css/src/table/base.scss b/packages/itwinui-css/src/table/base.scss
index 12b8cdb7455..d32129684bf 100644
--- a/packages/itwinui-css/src/table/base.scss
+++ b/packages/itwinui-css/src/table/base.scss
@@ -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
diff --git a/packages/itwinui-react/package.json b/packages/itwinui-react/package.json
index 072c7b41803..75ffe376048 100644
--- a/packages/itwinui-react/package.json
+++ b/packages/itwinui-react/package.json
@@ -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",
diff --git a/packages/itwinui-react/src/core/Dialog/Dialog.test.tsx b/packages/itwinui-react/src/core/Dialog/Dialog.test.tsx
index 30c5c2fcc6d..270a12c78bb 100644
--- a/packages/itwinui-react/src/core/Dialog/Dialog.test.tsx
+++ b/packages/itwinui-react/src/core/Dialog/Dialog.test.tsx
@@ -177,13 +177,6 @@ it('should not stay in the DOM when isOpen=false', () => {
rerender();
- // 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();
});
diff --git a/packages/itwinui-react/src/core/Dialog/Dialog.tsx b/packages/itwinui-react/src/core/Dialog/Dialog.tsx
index 6ee248dd281..26a5c7ab339 100644
--- a/packages/itwinui-react/src/core/Dialog/Dialog.tsx
+++ b/packages/itwinui-react/src/core/Dialog/Dialog.tsx
@@ -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;
const DialogComponent = React.forwardRef((props, ref) => {
const {
@@ -42,37 +41,35 @@ const DialogComponent = React.forwardRef((props, ref) => {
} = props;
const dialogRootRef = React.useRef(null);
+ const mergedRefs = useMergedRefs(ref, dialogRootRef);
- return (
-
-
-
-
-
-
-
- );
+ return isOpen ? (
+
+
+
+
+
+ ) : null;
}) as PolymorphicForwardRefComponent<'div', DialogProps>;
if (process.env.NODE_ENV === 'development') {
DialogComponent.displayName = 'Dialog';
diff --git a/packages/itwinui-react/src/core/Dialog/DialogBackdrop.tsx b/packages/itwinui-react/src/core/Dialog/DialogBackdrop.tsx
index 9c59baeec09..c7bb6a1e65d 100644
--- a/packages/itwinui-react/src/core/Dialog/DialogBackdrop.tsx
+++ b/packages/itwinui-react/src/core/Dialog/DialogBackdrop.tsx
@@ -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);
diff --git a/packages/itwinui-react/src/core/Dialog/DialogMain.tsx b/packages/itwinui-react/src/core/Dialog/DialogMain.tsx
index e8e034dee54..a39f9b22eea 100644
--- a/packages/itwinui-react/src/core/Dialog/DialogMain.tsx
+++ b/packages/itwinui-react/src/core/Dialog/DialogMain.tsx
@@ -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();
+ const { dialogRootRef } = dialogContext;
const dialogRef = React.useRef(null);
- const hasBeenResized = React.useRef(false);
const previousFocusedElement = React.useRef();
+ const [style, setStyle] = React.useState();
+ 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) => {
if (event.altKey) {
@@ -116,6 +118,7 @@ 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);
@@ -123,7 +126,7 @@ export const DialogMain = React.forwardRef((props, ref) => {
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 = (
{
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 && (
{
if (!hasBeenResized.current) {
hasBeenResized.current = true;
@@ -204,34 +236,14 @@ export const DialogMain = React.forwardRef((props, ref) => {
);
return (
- {
- 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}
+ ({ beforeClose }), [beforeClose])}
>
{trapFocus && {content}}
{!trapFocus && content}
-
+
);
}) as PolymorphicForwardRefComponent<'div', DialogMainProps>;
if (process.env.NODE_ENV === 'development') {
diff --git a/packages/itwinui-react/src/core/Dialog/DialogMainContext.tsx b/packages/itwinui-react/src/core/Dialog/DialogMainContext.tsx
new file mode 100644
index 00000000000..22645087741
--- /dev/null
+++ b/packages/itwinui-react/src/core/Dialog/DialogMainContext.tsx
@@ -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(null);
+
+export const useDialogMainContext = () => {
+ return React.useContext(DialogMainContext);
+};
diff --git a/packages/itwinui-react/src/core/Dialog/DialogTitleBar.tsx b/packages/itwinui-react/src/core/Dialog/DialogTitleBar.tsx
index cedf2b49d4d..4699e12f469 100644
--- a/packages/itwinui-react/src/core/Dialog/DialogTitleBar.tsx
+++ b/packages/itwinui-react/src/core/Dialog/DialogTitleBar.tsx
@@ -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) => {
+ dialogMainContext?.beforeClose();
+ onClose?.(e);
+ },
+ [dialogMainContext, onClose],
+ );
+
return (
diff --git a/packages/itwinui-react/src/core/SideNavigation/SideNavigation.tsx b/packages/itwinui-react/src/core/SideNavigation/SideNavigation.tsx
index 8b98509b048..2a53531662b 100644
--- a/packages/itwinui-react/src/core/SideNavigation/SideNavigation.tsx
+++ b/packages/itwinui-react/src/core/SideNavigation/SideNavigation.tsx
@@ -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
*
* Documents
@@ -178,16 +173,7 @@ export const SideNavigation = React.forwardRef((props, forwardedRef) => {
{expanderPlacement === 'bottom' && ExpandButton}
- {submenu && (
-
- {submenu}
-
- )}
+ {submenu && isSubmenuOpen ? submenu : null}
);
diff --git a/packages/itwinui-react/src/core/Toast/Toast.tsx b/packages/itwinui-react/src/core/Toast/Toast.tsx
index 57e4a32793e..cd986d726d9 100644
--- a/packages/itwinui-react/src/core/Toast/Toast.tsx
+++ b/packages/itwinui-react/src/core/Toast/Toast.tsx
@@ -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(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 (
- {
- 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 ? (
+
-
-
-
-
-
-
- );
+
+
+
+
+ ) : null;
};
export type ToastPresentationProps = Omit<
@@ -308,3 +269,132 @@ export const ToastPresentation = React.forwardRef((props, forwardedRef) => {
);
}) 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;
+ 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;
+};
diff --git a/packages/itwinui-react/src/utils/components/WithCSSTransition.tsx b/packages/itwinui-react/src/utils/components/WithCSSTransition.tsx
deleted file mode 100644
index d10442f7838..00000000000
--- a/packages/itwinui-react/src/utils/components/WithCSSTransition.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * 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';
-import { CSSTransition } from 'react-transition-group';
-import { styles } from '../../styles.js';
-
-export const WithCSSTransition = (
- props: Partial> & {
- children: JSX.Element;
- dimension?: 'height' | 'width';
- },
-) => {
- const { in: visible, dimension = 'height', children, ...rest } = props;
-
- const expandedSize = React.useRef(0);
-
- const dimensionCamelCase = dimension === 'height' ? 'Height' : 'Width';
-
- return (
- {
- node.style[`min${dimensionCamelCase}`] = 'initial';
- node.style[dimension] = '0px';
- }}
- onEntering={(node: HTMLElement) => {
- node.style[dimension] = `${expandedSize.current}px`;
- }}
- onEntered={(node: HTMLElement) => {
- node.style[`min${dimensionCamelCase}`] = '';
- node.style[dimension] = '';
- }}
- onExit={(node: HTMLElement) => {
- node.style[dimension] = `${expandedSize.current}px`;
- }}
- onExiting={(node: HTMLElement) => {
- node.style[`min${dimensionCamelCase}`] = 'initial';
- node.style[dimension] = '0px';
- }}
- classNames={{
- enter: styles['iui-enter'],
- enterActive: styles['iui-enter-active'],
- exit: styles['iui-exit'],
- exitActive: styles['iui-exit-active'],
- }}
- {...rest}
- >
- {React.isValidElement(children) ? (
- React.cloneElement(children as JSX.Element, {
- ref: (el: HTMLElement) => {
- if (el) {
- expandedSize.current = el.getBoundingClientRect()[dimension];
- }
- },
- })
- ) : (
- <>>
- )}
-
- );
-};
diff --git a/packages/itwinui-react/src/utils/components/index.ts b/packages/itwinui-react/src/utils/components/index.ts
index d537eaaf939..498923bebdf 100644
--- a/packages/itwinui-react/src/utils/components/index.ts
+++ b/packages/itwinui-react/src/utils/components/index.ts
@@ -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';
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 70563274c5c..30c0480f023 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -353,9 +353,6 @@ importers:
react-table:
specifier: ^7.8.0
version: 7.8.0(react@18.2.0)
- react-transition-group:
- specifier: ^4.4.5
- version: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
devDependencies:
'@swc/cli':
specifier: ^0.5.1
@@ -381,9 +378,6 @@ importers:
'@types/react-dom':
specifier: 18.2.18
version: 18.2.18
- '@types/react-transition-group':
- specifier: ^4.4.10
- version: 4.4.10
'@vitest/coverage-v8':
specifier: ^1.2.1
version: 1.2.1(vitest@1.2.1(@types/node@22.5.5)(jsdom@24.0.0)(lightningcss@1.25.1)(sass@1.72.0))
@@ -4092,12 +4086,6 @@ packages:
integrity: sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==,
}
- '@types/react-transition-group@4.4.10':
- resolution:
- {
- integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==,
- }
-
'@types/react@18.2.14':
resolution:
{
@@ -6190,12 +6178,6 @@ packages:
integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==,
}
- dom-helpers@5.2.1:
- resolution:
- {
- integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==,
- }
-
dotenv@16.4.5:
resolution:
{
@@ -11550,15 +11532,6 @@ packages:
peerDependencies:
react: ^16.8.3 || ^17.0.0-0 || ^18.0.0
- react-transition-group@4.4.5:
- resolution:
- {
- integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==,
- }
- peerDependencies:
- react: '>=16.6.0'
- react-dom: '>=16.6.0'
-
react@18.2.0:
resolution:
{
@@ -16619,10 +16592,6 @@ snapshots:
dependencies:
'@types/react': 18.2.14
- '@types/react-transition-group@4.4.10':
- dependencies:
- '@types/react': 18.2.14
-
'@types/react@18.2.14':
dependencies:
'@types/prop-types': 15.7.11
@@ -18097,11 +18066,6 @@ snapshots:
dom-accessibility-api@0.6.3: {}
- dom-helpers@5.2.1:
- dependencies:
- '@babel/runtime': 7.23.9
- csstype: 3.1.3
-
dotenv@16.4.5: {}
dset@3.1.4: {}
@@ -22012,15 +21976,6 @@ snapshots:
dependencies:
react: 18.2.0
- react-transition-group@4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
- dependencies:
- '@babel/runtime': 7.23.9
- dom-helpers: 5.2.1
- loose-envify: 1.4.0
- prop-types: 15.8.1
- react: 18.2.0
- react-dom: 18.2.0(react@18.2.0)
-
react@18.2.0:
dependencies:
loose-envify: 1.4.0