fix: restore case study drawer interactions
This commit is contained in:
@@ -9,20 +9,19 @@ async function expectDrawerOpen(page: import('@playwright/test').Page) {
|
|||||||
return dialog;
|
return dialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
test('a card CTA opens a route-backed drawer and restores the portfolio', async ({ page }) => {
|
test('a pointer click on the card CTA opens a route-backed drawer and restores the portfolio', async ({ page }) => {
|
||||||
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
window.scrollTo({ top: document.body.scrollHeight * 0.35, behavior: 'instant' });
|
window.scrollTo({ top: document.body.scrollHeight * 0.35, behavior: 'instant' });
|
||||||
});
|
});
|
||||||
const card = page.getByRole('article', { name: 'Qlockify' });
|
const card = page.getByRole('article', { name: 'Qlockify' });
|
||||||
const cta = card.getByRole('link', { name: 'View case study' });
|
const cta = card.getByRole('link', { name: 'View case study' });
|
||||||
await cta.focus();
|
|
||||||
const portfolioScroll = await page.evaluate(() => window.scrollY);
|
const portfolioScroll = await page.evaluate(() => window.scrollY);
|
||||||
await cta.press('Enter');
|
await cta.click();
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`${caseStudyPath}$`));
|
await expect(page).toHaveURL(new RegExp(`${caseStudyPath}$`));
|
||||||
const dialog = await expectDrawerOpen(page);
|
const dialog = await expectDrawerOpen(page);
|
||||||
await expect(dialog.getByRole('heading', { name: 'Qlockify', level: 1 })).toBeFocused();
|
await expect(dialog.getByRole('button', { name: 'Close case study' })).toBeFocused();
|
||||||
await expect(card.getByRole('link', { name: /Open the live Qlockify/ })).toHaveAttribute('target', '_blank');
|
await expect(card.getByRole('link', { name: /Open the live Qlockify/ })).toHaveAttribute('target', '_blank');
|
||||||
await expect(card.getByRole('link', { name: /Open the live Qlockify/ })).toHaveAttribute('rel', 'noopener noreferrer');
|
await expect(card.getByRole('link', { name: /Open the live Qlockify/ })).toHaveAttribute('rel', 'noopener noreferrer');
|
||||||
|
|
||||||
@@ -53,7 +52,7 @@ test('copy feedback, focus containment, and the localized RTL route work', async
|
|||||||
await page.goto('/fa/projects/qlockify', { waitUntil: 'domcontentloaded' });
|
await page.goto('/fa/projects/qlockify', { waitUntil: 'domcontentloaded' });
|
||||||
const dialog = await expectDrawerOpen(page);
|
const dialog = await expectDrawerOpen(page);
|
||||||
await expect(page.locator('html')).toHaveAttribute('dir', 'rtl');
|
await expect(page.locator('html')).toHaveAttribute('dir', 'rtl');
|
||||||
await expect(dialog.getByRole('heading', { name: 'Qlockify', level: 1 })).toBeFocused();
|
await expect(dialog.getByRole('button', { name: 'بستن مطالعه موردی' })).toBeFocused();
|
||||||
|
|
||||||
await dialog.getByRole('button', { name: 'کپی پیوند' }).click();
|
await dialog.getByRole('button', { name: 'کپی پیوند' }).click();
|
||||||
await expect(dialog.getByText('پیوند کپی شد', { exact: true }).first()).toBeVisible();
|
await expect(dialog.getByText('پیوند کپی شد', { exact: true }).first()).toBeVisible();
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function CaseStudyContent({
|
|||||||
<article className={styles.content}>
|
<article className={styles.content}>
|
||||||
<header className={styles.hero}>
|
<header className={styles.hero}>
|
||||||
<p className={styles.date}>{project.date}</p>
|
<p className={styles.date}>{project.date}</p>
|
||||||
<h1 id="case-study-title" className={styles.title} tabIndex={-1} data-case-study-heading>
|
<h1 id="case-study-title" className={styles.title}>
|
||||||
{project.title}
|
{project.title}
|
||||||
</h1>
|
</h1>
|
||||||
<p id="case-study-introduction" className={styles.introduction}>{caseStudy.introduction}</p>
|
<p id="case-study-introduction" className={styles.introduction}>{caseStudy.introduction}</p>
|
||||||
|
|||||||
@@ -204,16 +204,6 @@
|
|||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title:focus-visible {
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
outline: 3px solid var(--focus-ring);
|
|
||||||
outline-offset: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.introduction {
|
.introduction {
|
||||||
max-width: 46rem;
|
max-width: 46rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export function CaseStudyDrawer({ children, locale }: { children: ReactNode; loc
|
|||||||
root.style.overflow = 'hidden';
|
root.style.overflow = 'hidden';
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
dialog.dataset.state = 'open';
|
dialog.dataset.state = 'open';
|
||||||
dialog.querySelector<HTMLElement>('[data-case-study-heading]')?.focus();
|
dialog.querySelector<HTMLElement>('[data-case-study-initial-focus]')?.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -96,7 +96,7 @@ export function CaseStudyDrawer({ children, locale }: { children: ReactNode; loc
|
|||||||
scrollRef.current?.scrollTo({ top: 0, behavior: 'auto' });
|
scrollRef.current?.scrollTo({ top: 0, behavior: 'auto' });
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
setCopyState('idle');
|
setCopyState('idle');
|
||||||
dialogRef.current?.querySelector<HTMLElement>('[data-case-study-heading]')?.focus();
|
dialogRef.current?.querySelector<HTMLElement>('[data-case-study-initial-focus]')?.focus();
|
||||||
});
|
});
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
@@ -145,7 +145,14 @@ export function CaseStudyDrawer({ children, locale }: { children: ReactNode; loc
|
|||||||
{copyState === 'copied' ? <Check size={19} aria-hidden="true" /> : <Copy size={18} aria-hidden="true" />}
|
{copyState === 'copied' ? <Check size={19} aria-hidden="true" /> : <Copy size={18} aria-hidden="true" />}
|
||||||
<span className={styles.actionText}>{copyState === 'copied' ? copy.linkCopied : copy.copyLink}</span>
|
<span className={styles.actionText}>{copyState === 'copied' ? copy.linkCopied : copy.copyLink}</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className={styles.iconButton} onClick={(event) => requestDismiss(event.detail !== 0)} aria-label={copy.closeCaseStudy}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.iconButton}
|
||||||
|
onClick={(event) => requestDismiss(event.detail !== 0)}
|
||||||
|
aria-label={copy.closeCaseStudy}
|
||||||
|
data-case-study-initial-focus
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
<X size={21} aria-hidden="true" />
|
<X size={21} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -129,6 +129,10 @@ export const ProjectsSection = ({ locale }: { locale: Locale }) => {
|
|||||||
|
|
||||||
const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
|
const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
|
||||||
if (event.pointerType !== 'mouse' || event.button !== 0) return;
|
if (event.pointerType !== 'mouse' || event.button !== 0) return;
|
||||||
|
if (
|
||||||
|
event.target instanceof Element
|
||||||
|
&& event.target.closest('a, button, input, select, textarea, [role="button"]')
|
||||||
|
) return;
|
||||||
|
|
||||||
const viewport = viewportRef.current;
|
const viewport = viewportRef.current;
|
||||||
if (!viewport) return;
|
if (!viewport) return;
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ test('project cards separate case study navigation from live links', async () =>
|
|||||||
assert.match(source, /rel="noopener noreferrer"/);
|
assert.match(source, /rel="noopener noreferrer"/);
|
||||||
assert.match(source, /project\.liveUrl/);
|
assert.match(source, /project\.liveUrl/);
|
||||||
assert.match(source, /caseStudyInput/);
|
assert.match(source, /caseStudyInput/);
|
||||||
|
assert.match(source, /closest\('a, button/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the case study drawer has accessible and pointer-safe dismissal paths', async () => {
|
test('the case study drawer has accessible and pointer-safe dismissal paths', async () => {
|
||||||
@@ -63,6 +64,8 @@ test('the case study drawer has accessible and pointer-safe dismissal paths', as
|
|||||||
assert.match(component, /setPointerCapture/);
|
assert.match(component, /setPointerCapture/);
|
||||||
assert.match(component, /navigator\.clipboard/);
|
assert.match(component, /navigator\.clipboard/);
|
||||||
assert.match(component, /aria-live="polite"/);
|
assert.match(component, /aria-live="polite"/);
|
||||||
|
assert.match(component, /data-case-study-initial-focus/);
|
||||||
|
assert.doesNotMatch(component, /data-case-study-heading/);
|
||||||
assert.match(styles, /::backdrop/);
|
assert.match(styles, /::backdrop/);
|
||||||
assert.match(styles, /env\(safe-area-inset-bottom/);
|
assert.match(styles, /env\(safe-area-inset-bottom/);
|
||||||
assert.match(styles, /prefers-reduced-motion:\s*reduce/);
|
assert.match(styles, /prefers-reduced-motion:\s*reduce/);
|
||||||
|
|||||||
Reference in New Issue
Block a user