diff --git a/e2e/website/docs.spec.ts b/e2e/website/docs.spec.ts index 4ec40394..08e403d6 100644 --- a/e2e/website/docs.spec.ts +++ b/e2e/website/docs.spec.ts @@ -1,6 +1,7 @@ import { expect, test } from '@playwright/test' import AxeBuilder from '@axe-core/playwright' import { readFileSync } from 'node:fs' +import process from 'node:process' import { getReleasePresentation } from '../../website/content/release' const generatedPublicApi = JSON.parse( @@ -320,6 +321,7 @@ test('API reference renders every generated entry exactly once', async ({ test('API signatures stay out of keyboard order while links remain reachable', async ({ page, + browserName, }) => { await page.goto('/docs/api/') @@ -327,12 +329,15 @@ test('API signatures stay out of keyboard order while links remain reachable', a const controlledRecipe = page.getByRole('link', { name: 'Run the controlled-state recipe', }) + // macOS Safari uses Option+Tab to include links in keyboard navigation. + const modifier = + browserName === 'webkit' && process.platform === 'darwin' ? 'Alt+' : '' await source.focus() - await page.keyboard.press('Shift+Tab') + await page.keyboard.press(`${modifier}Shift+Tab`) await expect(page.locator(':focus')).toHaveAttribute('href') - await page.keyboard.press('Tab') + await page.keyboard.press(`${modifier}Tab`) await expect(source).toBeFocused() - await page.keyboard.press('Tab') + await page.keyboard.press(`${modifier}Tab`) await expect(controlledRecipe).toBeFocused() }) @@ -459,9 +464,16 @@ test('home page does not overflow a mobile viewport', async ({ page }) => { expect(dimensions.demo?.right).toBeLessThanOrEqual(dimensions.viewport) }) -test('skip link moves keyboard focus to the main content', async ({ page }) => { +test('skip link moves keyboard focus to the main content', async ({ + page, + browserName, +}) => { await page.goto('/') - await page.keyboard.press('Tab') + const tab = + browserName === 'webkit' && process.platform === 'darwin' + ? 'Alt+Tab' + : 'Tab' + await page.keyboard.press(tab) const skipLink = page.getByRole('link', { name: 'Skip to content' }) await expect(skipLink).toBeFocused() diff --git a/e2e/website/home.spec.ts b/e2e/website/home.spec.ts index 0764f8d8..e24825ff 100644 --- a/e2e/website/home.spec.ts +++ b/e2e/website/home.spec.ts @@ -3,6 +3,63 @@ import AxeBuilder from '@axe-core/playwright' import { readFileSync } from 'node:fs' import { buildEvidence } from '../../website/content/evidence' +test('homepage support links follow the demo and remain usable at mobile sizes', async ({ + page, +}, testInfo) => { + await page.goto('/') + const support = page.getByRole('region', { name: 'Useful in your project?' }) + await expect(support).toBeVisible() + await expect( + support.getByRole('heading', { level: 2, name: 'Useful in your project?' }), + ).toBeVisible() + expect( + await support.evaluate((element) => + element.previousElementSibling?.matches('.docs-hero'), + ), + ).toBe(true) + const links = [ + [ + 'Star on GitHub', + 'https://github.com/NIPE-Solutions/react-spring-bottom-sheet', + ], + ['Explore NIPE Open Source', 'https://opensource.nipesolutions.com'], + ] as const + for (const width of [1440, 320]) { + await page.setViewportSize({ width, height: 900 }) + await support.screenshot({ + path: testInfo.outputPath(`support-${width.toString()}.png`), + }) + for (const [name, href] of links) { + const link = support.getByRole('link', { name, exact: true }) + await expect(link).toHaveAttribute('href', href) + await link.focus() + await expect(link).toBeFocused() + await expect(link).toHaveCSS('outline-style', 'solid') + const bounds = await link.boundingBox() + expect(bounds?.height).toBeGreaterThanOrEqual(44) + expect(bounds?.width).toBeGreaterThanOrEqual(44) + expect(bounds?.x).toBeGreaterThanOrEqual(0) + expect((bounds?.x ?? 0) + (bounds?.width ?? 0)).toBeLessThanOrEqual(width) + } + expect( + await page.evaluate(() => document.documentElement.scrollWidth), + ).toBe(width) + } +}) + +test('support section stays off task and legal routes', async ({ page }) => { + for (const route of ['/impressum/', '/privacy/', '/examples/basic/embed/']) { + await page.goto(route) + await expect(page.getByRole('main')).toBeVisible() + await expect( + page.getByRole('region', { name: 'Useful in your project?' }), + ).toHaveCount(0) + await expect( + page.getByRole('link', { name: 'Star on GitHub', exact: true }), + ).toHaveCount(0) + } +}) + const packageVersion = JSON.parse( readFileSync(new URL('../../package.json', import.meta.url), 'utf8'), ).version as string @@ -138,11 +195,24 @@ test('live sheet opens, changes destination, and restores focus', async ({ await page.goto('/') const trigger = page.getByRole('button', { name: 'Open the live sheet' }) - await trigger.click() + await trigger.focus() + await trigger.press('Enter') const phone = page.getByLabel('Interactive sheet preview') const dialog = page.getByRole('dialog', { name: 'Try the real package' }) await expect(dialog).toBeVisible() await expect(phone.locator('[role="dialog"]')).toHaveCount(1) + await expect + .poll(() => + dialog.evaluate((element) => { + const phone = document + .querySelector('.docs-phone-screen') + ?.getBoundingClientRect() + return phone + ? element.getBoundingClientRect().top < phone.bottom + : false + }), + ) + .toBe(true) const containment = await page.evaluate(() => { const screen = document.querySelector('.docs-phone-screen') const phone = screen?.getBoundingClientRect() diff --git a/e2e/website/recipes.spec.ts b/e2e/website/recipes.spec.ts index 0fec7e6b..1790ba42 100644 --- a/e2e/website/recipes.spec.ts +++ b/e2e/website/recipes.spec.ts @@ -1031,10 +1031,14 @@ test('basic recipe opens, closes, and restores focus', async ({ page }) => { const frame = recipeFrame(page) const trigger = frame.getByRole('button', { name: 'Open basic sheet' }) - await trigger.click() + await trigger.focus() + await trigger.press('Enter') await expect( frame.getByRole('dialog', { name: 'Basic bottom sheet' }), ).toBeVisible() + await expect( + frame.getByRole('dialog', { name: 'Basic bottom sheet' }), + ).toHaveAttribute('data-rsbs-state', 'open') await frame.getByRole('button', { name: 'Close sheet' }).click() await expect(frame.getByRole('dialog')).toHaveCount(0) await expect(trigger).toBeFocused() @@ -1263,6 +1267,9 @@ test('confirmation recipe cannot dismiss without an explicit choice', async ({ await page.goto('/examples/confirmation/') const frame = recipeFrame(page) await frame.getByRole('button', { name: 'Delete workspace' }).click() + await expect( + frame.getByRole('dialog', { name: 'Delete this workspace?' }), + ).toHaveAttribute('data-rsbs-state', 'open') await page.keyboard.press('Escape') await expect( frame.getByRole('dialog', { name: 'Delete this workspace?' }), diff --git a/website/app/(site)/page.tsx b/website/app/(site)/page.tsx index f1f0b5f8..031018f9 100644 --- a/website/app/(site)/page.tsx +++ b/website/app/(site)/page.tsx @@ -33,6 +33,27 @@ export default function Home() { + + Useful in your project? + + If this sheet helps your project, a GitHub star is a simple way to + support the work and help others find it. + + + + Star on GitHub + + + Explore NIPE Open Source + + + diff --git a/website/app/site.css b/website/app/site.css index d21d6faa..e898da4a 100644 --- a/website/app/site.css +++ b/website/app/site.css @@ -253,6 +253,22 @@ color: var(--docs-muted); font-size: 0.86rem; } + .docs-support { + padding: clamp(2rem, 5vw, 4rem) clamp(1.2rem, 7vw, 7rem); + border-block: 1px solid var(--docs-rule); + } + .docs-support p { + max-width: 65ch; + color: var(--docs-muted); + } + .docs-support .docs-actions { + margin-bottom: 0; + } + .docs-support .docs-button { + display: inline-flex; + align-items: center; + min-height: 44px; + } .docs-lede { max-width: 44rem; color: var(--docs-muted);
+ If this sheet helps your project, a GitHub star is a simple way to + support the work and help others find it. +