Unable to use node ports when hover adornment is active

So I have implemented the node structure where the node has a bunch of elements in it, like Shape, Textblock, picture and buttons etc. The node also has incoming and outgoing ports on it which are linkable to other nodes.
The nodes also shows buttons on mouse hover. I followed the hoverButton example at (Buttons that show on Hover)

The problem is, when i hover over the node it shows the buttons and they are working fine, but the adornment placeholder covers the whole area over the node including ports, which i cannot use anymore.

The code for my node and adornment is as follows.

Node

this.$(
			go.Node,
			'Spot',
			{
				isShadowed: true,
				shadowColor: '#0000001A',
				shadowOffset: new go.Point(0, 5),
				shadowBlur: 15,
				selectionAdorned: false,
			},
			{
				click: (e: go.InputEvent, activeNode: go.Node) => {
					if (
						e.targetObject instanceof go.Shape ||
						e.targetObject instanceof go.TextBlock ||
						e.targetObject instanceof go.Picture
					) {
						nodeClickCallback(e, activeNode);
					}
				},
				mouseHover: (e, activeNode: go.Node) => {
					self.onNodeMouseHover(e, activeNode);
					nodeHoverAdornment.adornedObject = activeNode;
					activeNode.addAdornment('mouseHover', nodeHoverAdornment);
				},
				mouseLeave: (e, activeNode: go.Node) => {
					self.onNodeMouseLeave(e, activeNode);
				},
			},
			new go.Binding('location', 'loc').makeTwoWay(),
			this.$(
				go.Shape,
				'RoundedRectangle',
				{
					width: 160,
					height: 120,
					fill: 'white',
					stroke: null,
					strokeWidth: 3,
					spot1: go.Spot.TopLeft,
					spot2: go.Spot.BottomRight,
				},
				new go.Binding('stroke', 'isHighlighted', (h) => {
					return h ? self.highlightColor : 'white';
				}).ofObject()
			),
			this.$(
				go.Panel,
				'Auto',
				this.$(
					go.Panel,
					'Table',
					{
						defaultAlignment: go.Spot.Left,
					},
					this.$(go.RowColumnDefinition, { column: 0, width: 55 }),
					this.$(go.RowColumnDefinition, { column: 1, width: 80 }),
					this.$(go.RowColumnDefinition, { column: 2, width: 25 }),
					this.$(go.RowColumnDefinition, { row: 0, height: 50 }),
					this.$(go.RowColumnDefinition, { row: 1, height: 70 }),

					// Node Icon - Row 0
					this.$(
						go.Panel,
						'Auto',
						{ row: 0, column: 0, alignment: go.Spot.Left },
						this.$(
							go.Shape,
							'RoundedRectangle',
							{
								width: 42,
								height: 42,
								stroke: null,
								strokeWidth: 0,
								margin: 5,
							},
							new go.Binding(
								'fill',
								'transformationStep',
								(step: TransformationStepModel) =>
									TransformationTypeConstant.findById(
										step.transformationType
									).color
							)
						),
						this.$(
							go.Picture,
							{
								desiredSize: new go.Size(20, 20),
							},
							new go.Binding(
								'source',
								'transformationStep',
								(step: TransformationStepModel) =>
									TransformationTypeConstant.findById(
										step.transformationType
									).icon
							)
						)
					),

					// Node ID - Row 0
					this.$(
						go.Panel,
						'Horizontal',
						{
							row: 0,
							column: 1,
						},
						this.$(
							go.Picture,
							{
								desiredSize: new go.Size(18, 18),
								margin: new go.Margin(0, 5, 0, 0),
							},
							new go.Binding(
								'visible',
								'transformationStep',
								(step: TransformationStepModel) =>
									step.transformationType ===
									TransformationTypesEnum.INPUT
							),
							new go.Binding(
								'source',
								'transformationStep',
								(step: TransformationStepModel) =>
									this.getInputNodeIcon(step)
							)
						),
						this.$(
							go.TextBlock,
							{
								stroke: 'black',
								font: 'normal 10pt sans-serif',
							},
							new go.Binding(
								'text',
								'transformationStep',
								(step: TransformationStepModel) =>
									TransformationTypeConstant.findById(
										step.transformationType
									).title
							)
						)
					),

					// Node Description - Row 1
					this.$(
						go.TextBlock,
						{
							row: 1,
							column: 0,
							columnSpan: 3,
							alignment: go.Spot.LeftCenter,
							margin: new go.Margin(0, 10, 10, 10),
							stroke: '#707070',
							font: 'normal 9pt sans-serif',
							text: '...',
							overflow: go.TextBlock.OverflowEllipsis,
						},
						new go.Binding(
							'stroke',
							'transformationStep',
							(step: TransformationStepModel) => {
								return step.hasError ? '#ff4d4d' : '#707070';
							}
						),
						new go.Binding(
							'text',
							'transformationStep',
							(step: TransformationStepModel) =>
								NodeDescriptionHelper.showNodeDescription(step)
						)
					)
				)
			),
			this.$(
				go.Panel,
				'Vertical',
				{
					alignment: go.Spot.Left,
					alignmentFocus: new go.Spot(0, 0.5, 20, 0),
				},
				inports
			),
			this.$(
				go.Panel,
				'Vertical',
				{
					alignment: go.Spot.Right,
					alignmentFocus: new go.Spot(1, 0.5, -20, 0),
				},
				outports
			),
			this.$(
				go.Panel,
				'Horizontal',
				{
					alignment: go.Spot.Top,
					alignmentFocus: new go.Spot(0, 1, 75, 0),
				},
				// Error Icon
				this.$(
					go.Shape,
					{
						width: 15,
						height: 15,
						fill: '#ff4d4d',
						strokeWidth: 0,
						geometry: go.Geometry.parse(
							this.svgIcons['warning'],
							true
						),
					},
					new go.Binding(
						'visible',
						'transformationStep',
						(step: TransformationStepModel) => step.hasError
					)
				)
			)
		);

Adornment

const nodeHoverAdornment = this.$(
			go.Adornment,
			'Spot',
			{
				background: 'lightgrey',
				click: function (e, obj) {
					console.log('adornment click');
					const activeNode = obj.part.adornedPart;
					nodeClickCallback(e, activeNode);
				},
				mouseHover: (e, obj) => {
					console.log('adornment mouseHover');
					const activeNode = obj.part.adornedPart;
					self.onNodeMouseHover(e, activeNode);
				},
				mouseOver: (e, obj) => {
					console.log('adornment mouseOver');
					const activeNode = obj.part.adornedPart;
					self.onNodeMouseHover(e, activeNode);
				},
				mouseLeave: function (e, obj) {
					console.log('adornment mouseLeave');
					const ad = obj.part;
					// hide the Adornment when the mouse leaves it
					ad.adornedPart.removeAdornment('mouseHover');
					self.onNodeMouseLeave(e, obj.part.adornedPart);
				},
			},
			this.$(go.Placeholder, {
				background: 'transparent',
				isActionable: true,
			}),
			this.$(
				'Button',
				{
					alignment: go.Spot.Top,
					alignmentFocus: new go.Spot(0, 1, -35, 0),
					'ButtonBorder.stroke': null,
					_buttonStrokeOver: null,
					'ButtonBorder.fill': 'transparent',
					click: function (e, obj: go.Node) {
						const activeNode = obj.part;
						// functionality for this button
					},
				},
				this.$(go.Shape, {
					width: 15,
					height: 15,
					fill: '#bbbbbb',
					strokeWidth: 0,
					geometry: go.Geometry.parse(this.svgIcons['cog'], true),
				})
			),
			this.$(
				'Button',
				{
					alignment: go.Spot.Top,
					alignmentFocus: new go.Spot(0, 1, -60, 0),
					'ButtonBorder.stroke': null,
					_buttonStrokeOver: null,
					'ButtonBorder.fill': 'transparent',
					click: function (e, obj: go.Node) {
						// Delete Node
						const activeNode = obj.part;
						console.log('delete node', activeNode);
						nodeDeleteCallback(activeNode);
					},
				},
				this.$(go.Shape, {
					width: 15,
					height: 15,
					fill: '#bbbbbb',
					strokeWidth: 0,
					geometry: go.Geometry.parse(this.svgIcons['remove'], true),
				})
			)
		);

How can i manage the adornment placeholder size so that i does not cover the ports and they are still working?

Well, having the Adornment.background be “lightgray” is obviously a problem.

You could change that to “transparent” so that the user could see the node, but the user could still not interact with the node while the Adornment is visible, because mouse events would be handled by the Adornment.

You could change that to null (or just not set that property), and then mouse events will go through to the node, but for exactly that reason the mouse will have left the Adornment, so the mouseLeave event handler will be invoked on the adornment. Perhaps you should make the mouseLeave event handler smarter by checking whether the third argument (nextObj at GraphObject | GoJS API) warrants removing the Adornment.

The problem i am having right now is, the mouseLeave on adornment never gets executed unless i hover over the buttons inside the adornment.
And if i just hover over the node and leave it, then the adornment buttons stay there unless i hover over the buttons and leave from there.
captured

My code looks something like this now
Adornment

const nodeHoverAdornment = this.$(
			go.Adornment,
			'Spot',
			{
				mouseLeave: function (e, obj, nextObj) {
					console.log('adornment nextObj', nextObj);
					const ad = obj.part;
					ad.adornedPart.removeAdornment('mouseHover');
					self.onNodeMouseLeave(e, obj.part.adornedPart);
				},
			},
			this.$(go.Placeholder, {
				isActionable: true,
			}),
			this.$(
				'Button',
				{
					alignment: go.Spot.Top,
					alignmentFocus: new go.Spot(0, 1, -35, 0),
					'ButtonBorder.stroke': null,
					_buttonStrokeOver: null,
					'ButtonBorder.fill': 'transparent',
					click: function (e, obj: go.Node) {
						const activeNode = obj.part;
						nodeCloneCallback(activeNode);
					},
				},
				this.$(go.Shape, {
					width: 15,
					height: 15,
					fill: '#bbbbbb',
					strokeWidth: 0,
					geometry: go.Geometry.parse(this.svgIcons['file'], true),
				})
			),
			this.$(
				'Button',
				{
					alignment: go.Spot.Top,
					alignmentFocus: new go.Spot(0, 1, -60, 0),
					'ButtonBorder.stroke': null,
					_buttonStrokeOver: null,
					'ButtonBorder.fill': 'transparent',
					click: function (e, obj: go.Node) {
						// Delete Node
						const activeNode = obj.part;
						nodeDeleteCallback(activeNode);
					},
				},
				this.$(go.Shape, {
					width: 15,
					height: 15,
					fill: '#bbbbbb',
					strokeWidth: 0,
					geometry: go.Geometry.parse(this.svgIcons['remove'], true),
				})
			)
		);

I never get the chance to check for third arguement nextObj.

That’s because the mouse hasn’t entered the Adornment, because nothing of the Adornment is there, because the backgrounds and Shape.fill are all null, so it can’t have had the opportunity to leave it.

So is there any solution that can solve this problem? The only thing i can think of is, if I can somehow resize the placeholder size, so that it does not cover the ports anymore. Is there any way I can resize the placeholder?

Why doesn’t your Node.mouseLeave handler remove the adornment?

Yeah that’s how I have solved the problem. But it still looks like a hack to me. Because I had to add Timeout to go from node hover to adornment hover. My code looks like this now.

Node Hover

mouseHover: (e, activeNode: go.Node) => {
					// console.log('nodeHover');
					self.onNodeMouseHover(e, activeNode);
					nodeHoverAdornment.adornedObject = activeNode;
					activeNode.addAdornment('mouseHover', nodeHoverAdornment);
				},
				mouseLeave: (e, activeNode: go.Node, nextObj) => {
					// console.log('nextObj', nextObj);
					self.onNodeMouseLeave(e, activeNode);
					setTimeout(() => {
						activeNode.removeAdornment('mouseHover');
					}, 200);
				},

And Adornment Hover

mouseOver: (e, obj) => {
					console.log('adornment mouseOver');
					const activeNode = obj.part.adornedPart;
					setTimeout(() => {
						nodeHoverAdornment.adornedObject = activeNode;
						activeNode.addAdornment('mouseHover', nodeHoverAdornment);
					}, 201);
				},
				mouseLeave: function (e, obj, nextObj) {
					// console.log('adornment nextObj', nextObj);
					const ad = obj.part;
					ad.adornedPart.removeAdornment('mouseHover');
					self.onNodeMouseLeave(e, obj.part.adornedPart);
				},

It is working almost the same as expected. But I have noticed one other anomaly. After a link is drawn, the adornment is somehow not working now.

captured (1)

Any idea what could have gone wrong here?

Sorry, I have no idea.