@talelk/newui-organisms 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -4
- package/skills/newui-organisms/SKILL.md +91 -0
- package/src/exports.test.ts +3 -0
- package/src/organisms/AppSidebar/AppSidebar.a11y.test.tsx +75 -0
- package/src/organisms/CommandPalette/CommandPalette.a11y.test.tsx +58 -0
- package/src/organisms/DataState/DataState.a11y.test.tsx +59 -0
- package/src/organisms/DataState/DataState.browser.test.tsx +52 -0
- package/src/organisms/DataTable/DataTable.a11y.test.tsx +71 -0
- package/src/organisms/DataTable/DataTable.features.test.tsx +191 -0
- package/src/organisms/DataTable/DataTable.tsx +202 -20
- package/src/organisms/DetailHeader/DetailHeader.a11y.test.tsx +53 -0
- package/src/organisms/DetailHeader/DetailHeader.browser.test.tsx +50 -0
- package/src/organisms/FilterBar/FilterBar.a11y.test.tsx +58 -0
- package/src/organisms/FilterBar/FilterBar.browser.test.tsx +46 -0
- package/src/organisms/KanbanBoard/KanbanBoard.a11y.test.tsx +75 -0
- package/src/organisms/KanbanBoard/KanbanBoard.browser.test.tsx +84 -0
- package/src/organisms/MobileNav/MobileNav.a11y.test.tsx +49 -0
- package/src/organisms/Section/Section.a11y.test.tsx +49 -0
- package/src/organisms/Section/Section.browser.test.tsx +63 -0
- package/src/organisms/SelectionBar/SelectionBar.a11y.test.tsx +48 -0
- package/src/organisms/SelectionBar/SelectionBar.browser.test.tsx +57 -0
- package/src/organisms/Sidebar/Sidebar.a11y.test.tsx +71 -0
- package/src/organisms/VirtualList/VirtualList.a11y.test.tsx +109 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { render } from '@solidjs/testing-library'
|
|
2
|
+
import axe, { type AxeResults } from 'axe-core'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import { MobileNav, MobileNavItem } from './MobileNav'
|
|
5
|
+
|
|
6
|
+
const AXE_OPTIONS = {
|
|
7
|
+
rules: {
|
|
8
|
+
'color-contrast': { enabled: false },
|
|
9
|
+
region: { enabled: false },
|
|
10
|
+
},
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function summarize(results: AxeResults) {
|
|
14
|
+
return results.violations.map((violation) => ({
|
|
15
|
+
id: violation.id,
|
|
16
|
+
impact: violation.impact,
|
|
17
|
+
targets: violation.nodes.map((node) => node.target),
|
|
18
|
+
}))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('MobileNav accessibility', () => {
|
|
22
|
+
it('has no axe violations for a tab bar of links and buttons', async () => {
|
|
23
|
+
const { container } = render(() => (
|
|
24
|
+
<MobileNav aria-label="Primary">
|
|
25
|
+
<MobileNavItem
|
|
26
|
+
label="Home"
|
|
27
|
+
icon="lucide-house"
|
|
28
|
+
active
|
|
29
|
+
aria-current="page"
|
|
30
|
+
/>
|
|
31
|
+
<MobileNavItem label="Search" icon="lucide-search" route="/search" />
|
|
32
|
+
<MobileNavItem label="You" icon="lucide-user-round" />
|
|
33
|
+
</MobileNav>
|
|
34
|
+
))
|
|
35
|
+
expect(summarize(await axe.run(container, AXE_OPTIONS))).toEqual([])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('has no axe violations with custom content in place of an icon', async () => {
|
|
39
|
+
const { container } = render(() => (
|
|
40
|
+
<MobileNav aria-label="Primary">
|
|
41
|
+
<MobileNavItem label="Home" active>
|
|
42
|
+
<span>AC</span>
|
|
43
|
+
</MobileNavItem>
|
|
44
|
+
<MobileNavItem label="More" icon="lucide-ellipsis" />
|
|
45
|
+
</MobileNav>
|
|
46
|
+
))
|
|
47
|
+
expect(summarize(await axe.run(container, AXE_OPTIONS))).toEqual([])
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { render } from '@solidjs/testing-library'
|
|
2
|
+
import axe, { type AxeResults } from 'axe-core'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import { Section } from './Section'
|
|
5
|
+
|
|
6
|
+
const AXE_OPTIONS = {
|
|
7
|
+
rules: {
|
|
8
|
+
'color-contrast': { enabled: false },
|
|
9
|
+
region: { enabled: false },
|
|
10
|
+
},
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function summarize(results: AxeResults) {
|
|
14
|
+
return results.violations.map((violation) => ({
|
|
15
|
+
id: violation.id,
|
|
16
|
+
impact: violation.impact,
|
|
17
|
+
targets: violation.nodes.map((node) => node.target),
|
|
18
|
+
}))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('Section accessibility', () => {
|
|
22
|
+
it('has no axe violations for the full region anatomy', async () => {
|
|
23
|
+
const { container } = render(() => (
|
|
24
|
+
<Section
|
|
25
|
+
id="expenses"
|
|
26
|
+
title="Pending expenses"
|
|
27
|
+
description="Waiting for approval"
|
|
28
|
+
actions={<button type="button">Export</button>}
|
|
29
|
+
toolbar={<div>Filters</div>}
|
|
30
|
+
footer={<span>18 records</span>}
|
|
31
|
+
>
|
|
32
|
+
<ul>
|
|
33
|
+
<li>Taxi to client</li>
|
|
34
|
+
<li>Team lunch</li>
|
|
35
|
+
</ul>
|
|
36
|
+
</Section>
|
|
37
|
+
))
|
|
38
|
+
expect(summarize(await axe.run(container, AXE_OPTIONS))).toEqual([])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('has no axe violations when named by label alone', async () => {
|
|
42
|
+
const { container } = render(() => (
|
|
43
|
+
<Section label="Recent activity" headingLevel={3} surface="card">
|
|
44
|
+
<p>Nothing today.</p>
|
|
45
|
+
</Section>
|
|
46
|
+
))
|
|
47
|
+
expect(summarize(await axe.run(container, AXE_OPTIONS))).toEqual([])
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { render } from '@solidjs/testing-library'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { Section } from './Section'
|
|
4
|
+
|
|
5
|
+
describe('Section (browser, real CSS)', () => {
|
|
6
|
+
it('sits the footer under a real rule', () => {
|
|
7
|
+
const { container } = render(() => (
|
|
8
|
+
<Section title="Ledger" footer={<span>18 records</span>}>
|
|
9
|
+
<div>Body</div>
|
|
10
|
+
</Section>
|
|
11
|
+
))
|
|
12
|
+
const footer = Array.from(container.querySelectorAll('div')).find(
|
|
13
|
+
(node) => node.textContent === '18 records',
|
|
14
|
+
) as HTMLElement
|
|
15
|
+
const style = getComputedStyle(footer)
|
|
16
|
+
// "Pagination, totals or selection feedback, under a rule" — the rule is
|
|
17
|
+
// the contract; losing it strands the footer in the body.
|
|
18
|
+
expect(style.borderTopStyle).toBe('solid')
|
|
19
|
+
expect(Number.parseFloat(style.borderTopWidth)).toBeGreaterThan(0)
|
|
20
|
+
expect(Number.parseFloat(style.paddingTop)).toBeGreaterThan(0)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('paints the card surface and leaves the plain one bare', () => {
|
|
24
|
+
const card = render(() => (
|
|
25
|
+
<Section surface="card" title="Ledger">
|
|
26
|
+
<div>Body</div>
|
|
27
|
+
</Section>
|
|
28
|
+
))
|
|
29
|
+
const cardBox = card.container.querySelector(
|
|
30
|
+
'[data-slot="section"]',
|
|
31
|
+
) as HTMLElement
|
|
32
|
+
const cardStyle = getComputedStyle(cardBox)
|
|
33
|
+
expect(cardStyle.backgroundColor).not.toBe('rgba(0, 0, 0, 0)')
|
|
34
|
+
expect(Number.parseFloat(cardStyle.borderRadius)).toBeGreaterThan(0)
|
|
35
|
+
expect(Number.parseFloat(cardStyle.paddingTop)).toBeGreaterThan(0)
|
|
36
|
+
card.unmount()
|
|
37
|
+
|
|
38
|
+
const plain = render(() => (
|
|
39
|
+
<Section title="Ledger">
|
|
40
|
+
<div>Body</div>
|
|
41
|
+
</Section>
|
|
42
|
+
))
|
|
43
|
+
const plainBox = plain.container.querySelector(
|
|
44
|
+
'[data-slot="section"]',
|
|
45
|
+
) as HTMLElement
|
|
46
|
+
expect(getComputedStyle(plainBox).backgroundColor).toBe('rgba(0, 0, 0, 0)')
|
|
47
|
+
expect(getComputedStyle(plainBox).paddingTop).toBe('0px')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('renders the heading in its type scale', () => {
|
|
51
|
+
const { container } = render(() => (
|
|
52
|
+
<Section title="Pending expenses">
|
|
53
|
+
<div>Body</div>
|
|
54
|
+
</Section>
|
|
55
|
+
))
|
|
56
|
+
const heading = container.querySelector('h2') as HTMLElement
|
|
57
|
+
// `text-section` — a dropped scale utility drops the heading into body text.
|
|
58
|
+
expect(
|
|
59
|
+
Number.parseFloat(getComputedStyle(heading).fontSize),
|
|
60
|
+
).toBeGreaterThan(16)
|
|
61
|
+
expect(heading.getBoundingClientRect().height).toBeGreaterThan(0)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { render } from '@solidjs/testing-library'
|
|
2
|
+
import axe, { type AxeResults } from 'axe-core'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import { SelectionBar } from './SelectionBar'
|
|
5
|
+
|
|
6
|
+
const AXE_OPTIONS = {
|
|
7
|
+
rules: {
|
|
8
|
+
'color-contrast': { enabled: false },
|
|
9
|
+
region: { enabled: false },
|
|
10
|
+
},
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function summarize(results: AxeResults) {
|
|
14
|
+
return results.violations.map((violation) => ({
|
|
15
|
+
id: violation.id,
|
|
16
|
+
impact: violation.impact,
|
|
17
|
+
targets: violation.nodes.map((node) => node.target),
|
|
18
|
+
}))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('SelectionBar accessibility', () => {
|
|
22
|
+
it('has no axe violations with a primary action and clear', async () => {
|
|
23
|
+
const { container } = render(() => (
|
|
24
|
+
<SelectionBar
|
|
25
|
+
count={3}
|
|
26
|
+
summary="$14,650"
|
|
27
|
+
primary={{ label: 'Approve expenses', onClick: () => {} }}
|
|
28
|
+
onClear={() => {}}
|
|
29
|
+
/>
|
|
30
|
+
))
|
|
31
|
+
expect(summarize(await axe.run(container, AXE_OPTIONS))).toEqual([])
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('has no axe violations with a secondary control and no summary', async () => {
|
|
35
|
+
const { container } = render(() => (
|
|
36
|
+
<SelectionBar
|
|
37
|
+
count={12}
|
|
38
|
+
primary={{
|
|
39
|
+
label: 'Archive',
|
|
40
|
+
onClick: () => {},
|
|
41
|
+
disabled: true,
|
|
42
|
+
}}
|
|
43
|
+
secondary={<button type="button">Merge</button>}
|
|
44
|
+
/>
|
|
45
|
+
))
|
|
46
|
+
expect(summarize(await axe.run(container, AXE_OPTIONS))).toEqual([])
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { render } from '@solidjs/testing-library'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { SelectionBar } from './SelectionBar'
|
|
4
|
+
|
|
5
|
+
describe('SelectionBar (browser, real CSS)', () => {
|
|
6
|
+
it('paints a raised card over the data', () => {
|
|
7
|
+
const { container } = render(() => <SelectionBar count={3} />)
|
|
8
|
+
const bar = container.querySelector(
|
|
9
|
+
'[data-slot="selection-bar"]',
|
|
10
|
+
) as HTMLElement
|
|
11
|
+
const style = getComputedStyle(bar)
|
|
12
|
+
expect(style.backgroundColor).not.toBe('rgba(0, 0, 0, 0)')
|
|
13
|
+
// `shadow-sm` is what lifts it off the table it covers.
|
|
14
|
+
expect(style.boxShadow).not.toBe('none')
|
|
15
|
+
expect(Number.parseFloat(style.borderRadius)).toBeGreaterThan(0)
|
|
16
|
+
expect(bar.getBoundingClientRect().height).toBeGreaterThanOrEqual(24)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('sets the count in the medium interface weight', () => {
|
|
20
|
+
const { container } = render(() => (
|
|
21
|
+
<SelectionBar count={3} summary="$14,650" />
|
|
22
|
+
))
|
|
23
|
+
const count = Array.from(container.querySelectorAll('span')).find(
|
|
24
|
+
(node) => node.textContent === '3 selected',
|
|
25
|
+
) as HTMLElement
|
|
26
|
+
expect(
|
|
27
|
+
Number.parseInt(getComputedStyle(count).fontWeight, 10),
|
|
28
|
+
).toBeGreaterThanOrEqual(500)
|
|
29
|
+
// The summary is a quieter line: it must not match the count's colour.
|
|
30
|
+
const summary = Array.from(container.querySelectorAll('span')).find(
|
|
31
|
+
(node) => node.textContent === '$14,650',
|
|
32
|
+
) as HTMLElement
|
|
33
|
+
expect(getComputedStyle(summary).color).not.toBe(
|
|
34
|
+
getComputedStyle(count).color,
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('pins a sticky bar above the bottom edge on the harness phone viewport', () => {
|
|
39
|
+
// This project renders at 414px — below the `sm` breakpoint, which is
|
|
40
|
+
// exactly where `placement="sticky"` becomes `max-sm:fixed`.
|
|
41
|
+
const { container } = render(() => (
|
|
42
|
+
<SelectionBar count={3} placement="sticky" />
|
|
43
|
+
))
|
|
44
|
+
const bar = container.querySelector(
|
|
45
|
+
'[data-slot="selection-bar"]',
|
|
46
|
+
) as HTMLElement
|
|
47
|
+
expect(getComputedStyle(bar).position).toBe('fixed')
|
|
48
|
+
expect(Number.parseFloat(getComputedStyle(bar).bottom)).toBeGreaterThan(0)
|
|
49
|
+
|
|
50
|
+
// The inline default stays in flow at the same width.
|
|
51
|
+
const inline = render(() => <SelectionBar count={3} />)
|
|
52
|
+
const inlineBar = inline.container.querySelector(
|
|
53
|
+
'[data-slot="selection-bar"]',
|
|
54
|
+
) as HTMLElement
|
|
55
|
+
expect(getComputedStyle(inlineBar).position).not.toBe('fixed')
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { render } from '@solidjs/testing-library'
|
|
2
|
+
import axe, { type AxeResults } from 'axe-core'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import {
|
|
5
|
+
Sidebar,
|
|
6
|
+
SidebarCard,
|
|
7
|
+
SidebarHeader,
|
|
8
|
+
SidebarItem,
|
|
9
|
+
SidebarSearch,
|
|
10
|
+
SidebarSection,
|
|
11
|
+
SidebarTrigger,
|
|
12
|
+
} from './Sidebar'
|
|
13
|
+
|
|
14
|
+
const AXE_OPTIONS = {
|
|
15
|
+
rules: {
|
|
16
|
+
'color-contrast': { enabled: false },
|
|
17
|
+
region: { enabled: false },
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function summarize(results: AxeResults) {
|
|
22
|
+
return results.violations.map((violation) => ({
|
|
23
|
+
id: violation.id,
|
|
24
|
+
impact: violation.impact,
|
|
25
|
+
targets: violation.nodes.map((node) => node.target),
|
|
26
|
+
}))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('Sidebar accessibility', () => {
|
|
30
|
+
it('has no axe violations for an expanded workspace sidebar', async () => {
|
|
31
|
+
const { container } = render(() => (
|
|
32
|
+
<Sidebar ariaLabel="Main">
|
|
33
|
+
<SidebarHeader title="Acme" subtitle="Production" />
|
|
34
|
+
<SidebarTrigger />
|
|
35
|
+
<SidebarSearch placeholder="Filter pages" />
|
|
36
|
+
<SidebarItem route="/inbox" icon="lucide-inbox" active>
|
|
37
|
+
Inbox
|
|
38
|
+
</SidebarItem>
|
|
39
|
+
<SidebarItem route="/drafts" icon="lucide-file-pen">
|
|
40
|
+
Drafts
|
|
41
|
+
</SidebarItem>
|
|
42
|
+
<SidebarSection label="Projects" collapsible>
|
|
43
|
+
<SidebarItem route="/website">Website</SidebarItem>
|
|
44
|
+
<SidebarItem route="/app">App</SidebarItem>
|
|
45
|
+
</SidebarSection>
|
|
46
|
+
<SidebarCard
|
|
47
|
+
title="Trial ends soon"
|
|
48
|
+
description="Upgrade to keep your workspace."
|
|
49
|
+
dismissible
|
|
50
|
+
action={{ label: 'Upgrade' }}
|
|
51
|
+
/>
|
|
52
|
+
</Sidebar>
|
|
53
|
+
))
|
|
54
|
+
expect(summarize(await axe.run(container, AXE_OPTIONS))).toEqual([])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('has no axe violations for the collapsed rail of named icons', async () => {
|
|
58
|
+
const { container } = render(() => (
|
|
59
|
+
<Sidebar collapsed ariaLabel="Main">
|
|
60
|
+
<SidebarHeader title="Acme" logo="https://example.com/logo.png" />
|
|
61
|
+
<SidebarItem label="Inbox" route="/inbox" icon="lucide-inbox" active>
|
|
62
|
+
Inbox
|
|
63
|
+
</SidebarItem>
|
|
64
|
+
<SidebarItem label="Drafts" route="/drafts" icon="lucide-file-pen">
|
|
65
|
+
Drafts
|
|
66
|
+
</SidebarItem>
|
|
67
|
+
</Sidebar>
|
|
68
|
+
))
|
|
69
|
+
expect(summarize(await axe.run(container, AXE_OPTIONS))).toEqual([])
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { render, screen } from '@solidjs/testing-library'
|
|
2
|
+
import axe, { type AxeResults } from 'axe-core'
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { VirtualList } from './VirtualList'
|
|
5
|
+
|
|
6
|
+
const AXE_OPTIONS = {
|
|
7
|
+
rules: {
|
|
8
|
+
'color-contrast': { enabled: false },
|
|
9
|
+
region: { enabled: false },
|
|
10
|
+
},
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function summarize(results: AxeResults) {
|
|
14
|
+
return results.violations.map((violation) => ({
|
|
15
|
+
id: violation.id,
|
|
16
|
+
impact: violation.impact,
|
|
17
|
+
targets: violation.nodes.map((node) => node.target),
|
|
18
|
+
}))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ROWS = Array.from({ length: 1000 }, (_, index) => ({
|
|
22
|
+
id: `ROW-${index + 1}`,
|
|
23
|
+
title: `Row ${index + 1}`,
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
interface Row {
|
|
27
|
+
id: string
|
|
28
|
+
title: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const EMPTY: Row[] = []
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* jsdom has no layout, so the viewport reports a zero rect and the virtualiser
|
|
35
|
+
* renders nothing. The tests give every element a 440px box, which is the only
|
|
36
|
+
* measurement the list needs.
|
|
37
|
+
*/
|
|
38
|
+
class MeasuredObserver {
|
|
39
|
+
constructor(private readonly callback: ResizeObserverCallback) {}
|
|
40
|
+
observe(target: Element) {
|
|
41
|
+
const rect = target.getBoundingClientRect()
|
|
42
|
+
this.callback(
|
|
43
|
+
[
|
|
44
|
+
{
|
|
45
|
+
target,
|
|
46
|
+
contentRect: rect,
|
|
47
|
+
borderBoxSize: [{ inlineSize: rect.width, blockSize: rect.height }],
|
|
48
|
+
contentBoxSize: [{ inlineSize: rect.width, blockSize: rect.height }],
|
|
49
|
+
} as unknown as ResizeObserverEntry,
|
|
50
|
+
],
|
|
51
|
+
this as unknown as ResizeObserver,
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
unobserve() {}
|
|
55
|
+
disconnect() {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function withLayout() {
|
|
59
|
+
const rect = {
|
|
60
|
+
width: 600,
|
|
61
|
+
height: 440,
|
|
62
|
+
top: 0,
|
|
63
|
+
left: 0,
|
|
64
|
+
right: 600,
|
|
65
|
+
bottom: 440,
|
|
66
|
+
x: 0,
|
|
67
|
+
y: 0,
|
|
68
|
+
toJSON: () => ({}),
|
|
69
|
+
} as DOMRect
|
|
70
|
+
const spy = vi
|
|
71
|
+
.spyOn(HTMLElement.prototype, 'getBoundingClientRect')
|
|
72
|
+
.mockReturnValue(rect)
|
|
73
|
+
// The jsdom shim observes nothing; the virtualiser needs one measurement.
|
|
74
|
+
vi.stubGlobal('ResizeObserver', MeasuredObserver)
|
|
75
|
+
return spy
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
vi.unstubAllGlobals()
|
|
80
|
+
vi.restoreAllMocks()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('VirtualList accessibility', () => {
|
|
84
|
+
it('has no axe violations for a virtualised list of rows', async () => {
|
|
85
|
+
withLayout()
|
|
86
|
+
const { container } = render(() => (
|
|
87
|
+
<VirtualList label="Rows" items={ROWS} itemSize={72} height="440px">
|
|
88
|
+
{(row) => <span>{row.title}</span>}
|
|
89
|
+
</VirtualList>
|
|
90
|
+
))
|
|
91
|
+
expect(screen.getAllByRole('listitem').length).toBeGreaterThan(0)
|
|
92
|
+
expect(summarize(await axe.run(container, AXE_OPTIONS))).toEqual([])
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('has no axe violations for the empty state', async () => {
|
|
96
|
+
const { container } = render(() => (
|
|
97
|
+
<VirtualList
|
|
98
|
+
label="Rows"
|
|
99
|
+
items={EMPTY}
|
|
100
|
+
itemSize={72}
|
|
101
|
+
emptyTitle="No rows yet"
|
|
102
|
+
emptyDescription="Create the first one."
|
|
103
|
+
>
|
|
104
|
+
{(row) => <span>{row.title}</span>}
|
|
105
|
+
</VirtualList>
|
|
106
|
+
))
|
|
107
|
+
expect(summarize(await axe.run(container, AXE_OPTIONS))).toEqual([])
|
|
108
|
+
})
|
|
109
|
+
})
|