@yuno-payments/dashboard-design-system 0.0.1
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/.storybook/main.ts +20 -0
- package/.storybook/preview.ts +18 -0
- package/.storybook/vitest.setup.ts +7 -0
- package/README.md +69 -0
- package/components.json +21 -0
- package/eslint.config.js +26 -0
- package/index.html +13 -0
- package/package.json +57 -0
- package/public/vite.svg +1 -0
- package/src/App.css +42 -0
- package/src/App.tsx +11 -0
- package/src/assets/react.svg +1 -0
- package/src/components/atoms/button/button.stories.tsx +222 -0
- package/src/components/atoms/button/button.test.tsx +78 -0
- package/src/components/atoms/button/index.tsx +80 -0
- package/src/components/atoms/checkbox/checkbox.stories.tsx +314 -0
- package/src/components/atoms/checkbox/checkbox.test.tsx +278 -0
- package/src/components/atoms/checkbox/index.tsx +103 -0
- package/src/components/atoms/chip/chip.stories.tsx +317 -0
- package/src/components/atoms/chip/chip.test.tsx +300 -0
- package/src/components/atoms/chip/index.tsx +114 -0
- package/src/components/atoms/input/index.tsx +27 -0
- package/src/components/atoms/link/index.tsx +79 -0
- package/src/components/atoms/link/link.stories.tsx +159 -0
- package/src/components/atoms/link/link.test.tsx +176 -0
- package/src/components/atoms/radiobutton/index.tsx +103 -0
- package/src/components/atoms/radiobutton/radiobutton.stories.tsx +314 -0
- package/src/components/atoms/radiobutton/radiobutton.test.tsx +245 -0
- package/src/components/atoms/tag/index.tsx +196 -0
- package/src/components/atoms/tag/tag.stories.tsx +281 -0
- package/src/components/atoms/tag/tag.test.tsx +282 -0
- package/src/components/atoms/typography/index.tsx +62 -0
- package/src/components/atoms/typography/typography.stories.tsx +214 -0
- package/src/components/atoms/typography/typography.test.tsx +187 -0
- package/src/components/index.tsx +17 -0
- package/src/components/molecules/announcement/announcement.stories.tsx +277 -0
- package/src/components/molecules/announcement/announcement.test.tsx +354 -0
- package/src/components/molecules/announcement/index.tsx +200 -0
- package/src/components/molecules/notification-alert/index.tsx +293 -0
- package/src/components/molecules/notification-alert/notification-alert.stories.tsx +418 -0
- package/src/components/molecules/notification-alert/notification-alert.test.tsx +454 -0
- package/src/components/molecules/popover/index.tsx +175 -0
- package/src/components/molecules/popover/popover.stories.tsx +241 -0
- package/src/components/molecules/popover/popover.test.tsx +191 -0
- package/src/components/molecules/textfield/index.tsx +154 -0
- package/src/components/molecules/textfield/textfield.stories.tsx +168 -0
- package/src/components/molecules/textfield/textfield.test.tsx +157 -0
- package/src/components/molecules/tooltip/index.tsx +263 -0
- package/src/components/molecules/tooltip/tooltip.stories.tsx +363 -0
- package/src/components/molecules/tooltip/tooltip.test.tsx +468 -0
- package/src/components/organisms/dialog/dialog.stories.tsx +522 -0
- package/src/components/organisms/dialog/dialog.test.tsx +525 -0
- package/src/components/organisms/dialog/index.tsx +233 -0
- package/src/components/organisms/dropdown/dropdown.stories.tsx +529 -0
- package/src/components/organisms/dropdown/dropdown.test.tsx +390 -0
- package/src/components/organisms/dropdown/index.tsx +624 -0
- package/src/index.css +184 -0
- package/src/lib/color-utils.ts +94 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +10 -0
- package/src/stories/Colors.stories.tsx +107 -0
- package/src/stories/Shadows.stories.tsx +110 -0
- package/src/stories/Spacing.stories.tsx +121 -0
- package/src/stories/Typography.stories.tsx +197 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +33 -0
- package/tsconfig.json +13 -0
- package/tsconfig.node.json +25 -0
- package/vite.config.ts +43 -0
- package/vitest.config.ts +15 -0
- package/vitest.shims.d.ts +1 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { Copy, Download, Settings } from 'lucide-react'
|
|
3
|
+
import { Popover } from './index'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Popover> = {
|
|
6
|
+
title: 'Molecules/Popover',
|
|
7
|
+
component: Popover,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'centered',
|
|
10
|
+
},
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
argTypes: {
|
|
13
|
+
label: {
|
|
14
|
+
control: 'text',
|
|
15
|
+
description: 'Popover content text',
|
|
16
|
+
},
|
|
17
|
+
shortcut: {
|
|
18
|
+
control: 'text',
|
|
19
|
+
description: 'Optional keyboard shortcut to display',
|
|
20
|
+
},
|
|
21
|
+
children: {
|
|
22
|
+
control: 'text',
|
|
23
|
+
description: 'Trigger element content',
|
|
24
|
+
},
|
|
25
|
+
showOn: {
|
|
26
|
+
control: 'select',
|
|
27
|
+
options: ['hover', 'click'],
|
|
28
|
+
description: 'Trigger behavior for showing popover',
|
|
29
|
+
},
|
|
30
|
+
side: {
|
|
31
|
+
control: 'select',
|
|
32
|
+
options: ['top', 'bottom', 'left', 'right'],
|
|
33
|
+
description: 'Popover position relative to trigger',
|
|
34
|
+
},
|
|
35
|
+
isAvailable: {
|
|
36
|
+
control: 'boolean',
|
|
37
|
+
description: 'Whether the popover is available',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default meta
|
|
43
|
+
type Story = StoryObj<typeof meta>
|
|
44
|
+
|
|
45
|
+
export const Default: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
label: 'This is a popover',
|
|
48
|
+
children: 'Hover me',
|
|
49
|
+
showOn: 'hover',
|
|
50
|
+
side: 'bottom',
|
|
51
|
+
isAvailable: true,
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const WithShortcut: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
label: 'Copy ID',
|
|
58
|
+
shortcut: '⌘C',
|
|
59
|
+
children: 'Hover for shortcut',
|
|
60
|
+
showOn: 'hover',
|
|
61
|
+
side: 'bottom',
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const ClickTrigger: Story = {
|
|
66
|
+
args: {
|
|
67
|
+
label: 'Click to see this popover',
|
|
68
|
+
children: 'Click me',
|
|
69
|
+
showOn: 'click',
|
|
70
|
+
side: 'bottom',
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const WithIcon: Story = {
|
|
75
|
+
args: {
|
|
76
|
+
label: 'Download file',
|
|
77
|
+
shortcut: '⌘D',
|
|
78
|
+
children: (
|
|
79
|
+
<div className="flex items-center gap-2 p-2 border rounded hover:bg-accent">
|
|
80
|
+
<Download size={16} />
|
|
81
|
+
<span>Download</span>
|
|
82
|
+
</div>
|
|
83
|
+
),
|
|
84
|
+
showOn: 'hover',
|
|
85
|
+
side: 'top',
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const Positions: Story = {
|
|
90
|
+
render: () => (
|
|
91
|
+
<div className="grid grid-cols-2 gap-8 p-8">
|
|
92
|
+
<div className="space-y-4">
|
|
93
|
+
<h3 className="text-lg font-semibold">Hover Triggers</h3>
|
|
94
|
+
<div className="space-y-4">
|
|
95
|
+
<Popover label="Top position" side="top" showOn="hover">
|
|
96
|
+
<button className="px-4 py-2 bg-primary text-primary-foreground rounded">
|
|
97
|
+
Top
|
|
98
|
+
</button>
|
|
99
|
+
</Popover>
|
|
100
|
+
|
|
101
|
+
<Popover label="Bottom position" side="bottom" showOn="hover">
|
|
102
|
+
<button className="px-4 py-2 bg-primary text-primary-foreground rounded">
|
|
103
|
+
Bottom
|
|
104
|
+
</button>
|
|
105
|
+
</Popover>
|
|
106
|
+
|
|
107
|
+
<Popover label="Left position" side="left" showOn="hover">
|
|
108
|
+
<button className="px-4 py-2 bg-primary text-primary-foreground rounded">
|
|
109
|
+
Left
|
|
110
|
+
</button>
|
|
111
|
+
</Popover>
|
|
112
|
+
|
|
113
|
+
<Popover label="Right position" side="right" showOn="hover">
|
|
114
|
+
<button className="px-4 py-2 bg-primary text-primary-foreground rounded">
|
|
115
|
+
Right
|
|
116
|
+
</button>
|
|
117
|
+
</Popover>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div className="space-y-4">
|
|
122
|
+
<h3 className="text-lg font-semibold">Click Triggers</h3>
|
|
123
|
+
<div className="space-y-4">
|
|
124
|
+
<Popover label="Click popover with shortcut" shortcut="⌘K" side="top" showOn="click">
|
|
125
|
+
<button className="px-4 py-2 bg-secondary text-secondary-foreground rounded">
|
|
126
|
+
Click Top
|
|
127
|
+
</button>
|
|
128
|
+
</Popover>
|
|
129
|
+
|
|
130
|
+
<Popover label="Another click popover" side="bottom" showOn="click">
|
|
131
|
+
<button className="px-4 py-2 bg-secondary text-secondary-foreground rounded">
|
|
132
|
+
Click Bottom
|
|
133
|
+
</button>
|
|
134
|
+
</Popover>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const WithComplexContent: Story = {
|
|
142
|
+
args: {
|
|
143
|
+
label: 'Settings panel',
|
|
144
|
+
shortcut: '⌘,',
|
|
145
|
+
children: (
|
|
146
|
+
<div className="flex items-center gap-2 p-3 border rounded-lg hover:bg-accent cursor-pointer">
|
|
147
|
+
<Settings size={20} />
|
|
148
|
+
<div>
|
|
149
|
+
<div className="font-medium">Settings</div>
|
|
150
|
+
<div className="text-sm text-muted-foreground">Configure your preferences</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
),
|
|
154
|
+
showOn: 'hover',
|
|
155
|
+
side: 'right',
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const Disabled: Story = {
|
|
160
|
+
args: {
|
|
161
|
+
label: 'This popover is disabled',
|
|
162
|
+
children: 'Disabled popover',
|
|
163
|
+
isAvailable: false,
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const AllVariants: Story = {
|
|
168
|
+
render: () => (
|
|
169
|
+
<div className="space-y-8 p-4">
|
|
170
|
+
<div>
|
|
171
|
+
<h3 className="text-lg font-semibold mb-4">Basic Popovers</h3>
|
|
172
|
+
<div className="flex gap-4">
|
|
173
|
+
<Popover label="Simple hover popover" showOn="hover">
|
|
174
|
+
<span className="px-3 py-1 bg-muted rounded">Hover me</span>
|
|
175
|
+
</Popover>
|
|
176
|
+
|
|
177
|
+
<Popover label="Simple click popover" showOn="click">
|
|
178
|
+
<span className="px-3 py-1 bg-muted rounded cursor-pointer">Click me</span>
|
|
179
|
+
</Popover>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div>
|
|
184
|
+
<h3 className="text-lg font-semibold mb-4">With Shortcuts</h3>
|
|
185
|
+
<div className="flex gap-4">
|
|
186
|
+
<Popover label="Copy" shortcut="⌘C" showOn="hover">
|
|
187
|
+
<button className="flex items-center gap-2 px-3 py-1 border rounded hover:bg-accent">
|
|
188
|
+
<Copy size={14} />
|
|
189
|
+
Copy
|
|
190
|
+
</button>
|
|
191
|
+
</Popover>
|
|
192
|
+
|
|
193
|
+
<Popover label="Download file" shortcut="⌘D" showOn="hover">
|
|
194
|
+
<button className="flex items-center gap-2 px-3 py-1 border rounded hover:bg-accent">
|
|
195
|
+
<Download size={14} />
|
|
196
|
+
Download
|
|
197
|
+
</button>
|
|
198
|
+
</Popover>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div>
|
|
203
|
+
<h3 className="text-lg font-semibold mb-4">Different Positions</h3>
|
|
204
|
+
<div className="grid grid-cols-4 gap-4 place-items-center">
|
|
205
|
+
<Popover label="Top tooltip" side="top" showOn="hover">
|
|
206
|
+
<div className="px-4 py-2 bg-primary text-primary-foreground rounded text-center">
|
|
207
|
+
Top
|
|
208
|
+
</div>
|
|
209
|
+
</Popover>
|
|
210
|
+
|
|
211
|
+
<Popover label="Bottom tooltip" side="bottom" showOn="hover">
|
|
212
|
+
<div className="px-4 py-2 bg-primary text-primary-foreground rounded text-center">
|
|
213
|
+
Bottom
|
|
214
|
+
</div>
|
|
215
|
+
</Popover>
|
|
216
|
+
|
|
217
|
+
<Popover label="Left tooltip" side="left" showOn="hover">
|
|
218
|
+
<div className="px-4 py-2 bg-primary text-primary-foreground rounded text-center">
|
|
219
|
+
Left
|
|
220
|
+
</div>
|
|
221
|
+
</Popover>
|
|
222
|
+
|
|
223
|
+
<Popover label="Right tooltip" side="right" showOn="hover">
|
|
224
|
+
<div className="px-4 py-2 bg-primary text-primary-foreground rounded text-center">
|
|
225
|
+
Right
|
|
226
|
+
</div>
|
|
227
|
+
</Popover>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<div>
|
|
232
|
+
<h3 className="text-lg font-semibold mb-4">Disabled State</h3>
|
|
233
|
+
<Popover label="This won't show" isAvailable={false}>
|
|
234
|
+
<span className="px-3 py-1 bg-muted text-muted-foreground rounded">
|
|
235
|
+
Disabled popover
|
|
236
|
+
</span>
|
|
237
|
+
</Popover>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
),
|
|
241
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { act } from 'react-dom/test-utils'
|
|
5
|
+
import { Popover } from './index'
|
|
6
|
+
|
|
7
|
+
function render(ui: React.ReactElement) {
|
|
8
|
+
const container = document.createElement('div')
|
|
9
|
+
document.body.appendChild(container)
|
|
10
|
+
const root = createRoot(container)
|
|
11
|
+
act(() => {
|
|
12
|
+
root.render(ui)
|
|
13
|
+
})
|
|
14
|
+
return { container, root }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getByText(container: HTMLElement, text: string) {
|
|
18
|
+
const el = Array.from(container.querySelectorAll('*')).find((n) => n.textContent?.includes(text))
|
|
19
|
+
if (!el) throw new Error(`Element with text "${text}" not found`)
|
|
20
|
+
return el as HTMLElement
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
describe('Popover', () => {
|
|
25
|
+
it('renders trigger element correctly', () => {
|
|
26
|
+
const { container } = render(
|
|
27
|
+
<Popover label="Test popover">
|
|
28
|
+
<span>Trigger</span>
|
|
29
|
+
</Popover>
|
|
30
|
+
)
|
|
31
|
+
expect(getByText(container, 'Trigger')).toBeTruthy()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('renders default children when no children provided', () => {
|
|
35
|
+
const { container } = render(<Popover label="Test popover" />)
|
|
36
|
+
expect(getByText(container, 'Hover this to see the popover')).toBeTruthy()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('renders only children when isAvailable is false', () => {
|
|
40
|
+
const { container } = render(
|
|
41
|
+
<Popover label="Test popover" isAvailable={false}>
|
|
42
|
+
<span>Trigger</span>
|
|
43
|
+
</Popover>
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
expect(getByText(container, 'Trigger')).toBeTruthy()
|
|
47
|
+
expect(container.querySelector('[role="tooltip"]')).toBeFalsy()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('forwards ref correctly', () => {
|
|
51
|
+
const ref = { current: null }
|
|
52
|
+
render(
|
|
53
|
+
<Popover ref={ref} label="Test">
|
|
54
|
+
<span>Trigger</span>
|
|
55
|
+
</Popover>
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
expect(ref.current).toBeTruthy()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('applies showOn prop correctly', () => {
|
|
62
|
+
const { container: hoverContainer } = render(
|
|
63
|
+
<Popover label="Test popover" showOn="hover">
|
|
64
|
+
<span>Hover Trigger</span>
|
|
65
|
+
</Popover>
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const { container: clickContainer } = render(
|
|
69
|
+
<Popover label="Test popover" showOn="click">
|
|
70
|
+
<span>Click Trigger</span>
|
|
71
|
+
</Popover>
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
expect(getByText(hoverContainer, 'Hover Trigger')).toBeTruthy()
|
|
75
|
+
expect(getByText(clickContainer, 'Click Trigger')).toBeTruthy()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('applies side prop correctly', () => {
|
|
79
|
+
const { container } = render(
|
|
80
|
+
<Popover label="Test popover" side="top">
|
|
81
|
+
<span>Trigger</span>
|
|
82
|
+
</Popover>
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
expect(getByText(container, 'Trigger')).toBeTruthy()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('handles shortcut prop', () => {
|
|
89
|
+
const { container } = render(
|
|
90
|
+
<Popover label="Test popover" shortcut="⌘C">
|
|
91
|
+
<span>Trigger</span>
|
|
92
|
+
</Popover>
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
expect(getByText(container, 'Trigger')).toBeTruthy()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('handles complex children elements', () => {
|
|
99
|
+
const { container } = render(
|
|
100
|
+
<Popover label="Complex popover">
|
|
101
|
+
<div>
|
|
102
|
+
<span>Complex</span>
|
|
103
|
+
<button>Button</button>
|
|
104
|
+
</div>
|
|
105
|
+
</Popover>
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
expect(getByText(container, 'Complex')).toBeTruthy()
|
|
109
|
+
expect(getByText(container, 'Button')).toBeTruthy()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('applies custom className to container', () => {
|
|
113
|
+
const { container } = render(
|
|
114
|
+
<Popover label="Test popover" className="custom-class">
|
|
115
|
+
<span>Trigger</span>
|
|
116
|
+
</Popover>
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const popoverContainer = container.firstChild as HTMLElement
|
|
120
|
+
expect(popoverContainer.className).toMatch(/relative inline-block/)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('sets correct aria attributes on trigger', () => {
|
|
124
|
+
const { container } = render(
|
|
125
|
+
<Popover label="Test popover">
|
|
126
|
+
<span>Trigger</span>
|
|
127
|
+
</Popover>
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const trigger = getByText(container, 'Trigger').parentElement!
|
|
131
|
+
expect(trigger.getAttribute('aria-describedby')).toBeFalsy()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('renders with all size variants', () => {
|
|
135
|
+
const { container: topContainer } = render(
|
|
136
|
+
<Popover label="Top popover" side="top">
|
|
137
|
+
<span>Top</span>
|
|
138
|
+
</Popover>
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
const { container: bottomContainer } = render(
|
|
142
|
+
<Popover label="Bottom popover" side="bottom">
|
|
143
|
+
<span>Bottom</span>
|
|
144
|
+
</Popover>
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
const { container: leftContainer } = render(
|
|
148
|
+
<Popover label="Left popover" side="left">
|
|
149
|
+
<span>Left</span>
|
|
150
|
+
</Popover>
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const { container: rightContainer } = render(
|
|
154
|
+
<Popover label="Right popover" side="right">
|
|
155
|
+
<span>Right</span>
|
|
156
|
+
</Popover>
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
expect(getByText(topContainer, 'Top')).toBeTruthy()
|
|
160
|
+
expect(getByText(bottomContainer, 'Bottom')).toBeTruthy()
|
|
161
|
+
expect(getByText(leftContainer, 'Left')).toBeTruthy()
|
|
162
|
+
expect(getByText(rightContainer, 'Right')).toBeTruthy()
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('handles HTML attributes correctly', () => {
|
|
166
|
+
const { container } = render(
|
|
167
|
+
<Popover label="Test popover" data-testid="popover-test">
|
|
168
|
+
<span>Trigger</span>
|
|
169
|
+
</Popover>
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
expect(getByText(container, 'Trigger')).toBeTruthy()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('renders with different trigger behaviors', () => {
|
|
176
|
+
const { container: hoverContainer } = render(
|
|
177
|
+
<Popover label="Hover popover" showOn="hover">
|
|
178
|
+
<span>Hover me</span>
|
|
179
|
+
</Popover>
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
const { container: clickContainer } = render(
|
|
183
|
+
<Popover label="Click popover" showOn="click">
|
|
184
|
+
<span>Click me</span>
|
|
185
|
+
</Popover>
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
expect(getByText(hoverContainer, 'Hover me')).toBeTruthy()
|
|
189
|
+
expect(getByText(clickContainer, 'Click me')).toBeTruthy()
|
|
190
|
+
})
|
|
191
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
const textFieldVariants = cva(
|
|
6
|
+
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
11
|
+
error: "border-destructive focus-visible:border-destructive focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
|
|
12
|
+
},
|
|
13
|
+
hasStartIcon: {
|
|
14
|
+
true: "pl-9",
|
|
15
|
+
false: "",
|
|
16
|
+
},
|
|
17
|
+
hasEndIcon: {
|
|
18
|
+
true: "pr-9",
|
|
19
|
+
false: "",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: "default",
|
|
24
|
+
hasStartIcon: false,
|
|
25
|
+
hasEndIcon: false,
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
export interface TextFieldProps
|
|
31
|
+
extends Omit<React.ComponentProps<"input">, "size">,
|
|
32
|
+
VariantProps<typeof textFieldVariants> {
|
|
33
|
+
label?: string | React.ReactNode
|
|
34
|
+
helperText?: string
|
|
35
|
+
optionalHelperText?: string
|
|
36
|
+
startIcon?: React.ReactNode
|
|
37
|
+
endIcon?: React.ReactNode
|
|
38
|
+
onClickEnd?: () => void
|
|
39
|
+
onClickStart?: () => void
|
|
40
|
+
error?: boolean
|
|
41
|
+
iconWithHover?: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
|
45
|
+
(
|
|
46
|
+
{
|
|
47
|
+
className,
|
|
48
|
+
type,
|
|
49
|
+
label,
|
|
50
|
+
helperText,
|
|
51
|
+
optionalHelperText,
|
|
52
|
+
startIcon,
|
|
53
|
+
endIcon,
|
|
54
|
+
onClickEnd,
|
|
55
|
+
onClickStart,
|
|
56
|
+
error = false,
|
|
57
|
+
iconWithHover = true,
|
|
58
|
+
disabled,
|
|
59
|
+
...props
|
|
60
|
+
},
|
|
61
|
+
ref
|
|
62
|
+
) => {
|
|
63
|
+
const variant = error ? "error" : "default"
|
|
64
|
+
const hasStartIcon = !!startIcon
|
|
65
|
+
const hasEndIcon = !!endIcon
|
|
66
|
+
|
|
67
|
+
const renderStartIcon = () => {
|
|
68
|
+
if (!startIcon) return null
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
className={cn(
|
|
72
|
+
"absolute left-3 top-1/2 -translate-y-1/2 flex items-center justify-center",
|
|
73
|
+
onClickStart && "cursor-pointer",
|
|
74
|
+
iconWithHover && onClickStart && "hover:opacity-70 transition-opacity",
|
|
75
|
+
disabled && "opacity-50"
|
|
76
|
+
)}
|
|
77
|
+
onClick={onClickStart}
|
|
78
|
+
>
|
|
79
|
+
<div className="w-4 h-4 text-muted-foreground">
|
|
80
|
+
{startIcon}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const renderEndIcon = () => {
|
|
87
|
+
if (!endIcon) return null
|
|
88
|
+
return (
|
|
89
|
+
<div
|
|
90
|
+
className={cn(
|
|
91
|
+
"absolute right-3 top-1/2 -translate-y-1/2 flex items-center justify-center",
|
|
92
|
+
onClickEnd && "cursor-pointer",
|
|
93
|
+
iconWithHover && onClickEnd && "hover:opacity-70 transition-opacity",
|
|
94
|
+
disabled && "opacity-50"
|
|
95
|
+
)}
|
|
96
|
+
onClick={onClickEnd}
|
|
97
|
+
>
|
|
98
|
+
<div className="w-4 h-4 text-muted-foreground">
|
|
99
|
+
{endIcon}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div className="w-full">
|
|
107
|
+
{label && (
|
|
108
|
+
<label className={cn(
|
|
109
|
+
"block text-sm font-medium mb-1",
|
|
110
|
+
disabled ? "text-muted-foreground" : "text-foreground",
|
|
111
|
+
error && "text-destructive"
|
|
112
|
+
)}>
|
|
113
|
+
<span>{label}</span>
|
|
114
|
+
{optionalHelperText && (
|
|
115
|
+
<span className={cn(
|
|
116
|
+
"ml-2 font-normal",
|
|
117
|
+
disabled ? "text-muted-foreground" : "text-muted-foreground"
|
|
118
|
+
)}>
|
|
119
|
+
({optionalHelperText})
|
|
120
|
+
</span>
|
|
121
|
+
)}
|
|
122
|
+
</label>
|
|
123
|
+
)}
|
|
124
|
+
<div className="relative">
|
|
125
|
+
<input
|
|
126
|
+
type={type}
|
|
127
|
+
data-slot="input"
|
|
128
|
+
className={cn(
|
|
129
|
+
textFieldVariants({ variant, hasStartIcon, hasEndIcon, className })
|
|
130
|
+
)}
|
|
131
|
+
ref={ref}
|
|
132
|
+
disabled={disabled}
|
|
133
|
+
aria-invalid={error}
|
|
134
|
+
{...props}
|
|
135
|
+
/>
|
|
136
|
+
{renderStartIcon()}
|
|
137
|
+
{renderEndIcon()}
|
|
138
|
+
</div>
|
|
139
|
+
{helperText && (
|
|
140
|
+
<p className={cn(
|
|
141
|
+
"mt-1 text-sm",
|
|
142
|
+
error ? "text-destructive" : "text-muted-foreground"
|
|
143
|
+
)}>
|
|
144
|
+
{helperText}
|
|
145
|
+
</p>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
TextField.displayName = "TextField"
|
|
153
|
+
|
|
154
|
+
export { TextField }
|