fix: keep mobile notifications above navigation (#880)

[author: gpt-5-codex][reviewed-by: grok-4.3]

Co-authored-by: bradgroux <brad@digitalmeld.io>
This commit is contained in:
Brad Groux 2026-07-15 16:52:56 -05:00 committed by GitHub
parent d12e6f5f67
commit 82b83ad284
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 93 additions and 1 deletions

View file

@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- Kept mobile notifications above the safe-area-aware bottom navigation so
long-lived toasts cannot intercept touch input after Task Detail closes
(#869).
## [5.2.4] - 2026-07-13
### Fixed

View file

@ -155,4 +155,73 @@ test.describe('mobile responsive flows', () => {
await expect(page.getByRole('button', { name: 'Login' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Forgot password?' })).toBeVisible();
});
test('keeps mobile notifications operable above a conflict toast after task detail closes', async ({
page,
}) => {
const taskTitle = `E2E Mobile Notification Conflict ${Date.now()}`;
const task = await seedTestTask(page, {
title: taskTitle,
description: 'Verify notifications remain touchable while a conflict toast is visible.',
status: 'todo',
priority: 'high',
type: 'code',
});
testTaskId = (task as { id: string }).id;
let returnedConflict = false;
await page.route(`**/api/tasks/${testTaskId}`, async (route) => {
if (!returnedConflict && route.request().method() === 'PATCH') {
returnedConflict = true;
await route.fulfill({
status: 409,
contentType: 'application/json',
body: JSON.stringify({
success: false,
error: {
code: 'CONFLICT',
message: 'Task revision conflict',
details: { current: task },
},
meta: { timestamp: new Date().toISOString() },
}),
});
return;
}
await route.fallback();
});
await page.goto('/');
const statusSelect = page.getByRole('combobox', {
name: `Change status for ${taskTitle}`,
});
await statusSelect.click();
await page.getByRole('option', { name: 'Blocked' }).click();
await expect(page.getByText('Task changed elsewhere')).toBeVisible();
await page.getByText(taskTitle).first().click();
const detail = page.getByTestId('task-detail-panel');
await expect(detail).toBeVisible();
await detail.getByRole('button', { name: 'Close task details' }).click();
await expect(detail).not.toBeVisible();
const mobileNavigation = page.getByRole('navigation', { name: 'Mobile navigation' });
const notificationViewport = page.locator(
".veritas-notifications[data-position='bottom-right']"
);
const mobileNotificationsButton = page.getByRole('button', { name: 'Mobile notifications' });
const [navigationBox, notificationBox] = await Promise.all([
mobileNavigation.boundingBox(),
notificationViewport.boundingBox(),
]);
expect(navigationBox).not.toBeNull();
expect(notificationBox).not.toBeNull();
expect(notificationBox!.y + notificationBox!.height).toBeLessThanOrEqual(navigationBox!.y);
await tapLocatorCenter(page, mobileNotificationsButton);
await expect(page.getByLabel('Notifications', { exact: true })).toBeVisible();
});
});

View file

@ -316,6 +316,13 @@ describe('Mantine-backed shared UI primitives', () => {
expect(screen.getByText('Saved')).toBeDefined();
expect(screen.getByText('Mantine notification bridge is active.')).toBeDefined();
});
expect(
document.querySelector(".veritas-notifications[data-position='bottom-right']")
).not.toBeNull();
expect(globalStyles).toMatch(
/\.veritas-notifications\[data-position\^='bottom-'\]\s*\{[^}]*bottom:\s*calc\(5\.5rem \+ env\(safe-area-inset-bottom\)\);/s
);
});
it('preserves tabs value changes through the legacy onValueChange contract', () => {

View file

@ -139,6 +139,11 @@
right: max(1rem, env(safe-area-inset-right));
bottom: calc(5.5rem + env(safe-area-inset-bottom));
}
/* Keep high-z-index toasts from intercepting the fixed mobile navigation. */
.veritas-notifications[data-position^='bottom-'] {
bottom: calc(5.5rem + env(safe-area-inset-bottom));
}
}
@media (prefers-reduced-transparency: reduce) {

View file

@ -75,7 +75,12 @@ export function MantineRoot({ children, env = 'default' }: MantineRootProps) {
{env === 'test' ? <VeritasTestColorSchemeSync /> : <VeritasColorSchemeSync />}
<ModalsProvider>
{children}
<Notifications position="bottom-right" limit={5} zIndex={5000} />
<Notifications
className="veritas-notifications"
position="bottom-right"
limit={5}
zIndex={5000}
/>
</ModalsProvider>
</MantineProvider>
);