@titan-design/react-ui 0.1.1 → 0.2.5

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 (66) hide show
  1. package/README.md +47 -1
  2. package/dist/{chunk-MGW37MPO.mjs → chunk-UXVRPOME.mjs} +173 -3
  3. package/dist/chunk-UXVRPOME.mjs.map +1 -0
  4. package/dist/index.d.mts +101 -12
  5. package/dist/index.d.ts +101 -12
  6. package/dist/index.js +1267 -468
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +734 -112
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/theme/index.d.mts +86 -1
  11. package/dist/theme/index.d.ts +86 -1
  12. package/dist/theme/index.js +166 -0
  13. package/dist/theme/index.js.map +1 -1
  14. package/dist/theme/index.mjs +1 -1
  15. package/package.json +13 -2
  16. package/src/components/custom/Workout/RestTimer.stories.tsx +71 -0
  17. package/src/components/custom/Workout/RestTimer.test.tsx +145 -0
  18. package/src/components/custom/Workout/RestTimer.tsx +172 -0
  19. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +144 -0
  20. package/src/components/custom/Workout/SupersetWrapper.test.tsx +130 -0
  21. package/src/components/custom/Workout/SupersetWrapper.tsx +63 -0
  22. package/src/components/custom/Workout/index.ts +2 -0
  23. package/src/components/custom/index.ts +1 -0
  24. package/src/components/custom/stepper/index.ts +2 -2
  25. package/src/components/ui/avatar/Avatar.test.tsx +17 -0
  26. package/src/components/ui/avatar/Avatar.tsx +13 -5
  27. package/src/components/ui/badge/Badge.test.tsx +17 -0
  28. package/src/components/ui/badge/Badge.tsx +14 -0
  29. package/src/components/ui/badge/index.ts +1 -0
  30. package/src/components/ui/button/Button.tsx +55 -0
  31. package/src/components/ui/card/Card.test.tsx +29 -0
  32. package/src/components/ui/card/Card.tsx +32 -11
  33. package/src/components/ui/index.ts +2 -0
  34. package/src/components/ui/indicator/Indicator.stories.tsx +74 -0
  35. package/src/components/ui/indicator/Indicator.test.tsx +55 -0
  36. package/src/components/ui/indicator/Indicator.tsx +73 -0
  37. package/src/components/ui/indicator/index.ts +2 -0
  38. package/src/components/ui/pill/Pill.stories.tsx +71 -0
  39. package/src/components/ui/pill/Pill.test.tsx +69 -0
  40. package/src/components/ui/pill/Pill.tsx +104 -0
  41. package/src/components/ui/pill/index.ts +2 -0
  42. package/src/components/ui/popover/Popover.test.tsx +144 -2
  43. package/src/components/ui/popover/Popover.tsx +81 -6
  44. package/src/components/ui/progress/Progress.test.tsx +29 -0
  45. package/src/components/ui/progress/Progress.tsx +54 -35
  46. package/src/components/ui/select/Select.test.tsx +20 -0
  47. package/src/components/ui/select/Select.tsx +9 -2
  48. package/src/components/ui/toast/index.ts +1 -0
  49. package/src/components/ui/tooltip/Tooltip.test.tsx +83 -0
  50. package/src/components/ui/tooltip/Tooltip.tsx +117 -26
  51. package/src/stories/PresetShowcase.stories.tsx +462 -0
  52. package/src/stories/ThemePresets.stories.tsx +85 -0
  53. package/src/theme/global.css +129 -1
  54. package/src/theme/index.ts +1 -0
  55. package/src/theme/presets/apply.ts +97 -0
  56. package/src/theme/presets/audiobook.ts +93 -0
  57. package/src/theme/presets/default.ts +6 -0
  58. package/src/theme/presets/index.ts +4 -0
  59. package/src/theme/presets/presets.test.ts +51 -0
  60. package/src/theme/presets/types.ts +76 -0
  61. package/src/utils/avatar-color.test.ts +30 -0
  62. package/src/utils/avatar-color.ts +20 -0
  63. package/src/web-jsx/jsx-dev-runtime.ts +28 -0
  64. package/src/web-jsx/jsx-runtime.ts +41 -0
  65. package/tailwind.config.js +53 -3
  66. package/dist/chunk-MGW37MPO.mjs.map +0 -1
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { Indicator } from './Indicator'
5
+
6
+ describe('Indicator', () => {
7
+ it('renders with default props', () => {
8
+ const { container } = render(<Indicator />)
9
+ expect(container.firstChild).toBeInTheDocument()
10
+ })
11
+
12
+ it('renders with all size options', () => {
13
+ const sizes = ['xs', 'sm', 'md', 'lg'] as const
14
+ sizes.forEach((size) => {
15
+ const { unmount } = render(<Indicator size={size} />)
16
+ unmount()
17
+ })
18
+ })
19
+
20
+ it('renders with all color options', () => {
21
+ const colors = ['default', 'primary', 'success', 'error', 'warning', 'info'] as const
22
+ colors.forEach((color) => {
23
+ const { unmount } = render(<Indicator color={color} />)
24
+ unmount()
25
+ })
26
+ })
27
+
28
+ it('applies custom className', () => {
29
+ const { container } = render(<Indicator className="custom-class" />)
30
+ expect(container.firstChild).toBeInTheDocument()
31
+ })
32
+
33
+ it('supports customColor via inline style', () => {
34
+ const { container } = render(<Indicator customColor="#FF0000" />)
35
+ expect(container.firstChild).toBeInTheDocument()
36
+ })
37
+
38
+ it('supports ring prop', () => {
39
+ const { container } = render(<Indicator ring />)
40
+ expect(container.firstChild).toBeInTheDocument()
41
+ })
42
+
43
+ it('supports glow prop', () => {
44
+ const { container } = render(<Indicator glow color="success" />)
45
+ expect(container.firstChild).toBeInTheDocument()
46
+ })
47
+
48
+ describe('accessibility', () => {
49
+ it('has no accessibility violations', async () => {
50
+ const { container } = render(<Indicator color="success" />)
51
+ const results = await axe(container)
52
+ expect(results).toHaveNoViolations()
53
+ })
54
+ })
55
+ })
@@ -0,0 +1,73 @@
1
+ import React from 'react'
2
+ import { View, type ViewProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ export type IndicatorSize = 'xs' | 'sm' | 'md' | 'lg'
6
+ export type IndicatorColor = 'default' | 'primary' | 'success' | 'error' | 'warning' | 'info'
7
+
8
+ export interface IndicatorProps extends ViewProps {
9
+ /** Dot size */
10
+ size?: IndicatorSize
11
+ /** Semantic color */
12
+ color?: IndicatorColor
13
+ /** Custom hex color (overrides color prop) */
14
+ customColor?: string
15
+ /** Add a glow shadow effect */
16
+ glow?: boolean
17
+ /** Add a ring border */
18
+ ring?: boolean
19
+ /** Additional className */
20
+ className?: string
21
+ }
22
+
23
+ const sizeStyles: Record<IndicatorSize, string> = {
24
+ xs: 'w-1 h-1',
25
+ sm: 'w-1.5 h-1.5',
26
+ md: 'w-2 h-2',
27
+ lg: 'w-2.5 h-2.5',
28
+ }
29
+
30
+ const colorStyles: Record<IndicatorColor, string> = {
31
+ default: 'bg-text-tertiary',
32
+ primary: 'bg-brand-primary',
33
+ success: 'bg-status-success',
34
+ error: 'bg-status-error',
35
+ warning: 'bg-status-warning',
36
+ info: 'bg-status-info',
37
+ }
38
+
39
+ export function Indicator({
40
+ size = 'sm',
41
+ color = 'default',
42
+ customColor,
43
+ glow = false,
44
+ ring = false,
45
+ className,
46
+ style,
47
+ ...props
48
+ }: IndicatorProps) {
49
+ const dynamicStyle = {
50
+ ...(typeof style === 'object' ? style : {}),
51
+ ...(customColor ? { backgroundColor: customColor } : {}),
52
+ ...(glow && !customColor ? {} : {}),
53
+ ...(glow && customColor ? { boxShadow: `0 0 6px ${customColor}` } : {}),
54
+ }
55
+
56
+ return (
57
+ <View
58
+ className={cn(
59
+ 'rounded-full shrink-0',
60
+ sizeStyles[size],
61
+ !customColor && colorStyles[color],
62
+ ring && 'border-2 border-background-base',
63
+ glow && !customColor && color === 'success' && 'shadow-[0_0_6px_rgba(20,184,166,0.6)]',
64
+ glow && !customColor && color === 'error' && 'shadow-[0_0_6px_rgba(239,68,68,0.6)]',
65
+ glow && !customColor && color === 'warning' && 'shadow-[0_0_6px_rgba(245,158,11,0.6)]',
66
+ glow && !customColor && color === 'primary' && 'shadow-[0_0_6px_rgba(255,121,0,0.6)]',
67
+ className
68
+ )}
69
+ style={Object.keys(dynamicStyle).length > 0 ? dynamicStyle : style}
70
+ {...props}
71
+ />
72
+ )
73
+ }
@@ -0,0 +1,2 @@
1
+ export { Indicator } from './Indicator'
2
+ export type { IndicatorProps, IndicatorSize, IndicatorColor } from './Indicator'
@@ -0,0 +1,71 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { Pill } from './Pill'
4
+ import { Indicator } from '../indicator'
5
+
6
+ const meta: Meta<typeof Pill> = {
7
+ title: 'Components/Pill',
8
+ component: Pill,
9
+ tags: ['autodocs'],
10
+ argTypes: {
11
+ variant: { control: 'select', options: ['subtle', 'outline'] },
12
+ color: { control: 'select', options: ['default', 'primary', 'secondary', 'success', 'error', 'warning', 'info'] },
13
+ size: { control: 'select', options: ['xs', 'sm', 'md'] },
14
+ rounded: { control: 'boolean' },
15
+ },
16
+ }
17
+ export default meta
18
+ type Story = StoryObj<typeof Pill>
19
+
20
+ export const Default: Story = { args: { children: 'Label', color: 'primary' } }
21
+
22
+ export const AllVariants: Story = {
23
+ render: () => (
24
+ <View className="flex-row gap-2">
25
+ <Pill variant="subtle" color="primary">Subtle</Pill>
26
+ <Pill variant="outline" color="primary">Outline</Pill>
27
+ </View>
28
+ ),
29
+ }
30
+
31
+ export const AllColors: Story = {
32
+ render: () => (
33
+ <View className="flex-row gap-2 flex-wrap">
34
+ <Pill color="default">Default</Pill>
35
+ <Pill color="primary">Primary</Pill>
36
+ <Pill color="success">Success</Pill>
37
+ <Pill color="error">Error</Pill>
38
+ <Pill color="warning">Warning</Pill>
39
+ <Pill color="info">Info</Pill>
40
+ </View>
41
+ ),
42
+ }
43
+
44
+ export const AllSizes: Story = {
45
+ render: () => (
46
+ <View className="flex-row gap-2 items-center">
47
+ <Pill size="xs" color="primary">XS</Pill>
48
+ <Pill size="sm" color="primary">SM</Pill>
49
+ <Pill size="md" color="primary">MD</Pill>
50
+ </View>
51
+ ),
52
+ }
53
+
54
+ export const WithDot: Story = {
55
+ render: () => (
56
+ <View className="flex-row gap-2">
57
+ <Pill color="success" leftElement={<Indicator size="xs" color="success" />}>Active</Pill>
58
+ <Pill color="error" leftElement={<Indicator size="xs" color="error" />}>Failed</Pill>
59
+ <Pill color="warning" leftElement={<Indicator size="xs" color="warning" />}>Pending</Pill>
60
+ </View>
61
+ ),
62
+ }
63
+
64
+ export const SquareCorners: Story = {
65
+ render: () => (
66
+ <View className="flex-row gap-2">
67
+ <Pill rounded={false} color="primary">Tag</Pill>
68
+ <Pill rounded={false} color="success">Done</Pill>
69
+ </View>
70
+ ),
71
+ }
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { Pill } from './Pill'
5
+
6
+ describe('Pill', () => {
7
+ it('renders string children', () => {
8
+ render(<Pill>Active</Pill>)
9
+ expect(screen.getByText('Active')).toBeInTheDocument()
10
+ })
11
+
12
+ it('renders with all variants', () => {
13
+ const variants = ['subtle', 'outline'] as const
14
+ variants.forEach((variant) => {
15
+ const { unmount } = render(<Pill variant={variant}>Test</Pill>)
16
+ expect(screen.getByText('Test')).toBeInTheDocument()
17
+ unmount()
18
+ })
19
+ })
20
+
21
+ it('renders with all colors', () => {
22
+ const colors = ['default', 'primary', 'secondary', 'success', 'error', 'warning', 'info'] as const
23
+ colors.forEach((color) => {
24
+ const { unmount } = render(<Pill color={color}>Test</Pill>)
25
+ expect(screen.getByText('Test')).toBeInTheDocument()
26
+ unmount()
27
+ })
28
+ })
29
+
30
+ it('renders with all sizes', () => {
31
+ const sizes = ['xs', 'sm', 'md'] as const
32
+ sizes.forEach((size) => {
33
+ const { unmount } = render(<Pill size={size}>Test</Pill>)
34
+ expect(screen.getByText('Test')).toBeInTheDocument()
35
+ unmount()
36
+ })
37
+ })
38
+
39
+ it('applies rounded-full by default', () => {
40
+ const { container } = render(<Pill>Test</Pill>)
41
+ expect(container.firstChild).toBeInTheDocument()
42
+ })
43
+
44
+ it('supports leftElement', () => {
45
+ render(<Pill leftElement={<span data-testid="dot" />}>Label</Pill>)
46
+ expect(screen.getByTestId('dot')).toBeInTheDocument()
47
+ expect(screen.getByText('Label')).toBeInTheDocument()
48
+ })
49
+
50
+ it('handles onPress', () => {
51
+ const handler = vi.fn()
52
+ render(<Pill onPress={handler}>Click</Pill>)
53
+ fireEvent.click(screen.getByText('Click'))
54
+ expect(handler).toHaveBeenCalled()
55
+ })
56
+
57
+ it('applies custom className', () => {
58
+ const { container } = render(<Pill className="custom">Test</Pill>)
59
+ expect(container.firstChild).toBeInTheDocument()
60
+ })
61
+
62
+ describe('accessibility', () => {
63
+ it('has no accessibility violations', async () => {
64
+ const { container } = render(<Pill>Active</Pill>)
65
+ const results = await axe(container)
66
+ expect(results).toHaveNoViolations()
67
+ })
68
+ })
69
+ })
@@ -0,0 +1,104 @@
1
+ import React from 'react'
2
+ import { View, Text, Pressable, type ViewProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ export type PillVariant = 'subtle' | 'outline'
6
+ export type PillColor = 'default' | 'primary' | 'secondary' | 'success' | 'error' | 'warning' | 'info'
7
+ export type PillSize = 'xs' | 'sm' | 'md'
8
+
9
+ export interface PillProps extends ViewProps {
10
+ /** Pill content */
11
+ children: React.ReactNode
12
+ /** Visual variant */
13
+ variant?: PillVariant
14
+ /** Color scheme */
15
+ color?: PillColor
16
+ /** Size */
17
+ size?: PillSize
18
+ /** Fully rounded (default true) or slight radius */
19
+ rounded?: boolean
20
+ /** Element before the label (e.g., Indicator dot) */
21
+ leftElement?: React.ReactNode
22
+ /** Press handler (makes pill interactive) */
23
+ onPress?: () => void
24
+ /** Additional className */
25
+ className?: string
26
+ }
27
+
28
+ const variantColorStyles: Record<PillVariant, Record<PillColor, string>> = {
29
+ subtle: {
30
+ default: 'bg-surface-raised border-border-subtle text-text-secondary',
31
+ primary: 'bg-brand-primary/10 border-brand-primary/25 text-brand-primary',
32
+ secondary: 'bg-brand-secondary/10 border-brand-secondary/25 text-brand-secondary',
33
+ success: 'bg-status-success/10 border-status-success/25 text-status-success',
34
+ error: 'bg-status-error/10 border-status-error/25 text-status-error',
35
+ warning: 'bg-status-warning/10 border-status-warning/25 text-status-warning',
36
+ info: 'bg-status-info/10 border-status-info/25 text-status-info',
37
+ },
38
+ outline: {
39
+ default: 'border-border-default text-text-secondary',
40
+ primary: 'border-brand-primary text-brand-primary',
41
+ secondary: 'border-brand-secondary text-brand-secondary',
42
+ success: 'border-status-success text-status-success',
43
+ error: 'border-status-error text-status-error',
44
+ warning: 'border-status-warning text-status-warning',
45
+ info: 'border-status-info text-status-info',
46
+ },
47
+ }
48
+
49
+ const sizeStyles: Record<PillSize, { container: string; text: string }> = {
50
+ xs: { container: 'px-1 py-px', text: 'text-[9px]' },
51
+ sm: { container: 'px-2 py-0.5', text: 'text-[10px]' },
52
+ md: { container: 'px-2 py-1', text: 'text-[10px]' },
53
+ }
54
+
55
+ export function Pill({
56
+ children,
57
+ variant = 'subtle',
58
+ color = 'default',
59
+ size = 'sm',
60
+ rounded = true,
61
+ leftElement,
62
+ onPress,
63
+ className,
64
+ ...props
65
+ }: PillProps) {
66
+ const containerClasses = cn(
67
+ 'flex-row items-center gap-1 border self-start shrink-0',
68
+ rounded ? 'rounded-full' : 'rounded',
69
+ variantColorStyles[variant][color],
70
+ sizeStyles[size].container,
71
+ className
72
+ )
73
+
74
+ const textClasses = cn(
75
+ 'font-heading font-semibold',
76
+ sizeStyles[size].text,
77
+ 'text-inherit'
78
+ )
79
+
80
+ const content = (
81
+ <>
82
+ {leftElement}
83
+ {typeof children === 'string' ? (
84
+ <Text className={textClasses}>{children}</Text>
85
+ ) : (
86
+ children
87
+ )}
88
+ </>
89
+ )
90
+
91
+ if (onPress) {
92
+ return (
93
+ <Pressable onPress={onPress} className={containerClasses} {...props}>
94
+ {content}
95
+ </Pressable>
96
+ )
97
+ }
98
+
99
+ return (
100
+ <View className={containerClasses} {...props}>
101
+ {content}
102
+ </View>
103
+ )
104
+ }
@@ -0,0 +1,2 @@
1
+ export { Pill } from './Pill'
2
+ export type { PillProps, PillVariant, PillColor, PillSize } from './Pill'
@@ -1,5 +1,5 @@
1
- import { describe, it, expect, vi } from 'vitest'
2
- import { render, screen, fireEvent } from '@testing-library/react'
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { render, screen, fireEvent, act } from '@testing-library/react'
3
3
  import { axe } from 'jest-axe'
4
4
  import {
5
5
  Popover,
@@ -240,6 +240,148 @@ describe('Popover', () => {
240
240
  })
241
241
  })
242
242
 
243
+ describe('hover mode', () => {
244
+ beforeEach(() => {
245
+ vi.useFakeTimers()
246
+ })
247
+
248
+ afterEach(() => {
249
+ vi.useRealTimers()
250
+ })
251
+
252
+ it('opens popover on mouseEnter of trigger', () => {
253
+ render(
254
+ <Popover triggerMode="hover">
255
+ <PopoverTrigger>
256
+ <button>Hover me</button>
257
+ </PopoverTrigger>
258
+ <PopoverContent>
259
+ <span>Hover content</span>
260
+ </PopoverContent>
261
+ </Popover>
262
+ )
263
+
264
+ expect(screen.queryByText('Hover content')).not.toBeInTheDocument()
265
+ fireEvent.mouseEnter(screen.getByText('Hover me'))
266
+ expect(screen.getByText('Hover content')).toBeInTheDocument()
267
+ })
268
+
269
+ it('closes popover on mouseLeave after closeDelay', () => {
270
+ render(
271
+ <Popover triggerMode="hover" closeDelay={200}>
272
+ <PopoverTrigger>
273
+ <button>Hover me</button>
274
+ </PopoverTrigger>
275
+ <PopoverContent>
276
+ <span>Hover content</span>
277
+ </PopoverContent>
278
+ </Popover>
279
+ )
280
+
281
+ fireEvent.mouseEnter(screen.getByText('Hover me'))
282
+ expect(screen.getByText('Hover content')).toBeInTheDocument()
283
+
284
+ fireEvent.mouseLeave(screen.getByText('Hover me'))
285
+ // Still visible before delay expires
286
+ expect(screen.getByText('Hover content')).toBeInTheDocument()
287
+
288
+ act(() => {
289
+ vi.advanceTimersByTime(200)
290
+ })
291
+ expect(screen.queryByText('Hover content')).not.toBeInTheDocument()
292
+ })
293
+
294
+ it('cancels close when hovering back onto trigger', () => {
295
+ render(
296
+ <Popover triggerMode="hover" closeDelay={150}>
297
+ <PopoverTrigger>
298
+ <button>Hover me</button>
299
+ </PopoverTrigger>
300
+ <PopoverContent>
301
+ <span>Hover content</span>
302
+ </PopoverContent>
303
+ </Popover>
304
+ )
305
+
306
+ fireEvent.mouseEnter(screen.getByText('Hover me'))
307
+ expect(screen.getByText('Hover content')).toBeInTheDocument()
308
+
309
+ fireEvent.mouseLeave(screen.getByText('Hover me'))
310
+ act(() => {
311
+ vi.advanceTimersByTime(50)
312
+ })
313
+
314
+ // Re-enter trigger before delay expires
315
+ fireEvent.mouseEnter(screen.getByText('Hover me'))
316
+ act(() => {
317
+ vi.advanceTimersByTime(150)
318
+ })
319
+ expect(screen.getByText('Hover content')).toBeInTheDocument()
320
+ })
321
+
322
+ it('cancels close when hovering onto content', () => {
323
+ render(
324
+ <Popover triggerMode="hover" closeDelay={150}>
325
+ <PopoverTrigger>
326
+ <button>Hover me</button>
327
+ </PopoverTrigger>
328
+ <PopoverContent>
329
+ <span>Hover content</span>
330
+ </PopoverContent>
331
+ </Popover>
332
+ )
333
+
334
+ fireEvent.mouseEnter(screen.getByText('Hover me'))
335
+ fireEvent.mouseLeave(screen.getByText('Hover me'))
336
+
337
+ // Hover onto the content text (inside PopoverContent)
338
+ fireEvent.mouseEnter(screen.getByText('Hover content'))
339
+ act(() => {
340
+ vi.advanceTimersByTime(300)
341
+ })
342
+ expect(screen.getByText('Hover content')).toBeInTheDocument()
343
+ })
344
+
345
+ it('still closes on click outside in hover mode', () => {
346
+ render(
347
+ <Popover triggerMode="hover">
348
+ <PopoverTrigger>
349
+ <button>Hover me</button>
350
+ </PopoverTrigger>
351
+ <PopoverContent>
352
+ <span>Hover content</span>
353
+ </PopoverContent>
354
+ </Popover>
355
+ )
356
+
357
+ fireEvent.mouseEnter(screen.getByText('Hover me'))
358
+ expect(screen.getByText('Hover content')).toBeInTheDocument()
359
+
360
+ // The backdrop is the click-outside mechanism
361
+ fireEvent.click(screen.getByText('Hover content').parentElement!.previousSibling as Element)
362
+ expect(screen.queryByText('Hover content')).not.toBeInTheDocument()
363
+ })
364
+
365
+ it('defaults to click mode when triggerMode is not specified', () => {
366
+ render(
367
+ <Popover>
368
+ <PopoverTrigger>
369
+ <button>Click me</button>
370
+ </PopoverTrigger>
371
+ <PopoverContent>
372
+ <span>Click content</span>
373
+ </PopoverContent>
374
+ </Popover>
375
+ )
376
+
377
+ fireEvent.mouseEnter(screen.getByText('Click me'))
378
+ expect(screen.queryByText('Click content')).not.toBeInTheDocument()
379
+
380
+ fireEvent.click(screen.getByText('Click me'))
381
+ expect(screen.getByText('Click content')).toBeInTheDocument()
382
+ })
383
+ })
384
+
243
385
  describe('accessibility', () => {
244
386
  it('has no accessibility violations when closed', async () => {
245
387
  const { container } = render(