@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.
- package/README.md +47 -1
- package/dist/{chunk-MGW37MPO.mjs → chunk-UXVRPOME.mjs} +173 -3
- package/dist/chunk-UXVRPOME.mjs.map +1 -0
- package/dist/index.d.mts +101 -12
- package/dist/index.d.ts +101 -12
- package/dist/index.js +1267 -468
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +734 -112
- package/dist/index.mjs.map +1 -1
- package/dist/theme/index.d.mts +86 -1
- package/dist/theme/index.d.ts +86 -1
- package/dist/theme/index.js +166 -0
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +1 -1
- package/package.json +13 -2
- package/src/components/custom/Workout/RestTimer.stories.tsx +71 -0
- package/src/components/custom/Workout/RestTimer.test.tsx +145 -0
- package/src/components/custom/Workout/RestTimer.tsx +172 -0
- package/src/components/custom/Workout/SupersetWrapper.stories.tsx +144 -0
- package/src/components/custom/Workout/SupersetWrapper.test.tsx +130 -0
- package/src/components/custom/Workout/SupersetWrapper.tsx +63 -0
- package/src/components/custom/Workout/index.ts +2 -0
- package/src/components/custom/index.ts +1 -0
- package/src/components/custom/stepper/index.ts +2 -2
- package/src/components/ui/avatar/Avatar.test.tsx +17 -0
- package/src/components/ui/avatar/Avatar.tsx +13 -5
- package/src/components/ui/badge/Badge.test.tsx +17 -0
- package/src/components/ui/badge/Badge.tsx +14 -0
- package/src/components/ui/badge/index.ts +1 -0
- package/src/components/ui/button/Button.tsx +55 -0
- package/src/components/ui/card/Card.test.tsx +29 -0
- package/src/components/ui/card/Card.tsx +32 -11
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/indicator/Indicator.stories.tsx +74 -0
- package/src/components/ui/indicator/Indicator.test.tsx +55 -0
- package/src/components/ui/indicator/Indicator.tsx +73 -0
- package/src/components/ui/indicator/index.ts +2 -0
- package/src/components/ui/pill/Pill.stories.tsx +71 -0
- package/src/components/ui/pill/Pill.test.tsx +69 -0
- package/src/components/ui/pill/Pill.tsx +104 -0
- package/src/components/ui/pill/index.ts +2 -0
- package/src/components/ui/popover/Popover.test.tsx +144 -2
- package/src/components/ui/popover/Popover.tsx +81 -6
- package/src/components/ui/progress/Progress.test.tsx +29 -0
- package/src/components/ui/progress/Progress.tsx +54 -35
- package/src/components/ui/select/Select.test.tsx +20 -0
- package/src/components/ui/select/Select.tsx +9 -2
- package/src/components/ui/toast/index.ts +1 -0
- package/src/components/ui/tooltip/Tooltip.test.tsx +83 -0
- package/src/components/ui/tooltip/Tooltip.tsx +117 -26
- package/src/stories/PresetShowcase.stories.tsx +462 -0
- package/src/stories/ThemePresets.stories.tsx +85 -0
- package/src/theme/global.css +129 -1
- package/src/theme/index.ts +1 -0
- package/src/theme/presets/apply.ts +97 -0
- package/src/theme/presets/audiobook.ts +93 -0
- package/src/theme/presets/default.ts +6 -0
- package/src/theme/presets/index.ts +4 -0
- package/src/theme/presets/presets.test.ts +51 -0
- package/src/theme/presets/types.ts +76 -0
- package/src/utils/avatar-color.test.ts +30 -0
- package/src/utils/avatar-color.ts +20 -0
- package/src/web-jsx/jsx-dev-runtime.ts +28 -0
- package/src/web-jsx/jsx-runtime.ts +41 -0
- package/tailwind.config.js +53 -3
- 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,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
|
+
}
|
|
@@ -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(
|