fix: keep navigation labels single and accessible
This commit is contained in:
@@ -8,7 +8,7 @@ const props = defineProps<{
|
|||||||
onSectionLinkClick: (event: MouseEvent) => void;
|
onSectionLinkClick: (event: MouseEvent) => void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const compactLabels: Readonly<Record<SectionId, string>> = {
|
const navigationLabels: Readonly<Record<SectionId, string>> = {
|
||||||
start: 'Start',
|
start: 'Start',
|
||||||
profile: 'Profile',
|
profile: 'Profile',
|
||||||
experience: 'Career',
|
experience: 'Career',
|
||||||
@@ -30,8 +30,7 @@ const compactLabels: Readonly<Record<SectionId, string>> = {
|
|||||||
props.journey.activeSectionId === item.id ? 'page' : undefined
|
props.journey.activeSectionId === item.id ? 'page' : undefined
|
||||||
"
|
"
|
||||||
@click="props.onSectionLinkClick"
|
@click="props.onSectionLinkClick"
|
||||||
><span class="site-navigation__full-label">{{ item.label }}</span
|
>{{ navigationLabels[item.id] }}</a
|
||||||
><span class="site-navigation__compact-label">{{ compactLabels[item.id] }}</span></a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -73,10 +73,6 @@
|
|||||||
outline-offset: -3px;
|
outline-offset: -3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-navigation__compact-label {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portfolio-main {
|
.portfolio-main {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding-block: var(--space-4) var(--space-5);
|
padding-block: var(--space-4) var(--space-5);
|
||||||
@@ -131,14 +127,6 @@
|
|||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-navigation__full-label {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-navigation__compact-label {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portfolio-section {
|
.portfolio-section {
|
||||||
scroll-margin-block-start: calc(
|
scroll-margin-block-start: calc(
|
||||||
var(--site-navigation-block-size) + var(--space-3)
|
var(--site-navigation-block-size) + var(--space-3)
|
||||||
|
|||||||
@@ -194,16 +194,15 @@ test("responsive navigation exposes the exact visible label as each link's acces
|
|||||||
await page.setViewportSize({ width: 1440, height: 900 });
|
await page.setViewportSize({ width: 1440, height: 900 });
|
||||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||||
|
|
||||||
await expect(page.getByRole("link", { name: "Experience", exact: true })).toBeVisible();
|
await expect(page.getByRole("link", { name: "Career", exact: true })).toBeVisible();
|
||||||
await expect(page.getByRole("link", { name: "Additional work", exact: true })).toBeVisible();
|
await expect(page.getByRole("link", { name: "More work", exact: true })).toBeVisible();
|
||||||
await expect(page.getByRole("link", { name: "Career", exact: true })).toHaveCount(0);
|
await expect(page.getByRole("link", { name: "Experience", exact: true })).toHaveCount(0);
|
||||||
await expect(page.getByRole("link", { name: "More work", exact: true })).toHaveCount(0);
|
await expect(page.getByRole("link", { name: "Additional work", exact: true })).toHaveCount(0);
|
||||||
|
|
||||||
await page.setViewportSize({ width: 320, height: 800 });
|
await page.setViewportSize({ width: 320, height: 800 });
|
||||||
const career = page.getByRole("link", { name: "Career", exact: true });
|
const career = page.getByRole("link", { name: "Career", exact: true });
|
||||||
const moreWork = page.getByRole("link", { name: "More work", exact: true });
|
await expect(career).toHaveText("Career");
|
||||||
await expect(career.locator(".site-navigation__compact-label")).toHaveText("Career");
|
await expect(page.getByRole("link", { name: "More work", exact: true })).toHaveText("More work");
|
||||||
await expect(moreWork.locator(".site-navigation__compact-label")).toHaveText("More work");
|
|
||||||
await expect(page.getByRole("link", { name: "Experience", exact: true })).toHaveCount(0);
|
await expect(page.getByRole("link", { name: "Experience", exact: true })).toHaveCount(0);
|
||||||
await expect(page.getByRole("link", { name: "Additional work", exact: true })).toHaveCount(0);
|
await expect(page.getByRole("link", { name: "Additional work", exact: true })).toHaveCount(0);
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,17 @@ const sectionHeadings = [
|
|||||||
'CONTACT',
|
'CONTACT',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const navigationLabels: Readonly<Record<(typeof sectionIds)[number], string>> = {
|
||||||
|
start: 'Start',
|
||||||
|
profile: 'Profile',
|
||||||
|
experience: 'Career',
|
||||||
|
infrastructure: 'Infra',
|
||||||
|
'selected-builds': 'Builds',
|
||||||
|
'additional-work': 'More work',
|
||||||
|
toolkit: 'Toolkit',
|
||||||
|
contact: 'Contact',
|
||||||
|
};
|
||||||
|
|
||||||
const visibleTextLeafSelector = 'h1,h2,h3,p,dt,dd,li,figcaption,a,svg text';
|
const visibleTextLeafSelector = 'h1,h2,h3,p,dt,dd,li,figcaption,a,svg text';
|
||||||
const visibleTextSelector = [
|
const visibleTextSelector = [
|
||||||
'.site-header :is(h1,h2,h3,p,dt,dd,li,figcaption,a)',
|
'.site-header :is(h1,h2,h3,p,dt,dd,li,figcaption,a)',
|
||||||
@@ -96,12 +107,7 @@ const expectedVisibleTextSegments = (
|
|||||||
return [
|
return [
|
||||||
content.identity.name,
|
content.identity.name,
|
||||||
content.identity.role,
|
content.identity.role,
|
||||||
...sectionIds.map((id) => {
|
...sectionIds.map((id) => navigationLabels[id]),
|
||||||
const label = navigationById.get(id);
|
|
||||||
if (label === undefined)
|
|
||||||
throw new Error(`Missing navigation label: ${id}`);
|
|
||||||
return label;
|
|
||||||
}),
|
|
||||||
...section('start', 1, [content.identity.valueProposition]),
|
...section('start', 1, [content.identity.valueProposition]),
|
||||||
...section('profile', 2, [
|
...section('profile', 2, [
|
||||||
content.profile.introduction,
|
content.profile.introduction,
|
||||||
|
|||||||
@@ -57,17 +57,17 @@ describe('semantic portfolio document', () => {
|
|||||||
expect(document.body.textContent).toContain('For a current CV or role-specific information, contact me directly.');
|
expect(document.body.textContent).toContain('For a current CV or role-specific information, contact me directly.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders separate full and compact navigation labels without overriding their accessible names', async () => {
|
it('renders one semantic navigation label with no duplicate responsive text', async () => {
|
||||||
const document = await renderedDocument();
|
const document = await renderedDocument();
|
||||||
const experience = document.querySelector('nav a[href="#experience"]')!;
|
const experience = document.querySelector('nav a[href="#experience"]')!;
|
||||||
const additional = document.querySelector('nav a[href="#additional-work"]')!;
|
const additional = document.querySelector('nav a[href="#additional-work"]')!;
|
||||||
|
|
||||||
expect(experience.hasAttribute('aria-label')).toBe(false);
|
expect(experience.hasAttribute('aria-label')).toBe(false);
|
||||||
expect(experience.querySelector('.site-navigation__full-label')?.textContent).toBe('Experience');
|
expect(experience.textContent).toBe('Career');
|
||||||
expect(experience.querySelector('.site-navigation__compact-label')?.textContent).toBe('Career');
|
expect(experience.childElementCount).toBe(0);
|
||||||
expect(additional.hasAttribute('aria-label')).toBe(false);
|
expect(additional.hasAttribute('aria-label')).toBe(false);
|
||||||
expect(additional.querySelector('.site-navigation__full-label')?.textContent).toBe('Additional work');
|
expect(additional.textContent).toBe('More work');
|
||||||
expect(additional.querySelector('.site-navigation__compact-label')?.textContent).toBe('More work');
|
expect(additional.childElementCount).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('binds navigation and section labels to their canonical IDs when content navigation is reordered', async () => {
|
it('binds navigation and section labels to their canonical IDs when content navigation is reordered', async () => {
|
||||||
@@ -91,12 +91,9 @@ describe('semantic portfolio document', () => {
|
|||||||
expect([...document.querySelectorAll('main > section > h2')].map((heading) => heading.textContent)).toEqual([
|
expect([...document.querySelectorAll('main > section > h2')].map((heading) => heading.textContent)).toEqual([
|
||||||
'START', 'PROFILE', 'EXPERIENCE', 'INFRASTRUCTURE', 'SELECTED BUILDS', 'ADDITIONAL WORK', 'TOOLKIT', 'CONTACT',
|
'START', 'PROFILE', 'EXPERIENCE', 'INFRASTRUCTURE', 'SELECTED BUILDS', 'ADDITIONAL WORK', 'TOOLKIT', 'CONTACT',
|
||||||
]);
|
]);
|
||||||
expect([...document.querySelectorAll('nav a')].map((link) => [
|
expect([...document.querySelectorAll('nav a')].map((link) => [link.getAttribute('href'), link.textContent])).toEqual([
|
||||||
link.getAttribute('href'),
|
['#start', 'Start'], ['#profile', 'Profile'], ['#experience', 'Career'], ['#infrastructure', 'Infra'],
|
||||||
link.querySelector('.site-navigation__full-label')?.textContent,
|
['#selected-builds', 'Builds'], ['#additional-work', 'More work'], ['#toolkit', 'Toolkit'], ['#contact', 'Contact'],
|
||||||
])).toEqual([
|
|
||||||
['#start', 'Start'], ['#profile', 'Profile'], ['#experience', 'Experience'], ['#infrastructure', 'Infrastructure'],
|
|
||||||
['#selected-builds', 'Selected builds'], ['#additional-work', 'Additional work'], ['#toolkit', 'Toolkit'], ['#contact', 'Contact'],
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user