Skip to content

Commit

Permalink
Merge pull request #5710 from gooddata/ine-lx-599
Browse files Browse the repository at this point in the history
feat: show resizers + new style
  • Loading branch information
ivan-nejezchleb authored Dec 16, 2024
2 parents a42cde9 + 2c41e06 commit c7e7e55
Show file tree
Hide file tree
Showing 17 changed files with 281 additions and 27 deletions.
2 changes: 1 addition & 1 deletion libs/sdk-ui-dashboard/src/_staging/layout/sizing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ function normalizeSizeToParent(
return {
xl: {
gridHeight: itemSize.xl.gridHeight, // keep height untouched as container can be extended freely in this direction
gridWidth: width <= parentWidth ? width : itemMinWidth,
gridWidth: width <= parentWidth ? width : Math.max(parentWidth, itemMinWidth),
},
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ import { IDashboardProps } from "../types.js";
import { DashboardLoading } from "./DashboardLoading.js";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { DefaultEmptyLayoutDropZoneBody, LayoutResizeStateProvider } from "../../dragAndDrop/index.js";
import {
DefaultEmptyLayoutDropZoneBody,
LayoutResizeStateProvider,
HoveredWidgetProvider,
} from "../../dragAndDrop/index.js";
import { RenderModeAwareDashboardSidebar } from "../DashboardSidebar/RenderModeAwareDashboardSidebar.js";
import { DASHBOARD_OVERLAYS_Z_INDEX } from "../../constants/index.js";
import { DashboardItemPathAndSizeProvider } from "./DashboardItemPathAndSizeContext.js";
Expand Down Expand Up @@ -170,7 +174,9 @@ export const DashboardRenderer: React.FC<IDashboardProps> = (props: IDashboardPr
<DndProvider backend={HTML5Backend}>
<LayoutResizeStateProvider>
<DashboardItemPathAndSizeProvider>
<DashboardLoading {...props} />
<HoveredWidgetProvider>
<DashboardLoading {...props} />
</HoveredWidgetProvider>
</DashboardItemPathAndSizeProvider>
</LayoutResizeStateProvider>
</DndProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// (C) 2024 GoodData Corporation
import { ObjRef, areObjRefsEqual } from "@gooddata/sdk-model";
import React, { createContext, useContext, useState, ReactNode } from "react";

// Define the shape of the context state
interface HoveredWidgetContextState {
hoveredWidgets: ObjRef[] | null;
addHoveredWidget: (widgetRef: ObjRef | null) => void;
removeHoveredWidget: (widgetRef: ObjRef | null) => void;
isHovered: (widgetRef: ObjRef) => boolean;
}

// Create the context with a default value
const HoveredWidgetContext = createContext<HoveredWidgetContextState | undefined>(undefined);

// Create the provider component
export const HoveredWidgetProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [hoveredWidgets, setHoveredWidget] = useState<ObjRef[]>([]);

const addHoveredWidget = (widgetRef: ObjRef | null) => {
if (widgetRef && !hoveredWidgets?.some((ref) => areObjRefsEqual(ref, widgetRef))) {
setHoveredWidget((prevWidgets) => [...(prevWidgets || []), widgetRef]);
}
};

const removeHoveredWidget = (widgetRef: ObjRef | null) => {
if (widgetRef && hoveredWidgets) {
setHoveredWidget(
(prevWidgets) => prevWidgets?.filter((ref) => !areObjRefsEqual(ref, widgetRef)) ?? [],
);
}
};

const isHovered = (widgetRef: ObjRef) => {
return hoveredWidgets?.some((ref) => areObjRefsEqual(ref, widgetRef)) || false;
};

return (
<HoveredWidgetContext.Provider
value={{ hoveredWidgets, addHoveredWidget, removeHoveredWidget, isHovered }}
>
{children}
</HoveredWidgetContext.Provider>
);
};

// Custom hook to use the HoveredWidgetContext
export const useHoveredWidget = (): HoveredWidgetContextState => {
const context = useContext(HoveredWidgetContext);
if (!context) {
throw new Error("useHoveredWidget must be used within a HoveredWidgetProvider");
}
return context;
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export * from "./useDashboardDragScroll.js";
export * from "./LayoutResizeContext.js";
export * from "./WrapInsightListItemWithDrag.js";
export * from "./useCurrentDateFilterConfig.js";
export * from "./HoveredWidgetContext.js";
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { Hotspot } from "./dragAndDrop/draggableWidget/Hotspot.js";
import { useWidgetDragEndHandler } from "../dragAndDrop/draggableWidget/useWidgetDragEndHandler.js";
import { DashboardItemPathAndSizeProvider } from "../dashboard/components/DashboardItemPathAndSizeContext.js";
import { shouldShowRowEndDropZone } from "./dragAndDrop/draggableWidget/RowEndHotspot.js";
import { HoverDetector } from "./dragAndDrop/Resize/HoverDetector.js";

/**
* Tests in KD require widget index for css selectors.
Expand Down Expand Up @@ -218,18 +219,20 @@ export const DashboardLayoutWidget: IDashboardLayoutWidgetRenderer<
/>
) : null}
<DashboardItemPathAndSizeProvider itemPath={item.index()} itemSize={item.size()}>
<DashboardWidget
// @ts-expect-error Don't expose index prop on public interface (we need it only for css class for KD tests)
index={index}
onDrill={onDrill}
onError={onError}
onFiltersChange={onFiltersChange}
widget={widget as ExtendedDashboardWidget}
parentLayoutItemSize={item.size()}
parentLayoutPath={item.index()}
ErrorComponent={ErrorComponent}
LoadingComponent={LoadingComponent}
/>
<HoverDetector widgetRef={widget.ref}>
<DashboardWidget
// @ts-expect-error Don't expose index prop on public interface (we need it only for css class for KD tests)
index={index}
onDrill={onDrill}
onError={onError}
onFiltersChange={onFiltersChange}
widget={widget as ExtendedDashboardWidget}
parentLayoutItemSize={item.size()}
parentLayoutPath={item.index()}
ErrorComponent={ErrorComponent}
LoadingComponent={LoadingComponent}
/>
</HoverDetector>
</DashboardItemPathAndSizeProvider>
{canShowHotspot && !isAnyPlaceholderWidget(widget) && isActive ? (
<ResizeOverlay
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ export const HeightResizerDragPreview = (props: HeightResizerDragPreviewProps) =
};

return (
<div className="height-resizer-drag-preview s-height-resizer-drag-preview" style={style}>
<div
className="height-resizer-drag-preview s-height-resizer-drag-preview gd-grid-layout-resizer-drag-preview"
style={style}
>
<HeightResizer status={"active"} />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ export function WidthResizerDragPreview(props: WidthResizerDragPreviewProps) {
]);

return (
<div className="s-resizer-drag-preview resizer-drag-preview" style={style}>
<div
className="s-resizer-drag-preview resizer-drag-preview gd-grid-layout-resizer-drag-preview"
style={style}
>
<WidthResizer status="active" />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,20 @@ import { ResizerProps } from "./types.js";
export const HeightResizer: React.FC<ResizerProps> = (props) => {
const { status } = props;
const boxClassName = cx("gd-fluidlayout-height-resizer", status, "s-gd-fluidlayout-height-resizer");
const handlerClassName = cx("width-resizer-drag-handler", status);
const lineClassName = cx("height-resizer-line", status);

const showDragHandler = status === "active";

return (
<div className={boxClassName}>
<div className={lineClassName} />
{showDragHandler ? (
<>
<div className={handlerClassName} />
<div className={lineClassName} />
</>
) : null}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import cx from "classnames";
import { useScreenSize } from "../../../dashboard/components/DashboardScreenSizeContext.js";
import { useResizeContext } from "../../../dragAndDrop/index.js";
import { useDashboardItemPathAndSize } from "../../../dashboard/components/DashboardItemPathAndSizeContext.js";
import { useHoveredWidget } from "../../../dragAndDrop/HoveredWidgetContext.js";

export type HeightResizerHotspotProps = {
section: IDashboardLayoutSectionFacade<unknown>;
Expand All @@ -54,6 +55,10 @@ export function HeightResizerHotspot({ section, items, getLayoutDimensions }: He
const widgetIdentifiers = useMemo(() => widgets.map((widget) => widget.identifier), [widgets]);
const customWidgetsRestrictions = useMemo(() => getCustomWidgetRestrictions(items), [items]);

const { isHovered } = useHoveredWidget();

const showDefault = useMemo(() => widgets.some((widget) => isHovered(widget.ref)), [isHovered, widgets]);

const gridWidth = determineWidthForScreen(screen, itemSize);

const [{ isDragging }, dragRef] = useDashboardDrag(
Expand Down Expand Up @@ -117,7 +122,7 @@ export function HeightResizerHotspot({ section, items, getLayoutDimensions }: He
const shouldRenderResizer =
(areWidgetsResizing || isResizerVisible) && !isColumnResizing && !isOtherRowResizing;

const status = isDragging ? "muted" : "active";
const status = isDragging ? "muted" : showDefault ? "default" : "active";

return (
<div
Expand All @@ -126,6 +131,11 @@ export function HeightResizerHotspot({ section, items, getLayoutDimensions }: He
`gd-grid-layout__item--span-${gridWidth}`,
)}
>
{status === "default" ? (
<div className="dash-height-resizer-hotspot s-dash-height-resizer-hotspot">
{<HeightResizer status={status} />}
</div>
) : null}
{customWidgetsRestrictions.allowHeightResize ? (
<div
ref={dragRef}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// (C) 2019-2024 GoodData Corporation
import React, { useEffect, useRef } from "react";
import { ObjRef } from "@gooddata/sdk-model";
import { useHoveredWidget } from "../../../dragAndDrop/HoveredWidgetContext.js";

interface HoverDetectorProps {
widgetRef: ObjRef;
children?: React.ReactNode;
}

export const HoverDetector: React.FC<HoverDetectorProps> = ({ widgetRef, children }) => {
const { addHoveredWidget, removeHoveredWidget } = useHoveredWidget();
const divRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const handleMouseEnter = () => {
addHoveredWidget(widgetRef);
};
const handleMouseLeave = () => {
removeHoveredWidget(widgetRef);
};

const divElement = divRef.current;
if (divElement) {
divElement.addEventListener("mouseenter", handleMouseEnter);
divElement.addEventListener("mouseleave", handleMouseLeave);
}

return () => {
if (divElement) {
divElement.removeEventListener("mouseenter", handleMouseEnter);
divElement.removeEventListener("mouseleave", handleMouseLeave);
}
};
}, [addHoveredWidget, removeHoveredWidget, widgetRef]);

return (
<div ref={divRef} className="gd-hover-detector">
{children}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,21 @@ import { ResizerProps } from "./types.js";
export function WidthResizer({ status }: ResizerProps) {
const boxClassName = cx("gd-fluidlayout-width-resizer", status, "s-gd-fluidlayout-width-resizer");
const lineClassName = cx("width-resizer-line", status);
const handlerClassName = cx("width-resizer-drag-handler", status);

const showDragHandler = status === "active";

return (
<div className={boxClassName}>
<div className={lineClassName} />
<div className="gd-fluidlayout-width-resizer__container">
<div className={boxClassName}>
<div className={lineClassName} />
{showDragHandler ? (
<>
<div className={handlerClassName} />
<div className={lineClassName} />
</>
) : null}
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { getSizeAndXCoords } from "../DragLayerPreview/WidthResizerDragPreview.j
import { useDashboardDrag, useResizeHandlers, useResizeWidthItemStatus } from "../../../dragAndDrop/index.js";
import { WidthResizer } from "./WidthResizer.js";
import { useScreenSize } from "../../../dashboard/components/DashboardScreenSizeContext.js";
import { useHoveredWidget } from "../../../dragAndDrop/HoveredWidgetContext.js";

export type WidthResizerHotspotProps = {
item: IDashboardLayoutItemFacade<unknown>;
Expand All @@ -37,6 +38,7 @@ export function WidthResizerHotspot({
const widget = useMemo(() => item.widget() as IWidget, [item]);
const widgetIdentifier = widget.identifier;
const { isWidthResizing, isActive } = useResizeWidthItemStatus(widgetIdentifier);
const { isHovered } = useHoveredWidget();

const [isResizerVisible, setResizerVisibility] = useState<boolean>(false);
const onMouseEnter = () => setResizerVisibility(true);
Expand Down Expand Up @@ -94,14 +96,19 @@ export function WidthResizerHotspot({

const showHotspot = !isDragging || isWidthResizing || isResizerVisible;
const showResizer = isResizerVisible || isThisResizing;
const status = isDragging ? "muted" : "active";
const status = isDragging ? "muted" : isHovered(widget.ref) ? "default" : "active";

if (!showHotspot) {
return null;
}

return (
<div className="dash-width-resizer-container">
{status === "default" ? (
<div className="dash-width-resizer-hotspot s-dash-width-resizer-hotspot">
{<WidthResizer status={status} />}
</div>
) : null}
<div
className="dash-width-resizer-hotspot s-dash-width-resizer-hotspot"
onMouseEnter={onMouseEnter}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// (C) 2022-2024 GoodData Corporation

export type ResizerStatus = "active" | "muted" | "error";
export type ResizerStatus = "default" | "active" | "muted" | "error";

export interface ResizerProps {
status: ResizerStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ export interface IDashboardItemBaseProps {
* Callback to call when an item is selected. Called with the relevant mouse event if originating from a click.
*/
onSelected?: (e?: MouseEvent) => void;

onEnter?: () => void;
onLeave?: () => void;
}

const noopRender = () => null;
Expand All @@ -70,6 +73,8 @@ export const DashboardItemBase: React.FC<IDashboardItemBaseProps> = ({
isSelectable = false,
isSelected = false,
onSelected,
onEnter,
onLeave,
}) => {
return (
<DashboardItemContentWrapper>
Expand All @@ -82,6 +87,8 @@ export const DashboardItemBase: React.FC<IDashboardItemBaseProps> = ({
isSelectable={isSelectable}
isSelected={isSelected}
onSelected={onSelected}
onEnter={onEnter}
onLeave={onLeave}
>
{renderBeforeVisualization()}
<div className={visualizationClassName}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// (C) 2020-2022 GoodData Corporation
// (C) 2020-2024 GoodData Corporation
import React, { forwardRef, MouseEvent } from "react";
import cx from "classnames";

Expand All @@ -8,10 +8,15 @@ interface IDashboardItemContentProps {
isSelectable?: boolean;
isSelected?: boolean;
onSelected?: (e?: MouseEvent) => void;
onEnter?: () => void;
onLeave?: () => void;
}

export const DashboardItemContent = forwardRef<HTMLDivElement, IDashboardItemContentProps>(
function DashboardItemContent({ children, className, isSelectable, isSelected, onSelected }, ref) {
function DashboardItemContent(
{ children, className, isSelectable, isSelected, onSelected, onEnter, onLeave },
ref,
) {
return (
<div
className={cx("dash-item-content", className, {
Expand All @@ -20,6 +25,8 @@ export const DashboardItemContent = forwardRef<HTMLDivElement, IDashboardItemCon
})}
ref={ref}
onClick={onSelected}
onMouseOver={onEnter}
onMouseOut={onLeave}
>
{children}
</div>
Expand Down
Loading

0 comments on commit c7e7e55

Please sign in to comment.