Skip to content

Commit 64cae2c

Browse files
committed
fix(apollo-react): handle dark themed icon backgrounds on nodes
1 parent 0dea06f commit 64cae2c

14 files changed

Lines changed: 102 additions & 33 deletions

File tree

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const BaseCanvasInnerComponent = <NodeType extends Node = Node, EdgeType extends
4646
backgroundVariant = BASE_CANVAS_DEFAULTS.background.variant,
4747
backgroundGap = BASE_CANVAS_DEFAULTS.background.gap,
4848
backgroundSize = BASE_CANVAS_DEFAULTS.background.size,
49+
isDarkMode,
4950

5051
// Configuration
5152
minZoom = BASE_CANVAS_DEFAULTS.zoom.min,
@@ -147,7 +148,7 @@ const BaseCanvasInnerComponent = <NodeType extends Node = Node, EdgeType extends
147148
);
148149

149150
return (
150-
<CanvasProviders nodes={nodes} edges={edges} mode={mode}>
151+
<CanvasProviders nodes={nodes} edges={edges} mode={mode} isDarkMode={isDarkMode}>
151152
<ReactFlow
152153
{...reactFlowProps}
153154
nodes={nodes}

packages/apollo-react/src/canvas/components/BaseCanvas/BaseCanvas.types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,17 @@ export interface BaseCanvasProps<NodeType extends Node = Node, EdgeType extends
176176
* Used for visual indication in debug mode
177177
*/
178178
breakpoints?: Set<string>;
179+
180+
/**
181+
* Whether the canvas should render in dark mode.
182+
* Controls dark-mode-specific styling for node icons and toolbox items.
183+
*
184+
* - When `true`, components used in canvas will choose dark mode assets (e.g. icons) if available.
185+
* - When `false` or `undefined`, components will use light mode / default assets.
186+
*
187+
* @default undefined
188+
*/
189+
isDarkMode?: boolean;
179190
}
180191

181192
/**

packages/apollo-react/src/canvas/components/BaseCanvas/CanvasProviders.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ import type { Edge, Node } from '@uipath/apollo-react/canvas/xyflow/react';
22
import type { ReactNode } from 'react';
33
import type { BaseCanvasProps } from './BaseCanvas.types';
44
import { BaseCanvasModeProvider } from './BaseCanvasModeProvider';
5+
import { CanvasThemeProvider } from './CanvasThemeContext';
56
import { ConnectedHandlesProvider } from './ConnectedHandlesContext';
67
import { SelectionStateProvider } from './SelectionStateContext';
78

89
interface CanvasProvidersProps {
10+
children: ReactNode;
911
nodes: Node[];
1012
edges: Edge[];
1113
mode: BaseCanvasProps['mode'];
12-
children: ReactNode;
14+
isDarkMode?: boolean;
1315
}
1416

1517
/**
@@ -18,12 +20,20 @@ interface CanvasProvidersProps {
1820
*
1921
* This is purely a convenience wrapper - no performance implications.
2022
*/
21-
export function CanvasProviders({ nodes, edges, mode, children }: CanvasProvidersProps) {
23+
export function CanvasProviders({
24+
nodes,
25+
edges,
26+
mode,
27+
isDarkMode,
28+
children,
29+
}: CanvasProvidersProps) {
2230
return (
23-
<ConnectedHandlesProvider edges={edges}>
24-
<BaseCanvasModeProvider mode={mode}>
25-
<SelectionStateProvider nodes={nodes}>{children}</SelectionStateProvider>
26-
</BaseCanvasModeProvider>
27-
</ConnectedHandlesProvider>
31+
<CanvasThemeProvider isDarkMode={isDarkMode}>
32+
<ConnectedHandlesProvider edges={edges}>
33+
<BaseCanvasModeProvider mode={mode}>
34+
<SelectionStateProvider nodes={nodes}>{children}</SelectionStateProvider>
35+
</BaseCanvasModeProvider>
36+
</ConnectedHandlesProvider>
37+
</CanvasThemeProvider>
2838
);
2939
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type React from 'react';
2+
import { createContext, useContext, useMemo } from 'react';
3+
4+
interface CanvasThemeContextValue {
5+
isDarkMode?: boolean;
6+
}
7+
8+
const CanvasThemeContext = createContext<CanvasThemeContextValue | null>(null);
9+
10+
const defaultValue: CanvasThemeContextValue = { isDarkMode: false };
11+
12+
export const CanvasThemeProvider: React.FC<React.PropsWithChildren<{ isDarkMode?: boolean }>> = ({
13+
children,
14+
isDarkMode,
15+
}) => {
16+
const value = useMemo(() => ({ isDarkMode }), [isDarkMode]);
17+
return <CanvasThemeContext.Provider value={value}>{children}</CanvasThemeContext.Provider>;
18+
};
19+
20+
/**
21+
* Hook to access canvas theme context.
22+
* Falls back to light mode if used outside a CanvasThemeProvider.
23+
*/
24+
export function useCanvasTheme(): CanvasThemeContextValue {
25+
return useContext(CanvasThemeContext) ?? defaultValue;
26+
}

packages/apollo-react/src/canvas/components/BaseCanvas/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ export * from './BaseCanvas.hooks';
44
export * from './BaseCanvas.types';
55
export * from './CanvasBackground';
66
export * from './CanvasProviders';
7+
export * from './CanvasThemeContext';
78
export * from './ConnectedHandlesContext';
89
export * from './SelectionStateContext';

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { getIcon } from '../../utils/icon-registry';
1717
import { resolveDisplay, resolveHandles } from '../../utils/manifest-resolver';
1818
import { resolveToolbar } from '../../utils/toolbar-resolver';
1919
import { useBaseCanvasMode } from '../BaseCanvas/BaseCanvasModeProvider';
20+
import { useCanvasTheme } from '../BaseCanvas/CanvasThemeContext';
2021
import { useConnectedHandles } from '../BaseCanvas/ConnectedHandlesContext';
2122
import { useSelectionState } from '../BaseCanvas/SelectionStateContext';
2223
import type { HandleActionEvent } from '../ButtonHandle/ButtonHandle';
@@ -90,6 +91,8 @@ const BaseNodeComponent = (props: BaseNodeComponentProps) => {
9091
const isConnecting = useStore(selectIsConnecting);
9192
const { multipleNodesSelected } = useSelectionState();
9293

94+
const { isDarkMode } = useCanvasTheme();
95+
9396
// Get manifest and resolve with instance data
9497
const manifest = useMemo(() => nodeTypeRegistry.getManifest(type), [type, nodeTypeRegistry]);
9598

@@ -231,7 +234,9 @@ const BaseNodeComponent = (props: BaseNodeComponentProps) => {
231234
const displayShape = display.shape ?? 'square';
232235
const displayBackground = display.background;
233236
const displayColor = display.color;
234-
const displayIconBackground = display.iconBackground;
237+
const displayIconBackground = isDarkMode
238+
? (display.iconBackgroundDark ?? display.iconBackground)
239+
: display.iconBackground;
235240

236241
// Display customization from props (not data)
237242
const displayLabelTooltip = labelTooltip;

packages/apollo-react/src/canvas/components/BaseNode/BaseNode.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface BaseNodeData extends Record<string, unknown> {
1818
background?: string;
1919
icon?: string;
2020
iconBackground?: string;
21+
iconBackgroundDark?: string;
2122
iconColor?: string;
2223
};
2324

packages/apollo-react/src/canvas/components/Toolbox/ListView.test.tsx

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, it, vi } from 'vitest';
22
import { render, screen } from '../../utils/testing';
3+
import { CanvasThemeProvider } from '../BaseCanvas/CanvasThemeContext';
34
import type { ListItem } from './ListView';
45
import { ListView } from './ListView';
56

@@ -259,27 +260,33 @@ describe('ListView', () => {
259260
});
260261

261262
describe('Styling', () => {
262-
it('should apply custom color from getItemColor', () => {
263-
const items: ListItem[] = [{ id: 'item-1', name: 'Item 1', data: {} }];
264-
265-
const getItemColor = () => '#ff0000';
263+
it('should apply color from item.color if provided', () => {
264+
const items: ListItem[] = [{ id: 'item-1', name: 'Item 1', data: {}, color: 'rgb(1,2,3)' }];
266265

267-
render(<ListView {...defaultProps} items={items} getItemColor={getItemColor} />);
266+
render(<ListView {...defaultProps} items={items} />);
268267

269-
const button = screen.getByRole('button');
270-
// Check that the IconContainer has the background color set
271-
const iconContainer = button.querySelector('.css-q1gtze');
272-
expect(iconContainer).toBeDefined();
268+
const iconContainer = screen.getByTestId('list-item-icon');
269+
expect(iconContainer).toHaveStyle({ background: 'rgb(1,2,3)' });
273270
});
274271

275-
it('should apply color from item.color if provided', () => {
276-
const items: ListItem[] = [{ id: 'item-1', name: 'Item 1', data: {}, color: '#00ff00' }];
277-
278-
render(<ListView {...defaultProps} items={items} />);
272+
it('should use dark color in dark mode', () => {
273+
const items: ListItem[] = [
274+
{
275+
id: 'item-1',
276+
name: 'Item 1',
277+
data: {},
278+
color: 'rgb(1,2,3)',
279+
colorDark: 'rgb(3,2,1)',
280+
},
281+
];
282+
render(
283+
<CanvasThemeProvider isDarkMode={true}>
284+
<ListView {...defaultProps} items={items} />
285+
</CanvasThemeProvider>
286+
);
279287

280-
const button = screen.getByRole('button');
281-
const iconContainer = button.querySelector('.css-q1gtze');
282-
expect(iconContainer).toBeDefined();
288+
const iconContainer = screen.getByTestId('list-item-icon');
289+
expect(iconContainer).toHaveStyle({ background: 'rgb(3,2,1)' });
283290
});
284291
});
285292

packages/apollo-react/src/canvas/components/Toolbox/ListView.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ApSkeleton, ApTypography } from '@uipath/apollo-react/material';
55
import { ApIcon } from '@uipath/apollo-react/material/components';
66
import { memo, useCallback, useMemo } from 'react';
77
import type { RowComponentProps } from 'react-window';
8-
8+
import { useCanvasTheme } from '../BaseCanvas/CanvasThemeContext';
99
import { IconContainer, ListItemButton, SectionHeader, StyledList } from './ListView.styles';
1010

1111
export interface ListItemIcon {
@@ -31,6 +31,7 @@ export type ListItem<T = any> = {
3131
description?: string;
3232
icon?: ListItemIcon;
3333
color?: string;
34+
colorDark?: string;
3435
children?: ListItem<T>[] | ((id: string, name: string) => Promise<ListItem<T>[]>);
3536
};
3637

@@ -43,7 +44,6 @@ export interface ListViewRowProps<T extends ListItem> {
4344
isLoading?: boolean;
4445
onItemClick: (item: T) => void;
4546
onItemHover?: (item: T) => void;
46-
getItemColor?: (item: T) => string | undefined;
4747
}
4848

4949
const IconContainerMemoized = memo(IconContainer);
@@ -57,9 +57,9 @@ const ListViewRow = memo(
5757
isLoading,
5858
onItemClick,
5959
onItemHover,
60-
getItemColor,
6160
}: RowComponentProps<ListViewRowProps<T>>) => {
6261
const renderItem = renderedItems[index]!;
62+
const { isDarkMode } = useCanvasTheme();
6363

6464
const buttonStyle = useMemo(
6565
() => ({ ...style, padding: 0, paddingRight: '4px', height: '32px', outlineOffset: '-1px' }),
@@ -94,7 +94,7 @@ const ListViewRow = memo(
9494
}
9595

9696
const item = renderItem.item;
97-
const bgColor = getItemColor ? getItemColor(item) : 'color' in item ? item.color : undefined;
97+
const bgColor = isDarkMode ? (item.colorDark ?? item.color) : item.color;
9898

9999
return (
100100
<ListItemButton
@@ -151,7 +151,6 @@ interface ListViewProps<T extends ListItem> {
151151
items: T[];
152152
onItemClick: (item: T) => void;
153153
onItemHover?: (item: T) => void;
154-
getItemColor?: (item: T) => string | undefined;
155154
emptyStateMessage?: string;
156155
emptyStateIcon?: string;
157156
isLoading?: boolean;
@@ -161,7 +160,6 @@ interface ListViewProps<T extends ListItem> {
161160
export const ListView = memo(function ListView<T extends ListItem>({
162161
items,
163162
onItemClick,
164-
getItemColor,
165163
onItemHover,
166164
emptyStateMessage = 'No items found',
167165
emptyStateIcon = 'search_off',
@@ -208,8 +206,8 @@ export const ListView = memo(function ListView<T extends ListItem>({
208206
}, [items, enableSections]);
209207

210208
const rowProps = useMemo(
211-
() => ({ renderedItems, isLoading, onItemClick, getItemColor, onItemHover }),
212-
[renderedItems, isLoading, onItemClick, getItemColor, onItemHover]
209+
() => ({ renderedItems, isLoading, onItemClick, onItemHover }),
210+
[renderedItems, isLoading, onItemClick, onItemHover]
213211
);
214212

215213
// Only show skeleton loaders when loading and no items exist

packages/apollo-react/src/canvas/core/CategoryTreeAdapter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export class CategoryTreeAdapter {
4141
version: node.version,
4242
},
4343
icon: { name: node.display.icon },
44+
color: node.display.iconBackground,
45+
colorDark: node.display.iconBackgroundDark,
4446
});
4547

4648
const convertCategories = (categoryNodes: CategoryTreeNode[]): ListItem[] => {
@@ -70,6 +72,7 @@ export class CategoryTreeAdapter {
7072
data: null,
7173
icon: { name: category.icon },
7274
color: category.color,
75+
colorDark: category.colorDark,
7376
children,
7477
};
7578

0 commit comments

Comments
 (0)