Ver código fonte

fix(web): ensure tooltips and menus don't cause scrollbar reflow

Paul Armstrong 4 anos atrás
pai
commit
5043040530

+ 30 - 18
web/src/components/RelativeModal.jsx

@@ -2,7 +2,7 @@ import { h, Fragment } from 'preact';
 import { createPortal } from 'preact/compat';
 import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
 
-const WINDOW_PADDING = 10;
+const WINDOW_PADDING = 20;
 
 export default function RelativeModal({
   className,
@@ -13,7 +13,7 @@ export default function RelativeModal({
   relativeTo,
   widthRelative = false,
 }) {
-  const [position, setPosition] = useState({ top: -999, left: -999 });
+  const [position, setPosition] = useState({ top: -9999, left: -9999 });
   const [show, setShow] = useState(false);
   const portalRoot = portalRootID && document.getElementById(portalRootID);
   const ref = useRef(null);
@@ -53,33 +53,43 @@ export default function RelativeModal({
       const windowWidth = window.innerWidth;
       const windowHeight = window.innerHeight;
       const { width: menuWidth, height: menuHeight } = ref.current.getBoundingClientRect();
-      const { x, y, width: relativeWidth, height } = relativeTo.current.getBoundingClientRect();
+      const {
+        x: relativeToX,
+        y: relativeToY,
+        width: relativeToWidth,
+        // height: relativeToHeight,
+      } = relativeTo.current.getBoundingClientRect();
 
-      const width = widthRelative ? relativeWidth : menuWidth;
+      const _width = widthRelative ? relativeToWidth : menuWidth;
+      const width = _width * 1.1;
+
+      const left = relativeToX + window.scrollX;
+      const top = relativeToY + window.scrollY;
+
+      let newTop = top;
+      let newLeft = left;
 
-      let top = y + height;
-      let left = x;
       // too far right
-      if (left + width >= windowWidth - WINDOW_PADDING) {
-        left = windowWidth - width - WINDOW_PADDING;
+      if (newLeft + width + WINDOW_PADDING >= windowWidth - WINDOW_PADDING) {
+        newLeft = windowWidth - width - WINDOW_PADDING;
       }
       // too far left
       else if (left < WINDOW_PADDING) {
-        left = WINDOW_PADDING;
+        newLeft = WINDOW_PADDING;
       }
       // too close to bottom
-      if (top + menuHeight > windowHeight - WINDOW_PADDING) {
-        top = y - menuHeight;
+      if (top + menuHeight > windowHeight - WINDOW_PADDING + window.scrollY) {
+        newTop = relativeToY - menuHeight;
       }
 
-      if (top <= WINDOW_PADDING) {
-        top = WINDOW_PADDING;
+      if (top <= WINDOW_PADDING + window.scrollY) {
+        newTop = WINDOW_PADDING;
       }
 
       const maxHeight = windowHeight - WINDOW_PADDING * 2 > menuHeight ? null : windowHeight - WINDOW_PADDING * 2;
-      const newPosition = { left: left + window.scrollX, top: top + window.scrollY, maxHeight };
+      const newPosition = { left: newLeft, top: newTop, maxHeight };
       if (widthRelative) {
-        newPosition.width = relativeWidth;
+        newPosition.width = relativeToWidth;
       }
       setPosition(newPosition);
       const focusable = ref.current.querySelector('[tabindex]');
@@ -89,7 +99,9 @@ export default function RelativeModal({
 
   useEffect(() => {
     if (position.top >= 0) {
-      setShow(true);
+      window.requestAnimationFrame(() => {
+        setShow(true);
+      });
     } else {
       setShow(false);
     }
@@ -100,13 +112,13 @@ export default function RelativeModal({
       <div data-testid="scrim" key="scrim" className="absolute inset-0 z-10" onClick={handleDismiss} />
       <div
         key="menu"
-        className={`z-10 bg-white dark:bg-gray-700 dark:text-white absolute shadow-lg rounded w-auto h-auto transition-all duration-75 transform scale-90 opacity-0 overflow-scroll ${
+        className={`z-10 bg-white dark:bg-gray-700 dark:text-white absolute shadow-lg rounded w-auto h-auto transition-transform transition-opacity duration-75 transform scale-90 opacity-0 overflow-x-hidden overflow-y-auto ${
           show ? 'scale-100 opacity-100' : ''
         } ${className}`}
         onKeyDown={handleKeydown}
         role={role}
         ref={ref}
-        style={position.top >= 0 ? position : null}
+        style={position}
       >
         {children}
       </div>

+ 9 - 7
web/src/components/Tooltip.jsx

@@ -1,15 +1,15 @@
 import { h } from 'preact';
 import { createPortal } from 'preact/compat';
-import { useEffect, useRef, useState } from 'preact/hooks';
+import { useLayoutEffect, useRef, useState } from 'preact/hooks';
 
 const TIP_SPACE = 20;
 
 export default function Tooltip({ relativeTo, text }) {
-  const [position, setPosition] = useState({ top: -Infinity, left: -Infinity });
+  const [position, setPosition] = useState({ top: -9999, left: -9999 });
   const portalRoot = document.getElementById('tooltips');
   const ref = useRef();
 
-  useEffect(() => {
+  useLayoutEffect(() => {
     if (ref && ref.current && relativeTo && relativeTo.current) {
       const windowWidth = window.innerWidth;
       const {
@@ -18,7 +18,9 @@ export default function Tooltip({ relativeTo, text }) {
         width: relativeToWidth,
         height: relativeToHeight,
       } = relativeTo.current.getBoundingClientRect();
-      const { width: tipWidth, height: tipHeight } = ref.current.getBoundingClientRect();
+      const { width: _tipWidth, height: _tipHeight } = ref.current.getBoundingClientRect();
+      const tipWidth = _tipWidth * 1.1;
+      const tipHeight = _tipHeight * 1.1;
 
       const left = relativeToX + Math.round(relativeToWidth / 2) + window.scrollX;
       const top = relativeToY + Math.round(relativeToHeight / 2) + window.scrollY;
@@ -47,11 +49,11 @@ export default function Tooltip({ relativeTo, text }) {
   const tooltip = (
     <div
       role="tooltip"
-      className={`shadow max-w-lg absolute pointer-events-none bg-gray-900 dark:bg-gray-200 bg-opacity-80 rounded px-2 py-1 transition-opacity duration-200 opacity-0 text-gray-100 dark:text-gray-900 text-sm ${
-        position.top >= 0 ? 'opacity-100' : ''
+      className={`shadow max-w-lg absolute pointer-events-none bg-gray-900 dark:bg-gray-200 bg-opacity-80 rounded px-2 py-1 transition-transform transition-opacity duration-75 transform scale-90 opacity-0 text-gray-100 dark:text-gray-900 text-sm ${
+        position.top >= 0 ? 'opacity-100 scale-100' : ''
       }`}
       ref={ref}
-      style={position.top >= 0 ? position : null}
+      style={position}
     >
       {text}
     </div>

+ 5 - 5
web/src/components/__tests__/Toolltip.test.jsx

@@ -26,8 +26,8 @@ describe('Tooltip', () => {
 
     const tooltip = await screen.findByRole('tooltip');
     const style = window.getComputedStyle(tooltip);
-    expect(style.left).toEqual('105px');
-    expect(style.top).toEqual('70px');
+    expect(style.left).toEqual('103px');
+    expect(style.top).toEqual('68.5px');
   });
 
   test('if too far right, renders to the left', async () => {
@@ -54,7 +54,7 @@ describe('Tooltip', () => {
 
     const tooltip = await screen.findByRole('tooltip');
     const style = window.getComputedStyle(tooltip);
-    expect(style.left).toEqual('942px');
+    expect(style.left).toEqual('937px');
     expect(style.top).toEqual('97px');
   });
 
@@ -109,7 +109,7 @@ describe('Tooltip', () => {
 
     const tooltip = await screen.findByRole('tooltip');
     const style = window.getComputedStyle(tooltip);
-    expect(style.left).toEqual('87px');
-    expect(style.top).toEqual('160px');
+    expect(style.left).toEqual('84px');
+    expect(style.top).toEqual('158.5px');
   });
 });