Skip to content

Commit bbd5a5d

Browse files
committed
feat: make AboutMenu logo theme-aware
Update the About modal logo to switch between light and dark variants based on the current theme, matching the main header logo behavior. Changes: - Convert AboutMenu from class to functional component - Add useTheme hook to detect current theme - Apply theme-aware logo path switching: - Dark theme: use default che-logo.svg (white text) - Light theme: use lightTheme/che-logo.svg (dark text) - Add safe fallback handling for logo path replacement - Update test to wrap component with ThemeProvider - Mock window.matchMedia for test environment All tests pass (12 passed). Assisted-by: Claude Sonnet 4.5 Signed-off-by: Oleksii Orel <oorel@redhat.com>
1 parent cefbc21 commit bbd5a5d

File tree

2 files changed

+83
-77
lines changed

2 files changed

+83
-77
lines changed

packages/dashboard-frontend/src/Layout/Header/Tools/AboutMenu/__tests__/index.spec.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { Provider } from 'react-redux';
1717
import renderer from 'react-test-renderer';
1818
import { Store } from 'redux';
1919

20+
import { ThemeProvider } from '@/contexts/ThemeContext';
2021
import { AboutMenu } from '@/Layout/Header/Tools/AboutMenu';
2122
import { BRANDING_DEFAULT, BrandingData } from '@/services/bootstrap/branding.constant';
2223
import { AppThunk } from '@/store';
@@ -43,6 +44,21 @@ jest.mock('@/store/InfrastructureNamespaces', () => {
4344
describe('About Menu', () => {
4445
global.open = jest.fn();
4546

47+
// Mock matchMedia for ThemeProvider
48+
Object.defineProperty(window, 'matchMedia', {
49+
writable: true,
50+
value: jest.fn().mockImplementation(query => ({
51+
matches: false,
52+
media: query,
53+
onchange: null,
54+
addListener: jest.fn(),
55+
removeListener: jest.fn(),
56+
addEventListener: jest.fn(),
57+
removeEventListener: jest.fn(),
58+
dispatchEvent: jest.fn(),
59+
})),
60+
});
61+
4662
const productCli = 'crwctl';
4763
const email = 'johndoe@example.com';
4864
const username = 'John Doe';
@@ -51,7 +67,9 @@ describe('About Menu', () => {
5167

5268
const component = (
5369
<Provider store={store}>
54-
<AboutMenu branding={branding} username={username} />
70+
<ThemeProvider>
71+
<AboutMenu branding={branding} username={username} />
72+
</ThemeProvider>
5573
</Provider>
5674
);
5775

packages/dashboard-frontend/src/Layout/Header/Tools/AboutMenu/index.tsx

Lines changed: 64 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import { QuestionCircleIcon } from '@patternfly/react-icons';
2121
import React from 'react';
2222

23+
import { useTheme } from '@/contexts/ThemeContext';
2324
import { AboutModal } from '@/Layout/Header/Tools/AboutMenu/Modal';
2425
import { BrandingData } from '@/services/bootstrap/branding.constant';
2526
import { buildLogoSrc } from '@/services/helpers/brandingLogo';
@@ -29,23 +30,13 @@ type Props = {
2930
username: string;
3031
dashboardLogo?: { base64data: string; mediatype: string };
3132
};
32-
type State = {
33-
isLauncherOpen: boolean;
34-
isModalOpen: boolean;
35-
};
36-
37-
export class AboutMenu extends React.PureComponent<Props, State> {
38-
constructor(props: Props) {
39-
super(props);
4033

41-
this.state = {
42-
isLauncherOpen: false,
43-
isModalOpen: false,
44-
};
45-
}
34+
export const AboutMenu: React.FC<Props> = ({ branding, username, dashboardLogo }) => {
35+
const [isLauncherOpen, setIsLauncherOpen] = React.useState(false);
36+
const [isModalOpen, setIsModalOpen] = React.useState(false);
37+
const { isDarkTheme } = useTheme();
4638

47-
private buildDropdownItems(): React.ReactNode[] {
48-
const branding = this.props.branding;
39+
const buildDropdownItems = React.useCallback(() => {
4940
const items: React.ReactElement[] = [];
5041
branding.links?.forEach(link => {
5142
items.push(
@@ -56,72 +47,69 @@ export class AboutMenu extends React.PureComponent<Props, State> {
5647
});
5748

5849
items.push(
59-
<DropdownItem key="about" onClick={e => this.showModal(e)}>
50+
<DropdownItem
51+
key="about"
52+
onClick={e => {
53+
e.preventDefault();
54+
setIsLauncherOpen(false);
55+
setIsModalOpen(true);
56+
}}
57+
>
6058
About
6159
</DropdownItem>,
6260
);
6361
return items;
64-
}
65-
66-
private onToggle() {
67-
this.setState({
68-
isLauncherOpen: !this.state.isLauncherOpen,
69-
});
70-
}
71-
72-
private showModal(e: MouseEvent | React.MouseEvent | React.KeyboardEvent) {
73-
e.preventDefault();
74-
this.setState({
75-
isLauncherOpen: false,
76-
isModalOpen: true,
77-
});
78-
}
79-
80-
private closeModal() {
81-
this.setState({
82-
isLauncherOpen: false,
83-
isModalOpen: false,
84-
});
85-
}
62+
}, [branding.links]);
8663

87-
public render(): React.ReactElement {
88-
const { username, dashboardLogo } = this.props;
89-
const { isLauncherOpen, isModalOpen } = this.state;
64+
const { logoFile, name, productVersion } = branding;
9065

91-
const { logoFile, name, productVersion } = this.props.branding;
66+
// Use light theme logo for light mode, default logo for dark mode
67+
const themeAwareLogoFile = React.useMemo(() => {
68+
if (!logoFile || isDarkTheme) {
69+
return logoFile;
70+
}
71+
// Replace the assets path prefix if present
72+
if (logoFile.includes('./assets/branding/')) {
73+
return logoFile.replace('./assets/branding/', './assets/branding/lightTheme/');
74+
}
75+
// Fallback: prepend lightTheme/ if no prefix found
76+
return `lightTheme/${logoFile}`;
77+
}, [logoFile, isDarkTheme]);
9278

93-
const logoSrc = buildLogoSrc(dashboardLogo, logoFile);
79+
const logoSrc = buildLogoSrc(dashboardLogo, themeAwareLogoFile);
9480

95-
return (
96-
<>
97-
<Dropdown
98-
onSelect={() => this.setState({ isLauncherOpen: false })}
99-
onOpenChange={isOpen => this.setState({ isLauncherOpen: isOpen })}
100-
isOpen={isLauncherOpen}
101-
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
102-
<MenuToggle
103-
ref={toggleRef}
104-
onClick={() => this.onToggle()}
105-
isExpanded={isLauncherOpen}
106-
variant="plain"
107-
aria-label="About Menu"
108-
>
109-
<QuestionCircleIcon />
110-
</MenuToggle>
111-
)}
112-
popperProps={{ position: 'right' }}
113-
>
114-
<DropdownList>{this.buildDropdownItems()}</DropdownList>
115-
</Dropdown>
116-
<AboutModal
117-
isOpen={isModalOpen}
118-
closeModal={() => this.closeModal()}
119-
username={username}
120-
logo={logoSrc}
121-
productName={name}
122-
serverVersion={productVersion}
123-
/>
124-
</>
125-
);
126-
}
127-
}
81+
return (
82+
<>
83+
<Dropdown
84+
onSelect={() => setIsLauncherOpen(false)}
85+
onOpenChange={isOpen => setIsLauncherOpen(isOpen)}
86+
isOpen={isLauncherOpen}
87+
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
88+
<MenuToggle
89+
ref={toggleRef}
90+
onClick={() => setIsLauncherOpen(!isLauncherOpen)}
91+
isExpanded={isLauncherOpen}
92+
variant="plain"
93+
aria-label="About Menu"
94+
>
95+
<QuestionCircleIcon />
96+
</MenuToggle>
97+
)}
98+
popperProps={{ position: 'right' }}
99+
>
100+
<DropdownList>{buildDropdownItems()}</DropdownList>
101+
</Dropdown>
102+
<AboutModal
103+
isOpen={isModalOpen}
104+
closeModal={() => {
105+
setIsLauncherOpen(false);
106+
setIsModalOpen(false);
107+
}}
108+
username={username}
109+
logo={logoSrc}
110+
productName={name}
111+
serverVersion={productVersion}
112+
/>
113+
</>
114+
);
115+
};

0 commit comments

Comments
 (0)