Skip to content

Commit

Permalink
Adjustments and new menu utility components
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenthoms committed Aug 19, 2024
1 parent d246368 commit c8d53c5
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 62 deletions.
1 change: 1 addition & 0 deletions frontend/src/lib/components/MenuDivider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { MenuDivider } from "./menuDivider";
3 changes: 3 additions & 0 deletions frontend/src/lib/components/MenuDivider/menuDivider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function MenuDivider(): React.ReactNode {
return <div className="border-t border-gray-200 my-1" />;
}
2 changes: 2 additions & 0 deletions frontend/src/lib/components/MenuHeading/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { MenuHeading } from "./menuHeading";
export type { MenuHeadingProps } from "./menuHeading";
9 changes: 9 additions & 0 deletions frontend/src/lib/components/MenuHeading/menuHeading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type MenuHeadingProps = {
children: React.ReactNode;
};

export function MenuHeading(props: MenuHeadingProps): React.ReactNode {
return (
<div className="text-xs text-gray-500 uppercase font-semibold tracking-wider px-3 py-1">{props.children}</div>
);
}
90 changes: 58 additions & 32 deletions frontend/src/lib/components/SortableList/sortableList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -146,6 +146,7 @@ export function SortableList(props: SortableListProps): React.ReactNode {
React.ReactElement<SortableListItemProps | SortableListGroupProps>[]
>(props.children);

const mainDivRef = React.useRef<HTMLDivElement>(null);
const listDivRef = React.useRef<HTMLDivElement>(null);
const scrollDivRef = React.useRef<HTMLDivElement>(null);
const upperScrollDivRef = React.useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -382,6 +391,9 @@ export function SortableList(props: SortableListProps): React.ReactNode {
}

function handlePointerMove(e: PointerEvent) {
e.preventDefault();
e.stopPropagation();

if (!pointerDownPosition || !draggedElement) {
return;
}
Expand Down Expand Up @@ -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;
Expand All @@ -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 (
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -470,26 +491,27 @@ 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,
movedItemType: draggedElement.type,
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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -578,7 +601,7 @@ export function SortableList(props: SortableListProps): React.ReactNode {
}

return (
<div className="w-full h-full flex flex-col relative">
<div className="w-full h-full flex flex-col relative" ref={mainDivRef}>
<SortableListContext.Provider
value={{
draggedElementId: draggedItemId,
Expand All @@ -602,6 +625,9 @@ export function SortableList(props: SortableListProps): React.ReactNode {
>
<div className="flex flex-col border border-slate-100 relative max-h-0" ref={listDivRef}>
{makeChildren()}
<div className="h-2 min-h-2">
<div className="h-2" />
</div>
</div>
</div>
{isDragging &&
Expand Down
65 changes: 44 additions & 21 deletions frontend/src/lib/components/SortableList/sortableListGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,24 @@ 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;

function handleToggleExpanded() {
setIsExpanded(!isExpanded);
}

const hasContent = props.children !== undefined && props.children.length > 0;

return (
<>
{isHovered && sortableListContext.hoveredArea === HoveredArea.TOP && <SortableListDropIndicator />}
<div
className={resolveClassNames("sortable-list-element sortable-list-group relative", {
"bg-blue-200": isHeaderHovered,
"bg-gray-200": !isHeaderHovered,
})}
className={resolveClassNames("sortable-list-element sortable-list-group relative bg-gray-200")}
data-item-id={props.id}
ref={divRef}
>
Expand All @@ -63,34 +65,43 @@ export function SortableListGroup(props: SortableListGroupProps): React.ReactNod
hidden: !isDragging,
})}
></div>
<Header onToggleExpanded={handleToggleExpanded} expanded={isExpanded} {...props} />
<Header
onToggleExpanded={handleToggleExpanded}
expanded={isExpanded}
expandable={hasContent}
hovered={isHeaderHovered}
{...props}
/>
{isDragging &&
dragPosition &&
createPortal(
<div
className={resolveClassNames(
"flex h-8 px-1 bg-blue-50 text-sm items-center gap-1 border-b border-b-gray-300 absolute z-50 opacity-75"
"flex h-8 bg-blue-50 text-sm items-center gap-1 border-b border-b-gray-300 absolute z-50 opacity-75"
)}
style={{
left: dragPosition.x,
top: dragPosition.y,
width: isDragging ? boundingClientRect.width : undefined,
}}
>
<Header expanded={isExpanded} {...props} />
<Header
expanded={isExpanded}
expandable={hasContent}
hovered={isHeaderHovered}
{...props}
/>
</div>
)}
<div
className={resolveClassNames(
"sortable-list-group-content pl-2 bg-white mb-1 shadow-inner border-b border-b-gray-300",
"sortable-list-group-content ml-1 bg-white shadow-inner border-b border-b-gray-300",
{
"overflow-hidden h-[0px]": !isExpanded,
hidden: !isExpanded,
}
)}
>
{props.children === undefined || props.children.length === 0
? props.contentWhenEmpty
: props.children}
{hasContent ? props.children : props.contentWhenEmpty}
</div>
</div>
{isHovered && sortableListContext.hoveredArea === HoveredArea.BOTTOM && <SortableListDropIndicator />}
Expand All @@ -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;
Expand All @@ -109,17 +122,27 @@ type HeaderProps = {

function Header(props: HeaderProps): React.ReactNode {
return (
<div className="sortable-list-item-header flex items-center gap-1 h-8 text-sm border-b border-b-gray-300 px-2">
<div
className={resolveClassNames(
"sortable-list-item-header flex w-full items-center gap-1 h-8 text-sm border-b border-b-gray-400 px-2",
{
"bg-blue-300": props.hovered,
"bg-slate-300": !props.hovered,
}
)}
>
<div className={resolveClassNames("sortable-list-element-indicator hover:cursor-grab")}>
<DragIndicator fontSize="inherit" className="pointer-events-none" />
</div>
<div
className="hover:cursor-pointer hover:text-blue-800 p-0.5 rounded"
onClick={props.onToggleExpanded}
title={props.expanded ? "Hide children" : "Show children"}
>
{props.expanded ? <ExpandLess fontSize="inherit" /> : <ExpandMore fontSize="inherit" />}
</div>
{props.expandable && (
<div
className="hover:cursor-pointer hover:text-blue-800 p-0.5 rounded"
onClick={props.onToggleExpanded}
title={props.expanded ? "Hide children" : "Show children"}
>
{props.expanded ? <ExpandLess fontSize="inherit" /> : <ExpandMore fontSize="inherit" />}
</div>
)}
<div className="flex items-center gap-2 flex-grow">
{props.startAdornment}
<div className="flex-grow font-bold">{props.title}</div>
Expand Down
11 changes: 2 additions & 9 deletions frontend/src/lib/components/SortableList/sortableListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -93,7 +93,7 @@ type HeaderProps = {

function Header(props: HeaderProps): React.ReactNode {
return (
<div className="flex gap-1 h-8 bg-slate-100 hover:bg-blue-100 text-sm items-center border-b border-b-gray-300 px-2">
<div className="flex gap-1 h-8 bg-slate-100 text-sm items-center border-b border-b-gray-300 px-2">
<div className={resolveClassNames("sortable-list-element-indicator hover:cursor-grab")}>
<DragIndicator fontSize="inherit" className="pointer-events-none" />
</div>
Expand All @@ -102,13 +102,6 @@ function Header(props: HeaderProps): React.ReactNode {
<div className="flex-grow">{props.title}</div>
{props.endAdornment}
</div>
<div
className="hover:cursor-pointer hover:text-blue-800 p-0.5 rounded"
onClick={props.onToggleExpanded}
title={props.expanded ? "Hide children" : "Show children"}
>
{props.expanded ? <ExpandLess fontSize="inherit" /> : <ExpandMore fontSize="inherit" />}
</div>
</div>
);
}

0 comments on commit c8d53c5

Please sign in to comment.