Skip to content

Commit e06d2c2

Browse files
fix(apollo-react): preserve status borders on hover
1 parent f2144f2 commit e06d2c2

4 files changed

Lines changed: 91 additions & 26 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { describe, expect, it } from 'vitest';
3+
import { BaseContainer } from './BaseNodeContainer';
4+
5+
describe('BaseContainer status border hover treatment', () => {
6+
it('preserves a resolved status border while hovered', () => {
7+
render(
8+
<BaseContainer isHovered executionStatus="Completed">
9+
<span>content</span>
10+
</BaseContainer>
11+
);
12+
13+
const container = screen.getByTestId('base-container');
14+
15+
expect(container).toHaveClass('border-success');
16+
expect(container).toHaveClass('shadow-(--canvas-node-shadow-hover)');
17+
expect(container).not.toHaveClass('border-border-hover');
18+
});
19+
20+
it('keeps the hover border for neutral statuses without a resolved status border', () => {
21+
render(
22+
<BaseContainer isHovered executionStatus="NotExecuted">
23+
<span>content</span>
24+
</BaseContainer>
25+
);
26+
27+
const container = screen.getByTestId('base-container');
28+
29+
expect(container).toHaveClass('border-border-hover');
30+
expect(container).toHaveClass('shadow-(--canvas-node-shadow-hover)');
31+
});
32+
});

packages/apollo-react/src/canvas/components/BaseNode/BaseNodeContainer.tsx

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -64,29 +64,31 @@ export const BaseContainer = ({
6464

6565
// cn() is needed here because border-color classes from getStatusBorder,
6666
// hover, and selection states intentionally override the base `border-border`.
67-
const className = useMemo(
68-
() =>
69-
cn(
70-
'relative flex items-center cursor-pointer bg-surface-overlay border border-border',
71-
'w-(--node-w) h-(--node-h) rounded-(--node-radius)',
72-
'shadow-(--canvas-node-shadow-rest) outline-offset-0 transition-[box-shadow,border-color] duration-150',
73-
shape === 'rectangle'
74-
? 'flex-row justify-start gap-3 p-(--node-gap)'
75-
: 'flex-col justify-center',
76-
hasFooter && 'flex-wrap',
77-
getStatusBorder(activeStatus),
78-
isHovered && 'shadow-(--canvas-node-shadow-hover) border-border-hover',
79-
isSelected && 'outline outline-2 outline-foreground-accent-muted',
80-
interactionState === 'disabled' && 'opacity-50 cursor-not-allowed',
81-
interactionState === 'drag' && 'cursor-grabbing shadow-(--canvas-node-shadow-lifted)',
82-
// Decorative stacked layer for drillable / collapsed nodes. Purely visual:
83-
// a ::before pseudo inherits the container's radius/size, sits behind at
84-
// -z-10, and is offset down so only a thin arc peeks out below the card.
85-
isStacked &&
86-
'before:content-[""] before:absolute before:inset-x-0 before:top-0 before:h-full before:rounded-[inherit] before:bg-surface-overlay before:border before:border-brand before:translate-y-[6px] before:-z-10 before:pointer-events-none'
87-
),
88-
[shape, hasFooter, activeStatus, isSelected, isHovered, isStacked, interactionState]
89-
);
67+
const className = useMemo(() => {
68+
const statusBorder = getStatusBorder(activeStatus);
69+
const hasStatusBorder = statusBorder.length > 0;
70+
71+
return cn(
72+
'relative flex items-center cursor-pointer bg-surface-overlay border border-border',
73+
'w-(--node-w) h-(--node-h) rounded-(--node-radius)',
74+
'shadow-(--canvas-node-shadow-rest) outline-offset-0 transition-[box-shadow,border-color] duration-150',
75+
shape === 'rectangle'
76+
? 'flex-row justify-start gap-3 p-(--node-gap)'
77+
: 'flex-col justify-center',
78+
hasFooter && 'flex-wrap',
79+
statusBorder,
80+
isHovered && 'shadow-(--canvas-node-shadow-hover)',
81+
isHovered && !hasStatusBorder && 'border-border-hover',
82+
isSelected && 'outline outline-2 outline-foreground-accent-muted',
83+
interactionState === 'disabled' && 'opacity-50 cursor-not-allowed',
84+
interactionState === 'drag' && 'cursor-grabbing shadow-(--canvas-node-shadow-lifted)',
85+
// Decorative stacked layer for drillable / collapsed nodes. Purely visual:
86+
// a ::before pseudo inherits the container's radius/size, sits behind at
87+
// -z-10, and is offset down so only a thin arc peeks out below the card.
88+
isStacked &&
89+
'before:content-[""] before:absolute before:inset-x-0 before:top-0 before:h-full before:rounded-[inherit] before:bg-surface-overlay before:border before:border-brand before:translate-y-[6px] before:-z-10 before:pointer-events-none'
90+
);
91+
}, [shape, hasFooter, activeStatus, isSelected, isHovered, isStacked, interactionState]);
9092

9193
return (
9294
<div

packages/apollo-react/src/canvas/components/LoopNode/LoopNode.test.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render, screen } from '@testing-library/react';
1+
import { fireEvent, render, screen } from '@testing-library/react';
22
import type { Node, NodeProps } from '@uipath/apollo-react/canvas/xyflow/react';
33
import { describe, expect, it, vi } from 'vitest';
44
import type { LoopNodeData } from './LoopNode.types';
@@ -79,6 +79,10 @@ function renderLoopNode(props: Partial<React.ComponentProps<typeof LoopNode>> =
7979
return screen.getByTestId('loop-node-header');
8080
}
8181

82+
function getLoopContainer() {
83+
return document.querySelector('[data-loop-container]') as HTMLElement;
84+
}
85+
8286
describe('LoopNode header adornment spacing', () => {
8387
it('keeps the default header padding when no adornments are visible', () => {
8488
const header = renderLoopNode();
@@ -121,3 +125,26 @@ describe('LoopNode header adornment spacing', () => {
121125
expect(header.style.paddingRight).toBe('34px');
122126
});
123127
});
128+
129+
describe('LoopNode status border hover treatment', () => {
130+
it('preserves a resolved status border while hovered', () => {
131+
renderLoopNode({ executionStatusOverride: 'Completed' });
132+
133+
const container = getLoopContainer();
134+
fireEvent.mouseEnter(container);
135+
136+
expect(container).toHaveClass('border-success');
137+
expect(container).toHaveClass('shadow-(--canvas-node-shadow-hover)');
138+
expect(container).not.toHaveClass('border-border-hover');
139+
});
140+
141+
it('keeps the hover border for neutral statuses without a resolved status border', () => {
142+
renderLoopNode({ executionStatusOverride: 'NotExecuted' });
143+
144+
const container = getLoopContainer();
145+
fireEvent.mouseEnter(container);
146+
147+
expect(container).toHaveClass('border-border-hover');
148+
expect(container).toHaveClass('shadow-(--canvas-node-shadow-hover)');
149+
});
150+
});

packages/apollo-react/src/canvas/components/LoopNode/LoopNode.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,9 @@ function LoopNodeComponent(props: LoopNodeProps) {
316316
const showEmptyStateButton = isDesignMode && !hasChildNodes && !!onAddFirstChild;
317317

318318
const interactionState = resolveInteractionState(dragging, selected, isHovered);
319+
const activeStatus = suggestionType ?? validationState?.validationStatus ?? executionStatus;
320+
const statusBorder = getStatusBorder(activeStatus);
321+
const hasStatusBorder = statusBorder.length > 0;
319322

320323
if (!manifest) {
321324
return (
@@ -350,8 +353,9 @@ function LoopNodeComponent(props: LoopNodeProps) {
350353
'group/loop-shell relative box-border flex h-full w-full flex-col overflow-visible rounded-[20px] border bg-transparent',
351354
'transition-[border-color,box-shadow,opacity] shadow-(--canvas-node-shadow-rest)',
352355
'border-border',
353-
getStatusBorder(suggestionType ?? validationState?.validationStatus ?? executionStatus),
354-
isHovered && 'shadow-(--canvas-node-shadow-hover) border-border-hover',
356+
statusBorder,
357+
isHovered && 'shadow-(--canvas-node-shadow-hover)',
358+
isHovered && !hasStatusBorder && 'border-border-hover',
355359
selected && 'outline outline-2 outline-foreground-accent-muted',
356360
isDropTarget && 'bg-surface-hover outline outline-2 outline-brand',
357361
interactionState === 'drag' && 'cursor-grabbing shadow-(--canvas-node-shadow-lifted)'

0 commit comments

Comments
 (0)