@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.
@@ -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
+ })