Skip to content
Open
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
126 changes: 126 additions & 0 deletions src/__testing__/BottomSheet.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import BottomSheet from '../custom/BottomSheet/BottomSheet';
import {
createCustomTheme,
readableTextColor,
SistentThemeProvider,
ThemeProvider
} from '../theme';

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

const noop = () => {};

const rgb = (hex: string | undefined) => {
let h = String(hex).replace('#', '');
if (h.length === 3)
h = h
.split('')
.map((c) => c + c)
.join('');
const int = parseInt(h, 16);
return `rgb(${(int >> 16) & 255}, ${(int >> 8) & 255}, ${int & 255})`;
};

/** The flex container that carries the resolved header `background`/`color`. */
const getHeader = () => screen.getByText('Test Title').parentElement as HTMLElement;

function renderSheet(
props: Partial<React.ComponentProps<typeof BottomSheet>> = {},
{ mode = 'light' }: { mode?: 'light' | 'dark' } = {}
) {
return render(
<SistentThemeProvider initialMode={mode}>
<BottomSheet open title="Test Title" onClose={noop} {...props}>
<p>content</p>
</BottomSheet>
</SistentThemeProvider>
);
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe('BottomSheet header color resolution', () => {
it('falls back to surface.tint for the background and light ink for the text', () => {
const theme = createCustomTheme('light');
renderSheet();

const header = getHeader();
expect(getComputedStyle(header).background).toContain('gradient');
expect(getComputedStyle(header).color).toBe(rgb(theme.palette.common.white));
});

it('keeps the surface.tint header readable in dark mode (light ink, not text.inverse)', () => {
const theme = createCustomTheme('dark');
// In the dark palette text.inverse is near-black, which would be invisible
// on the dark surface.tint gradient — the tint header must stay light.
expect(rgb(theme.palette.text.inverse)).not.toBe(rgb(theme.palette.common.white));

renderSheet({}, { mode: 'dark' });

const header = getHeader();
expect(getComputedStyle(header).background).toContain('gradient');
expect(getComputedStyle(header).color).toBe(rgb(theme.palette.common.white));
});

it('falls back to background.default when the palette has no surface.tint', () => {
const theme = createCustomTheme('light');
delete (theme.palette.surface as { tint?: string }).tint;

render(
<ThemeProvider theme={theme}>
<BottomSheet open title="Test Title" onClose={noop}>
<p>content</p>
</BottomSheet>
</ThemeProvider>
);

const header = getHeader();
expect(getComputedStyle(header).background).toBe(rgb(theme.palette.background.default));
expect(getComputedStyle(header).color).toBe(rgb(theme.palette.text.default));
});

it('picks the light (inverse) ink for a dark custom background', () => {
const theme = createCustomTheme('light');
renderSheet({ headerBackgroundColor: '#121212' });

const expected = readableTextColor(
'#121212',
theme.palette.text.inverse,
theme.palette.text.default
);
expect(expected).toBe(theme.palette.text.inverse);
expect(getComputedStyle(getHeader()).color).toBe(rgb(expected));
});

it('picks the dark (default) ink for a light custom background', () => {
const theme = createCustomTheme('light');
renderSheet({ headerBackgroundColor: '#f5f5f5' });

const expected = readableTextColor(
'#f5f5f5',
theme.palette.text.inverse,
theme.palette.text.default
);
expect(expected).toBe(theme.palette.text.default);
expect(getComputedStyle(getHeader()).color).toBe(rgb(expected));
});

it('lets an explicit headerTextColor win over the computed ink', () => {
renderSheet({ headerBackgroundColor: '#121212', headerTextColor: '#ff0000' });

expect(getComputedStyle(getHeader()).color).toBe('rgb(255, 0, 0)');
});

it('applies the same resolved ink to the close-button icon', () => {
renderSheet({ headerBackgroundColor: '#121212', headerTextColor: '#ff0000' });

const icon = screen.getByLabelText('Close').querySelector('svg') as SVGElement;
expect(getComputedStyle(icon).fill).toBe('#ff0000');
});
});
34 changes: 27 additions & 7 deletions src/custom/BottomSheet/BottomSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Slide, { SlideProps } from '@mui/material/Slide';
import { readableTextColor, useTheme } from '../../theme';
import React, { useId } from 'react';
import { Box } from '../../base/Box';
import { Dialog } from '../../base/Dialog';
Expand Down Expand Up @@ -38,6 +39,25 @@ const BottomSheet = ({
headerTextColor
}: BottomSheetProps) => {
const titleId = useId();
const theme = useTheme();

const tint = theme.palette.surface?.tint;
const finalHeaderBackgroundColor =
headerBackgroundColor || tint || theme.palette.background.default;

const defaultForeground = headerBackgroundColor
? readableTextColor(
headerBackgroundColor,
theme.palette.text.inverse,
theme.palette.text.default
)
: tint
? // surface.tint is a dark gradient in both palettes, so always light ink
// (matches Modal / UniversalFilter tinted headers).
theme.palette.common.white
: theme.palette.text.default;

const finalHeaderTextColor = headerTextColor ?? defaultForeground;

return (
<Dialog
Expand All @@ -61,15 +81,15 @@ const BottomSheet = ({
{title && (
<>
<Box
sx={(theme) => ({
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '1rem',
textAlign: 'center',
background: headerBackgroundColor || theme.palette.surface.tint,
color: headerTextColor || theme.palette.text.primary
})}
background: finalHeaderBackgroundColor,
color: finalHeaderTextColor
}}
>
<Typography
id={titleId}
Expand All @@ -91,17 +111,17 @@ const BottomSheet = ({
onClick={onClose}
size="small"
edge="end"
sx={(theme) => ({
sx={{
'& svg': {
fill: headerTextColor || theme.palette.text.primary
fill: finalHeaderTextColor
},
transform: 'rotate(-90deg)',
'&:hover': {
transform: 'rotate(90deg)',
transition: 'all 0.3s ease-in',
cursor: 'pointer'
}
})}
}}
>
<CloseIcon />
</IconButton>
Expand Down
12 changes: 11 additions & 1 deletion src/custom/DashboardLayout/DashboardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ export interface DashboardLayoutProps {

/** Optional fixed height for the sticky sidebar. Defaults to 100vh */
sidebarHeight?: string | number;

/** Background color for the mobile bottom sheet header */
headerBackgroundColor?: string;

/** Text color for the mobile bottom sheet header */
headerTextColor?: string;
}

export const DashboardLayout: React.FC<DashboardLayoutProps> = ({
Expand All @@ -36,7 +42,9 @@ export const DashboardLayout: React.FC<DashboardLayoutProps> = ({
sidebarTitle = 'Widget Picker',
sidebarWidth = { xs: '100%', md: '350px' },
sidebarTopOffset = '0',
sidebarHeight = '100vh'
sidebarHeight = '100vh',
headerBackgroundColor,
headerTextColor
}) => {
const theme = useTheme();
// We use the 'md' breakpoint (900px default) to switch between mobile and desktop layout
Expand Down Expand Up @@ -76,6 +84,8 @@ export const DashboardLayout: React.FC<DashboardLayoutProps> = ({
onClose={() => setIsSheetVisible(false)}
title={sidebarTitle}
maxHeight="50vh"
headerBackgroundColor={headerBackgroundColor}
headerTextColor={headerTextColor}
>
{sidebarContent}
</BottomSheet>
Expand Down
Loading