Merge branch 'feat/add-node-tooltips' into 'demo'
feat: added node-tooltips to the editor See merge request ics/sp/2025/n25b/pepperplus-ui!39
This commit was merged in pull request #39.
This commit is contained in:
@@ -33,6 +33,12 @@
|
|||||||
|
|
||||||
/* Node Styles */
|
/* Node Styles */
|
||||||
|
|
||||||
|
:global(.react-flow__node.selected) {
|
||||||
|
outline: 1px dashed blue !important;
|
||||||
|
border-radius: 5pt;
|
||||||
|
outline-offset: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.default-node {
|
.default-node {
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
background-color: canvas;
|
background-color: canvas;
|
||||||
@@ -41,6 +47,8 @@
|
|||||||
filter: drop-shadow(0 0 0.75rem black);
|
filter: drop-shadow(0 0 0.75rem black);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.node-norm {
|
.node-norm {
|
||||||
outline: rgb(0, 149, 25) solid 2pt;
|
outline: rgb(0, 149, 25) solid 2pt;
|
||||||
filter: drop-shadow(0 0 0.25rem forestgreen);
|
filter: drop-shadow(0 0 0.25rem forestgreen);
|
||||||
@@ -76,10 +84,13 @@
|
|||||||
filter: drop-shadow(0 0 0.25rem plum);
|
filter: drop-shadow(0 0 0.25rem plum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.draggable-node {
|
.draggable-node {
|
||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
background-color: canvas;
|
background-color: canvas;
|
||||||
border-radius: 5pt;
|
border-radius: 5pt;
|
||||||
|
cursor: move;
|
||||||
outline: black solid 2pt;
|
outline: black solid 2pt;
|
||||||
filter: drop-shadow(0 0 0.75rem black);
|
filter: drop-shadow(0 0 0.75rem black);
|
||||||
}
|
}
|
||||||
@@ -88,6 +99,7 @@
|
|||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
background-color: canvas;
|
background-color: canvas;
|
||||||
border-radius: 5pt;
|
border-radius: 5pt;
|
||||||
|
cursor: move;
|
||||||
outline: forestgreen solid 2pt;
|
outline: forestgreen solid 2pt;
|
||||||
filter: drop-shadow(0 0 0.25rem forestgreen);
|
filter: drop-shadow(0 0 0.25rem forestgreen);
|
||||||
}
|
}
|
||||||
@@ -96,6 +108,7 @@
|
|||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
background-color: canvas;
|
background-color: canvas;
|
||||||
border-radius: 5pt;
|
border-radius: 5pt;
|
||||||
|
cursor: move;
|
||||||
outline: yellow solid 2pt;
|
outline: yellow solid 2pt;
|
||||||
filter: drop-shadow(0 0 0.25rem yellow);
|
filter: drop-shadow(0 0 0.25rem yellow);
|
||||||
}
|
}
|
||||||
@@ -104,6 +117,7 @@
|
|||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
background-color: canvas;
|
background-color: canvas;
|
||||||
border-radius: 5pt;
|
border-radius: 5pt;
|
||||||
|
cursor: move;
|
||||||
outline: teal solid 2pt;
|
outline: teal solid 2pt;
|
||||||
filter: drop-shadow(0 0 0.25rem teal);
|
filter: drop-shadow(0 0 0.25rem teal);
|
||||||
}
|
}
|
||||||
@@ -112,6 +126,7 @@
|
|||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
background-color: canvas;
|
background-color: canvas;
|
||||||
border-radius: 5pt;
|
border-radius: 5pt;
|
||||||
|
cursor: move;
|
||||||
outline: dodgerblue solid 2pt;
|
outline: dodgerblue solid 2pt;
|
||||||
filter: drop-shadow(0 0 0.25rem dodgerblue);
|
filter: drop-shadow(0 0 0.25rem dodgerblue);
|
||||||
}
|
}
|
||||||
@@ -120,6 +135,7 @@
|
|||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
background-color: canvas;
|
background-color: canvas;
|
||||||
border-radius: 5pt;
|
border-radius: 5pt;
|
||||||
|
cursor: move;
|
||||||
outline: orange solid 2pt;
|
outline: orange solid 2pt;
|
||||||
filter: drop-shadow(0 0 0.25rem orange);
|
filter: drop-shadow(0 0 0.25rem orange);
|
||||||
}
|
}
|
||||||
@@ -128,6 +144,7 @@
|
|||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
background-color: canvas;
|
background-color: canvas;
|
||||||
border-radius: 5pt;
|
border-radius: 5pt;
|
||||||
|
cursor: move;
|
||||||
outline: red solid 2pt;
|
outline: red solid 2pt;
|
||||||
filter: drop-shadow(0 0 0.25rem red);
|
filter: drop-shadow(0 0 0.25rem red);
|
||||||
}
|
}
|
||||||
@@ -136,6 +153,7 @@
|
|||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
background-color: canvas;
|
background-color: canvas;
|
||||||
border-radius: 5pt;
|
border-radius: 5pt;
|
||||||
|
cursor: move;
|
||||||
outline: plum solid 2pt;
|
outline: plum solid 2pt;
|
||||||
filter: drop-shadow(0 0 0.25rem plum);
|
filter: drop-shadow(0 0 0.25rem plum);
|
||||||
}
|
}
|
||||||
@@ -144,4 +162,53 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-toolbar-tooltip {
|
||||||
|
background-color: darkgray;
|
||||||
|
color: white;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: help;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tooltip {
|
||||||
|
pointer-events: none;
|
||||||
|
background-color: Canvas;
|
||||||
|
color: CanvasText;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
outline: CanvasText solid 2px;
|
||||||
|
font-size: 14px;
|
||||||
|
filter: drop-shadow(0 0 0.25rem CanvasText);
|
||||||
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
animation: fadeIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tooltip-header {
|
||||||
|
pointer-events: none;
|
||||||
|
background-color: CanvasText;
|
||||||
|
color: Canvas;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px 0 0 6px;
|
||||||
|
outline: CanvasText solid 2px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-variant-caps: small-caps;
|
||||||
|
filter: drop-shadow(0 0 0.25rem CanvasText);
|
||||||
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
animation: fadeIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(5px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
MarkerType,
|
MarkerType,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import {useEffect} from "react";
|
import {type CSSProperties, useEffect, useState} from "react";
|
||||||
import {useShallow} from 'zustand/react/shallow';
|
import {useShallow} from 'zustand/react/shallow';
|
||||||
import orderPhaseNodeArray from "../../utils/orderPhaseNodes.ts";
|
import orderPhaseNodeArray from "../../utils/orderPhaseNodes.ts";
|
||||||
import useProgramStore from "../../utils/programStore.ts";
|
import useProgramStore from "../../utils/programStore.ts";
|
||||||
@@ -79,7 +79,7 @@ const VisProgUI = () => {
|
|||||||
endBatchAction,
|
endBatchAction,
|
||||||
scrollable
|
scrollable
|
||||||
} = useFlowStore(useShallow(selector)); // instructs the editor to use the corresponding functions from the FlowStore
|
} = useFlowStore(useShallow(selector)); // instructs the editor to use the corresponding functions from the FlowStore
|
||||||
|
const [zoom, setZoom] = useState(1);
|
||||||
// adds ctrl+z and ctrl+y support to respectively undo and redo actions
|
// adds ctrl+z and ctrl+y support to respectively undo and redo actions
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
@@ -91,7 +91,7 @@ const VisProgUI = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.innerEditorContainer} round-lg border-lg`}>
|
<div className={`${styles.innerEditorContainer} round-lg border-lg`} style={({'--flow-zoom': zoom} as CSSProperties)}>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
@@ -107,6 +107,7 @@ const VisProgUI = () => {
|
|||||||
onNodeDragStart={beginBatchAction}
|
onNodeDragStart={beginBatchAction}
|
||||||
onNodeDragStop={endBatchAction}
|
onNodeDragStop={endBatchAction}
|
||||||
preventScrolling={scrollable}
|
preventScrolling={scrollable}
|
||||||
|
onMove={(_, viewport) => setZoom(viewport.zoom)}
|
||||||
snapToGrid
|
snapToGrid
|
||||||
fitView
|
fitView
|
||||||
proOptions={{hideAttribution: true}}
|
proOptions={{hideAttribution: true}}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import EndNode, {
|
|||||||
EndConnectionSource,
|
EndConnectionSource,
|
||||||
EndDisconnectionTarget,
|
EndDisconnectionTarget,
|
||||||
EndDisconnectionSource,
|
EndDisconnectionSource,
|
||||||
EndReduce
|
EndReduce,
|
||||||
|
EndTooltip
|
||||||
} from "./nodes/EndNode";
|
} from "./nodes/EndNode";
|
||||||
import { EndNodeDefaults } from "./nodes/EndNode.default";
|
import { EndNodeDefaults } from "./nodes/EndNode.default";
|
||||||
import StartNode, {
|
import StartNode, {
|
||||||
@@ -11,7 +12,8 @@ import StartNode, {
|
|||||||
StartConnectionSource,
|
StartConnectionSource,
|
||||||
StartDisconnectionTarget,
|
StartDisconnectionTarget,
|
||||||
StartDisconnectionSource,
|
StartDisconnectionSource,
|
||||||
StartReduce
|
StartReduce,
|
||||||
|
StartTooltip
|
||||||
} from "./nodes/StartNode";
|
} from "./nodes/StartNode";
|
||||||
import { StartNodeDefaults } from "./nodes/StartNode.default";
|
import { StartNodeDefaults } from "./nodes/StartNode.default";
|
||||||
import PhaseNode, {
|
import PhaseNode, {
|
||||||
@@ -19,7 +21,8 @@ import PhaseNode, {
|
|||||||
PhaseConnectionSource,
|
PhaseConnectionSource,
|
||||||
PhaseDisconnectionTarget,
|
PhaseDisconnectionTarget,
|
||||||
PhaseDisconnectionSource,
|
PhaseDisconnectionSource,
|
||||||
PhaseReduce
|
PhaseReduce,
|
||||||
|
PhaseTooltip
|
||||||
} from "./nodes/PhaseNode";
|
} from "./nodes/PhaseNode";
|
||||||
import { PhaseNodeDefaults } from "./nodes/PhaseNode.default";
|
import { PhaseNodeDefaults } from "./nodes/PhaseNode.default";
|
||||||
import NormNode, {
|
import NormNode, {
|
||||||
@@ -27,7 +30,8 @@ import NormNode, {
|
|||||||
NormConnectionSource,
|
NormConnectionSource,
|
||||||
NormDisconnectionTarget,
|
NormDisconnectionTarget,
|
||||||
NormDisconnectionSource,
|
NormDisconnectionSource,
|
||||||
NormReduce
|
NormReduce,
|
||||||
|
NormTooltip
|
||||||
} from "./nodes/NormNode";
|
} from "./nodes/NormNode";
|
||||||
import { NormNodeDefaults } from "./nodes/NormNode.default";
|
import { NormNodeDefaults } from "./nodes/NormNode.default";
|
||||||
import GoalNode, {
|
import GoalNode, {
|
||||||
@@ -35,7 +39,8 @@ import GoalNode, {
|
|||||||
GoalConnectionSource,
|
GoalConnectionSource,
|
||||||
GoalDisconnectionTarget,
|
GoalDisconnectionTarget,
|
||||||
GoalDisconnectionSource,
|
GoalDisconnectionSource,
|
||||||
GoalReduce
|
GoalReduce,
|
||||||
|
GoalTooltip
|
||||||
} from "./nodes/GoalNode";
|
} from "./nodes/GoalNode";
|
||||||
import { GoalNodeDefaults } from "./nodes/GoalNode.default";
|
import { GoalNodeDefaults } from "./nodes/GoalNode.default";
|
||||||
import TriggerNode, {
|
import TriggerNode, {
|
||||||
@@ -43,10 +48,18 @@ import TriggerNode, {
|
|||||||
TriggerConnectionSource,
|
TriggerConnectionSource,
|
||||||
TriggerDisconnectionTarget,
|
TriggerDisconnectionTarget,
|
||||||
TriggerDisconnectionSource,
|
TriggerDisconnectionSource,
|
||||||
TriggerReduce
|
TriggerReduce,
|
||||||
|
TriggerTooltip
|
||||||
} from "./nodes/TriggerNode";
|
} from "./nodes/TriggerNode";
|
||||||
import { TriggerNodeDefaults } from "./nodes/TriggerNode.default";
|
import { TriggerNodeDefaults } from "./nodes/TriggerNode.default";
|
||||||
import BasicBeliefNode, { BasicBeliefConnectionSource, BasicBeliefConnectionTarget, BasicBeliefDisconnectionSource, BasicBeliefDisconnectionTarget, BasicBeliefReduce } from "./nodes/BasicBeliefNode";
|
import BasicBeliefNode, {
|
||||||
|
BasicBeliefConnectionSource,
|
||||||
|
BasicBeliefConnectionTarget,
|
||||||
|
BasicBeliefDisconnectionSource,
|
||||||
|
BasicBeliefDisconnectionTarget,
|
||||||
|
BasicBeliefReduce,
|
||||||
|
BasicBeliefTooltip
|
||||||
|
} from "./nodes/BasicBeliefNode";
|
||||||
import { BasicBeliefNodeDefaults } from "./nodes/BasicBeliefNode.default";
|
import { BasicBeliefNodeDefaults } from "./nodes/BasicBeliefNode.default";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -173,4 +186,17 @@ export const NodesInPhase = {
|
|||||||
end: () => false,
|
end: () => false,
|
||||||
phase: () => false,
|
phase: () => false,
|
||||||
basic_belief: () => false,
|
basic_belief: () => false,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects the tooltips for all nodeTypes so they can be accessed by the tooltip component
|
||||||
|
*/
|
||||||
|
export const NodeTooltips = {
|
||||||
|
start: StartTooltip,
|
||||||
|
end: EndTooltip,
|
||||||
|
phase: PhaseTooltip,
|
||||||
|
norm: NormTooltip,
|
||||||
|
goal: GoalTooltip,
|
||||||
|
trigger: TriggerTooltip,
|
||||||
|
basic_belief: BasicBeliefTooltip,
|
||||||
}
|
}
|
||||||
@@ -45,9 +45,9 @@ function createNode(id: string, type: string, position: XYPosition, data: Record
|
|||||||
|
|
||||||
//* Initial nodes, created by using createNode. */
|
//* Initial nodes, created by using createNode. */
|
||||||
// Start and End don't need to apply the UUID, since they are technically never compiled into a program.
|
// Start and End don't need to apply the UUID, since they are technically never compiled into a program.
|
||||||
const startNode = createNode('start', 'start', {x: 100, y: 100}, {label: "Start"}, false)
|
const startNode = createNode('start', 'start', {x: 110, y: 100}, {label: "Start"}, false)
|
||||||
const endNode = createNode('end', 'end', {x: 500, y: 100}, {label: "End"}, false)
|
const endNode = createNode('end', 'end', {x: 590, y: 100}, {label: "End"}, false)
|
||||||
const initialPhaseNode = createNode(crypto.randomUUID(), 'phase', {x:200, y:100}, {label: "Phase 1", children : [], isFirstPhase: false, nextPhaseId: null})
|
const initialPhaseNode = createNode(crypto.randomUUID(), 'phase', {x:235, y:100}, {label: "Phase 1", children : [], isFirstPhase: false, nextPhaseId: null})
|
||||||
|
|
||||||
const initialNodes : Node[] = [startNode, endNode, initialPhaseNode,];
|
const initialNodes : Node[] = [startNode, endNode, initialPhaseNode,];
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { useReactFlow, type XYPosition } from '@xyflow/react';
|
|||||||
import { type ReactNode, useCallback, useRef, useState } from 'react';
|
import { type ReactNode, useCallback, useRef, useState } from 'react';
|
||||||
import useFlowStore from '../VisProgStores';
|
import useFlowStore from '../VisProgStores';
|
||||||
import styles from '../../VisProg.module.css';
|
import styles from '../../VisProg.module.css';
|
||||||
import { NodeDefaults, type NodeTypes } from '../NodeRegistry'
|
import { NodeDefaults, type NodeTypes} from '../NodeRegistry'
|
||||||
|
import {Tooltip} from "./NodeComponents.tsx";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props for a draggable node within the drag-and-drop toolbar.
|
* Props for a draggable node within the drag-and-drop toolbar.
|
||||||
@@ -47,14 +48,17 @@ function DraggableNode({ className, children, nodeType, onDrop }: DraggableNodeP
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}
|
<Tooltip nodeType={nodeType}>
|
||||||
ref={draggableRef}
|
<div>
|
||||||
id={`draggable-${nodeType}`}
|
<div className={className}
|
||||||
data-testid={`draggable-${nodeType}`}
|
ref={draggableRef}
|
||||||
>
|
id={`draggable-${nodeType}`}
|
||||||
{children}
|
data-testid={`draggable-${nodeType}`}
|
||||||
</div>
|
>
|
||||||
);
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -133,7 +137,7 @@ export function DndToolbar() {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex-col gap-lg padding-md ${styles.innerDndPanel}`}>
|
<div className={`flex-col gap-lg padding-md ${styles.innerDndPanel}`} id={"draggable-sidebar"}>
|
||||||
<div className="description">
|
<div className="description">
|
||||||
You can drag these nodes to the pane to create new nodes.
|
You can drag these nodes to the pane to create new nodes.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { NodeToolbar } from '@xyflow/react';
|
import {NodeToolbar} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
|
import {type JSX, useState} from "react";
|
||||||
|
import {createPortal} from "react-dom";
|
||||||
|
import styles from "../../VisProg.module.css";
|
||||||
|
import {NodeTooltips} from "../NodeRegistry.ts";
|
||||||
import useFlowStore from "../VisProgStores.tsx";
|
import useFlowStore from "../VisProgStores.tsx";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props for the Toolbar component.
|
* Props for the Toolbar component.
|
||||||
*
|
*
|
||||||
@@ -24,14 +29,94 @@ type ToolbarProps = {
|
|||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export function Toolbar({nodeId, allowDelete}: ToolbarProps) {
|
export function Toolbar({nodeId, allowDelete}: ToolbarProps) {
|
||||||
const {deleteNode} = useFlowStore();
|
const {nodes, deleteNode} = useFlowStore();
|
||||||
|
|
||||||
const deleteParentNode = ()=> {
|
|
||||||
|
const deleteParentNode = () => {
|
||||||
deleteNode(nodeId);
|
deleteNode(nodeId);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const nodeType = nodes.find((node) => node.id === nodeId)?.type as keyof typeof NodeTooltips;
|
||||||
return (
|
return (
|
||||||
<NodeToolbar>
|
<NodeToolbar className={"flex-row align-center"}>
|
||||||
<button className="Node-toolbar__deletebutton" onClick={deleteParentNode} disabled={!allowDelete}>delete</button>
|
<button className="Node-toolbar__deletebutton" onClick={deleteParentNode} disabled={!allowDelete}>delete</button>
|
||||||
|
<Tooltip nodeType={nodeType}>
|
||||||
|
<div className={styles.nodeToolbarTooltip}>i</div>
|
||||||
|
</Tooltip>
|
||||||
</NodeToolbar>);
|
</NodeToolbar>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type TooltipProps = {
|
||||||
|
nodeType?: keyof typeof NodeTooltips;
|
||||||
|
children: JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A general tooltip component, that can be used as a wrapper for any component
|
||||||
|
* that has a nodeType and a corresponding nodeTooltip.
|
||||||
|
*
|
||||||
|
* currently used to show tooltips for draggable-nodes and nodes inside the editor
|
||||||
|
*
|
||||||
|
* @param {"start" | "end" | "phase" | "norm" | "goal" | "trigger" | "basic_belief" | undefined} nodeType
|
||||||
|
* @param {React.JSX.Element} children
|
||||||
|
* @returns {React.JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export function Tooltip({ nodeType, children }: TooltipProps) {
|
||||||
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
|
const [disabled , setDisabled] = useState(false);
|
||||||
|
const [coords, setCoords] = useState({ top: 0, left: 0 });
|
||||||
|
|
||||||
|
const updateTooltipPos = () => {
|
||||||
|
const rect = document.getElementById("draggable-sidebar")!.getBoundingClientRect();
|
||||||
|
setCoords({
|
||||||
|
// Position exactly below the bottom edge of the draggable sidebar (plus a small gap)
|
||||||
|
top: rect.bottom + 10,
|
||||||
|
left: rect.left + rect.width / 2, // Keep it horizontally centered
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return nodeType ?
|
||||||
|
(<div>
|
||||||
|
<div
|
||||||
|
onMouseDown={() => {
|
||||||
|
updateTooltipPos();
|
||||||
|
setShowTooltip(false);
|
||||||
|
setDisabled(true);
|
||||||
|
}}
|
||||||
|
onMouseUp={() => {
|
||||||
|
setDisabled(false);
|
||||||
|
}}
|
||||||
|
onMouseOver={() => {
|
||||||
|
if (!disabled) {
|
||||||
|
updateTooltipPos();
|
||||||
|
setShowTooltip(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={ () => setShowTooltip(false)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
{showTooltip && createPortal(
|
||||||
|
<div
|
||||||
|
className={"flex-row"}
|
||||||
|
style={{
|
||||||
|
pointerEvents: 'none',
|
||||||
|
position: 'fixed',
|
||||||
|
top: `${coords.top}px`,
|
||||||
|
left: `${coords.left}px`,
|
||||||
|
transform: 'translateX(-50%)', // Center based on the midpoint
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className={styles.customTooltipHeader}>{nodeType}</span>
|
||||||
|
<span className={styles.customTooltip}>
|
||||||
|
{NodeTooltips[nodeType] || "Available for drag"}
|
||||||
|
</span>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : children
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,4 +31,13 @@
|
|||||||
filter: drop-shadow(0 0 0.25rem green);
|
filter: drop-shadow(0 0 0.25rem green);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.react-flow__handle) {
|
||||||
|
width: calc(8px / var(--flow-zoom, 1));
|
||||||
|
height: calc(8px / var(--flow-zoom, 1));
|
||||||
|
transition: width 0.1s ease, height 0.1s ease;
|
||||||
|
min-width: 8px;
|
||||||
|
min-height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ type Emotion = { type: "emotion", id: string, value: string, label: "Emotion rec
|
|||||||
|
|
||||||
export type BasicBeliefNode = Node<BasicBeliefNodeData>
|
export type BasicBeliefNode = Node<BasicBeliefNodeData>
|
||||||
|
|
||||||
|
// update the tooltip to reflect newly added connection options for a belief
|
||||||
|
export const BasicBeliefTooltip = `
|
||||||
|
A belief describes a condition that must be met
|
||||||
|
in order for a connected norm to be activated`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function is called whenever a connection is made with this node type as the target
|
* This function is called whenever a connection is made with this node type as the target
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ export function EndReduce(node: Node, _nodes: Node[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EndTooltip = `
|
||||||
|
The end node signifies the endpoint of your program;
|
||||||
|
the output of the final phase of your program should connect to the end node`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function is called whenever a connection is made with this node type as the target
|
* This function is called whenever a connection is made with this node type as the target
|
||||||
* @param _thisNode the node of this node type which function is called
|
* @param _thisNode the node of this node type which function is called
|
||||||
|
|||||||
@@ -136,6 +136,11 @@ export function GoalReduce(node: Node, _nodes: Node[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const GoalTooltip = `
|
||||||
|
The goal node allows you to set goals that Pepper has to achieve
|
||||||
|
before moving to the next phase of your program`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function is called whenever a connection is made with this node type as the target
|
* This function is called whenever a connection is made with this node type as the target
|
||||||
* @param _thisNode the node of this node type which function is called
|
* @param _thisNode the node of this node type which function is called
|
||||||
|
|||||||
@@ -114,6 +114,10 @@ export function NormReduce(node: Node, nodes: Node[]) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const NormTooltip = `
|
||||||
|
A norm describes a behavioral rule Pepper must follow during the connected phase(-s),
|
||||||
|
for example: "respond using formal language"`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function is called whenever a connection is made with this node type as the target
|
* This function is called whenever a connection is made with this node type as the target
|
||||||
* @param _thisNode the node of this node type which function is called
|
* @param _thisNode the node of this node type which function is called
|
||||||
|
|||||||
@@ -115,6 +115,10 @@ export function PhaseReduce(node: Node, nodes: Node[]) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PhaseTooltip = `
|
||||||
|
A phase is a single stage of the program, during a phase Pepper will behave
|
||||||
|
in accordance with any connected norms, goals and triggers`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function is called whenever a connection is made with this node type as the target (phase)
|
* This function is called whenever a connection is made with this node type as the target (phase)
|
||||||
* @param _thisNode the node of this node type which function is called
|
* @param _thisNode the node of this node type which function is called
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ export function StartReduce(node: Node, _nodes: Node[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const StartTooltip = `
|
||||||
|
The start node acts as the starting point for a program,
|
||||||
|
it should be connected to the left handle of the first phase of your program`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function is called whenever a connection is made with this node type as the target
|
* This function is called whenever a connection is made with this node type as the target
|
||||||
* @param _thisNode the node of this node type which function is called
|
* @param _thisNode the node of this node type which function is called
|
||||||
|
|||||||
@@ -101,6 +101,11 @@ export function TriggerReduce(node: Node, nodes: Node[]) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const TriggerTooltip = `
|
||||||
|
A trigger node is used to make Pepper execute a predefined plan -
|
||||||
|
consisting of one or more actions - when the connected beliefs are met`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function is called whenever a connection is made with this node type as the target
|
* This function is called whenever a connection is made with this node type as the target
|
||||||
* @param _thisNode the node of this node type which function is called
|
* @param _thisNode the node of this node type which function is called
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { fireEvent, screen } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import {Tooltip} from "../../../../../src/pages/VisProgPage/visualProgrammingUI/components/NodeComponents.tsx";
|
||||||
|
import {renderWithSidebar} from "../../../../test-utils/test-utils.tsx";
|
||||||
|
|
||||||
|
describe('Tooltip component test', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders and shows tooltip content on hover', () => {
|
||||||
|
renderWithSidebar(
|
||||||
|
<Tooltip nodeType="phase">
|
||||||
|
<div>?</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
const trigger = screen.getByText('?');
|
||||||
|
|
||||||
|
// initially hidden
|
||||||
|
expect(
|
||||||
|
screen.queryByText('Phase tooltip text')
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
// hover shows tooltip
|
||||||
|
fireEvent.mouseOver(trigger);
|
||||||
|
|
||||||
|
expect(screen.getByText('phase')).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText('A phase is a single stage of the program, during a phase Pepper will behave in accordance with any connected norms, goals and triggers')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
// rendered via portal
|
||||||
|
expect(
|
||||||
|
document.body.contains(
|
||||||
|
screen.getByText('A phase is a single stage of the program, during a phase Pepper will behave in accordance with any connected norms, goals and triggers')
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -19,6 +19,48 @@ export function renderWithProviders(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type SidebarRect = Partial<DOMRect>;
|
||||||
|
|
||||||
|
const defaultRect: DOMRect = {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 100,
|
||||||
|
right: 200,
|
||||||
|
width: 200,
|
||||||
|
height: 100,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
toJSON: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a component and injects a mock `#draggable-sidebar`
|
||||||
|
* element required by Tooltip positioning logic.
|
||||||
|
*/
|
||||||
|
export function renderWithSidebar(
|
||||||
|
ui: ReactElement,
|
||||||
|
rect: SidebarRect = {},
|
||||||
|
options?: RenderOptions
|
||||||
|
) {
|
||||||
|
const sidebar = document.createElement('div');
|
||||||
|
sidebar.id = 'draggable-sidebar';
|
||||||
|
|
||||||
|
sidebar.getBoundingClientRect = jest.fn(() => ({
|
||||||
|
...defaultRect,
|
||||||
|
...rect,
|
||||||
|
}));
|
||||||
|
|
||||||
|
document.body.appendChild(sidebar);
|
||||||
|
|
||||||
|
const result = render(ui, options);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
sidebar,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Re-export everything from testing library
|
// Re-export everything from testing library
|
||||||
//eslint-disable-next-line react-refresh/only-export-components
|
//eslint-disable-next-line react-refresh/only-export-components
|
||||||
export * from '@testing-library/react';
|
export * from '@testing-library/react';
|
||||||
|
|||||||
Reference in New Issue
Block a user