Browse Source

fix(clippedbox): Adjust max-height to the content height when revealing (#62015)

Fixes https://github.com/getsentry/sentry/issues/61996

This fixes a couple problems with the ClippedBox component:

1. Content greater than 9999px in height is cut off when revealed
2. The reveal animation speed is inconsistent based on how large the
content is

The new logic sets the max-height to the content height when revealing.
After the animation finishes, the max-height is cleared so that height
changes are reflected.
Malachi Willey 1 year ago
parent
commit
6f98f2522b
1 changed files with 80 additions and 5 deletions
  1. 80 5
      static/app/components/clippedBox.tsx

+ 80 - 5
static/app/components/clippedBox.tsx

@@ -6,6 +6,11 @@ import {Button, ButtonProps} from 'sentry/components/button';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 
+// Content may have margins which can't be measured by our refs, but will affect
+// the total content height. We add this to the max-height to ensure the animation
+// doesn't cut off early.
+const HEIGHT_ADJUSTMENT_FOR_CONTENT_MARGIN = 20;
+
 function isClipped(args: {clipFlex: number; clipHeight: number; height: number}) {
   return args.height > args.clipHeight + args.clipFlex;
 }
@@ -16,11 +21,59 @@ function supportsResizeObserver(
   return typeof observerOrUndefined !== 'undefined';
 }
 
+/**
+ * The Wrapper component contains padding by default, which may be modified by consumers.
+ * Without adding this padding to the max-height of the child content, the reveal
+ * animation will be cut short.
+ */
+function calculateAddedHeight({
+  wrapperRef,
+}: {
+  wrapperRef: React.MutableRefObject<HTMLElement | null>;
+}): number {
+  if (wrapperRef.current === null) {
+    return 0;
+  }
+
+  try {
+    const {paddingTop, paddingBottom} = getComputedStyle(wrapperRef.current);
+
+    const addedHeight =
+      parseInt(paddingTop, 10) +
+      parseInt(paddingBottom, 10) +
+      HEIGHT_ADJUSTMENT_FOR_CONTENT_MARGIN;
+
+    return isNaN(addedHeight) ? 0 : addedHeight;
+  } catch {
+    return 0;
+  }
+}
+
+function clearMaxHeight(element?: HTMLElement | null) {
+  if (element) {
+    element.style.maxHeight = 'none';
+  }
+}
+
+function onTransitionEnd(e: TransitionEvent) {
+  // This can fire for children transitions, so we need to make sure it's the
+  // reveal animation that has ended.
+  if (e.target === e.currentTarget && e.propertyName === 'max-height') {
+    const element = e.currentTarget as HTMLElement;
+    clearMaxHeight(element);
+    element.removeEventListener('transitionend', onTransitionEnd);
+  }
+}
+
 function revealAndDisconnectObserver({
+  contentRef,
   observerRef,
   revealRef,
   wrapperRef,
+  clipHeight,
 }: {
+  clipHeight: number;
+  contentRef: React.MutableRefObject<HTMLElement | null>;
   observerRef: React.MutableRefObject<ResizeObserver | null>;
   revealRef: React.MutableRefObject<boolean>;
   wrapperRef: React.MutableRefObject<HTMLElement | null>;
@@ -29,7 +82,17 @@ function revealAndDisconnectObserver({
     return;
   }
 
-  wrapperRef.current.style.maxHeight = '9999px';
+  const revealedWrapperHeight =
+    (contentRef.current?.clientHeight || 9999) + calculateAddedHeight({wrapperRef});
+
+  // Only animate if the revealed height is greater than the clip height
+  if (revealedWrapperHeight > clipHeight) {
+    wrapperRef.current.addEventListener('transitionend', onTransitionEnd);
+    wrapperRef.current.style.maxHeight = `${revealedWrapperHeight}px`;
+  } else {
+    clearMaxHeight();
+  }
+
   revealRef.current = true;
 
   if (observerRef.current) {
@@ -90,14 +153,20 @@ function ClippedBox(props: ClippedBoxProps) {
 
       event.stopPropagation();
 
-      revealAndDisconnectObserver({wrapperRef, revealRef, observerRef});
+      revealAndDisconnectObserver({
+        contentRef,
+        wrapperRef,
+        revealRef,
+        observerRef,
+        clipHeight,
+      });
       if (typeof onReveal === 'function') {
         onReveal();
       }
 
       setClipped(false);
     },
-    [onReveal]
+    [clipHeight, onReveal]
   );
 
   const onWrapperRef = useCallback(
@@ -159,7 +228,13 @@ function ClippedBox(props: ClippedBoxProps) {
         });
 
         if (!_clipped && contentRef.current) {
-          revealAndDisconnectObserver({wrapperRef, revealRef, observerRef});
+          revealAndDisconnectObserver({
+            contentRef,
+            wrapperRef,
+            revealRef,
+            observerRef,
+            clipHeight,
+          });
         }
 
         setClipped(_clipped);
@@ -218,7 +293,7 @@ const Wrapper = styled('div')`
   padding: ${space(1.5)} 0;
   overflow: hidden;
   will-change: max-height;
-  transition: all 5s ease-in-out 0s;
+  transition: max-height 500ms ease-in-out;
 `;
 
 const Title = styled('h5')`