From c8d53c51cb111f5798f293277cb3886782f6839f Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Mon, 19 Aug 2024 16:26:31 +0200 Subject: [PATCH] Adjustments and new menu utility components --- .../src/lib/components/MenuDivider/index.ts | 1 + .../components/MenuDivider/menuDivider.tsx | 3 + .../src/lib/components/MenuHeading/index.ts | 2 + .../components/MenuHeading/menuHeading.tsx | 9 ++ .../components/SortableList/sortableList.tsx | 90 ++++++++++++------- .../SortableList/sortableListGroup.tsx | 65 +++++++++----- .../SortableList/sortableListItem.tsx | 11 +-- 7 files changed, 119 insertions(+), 62 deletions(-) create mode 100644 frontend/src/lib/components/MenuDivider/index.ts create mode 100644 frontend/src/lib/components/MenuDivider/menuDivider.tsx create mode 100644 frontend/src/lib/components/MenuHeading/index.ts create mode 100644 frontend/src/lib/components/MenuHeading/menuHeading.tsx diff --git a/frontend/src/lib/components/MenuDivider/index.ts b/frontend/src/lib/components/MenuDivider/index.ts new file mode 100644 index 000000000..607a0e175 --- /dev/null +++ b/frontend/src/lib/components/MenuDivider/index.ts @@ -0,0 +1 @@ +export { MenuDivider } from "./menuDivider"; diff --git a/frontend/src/lib/components/MenuDivider/menuDivider.tsx b/frontend/src/lib/components/MenuDivider/menuDivider.tsx new file mode 100644 index 000000000..b72cfe9f9 --- /dev/null +++ b/frontend/src/lib/components/MenuDivider/menuDivider.tsx @@ -0,0 +1,3 @@ +export function MenuDivider(): React.ReactNode { + return
; +} diff --git a/frontend/src/lib/components/MenuHeading/index.ts b/frontend/src/lib/components/MenuHeading/index.ts new file mode 100644 index 000000000..deaa71b3c --- /dev/null +++ b/frontend/src/lib/components/MenuHeading/index.ts @@ -0,0 +1,2 @@ +export { MenuHeading } from "./menuHeading"; +export type { MenuHeadingProps } from "./menuHeading"; diff --git a/frontend/src/lib/components/MenuHeading/menuHeading.tsx b/frontend/src/lib/components/MenuHeading/menuHeading.tsx new file mode 100644 index 000000000..aa9eb94e4 --- /dev/null +++ b/frontend/src/lib/components/MenuHeading/menuHeading.tsx @@ -0,0 +1,9 @@ +export type MenuHeadingProps = { + children: React.ReactNode; +}; + +export function MenuHeading(props: MenuHeadingProps): React.ReactNode { + return ( +
{props.children}
+ ); +} diff --git a/frontend/src/lib/components/SortableList/sortableList.tsx b/frontend/src/lib/components/SortableList/sortableList.tsx index f06ad63bc..9ddd0e8c0 100644 --- a/frontend/src/lib/components/SortableList/sortableList.tsx +++ b/frontend/src/lib/components/SortableList/sortableList.tsx @@ -3,7 +3,7 @@ import React from "react"; import { createPortal } from "@lib/utils/createPortal"; import { MANHATTAN_LENGTH, rectContainsPoint } from "@lib/utils/geometry"; import { resolveClassNames } from "@lib/utils/resolveClassNames"; -import { Vec2, point2Distance } from "@lib/utils/vec2"; +import { Vec2, point2Distance, vec2FromPointerEvent } from "@lib/utils/vec2"; import { isEqual } from "lodash"; @@ -146,6 +146,7 @@ export function SortableList(props: SortableListProps): React.ReactNode { React.ReactElement[] >(props.children); + const mainDivRef = React.useRef(null); const listDivRef = React.useRef(null); const scrollDivRef = React.useRef(null); const upperScrollDivRef = React.useRef(null); @@ -178,6 +179,8 @@ export function SortableList(props: SortableListProps): React.ReactNode { let currentScrollTime = 100; function handlePointerDown(e: PointerEvent) { + e.preventDefault(); + e.stopPropagation(); const target = e.target; if (!target) { return; @@ -280,24 +283,32 @@ export function SortableList(props: SortableListProps): React.ReactNode { return items; } - function getHoveredElement(e: PointerEvent): HTMLElement | null { - const items = getDragElementsRecursively(); - for (const item of items) { - if (rectContainsPoint(item.getBoundingClientRect(), { x: e.clientX, y: e.clientY })) { - const type = getItemType(item); + function getHoveredElementAndArea(e: PointerEvent): { element: HTMLElement; area: HoveredArea } | null { + const elements = getDragElementsRecursively(); + for (const element of elements) { + if (rectContainsPoint(element.getBoundingClientRect(), vec2FromPointerEvent(e))) { + const type = getItemType(element); if (type === ItemType.GROUP) { - const content = item.querySelector(".sortable-list-group-content"); + const content = element.querySelector(".sortable-list-group-content"); if ( content && - rectContainsPoint(content.getBoundingClientRect(), { x: e.clientX, y: e.clientY }) + rectContainsPoint(content.getBoundingClientRect(), vec2FromPointerEvent(e)) && + content.getElementsByClassName("sortable-list-item").length > 0 ) { continue; } } - return item; + return { element, area: getHoveredAreaOfItem(element, e) }; } } + const directChildren = elements.filter((el) => el.parentElement === currentListDivRef); + if ( + mainDivRef.current && + rectContainsPoint(mainDivRef.current.getBoundingClientRect(), vec2FromPointerEvent(e)) + ) { + return { element: directChildren[directChildren.length - 1], area: HoveredArea.BOTTOM }; + } return null; } @@ -332,13 +343,11 @@ export function SortableList(props: SortableListProps): React.ReactNode { } const headerElement = item.querySelector(".sortable-list-item-header"); - if (!headerElement) { - return HoveredArea.CENTER; - } - - const headerRect = headerElement.getBoundingClientRect(); - if (rectContainsPoint(headerRect, { x: e.clientX, y: e.clientY })) { - return HoveredArea.HEADER; + if (headerElement) { + const headerRect = headerElement.getBoundingClientRect(); + if (rectContainsPoint(headerRect, { x: e.clientX, y: e.clientY })) { + return HoveredArea.HEADER; + } } return HoveredArea.CENTER; @@ -382,6 +391,9 @@ export function SortableList(props: SortableListProps): React.ReactNode { } function handlePointerMove(e: PointerEvent) { + e.preventDefault(); + e.stopPropagation(); + if (!pointerDownPosition || !draggedElement) { return; } @@ -413,9 +425,9 @@ export function SortableList(props: SortableListProps): React.ReactNode { return; } - const hoveredElement = getHoveredElement(e); - if (hoveredElement && hoveredElement instanceof HTMLElement) { - const area = getHoveredAreaOfItem(hoveredElement, e); + const hoveredElementAndArea = getHoveredElementAndArea(e); + if (hoveredElementAndArea) { + const { element: hoveredElement, area } = hoveredElementAndArea; const itemType = getItemType(hoveredElement); if (itemType === ItemType.ITEM && (area === HoveredArea.CENTER || area === HoveredArea.HEADER)) { currentlyHoveredElement = null; @@ -427,8 +439,17 @@ export function SortableList(props: SortableListProps): React.ReactNode { const parentType = parentElement ? getItemType(parentElement) : null; let destinationType = parentType; - if (itemType === ItemType.GROUP && area === HoveredArea.HEADER) { - destinationType = ItemType.GROUP; + let destinationId = getItemParentGroupId(hoveredElement); + + if (itemType === ItemType.GROUP) { + if (area === HoveredArea.HEADER) { + destinationType = ItemType.GROUP; + destinationId = hoveredElement.dataset.itemId ?? ""; + } + if (area === HoveredArea.CENTER) { + destinationType = ItemType.GROUP; + destinationId = hoveredElement.dataset.itemId ?? ""; + } } if ( @@ -438,8 +459,8 @@ export function SortableList(props: SortableListProps): React.ReactNode { movedItemType: draggedElement.type, originId: draggedElement.parentId, originType: draggedElement.parentType, - destinationId: getItemParentGroupId(hoveredElement), - destinationType: destinationType, + destinationId, + destinationType, }) ) { currentlyHoveredElement = null; @@ -452,8 +473,8 @@ export function SortableList(props: SortableListProps): React.ReactNode { id: hoveredElement.dataset.itemId ?? "", type: itemType, area, - parentId: getItemParentGroupId(hoveredElement), - parentType: parentType, + parentId: destinationId, + parentType: destinationType, }; } else { currentlyHoveredElement = null; @@ -470,11 +491,9 @@ export function SortableList(props: SortableListProps): React.ReactNode { return; } - if (currentlyHoveredElement.area === HoveredArea.CENTER) { - return; - } - if (isMoveAllowed !== undefined) { + const parentElement = getItemParent(currentlyHoveredElement.element); + const parentType = parentElement ? getItemType(parentElement) : null; if ( !isMoveAllowed({ movedItemId: draggedElement.id, @@ -482,14 +501,17 @@ export function SortableList(props: SortableListProps): React.ReactNode { originId: getItemParentGroupId(draggedElement.element), originType: getItemType(draggedElement.element), destinationId: getItemParentGroupId(currentlyHoveredElement.element), - destinationType: currentlyHoveredElement.type, + destinationType: parentType, }) ) { return; } } - if (currentlyHoveredElement.area === HoveredArea.HEADER) { + if ( + currentlyHoveredElement.area === HoveredArea.HEADER || + currentlyHoveredElement.area === HoveredArea.CENTER + ) { const originId = getItemParentGroupId(draggedElement.element); const destinationId = currentlyHoveredElement.id; const position = 0; @@ -518,6 +540,7 @@ export function SortableList(props: SortableListProps): React.ReactNode { setIsDragging(false); setDraggedItemId(null); setHoveredItemIdAndArea(null); + doScroll = false; document.removeEventListener("pointermove", handlePointerMove); document.removeEventListener("pointerup", handlePointerUp); @@ -578,7 +601,7 @@ export function SortableList(props: SortableListProps): React.ReactNode { } return ( -
+
{makeChildren()} +
+
+
{isDragging && diff --git a/frontend/src/lib/components/SortableList/sortableListGroup.tsx b/frontend/src/lib/components/SortableList/sortableListGroup.tsx index 7deaa0ec8..127d83d41 100644 --- a/frontend/src/lib/components/SortableList/sortableListGroup.tsx +++ b/frontend/src/lib/components/SortableList/sortableListGroup.tsx @@ -39,7 +39,10 @@ export function SortableListGroup(props: SortableListGroupProps): React.ReactNod const sortableListContext = React.useContext(SortableListContext); const isHovered = sortableListContext.hoveredElementId === props.id; - const isHeaderHovered = isHovered && sortableListContext.hoveredArea === HoveredArea.HEADER; + const isHeaderHovered = + isHovered && + (sortableListContext.hoveredArea === HoveredArea.HEADER || + sortableListContext.hoveredArea === HoveredArea.CENTER); const isDragging = sortableListContext.draggedElementId === props.id; const dragPosition = sortableListContext.dragPosition; @@ -47,14 +50,13 @@ export function SortableListGroup(props: SortableListGroupProps): React.ReactNod setIsExpanded(!isExpanded); } + const hasContent = props.children !== undefined && props.children.length > 0; + return ( <> {isHovered && sortableListContext.hoveredArea === HoveredArea.TOP && }
@@ -63,13 +65,19 @@ export function SortableListGroup(props: SortableListGroupProps): React.ReactNod hidden: !isDragging, })} >
-
+
{isDragging && dragPosition && createPortal(
-
+
)}
- {props.children === undefined || props.children.length === 0 - ? props.contentWhenEmpty - : props.children} + {hasContent ? props.children : props.contentWhenEmpty}
{isHovered && sortableListContext.hoveredArea === HoveredArea.BOTTOM && } @@ -101,6 +112,8 @@ export function SortableListGroup(props: SortableListGroupProps): React.ReactNod type HeaderProps = { title: React.ReactNode; expanded: boolean; + expandable: boolean; + hovered: boolean; onToggleExpanded?: () => void; icon?: React.ReactNode; startAdornment?: React.ReactNode; @@ -109,17 +122,27 @@ type HeaderProps = { function Header(props: HeaderProps): React.ReactNode { return ( -
+
-
- {props.expanded ? : } -
+ {props.expandable && ( +
+ {props.expanded ? : } +
+ )}
{props.startAdornment}
{props.title}
diff --git a/frontend/src/lib/components/SortableList/sortableListItem.tsx b/frontend/src/lib/components/SortableList/sortableListItem.tsx index 3b06e8de9..c9429b9ea 100644 --- a/frontend/src/lib/components/SortableList/sortableListItem.tsx +++ b/frontend/src/lib/components/SortableList/sortableListItem.tsx @@ -3,7 +3,7 @@ import React from "react"; import { useElementBoundingRect } from "@lib/hooks/useElementBoundingRect"; import { createPortal } from "@lib/utils/createPortal"; import { resolveClassNames } from "@lib/utils/resolveClassNames"; -import { DragIndicator, ExpandLess, ExpandMore } from "@mui/icons-material"; +import { DragIndicator } from "@mui/icons-material"; import { HoveredArea, SortableListContext } from "./sortableList"; import { SortableListDropIndicator } from "./sortableListDropIndicator"; @@ -93,7 +93,7 @@ type HeaderProps = { function Header(props: HeaderProps): React.ReactNode { return ( -
+
@@ -102,13 +102,6 @@ function Header(props: HeaderProps): React.ReactNode {
{props.title}
{props.endAdornment}
-
- {props.expanded ? : } -
); }