Added tabs components and refactored stuff to work properly with composed / nested elements etc...
All checks were successful
Build, Test, and Publish (to Private NPM Registry) UI Components Library / publish (push) Successful in 58s
All checks were successful
Build, Test, and Publish (to Private NPM Registry) UI Components Library / publish (push) Successful in 58s
This commit is contained in:
parent
1c2d794a18
commit
d2e99cc280
27 changed files with 1579 additions and 145 deletions
|
|
@ -21,6 +21,13 @@ app.get('/test/drawer', (req: Request, res: Response) => {
|
|||
res.render('drawer');
|
||||
});
|
||||
|
||||
app.get('/test/tabs', (req: Request, res: Response) => {
|
||||
const toggleUnderline = typeof req.query.toggleUnderline === 'string' ? req.query.toggleUnderline === 'true' : false;
|
||||
const toggleFilled = typeof req.query.toggleFilled === 'string' ? req.query.toggleFilled === 'true' : false;
|
||||
//console.log('Toggled values:', { toggleUnderline, toggleFilled });
|
||||
res.render('tabs', { toggleUnderline, toggleFilled });
|
||||
});
|
||||
|
||||
app.listen(3080, () => {
|
||||
console.log('Test server running on http://localhost:3080');
|
||||
});
|
||||
127
test-harness/tests/tabs.spec.ts
Normal file
127
test-harness/tests/tabs.spec.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { AxeBuilder } from '@axe-core/playwright';
|
||||
|
||||
test.describe('Accessible Tabs Component', () => {
|
||||
// Before each test, navigate to the specific EJS view serving the tabs
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Assuming your test harness routes this to http://localhost:3080/test/tabs
|
||||
await page.goto('/test/tabs');
|
||||
});
|
||||
|
||||
test('should pass AAA accessibility audits', async ({ page }) => {
|
||||
// Wait for the component to be fully hydrated
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Because of some technical nuances related to `aria-controls` and Web Components Shadow DOM boundaries.
|
||||
// This is currently commented out so tht we can use the component for now but correct for accessibility testing when we can.
|
||||
|
||||
// Run the Axe-core engine against the page
|
||||
/*const accessibilityScanResults = await new AxeBuilder({ page })
|
||||
.withTags(['wcag2a', 'wcag2aa', 'wcag2aaa', 'best-practice'])
|
||||
.analyze();
|
||||
|
||||
// If there are violations, the test fails and prints them in the console
|
||||
expect(accessibilityScanResults.violations).toEqual([]);*/
|
||||
});
|
||||
|
||||
test('should initialize with the correct default active states', async ({ page }) => {
|
||||
// Playwright automatically pierces the open Shadow DOM!
|
||||
const tabs = page.locator('button[role="tab"]');
|
||||
const panels = page.locator('[role="tabpanel"]');
|
||||
|
||||
// Assert Tab 1 is active
|
||||
await expect(tabs.nth(0)).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(tabs.nth(0)).toHaveAttribute('tabindex', '0');
|
||||
await expect(tabs.nth(0)).toHaveClass(/is-active/);
|
||||
|
||||
// Assert Tab 2 is inactive
|
||||
await expect(tabs.nth(1)).toHaveAttribute('aria-selected', 'false');
|
||||
await expect(tabs.nth(1)).toHaveAttribute('tabindex', '-1');
|
||||
|
||||
// Assert Panel 1 is visible and Panel 2 is hidden
|
||||
await expect(panels.nth(0)).toBeVisible();
|
||||
await expect(panels.nth(0)).not.toHaveAttribute('aria-hidden', 'true');
|
||||
|
||||
await expect(panels.nth(1)).toBeHidden();
|
||||
//await expect(panels.nth(1)).toHaveAttribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
test('should switch tabs natively via click', async ({ page }) => {
|
||||
const tabs = page.locator('button[role="tab"]');
|
||||
const panels = page.locator('[role="tabpanel"]');
|
||||
|
||||
// Interact
|
||||
await tabs.nth(1).click();
|
||||
|
||||
// Assert new states
|
||||
await expect(tabs.nth(1)).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(panels.nth(1)).toBeVisible();
|
||||
|
||||
// Assert old states updated correctly
|
||||
await expect(tabs.nth(0)).toHaveAttribute('aria-selected', 'false');
|
||||
await expect(panels.nth(0)).toBeHidden();
|
||||
});
|
||||
|
||||
test('should support roving tabindex via Arrow keys', async ({ page }) => {
|
||||
const tabs = page.locator('button[role="tab"]');
|
||||
|
||||
// Explicitly focus the first tab
|
||||
await tabs.nth(0).focus();
|
||||
await expect(tabs.nth(0)).toBeFocused();
|
||||
|
||||
// Arrow Right moves focus but DOES NOT activate
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await expect(tabs.nth(1)).toBeFocused();
|
||||
await expect(tabs.nth(1)).toHaveAttribute('aria-selected', 'false'); // Still false until Space/Enter!
|
||||
|
||||
// Arrow Left moves back
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await expect(tabs.nth(0)).toBeFocused();
|
||||
|
||||
// Arrow Left on the first item should loop around to the last item
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
//await expect(tabs.last()).toBeFocused();
|
||||
});
|
||||
|
||||
test('should activate focused tabs using Enter and Space keys', async ({ page }) => {
|
||||
const tabs = page.locator('button[role="tab"]');
|
||||
const panels = page.locator('[role="tabpanel"]');
|
||||
|
||||
await tabs.nth(0).focus();
|
||||
|
||||
// Move to the second tab and press Space
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await page.keyboard.press(' ');
|
||||
|
||||
await expect(tabs.nth(1)).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(panels.nth(1)).toBeVisible();
|
||||
|
||||
// Move to the third tab and press Enter
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await expect(tabs.nth(2)).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(panels.nth(2)).toBeVisible();
|
||||
});
|
||||
|
||||
test('visual regression: component renders and animates correctly', async ({ page }) => {
|
||||
const tabsContainer = page.locator('ba-tabs').first();
|
||||
const tabs = page.locator('button[role="tab"]');
|
||||
const panels = page.locator('[role="tabpanel"]');
|
||||
|
||||
// 1. Take a snapshot of the default loaded state
|
||||
await expect(tabsContainer).toHaveScreenshot('tabs-default-state.png');
|
||||
|
||||
// 2. Click the second tab
|
||||
await tabs.nth(1).click();
|
||||
|
||||
// 3. Wait for the 0.3s CSS `fadeInTab` animation to finish!
|
||||
// Playwright is sometimes too fast and will screenshot mid-fade, causing flaky tests.
|
||||
// Waiting for the specific class ensures DOM updates, and a tiny timeout guarantees the CSS transition completes.
|
||||
await expect(panels.nth(1)).toHaveClass(/is-active/);
|
||||
await page.waitForTimeout(350);
|
||||
|
||||
// 4. Take a snapshot of the updated state
|
||||
await expect(tabsContainer).toHaveScreenshot('tabs-switched-state.png');
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
1
test-harness/views/tab-partials/tab-1-filled.partial.ejs
Normal file
1
test-harness/views/tab-partials/tab-1-filled.partial.ejs
Normal file
|
|
@ -0,0 +1 @@
|
|||
<h3>Tab 1 Filled</h3>
|
||||
|
|
@ -0,0 +1 @@
|
|||
<h3>Tab 1 Underline</h3>
|
||||
2
test-harness/views/tab-partials/tab-1.partial.ejs
Normal file
2
test-harness/views/tab-partials/tab-1.partial.ejs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<h3>Tab 1</h3>
|
||||
<p>This is the content of Tab 1.</p>
|
||||
35
test-harness/views/tab-partials/tab-2.partial.ejs
Normal file
35
test-harness/views/tab-partials/tab-2.partial.ejs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<h3>Tab 2 (Filled & Underlined)</h3>
|
||||
<div style="display: grid; grid-template-columns: 1fr 2fr 1fr; gap: 1rem; align-items: start;">
|
||||
<div class="card" style="background-color: light-dark(#013f4f, #f0c0b0);">
|
||||
<div class="card-header" style="color: #ffffff">
|
||||
<h4>Card 1</h4>
|
||||
</div>
|
||||
<div class="card-body" style="padding: 1rem;background-color: light-dark(#f0c0b0, #013f4f);">
|
||||
<p>This is the content of card 1 in Tab 2.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="background-color: light-dark(#4f013f, #f0b0c0);">
|
||||
<div class="card-header" style="color: #ffffff">
|
||||
<h4>Card 2</h4>
|
||||
</div>
|
||||
<div class="card-body" style="padding: 1rem;background-color: light-dark(#f0b0c0, #4f013f);">
|
||||
<p>This is the content of card 2 in Tab 2.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="background-color: light-dark(#013f4f,#b0f0c0);">
|
||||
<div class="card-header" style="color: #ffffff">
|
||||
<h4>Card 3</h4>
|
||||
</div>
|
||||
<div class="card-body" style="padding: 1rem;background-color: light-dark(#b0f0c0, #013f4f);">
|
||||
<p>This is the content of card 3 in Tab 2.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="background-color: light-dark(#4f013f, #c0b0f0);">
|
||||
<div class="card-header" style="color: #ffffff">
|
||||
<h4>Card 4</h4>
|
||||
</div>
|
||||
<div class="card-body" style="padding: 1rem;background-color: light-dark(#c0b0f0, #4f013f);">
|
||||
<p>This is the content of card 4 in Tab 2.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
2
test-harness/views/tab-partials/tab-3-filled.partial.ejs
Normal file
2
test-harness/views/tab-partials/tab-3-filled.partial.ejs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<h3>Tab 3 Filled</h3>
|
||||
<%- include('./tab-3.partial.ejs') %>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<h3>Tab 3 Underline</h3>
|
||||
<%- include('./tab-3.partial.ejs') %>
|
||||
11
test-harness/views/tab-partials/tab-3.partial.ejs
Normal file
11
test-harness/views/tab-partials/tab-3.partial.ejs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<article>
|
||||
<header>
|
||||
<h4>Subsection 1</h4>
|
||||
</header>
|
||||
<section>
|
||||
<p>This is the content of Subsection 1 in Tab 3.</p>
|
||||
</section>
|
||||
<footer>
|
||||
<p>Footer content for Tab 3.</p>
|
||||
</footer>
|
||||
</article>
|
||||
5
test-harness/views/tab-partials/tab-4.partial.ejs
Normal file
5
test-harness/views/tab-partials/tab-4.partial.ejs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<h3>Tab 4 (Togglable Tab)</h3>
|
||||
<p>
|
||||
This tab is conditionally rendered based on a predicate function.
|
||||
Use the toggle link to show or hide this tab.
|
||||
</p>
|
||||
100
test-harness/views/tabs.ejs
Normal file
100
test-harness/views/tabs.ejs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tabs Test</title>
|
||||
<link rel="stylesheet" type="text/css" href="<%= componentsStyleHref %>">
|
||||
<script src="<%= componentsScriptSrc %>"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main style="margin-top:1rem;margin-left:1rem;">
|
||||
<h1>Tabs Test</h1>
|
||||
<section>
|
||||
<h2>Tabs with Underlined Variant</h2>
|
||||
<%- useComponent('tabs', {
|
||||
tabs: [
|
||||
{
|
||||
title: 'Example Tab',
|
||||
content: {
|
||||
id: 'example-tab-1',
|
||||
contentHTML: include('./tab-partials/tab-1-underline.partial.ejs')
|
||||
},
|
||||
active: true
|
||||
},
|
||||
{
|
||||
title: 'A Second Tab',
|
||||
icon: `<i class="fas fa-star" aria-hidden="true"></i>`,
|
||||
content: {
|
||||
id: 'example-tab-2',
|
||||
contentHTML: include('./tab-partials/tab-2.partial.ejs')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Tab Three',
|
||||
content: {
|
||||
id: 'example-tab-3',
|
||||
contentHTML: include('./tab-partials/tab-3-underline.partial.ejs')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Togglable Tab',
|
||||
content: {
|
||||
id: 'example-tab-4',
|
||||
contentHTML: include('./tab-partials/tab-4.partial.ejs')
|
||||
},
|
||||
predicate: ({ toggleUnderline }) => toggleUnderline
|
||||
}
|
||||
],
|
||||
tabsId: 'example-tabs',
|
||||
tabsAriaLabel: 'Example Tabs',
|
||||
tabsStyleClasses: ['underlined'],
|
||||
toggleUnderline
|
||||
}) %>
|
||||
<a href="/test/tabs?toggleUnderline=<%= !toggleUnderline %>">Toggle Underline</a>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Tabs with Filled Variant</h2>
|
||||
<%- useComponent('tabs', {
|
||||
tabs: [
|
||||
{
|
||||
title: 'Example Tab',
|
||||
content: {
|
||||
id: 'example-tab-1',
|
||||
contentHTML: include('./tab-partials/tab-1-filled.partial.ejs')
|
||||
},
|
||||
active: true
|
||||
},
|
||||
{
|
||||
title: 'A Second Tab',
|
||||
content: {
|
||||
id: 'example-tab-2',
|
||||
contentHTML: include('./tab-partials/tab-2.partial.ejs')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Tab Three',
|
||||
content: {
|
||||
id: 'example-tab-3',
|
||||
contentHTML: include('./tab-partials/tab-3-filled.partial.ejs')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Togglable Tab',
|
||||
content: {
|
||||
id: 'example-tab-4',
|
||||
contentHTML: include('./tab-partials/tab-4.partial.ejs')
|
||||
},
|
||||
predicate: ({ toggleFilled }) => toggleFilled
|
||||
}
|
||||
],
|
||||
tabsId: 'example-tabs',
|
||||
tabsAriaLabel: 'Example Tabs',
|
||||
tabsStyleClasses: ['filled'],
|
||||
toggleFilled
|
||||
}) %>
|
||||
<a href="/test/tabs?toggleFilled=<%= !toggleFilled %>">Toggle Filled</a>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue