Skip to content
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
8 changes: 5 additions & 3 deletions packages/design-system/src/components/ButtonBase/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { cn } from '../../utils/cn';

export const buttonBaseVariants = cva(
cn(
'items-center justify-center min-w-0 rounded-8',
'text-sm font-medium font-sans',
'items-center justify-center min-w-0 shrink-0 rounded-8',
'text-sm font-medium font-sans whitespace-nowrap',
'cursor-pointer overflow-hidden transition-[color,border,box-shadow,opacity]',
'focus-visible:outline-none focus-visible:ring-3',
'data-[focus=true]:outline-none data-[focus=true]:ring-3',
Expand All @@ -31,7 +31,9 @@ export const buttonBaseVariants = cva(
false: '',
},
fullWidth: {
true: 'flex flex-1 basis-full w-full',
// shrink re-enables shrinking (overrides base shrink-0 via cn) so
// multiple fullWidth buttons can split a row without overflowing
true: 'flex flex-1 shrink basis-full w-full',
false: 'inline-flex',
},
},
Expand Down
1 change: 1 addition & 0 deletions packages/design-system/src/components/Code/Code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const codeVariants = cva('font-mono', {
},
color: {
primary: 'text-text-primary',
'primary-alt': 'text-text-primary-alt',
secondary: 'text-text-secondary',
destructive: 'text-text-danger',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export const createSelectionColumn = <T,>(): ColumnDef<T> => {
enablePinning: false,
meta: {
headerClassName: 'px-8 py-4',
cellClassName: 'px-8 py-4',
cellClassName: 'px-8 py-8',
},
header: ({ table }) => {
const checked = table.getIsAllPageRowsSelected();
Expand Down
44 changes: 44 additions & 0 deletions packages/design-system/src/components/Toast/Toast.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createStoryHelper } from '@wallarm-org/playwright-config/storybook';

const toastStory = createStoryHelper('messaging-toast', [
'Basic',
'With Nested Overlays',
'Update Loading To Success',
'Simple With Actions',
'Extended With Actions',
Expand Down Expand Up @@ -200,4 +201,47 @@ test.describe('Toast Component', () => {
await expect(page).toHaveScreenshot();
});
});

test.describe('Interactions', () => {
test('Should stack the toast above open drawer and nested dialog overlays', async ({
page,
}) => {
// Regression: the toaster region's forced !z-[50] sat at or below the
// drawer/dialog positioners (50 + layer * 20) and nested overlays
// (40 + layer * 20), hiding toasts behind them. The region now sits on
// the dedicated top-tier --toast-z-index.
const expectFrontToastOnTop = async () => {
const toast = getToasts(page).first();
await expect(toast).toBeVisible();

// The topmost element at the toast's location must belong to the
// toast itself — not to a drawer, dialog, or overlay covering it.
// Polled because the toast slides in: until the enter transition
// settles its center may still be outside the viewport.
await expect
.poll(() =>
toast.evaluate(el => {
const rect = el.getBoundingClientRect();
const top = document.elementFromPoint(
rect.left + rect.width / 2,
rect.top + rect.height / 2,
);
return !!top && el.contains(top);
}),
)
.toBe(true);
};

await toastStory.goto(page, 'With Nested Overlays');

await page.getByRole('button', { name: 'Open drawer' }).click();
await page.getByRole('button', { name: 'Show toast from drawer' }).click();
await expectFrontToastOnTop();

await page.getByRole('button', { name: 'Open nested dialog' }).click();
await page.getByRole('button', { name: 'Show toast from nested dialog' }).click();
await expect(getToasts(page)).toHaveCount(2);
await expectFrontToastOnTop();
});
});
});
70 changes: 70 additions & 0 deletions packages/design-system/src/components/Toast/Toast.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@ import { useRef } from 'react';
import type { Meta, StoryFn } from 'storybook-react-rsbuild';
import { Activity } from '../../icons';
import { Button } from '../Button';
import {
Dialog,
DialogBody,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '../Dialog';
import {
Drawer,
DrawerBody,
DrawerContent,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from '../Drawer';
import { HStack, VStack } from '../Stack';
import { Text } from '../Text';
import { ToastActions, Toaster, useToast } from './index';
Expand Down Expand Up @@ -119,6 +135,60 @@ export const Basic: StoryFn = () => {
return <ToastDemo />;
};

export const WithNestedOverlays: StoryFn = () => {
const toast = useToast();

const fireToast = () =>
toast.create({
title: 'Saved',
description: 'This toast must render above every open overlay.',
type: 'success',
duration: STATIC_DURATION,
});

return (
<Drawer data-testid='overlay-drawer'>
<DrawerTrigger asChild>
<Button>Open drawer</Button>
</DrawerTrigger>

<DrawerContent>
<DrawerHeader>
<DrawerTitle>[Level 1] Drawer</DrawerTitle>
</DrawerHeader>

<DrawerBody>
<VStack gap={12} align='start'>
<Button variant='outline' color='neutral' onClick={fireToast}>
Show toast from drawer
</Button>

<Dialog>
<DialogTrigger asChild>
<Button variant='ghost' color='neutral'>
Open nested dialog
</Button>
</DialogTrigger>

<DialogContent>
<DialogHeader>
<DialogTitle>[Level 2] Dialog</DialogTitle>
</DialogHeader>

<DialogBody>
<Button variant='outline' color='neutral' onClick={fireToast}>
Show toast from nested dialog
</Button>
</DialogBody>
</DialogContent>
</Dialog>
</VStack>
</DrawerBody>
</DrawerContent>
</Drawer>
);
};

export const UpdateLoadingToSuccess: StoryFn = () => {
const toast = useToast();
const id = useRef<string | undefined>(undefined);
Expand Down
7 changes: 6 additions & 1 deletion packages/design-system/src/components/Toast/Toaster.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,12 @@ export const Toaster: FC = () => {
// center it so pausing tracks the toast column, not the full viewport width.
// (These were previously on a `[data-part=group]` descendant selector that
// never matched — the region element itself carries data-part="group".)
'fixed bottom-0 right-0 !z-[50] flex flex-col gap-12 p-24 max-w-[560px] mx-auto',
// The `!` on z is required to beat zag's inline zIndex on the region;
// --toast-z-index keeps toasts above any drawer/dialog stack and its
// overlays (positioners compute 50 + layer * 20) while staying below
// tooltips. Safe to raise: zag sets pointer-events: none on the
// region while it holds no toasts.
'fixed bottom-0 right-0 !z-(--toast-z-index) flex flex-col gap-12 p-24 max-w-[560px] mx-auto',
// Root styles with CSS variables for animations
'[&_[data-scope=toast][data-part=root]]:[translate:var(--x)_var(--y)] [&_[data-scope=toast][data-part=root]]:scale-[var(--scale)] [&_[data-scope=toast][data-part=root]]:z-[var(--z-index)] [&_[data-scope=toast][data-part=root]]:h-[var(--height)] [&_[data-scope=toast][data-part=root]]:opacity-[var(--opacity)] [&_[data-scope=toast][data-part=root]]:[will-change:translate,opacity,scale]',
// Match Drawer/Dialog timings: 300ms open, 150ms close
Expand Down
29 changes: 29 additions & 0 deletions packages/design-system/src/components/Tooltip/Tooltip.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const tooltipStory = createStoryHelper('overlay-tooltip', [
'Basic',
'With Description',
'With Kbd',
'With Nested Dialog',
] as const);

const getTooltipTrigger = (page: Page) => page.getByTestId('tooltip--trigger');
Expand Down Expand Up @@ -44,6 +45,34 @@ test.describe('Component: Tooltip', () => {
await expect(getTooltipContent(page)).toHaveText('Right');
});

test('Should stack the tooltip above a nested dialog when hovered inside it', async ({
page,
}) => {
// Regression: the tooltip content's static z-50 mirrored into its
// positioner sat below the nested dialog positioner (50 + layer * 20),
// hiding the open tooltip. Tooltips never join zag's dismissable layer
// stack, so the content uses the dedicated top-tier --tooltip-z-index.
await tooltipStory.goto(page, 'With Nested Dialog');
await page.getByRole('button', { name: 'Open dialog with nested tooltip' }).click();
await page.getByRole('button', { name: 'Open nested dialog' }).click();
await page.getByTestId('nested-tooltip--trigger').hover();

const content = page.getByTestId('nested-tooltip--content');
await expect(content).toBeVisible();

// The topmost element at the tooltip's location must belong to the
// tooltip itself — not to the nested dialog covering it.
const isOnTop = await content.evaluate(el => {
const rect = el.getBoundingClientRect();
const top = document.elementFromPoint(
rect.left + rect.width / 2,
rect.top + rect.height / 2,
);
return !!top && el.contains(top);
});
expect(isOnTop).toBe(true);
});

test('Should hide tooltip when mouse leaves trigger', async ({ page }) => {
await tooltipStory.goto(page, 'Basic');
await getTooltipTrigger(page).hover();
Expand Down
49 changes: 49 additions & 0 deletions packages/design-system/src/components/Tooltip/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import type { Meta, StoryFn } from 'storybook-react-rsbuild';
import { ChevronRight } from '../../icons';
import { Button } from '../Button';
import {
Dialog,
DialogBody,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '../Dialog';
import { Kbd } from '../Kbd';
import { Text } from '../Text';
import { Tooltip } from './Tooltip';
Expand Down Expand Up @@ -45,6 +53,47 @@ export const WithDescription: StoryFn<typeof meta> = () => (
</Tooltip>
);

export const WithNestedDialog: StoryFn<typeof meta> = () => (
<Dialog>
<DialogTrigger asChild>
<Button>Open dialog with nested tooltip</Button>
</DialogTrigger>

<DialogContent>
<DialogHeader>
<DialogTitle>[Level 1] Main Dialog</DialogTitle>
</DialogHeader>

<DialogBody>
<Dialog>
<DialogTrigger asChild>
<Button variant='ghost' color='neutral' size='small'>
Open nested dialog
</Button>
</DialogTrigger>

<DialogContent>
<DialogHeader>
<DialogTitle>[Level 2] With Tooltip</DialogTitle>
</DialogHeader>

<DialogBody>
<Tooltip data-testid='nested-tooltip' interactive>
<TooltipTrigger asChild>
<Button variant='outline' color='neutral'>
Hover me
</Button>
</TooltipTrigger>
<TooltipContent>Tooltip above the dialog</TooltipContent>
</Tooltip>
</DialogBody>
</DialogContent>
</Dialog>
</DialogBody>
</DialogContent>
</Dialog>
);

export const WithKbd: StoryFn<typeof meta> = () => (
<Tooltip>
<TooltipTrigger asChild>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ const tooltipContentVariants = cva(
[
// Base
'py-4 rounded-8 bg-component-tooltip-bg text-text-primary-alt text-xs font-medium',
// Behavior
'z-50 overflow-hidden',
// Behavior: tooltips sit on a dedicated top tier above nested
// drawer/dialog positioners (50 + layer * 20). They can't reuse the
// layer-aware formula from DropdownMenu/Popover because zag never
// registers tooltips in its dismissable layer stack, so --layer-index
// is never set on this node. Zag's popper mirrors the computed z-index
// into the portal positioner's --z-index, so the value lives here.
'z-(--tooltip-z-index) overflow-hidden',
// Animation base
'animate-in fade-in-0 zoom-in-95 origin-[--transform-origin]',
// Animation closed
Expand Down
8 changes: 8 additions & 0 deletions packages/design-system/src/theme/components/toast.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
:root {
/* Toasts are viewport-level notifications that must stay visible above
any open drawer/dialog stack and its overlays (positioners compute
50 + layer * 20, overlays 40 + layer * 20), so they sit on a dedicated
top tier. Kept below --tooltip-z-index (1000) so tooltips triggered
from toast controls still render above the toast itself. */
--toast-z-index: 900;
}
8 changes: 8 additions & 0 deletions packages/design-system/src/theme/components/tooltip.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
:root {
/* Tooltips never join zag's dismissable layer stack (unlike menus,
selects, popovers, and dialogs), so they cannot derive a layer-aware
z-index from --layer-index. Instead they live on a dedicated top tier
above any realistic overlay stack — drawer/dialog positioners compute
50 + layer * 20, so 1000 clears dozens of nested levels. */
--tooltip-z-index: 1000;
}
2 changes: 2 additions & 0 deletions packages/design-system/src/theme/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
@import './utilities/code-snippet-bg.css';
@import './components/drawer.css';
@import './components/dialog.css';
@import './components/tooltip.css';
@import './components/toast.css';
@import './components/tour.css';
@import './components/skeleton.css';
@import './components/accordion.css';
Expand Down
Loading