@open-mercato/ui 0.6.4-develop.4199.1.86677441c2 → 0.6.4-develop.4210.1.d412061cfe
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +14 -0
- package/dist/backend/DataTable.js +28 -62
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/detail/AttachmentMetadataDialog.js +1 -1
- package/dist/backend/detail/AttachmentMetadataDialog.js.map +2 -2
- package/dist/primitives/activity-feed.js +196 -0
- package/dist/primitives/activity-feed.js.map +7 -0
- package/dist/primitives/avatar.js +140 -14
- package/dist/primitives/avatar.js.map +3 -3
- package/dist/primitives/badge.js +94 -5
- package/dist/primitives/badge.js.map +2 -2
- package/dist/primitives/button-group.js +52 -0
- package/dist/primitives/button-group.js.map +7 -0
- package/dist/primitives/color-picker.js +520 -0
- package/dist/primitives/color-picker.js.map +7 -0
- package/dist/primitives/command-menu.js +355 -0
- package/dist/primitives/command-menu.js.map +7 -0
- package/dist/primitives/dialog.js +210 -35
- package/dist/primitives/dialog.js.map +2 -2
- package/dist/primitives/drawer.js +219 -0
- package/dist/primitives/drawer.js.map +7 -0
- package/dist/primitives/notification-feed.js +213 -0
- package/dist/primitives/notification-feed.js.map +7 -0
- package/dist/primitives/notification.js +23 -2
- package/dist/primitives/notification.js.map +2 -2
- package/dist/primitives/pagination.js +245 -0
- package/dist/primitives/pagination.js.map +7 -0
- package/dist/primitives/progress.js +178 -5
- package/dist/primitives/progress.js.map +2 -2
- package/dist/primitives/rating.js +174 -0
- package/dist/primitives/rating.js.map +7 -0
- package/dist/primitives/scroll-area.js +82 -0
- package/dist/primitives/scroll-area.js.map +7 -0
- package/dist/primitives/segmented-control.js +101 -0
- package/dist/primitives/segmented-control.js.map +7 -0
- package/dist/primitives/separator.js +103 -4
- package/dist/primitives/separator.js.map +2 -2
- package/dist/primitives/slider.js +115 -0
- package/dist/primitives/slider.js.map +7 -0
- package/dist/primitives/step-indicator.js +187 -0
- package/dist/primitives/step-indicator.js.map +7 -0
- package/dist/primitives/table.js +102 -10
- package/dist/primitives/table.js.map +2 -2
- package/dist/primitives/tabs.js +122 -14
- package/dist/primitives/tabs.js.map +2 -2
- package/dist/primitives/time-picker.js +9 -11
- package/dist/primitives/time-picker.js.map +2 -2
- package/package.json +8 -4
- package/src/backend/DataTable.tsx +43 -60
- package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
- package/src/primitives/__tests__/activity-feed.test.tsx +216 -0
- package/src/primitives/__tests__/avatar.test.tsx +131 -0
- package/src/primitives/__tests__/badge.test.tsx +189 -0
- package/src/primitives/__tests__/button-group.test.tsx +117 -0
- package/src/primitives/__tests__/color-picker.test.tsx +589 -0
- package/src/primitives/__tests__/command-menu.test.tsx +231 -0
- package/src/primitives/__tests__/dialog.test.tsx +290 -0
- package/src/primitives/__tests__/drawer.test.tsx +264 -0
- package/src/primitives/__tests__/notification-feed.test.tsx +303 -0
- package/src/primitives/__tests__/notification.test.tsx +102 -2
- package/src/primitives/__tests__/pagination.test.tsx +268 -0
- package/src/primitives/__tests__/progress.test.tsx +222 -0
- package/src/primitives/__tests__/rating.test.tsx +164 -0
- package/src/primitives/__tests__/scroll-area.test.tsx +109 -0
- package/src/primitives/__tests__/segmented-control.test.tsx +129 -0
- package/src/primitives/__tests__/separator.test.tsx +143 -0
- package/src/primitives/__tests__/slider.test.tsx +86 -0
- package/src/primitives/__tests__/step-indicator.test.tsx +205 -0
- package/src/primitives/__tests__/table.test.tsx +199 -0
- package/src/primitives/__tests__/tabs.test.tsx +139 -0
- package/src/primitives/activity-feed.tsx +326 -0
- package/src/primitives/avatar.tsx +225 -22
- package/src/primitives/badge.tsx +141 -6
- package/src/primitives/button-group.tsx +100 -0
- package/src/primitives/color-picker.tsx +693 -0
- package/src/primitives/command-menu.tsx +503 -0
- package/src/primitives/dialog.tsx +315 -51
- package/src/primitives/drawer.tsx +338 -0
- package/src/primitives/notification-feed.tsx +358 -0
- package/src/primitives/notification.tsx +42 -2
- package/src/primitives/pagination.tsx +376 -0
- package/src/primitives/progress.tsx +267 -6
- package/src/primitives/rating.tsx +245 -0
- package/src/primitives/scroll-area.tsx +169 -0
- package/src/primitives/segmented-control.tsx +162 -0
- package/src/primitives/separator.tsx +163 -3
- package/src/primitives/slider.tsx +160 -0
- package/src/primitives/step-indicator.tsx +287 -0
- package/src/primitives/table.tsx +142 -11
- package/src/primitives/tabs.tsx +174 -12
- package/src/primitives/time-picker.tsx +9 -11
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { render as rtlRender, fireEvent, screen } from '@testing-library/react'
|
|
5
|
+
import { I18nProvider } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
|
|
7
|
+
// CommandMenu uses useT() for the sr-only title + footer hints (Navigate / Select).
|
|
8
|
+
// Wrap every render in an empty-dict I18nProvider so the primitive
|
|
9
|
+
// falls back to its English fallbacks without real translations.
|
|
10
|
+
const render: typeof rtlRender = (ui: React.ReactElement, options?: Parameters<typeof rtlRender>[1]) =>
|
|
11
|
+
rtlRender(
|
|
12
|
+
<I18nProvider locale="en" dict={{}}>
|
|
13
|
+
{ui}
|
|
14
|
+
</I18nProvider>,
|
|
15
|
+
options,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// jsdom doesn't implement Element.scrollIntoView; cmdk uses it when
|
|
19
|
+
// the selected item changes. Provide a no-op so the component can mount.
|
|
20
|
+
if (typeof Element !== 'undefined' && !Element.prototype.scrollIntoView) {
|
|
21
|
+
Element.prototype.scrollIntoView = () => {}
|
|
22
|
+
}
|
|
23
|
+
import {
|
|
24
|
+
CommandMenu,
|
|
25
|
+
CommandMenuTrigger,
|
|
26
|
+
CommandMenuContent,
|
|
27
|
+
CommandMenuInput,
|
|
28
|
+
CommandMenuList,
|
|
29
|
+
CommandMenuEmpty,
|
|
30
|
+
CommandMenuGroup,
|
|
31
|
+
CommandMenuItem,
|
|
32
|
+
CommandMenuSeparator,
|
|
33
|
+
CommandMenuFooter,
|
|
34
|
+
} from '../command-menu'
|
|
35
|
+
|
|
36
|
+
function ExampleMenu({
|
|
37
|
+
onSelectMonday,
|
|
38
|
+
defaultOpen = true,
|
|
39
|
+
withSeparator = true,
|
|
40
|
+
}: {
|
|
41
|
+
onSelectMonday?: () => void
|
|
42
|
+
defaultOpen?: boolean
|
|
43
|
+
withSeparator?: boolean
|
|
44
|
+
}) {
|
|
45
|
+
return (
|
|
46
|
+
<CommandMenu defaultOpen={defaultOpen}>
|
|
47
|
+
<CommandMenuTrigger>Open</CommandMenuTrigger>
|
|
48
|
+
<CommandMenuContent>
|
|
49
|
+
<CommandMenuInput placeholder="Search..." />
|
|
50
|
+
<CommandMenuList>
|
|
51
|
+
<CommandMenuEmpty>No results.</CommandMenuEmpty>
|
|
52
|
+
<CommandMenuGroup heading="Tools & Apps">
|
|
53
|
+
<CommandMenuItem value="monday.com" onSelect={onSelectMonday}>
|
|
54
|
+
Monday.com
|
|
55
|
+
</CommandMenuItem>
|
|
56
|
+
<CommandMenuItem value="loom">Loom</CommandMenuItem>
|
|
57
|
+
<CommandMenuItem value="asana">Asana</CommandMenuItem>
|
|
58
|
+
</CommandMenuGroup>
|
|
59
|
+
{withSeparator ? <CommandMenuSeparator /> : null}
|
|
60
|
+
<CommandMenuGroup heading="Employees">
|
|
61
|
+
<CommandMenuItem value="james brown" description="Engineer">
|
|
62
|
+
James Brown
|
|
63
|
+
</CommandMenuItem>
|
|
64
|
+
<CommandMenuItem value="sophia williams" description="Designer">
|
|
65
|
+
Sophia Williams
|
|
66
|
+
</CommandMenuItem>
|
|
67
|
+
</CommandMenuGroup>
|
|
68
|
+
</CommandMenuList>
|
|
69
|
+
<CommandMenuFooter />
|
|
70
|
+
</CommandMenuContent>
|
|
71
|
+
</CommandMenu>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('CommandMenu', () => {
|
|
76
|
+
it('renders content/input/list/group/item/footer slots inside a Radix Dialog', () => {
|
|
77
|
+
render(<ExampleMenu />)
|
|
78
|
+
expect(document.querySelector('[data-slot="command-menu-overlay"]')).not.toBeNull()
|
|
79
|
+
expect(document.querySelector('[data-slot="command-menu-content"]')).not.toBeNull()
|
|
80
|
+
expect(document.querySelector('[data-slot="command-menu-root"]')).not.toBeNull()
|
|
81
|
+
expect(document.querySelector('[data-slot="command-menu-input"]')).not.toBeNull()
|
|
82
|
+
expect(document.querySelector('[data-slot="command-menu-list"]')).not.toBeNull()
|
|
83
|
+
expect(document.querySelectorAll('[data-slot="command-menu-group"]').length).toBe(2)
|
|
84
|
+
expect(document.querySelectorAll('[data-slot="command-menu-item"]').length).toBeGreaterThanOrEqual(5)
|
|
85
|
+
expect(document.querySelector('[data-slot="command-menu-separator"]')).not.toBeNull()
|
|
86
|
+
expect(document.querySelector('[data-slot="command-menu-footer"]')).not.toBeNull()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('opens on trigger click when controlled-default is closed', () => {
|
|
90
|
+
function Controlled() {
|
|
91
|
+
const [open, setOpen] = React.useState(false)
|
|
92
|
+
return (
|
|
93
|
+
<CommandMenu open={open} onOpenChange={setOpen}>
|
|
94
|
+
<CommandMenuTrigger>Open palette</CommandMenuTrigger>
|
|
95
|
+
<CommandMenuContent>
|
|
96
|
+
<CommandMenuInput placeholder="Search..." />
|
|
97
|
+
<CommandMenuList>
|
|
98
|
+
<CommandMenuGroup heading="Tools">
|
|
99
|
+
<CommandMenuItem value="loom">Loom</CommandMenuItem>
|
|
100
|
+
</CommandMenuGroup>
|
|
101
|
+
</CommandMenuList>
|
|
102
|
+
</CommandMenuContent>
|
|
103
|
+
</CommandMenu>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
const { getByText } = render(<Controlled />)
|
|
107
|
+
expect(document.querySelector('[data-slot="command-menu-content"]')).toBeNull()
|
|
108
|
+
fireEvent.click(getByText('Open palette'))
|
|
109
|
+
expect(document.querySelector('[data-slot="command-menu-content"]')).not.toBeNull()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('renders role="dialog" with the auto-hidden title for screen readers', () => {
|
|
113
|
+
render(<ExampleMenu />)
|
|
114
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
|
115
|
+
expect(screen.getByText('Command menu')).toBeInTheDocument()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('renders leading magnifier icon and the ⌘K kbd hint by default', () => {
|
|
119
|
+
render(<ExampleMenu />)
|
|
120
|
+
expect(document.querySelector('[data-slot="command-menu-input-icon"]')).not.toBeNull()
|
|
121
|
+
expect(document.querySelector('[data-slot="command-menu-input-shortcut"]')).not.toBeNull()
|
|
122
|
+
expect(document.querySelector('[data-slot="command-menu-input-clear"]')).toBeNull()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('swaps the ⌘K kbd for a clear × button once the input has a value', () => {
|
|
126
|
+
render(<ExampleMenu />)
|
|
127
|
+
const input = document.querySelector('[data-slot="command-menu-input"]') as HTMLInputElement
|
|
128
|
+
fireEvent.change(input, { target: { value: 'monday' } })
|
|
129
|
+
expect(document.querySelector('[data-slot="command-menu-input-shortcut"]')).toBeNull()
|
|
130
|
+
expect(document.querySelector('[data-slot="command-menu-input-clear"]')).not.toBeNull()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('clears the value when the × button is clicked', () => {
|
|
134
|
+
render(<ExampleMenu />)
|
|
135
|
+
const input = document.querySelector('[data-slot="command-menu-input"]') as HTMLInputElement
|
|
136
|
+
fireEvent.change(input, { target: { value: 'monday' } })
|
|
137
|
+
const clear = document.querySelector('[data-slot="command-menu-input-clear"]') as HTMLButtonElement
|
|
138
|
+
fireEvent.click(clear)
|
|
139
|
+
expect(input.value).toBe('')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('renders item description and chevron slot when applicable', () => {
|
|
143
|
+
render(<ExampleMenu />)
|
|
144
|
+
// Description slot exists for items configured with `description`.
|
|
145
|
+
expect(
|
|
146
|
+
document.querySelectorAll('[data-slot="command-menu-item-description"]').length,
|
|
147
|
+
).toBeGreaterThan(0)
|
|
148
|
+
// Auto chevron renders by default (no `shortcut`, no `hideChevron`).
|
|
149
|
+
expect(document.querySelectorAll('[data-slot="command-menu-item-chevron"]').length).toBeGreaterThan(0)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('fires onSelect when an item is clicked', () => {
|
|
153
|
+
const onSelectMonday = jest.fn()
|
|
154
|
+
render(<ExampleMenu onSelectMonday={onSelectMonday} />)
|
|
155
|
+
const items = document.querySelectorAll('[data-slot="command-menu-item"]') as NodeListOf<HTMLElement>
|
|
156
|
+
const monday = Array.from(items).find((el) => el.textContent?.includes('Monday.com')) as HTMLElement
|
|
157
|
+
expect(monday).toBeTruthy()
|
|
158
|
+
fireEvent.click(monday)
|
|
159
|
+
expect(onSelectMonday).toHaveBeenCalledTimes(1)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('renders the group action button when actionLabel + onAction are provided', () => {
|
|
163
|
+
const onAction = jest.fn()
|
|
164
|
+
render(
|
|
165
|
+
<CommandMenu defaultOpen>
|
|
166
|
+
<CommandMenuTrigger>Open</CommandMenuTrigger>
|
|
167
|
+
<CommandMenuContent>
|
|
168
|
+
<CommandMenuInput placeholder="Search..." />
|
|
169
|
+
<CommandMenuList>
|
|
170
|
+
<CommandMenuGroup heading="Tools" actionLabel="See all" onAction={onAction}>
|
|
171
|
+
<CommandMenuItem value="loom">Loom</CommandMenuItem>
|
|
172
|
+
</CommandMenuGroup>
|
|
173
|
+
</CommandMenuList>
|
|
174
|
+
</CommandMenuContent>
|
|
175
|
+
</CommandMenu>,
|
|
176
|
+
)
|
|
177
|
+
const action = document.querySelector('[data-slot="command-menu-group-action"] button') as HTMLButtonElement
|
|
178
|
+
expect(action).not.toBeNull()
|
|
179
|
+
expect(action.textContent).toContain('See all')
|
|
180
|
+
fireEvent.click(action)
|
|
181
|
+
expect(onAction).toHaveBeenCalledTimes(1)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('renders default footer hints (Navigate / Select)', () => {
|
|
185
|
+
render(<ExampleMenu />)
|
|
186
|
+
const hints = document.querySelector('[data-slot="command-menu-footer-hints"]') as HTMLElement
|
|
187
|
+
expect(hints).not.toBeNull()
|
|
188
|
+
expect(hints.textContent).toContain('Navigate')
|
|
189
|
+
expect(hints.textContent).toContain('Select')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('renders the help slot when provided', () => {
|
|
193
|
+
render(
|
|
194
|
+
<CommandMenu defaultOpen>
|
|
195
|
+
<CommandMenuTrigger>Open</CommandMenuTrigger>
|
|
196
|
+
<CommandMenuContent>
|
|
197
|
+
<CommandMenuInput placeholder="Search..." />
|
|
198
|
+
<CommandMenuList>
|
|
199
|
+
<CommandMenuGroup heading="Tools">
|
|
200
|
+
<CommandMenuItem value="loom">Loom</CommandMenuItem>
|
|
201
|
+
</CommandMenuGroup>
|
|
202
|
+
</CommandMenuList>
|
|
203
|
+
<CommandMenuFooter helpSlot={<a href="#contact">Contact</a>} />
|
|
204
|
+
</CommandMenuContent>
|
|
205
|
+
</CommandMenu>,
|
|
206
|
+
)
|
|
207
|
+
const help = document.querySelector('[data-slot="command-menu-footer-help"]') as HTMLElement
|
|
208
|
+
expect(help).not.toBeNull()
|
|
209
|
+
expect(help.textContent).toContain('Contact')
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('forwards className to command-menu-content without dropping default positioning', () => {
|
|
213
|
+
render(
|
|
214
|
+
<CommandMenu defaultOpen>
|
|
215
|
+
<CommandMenuTrigger>Open</CommandMenuTrigger>
|
|
216
|
+
<CommandMenuContent className="custom-class">
|
|
217
|
+
<CommandMenuInput placeholder="Search..." />
|
|
218
|
+
<CommandMenuList>
|
|
219
|
+
<CommandMenuGroup heading="Tools">
|
|
220
|
+
<CommandMenuItem value="loom">Loom</CommandMenuItem>
|
|
221
|
+
</CommandMenuGroup>
|
|
222
|
+
</CommandMenuList>
|
|
223
|
+
</CommandMenuContent>
|
|
224
|
+
</CommandMenu>,
|
|
225
|
+
)
|
|
226
|
+
const content = document.querySelector('[data-slot="command-menu-content"]') as HTMLElement
|
|
227
|
+
expect(content.className).toContain('custom-class')
|
|
228
|
+
expect(content.className).toContain('fixed')
|
|
229
|
+
expect(content.className).toContain('z-popover')
|
|
230
|
+
})
|
|
231
|
+
})
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { render, fireEvent, screen } from '@testing-library/react'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogTrigger,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
DialogDescription,
|
|
13
|
+
DialogFooter,
|
|
14
|
+
DialogClose,
|
|
15
|
+
} from '../dialog'
|
|
16
|
+
import { I18nProvider } from '@open-mercato/shared/lib/i18n/context'
|
|
17
|
+
|
|
18
|
+
function renderDialog(ui: React.ReactElement) {
|
|
19
|
+
return render(
|
|
20
|
+
<I18nProvider locale="en" dict={{ ui: { dialog: { close: { ariaLabel: 'Close' } } } }}>
|
|
21
|
+
{ui}
|
|
22
|
+
</I18nProvider>,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ExampleDialog({
|
|
27
|
+
size,
|
|
28
|
+
dismissible,
|
|
29
|
+
defaultOpen = true,
|
|
30
|
+
leading,
|
|
31
|
+
footerLayout,
|
|
32
|
+
}: {
|
|
33
|
+
size?: 'sm' | 'default' | 'lg' | 'xl'
|
|
34
|
+
dismissible?: boolean
|
|
35
|
+
defaultOpen?: boolean
|
|
36
|
+
leading?: React.ReactNode
|
|
37
|
+
footerLayout?: 'default' | 'equal'
|
|
38
|
+
}) {
|
|
39
|
+
return (
|
|
40
|
+
<Dialog defaultOpen={defaultOpen}>
|
|
41
|
+
<DialogTrigger>Open</DialogTrigger>
|
|
42
|
+
<DialogContent size={size} dismissible={dismissible}>
|
|
43
|
+
<DialogHeader leading={leading}>
|
|
44
|
+
<DialogTitle>Confirm action</DialogTitle>
|
|
45
|
+
<DialogDescription>This dialog confirms a critical action.</DialogDescription>
|
|
46
|
+
</DialogHeader>
|
|
47
|
+
<DialogFooter layout={footerLayout}>
|
|
48
|
+
<DialogClose>Cancel</DialogClose>
|
|
49
|
+
<button>Continue</button>
|
|
50
|
+
</DialogFooter>
|
|
51
|
+
</DialogContent>
|
|
52
|
+
</Dialog>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('Dialog (Phase B.7)', () => {
|
|
57
|
+
it('renders content / overlay / header / title / description / footer slots inside a Radix Dialog', () => {
|
|
58
|
+
renderDialog(<ExampleDialog />)
|
|
59
|
+
expect(document.querySelector('[data-slot="dialog-content"]')).not.toBeNull()
|
|
60
|
+
expect(document.querySelector('[data-slot="dialog-overlay"]')).not.toBeNull()
|
|
61
|
+
expect(document.querySelector('[data-slot="dialog-header"]')).not.toBeNull()
|
|
62
|
+
expect(document.querySelector('[data-slot="dialog-title"]')).not.toBeNull()
|
|
63
|
+
expect(document.querySelector('[data-slot="dialog-description"]')).not.toBeNull()
|
|
64
|
+
expect(document.querySelector('[data-slot="dialog-footer"]')).not.toBeNull()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('opens on trigger click when defaultOpen=false', () => {
|
|
68
|
+
function Controlled() {
|
|
69
|
+
const [open, setOpen] = React.useState(false)
|
|
70
|
+
return (
|
|
71
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
72
|
+
<DialogTrigger>Open</DialogTrigger>
|
|
73
|
+
<DialogContent>
|
|
74
|
+
<DialogHeader>
|
|
75
|
+
<DialogTitle>Test</DialogTitle>
|
|
76
|
+
</DialogHeader>
|
|
77
|
+
</DialogContent>
|
|
78
|
+
</Dialog>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
renderDialog(<Controlled />)
|
|
82
|
+
expect(document.querySelector('[data-slot="dialog-content"]')).toBeNull()
|
|
83
|
+
fireEvent.click(screen.getByText('Open'))
|
|
84
|
+
expect(document.querySelector('[data-slot="dialog-content"]')).not.toBeNull()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('renders the auto X close button by default', () => {
|
|
88
|
+
renderDialog(<ExampleDialog />)
|
|
89
|
+
expect(document.querySelector('[data-slot="dialog-close-button"]')).not.toBeNull()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('omits the auto X when dismissible=false', () => {
|
|
93
|
+
renderDialog(<ExampleDialog dismissible={false} />)
|
|
94
|
+
expect(document.querySelector('[data-slot="dialog-close-button"]')).toBeNull()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('default size="default" applies sm:max-w-lg', () => {
|
|
98
|
+
renderDialog(<ExampleDialog />)
|
|
99
|
+
const content = document.querySelector('[data-slot="dialog-content"]') as HTMLElement
|
|
100
|
+
expect(content.getAttribute('data-size')).toBe('default')
|
|
101
|
+
expect(content.className).toContain('sm:max-w-lg')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('size variants apply matching max-width', () => {
|
|
105
|
+
const cases: Array<{ size: 'sm' | 'default' | 'lg' | 'xl'; cls: string }> = [
|
|
106
|
+
{ size: 'sm', cls: 'sm:max-w-sm' },
|
|
107
|
+
{ size: 'default', cls: 'sm:max-w-lg' },
|
|
108
|
+
{ size: 'lg', cls: 'sm:max-w-2xl' },
|
|
109
|
+
{ size: 'xl', cls: 'sm:max-w-4xl' },
|
|
110
|
+
]
|
|
111
|
+
for (const { size, cls } of cases) {
|
|
112
|
+
const { unmount } = renderDialog(<ExampleDialog size={size} />)
|
|
113
|
+
const content = document.querySelector('[data-slot="dialog-content"]') as HTMLElement
|
|
114
|
+
expect(content.getAttribute('data-size')).toBe(size)
|
|
115
|
+
expect(content.className).toContain(cls)
|
|
116
|
+
unmount()
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('renders the header leading badge when leading is provided', () => {
|
|
121
|
+
renderDialog(<ExampleDialog leading={<span data-testid="lead-icon">⚙</span>} />)
|
|
122
|
+
const badge = document.querySelector('[data-slot="dialog-header-leading"]') as HTMLElement
|
|
123
|
+
expect(badge).not.toBeNull()
|
|
124
|
+
expect(badge.querySelector('[data-testid="lead-icon"]')).not.toBeNull()
|
|
125
|
+
expect(badge.className).toContain('rounded-full')
|
|
126
|
+
expect(badge.className).toContain('size-10')
|
|
127
|
+
expect(badge.className).toContain('border')
|
|
128
|
+
// Title + description live inside the text wrapper alongside the badge.
|
|
129
|
+
const text = document.querySelector('[data-slot="dialog-header-text"]') as HTMLElement
|
|
130
|
+
expect(text).not.toBeNull()
|
|
131
|
+
expect(text.querySelector('[data-slot="dialog-title"]')).not.toBeNull()
|
|
132
|
+
expect(text.querySelector('[data-slot="dialog-description"]')).not.toBeNull()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('omits the leading badge by default', () => {
|
|
136
|
+
renderDialog(<ExampleDialog />)
|
|
137
|
+
expect(document.querySelector('[data-slot="dialog-header-leading"]')).toBeNull()
|
|
138
|
+
expect(document.querySelector('[data-slot="dialog-header-text"]')).toBeNull()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('default footer layout reads "default" + flex-col-reverse classes', () => {
|
|
142
|
+
renderDialog(<ExampleDialog />)
|
|
143
|
+
const footer = document.querySelector('[data-slot="dialog-footer"]') as HTMLElement
|
|
144
|
+
expect(footer.getAttribute('data-layout')).toBe('default')
|
|
145
|
+
expect(footer.className).toContain('flex-col-reverse')
|
|
146
|
+
expect(footer.className).toContain('sm:justify-end')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('footer ships border-t separator by default per Figma `Modal Footer [1.1]`', () => {
|
|
150
|
+
renderDialog(<ExampleDialog />)
|
|
151
|
+
const footer = document.querySelector('[data-slot="dialog-footer"]') as HTMLElement
|
|
152
|
+
expect(footer.getAttribute('data-bordered')).toBe('true')
|
|
153
|
+
expect(footer.className).toContain('border-t')
|
|
154
|
+
expect(footer.className).toContain('pt-4')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('footer bordered=false drops the separator', () => {
|
|
158
|
+
renderDialog(
|
|
159
|
+
<Dialog defaultOpen>
|
|
160
|
+
<DialogTrigger>Open</DialogTrigger>
|
|
161
|
+
<DialogContent>
|
|
162
|
+
<DialogHeader>
|
|
163
|
+
<DialogTitle>x</DialogTitle>
|
|
164
|
+
</DialogHeader>
|
|
165
|
+
<DialogFooter bordered={false}>
|
|
166
|
+
<DialogClose>Cancel</DialogClose>
|
|
167
|
+
</DialogFooter>
|
|
168
|
+
</DialogContent>
|
|
169
|
+
</Dialog>,
|
|
170
|
+
)
|
|
171
|
+
const footer = document.querySelector('[data-slot="dialog-footer"]') as HTMLElement
|
|
172
|
+
expect(footer.getAttribute('data-bordered')).toBeNull()
|
|
173
|
+
expect(footer.className).not.toContain('border-t')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('footer layout="equal" stretches children flex-1', () => {
|
|
177
|
+
renderDialog(<ExampleDialog footerLayout="equal" />)
|
|
178
|
+
const footer = document.querySelector('[data-slot="dialog-footer"]') as HTMLElement
|
|
179
|
+
expect(footer.getAttribute('data-layout')).toBe('equal')
|
|
180
|
+
expect(footer.className).toContain('[&>*]:flex-1')
|
|
181
|
+
expect(footer.className).not.toContain('flex-col-reverse')
|
|
182
|
+
// border-t default still applies on equal layout.
|
|
183
|
+
expect(footer.className).toContain('border-t')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('footer leading slot renders left content + right-aligned trailing buttons per Figma `Modal Footer [1.1]` variants 2-6', () => {
|
|
187
|
+
renderDialog(
|
|
188
|
+
<Dialog defaultOpen>
|
|
189
|
+
<DialogTrigger>Open</DialogTrigger>
|
|
190
|
+
<DialogContent>
|
|
191
|
+
<DialogHeader>
|
|
192
|
+
<DialogTitle>x</DialogTitle>
|
|
193
|
+
</DialogHeader>
|
|
194
|
+
<DialogFooter leading={<label data-testid="dont-show">Don't show again</label>}>
|
|
195
|
+
<DialogClose>Cancel</DialogClose>
|
|
196
|
+
<button>Continue</button>
|
|
197
|
+
</DialogFooter>
|
|
198
|
+
</DialogContent>
|
|
199
|
+
</Dialog>,
|
|
200
|
+
)
|
|
201
|
+
const leading = document.querySelector('[data-slot="dialog-footer-leading"]') as HTMLElement
|
|
202
|
+
expect(leading).not.toBeNull()
|
|
203
|
+
expect(leading.className).toContain('sm:mr-auto')
|
|
204
|
+
expect(leading.querySelector('[data-testid="dont-show"]')).not.toBeNull()
|
|
205
|
+
const trailing = document.querySelector('[data-slot="dialog-footer-trailing"]') as HTMLElement
|
|
206
|
+
expect(trailing).not.toBeNull()
|
|
207
|
+
// Footer wraps as flex with leading mr-auto pushing trailing to the right.
|
|
208
|
+
const footer = document.querySelector('[data-slot="dialog-footer"]') as HTMLElement
|
|
209
|
+
expect(footer.className).toContain('border-t')
|
|
210
|
+
expect(footer.className).not.toContain('justify-end')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('header leadingTone defaults to "default" with bordered white badge', () => {
|
|
214
|
+
renderDialog(<ExampleDialog leading={<span>x</span>} />)
|
|
215
|
+
const badge = document.querySelector('[data-slot="dialog-header-leading"]') as HTMLElement
|
|
216
|
+
expect(badge.getAttribute('data-tone')).toBe('default')
|
|
217
|
+
expect(badge.className).toContain('border')
|
|
218
|
+
expect(badge.className).toContain('border-input')
|
|
219
|
+
expect(badge.className).toContain('bg-background')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('header leadingTone applies soft status tint + colored icon per Figma `Status Modals [1.1]`', () => {
|
|
223
|
+
const cases: Array<{
|
|
224
|
+
tone: 'accent' | 'success' | 'warning' | 'error' | 'info'
|
|
225
|
+
bg: string
|
|
226
|
+
tx: string
|
|
227
|
+
}> = [
|
|
228
|
+
// Canonical Figma Status Modals — soft tint background paired
|
|
229
|
+
// with a saturated colored icon (red `!` on light pink, etc.).
|
|
230
|
+
{ tone: 'accent', bg: 'bg-accent-indigo/10', tx: 'text-accent-indigo' },
|
|
231
|
+
{ tone: 'success', bg: 'bg-status-success-bg', tx: 'text-status-success-icon' },
|
|
232
|
+
{ tone: 'warning', bg: 'bg-status-warning-bg', tx: 'text-status-warning-icon' },
|
|
233
|
+
{ tone: 'error', bg: 'bg-status-error-bg', tx: 'text-status-error-icon' },
|
|
234
|
+
{ tone: 'info', bg: 'bg-status-info-bg', tx: 'text-status-info-icon' },
|
|
235
|
+
]
|
|
236
|
+
for (const { tone, bg, tx } of cases) {
|
|
237
|
+
const { unmount } = renderDialog(
|
|
238
|
+
<Dialog defaultOpen>
|
|
239
|
+
<DialogTrigger>Open</DialogTrigger>
|
|
240
|
+
<DialogContent>
|
|
241
|
+
<DialogHeader leading={<span>x</span>} leadingTone={tone}>
|
|
242
|
+
<DialogTitle>t</DialogTitle>
|
|
243
|
+
</DialogHeader>
|
|
244
|
+
</DialogContent>
|
|
245
|
+
</Dialog>,
|
|
246
|
+
)
|
|
247
|
+
const badge = document.querySelector('[data-slot="dialog-header-leading"]') as HTMLElement
|
|
248
|
+
expect(badge.getAttribute('data-tone')).toBe(tone)
|
|
249
|
+
expect(badge.className).toContain(bg)
|
|
250
|
+
expect(badge.className).toContain(tx)
|
|
251
|
+
// Status tones drop the bordered white shell.
|
|
252
|
+
expect(badge.className).not.toContain('border-input')
|
|
253
|
+
unmount()
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('Radix Dialog ARIA contract: role="dialog", labelledby/describedby from Title + Description', () => {
|
|
258
|
+
renderDialog(<ExampleDialog />)
|
|
259
|
+
const dialog = screen.getByRole('dialog')
|
|
260
|
+
expect(dialog).toBeInTheDocument()
|
|
261
|
+
const labelledBy = dialog.getAttribute('aria-labelledby')
|
|
262
|
+
const describedBy = dialog.getAttribute('aria-describedby')
|
|
263
|
+
expect(labelledBy).toBeTruthy()
|
|
264
|
+
expect(describedBy).toBeTruthy()
|
|
265
|
+
expect(document.getElementById(labelledBy as string)?.textContent).toBe('Confirm action')
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('DialogClose dismisses the dialog when clicked', () => {
|
|
269
|
+
renderDialog(<ExampleDialog />)
|
|
270
|
+
expect(document.querySelector('[data-slot="dialog-content"]')).not.toBeNull()
|
|
271
|
+
fireEvent.click(screen.getByText('Cancel'))
|
|
272
|
+
expect(document.querySelector('[data-slot="dialog-content"]')).toBeNull()
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('forwards className to DialogContent without dropping size classes', () => {
|
|
276
|
+
renderDialog(
|
|
277
|
+
<Dialog defaultOpen>
|
|
278
|
+
<DialogTrigger>Open</DialogTrigger>
|
|
279
|
+
<DialogContent className="custom-class" size="lg">
|
|
280
|
+
<DialogHeader>
|
|
281
|
+
<DialogTitle>Title</DialogTitle>
|
|
282
|
+
</DialogHeader>
|
|
283
|
+
</DialogContent>
|
|
284
|
+
</Dialog>,
|
|
285
|
+
)
|
|
286
|
+
const content = document.querySelector('[data-slot="dialog-content"]') as HTMLElement
|
|
287
|
+
expect(content.className).toContain('custom-class')
|
|
288
|
+
expect(content.className).toContain('sm:max-w-2xl')
|
|
289
|
+
})
|
|
290
|
+
})
|