@talelk/newui-organisms 0.0.0-stage → 2.0.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.
Files changed (64) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +12 -2
  3. package/package.json +38 -3
  4. package/src/exports.test.ts +27 -0
  5. package/src/index.ts +1 -0
  6. package/src/organisms/AppSidebar/AppSidebar.test.tsx +136 -0
  7. package/src/organisms/AppSidebar/AppSidebar.tsx +259 -0
  8. package/src/organisms/AppSidebar/index.ts +1 -0
  9. package/src/organisms/AuthCard/AuthCard.a11y.test.tsx +36 -0
  10. package/src/organisms/AuthCard/AuthCard.test.tsx +56 -0
  11. package/src/organisms/AuthCard/AuthCard.tsx +89 -0
  12. package/src/organisms/AuthCard/index.ts +1 -0
  13. package/src/organisms/CommandPalette/CommandPalette.test.tsx +196 -0
  14. package/src/organisms/CommandPalette/CommandPalette.tsx +468 -0
  15. package/src/organisms/CommandPalette/index.ts +1 -0
  16. package/src/organisms/DataState/DataState.test.tsx +45 -0
  17. package/src/organisms/DataState/DataState.tsx +111 -0
  18. package/src/organisms/DataState/index.ts +6 -0
  19. package/src/organisms/DataTable/DataTable.test.tsx +161 -0
  20. package/src/organisms/DataTable/DataTable.tsx +612 -0
  21. package/src/organisms/DataTable/index.ts +1 -0
  22. package/src/organisms/DetailHeader/DetailHeader.test.tsx +55 -0
  23. package/src/organisms/DetailHeader/DetailHeader.tsx +95 -0
  24. package/src/organisms/DetailHeader/index.ts +1 -0
  25. package/src/organisms/FilterBar/FilterBar.test.tsx +152 -0
  26. package/src/organisms/FilterBar/FilterBar.tsx +251 -0
  27. package/src/organisms/FilterBar/index.ts +1 -0
  28. package/src/organisms/KanbanBoard/KanbanBoard.test.tsx +255 -0
  29. package/src/organisms/KanbanBoard/KanbanBoard.tsx +605 -0
  30. package/src/organisms/KanbanBoard/drag.test.ts +448 -0
  31. package/src/organisms/KanbanBoard/drag.ts +407 -0
  32. package/src/organisms/KanbanBoard/index.ts +17 -0
  33. package/src/organisms/MobileNav/MobileNav.browser.test.tsx +39 -0
  34. package/src/organisms/MobileNav/MobileNav.test.tsx +108 -0
  35. package/src/organisms/MobileNav/MobileNav.tsx +120 -0
  36. package/src/organisms/MobileNav/index.ts +6 -0
  37. package/src/organisms/PageToolbar/PageToolbar.a11y.test.tsx +35 -0
  38. package/src/organisms/PageToolbar/PageToolbar.test.tsx +42 -0
  39. package/src/organisms/PageToolbar/PageToolbar.tsx +126 -0
  40. package/src/organisms/PageToolbar/index.ts +1 -0
  41. package/src/organisms/Section/Section.test.tsx +41 -0
  42. package/src/organisms/Section/Section.tsx +108 -0
  43. package/src/organisms/Section/index.ts +1 -0
  44. package/src/organisms/SelectionBar/SelectionBar.test.tsx +35 -0
  45. package/src/organisms/SelectionBar/SelectionBar.tsx +84 -0
  46. package/src/organisms/SelectionBar/index.ts +5 -0
  47. package/src/organisms/SettingsDialog/SettingsDialog.a11y.test.tsx +58 -0
  48. package/src/organisms/SettingsDialog/SettingsDialog.browser.test.tsx +43 -0
  49. package/src/organisms/SettingsDialog/SettingsDialog.ssr.test.tsx +27 -0
  50. package/src/organisms/SettingsDialog/SettingsDialog.test.tsx +202 -0
  51. package/src/organisms/SettingsDialog/SettingsDialog.tsx +548 -0
  52. package/src/organisms/SettingsDialog/index.ts +20 -0
  53. package/src/organisms/Sidebar/Sidebar.browser.test.tsx +52 -0
  54. package/src/organisms/Sidebar/Sidebar.test.tsx +308 -0
  55. package/src/organisms/Sidebar/Sidebar.tsx +732 -0
  56. package/src/organisms/Sidebar/index.ts +22 -0
  57. package/src/organisms/StatGrid/StatGrid.a11y.test.tsx +40 -0
  58. package/src/organisms/StatGrid/StatGrid.test.tsx +95 -0
  59. package/src/organisms/StatGrid/StatGrid.tsx +151 -0
  60. package/src/organisms/StatGrid/index.ts +1 -0
  61. package/src/organisms/VirtualList/VirtualList.test.tsx +80 -0
  62. package/src/organisms/VirtualList/VirtualList.tsx +113 -0
  63. package/src/organisms/VirtualList/index.ts +1 -0
  64. package/src/organisms/index.ts +16 -0
@@ -0,0 +1,35 @@
1
+ import { render } from '@solidjs/testing-library'
2
+ import axe, { type AxeResults } from 'axe-core'
3
+ import { describe, expect, it } from 'vitest'
4
+ import { PageToolbar } from './PageToolbar'
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('PageToolbar accessibility', () => {
22
+ it('has no axe violations with a menu and a primary action', async () => {
23
+ const { container } = render(() => (
24
+ <PageToolbar
25
+ title="Q4 invoices"
26
+ menu={[
27
+ { label: 'Rename', onClick: () => {} },
28
+ { label: 'Archive', onClick: () => {} },
29
+ ]}
30
+ action={{ label: 'New invoice', onClick: () => {} }}
31
+ />
32
+ ))
33
+ expect(summarize(await axe.run(container, AXE_OPTIONS))).toEqual([])
34
+ })
35
+ })
@@ -0,0 +1,42 @@
1
+ import { render, screen } from '@solidjs/testing-library'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { describe, expect, it, vi } from 'vitest'
4
+ import { PageToolbar } from './PageToolbar'
5
+
6
+ describe('PageToolbar', () => {
7
+ it('renders the title and the primary action', () => {
8
+ render(() => <PageToolbar title="Tickets" action={{ label: 'Add new' }} />)
9
+ expect(screen.getByText('Tickets')).toBeTruthy()
10
+ expect(screen.getByRole('button', { name: /Add new/ })).toBeTruthy()
11
+ })
12
+
13
+ it('calls the primary action', async () => {
14
+ const onClick = vi.fn()
15
+ render(() => (
16
+ <PageToolbar title="Tickets" action={{ label: 'Add new', onClick }} />
17
+ ))
18
+ await userEvent.click(screen.getByRole('button', { name: /Add new/ }))
19
+ expect(onClick).toHaveBeenCalled()
20
+ })
21
+
22
+ it('renders no title menu unless one is given', () => {
23
+ const { container } = render(() => <PageToolbar title="Tickets" />)
24
+ expect(container.querySelector('[data-slot="menu-trigger"]')).toBeNull()
25
+ })
26
+
27
+ it('opens the title menu and runs the chosen entry', async () => {
28
+ const onArchive = vi.fn()
29
+ render(() => (
30
+ <PageToolbar
31
+ title="Tickets"
32
+ menu={[{ label: 'Archive', onClick: onArchive }]}
33
+ />
34
+ ))
35
+
36
+ await userEvent.click(screen.getByRole('button', { name: 'Page actions' }))
37
+ await userEvent.click(
38
+ await screen.findByRole('menuitem', { name: /Archive/ }),
39
+ )
40
+ expect(onArchive).toHaveBeenCalled()
41
+ })
42
+ })
@@ -0,0 +1,126 @@
1
+ import { Button, cn, Icon, type IconProp } from '@talelk/newui-atoms'
2
+ import { Menu } from '@talelk/newui-molecules'
3
+ import { For, type JSX, Show } from 'solid-js'
4
+ import { Dynamic } from 'solid-js/web'
5
+
6
+ export interface PageToolbarAction {
7
+ label: string
8
+ /** A `lucide-*` string or a component (P11). No icon is implied. */
9
+ icon?: IconProp
10
+ onClick?: () => void
11
+ loading?: boolean
12
+ disabled?: boolean
13
+ }
14
+
15
+ export interface PageToolbarProps {
16
+ title: JSX.Element
17
+ /** Semantic level of the title. @default 1 */
18
+ headingLevel?: 1 | 2 | 3 | 4
19
+ /** Options for the ellipsis menu beside the title. Omitted → no menu. */
20
+ menu?: PageToolbarAction[]
21
+ /** Secondary actions, before the primary. */
22
+ secondary?: PageToolbarAction[]
23
+ /** The primary action — the one thing this screen is for. */
24
+ action?: PageToolbarAction
25
+ /** Anything else on the trailing edge, after the primary action. */
26
+ children?: JSX.Element
27
+ class?: string
28
+ }
29
+
30
+ /**
31
+ * The page header: a title, an optional title menu, and the actions.
32
+ *
33
+ * Small, but it is the three details inside it that were being re-decided on
34
+ * every screen: the title menu is an ellipsis *button* (a chevron next to a
35
+ * title reads as a dropdown for the title, which is exactly what it is — but
36
+ * the ellipsis is the convention), the primary action carries a leading `+` only
37
+ * when it creates something, and the header is `PageHeader` rather than a bare
38
+ * sticky div so it flips to the pinned mobile variant with the shell.
39
+ */
40
+ export function PageToolbar(props: PageToolbarProps): JSX.Element {
41
+ return (
42
+ <header
43
+ class={`flex min-h-12 shrink-0 items-center justify-between gap-control-gap border-b border-outline-gray-1 bg-surface-base px-page-gutter-compact sm:px-page-gutter ${props.class ?? ''}`}
44
+ >
45
+ <div class="flex min-w-0 items-center gap-1">
46
+ <Dynamic
47
+ component={`h${props.headingLevel ?? 1}`}
48
+ class={cn(
49
+ 'truncate text-ink-gray-9',
50
+ (props.headingLevel ?? 1) === 1 ? 'text-heading' : 'text-section',
51
+ )}
52
+ >
53
+ {props.title}
54
+ </Dynamic>
55
+ <Show when={props.menu}>
56
+ {(menu) => (
57
+ <Menu.Root
58
+ onSelect={(value) =>
59
+ menu()
60
+ .find((entry) => entry.label === value)
61
+ ?.onClick?.()
62
+ }
63
+ >
64
+ <Menu.Trigger
65
+ aria-label="Page actions"
66
+ class="grid size-7 shrink-0 place-items-center rounded-3 text-ink-gray-6 transition-colors hover:bg-surface-gray-2 hover:text-ink-gray-9 focus-visible:focus-ring"
67
+ >
68
+ <Icon icon="lucide-ellipsis" class="size-4" />
69
+ </Menu.Trigger>
70
+ <Menu.Content>
71
+ <For each={menu()}>
72
+ {(entry) => (
73
+ <Menu.Item value={entry.label}>
74
+ <Show when={entry.icon}>
75
+ {(icon) => (
76
+ <Icon
77
+ icon={icon()}
78
+ class="size-4 shrink-0 text-ink-gray-6"
79
+ />
80
+ )}
81
+ </Show>
82
+ <span>{entry.label}</span>
83
+ </Menu.Item>
84
+ )}
85
+ </For>
86
+ </Menu.Content>
87
+ </Menu.Root>
88
+ )}
89
+ </Show>
90
+ </div>
91
+
92
+ <div class="flex shrink-0 items-center gap-control-gap">
93
+ {props.children}
94
+ <Show when={props.secondary}>
95
+ <For each={props.secondary}>
96
+ {(action) => (
97
+ <Button
98
+ variant="outline"
99
+ iconLeft={action.icon}
100
+ loading={action.loading}
101
+ disabled={action.disabled}
102
+ onClick={action.onClick}
103
+ >
104
+ {action.label}
105
+ </Button>
106
+ )}
107
+ </For>
108
+ </Show>
109
+ <Show when={props.action}>
110
+ {(action) => (
111
+ <Button
112
+ variant="solid"
113
+ theme="blue"
114
+ iconLeft={action().icon}
115
+ loading={action().loading}
116
+ disabled={action().disabled}
117
+ onClick={action().onClick}
118
+ >
119
+ {action().label}
120
+ </Button>
121
+ )}
122
+ </Show>
123
+ </div>
124
+ </header>
125
+ )
126
+ }
@@ -0,0 +1 @@
1
+ export * from './PageToolbar'
@@ -0,0 +1,41 @@
1
+ import { render, screen } from '@solidjs/testing-library'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { Section } from './Section'
4
+
5
+ describe('Section', () => {
6
+ it('renders a default H2 and the four region slots', () => {
7
+ render(() => (
8
+ <Section
9
+ title="Pending expenses"
10
+ description="Waiting for approval"
11
+ actions={<button type="button">Export</button>}
12
+ toolbar={<div>Filters</div>}
13
+ footer={<span>18 records</span>}
14
+ >
15
+ <div>Body</div>
16
+ </Section>
17
+ ))
18
+ expect(screen.getByRole('heading', { level: 2 }).textContent).toBe(
19
+ 'Pending expenses',
20
+ )
21
+ expect(screen.getByText('Waiting for approval')).toBeTruthy()
22
+ expect(screen.getByRole('button', { name: 'Export' })).toBeTruthy()
23
+ expect(screen.getByText('Filters')).toBeTruthy()
24
+ expect(screen.getByText('Body')).toBeTruthy()
25
+ expect(screen.getByText('18 records')).toBeTruthy()
26
+ })
27
+
28
+ it('honours the heading level and compact density', () => {
29
+ const { container } = render(() => (
30
+ <Section title="Ledger" headingLevel={3} density="compact">
31
+ <div />
32
+ </Section>
33
+ ))
34
+ expect(screen.getByRole('heading', { level: 3 }).textContent).toBe('Ledger')
35
+ expect(
36
+ container
37
+ .querySelector('[data-slot="section"]')
38
+ ?.getAttribute('data-density'),
39
+ ).toBe('compact')
40
+ })
41
+ })
@@ -0,0 +1,108 @@
1
+ import { cn } from '@talelk/newui-atoms'
2
+ import type { JSX } from 'solid-js'
3
+ import { Show } from 'solid-js'
4
+ import { Dynamic } from 'solid-js/web'
5
+
6
+ export type SectionHeadingLevel = 1 | 2 | 3 | 4
7
+
8
+ export interface SectionProps {
9
+ title?: JSX.Element
10
+ description?: JSX.Element
11
+ /** Page or section actions, rendered opposite the title. */
12
+ actions?: JSX.Element
13
+ /** Controls that change the data, between the header and the body. */
14
+ toolbar?: JSX.Element
15
+ /** Pagination, totals or selection feedback, under a rule. */
16
+ footer?: JSX.Element
17
+ /** @default 2 */
18
+ headingLevel?: SectionHeadingLevel
19
+ /** Explicit id for the rendered heading. Defaults to `<id>-title`. */
20
+ headingId?: string
21
+ /** Accessible name when the section has no visible title. */
22
+ label?: string
23
+ /** Id of the element that names the region; beats `label`/the heading id. */
24
+ labelledBy?: string
25
+ /** Surface treatment. @default 'plain' */
26
+ surface?: 'plain' | 'card' | 'inset'
27
+ /** @default 'comfortable' */
28
+ density?: 'comfortable' | 'compact'
29
+ id?: string
30
+ class?: string
31
+ children: JSX.Element
32
+ }
33
+
34
+ const HEADING: Record<SectionHeadingLevel, string> = {
35
+ 1: 'text-heading',
36
+ 2: 'text-section',
37
+ 3: 'text-base-medium',
38
+ 4: 'text-interface font-medium',
39
+ }
40
+
41
+ /**
42
+ * One page-region anatomy: header, optional toolbar, body, optional footer.
43
+ *
44
+ * Data organisms should not each invent their own title row or footer rule.
45
+ * `Section` owns the region; the organism owns the data surface inside it.
46
+ */
47
+ export function Section(props: SectionProps): JSX.Element {
48
+ const surface = () => props.surface ?? 'plain'
49
+ const density = () => props.density ?? 'comfortable'
50
+ const hasHeader = () =>
51
+ Boolean(props.title || props.description || props.actions)
52
+ const headingId = () =>
53
+ props.headingId ?? (props.id ? `${props.id}-title` : undefined)
54
+ const labelledBy = () =>
55
+ props.labelledBy ?? (props.title ? headingId() : undefined)
56
+ return (
57
+ <section
58
+ id={props.id}
59
+ aria-label={props.label}
60
+ aria-labelledby={labelledBy()}
61
+ data-slot="section"
62
+ data-surface={surface()}
63
+ data-density={density()}
64
+ class={cn(
65
+ 'flex min-w-0 flex-col',
66
+ density() === 'compact' ? 'gap-2' : 'gap-3',
67
+ surface() === 'card' &&
68
+ 'rounded-card border border-outline-gray-1 bg-surface-elevation-1 p-card-pad',
69
+ surface() === 'inset' && 'rounded-card bg-surface-gray-1 p-card-pad',
70
+ props.class,
71
+ )}
72
+ >
73
+ <Show when={hasHeader()}>
74
+ <div class="flex flex-wrap items-start justify-between gap-3">
75
+ <div class="flex min-w-0 flex-col gap-1">
76
+ <Show when={props.title}>
77
+ <Dynamic
78
+ component={`h${props.headingLevel ?? 2}`}
79
+ id={headingId()}
80
+ class={cn(
81
+ 'min-w-0 text-ink-gray-9',
82
+ HEADING[props.headingLevel ?? 2],
83
+ )}
84
+ >
85
+ {props.title}
86
+ </Dynamic>
87
+ </Show>
88
+ <Show when={props.description}>
89
+ <p class="max-w-[56ch] text-caption text-ink-gray-7">
90
+ {props.description}
91
+ </p>
92
+ </Show>
93
+ </div>
94
+ <Show when={props.actions}>
95
+ <div class="flex shrink-0 flex-wrap items-center gap-2">
96
+ {props.actions}
97
+ </div>
98
+ </Show>
99
+ </div>
100
+ </Show>
101
+ <Show when={props.toolbar}>{props.toolbar}</Show>
102
+ <div class="min-w-0">{props.children}</div>
103
+ <Show when={props.footer}>
104
+ <div class="border-t border-outline-gray-1 pt-3">{props.footer}</div>
105
+ </Show>
106
+ </section>
107
+ )
108
+ }
@@ -0,0 +1 @@
1
+ export { Section, type SectionHeadingLevel, type SectionProps } from './Section'
@@ -0,0 +1,35 @@
1
+ import { render, screen } from '@solidjs/testing-library'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { describe, expect, it, vi } from 'vitest'
4
+ import { SelectionBar } from './SelectionBar'
5
+
6
+ describe('SelectionBar', () => {
7
+ it('renders nothing without a selection', () => {
8
+ const { container } = render(() => <SelectionBar count={0} />)
9
+ expect(container.querySelector('[data-slot="selection-bar"]')).toBeNull()
10
+ })
11
+
12
+ it('shows the count, action and clear control', async () => {
13
+ const onAction = vi.fn()
14
+ const onClear = vi.fn()
15
+ render(() => (
16
+ <SelectionBar
17
+ count={3}
18
+ summary="$14,650"
19
+ primary={{ label: 'Approve expenses', onClick: onAction }}
20
+ onClear={onClear}
21
+ />
22
+ ))
23
+ expect(
24
+ screen.getByRole('region', { name: 'Selection actions' }),
25
+ ).toBeTruthy()
26
+ expect(screen.getByText('3 selected')).toBeTruthy()
27
+ expect(screen.getByText('$14,650')).toBeTruthy()
28
+ await userEvent.click(
29
+ screen.getByRole('button', { name: 'Approve expenses' }),
30
+ )
31
+ await userEvent.click(screen.getByRole('button', { name: 'Clear' }))
32
+ expect(onAction).toHaveBeenCalled()
33
+ expect(onClear).toHaveBeenCalled()
34
+ })
35
+ })
@@ -0,0 +1,84 @@
1
+ import { Button, cn, type IconProp } from '@talelk/newui-atoms'
2
+ import { type JSX, Show } from 'solid-js'
3
+
4
+ export interface SelectionPrimaryAction {
5
+ label: string
6
+ onClick: () => void
7
+ icon?: IconProp
8
+ disabled?: boolean
9
+ }
10
+
11
+ export interface SelectionBarProps {
12
+ count: number
13
+ /** Context beside the count, e.g. a total or a scope. */
14
+ summary?: JSX.Element
15
+ primary?: SelectionPrimaryAction
16
+ secondary?: JSX.Element
17
+ onClear?: () => void
18
+ /** `sticky` fixes the bar to the bottom below `sm`. @default 'inline' */
19
+ placement?: 'inline' | 'sticky'
20
+ class?: string
21
+ }
22
+
23
+ /**
24
+ * The one bulk-action surface for a data region.
25
+ *
26
+ * Inline on desktop, fixed above the bottom edge on a phone, so selecting a
27
+ * row far down a card list never requires scrolling back to a toolbar.
28
+ */
29
+ export function SelectionBar(props: SelectionBarProps): JSX.Element {
30
+ return (
31
+ <Show when={props.count > 0}>
32
+ <section
33
+ aria-label="Selection actions"
34
+ data-slot="selection-bar"
35
+ data-placement={props.placement ?? 'inline'}
36
+ class={cn(
37
+ 'flex flex-wrap items-center justify-between gap-2 rounded-card border border-outline-gray-1 bg-surface-elevation-1 p-2 shadow-sm',
38
+ props.placement === 'sticky' &&
39
+ 'max-sm:fixed max-sm:inset-x-4 max-sm:bottom-4 max-sm:z-top max-sm:shadow-lg',
40
+ props.class,
41
+ )}
42
+ >
43
+ <div class="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5">
44
+ <span class="text-interface font-medium text-ink-gray-9">
45
+ {props.count} selected
46
+ </span>
47
+ <Show when={props.summary}>
48
+ <span class="text-caption tabular-nums text-ink-gray-7">
49
+ {props.summary}
50
+ </span>
51
+ </Show>
52
+ </div>
53
+ <div class="flex flex-wrap items-center gap-2">
54
+ {props.secondary}
55
+ <Show when={props.onClear}>
56
+ <Button
57
+ variant="ghost"
58
+ size="sm"
59
+ class="max-sm:h-10"
60
+ onClick={props.onClear}
61
+ >
62
+ Clear
63
+ </Button>
64
+ </Show>
65
+ <Show when={props.primary}>
66
+ {(primary) => (
67
+ <Button
68
+ variant="solid"
69
+ theme="blue"
70
+ size="sm"
71
+ class="max-sm:h-10"
72
+ iconLeft={primary().icon}
73
+ disabled={primary().disabled}
74
+ onClick={primary().onClick}
75
+ >
76
+ {primary().label}
77
+ </Button>
78
+ )}
79
+ </Show>
80
+ </div>
81
+ </section>
82
+ </Show>
83
+ )
84
+ }
@@ -0,0 +1,5 @@
1
+ export {
2
+ SelectionBar,
3
+ type SelectionBarProps,
4
+ type SelectionPrimaryAction,
5
+ } from './SelectionBar'
@@ -0,0 +1,58 @@
1
+ import { render } from '@solidjs/testing-library'
2
+ import axe, { type AxeResults } from 'axe-core'
3
+ import { describe, expect, it } from 'vitest'
4
+ import { SettingsDialog } from './SettingsDialog'
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('SettingsDialog accessibility', () => {
22
+ it('has no axe violations when open', async () => {
23
+ render(() => (
24
+ <SettingsDialog.Root defaultOpen defaultValue="profile">
25
+ <SettingsDialog.Sidebar>
26
+ <SettingsDialog.NavGroup label="User settings">
27
+ <SettingsDialog.NavItem value="profile">
28
+ Profile
29
+ </SettingsDialog.NavItem>
30
+ <SettingsDialog.NavItem value="notifications">
31
+ Notifications
32
+ </SettingsDialog.NavItem>
33
+ </SettingsDialog.NavGroup>
34
+ </SettingsDialog.Sidebar>
35
+ <SettingsDialog.Content>
36
+ <SettingsDialog.Panel value="profile">
37
+ <SettingsDialog.Header title="Profile" description="Your details" />
38
+ <SettingsDialog.Body>
39
+ <SettingsDialog.Row
40
+ title="Display name"
41
+ description="Shown to others"
42
+ labelFor="display-name"
43
+ >
44
+ <input id="display-name" type="text" />
45
+ </SettingsDialog.Row>
46
+ </SettingsDialog.Body>
47
+ </SettingsDialog.Panel>
48
+ <SettingsDialog.Panel value="notifications">
49
+ <SettingsDialog.Header title="Notifications" />
50
+ <SettingsDialog.Body>Nothing here.</SettingsDialog.Body>
51
+ </SettingsDialog.Panel>
52
+ </SettingsDialog.Content>
53
+ </SettingsDialog.Root>
54
+ ))
55
+
56
+ expect(summarize(await axe.run(document.body, AXE_OPTIONS))).toEqual([])
57
+ })
58
+ })
@@ -0,0 +1,43 @@
1
+ import { render } from '@solidjs/testing-library'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { SettingsDialog } from './SettingsDialog'
4
+
5
+ describe('SettingsDialog (browser, real CSS)', () => {
6
+ it('paints the dialog, sidebar and nav rows', () => {
7
+ render(() => (
8
+ <SettingsDialog.Root defaultOpen>
9
+ <SettingsDialog.Sidebar>
10
+ <SettingsDialog.NavItem value="general">
11
+ General
12
+ </SettingsDialog.NavItem>
13
+ </SettingsDialog.Sidebar>
14
+ <SettingsDialog.Content>
15
+ <SettingsDialog.Panel value="general">
16
+ <SettingsDialog.Header title="General" />
17
+ <SettingsDialog.Body>Body</SettingsDialog.Body>
18
+ </SettingsDialog.Panel>
19
+ </SettingsDialog.Content>
20
+ </SettingsDialog.Root>
21
+ ))
22
+
23
+ const content = document.querySelector(
24
+ '[data-slot="dialog-content"]',
25
+ ) as HTMLElement
26
+ expect(content).not.toBeNull()
27
+ expect(getComputedStyle(content).backgroundColor).not.toBe(
28
+ 'rgba(0, 0, 0, 0)',
29
+ )
30
+
31
+ const sidebar = document.querySelector(
32
+ '[data-slot="settings-sidebar"]',
33
+ ) as HTMLElement
34
+ expect(getComputedStyle(sidebar).backgroundColor).not.toBe(
35
+ 'rgba(0, 0, 0, 0)',
36
+ )
37
+
38
+ const navItem = document.querySelector(
39
+ '[data-slot="settings-nav-item"]',
40
+ ) as HTMLElement
41
+ expect(navItem.getBoundingClientRect().height).toBeGreaterThanOrEqual(24)
42
+ })
43
+ })
@@ -0,0 +1,27 @@
1
+ import { renderToString } from 'solid-js/web'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { SettingsDialog } from './SettingsDialog'
4
+
5
+ describe('SSR smoke', () => {
6
+ it('SettingsDialog renders its closed state without a dialog', () => {
7
+ const html = renderToString(() => (
8
+ <SettingsDialog.Root>
9
+ <SettingsDialog.Sidebar>
10
+ <SettingsDialog.NavGroup label="User settings">
11
+ <SettingsDialog.NavItem value="profile">
12
+ Profile
13
+ </SettingsDialog.NavItem>
14
+ </SettingsDialog.NavGroup>
15
+ </SettingsDialog.Sidebar>
16
+ <SettingsDialog.Content>
17
+ <SettingsDialog.Panel value="profile">
18
+ Profile body
19
+ </SettingsDialog.Panel>
20
+ </SettingsDialog.Content>
21
+ </SettingsDialog.Root>
22
+ ))
23
+
24
+ expect(html).not.toContain('role="dialog"')
25
+ expect(html).not.toContain('data-slot="settings-sidebar"')
26
+ })
27
+ })