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