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
38 changes: 38 additions & 0 deletions apps/web/e2e/mocked/browser-behavior.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,44 @@
import { expect, test } from '@playwright/test'
import { authenticatedApi, entry, entryId, guestApi, mockApi, project, projectId } from './mock-api'

function backgroundPixels(element: Element) {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')!
context.fillStyle = getComputedStyle(element).backgroundColor
context.fillRect(0, 0, 1, 1)
return Array.from(context.getImageData(0, 0, 1, 1).data)
}

for (const theme of ['dark', 'light']) {
test(`mobile navigation has an opaque ${theme} surface and a dark backdrop without blur`, async ({ page }, testInfo) => {
await page.setViewportSize({ width: 390, height: 844 })
const assertNoUnexpected = await mockApi(page, authenticatedApi)
await page.goto('/dashboard')
await page.evaluate((dark) => document.documentElement.classList.toggle('dark', dark), theme === 'dark')
await page.getByRole('button', { name: 'Open navigation menu' }).click()
const drawer = page.getByRole('dialog', { name: 'Sidebar' })
await expect(drawer).toBeVisible()

// Browser-computed colors catch transparent surfaces even when the theme's
// CSS variables or generated utility classes change.
const surface = await drawer.evaluate(backgroundPixels)
expect(surface[3], 'Navigation must hide the page behind it').toBe(255)
expect(surface[0]).toBe(theme === 'dark' ? 10 : 255)
await expect(drawer).toHaveCSS('backdrop-filter', 'none')

const overlay = page.locator('[data-slot="sheet-overlay"]')
await expect(overlay).toHaveCSS('backdrop-filter', 'none')
const backdrop = await overlay.evaluate(backgroundPixels)
expect(backdrop.slice(0, 3)).toEqual([0, 0, 0])
expect(backdrop[3]).toBeGreaterThanOrEqual(178)
expect(backdrop[3]).toBeLessThanOrEqual(179)
await page.screenshot({ path: testInfo.outputPath(`mobile-navigation-${theme}.png`) })
await page.keyboard.press('Escape')
await expect(drawer).toBeHidden()
assertNoUnexpected()
})
}

test('mobile drawer exposes working links and closes after navigation', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
const assertNoUnexpected = await mockApi(page, authenticatedApi)
Expand Down
14 changes: 14 additions & 0 deletions apps/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,17 @@ html.dark {
min-width: 20rem;
}
}

/* The mobile sidebar is portaled outside the app layout. Give the drawer its
own opaque theme surface so page content cannot compete with navigation. */
[data-slot='sidebar'][data-mobile='true'] {
@apply bg-background text-foreground;
backdrop-filter: none;
}

/* Limit the darker scrim to navigation; other sheets keep their own styling.
Radix renders the overlay immediately before its portaled drawer. */
[data-slot='sheet-overlay']:has(+ [data-slot='sidebar'][data-mobile='true']) {
@apply bg-black/70;
backdrop-filter: none;
}
Binary file added docs/images/issue-18-mobile-navigation-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/issue-18-mobile-navigation-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading