This is an automated email from the ASF dual-hosted git repository.

sadpandajoe pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 4bce27f12f4 fix(dashboard): shrink row height on chart resize and 
unblock resize handles above columns (#44359)
4bce27f12f4 is described below

commit 4bce27f12f486490fbd300ed89f114a3d6acc901
Author: Joe Li <[email protected]>
AuthorDate: Fri Sep 25 10:51:05 2026 -0700

    fix(dashboard): shrink row height on chart resize and unblock resize 
handles above columns (#44359)
    
    Co-authored-by: Claude Opus 5 <[email protected]>
    Co-authored-by: Evan Rusackas <[email protected]>
    Co-authored-by: Evan Rusackas <[email protected]>
---
 .../components/gridComponents/Row/Row.test.tsx     |  43 ++++++-
 .../components/gridComponents/Row/Row.tsx          | 124 +++++++++++----------
 .../src/dashboard/components/menu/HoverMenu.tsx    |   4 +-
 .../components/resizable/ResizableContainer.tsx    |   3 +-
 superset-frontend/src/dashboard/constants.ts       |   6 +
 5 files changed, 117 insertions(+), 63 deletions(-)

diff --git 
a/superset-frontend/src/dashboard/components/gridComponents/Row/Row.test.tsx 
b/superset-frontend/src/dashboard/components/gridComponents/Row/Row.test.tsx
index 4c25e0f80e3..aaaee34efba 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Row/Row.test.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Row/Row.test.tsx
@@ -65,11 +65,20 @@ jest.mock('src/dashboard/components/dnd/DragDroppable', () 
=> ({
   Droppable: ({
     children,
     depth,
+    style,
+    className,
   }: {
     children: (args: object) => React.ReactNode;
     depth: number;
+    style?: React.CSSProperties;
+    className?: string;
   }) => (
-    <div data-test="mock-droppable" data-depth={depth}>
+    <div
+      data-test="mock-droppable"
+      data-depth={depth}
+      className={className}
+      style={style}
+    >
       {children({})}
     </div>
   ),
@@ -269,6 +278,38 @@ test('should increment the depth of its children', () => {
   );
 });
 
+test('row droptargets size via CSS instead of a measured pixel height that can 
only grow (regression for #37644)', () => {
+  const { container } = setup({ editMode: true });
+  const getDroptargetHeights = () =>
+    Array.from(
+      container.querySelectorAll<HTMLElement>('.empty-droptarget--vertical'),
+    ).map(el => el.style.height);
+
+  // The leading droptarget (index 0) is absolutely positioned, so a
+  // percentage height resolves fine and stretches it to the row via CSS
+  // rather than a pixel value measured from the row's tallest chart -- it
+  // can never be left pinned to a chart's prior (larger) height after a
+  // resize. The droptarget after the chart is an in-flow flex item under
+  // GridRow's indefinite `height: fit-content`, where a percentage height
+  // resolves to `auto`/is ignored per the flexbox spec (defeating the
+  // `align-self: stretch` CSS already declares for it) -- it gets an
+  // explicit `auto` instead, letting that stretch actually apply.
+  expect(getDroptargetHeights()).toEqual(['100%', 'auto']);
+});
+
+test('trailing droptarget also gets a CSS-driven height when the row is full 
(regression for #37644)', () => {
+  const { container } = setup({ editMode: true, occupiedColumnCount: 12 });
+  const getDroptargetHeights = () =>
+    Array.from(
+      container.querySelectorAll<HTMLElement>('.empty-droptarget--vertical'),
+    ).map(el => el.style.height);
+
+  // With no remaining columns, the droptarget after the last chart is
+  // treated as a side target too (isTrailingSideTarget), so it takes the
+  // same '100%' CSS height as the leading one instead of 'auto'.
+  expect(getDroptargetHeights()).toEqual(['100%', '100%']);
+});
+
 // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from 
describe blocks
 describe('visibility handling for intersection observers', () => {
   const mockIntersectionObserver = jest.fn();
diff --git 
a/superset-frontend/src/dashboard/components/gridComponents/Row/Row.tsx 
b/superset-frontend/src/dashboard/components/gridComponents/Row/Row.tsx
index a641e5481c7..b5f4d3d0280 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Row/Row.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Row/Row.tsx
@@ -22,7 +22,6 @@ import {
   useCallback,
   useRef,
   useEffect,
-  useLayoutEffect,
   useMemo,
   memo,
   RefObject,
@@ -102,6 +101,7 @@ const GridRow = styled.div<{ editMode: boolean }>`
       align-self: center;
       &.empty-droptarget--vertical {
         min-width: ${theme.sizeUnit * 4}px;
+        align-self: stretch;
         &:not(:last-child) {
           width: ${theme.sizeUnit * 4}px;
         }
@@ -183,7 +183,6 @@ const Row = memo((props: RowProps) => {
   const [isFocused, setIsFocused] = useState(false);
   const [isInView, setIsInView] = useState(false);
   const [hoverMenuHovered, setHoverMenuHovered] = useState(false);
-  const [containerHeight, setContainerHeight] = useState<number | null>(null);
   const containerRef = useRef<HTMLDivElement | null>(null);
   const isComponentVisibleRef = useRef(isComponentVisible);
 
@@ -284,14 +283,6 @@ const Row = memo((props: RowProps) => {
     };
   }, []);
 
-  useLayoutEffect(() => {
-    if (!editMode) return;
-    const updatedHeight = containerRef.current?.clientHeight;
-    if (updatedHeight !== undefined && updatedHeight !== containerHeight) {
-      setContainerHeight(updatedHeight);
-    }
-  });
-
   const handleChangeFocus = useCallback((nextFocus: boolean) => {
     setIsFocused(Boolean(nextFocus));
   }, []);
@@ -403,7 +394,7 @@ const Row = memo((props: RowProps) => {
               )}
               editMode
               style={{
-                height: rowItems.length > 0 ? containerHeight : '100%',
+                height: '100%',
                 ...(rowItems.length > 0 && { width: 16 }),
               }}
             >
@@ -416,54 +407,68 @@ const Row = memo((props: RowProps) => {
             <div css={emptyRowContentStyles as any}>{t('Empty row')}</div>
           )}
           {rowItems.length > 0 &&
-            rowItems.map((componentId, itemIndex) => (
-              <Fragment key={componentId}>
-                <DashboardComponent
-                  key={componentId}
-                  id={componentId}
-                  parentId={rowComponent.id as string}
-                  depth={depth + 1}
-                  index={itemIndex}
-                  availableColumnCount={remainColumnCount}
-                  columnWidth={columnWidth}
-                  onResizeStart={onResizeStart}
-                  onResize={onResize}
-                  onResizeStop={onResizeStop}
-                  isComponentVisible={isComponentVisible}
-                  onChangeTab={onChangeTab}
-                  isInView={isInView}
-                />
-                {editMode && (
-                  <Droppable
-                    component={rowItems}
-                    parentComponent={rowComponent}
-                    depth={depth}
-                    index={itemIndex + 1}
-                    orientation="row"
-                    onDrop={handleComponentDrop}
-                    className={cx(
-                      'empty-droptarget',
-                      'empty-droptarget--vertical',
-                      remainColumnCount === 0 &&
-                        itemIndex === rowItems.length - 1 &&
-                        'droptarget-side',
-                    )}
-                    editMode
-                    style={{
-                      height: containerHeight,
-                      ...(remainColumnCount === 0 &&
-                        itemIndex === rowItems.length - 1 && { width: 16 }),
-                    }}
-                  >
-                    {({
-                      dropIndicatorProps,
-                    }: {
-                      dropIndicatorProps: JsonObject;
-                    }) => dropIndicatorProps && <div {...dropIndicatorProps} 
/>}
-                  </Droppable>
-                )}
-              </Fragment>
-            ))}
+            rowItems.map((componentId, itemIndex) => {
+              const isTrailingSideTarget =
+                remainColumnCount === 0 && itemIndex === rowItems.length - 1;
+              return (
+                <Fragment key={componentId}>
+                  <DashboardComponent
+                    key={componentId}
+                    id={componentId}
+                    parentId={rowComponent.id as string}
+                    depth={depth + 1}
+                    index={itemIndex}
+                    availableColumnCount={remainColumnCount}
+                    columnWidth={columnWidth}
+                    onResizeStart={onResizeStart}
+                    onResize={onResize}
+                    onResizeStop={onResizeStop}
+                    isComponentVisible={isComponentVisible}
+                    onChangeTab={onChangeTab}
+                    isInView={isInView}
+                  />
+                  {editMode && (
+                    <Droppable
+                      component={rowItems}
+                      parentComponent={rowComponent}
+                      depth={depth}
+                      index={itemIndex + 1}
+                      orientation="row"
+                      onDrop={handleComponentDrop}
+                      className={cx(
+                        'empty-droptarget',
+                        'empty-droptarget--vertical',
+                        isTrailingSideTarget && 'droptarget-side',
+                      )}
+                      editMode
+                      style={{
+                        // Only the last target in a full row (the absolutely
+                        // positioned "side" target) actually resolves a
+                        // percentage height -- for every other, in-flow
+                        // target, GridRow's height is `fit-content`
+                        // (indefinite), and a percentage height on a flex
+                        // item under an indefinite-height parent resolves to
+                        // `auto`/is ignored per the CSS flexbox spec, which
+                        // defeats the `align-self: stretch` the CSS above
+                        // already declares for it. `height: 'auto'` lets that
+                        // stretch actually apply instead of collapsing the
+                        // target to its content size.
+                        height: isTrailingSideTarget ? '100%' : 'auto',
+                        ...(isTrailingSideTarget && { width: 16 }),
+                      }}
+                    >
+                      {({
+                        dropIndicatorProps,
+                      }: {
+                        dropIndicatorProps: JsonObject;
+                      }) =>
+                        dropIndicatorProps && <div {...dropIndicatorProps} />
+                      }
+                    </Droppable>
+                  )}
+                </Fragment>
+              );
+            })}
         </GridRow>
       </WithPopoverMenu>
     ),
@@ -471,7 +476,6 @@ const Row = memo((props: RowProps) => {
       backgroundStyle.className,
       backgroundStyle.value,
       columnWidth,
-      containerHeight,
       depth,
       editMode,
       handleChangeBackground,
diff --git a/superset-frontend/src/dashboard/components/menu/HoverMenu.tsx 
b/superset-frontend/src/dashboard/components/menu/HoverMenu.tsx
index 58549b4bea2..24b78f72db0 100644
--- a/superset-frontend/src/dashboard/components/menu/HoverMenu.tsx
+++ b/superset-frontend/src/dashboard/components/menu/HoverMenu.tsx
@@ -21,6 +21,8 @@ import { RefObject, ReactNode, useCallback, memo } from 
'react';
 import { styled } from '@apache-superset/core/theme';
 import cx from 'classnames';
 
+import { HOVER_MENU_Z_INDEX } from 'src/dashboard/constants';
+
 interface HoverMenuProps {
   position?: 'left' | 'top';
   innerRef?: RefObject<HTMLDivElement> | null;
@@ -32,7 +34,7 @@ const HoverStyleOverrides = styled.div`
   .hover-menu {
     opacity: 0;
     position: absolute;
-    z-index: 11; /* one more than DragDroppable */
+    z-index: ${HOVER_MENU_Z_INDEX};
     font-size: ${({ theme }) => theme.fontSize};
   }
 
diff --git 
a/superset-frontend/src/dashboard/components/resizable/ResizableContainer.tsx 
b/superset-frontend/src/dashboard/components/resizable/ResizableContainer.tsx
index a84631b61c5..40eb93c3a58 100644
--- 
a/superset-frontend/src/dashboard/components/resizable/ResizableContainer.tsx
+++ 
b/superset-frontend/src/dashboard/components/resizable/ResizableContainer.tsx
@@ -36,6 +36,7 @@ import {
   BottomRightResizeHandle,
 } from './ResizableHandle';
 import { isMobileConsumptionEnabled } from 'src/hooks/useIsMobile';
+import { RESIZE_HANDLE_Z_INDEX } from 'src/dashboard/constants';
 import resizableConfig from '../../util/resizableConfig';
 import {
   GRID_BASE_UNIT,
@@ -130,7 +131,7 @@ const StyledResizable = styled(Resizable)`
 
     .resize-handle {
       opacity: 0;
-      z-index: 10;
+      z-index: ${RESIZE_HANDLE_Z_INDEX};
 
       &--bottom-right {
         position: absolute;
diff --git a/superset-frontend/src/dashboard/constants.ts 
b/superset-frontend/src/dashboard/constants.ts
index aa90bd2dfc9..5bd12ffe0d4 100644
--- a/superset-frontend/src/dashboard/constants.ts
+++ b/superset-frontend/src/dashboard/constants.ts
@@ -43,6 +43,12 @@ export const FILTER_BAR_TABS_HEIGHT = 46;
 export const BUILDER_SIDEPANEL_WIDTH = 374;
 export const OVERWRITE_INSPECT_FIELDS = ['css', 'json_metadata.filter_scopes'];
 export const EMPTY_CONTAINER_Z_INDEX = 10;
+// A grid component's hover menu floats outside the component's own box, so it
+// must paint above the drop targets it overlaps.
+export const HOVER_MENU_Z_INDEX = EMPTY_CONTAINER_Z_INDEX + 1;
+// A hover menu rendered above a column overlaps the bottom edge of whatever
+// sits above it, so resize handles have to win hit testing against it.
+export const RESIZE_HANDLE_Z_INDEX = HOVER_MENU_Z_INDEX + 1;
 
 export const DEFAULT_CROSS_FILTER_SCOPING: NativeFilterScope = {
   rootPath: [DASHBOARD_ROOT_ID],

Reply via email to