Skip to content

Commit bf208da

Browse files
committed
fix(apollo-react): handle dark themed icon backgrounds on nodes
1 parent 9edf103 commit bf208da

14 files changed

Lines changed: 95 additions & 32 deletions

File tree

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useTheme } from '@mui/material/styles';
12
import type { Edge, Node, ReactFlowInstance } from '@uipath/apollo-react/canvas/xyflow/react';
23
import { ConnectionMode, ReactFlow } from '@uipath/apollo-react/canvas/xyflow/react';
34
import {
@@ -46,6 +47,7 @@ const BaseCanvasInnerComponent = <NodeType extends Node = Node, EdgeType extends
4647
backgroundVariant = BASE_CANVAS_DEFAULTS.background.variant,
4748
backgroundGap = BASE_CANVAS_DEFAULTS.background.gap,
4849
backgroundSize = BASE_CANVAS_DEFAULTS.background.size,
50+
isDarkMode = false,
4951

5052
// Configuration
5153
minZoom = BASE_CANVAS_DEFAULTS.zoom.min,
@@ -101,6 +103,10 @@ const BaseCanvasInnerComponent = <NodeType extends Node = Node, EdgeType extends
101103
const isInteractive = mode !== 'readonly';
102104
const isDesignMode = mode === 'design';
103105

106+
// Check MUI theme for dark mode if not explicitly set by prop
107+
const muiTheme = useTheme();
108+
const isDarkModeWithMuiCheck = isDarkMode || muiTheme.palette.mode === 'dark';
109+
104110
const [reactFlowInstance, setReactFlowInstance] =
105111
useState<ReactFlowInstance<NodeType, EdgeType>>();
106112

@@ -147,7 +153,7 @@ const BaseCanvasInnerComponent = <NodeType extends Node = Node, EdgeType extends
147153
);
148154

149155
return (
150-
<CanvasProviders nodes={nodes} edges={edges} mode={mode}>
156+
<CanvasProviders nodes={nodes} edges={edges} mode={mode} isDarkMode={isDarkModeWithMuiCheck}>
151157
<ReactFlow
152158
{...reactFlowProps}
153159
nodes={nodes}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,13 @@ 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+
* @default false
184+
*/
185+
isDarkMode?: boolean;
179186
}
180187

181188
/**

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ 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 {
910
nodes: Node[];
1011
edges: Edge[];
1112
mode: BaseCanvasProps['mode'];
13+
isDarkMode: boolean;
1214
children: ReactNode;
1315
}
1416

@@ -18,12 +20,14 @@ 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({ nodes, edges, mode, isDarkMode, children }: CanvasProvidersProps) {
2224
return (
23-
<ConnectedHandlesProvider edges={edges}>
24-
<BaseCanvasModeProvider mode={mode}>
25-
<SelectionStateProvider nodes={nodes}>{children}</SelectionStateProvider>
26-
</BaseCanvasModeProvider>
27-
</ConnectedHandlesProvider>
25+
<CanvasThemeProvider isDarkMode={isDarkMode}>
26+
<ConnectedHandlesProvider edges={edges}>
27+
<BaseCanvasModeProvider mode={mode}>
28+
<SelectionStateProvider nodes={nodes}>{children}</SelectionStateProvider>
29+
</BaseCanvasModeProvider>
30+
</ConnectedHandlesProvider>
31+
</CanvasThemeProvider>
2832
);
2933
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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<
13+
React.PropsWithChildren<{ isDarkMode: boolean }>
14+
> = ({ children, isDarkMode }) => {
15+
const value = useMemo(() => ({ isDarkMode }), [isDarkMode]);
16+
return <CanvasThemeContext.Provider value={value}>{children}</CanvasThemeContext.Provider>;
17+
};
18+
19+
/**
20+
* Hook to access canvas theme context.
21+
* Falls back to light mode if used outside a CanvasThemeProvider.
22+
*/
23+
export function useCanvasTheme(): CanvasThemeContextValue {
24+
return useContext(CanvasThemeContext) ?? defaultValue;
25+
}

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';
@@ -58,6 +59,8 @@ const BaseNodeComponent = (props: NodeProps<Node<BaseNodeData>>) => {
5859
const isConnecting = useStore(selectIsConnecting);
5960
const { multipleNodesSelected } = useSelectionState();
6061

62+
const { isDarkMode } = useCanvasTheme();
63+
6164
// Get manifest and resolve with instance data
6265
const manifest = useMemo(() => nodeTypeRegistry.getManifest(type), [type, nodeTypeRegistry]);
6366

@@ -199,7 +202,9 @@ const BaseNodeComponent = (props: NodeProps<Node<BaseNodeData>>) => {
199202
const displayShape = display.shape ?? 'square';
200203
const displayBackground = display.background;
201204
const displayColor = display.color;
202-
const displayIconBackground = display.iconBackground;
205+
const displayIconBackground = isDarkMode
206+
? (display.iconBackgroundDark ?? display.iconBackground)
207+
: display.iconBackground;
203208
const displayCenterAdornment = display.centerAdornmentComponent;
204209
const displayFooter = display.footerComponent;
205210
const displayFooterVariant = display.footerVariant;

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
icon?: string;
1919
iconElement?: React.ReactNode;
2020
iconBackground?: string;
21+
iconBackgroundDark?: string;
2122
iconColor?: string;
2223
labelTooltip?: string;
2324
labelBackgroundColor?: string;

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: '#123456' }];
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: '#123456' });
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: '#123456',
279+
colorDark: '#654321',
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: '#654321' });
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)