Skip to content

chore(workspaces): move workspace tab theming to provider #7035

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions packages/compass-collection/src/plugin-tab-title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import toNS from 'mongodb-ns';
import {
useConnectionInfo,
useConnectionsListRef,
useTabConnectionTheme,
} from '@mongodb-js/compass-connections/provider';
import {
WorkspaceTab,
Expand Down Expand Up @@ -32,7 +31,6 @@ function _PluginTitle({
namespace,
...tabProps
}: PluginTitleProps) {
const { getThemeOf } = useTabConnectionTheme();
const { getConnectionById } = useConnectionsListRef();
const { id: connectionId } = useConnectionInfo();

Expand Down Expand Up @@ -75,7 +73,6 @@ function _PluginTitle({
: 'Folder'
}
data-namespace={ns}
tabTheme={getThemeOf(connectionId)}
isNonExistent={isNonExistent}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useDefaultAction } from '../../hooks/use-default-action';
import { LogoIcon } from '../icons/logo-icon';
import { Tooltip } from '../leafygreen';
import { ServerIcon } from '../icons/server-icon';
import { useTabTheme } from './use-tab-theme';

function focusedChild(className: string) {
return `&:hover ${className}, &:focus-visible ${className}, &:focus-within:not(:focus) ${className}`;
Expand Down Expand Up @@ -86,20 +87,6 @@ const tabStyles = css({
},
});

export type TabTheme = {
'--workspace-tab-background-color': string;
'--workspace-tab-selected-background-color': string;
'--workspace-tab-top-border-color': string;
'--workspace-tab-selected-top-border-color': string;
'--workspace-tab-border-color': string;
'--workspace-tab-color': string;
'--workspace-tab-selected-color': string;
'&:focus-visible': {
'--workspace-tab-selected-color': string;
'--workspace-tab-border-color': string;
};
};

const tabLightThemeStyles = css({
'--workspace-tab-background-color': palette.gray.light3,
'--workspace-tab-selected-background-color': palette.white,
Expand Down Expand Up @@ -199,7 +186,6 @@ export type WorkspaceTabPluginProps = {
isNonExistent?: boolean;
iconGlyph: GlyphName | 'Logo' | 'Server';
tooltip?: [string, string][];
tabTheme?: Partial<TabTheme>;
};

export type WorkspaceTabCoreProps = {
Expand All @@ -224,7 +210,6 @@ function Tab({
onClose,
tabContentId,
iconGlyph,
tabTheme,
className: tabClassName,
...props
}: TabProps & Omit<React.HTMLProps<HTMLDivElement>, 'title'>) {
Expand All @@ -233,6 +218,7 @@ function Tab({
const { listeners, setNodeRef, transform, transition } = useSortable({
id: tabContentId,
});
const tabTheme = useTabTheme();

const tabProps = mergeProps<HTMLDivElement>(
defaultActionProps,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { useContext } from 'react';

export type TabTheme = {
'--workspace-tab-background-color': string;
'--workspace-tab-selected-background-color': string;
'--workspace-tab-top-border-color': string;
'--workspace-tab-selected-top-border-color': string;
'--workspace-tab-border-color': string;
'--workspace-tab-color': string;
'--workspace-tab-selected-color': string;
'&:focus-visible': {
'--workspace-tab-selected-color': string;
'--workspace-tab-border-color': string;
};
};

type TabThemeProviderValue = Partial<TabTheme> | undefined;

type TabThemeContextValue = TabThemeProviderValue | null;

const TabThemeContext = React.createContext<TabThemeContextValue>(null);

export const TabThemeProvider: React.FunctionComponent<{
children: React.ReactNode;
theme: Partial<TabTheme> | undefined | null;
}> = ({ children, theme }) => {
return (
<TabThemeContext.Provider value={theme}>
{children}
</TabThemeContext.Provider>
);
};

export function useTabTheme(): Partial<TabTheme> | undefined | null {
const context = useContext(TabThemeContext);

return context;
}
5 changes: 4 additions & 1 deletion packages/compass-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@ import { Accordion } from './components/accordion';
import { CollapsibleFieldSet } from './components/collapsible-field-set';
export {
Tab as WorkspaceTab,
type TabTheme,
type WorkspaceTabCoreProps,
} from './components/workspace-tabs/tab';
export {
TabThemeProvider,
useTabTheme,
} from './components/workspace-tabs/use-tab-theme';
import { WorkspaceTabs } from './components/workspace-tabs/workspace-tabs';
import ResizableSidebar, {
defaultSidebarWidth,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import React from 'react';
import { expect } from 'chai';
import { useTabTheme } from '@mongodb-js/compass-components/src/components/workspace-tabs/use-tab-theme';
import { render } from '@mongodb-js/testing-library-compass';
import { ConnectionThemeProvider } from './connection-tab-theme-provider';

const CONNECTION_INFO = {
id: '1234',
connectionOptions: {
connectionString: 'mongodb://localhost:27017',
},
favorite: {
color: 'color3',
name: 'my kingdom for a hook',
},
};

const CONNECTION_INFO_NO_COLOR = {
id: '1234',
connectionOptions: {
connectionString: 'mongodb://localhost:27017',
},
favorite: {
name: 'look what is done cannot be now amended',
},
};

describe('ConnectionThemeProvider', function () {
describe('when a connection does not exist', function () {
it('should not provide a theme to useTabTheme', function () {
let capturedTheme: ReturnType<typeof useTabTheme> = undefined;

const TestComponent = () => {
capturedTheme = useTabTheme();
return null;
};

render(
<ConnectionThemeProvider connectionId="nonexistent-connection">
<TestComponent />
</ConnectionThemeProvider>,
{
connections: [CONNECTION_INFO],
}
);

expect(capturedTheme).to.equal(undefined);
});
});

describe('when a connection exists with a valid color', function () {
it('should provide the correct theme to useTabTheme', function () {
let capturedTheme: ReturnType<typeof useTabTheme> = undefined;

const TestComponent = () => {
capturedTheme = useTabTheme();
return null;
};

render(
<ConnectionThemeProvider connectionId={CONNECTION_INFO.id}>
<TestComponent />
</ConnectionThemeProvider>,
{
connections: [CONNECTION_INFO],
}
);

expect(capturedTheme).to.have.property(
'--workspace-tab-background-color',
'#D5EFFF'
);
expect(capturedTheme).to.have.property(
'--workspace-tab-top-border-color',
'#D5EFFF'
);
expect(capturedTheme).to.have.property(
'--workspace-tab-selected-top-border-color',
'#C2E5FF'
);
expect(capturedTheme).to.have.deep.property('&:focus-visible');
});
});

describe('when a connection exists without a color', function () {
it('should not provide a theme to useTabTheme', function () {
let capturedTheme: ReturnType<typeof useTabTheme> = undefined;

const TestComponent = () => {
capturedTheme = useTabTheme();
return null;
};

render(
<ConnectionThemeProvider connectionId={CONNECTION_INFO_NO_COLOR.id}>
<TestComponent />
</ConnectionThemeProvider>,
{
connections: [CONNECTION_INFO_NO_COLOR],
}
);

expect(capturedTheme).to.equal(undefined);
});
});

describe('when a connection exists with an invalid color', function () {
it('should not provide a theme to useTabTheme', function () {
let capturedTheme: ReturnType<typeof useTabTheme> = undefined;
const INVALID_COLOR_CONNECTION = {
id: '5678',
connectionOptions: {
connectionString: 'mongodb://localhost:27017',
},
favorite: {
color: 'notavalidcolor',
name: 'invalid color connection',
},
};

const TestComponent = () => {
capturedTheme = useTabTheme();
return null;
};

render(
<ConnectionThemeProvider connectionId={INVALID_COLOR_CONNECTION.id}>
<TestComponent />
</ConnectionThemeProvider>,
{
connections: [INVALID_COLOR_CONNECTION],
}
);

expect(capturedTheme).to.equal(undefined);
});
});

describe('when a connection color is updated', function () {
it('should update the theme provided to useTabTheme', async function () {
let capturedTheme: ReturnType<typeof useTabTheme> = undefined;
const connection = {
id: 'changeable-color',
connectionOptions: {
connectionString: 'mongodb://localhost:27017',
},
favorite: {
color: 'color3', // Initial color
name: 'changing colors',
},
};

const TestComponent = () => {
capturedTheme = useTabTheme();
return <div>Theme consumer</div>;
};

const { rerender, connectionsStore } = render(
<ConnectionThemeProvider connectionId={connection.id}>
<TestComponent />
</ConnectionThemeProvider>,
{
connections: [connection],
}
);

// Initial theme should have color3 values
expect(capturedTheme).to.not.equal(null);
expect(capturedTheme).to.have.property(
'--workspace-tab-background-color',
'#D5EFFF'
);

// Update the connection color
await connectionsStore.actions.saveEditedConnection({
...connection,
favorite: {
...connection.favorite,
color: 'color1', // Change to color1
},
});

// Re-render to pick up the new color
rerender(
<ConnectionThemeProvider connectionId={connection.id}>
<TestComponent />
</ConnectionThemeProvider>
);

// Theme should have been updated with color1 values
expect(capturedTheme).to.not.equal(null);
// color1 should have a different background color than color3
expect(capturedTheme)
.to.have.property('--workspace-tab-background-color')
.that.does.not.equal('#D5EFFF');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { useMemo } from 'react';
import { type ConnectionInfo } from '@mongodb-js/connection-info';
import { useConnectionColor } from '@mongodb-js/connection-form';
import {
palette,
useDarkMode,
TabThemeProvider,
} from '@mongodb-js/compass-components';
import { useConnectionsColorList } from '../stores/store-context';

export const ConnectionThemeProvider: React.FunctionComponent<{
children: React.ReactNode;
connectionId?: ConnectionInfo['id'];
}> = ({ children, connectionId }) => {
const { connectionColorToHex, connectionColorToHexActive } =
useConnectionColor();
const connectionColorsList = useConnectionsColorList();
const darkMode = useDarkMode();

const theme = useMemo(() => {
const color = connectionColorsList.find((connection) => {
return connection.id === connectionId;
})?.color;
const bgColor = connectionColorToHex(color);
const activeBgColor = connectionColorToHexActive(color);

if (!color || !bgColor || !activeBgColor) {
return;
}

return {
'--workspace-tab-background-color': bgColor,
'--workspace-tab-top-border-color': bgColor,
'--workspace-tab-border-color': darkMode
? palette.gray.dark2
: palette.gray.light2,
'--workspace-tab-color': darkMode
? palette.gray.base
: palette.gray.dark1,
'--workspace-tab-selected-background-color': darkMode
? palette.black
: palette.white,
'--workspace-tab-selected-top-border-color': activeBgColor,
'--workspace-tab-selected-color': darkMode
? palette.white
: palette.gray.dark3,
'&:focus-visible': {
'--workspace-tab-border-color': darkMode
? palette.blue.light1
: palette.blue.base,
'--workspace-tab-selected-color': darkMode
? palette.blue.light1
: palette.blue.base,
},
};
}, [
connectionId,
connectionColorsList,
connectionColorToHex,
connectionColorToHexActive,
darkMode,
]);

return <TabThemeProvider theme={theme}>{children}</TabThemeProvider>;
};
Loading