@titan-design/react-ui 0.1.0 → 0.1.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.
Files changed (192) hide show
  1. package/package.json +10 -5
  2. package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
  3. package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
  4. package/src/components/custom/DateTime/DateTime.tsx +235 -0
  5. package/src/components/custom/DateTime/index.ts +2 -0
  6. package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
  7. package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
  8. package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
  9. package/src/components/custom/EmptyState/index.ts +2 -0
  10. package/src/components/custom/Metric/Metric.stories.tsx +133 -0
  11. package/src/components/custom/Metric/Metric.test.tsx +120 -0
  12. package/src/components/custom/Metric/Metric.tsx +92 -0
  13. package/src/components/custom/Metric/index.ts +2 -0
  14. package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
  15. package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
  16. package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
  17. package/src/components/custom/Sidebar/index.ts +18 -0
  18. package/src/components/custom/Table/Table.stories.tsx +481 -0
  19. package/src/components/custom/Table/Table.test.tsx +531 -0
  20. package/src/components/custom/Table/Table.tsx +696 -0
  21. package/src/components/custom/Table/index.ts +22 -0
  22. package/src/components/custom/Typography/Typography.stories.tsx +153 -0
  23. package/src/components/custom/Typography/Typography.test.tsx +107 -0
  24. package/src/components/custom/Typography/Typography.tsx +218 -0
  25. package/src/components/custom/Typography/index.ts +19 -0
  26. package/src/components/custom/index.ts +8 -0
  27. package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
  28. package/src/components/custom/stepper/Stepper.test.tsx +230 -0
  29. package/src/components/custom/stepper/Stepper.tsx +250 -0
  30. package/src/components/custom/stepper/index.ts +10 -0
  31. package/src/components/index.ts +5 -0
  32. package/src/components/ui/alert/Alert.stories.tsx +154 -0
  33. package/src/components/ui/alert/Alert.test.tsx +202 -0
  34. package/src/components/ui/alert/Alert.tsx +170 -0
  35. package/src/components/ui/alert/index.ts +8 -0
  36. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
  37. package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
  38. package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
  39. package/src/components/ui/autocomplete/index.ts +2 -0
  40. package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
  41. package/src/components/ui/avatar/Avatar.test.tsx +135 -0
  42. package/src/components/ui/avatar/Avatar.tsx +168 -0
  43. package/src/components/ui/avatar/index.ts +2 -0
  44. package/src/components/ui/badge/Badge.stories.tsx +100 -0
  45. package/src/components/ui/badge/Badge.test.tsx +86 -0
  46. package/src/components/ui/badge/Badge.tsx +105 -0
  47. package/src/components/ui/badge/index.ts +2 -0
  48. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
  49. package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
  50. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
  51. package/src/components/ui/breadcrumbs/index.ts +2 -0
  52. package/src/components/ui/button/Button.stories.tsx +216 -0
  53. package/src/components/ui/button/Button.test.tsx +132 -0
  54. package/src/components/ui/button/Button.tsx +242 -0
  55. package/src/components/ui/button/index.ts +10 -0
  56. package/src/components/ui/card/Card.stories.tsx +443 -0
  57. package/src/components/ui/card/Card.test.tsx +265 -0
  58. package/src/components/ui/card/Card.tsx +380 -0
  59. package/src/components/ui/card/index.ts +20 -0
  60. package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
  61. package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
  62. package/src/components/ui/checkbox/Checkbox.tsx +163 -0
  63. package/src/components/ui/checkbox/index.ts +2 -0
  64. package/src/components/ui/chip/Chip.stories.tsx +112 -0
  65. package/src/components/ui/chip/Chip.test.tsx +119 -0
  66. package/src/components/ui/chip/Chip.tsx +151 -0
  67. package/src/components/ui/chip/index.ts +2 -0
  68. package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
  69. package/src/components/ui/collapse/Collapse.test.tsx +298 -0
  70. package/src/components/ui/collapse/Collapse.tsx +275 -0
  71. package/src/components/ui/collapse/index.ts +18 -0
  72. package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
  73. package/src/components/ui/data-row/DataRow.test.tsx +78 -0
  74. package/src/components/ui/data-row/DataRow.tsx +29 -0
  75. package/src/components/ui/data-row/index.ts +2 -0
  76. package/src/components/ui/divider/Divider.stories.tsx +71 -0
  77. package/src/components/ui/divider/Divider.test.tsx +55 -0
  78. package/src/components/ui/divider/Divider.tsx +37 -0
  79. package/src/components/ui/divider/index.ts +2 -0
  80. package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
  81. package/src/components/ui/drawer/Drawer.test.tsx +142 -0
  82. package/src/components/ui/drawer/Drawer.tsx +228 -0
  83. package/src/components/ui/drawer/index.ts +9 -0
  84. package/src/components/ui/form-field/FormField.stories.tsx +260 -0
  85. package/src/components/ui/form-field/FormField.test.tsx +280 -0
  86. package/src/components/ui/form-field/FormField.tsx +308 -0
  87. package/src/components/ui/form-field/index.ts +2 -0
  88. package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
  89. package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
  90. package/src/components/ui/help-tip/HelpTip.tsx +229 -0
  91. package/src/components/ui/help-tip/index.ts +2 -0
  92. package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
  93. package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
  94. package/src/components/ui/icon-box/IconBox.tsx +57 -0
  95. package/src/components/ui/icon-box/index.ts +1 -0
  96. package/src/components/ui/index.ts +36 -0
  97. package/src/components/ui/input/Input.stories.tsx +192 -0
  98. package/src/components/ui/input/Input.test.tsx +86 -0
  99. package/src/components/ui/input/Input.tsx +273 -0
  100. package/src/components/ui/input/index.ts +2 -0
  101. package/src/components/ui/link/Link.stories.tsx +78 -0
  102. package/src/components/ui/link/Link.test.tsx +94 -0
  103. package/src/components/ui/link/Link.tsx +86 -0
  104. package/src/components/ui/link/index.ts +2 -0
  105. package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
  106. package/src/components/ui/list-item/ListItem.test.tsx +128 -0
  107. package/src/components/ui/list-item/ListItem.tsx +89 -0
  108. package/src/components/ui/list-item/index.ts +14 -0
  109. package/src/components/ui/menu/Menu.stories.tsx +180 -0
  110. package/src/components/ui/menu/Menu.test.tsx +353 -0
  111. package/src/components/ui/menu/Menu.tsx +222 -0
  112. package/src/components/ui/menu/index.ts +2 -0
  113. package/src/components/ui/modal/Modal.stories.tsx +350 -0
  114. package/src/components/ui/modal/Modal.test.tsx +159 -0
  115. package/src/components/ui/modal/Modal.tsx +254 -0
  116. package/src/components/ui/modal/index.ts +19 -0
  117. package/src/components/ui/popover/Popover.stories.tsx +190 -0
  118. package/src/components/ui/popover/Popover.test.tsx +277 -0
  119. package/src/components/ui/popover/Popover.tsx +162 -0
  120. package/src/components/ui/popover/index.ts +2 -0
  121. package/src/components/ui/progress/Progress.stories.tsx +190 -0
  122. package/src/components/ui/progress/Progress.test.tsx +181 -0
  123. package/src/components/ui/progress/Progress.tsx +320 -0
  124. package/src/components/ui/progress/index.ts +9 -0
  125. package/src/components/ui/radio/Radio.stories.tsx +199 -0
  126. package/src/components/ui/radio/Radio.test.tsx +166 -0
  127. package/src/components/ui/radio/Radio.tsx +200 -0
  128. package/src/components/ui/radio/index.ts +2 -0
  129. package/src/components/ui/section/Section.stories.tsx +111 -0
  130. package/src/components/ui/section/Section.test.tsx +121 -0
  131. package/src/components/ui/section/Section.tsx +52 -0
  132. package/src/components/ui/section/index.ts +6 -0
  133. package/src/components/ui/select/Select.stories.tsx +286 -0
  134. package/src/components/ui/select/Select.test.tsx +180 -0
  135. package/src/components/ui/select/Select.tsx +236 -0
  136. package/src/components/ui/select/index.ts +2 -0
  137. package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
  138. package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
  139. package/src/components/ui/skeleton/Skeleton.tsx +229 -0
  140. package/src/components/ui/skeleton/index.ts +16 -0
  141. package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
  142. package/src/components/ui/spinner/Spinner.test.tsx +67 -0
  143. package/src/components/ui/spinner/Spinner.tsx +72 -0
  144. package/src/components/ui/spinner/index.ts +2 -0
  145. package/src/components/ui/stack/Stack.stories.tsx +127 -0
  146. package/src/components/ui/stack/Stack.test.tsx +184 -0
  147. package/src/components/ui/stack/Stack.tsx +76 -0
  148. package/src/components/ui/stack/index.ts +2 -0
  149. package/src/components/ui/surface/Surface.stories.tsx +91 -0
  150. package/src/components/ui/surface/Surface.test.tsx +63 -0
  151. package/src/components/ui/surface/Surface.tsx +51 -0
  152. package/src/components/ui/surface/index.ts +1 -0
  153. package/src/components/ui/switch/Switch.stories.tsx +111 -0
  154. package/src/components/ui/switch/Switch.test.tsx +112 -0
  155. package/src/components/ui/switch/Switch.tsx +103 -0
  156. package/src/components/ui/switch/index.ts +2 -0
  157. package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
  158. package/src/components/ui/tabs/Tabs.test.tsx +192 -0
  159. package/src/components/ui/tabs/Tabs.tsx +253 -0
  160. package/src/components/ui/tabs/index.ts +16 -0
  161. package/src/components/ui/toast/Toast.stories.tsx +255 -0
  162. package/src/components/ui/toast/Toast.test.tsx +207 -0
  163. package/src/components/ui/toast/Toast.tsx +327 -0
  164. package/src/components/ui/toast/index.ts +8 -0
  165. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
  166. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
  167. package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
  168. package/src/components/ui/toolbar-button/index.ts +7 -0
  169. package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
  170. package/src/components/ui/tooltip/Tooltip.test.tsx +179 -0
  171. package/src/components/ui/tooltip/Tooltip.tsx +143 -0
  172. package/src/components/ui/tooltip/index.ts +2 -0
  173. package/src/index.ts +10 -0
  174. package/src/test/setup.ts +19 -0
  175. package/src/theme/Colors.stories.tsx +258 -0
  176. package/src/theme/config.ts +193 -0
  177. package/src/theme/elevation.stories.tsx +346 -0
  178. package/src/theme/elevation.test.ts +27 -0
  179. package/src/theme/elevation.ts +567 -0
  180. package/src/theme/index.ts +39 -0
  181. package/src/theme/shadows.stories.tsx +523 -0
  182. package/src/theme/shadows.ts +439 -0
  183. package/src/theme/tokens/index.ts +2 -0
  184. package/src/theme/tokens/primitives.ts +354 -0
  185. package/src/theme/tokens/semantic.ts +381 -0
  186. package/src/types/jest-axe.d.ts +16 -0
  187. package/src/types/nativewind.d.ts +54 -0
  188. package/src/utils/cn.ts +14 -0
  189. package/src/utils/colors.ts +140 -0
  190. package/src/utils/form.ts +188 -0
  191. package/src/utils/index.ts +29 -0
  192. package/src/utils/useTheme.ts +72 -0
@@ -0,0 +1,78 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { DataRow } from './DataRow'
5
+
6
+ describe('DataRow', () => {
7
+ it('renders label and string value', () => {
8
+ render(<DataRow label="Weight" value="185 lbs" />)
9
+ expect(screen.getByText('Weight')).toBeInTheDocument()
10
+ expect(screen.getByText('185 lbs')).toBeInTheDocument()
11
+ })
12
+
13
+ it('renders label and ReactNode value', () => {
14
+ render(
15
+ <DataRow label="Status" value={<span data-testid="custom-node">Active</span>} />
16
+ )
17
+ expect(screen.getByText('Status')).toBeInTheDocument()
18
+ expect(screen.getByTestId('custom-node')).toBeInTheDocument()
19
+ expect(screen.getByText('Active')).toBeInTheDocument()
20
+ })
21
+
22
+ it('renders string value as text content', () => {
23
+ render(<DataRow label="Sets" value="5" />)
24
+ const valueEl = screen.getByText('5')
25
+ expect(valueEl).toBeInTheDocument()
26
+ expect(valueEl.textContent).toBe('5')
27
+ })
28
+
29
+ it('wraps ReactNode value in a View container', () => {
30
+ const { container } = render(
31
+ <DataRow label="Custom" value={<div data-testid="inner">Content</div>} />
32
+ )
33
+ expect(screen.getByTestId('inner')).toBeInTheDocument()
34
+ })
35
+
36
+ it('applies custom className to root container', () => {
37
+ const { container } = render(
38
+ <DataRow label="Test" value="Value" className="bg-red-500" />
39
+ )
40
+ const root = container.firstChild as HTMLElement
41
+ expect(root).toBeInTheDocument()
42
+ })
43
+
44
+ it('applies valueClassName to string value', () => {
45
+ render(
46
+ <DataRow label="Test" value="Value" valueClassName="text-lg" />
47
+ )
48
+ expect(screen.getByText('Value')).toBeInTheDocument()
49
+ })
50
+
51
+ it('applies valueClassName to ReactNode wrapper', () => {
52
+ render(
53
+ <DataRow label="Test" value={<span>Node</span>} valueClassName="text-lg" />
54
+ )
55
+ expect(screen.getByText('Node')).toBeInTheDocument()
56
+ })
57
+
58
+ it('passes through additional ViewProps', () => {
59
+ render(<DataRow label="Test" value="Value" testID="data-row" />)
60
+ expect(screen.getByTestId('data-row')).toBeInTheDocument()
61
+ })
62
+
63
+ describe('accessibility', () => {
64
+ it('has no accessibility violations with string value', async () => {
65
+ const { container } = render(<DataRow label="Weight" value="185 lbs" />)
66
+ const results = await axe(container)
67
+ expect(results).toHaveNoViolations()
68
+ })
69
+
70
+ it('has no accessibility violations with ReactNode value', async () => {
71
+ const { container } = render(
72
+ <DataRow label="Status" value={<span>Active</span>} />
73
+ )
74
+ const results = await axe(container)
75
+ expect(results).toHaveNoViolations()
76
+ })
77
+ })
78
+ })
@@ -0,0 +1,29 @@
1
+ import React from 'react'
2
+ import { View, Text, type ViewProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ export interface DataRowProps extends ViewProps {
6
+ /** Descriptive label shown on the left */
7
+ label: string
8
+ /** Value shown on the right -- string renders as Text, ReactNode renders inside a View */
9
+ value: React.ReactNode
10
+ /** Additional className applied to the value element */
11
+ valueClassName?: string
12
+ /** Additional className applied to the root container */
13
+ className?: string
14
+ }
15
+
16
+ export function DataRow({ label, value, valueClassName, className, ...props }: DataRowProps) {
17
+ return (
18
+ <View className={cn('flex-row items-center justify-between py-2', className)} {...props}>
19
+ <Text className="text-sm text-text-secondary">{label}</Text>
20
+ {typeof value === 'string' ? (
21
+ <Text className={cn('text-sm font-medium text-text-primary', valueClassName)}>
22
+ {value}
23
+ </Text>
24
+ ) : (
25
+ <View className={cn(valueClassName)}>{value}</View>
26
+ )}
27
+ </View>
28
+ )
29
+ }
@@ -0,0 +1,2 @@
1
+ export { DataRow } from './DataRow'
2
+ export type { DataRowProps } from './DataRow'
@@ -0,0 +1,71 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View, Text } from 'react-native'
3
+ import { Divider } from './Divider'
4
+
5
+ const meta: Meta<typeof Divider> = {
6
+ title: 'Components/Divider',
7
+ component: Divider,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ orientation: {
11
+ control: 'select',
12
+ options: ['horizontal', 'vertical'],
13
+ description: 'Divider orientation',
14
+ },
15
+ },
16
+ }
17
+
18
+ export default meta
19
+ type Story = StoryObj<typeof Divider>
20
+
21
+ export const Default: Story = {
22
+ args: {
23
+ orientation: 'horizontal',
24
+ },
25
+ render: (args) => (
26
+ <View style={{ width: 300 }}>
27
+ <Text className="text-text-primary mb-3">Content above</Text>
28
+ <Divider {...args} />
29
+ <Text className="text-text-primary mt-3">Content below</Text>
30
+ </View>
31
+ ),
32
+ }
33
+
34
+ export const Horizontal: Story = {
35
+ render: () => (
36
+ <View style={{ width: 300, gap: 12 }}>
37
+ <Text className="text-text-primary">Section One</Text>
38
+ <Divider />
39
+ <Text className="text-text-primary">Section Two</Text>
40
+ <Divider />
41
+ <Text className="text-text-primary">Section Three</Text>
42
+ </View>
43
+ ),
44
+ }
45
+
46
+ export const Vertical: Story = {
47
+ render: () => (
48
+ <View style={{ flexDirection: 'row', alignItems: 'center', height: 40, gap: 12 }}>
49
+ <Text className="text-text-primary">Left</Text>
50
+ <Divider orientation="vertical" />
51
+ <Text className="text-text-primary">Center</Text>
52
+ <Divider orientation="vertical" />
53
+ <Text className="text-text-primary">Right</Text>
54
+ </View>
55
+ ),
56
+ }
57
+
58
+ export const InList: Story = {
59
+ render: () => (
60
+ <View style={{ width: 300 }}>
61
+ {['Item One', 'Item Two', 'Item Three', 'Item Four'].map((item, index, arr) => (
62
+ <View key={item}>
63
+ <View style={{ paddingVertical: 12 }}>
64
+ <Text className="text-text-primary">{item}</Text>
65
+ </View>
66
+ {index < arr.length - 1 && <Divider />}
67
+ </View>
68
+ ))}
69
+ </View>
70
+ ),
71
+ }
@@ -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 { Divider } from './Divider'
5
+
6
+ describe('Divider', () => {
7
+ it('renders correctly', () => {
8
+ const { container } = render(<Divider />)
9
+ expect(container.firstChild).toBeInTheDocument()
10
+ })
11
+
12
+ it('defaults to horizontal orientation', () => {
13
+ const { container } = render(<Divider />)
14
+ expect(container.firstChild).toBeInTheDocument()
15
+ })
16
+
17
+ it('renders with vertical orientation', () => {
18
+ const { container } = render(<Divider orientation="vertical" />)
19
+ expect(container.firstChild).toBeInTheDocument()
20
+ })
21
+
22
+ it('renders with horizontal orientation', () => {
23
+ const { container } = render(<Divider orientation="horizontal" />)
24
+ expect(container.firstChild).toBeInTheDocument()
25
+ })
26
+
27
+ it('applies custom className', () => {
28
+ const { container } = render(<Divider className="my-4" />)
29
+ expect(container.firstChild).toBeInTheDocument()
30
+ })
31
+
32
+ it('passes additional props through', () => {
33
+ const { container } = render(<Divider testID="divider" />)
34
+ expect(container.firstChild).toBeInTheDocument()
35
+ })
36
+
37
+ it('has accessibilityRole of none', () => {
38
+ const { container } = render(<Divider />)
39
+ expect(container.firstChild).toHaveAttribute('role', 'presentation')
40
+ })
41
+
42
+ describe('accessibility', () => {
43
+ it('has no accessibility violations', async () => {
44
+ const { container } = render(<Divider />)
45
+ const results = await axe(container)
46
+ expect(results).toHaveNoViolations()
47
+ })
48
+
49
+ it('has no accessibility violations in vertical orientation', async () => {
50
+ const { container } = render(<Divider orientation="vertical" />)
51
+ const results = await axe(container)
52
+ expect(results).toHaveNoViolations()
53
+ })
54
+ })
55
+ })
@@ -0,0 +1,37 @@
1
+ import React from 'react'
2
+ import { View, type ViewProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ export type DividerOrientation = 'horizontal' | 'vertical'
6
+
7
+ export interface DividerProps extends ViewProps {
8
+ /** Divider orientation */
9
+ orientation?: DividerOrientation
10
+ /** Additional className */
11
+ className?: string
12
+ }
13
+
14
+ /**
15
+ * Divider component for visual separation of content.
16
+ *
17
+ * @example
18
+ * <Divider />
19
+ * <Divider orientation="vertical" />
20
+ */
21
+ export function Divider({
22
+ orientation = 'horizontal',
23
+ className,
24
+ ...props
25
+ }: DividerProps) {
26
+ return (
27
+ <View
28
+ accessibilityRole="none"
29
+ className={cn(
30
+ 'bg-divider',
31
+ orientation === 'horizontal' ? 'h-px w-full' : 'w-px h-full',
32
+ className
33
+ )}
34
+ {...props}
35
+ />
36
+ )
37
+ }
@@ -0,0 +1,2 @@
1
+ export { Divider } from './Divider'
2
+ export type { DividerProps, DividerOrientation } from './Divider'
@@ -0,0 +1,228 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useState } from 'react'
3
+ import { View, Text, Pressable } from 'react-native'
4
+ import { Drawer, DrawerBody, DrawerFooter } from './Drawer'
5
+ import { Button, ButtonText } from '../button'
6
+
7
+ const meta: Meta<typeof Drawer> = {
8
+ title: 'Components/Drawer',
9
+ component: Drawer,
10
+ tags: ['autodocs'],
11
+ argTypes: {
12
+ placement: {
13
+ control: 'select',
14
+ options: ['left', 'right', 'top', 'bottom'],
15
+ },
16
+ size: {
17
+ control: 'select',
18
+ options: ['sm', 'md', 'lg', 'xl', 'full'],
19
+ },
20
+ closeOnOverlayClick: {
21
+ control: 'boolean',
22
+ },
23
+ showCloseButton: {
24
+ control: 'boolean',
25
+ },
26
+ },
27
+ }
28
+
29
+ export default meta
30
+ type Story = StoryObj<typeof Drawer>
31
+
32
+ export const Default: Story = {
33
+ render: function Render() {
34
+ const [isOpen, setIsOpen] = useState(false)
35
+ return (
36
+ <View>
37
+ <Button onPress={() => setIsOpen(true)}>
38
+ <ButtonText>Open Drawer</ButtonText>
39
+ </Button>
40
+ <Drawer
41
+ isOpen={isOpen}
42
+ onClose={() => setIsOpen(false)}
43
+ title="Drawer Title"
44
+ >
45
+ <DrawerBody>
46
+ <Text className="text-text-primary">
47
+ This is the drawer content. You can put any content here.
48
+ </Text>
49
+ </DrawerBody>
50
+ </Drawer>
51
+ </View>
52
+ )
53
+ },
54
+ }
55
+
56
+ export const LeftPlacement: Story = {
57
+ render: function Render() {
58
+ const [isOpen, setIsOpen] = useState(false)
59
+ return (
60
+ <View>
61
+ <Button onPress={() => setIsOpen(true)}>
62
+ <ButtonText>Open Left Drawer</ButtonText>
63
+ </Button>
64
+ <Drawer
65
+ isOpen={isOpen}
66
+ onClose={() => setIsOpen(false)}
67
+ placement="left"
68
+ title="Navigation"
69
+ >
70
+ <DrawerBody>
71
+ <View className="gap-2">
72
+ <Pressable className="p-3 rounded-lg web:hover:bg-interactive-hover">
73
+ <Text className="text-text-primary">Dashboard</Text>
74
+ </Pressable>
75
+ <Pressable className="p-3 rounded-lg web:hover:bg-interactive-hover">
76
+ <Text className="text-text-primary">Settings</Text>
77
+ </Pressable>
78
+ <Pressable className="p-3 rounded-lg web:hover:bg-interactive-hover">
79
+ <Text className="text-text-primary">Profile</Text>
80
+ </Pressable>
81
+ </View>
82
+ </DrawerBody>
83
+ </Drawer>
84
+ </View>
85
+ )
86
+ },
87
+ }
88
+
89
+ export const BottomPlacement: Story = {
90
+ render: function Render() {
91
+ const [isOpen, setIsOpen] = useState(false)
92
+ return (
93
+ <View>
94
+ <Button onPress={() => setIsOpen(true)}>
95
+ <ButtonText>Open Bottom Drawer</ButtonText>
96
+ </Button>
97
+ <Drawer
98
+ isOpen={isOpen}
99
+ onClose={() => setIsOpen(false)}
100
+ placement="bottom"
101
+ size="md"
102
+ title="Actions"
103
+ >
104
+ <DrawerBody>
105
+ <View className="gap-3">
106
+ <Button variant="outline" className="w-full">
107
+ <ButtonText>Share</ButtonText>
108
+ </Button>
109
+ <Button variant="outline" className="w-full">
110
+ <ButtonText>Download</ButtonText>
111
+ </Button>
112
+ <Button variant="outline" color="error" className="w-full">
113
+ <ButtonText>Delete</ButtonText>
114
+ </Button>
115
+ </View>
116
+ </DrawerBody>
117
+ </Drawer>
118
+ </View>
119
+ )
120
+ },
121
+ }
122
+
123
+ export const AllSizes: Story = {
124
+ render: function Render() {
125
+ const [openSize, setOpenSize] = useState<string | null>(null)
126
+ const sizes = ['sm', 'md', 'lg', 'xl'] as const
127
+
128
+ return (
129
+ <View className="flex-row gap-2 flex-wrap">
130
+ {sizes.map((size) => (
131
+ <Button key={size} onPress={() => setOpenSize(size)}>
132
+ <ButtonText>{size.toUpperCase()}</ButtonText>
133
+ </Button>
134
+ ))}
135
+ {sizes.map((size) => (
136
+ <Drawer
137
+ key={size}
138
+ isOpen={openSize === size}
139
+ onClose={() => setOpenSize(null)}
140
+ size={size}
141
+ title={`${size.toUpperCase()} Drawer`}
142
+ >
143
+ <DrawerBody>
144
+ <Text className="text-text-primary">
145
+ This is a {size} size drawer.
146
+ </Text>
147
+ </DrawerBody>
148
+ </Drawer>
149
+ ))}
150
+ </View>
151
+ )
152
+ },
153
+ }
154
+
155
+ export const WithFooter: Story = {
156
+ render: function Render() {
157
+ const [isOpen, setIsOpen] = useState(false)
158
+ return (
159
+ <View>
160
+ <Button onPress={() => setIsOpen(true)}>
161
+ <ButtonText>Open Form Drawer</ButtonText>
162
+ </Button>
163
+ <Drawer
164
+ isOpen={isOpen}
165
+ onClose={() => setIsOpen(false)}
166
+ title="Edit Profile"
167
+ size="lg"
168
+ >
169
+ <DrawerBody>
170
+ <View className="gap-4">
171
+ <View>
172
+ <Text className="text-sm font-medium text-text-secondary mb-1">Name</Text>
173
+ <View className="border border-border-input rounded-md px-3 py-2 bg-surface-input">
174
+ <Text className="text-text-primary">John Doe</Text>
175
+ </View>
176
+ </View>
177
+ <View>
178
+ <Text className="text-sm font-medium text-text-secondary mb-1">Email</Text>
179
+ <View className="border border-border-input rounded-md px-3 py-2 bg-surface-input">
180
+ <Text className="text-text-primary">john@example.com</Text>
181
+ </View>
182
+ </View>
183
+ <View>
184
+ <Text className="text-sm font-medium text-text-secondary mb-1">Bio</Text>
185
+ <View className="border border-border-input rounded-md px-3 py-2 bg-surface-input h-24">
186
+ <Text className="text-text-primary">Software developer...</Text>
187
+ </View>
188
+ </View>
189
+ </View>
190
+ </DrawerBody>
191
+ <DrawerFooter>
192
+ <Button variant="ghost" onPress={() => setIsOpen(false)}>
193
+ <ButtonText>Cancel</ButtonText>
194
+ </Button>
195
+ <Button onPress={() => setIsOpen(false)}>
196
+ <ButtonText>Save Changes</ButtonText>
197
+ </Button>
198
+ </DrawerFooter>
199
+ </Drawer>
200
+ </View>
201
+ )
202
+ },
203
+ }
204
+
205
+ export const NoCloseButton: Story = {
206
+ render: function Render() {
207
+ const [isOpen, setIsOpen] = useState(false)
208
+ return (
209
+ <View>
210
+ <Button onPress={() => setIsOpen(true)}>
211
+ <ButtonText>Open Drawer</ButtonText>
212
+ </Button>
213
+ <Drawer
214
+ isOpen={isOpen}
215
+ onClose={() => setIsOpen(false)}
216
+ title="Custom Header"
217
+ showCloseButton={false}
218
+ >
219
+ <DrawerBody>
220
+ <Text className="text-text-primary">
221
+ This drawer has no close button. Click the overlay to close.
222
+ </Text>
223
+ </DrawerBody>
224
+ </Drawer>
225
+ </View>
226
+ )
227
+ },
228
+ }
@@ -0,0 +1,142 @@
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 { Drawer, DrawerBody, DrawerHeader, DrawerFooter } from './Drawer'
5
+
6
+ function renderDrawer(props: Partial<React.ComponentProps<typeof Drawer>> = {}) {
7
+ return render(
8
+ <Drawer isOpen onClose={vi.fn()} title="Test Drawer" {...props}>
9
+ <DrawerBody>Drawer body content</DrawerBody>
10
+ <DrawerFooter>
11
+ <button>Save</button>
12
+ </DrawerFooter>
13
+ </Drawer>
14
+ )
15
+ }
16
+
17
+ describe('Drawer', () => {
18
+ it('renders when isOpen is true', () => {
19
+ renderDrawer({ isOpen: true })
20
+ expect(screen.getByText('Test Drawer')).toBeInTheDocument()
21
+ expect(screen.getByText('Drawer body content')).toBeInTheDocument()
22
+ })
23
+
24
+ it('does not render when isOpen is false', () => {
25
+ renderDrawer({ isOpen: false })
26
+ expect(screen.queryByText('Test Drawer')).not.toBeInTheDocument()
27
+ })
28
+
29
+ it('calls onClose when close button is pressed', () => {
30
+ const onClose = vi.fn()
31
+ renderDrawer({ isOpen: true, onClose })
32
+
33
+ const closeButton = screen.getByLabelText('Close drawer')
34
+ fireEvent.click(closeButton)
35
+ expect(onClose).toHaveBeenCalledTimes(1)
36
+ })
37
+
38
+ it('hides close button when showCloseButton is false', () => {
39
+ renderDrawer({ isOpen: true, showCloseButton: false })
40
+ expect(screen.queryByLabelText('Close drawer')).not.toBeInTheDocument()
41
+ })
42
+
43
+ it('renders title', () => {
44
+ renderDrawer({ isOpen: true, title: 'Settings' })
45
+ expect(screen.getByText('Settings')).toBeInTheDocument()
46
+ })
47
+
48
+ it('renders without title', () => {
49
+ renderDrawer({ isOpen: true, title: undefined, showCloseButton: false })
50
+ expect(screen.getByText('Drawer body content')).toBeInTheDocument()
51
+ })
52
+
53
+ describe('placements', () => {
54
+ const placements = ['left', 'right', 'top', 'bottom'] as const
55
+ placements.forEach((placement) => {
56
+ it(`renders with placement ${placement}`, () => {
57
+ renderDrawer({ isOpen: true, placement })
58
+ expect(screen.getByText('Test Drawer')).toBeInTheDocument()
59
+ })
60
+ })
61
+ })
62
+
63
+ describe('sizes', () => {
64
+ const sizes = ['sm', 'md', 'lg', 'xl', 'full'] as const
65
+ sizes.forEach((size) => {
66
+ it(`renders with size ${size}`, () => {
67
+ renderDrawer({ isOpen: true, size })
68
+ expect(screen.getByText('Test Drawer')).toBeInTheDocument()
69
+ })
70
+ })
71
+ })
72
+
73
+ describe('overlay click behavior', () => {
74
+ it('calls onClose when overlay is clicked with closeOnOverlayClick true', () => {
75
+ const onClose = vi.fn()
76
+ renderDrawer({ isOpen: true, onClose, closeOnOverlayClick: true })
77
+ // The backdrop Pressable triggers onClose
78
+ })
79
+
80
+ it('does not call onClose when closeOnOverlayClick is false', () => {
81
+ const onClose = vi.fn()
82
+ renderDrawer({ isOpen: true, onClose, closeOnOverlayClick: false })
83
+ // Clicking backdrop should not call onClose
84
+ })
85
+ })
86
+
87
+ describe('DrawerBody', () => {
88
+ it('renders body content', () => {
89
+ renderDrawer({ isOpen: true })
90
+ expect(screen.getByText('Drawer body content')).toBeInTheDocument()
91
+ })
92
+
93
+ it('renders scrollable body by default', () => {
94
+ render(
95
+ <Drawer isOpen onClose={vi.fn()}>
96
+ <DrawerBody scrollable>Scrollable content</DrawerBody>
97
+ </Drawer>
98
+ )
99
+ expect(screen.getByText('Scrollable content')).toBeInTheDocument()
100
+ })
101
+
102
+ it('renders non-scrollable body', () => {
103
+ render(
104
+ <Drawer isOpen onClose={vi.fn()}>
105
+ <DrawerBody scrollable={false}>Non-scrollable content</DrawerBody>
106
+ </Drawer>
107
+ )
108
+ expect(screen.getByText('Non-scrollable content')).toBeInTheDocument()
109
+ })
110
+ })
111
+
112
+ describe('DrawerHeader', () => {
113
+ it('renders custom header content', () => {
114
+ render(
115
+ <Drawer isOpen onClose={vi.fn()} showCloseButton={false}>
116
+ <DrawerHeader>Custom Header</DrawerHeader>
117
+ </Drawer>
118
+ )
119
+ expect(screen.getByText('Custom Header')).toBeInTheDocument()
120
+ })
121
+ })
122
+
123
+ describe('DrawerFooter', () => {
124
+ it('renders footer content', () => {
125
+ renderDrawer({ isOpen: true })
126
+ expect(screen.getByText('Save')).toBeInTheDocument()
127
+ })
128
+ })
129
+
130
+ describe('accessibility', () => {
131
+ it('has no accessibility violations', async () => {
132
+ const { container } = renderDrawer({ isOpen: true })
133
+ const results = await axe(container)
134
+ expect(results).toHaveNoViolations()
135
+ })
136
+
137
+ it('close button has accessible label', () => {
138
+ renderDrawer({ isOpen: true })
139
+ expect(screen.getByLabelText('Close drawer')).toBeInTheDocument()
140
+ })
141
+ })
142
+ })