From fd26966a03af63cd5b862aeda9f33bfad3d41e55 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Thu, 10 Aug 2023 22:17:36 +0530 Subject: [PATCH] style: menu animations (#2401) --- .../Components/Menu/MenuRadioButtonItem.tsx | 6 +- .../Popover/GetPositionedPopoverStyles.ts | 75 ++++++++++++--- .../Components/Popover/Popover.tsx | 96 +++++++++---------- .../Popover/PositionedPopoverContent.tsx | 31 +++++- .../javascripts/Components/Popover/Types.ts | 3 + .../javascripts/Constants/AnimationConfigs.ts | 1 + .../Hooks/useLifecycleAnimation.ts | 46 ++++++--- 7 files changed, 172 insertions(+), 86 deletions(-) diff --git a/packages/web/src/javascripts/Components/Menu/MenuRadioButtonItem.tsx b/packages/web/src/javascripts/Components/Menu/MenuRadioButtonItem.tsx index 7b93b2963..0bacfc159 100644 --- a/packages/web/src/javascripts/Components/Menu/MenuRadioButtonItem.tsx +++ b/packages/web/src/javascripts/Components/Menu/MenuRadioButtonItem.tsx @@ -45,10 +45,8 @@ const Tooltip = ({ text }: { text: string }) => { title="Info" anchorElement={anchorElement} disableMobileFullscreenTakeover - className={classNames( - 'w-60 translate-x-2 translate-y-1 select-none rounded border border-border shadow-main', - 'z-modal bg-default px-3 py-1.5 text-left', - )} + className="z-modal bg-default px-3 py-1.5 text-left" + containerClassName="w-60" > {text} diff --git a/packages/web/src/javascripts/Components/Popover/GetPositionedPopoverStyles.ts b/packages/web/src/javascripts/Components/Popover/GetPositionedPopoverStyles.ts index 511ad7728..bce68fe13 100644 --- a/packages/web/src/javascripts/Components/Popover/GetPositionedPopoverStyles.ts +++ b/packages/web/src/javascripts/Components/Popover/GetPositionedPopoverStyles.ts @@ -10,18 +10,63 @@ const percentOf = (percent: number, value: number) => (percent / 100) * value export type PopoverCSSProperties = CSSProperties & { '--translate-x': string '--translate-y': string + '--transform-origin': string '--offset': string } -const getStylesFromRect = ( - rect: DOMRect, - options: { - disableMobileFullscreenTakeover?: boolean - maxHeight?: number | 'none' - offset?: number - }, -): PopoverCSSProperties => { - const { disableMobileFullscreenTakeover = false, maxHeight = 'none' } = options +const getTransformOrigin = (side: PopoverSide, align: PopoverAlignment) => { + switch (side) { + case 'top': + switch (align) { + case 'start': + return 'bottom left' + case 'center': + return 'bottom center' + case 'end': + return 'bottom right' + } + break + case 'bottom': + switch (align) { + case 'start': + return 'top left' + case 'center': + return 'top center' + case 'end': + return 'top right' + } + break + case 'left': + switch (align) { + case 'start': + return 'top right' + case 'center': + return 'top center' + case 'end': + return 'bottom right' + } + break + case 'right': + switch (align) { + case 'start': + return 'top left' + case 'center': + return 'top center' + case 'end': + return 'bottom left' + } + } +} + +const getStylesFromRect = (options: { + rect: DOMRect + side: PopoverSide + align: PopoverAlignment + disableMobileFullscreenTakeover?: boolean + maxHeight?: number | 'none' + offset?: number +}): PopoverCSSProperties => { + const { rect, disableMobileFullscreenTakeover = false, maxHeight = 'none' } = options const canApplyMaxHeight = maxHeight !== 'none' && (!isMobileScreen() || disableMobileFullscreenTakeover) const shouldApplyMobileWidth = isMobileScreen() && disableMobileFullscreenTakeover @@ -32,7 +77,8 @@ const getStylesFromRect = ( '--translate-x': `${shouldApplyMobileWidth ? marginForMobile / 2 : Math.floor(rect.x)}px`, '--translate-y': `${Math.floor(rect.y)}px`, '--offset': `${options.offset}px`, - transform: 'translate(var(--translate-x), var(--translate-y))', + transform: 'translate3d(var(--translate-x), var(--translate-y), 0)', + '--transform-origin': getTransformOrigin(options.side, options.align), visibility: 'visible', ...(canApplyMaxHeight && { maxHeight: `${maxHeight}px`, @@ -111,5 +157,12 @@ export const getPositionedPopoverStyles = ({ maxHeight = maxHeightFunction(maxHeight) } - return getStylesFromRect(finalPositionedRect, { disableMobileFullscreenTakeover, maxHeight, offset }) + return getStylesFromRect({ + rect: finalPositionedRect, + side: sideWithLessOverflows, + align: finalAlignment, + disableMobileFullscreenTakeover, + maxHeight, + offset, + }) } diff --git a/packages/web/src/javascripts/Components/Popover/Popover.tsx b/packages/web/src/javascripts/Components/Popover/Popover.tsx index 1cf960071..7927b9ced 100644 --- a/packages/web/src/javascripts/Components/Popover/Popover.tsx +++ b/packages/web/src/javascripts/Components/Popover/Popover.tsx @@ -5,6 +5,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, use import MobilePopoverContent from './MobilePopoverContent' import PositionedPopoverContent from './PositionedPopoverContent' import { PopoverProps } from './Types' +import { useLifecycleAnimation } from '@/Hooks/useLifecycleAnimation' type PopoverContextData = { registerChildPopover: (id: string) => void @@ -27,28 +28,40 @@ const useRegisterPopoverToParent = (popoverId: string) => { }, [parentPopoverContext, popoverId]) } -type Props = PopoverProps & { - open: boolean +const PositionedPopoverContentWithAnimation = ( + props: PopoverProps & { + childPopovers: Set + id: string + }, +) => { + const [isMounted, setElement] = useLifecycleAnimation({ + open: props.open, + exit: { + keyframes: [ + { + opacity: 0, + transform: 'scale(0.95)', + }, + ], + reducedMotionKeyframes: [ + { + opacity: 0, + }, + ], + options: { + duration: 75, + }, + }, + }) + + return isMounted ? ( + + {props.children} + + ) : null } -const Popover = ({ - align, - anchorElement, - anchorPoint, - children, - className, - open, - overrideZIndex, - side, - title, - togglePopover, - disableClickOutside, - disableMobileFullscreenTakeover, - maxHeight, - portal, - offset, - hideOnClickInModal, -}: Props) => { +const Popover = (props: PopoverProps) => { const popoverId = useRef(UuidGenerator.GenerateUuid()) const addAndroidBackHandler = useAndroidBackHandler() @@ -79,9 +92,9 @@ const Popover = ({ useEffect(() => { let removeListener: (() => void) | undefined - if (open) { + if (props.open) { removeListener = addAndroidBackHandler(() => { - togglePopover?.() + props.togglePopover?.() return true }) } @@ -91,50 +104,31 @@ const Popover = ({ removeListener() } } - }, [addAndroidBackHandler, open, togglePopover]) + }, [addAndroidBackHandler, props, props.open]) const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm) - if (isMobileScreen && !disableMobileFullscreenTakeover) { + if (isMobileScreen && !props.disableMobileFullscreenTakeover) { return ( { - togglePopover?.() + props.togglePopover?.() }} - title={title} - className={className} + title={props.title} + className={props.className} id={popoverId.current} > - {children} + {props.children} ) } - return open ? ( + return ( - - {children} - + - ) : null + ) } export default Popover diff --git a/packages/web/src/javascripts/Components/Popover/PositionedPopoverContent.tsx b/packages/web/src/javascripts/Components/Popover/PositionedPopoverContent.tsx index c43654d53..c56cdb817 100644 --- a/packages/web/src/javascripts/Components/Popover/PositionedPopoverContent.tsx +++ b/packages/web/src/javascripts/Components/Popover/PositionedPopoverContent.tsx @@ -30,6 +30,8 @@ const PositionedPopoverContent = ({ portal = true, offset, hideOnClickInModal = false, + setAnimationElement, + containerClassName, }: PopoverContentProps) => { const [popoverElement, setPopoverElement] = useState(null) const popoverRect = useAutoElementRect(popoverElement) @@ -55,6 +57,16 @@ const PositionedPopoverContent = ({ offset, }) + if (!styles) { + document.body.style.overflow = 'hidden' + } + + useLayoutEffect(() => { + return () => { + document.body.style.overflow = '' + } + }, []) + let adjustedStyles: PopoverCSSProperties | undefined = undefined if (!portal && popoverElement && styles) { @@ -97,13 +109,11 @@ const PositionedPopoverContent = ({
- {children} +
+ {children} +
) diff --git a/packages/web/src/javascripts/Components/Popover/Types.ts b/packages/web/src/javascripts/Components/Popover/Types.ts index 37e7caf59..13ea287a5 100644 --- a/packages/web/src/javascripts/Components/Popover/Types.ts +++ b/packages/web/src/javascripts/Components/Popover/Types.ts @@ -36,6 +36,7 @@ type CommonPopoverProps = { side?: PopoverSide overrideZIndex?: string className?: string + containerClassName?: string disableClickOutside?: boolean maxHeight?: (calculatedMaxHeight: number) => number togglePopover?: () => void @@ -44,6 +45,7 @@ type CommonPopoverProps = { portal?: boolean offset?: number hideOnClickInModal?: boolean + open: boolean } export type PopoverContentProps = CommonPopoverProps & { @@ -53,6 +55,7 @@ export type PopoverContentProps = CommonPopoverProps & { togglePopover?: () => void disableMobileFullscreenTakeover?: boolean id: string + setAnimationElement: (element: HTMLElement | null) => void } export type PopoverProps = diff --git a/packages/web/src/javascripts/Constants/AnimationConfigs.ts b/packages/web/src/javascripts/Constants/AnimationConfigs.ts index 8254b591d..8459176ae 100644 --- a/packages/web/src/javascripts/Constants/AnimationConfigs.ts +++ b/packages/web/src/javascripts/Constants/AnimationConfigs.ts @@ -1,5 +1,6 @@ export type AnimationConfig = { keyframes: Keyframe[] + reducedMotionKeyframes?: Keyframe[] options: KeyframeAnimationOptions initialStyle?: Partial } diff --git a/packages/web/src/javascripts/Hooks/useLifecycleAnimation.ts b/packages/web/src/javascripts/Hooks/useLifecycleAnimation.ts index d9abf7a80..dad4483e7 100644 --- a/packages/web/src/javascripts/Hooks/useLifecycleAnimation.ts +++ b/packages/web/src/javascripts/Hooks/useLifecycleAnimation.ts @@ -4,9 +4,9 @@ import { useStateRef } from './useStateRef' type Options = { open: boolean - enter: AnimationConfig + enter?: AnimationConfig enterCallback?: (element: HTMLElement) => void - exit: AnimationConfig + exit?: AnimationConfig exitCallback?: (element: HTMLElement) => void } @@ -52,37 +52,53 @@ export const useLifecycleAnimation = ( const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches - if (prefersReducedMotion) { - setIsMounted(open) - return - } - const enter = enterRef.current const enterCallback = enterCallbackRef.current const exit = exitRef.current const exitCallback = exitCallbackRef.current + if (prefersReducedMotion && !enter?.reducedMotionKeyframes && !exit?.reducedMotionKeyframes) { + setIsMounted(open) + return + } + if (open) { + if (!enter) { + setIsMounted(true) + enterCallback?.(element) + return + } if (enter.initialStyle) { Object.assign(element.style, enter.initialStyle) } - const animation = element.animate(enter.keyframes, { - ...enter.options, - fill: 'forwards', - }) + const animation = element.animate( + prefersReducedMotion && enter.reducedMotionKeyframes ? enter.reducedMotionKeyframes : enter.keyframes, + { + ...enter.options, + fill: 'forwards', + }, + ) animation.finished .then(() => { enterCallback?.(element) }) .catch(console.error) } else { + if (!exit) { + setIsMounted(false) + exitCallback?.(element) + return + } if (exit.initialStyle) { Object.assign(element.style, exit.initialStyle) } - const animation = element.animate(exit.keyframes, { - ...exit.options, - fill: 'forwards', - }) + const animation = element.animate( + prefersReducedMotion && exit.reducedMotionKeyframes ? exit.reducedMotionKeyframes : exit.keyframes, + { + ...exit.options, + fill: 'forwards', + }, + ) animation.finished .then(() => { setIsMounted(false)