@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.
- package/package.json +10 -5
- package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
- package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
- package/src/components/custom/DateTime/DateTime.tsx +235 -0
- package/src/components/custom/DateTime/index.ts +2 -0
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
- package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
- package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
- package/src/components/custom/EmptyState/index.ts +2 -0
- package/src/components/custom/Metric/Metric.stories.tsx +133 -0
- package/src/components/custom/Metric/Metric.test.tsx +120 -0
- package/src/components/custom/Metric/Metric.tsx +92 -0
- package/src/components/custom/Metric/index.ts +2 -0
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
- package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
- package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
- package/src/components/custom/Sidebar/index.ts +18 -0
- package/src/components/custom/Table/Table.stories.tsx +481 -0
- package/src/components/custom/Table/Table.test.tsx +531 -0
- package/src/components/custom/Table/Table.tsx +696 -0
- package/src/components/custom/Table/index.ts +22 -0
- package/src/components/custom/Typography/Typography.stories.tsx +153 -0
- package/src/components/custom/Typography/Typography.test.tsx +107 -0
- package/src/components/custom/Typography/Typography.tsx +218 -0
- package/src/components/custom/Typography/index.ts +19 -0
- package/src/components/custom/index.ts +8 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
- package/src/components/custom/stepper/Stepper.test.tsx +230 -0
- package/src/components/custom/stepper/Stepper.tsx +250 -0
- package/src/components/custom/stepper/index.ts +10 -0
- package/src/components/index.ts +5 -0
- package/src/components/ui/alert/Alert.stories.tsx +154 -0
- package/src/components/ui/alert/Alert.test.tsx +202 -0
- package/src/components/ui/alert/Alert.tsx +170 -0
- package/src/components/ui/alert/index.ts +8 -0
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
- package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
- package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
- package/src/components/ui/autocomplete/index.ts +2 -0
- package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
- package/src/components/ui/avatar/Avatar.test.tsx +135 -0
- package/src/components/ui/avatar/Avatar.tsx +168 -0
- package/src/components/ui/avatar/index.ts +2 -0
- package/src/components/ui/badge/Badge.stories.tsx +100 -0
- package/src/components/ui/badge/Badge.test.tsx +86 -0
- package/src/components/ui/badge/Badge.tsx +105 -0
- package/src/components/ui/badge/index.ts +2 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
- package/src/components/ui/breadcrumbs/index.ts +2 -0
- package/src/components/ui/button/Button.stories.tsx +216 -0
- package/src/components/ui/button/Button.test.tsx +132 -0
- package/src/components/ui/button/Button.tsx +242 -0
- package/src/components/ui/button/index.ts +10 -0
- package/src/components/ui/card/Card.stories.tsx +443 -0
- package/src/components/ui/card/Card.test.tsx +265 -0
- package/src/components/ui/card/Card.tsx +380 -0
- package/src/components/ui/card/index.ts +20 -0
- package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
- package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
- package/src/components/ui/checkbox/Checkbox.tsx +163 -0
- package/src/components/ui/checkbox/index.ts +2 -0
- package/src/components/ui/chip/Chip.stories.tsx +112 -0
- package/src/components/ui/chip/Chip.test.tsx +119 -0
- package/src/components/ui/chip/Chip.tsx +151 -0
- package/src/components/ui/chip/index.ts +2 -0
- package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
- package/src/components/ui/collapse/Collapse.test.tsx +298 -0
- package/src/components/ui/collapse/Collapse.tsx +275 -0
- package/src/components/ui/collapse/index.ts +18 -0
- package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
- package/src/components/ui/data-row/DataRow.test.tsx +78 -0
- package/src/components/ui/data-row/DataRow.tsx +29 -0
- package/src/components/ui/data-row/index.ts +2 -0
- package/src/components/ui/divider/Divider.stories.tsx +71 -0
- package/src/components/ui/divider/Divider.test.tsx +55 -0
- package/src/components/ui/divider/Divider.tsx +37 -0
- package/src/components/ui/divider/index.ts +2 -0
- package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
- package/src/components/ui/drawer/Drawer.test.tsx +142 -0
- package/src/components/ui/drawer/Drawer.tsx +228 -0
- package/src/components/ui/drawer/index.ts +9 -0
- package/src/components/ui/form-field/FormField.stories.tsx +260 -0
- package/src/components/ui/form-field/FormField.test.tsx +280 -0
- package/src/components/ui/form-field/FormField.tsx +308 -0
- package/src/components/ui/form-field/index.ts +2 -0
- package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
- package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
- package/src/components/ui/help-tip/HelpTip.tsx +229 -0
- package/src/components/ui/help-tip/index.ts +2 -0
- package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
- package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
- package/src/components/ui/icon-box/IconBox.tsx +57 -0
- package/src/components/ui/icon-box/index.ts +1 -0
- package/src/components/ui/index.ts +36 -0
- package/src/components/ui/input/Input.stories.tsx +192 -0
- package/src/components/ui/input/Input.test.tsx +86 -0
- package/src/components/ui/input/Input.tsx +273 -0
- package/src/components/ui/input/index.ts +2 -0
- package/src/components/ui/link/Link.stories.tsx +78 -0
- package/src/components/ui/link/Link.test.tsx +94 -0
- package/src/components/ui/link/Link.tsx +86 -0
- package/src/components/ui/link/index.ts +2 -0
- package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
- package/src/components/ui/list-item/ListItem.test.tsx +128 -0
- package/src/components/ui/list-item/ListItem.tsx +89 -0
- package/src/components/ui/list-item/index.ts +14 -0
- package/src/components/ui/menu/Menu.stories.tsx +180 -0
- package/src/components/ui/menu/Menu.test.tsx +353 -0
- package/src/components/ui/menu/Menu.tsx +222 -0
- package/src/components/ui/menu/index.ts +2 -0
- package/src/components/ui/modal/Modal.stories.tsx +350 -0
- package/src/components/ui/modal/Modal.test.tsx +159 -0
- package/src/components/ui/modal/Modal.tsx +254 -0
- package/src/components/ui/modal/index.ts +19 -0
- package/src/components/ui/popover/Popover.stories.tsx +190 -0
- package/src/components/ui/popover/Popover.test.tsx +277 -0
- package/src/components/ui/popover/Popover.tsx +162 -0
- package/src/components/ui/popover/index.ts +2 -0
- package/src/components/ui/progress/Progress.stories.tsx +190 -0
- package/src/components/ui/progress/Progress.test.tsx +181 -0
- package/src/components/ui/progress/Progress.tsx +320 -0
- package/src/components/ui/progress/index.ts +9 -0
- package/src/components/ui/radio/Radio.stories.tsx +199 -0
- package/src/components/ui/radio/Radio.test.tsx +166 -0
- package/src/components/ui/radio/Radio.tsx +200 -0
- package/src/components/ui/radio/index.ts +2 -0
- package/src/components/ui/section/Section.stories.tsx +111 -0
- package/src/components/ui/section/Section.test.tsx +121 -0
- package/src/components/ui/section/Section.tsx +52 -0
- package/src/components/ui/section/index.ts +6 -0
- package/src/components/ui/select/Select.stories.tsx +286 -0
- package/src/components/ui/select/Select.test.tsx +180 -0
- package/src/components/ui/select/Select.tsx +236 -0
- package/src/components/ui/select/index.ts +2 -0
- package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
- package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
- package/src/components/ui/skeleton/Skeleton.tsx +229 -0
- package/src/components/ui/skeleton/index.ts +16 -0
- package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
- package/src/components/ui/spinner/Spinner.test.tsx +67 -0
- package/src/components/ui/spinner/Spinner.tsx +72 -0
- package/src/components/ui/spinner/index.ts +2 -0
- package/src/components/ui/stack/Stack.stories.tsx +127 -0
- package/src/components/ui/stack/Stack.test.tsx +184 -0
- package/src/components/ui/stack/Stack.tsx +76 -0
- package/src/components/ui/stack/index.ts +2 -0
- package/src/components/ui/surface/Surface.stories.tsx +91 -0
- package/src/components/ui/surface/Surface.test.tsx +63 -0
- package/src/components/ui/surface/Surface.tsx +51 -0
- package/src/components/ui/surface/index.ts +1 -0
- package/src/components/ui/switch/Switch.stories.tsx +111 -0
- package/src/components/ui/switch/Switch.test.tsx +112 -0
- package/src/components/ui/switch/Switch.tsx +103 -0
- package/src/components/ui/switch/index.ts +2 -0
- package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
- package/src/components/ui/tabs/Tabs.test.tsx +192 -0
- package/src/components/ui/tabs/Tabs.tsx +253 -0
- package/src/components/ui/tabs/index.ts +16 -0
- package/src/components/ui/toast/Toast.stories.tsx +255 -0
- package/src/components/ui/toast/Toast.test.tsx +207 -0
- package/src/components/ui/toast/Toast.tsx +327 -0
- package/src/components/ui/toast/index.ts +8 -0
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
- package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
- package/src/components/ui/toolbar-button/index.ts +7 -0
- package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
- package/src/components/ui/tooltip/Tooltip.test.tsx +179 -0
- package/src/components/ui/tooltip/Tooltip.tsx +143 -0
- package/src/components/ui/tooltip/index.ts +2 -0
- package/src/index.ts +10 -0
- package/src/test/setup.ts +19 -0
- package/src/theme/Colors.stories.tsx +258 -0
- package/src/theme/config.ts +193 -0
- package/src/theme/elevation.stories.tsx +346 -0
- package/src/theme/elevation.test.ts +27 -0
- package/src/theme/elevation.ts +567 -0
- package/src/theme/index.ts +39 -0
- package/src/theme/shadows.stories.tsx +523 -0
- package/src/theme/shadows.ts +439 -0
- package/src/theme/tokens/index.ts +2 -0
- package/src/theme/tokens/primitives.ts +354 -0
- package/src/theme/tokens/semantic.ts +381 -0
- package/src/types/jest-axe.d.ts +16 -0
- package/src/types/nativewind.d.ts +54 -0
- package/src/utils/cn.ts +14 -0
- package/src/utils/colors.ts +140 -0
- package/src/utils/form.ts +188 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/useTheme.ts +72 -0
|
@@ -0,0 +1,156 @@
|
|
|
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 { Breadcrumbs, BreadcrumbItem } from './Breadcrumbs'
|
|
5
|
+
|
|
6
|
+
describe('Breadcrumbs', () => {
|
|
7
|
+
it('renders breadcrumb items', () => {
|
|
8
|
+
render(
|
|
9
|
+
<Breadcrumbs>
|
|
10
|
+
<BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
|
|
11
|
+
<BreadcrumbItem onPress={() => {}}>Products</BreadcrumbItem>
|
|
12
|
+
<BreadcrumbItem isCurrentPage>Widget</BreadcrumbItem>
|
|
13
|
+
</Breadcrumbs>
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
expect(screen.getByText('Home')).toBeInTheDocument()
|
|
17
|
+
expect(screen.getByText('Products')).toBeInTheDocument()
|
|
18
|
+
expect(screen.getByText('Widget')).toBeInTheDocument()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('renders default separator between items', () => {
|
|
22
|
+
render(
|
|
23
|
+
<Breadcrumbs>
|
|
24
|
+
<BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
|
|
25
|
+
<BreadcrumbItem isCurrentPage>Page</BreadcrumbItem>
|
|
26
|
+
</Breadcrumbs>
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
// Default separator is '/'
|
|
30
|
+
const separators = screen.getAllByText('/')
|
|
31
|
+
expect(separators).toHaveLength(1)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('renders custom separator', () => {
|
|
35
|
+
render(
|
|
36
|
+
<Breadcrumbs separator=">">
|
|
37
|
+
<BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
|
|
38
|
+
<BreadcrumbItem isCurrentPage>Page</BreadcrumbItem>
|
|
39
|
+
</Breadcrumbs>
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
expect(screen.getByText('>')).toBeInTheDocument()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('does not render separator after the last item', () => {
|
|
46
|
+
render(
|
|
47
|
+
<Breadcrumbs>
|
|
48
|
+
<BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
|
|
49
|
+
<BreadcrumbItem onPress={() => {}}>Products</BreadcrumbItem>
|
|
50
|
+
<BreadcrumbItem isCurrentPage>Widget</BreadcrumbItem>
|
|
51
|
+
</Breadcrumbs>
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const separators = screen.getAllByText('/')
|
|
55
|
+
expect(separators).toHaveLength(2)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('maxItems with collapse', () => {
|
|
59
|
+
it('collapses items when exceeding maxItems', () => {
|
|
60
|
+
render(
|
|
61
|
+
<Breadcrumbs maxItems={3}>
|
|
62
|
+
<BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
|
|
63
|
+
<BreadcrumbItem onPress={() => {}}>Products</BreadcrumbItem>
|
|
64
|
+
<BreadcrumbItem onPress={() => {}}>Category</BreadcrumbItem>
|
|
65
|
+
<BreadcrumbItem onPress={() => {}}>Subcategory</BreadcrumbItem>
|
|
66
|
+
<BreadcrumbItem isCurrentPage>Widget</BreadcrumbItem>
|
|
67
|
+
</Breadcrumbs>
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
expect(screen.getByText('...')).toBeInTheDocument()
|
|
71
|
+
expect(screen.getByText('Home')).toBeInTheDocument()
|
|
72
|
+
expect(screen.getByText('Widget')).toBeInTheDocument()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('does not collapse when items are within maxItems', () => {
|
|
76
|
+
render(
|
|
77
|
+
<Breadcrumbs maxItems={5}>
|
|
78
|
+
<BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
|
|
79
|
+
<BreadcrumbItem isCurrentPage>Page</BreadcrumbItem>
|
|
80
|
+
</Breadcrumbs>
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
expect(screen.queryByText('...')).not.toBeInTheDocument()
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('BreadcrumbItem', () => {
|
|
88
|
+
it('renders as link when not current page', () => {
|
|
89
|
+
render(
|
|
90
|
+
<Breadcrumbs>
|
|
91
|
+
<BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
|
|
92
|
+
</Breadcrumbs>
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
expect(screen.getByRole('link')).toBeInTheDocument()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('renders as text when isCurrentPage is true', () => {
|
|
99
|
+
render(
|
|
100
|
+
<Breadcrumbs>
|
|
101
|
+
<BreadcrumbItem isCurrentPage>Current</BreadcrumbItem>
|
|
102
|
+
</Breadcrumbs>
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
expect(screen.queryByRole('link')).not.toBeInTheDocument()
|
|
106
|
+
expect(screen.getByText('Current')).toBeInTheDocument()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('calls onPress handler when clicked', () => {
|
|
110
|
+
const onPress = vi.fn()
|
|
111
|
+
render(
|
|
112
|
+
<Breadcrumbs>
|
|
113
|
+
<BreadcrumbItem onPress={onPress}>Home</BreadcrumbItem>
|
|
114
|
+
</Breadcrumbs>
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
fireEvent.click(screen.getByRole('link'))
|
|
118
|
+
expect(onPress).toHaveBeenCalledTimes(1)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('sets aria-current on current page item', () => {
|
|
122
|
+
render(
|
|
123
|
+
<Breadcrumbs>
|
|
124
|
+
<BreadcrumbItem isCurrentPage>Current Page</BreadcrumbItem>
|
|
125
|
+
</Breadcrumbs>
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
expect(screen.getByText('Current Page')).toHaveAttribute('aria-current', 'page')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('accepts custom className', () => {
|
|
132
|
+
render(
|
|
133
|
+
<Breadcrumbs>
|
|
134
|
+
<BreadcrumbItem className="custom-class" isCurrentPage>
|
|
135
|
+
Styled
|
|
136
|
+
</BreadcrumbItem>
|
|
137
|
+
</Breadcrumbs>
|
|
138
|
+
)
|
|
139
|
+
expect(screen.getByText('Styled')).toBeInTheDocument()
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
describe('accessibility', () => {
|
|
144
|
+
it('has no accessibility violations', async () => {
|
|
145
|
+
const { container } = render(
|
|
146
|
+
<Breadcrumbs>
|
|
147
|
+
<BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
|
|
148
|
+
<BreadcrumbItem onPress={() => {}}>Products</BreadcrumbItem>
|
|
149
|
+
<BreadcrumbItem isCurrentPage>Widget</BreadcrumbItem>
|
|
150
|
+
</Breadcrumbs>
|
|
151
|
+
)
|
|
152
|
+
const results = await axe(container)
|
|
153
|
+
expect(results).toHaveNoViolations()
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
})
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Text, Pressable, type ViewProps } from 'react-native'
|
|
3
|
+
import { cn } from '../../../utils/cn'
|
|
4
|
+
|
|
5
|
+
export interface BreadcrumbsProps extends ViewProps {
|
|
6
|
+
/** Custom separator */
|
|
7
|
+
separator?: React.ReactNode
|
|
8
|
+
/** Maximum items to show (with collapse) */
|
|
9
|
+
maxItems?: number
|
|
10
|
+
/** Additional className */
|
|
11
|
+
className?: string
|
|
12
|
+
children?: React.ReactNode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Breadcrumbs navigation component.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* <Breadcrumbs>
|
|
20
|
+
* <BreadcrumbItem href="/">Home</BreadcrumbItem>
|
|
21
|
+
* <BreadcrumbItem href="/products">Products</BreadcrumbItem>
|
|
22
|
+
* <BreadcrumbItem isCurrentPage>Widget</BreadcrumbItem>
|
|
23
|
+
* </Breadcrumbs>
|
|
24
|
+
*/
|
|
25
|
+
export function Breadcrumbs({
|
|
26
|
+
separator = '/',
|
|
27
|
+
maxItems,
|
|
28
|
+
className,
|
|
29
|
+
children,
|
|
30
|
+
...props
|
|
31
|
+
}: BreadcrumbsProps) {
|
|
32
|
+
const items = React.Children.toArray(children)
|
|
33
|
+
let displayItems = items
|
|
34
|
+
|
|
35
|
+
// Handle max items with ellipsis
|
|
36
|
+
if (maxItems && items.length > maxItems) {
|
|
37
|
+
const firstItem = items[0]
|
|
38
|
+
const lastItems = items.slice(-Math.floor(maxItems / 2))
|
|
39
|
+
displayItems = [
|
|
40
|
+
firstItem,
|
|
41
|
+
<BreadcrumbEllipsis key="ellipsis" />,
|
|
42
|
+
...lastItems,
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<View
|
|
48
|
+
accessibilityRole="none"
|
|
49
|
+
className={cn('flex-row items-center flex-wrap', className)}
|
|
50
|
+
{...props}
|
|
51
|
+
>
|
|
52
|
+
{displayItems.map((child, index) => (
|
|
53
|
+
<View key={index} className="flex-row items-center">
|
|
54
|
+
{child}
|
|
55
|
+
{index < displayItems.length - 1 && (
|
|
56
|
+
<Text className="mx-2 text-text-tertiary text-sm">
|
|
57
|
+
{separator}
|
|
58
|
+
</Text>
|
|
59
|
+
)}
|
|
60
|
+
</View>
|
|
61
|
+
))}
|
|
62
|
+
</View>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface BreadcrumbItemProps {
|
|
67
|
+
/** Navigation URL */
|
|
68
|
+
href?: string
|
|
69
|
+
/** Whether this is the current page */
|
|
70
|
+
isCurrentPage?: boolean
|
|
71
|
+
/** Press handler */
|
|
72
|
+
onPress?: () => void
|
|
73
|
+
/** Additional className */
|
|
74
|
+
className?: string
|
|
75
|
+
children?: React.ReactNode
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Individual breadcrumb item.
|
|
80
|
+
*/
|
|
81
|
+
export function BreadcrumbItem({
|
|
82
|
+
href,
|
|
83
|
+
isCurrentPage = false,
|
|
84
|
+
onPress,
|
|
85
|
+
className,
|
|
86
|
+
children,
|
|
87
|
+
}: BreadcrumbItemProps) {
|
|
88
|
+
if (isCurrentPage) {
|
|
89
|
+
return (
|
|
90
|
+
<Text
|
|
91
|
+
accessibilityRole="text"
|
|
92
|
+
aria-current="page"
|
|
93
|
+
className={cn('text-sm font-medium text-text-primary', className)}
|
|
94
|
+
>
|
|
95
|
+
{children}
|
|
96
|
+
</Text>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<Pressable
|
|
102
|
+
onPress={onPress}
|
|
103
|
+
accessibilityRole="link"
|
|
104
|
+
accessibilityHint={href}
|
|
105
|
+
>
|
|
106
|
+
<Text
|
|
107
|
+
className={cn(
|
|
108
|
+
'text-sm text-text-secondary',
|
|
109
|
+
'web:hover:text-text-primary web:hover:underline',
|
|
110
|
+
'active:opacity-70',
|
|
111
|
+
className
|
|
112
|
+
)}
|
|
113
|
+
>
|
|
114
|
+
{children}
|
|
115
|
+
</Text>
|
|
116
|
+
</Pressable>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Ellipsis for collapsed breadcrumbs.
|
|
122
|
+
*/
|
|
123
|
+
function BreadcrumbEllipsis() {
|
|
124
|
+
return (
|
|
125
|
+
<Text className="text-sm text-text-tertiary">...</Text>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { Button, ButtonText, ButtonIcon } from './Button'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Button> = {
|
|
6
|
+
title: 'Components/Button',
|
|
7
|
+
component: Button,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
variant: {
|
|
11
|
+
control: 'select',
|
|
12
|
+
options: ['solid', 'outline', 'ghost', 'link'],
|
|
13
|
+
description: 'Visual style variant',
|
|
14
|
+
},
|
|
15
|
+
size: {
|
|
16
|
+
control: 'select',
|
|
17
|
+
options: ['sm', 'md', 'lg'],
|
|
18
|
+
description: 'Button size',
|
|
19
|
+
},
|
|
20
|
+
color: {
|
|
21
|
+
control: 'select',
|
|
22
|
+
options: ['primary', 'secondary', 'accent', 'success', 'error', 'warning', 'info'],
|
|
23
|
+
description: 'Color scheme',
|
|
24
|
+
},
|
|
25
|
+
isIconButton: {
|
|
26
|
+
control: 'boolean',
|
|
27
|
+
description: 'Whether this is an icon-only button (square aspect ratio)',
|
|
28
|
+
},
|
|
29
|
+
isDisabled: {
|
|
30
|
+
control: 'boolean',
|
|
31
|
+
description: 'Whether the button is disabled',
|
|
32
|
+
},
|
|
33
|
+
isLoading: {
|
|
34
|
+
control: 'boolean',
|
|
35
|
+
description: 'Whether the button is in a loading state',
|
|
36
|
+
},
|
|
37
|
+
fullWidth: {
|
|
38
|
+
control: 'boolean',
|
|
39
|
+
description: 'Whether the button should take full width',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default meta
|
|
45
|
+
type Story = StoryObj<typeof Button>
|
|
46
|
+
|
|
47
|
+
export const Primary: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
variant: 'solid',
|
|
50
|
+
color: 'primary',
|
|
51
|
+
size: 'md',
|
|
52
|
+
},
|
|
53
|
+
render: (args) => (
|
|
54
|
+
<Button {...args}>
|
|
55
|
+
<ButtonText>Primary Button</ButtonText>
|
|
56
|
+
</Button>
|
|
57
|
+
),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const Secondary: Story = {
|
|
61
|
+
args: {
|
|
62
|
+
variant: 'solid',
|
|
63
|
+
color: 'secondary',
|
|
64
|
+
size: 'md',
|
|
65
|
+
},
|
|
66
|
+
render: (args) => (
|
|
67
|
+
<Button {...args}>
|
|
68
|
+
<ButtonText>Secondary Button</ButtonText>
|
|
69
|
+
</Button>
|
|
70
|
+
),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const Outline: Story = {
|
|
74
|
+
args: {
|
|
75
|
+
variant: 'outline',
|
|
76
|
+
color: 'primary',
|
|
77
|
+
},
|
|
78
|
+
render: (args) => (
|
|
79
|
+
<Button {...args}>
|
|
80
|
+
<ButtonText>Outline Button</ButtonText>
|
|
81
|
+
</Button>
|
|
82
|
+
),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const Ghost: Story = {
|
|
86
|
+
args: {
|
|
87
|
+
variant: 'ghost',
|
|
88
|
+
color: 'primary',
|
|
89
|
+
},
|
|
90
|
+
render: (args) => (
|
|
91
|
+
<Button {...args}>
|
|
92
|
+
<ButtonText>Ghost Button</ButtonText>
|
|
93
|
+
</Button>
|
|
94
|
+
),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const Link: Story = {
|
|
98
|
+
args: {
|
|
99
|
+
variant: 'link',
|
|
100
|
+
color: 'primary',
|
|
101
|
+
},
|
|
102
|
+
render: (args) => (
|
|
103
|
+
<Button {...args}>
|
|
104
|
+
<ButtonText>Link Button</ButtonText>
|
|
105
|
+
</Button>
|
|
106
|
+
),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const Loading: Story = {
|
|
110
|
+
args: {
|
|
111
|
+
isLoading: true,
|
|
112
|
+
loadingText: 'Saving...',
|
|
113
|
+
variant: 'solid',
|
|
114
|
+
color: 'primary',
|
|
115
|
+
},
|
|
116
|
+
render: (args) => (
|
|
117
|
+
<Button {...args}>
|
|
118
|
+
<ButtonText>Save</ButtonText>
|
|
119
|
+
</Button>
|
|
120
|
+
),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const Disabled: Story = {
|
|
124
|
+
args: {
|
|
125
|
+
isDisabled: true,
|
|
126
|
+
variant: 'solid',
|
|
127
|
+
color: 'primary',
|
|
128
|
+
},
|
|
129
|
+
render: (args) => (
|
|
130
|
+
<Button {...args}>
|
|
131
|
+
<ButtonText>Disabled Button</ButtonText>
|
|
132
|
+
</Button>
|
|
133
|
+
),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const AllVariants: Story = {
|
|
137
|
+
render: () => (
|
|
138
|
+
<View style={{ gap: 16 }}>
|
|
139
|
+
<View style={{ flexDirection: 'row', gap: 8, flexWrap: 'wrap' }}>
|
|
140
|
+
<Button variant="solid" color="primary">
|
|
141
|
+
<ButtonText>Solid</ButtonText>
|
|
142
|
+
</Button>
|
|
143
|
+
<Button variant="outline" color="primary">
|
|
144
|
+
<ButtonText>Outline</ButtonText>
|
|
145
|
+
</Button>
|
|
146
|
+
<Button variant="ghost" color="primary">
|
|
147
|
+
<ButtonText>Ghost</ButtonText>
|
|
148
|
+
</Button>
|
|
149
|
+
<Button variant="link" color="primary">
|
|
150
|
+
<ButtonText>Link</ButtonText>
|
|
151
|
+
</Button>
|
|
152
|
+
</View>
|
|
153
|
+
</View>
|
|
154
|
+
),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const AllColors: Story = {
|
|
158
|
+
render: () => (
|
|
159
|
+
<View style={{ gap: 8 }}>
|
|
160
|
+
<View style={{ flexDirection: 'row', gap: 8, flexWrap: 'wrap' }}>
|
|
161
|
+
<Button color="primary"><ButtonText>Primary</ButtonText></Button>
|
|
162
|
+
<Button color="secondary"><ButtonText>Secondary</ButtonText></Button>
|
|
163
|
+
<Button color="success"><ButtonText>Success</ButtonText></Button>
|
|
164
|
+
<Button color="error"><ButtonText>Error</ButtonText></Button>
|
|
165
|
+
<Button color="warning"><ButtonText>Warning</ButtonText></Button>
|
|
166
|
+
<Button color="info"><ButtonText>Info</ButtonText></Button>
|
|
167
|
+
</View>
|
|
168
|
+
</View>
|
|
169
|
+
),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export const AllSizes: Story = {
|
|
173
|
+
render: () => (
|
|
174
|
+
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
|
|
175
|
+
<Button size="sm"><ButtonText>Small</ButtonText></Button>
|
|
176
|
+
<Button size="md"><ButtonText>Medium</ButtonText></Button>
|
|
177
|
+
<Button size="lg"><ButtonText>Large</ButtonText></Button>
|
|
178
|
+
</View>
|
|
179
|
+
),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Simple plus icon component for demos
|
|
183
|
+
function PlusIcon({ size = 16 }: { size?: number }) {
|
|
184
|
+
return (
|
|
185
|
+
<View style={{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }}>
|
|
186
|
+
<View style={{ position: 'absolute', width: size * 0.6, height: 2, backgroundColor: 'currentColor', borderRadius: 1 }} />
|
|
187
|
+
<View style={{ position: 'absolute', width: 2, height: size * 0.6, backgroundColor: 'currentColor', borderRadius: 1 }} />
|
|
188
|
+
</View>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export const IconButton: Story = {
|
|
193
|
+
render: () => (
|
|
194
|
+
<View style={{ gap: 16 }}>
|
|
195
|
+
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
|
|
196
|
+
<Button isIconButton size="sm" color="primary">
|
|
197
|
+
<ButtonIcon as={PlusIcon} size={14} />
|
|
198
|
+
</Button>
|
|
199
|
+
<Button isIconButton size="md" color="primary">
|
|
200
|
+
<ButtonIcon as={PlusIcon} size={16} />
|
|
201
|
+
</Button>
|
|
202
|
+
<Button isIconButton size="lg" color="primary">
|
|
203
|
+
<ButtonIcon as={PlusIcon} size={20} />
|
|
204
|
+
</Button>
|
|
205
|
+
</View>
|
|
206
|
+
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
|
|
207
|
+
<Button isIconButton variant="outline" color="primary">
|
|
208
|
+
<ButtonIcon as={PlusIcon} />
|
|
209
|
+
</Button>
|
|
210
|
+
<Button isIconButton variant="ghost" color="primary">
|
|
211
|
+
<ButtonIcon as={PlusIcon} />
|
|
212
|
+
</Button>
|
|
213
|
+
</View>
|
|
214
|
+
</View>
|
|
215
|
+
),
|
|
216
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
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 { Button, ButtonText } from './Button'
|
|
5
|
+
|
|
6
|
+
describe('Button', () => {
|
|
7
|
+
it('renders children correctly', () => {
|
|
8
|
+
render(
|
|
9
|
+
<Button>
|
|
10
|
+
<ButtonText>Click me</ButtonText>
|
|
11
|
+
</Button>
|
|
12
|
+
)
|
|
13
|
+
expect(screen.getByText('Click me')).toBeInTheDocument()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('handles press events', () => {
|
|
17
|
+
const onPress = vi.fn()
|
|
18
|
+
render(
|
|
19
|
+
<Button onPress={onPress}>
|
|
20
|
+
<ButtonText>Click</ButtonText>
|
|
21
|
+
</Button>
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
fireEvent.click(screen.getByRole('button'))
|
|
25
|
+
expect(onPress).toHaveBeenCalledTimes(1)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('respects isDisabled prop', () => {
|
|
29
|
+
const onPress = vi.fn()
|
|
30
|
+
render(
|
|
31
|
+
<Button isDisabled onPress={onPress}>
|
|
32
|
+
<ButtonText>Disabled</ButtonText>
|
|
33
|
+
</Button>
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const button = screen.getByRole('button')
|
|
37
|
+
expect(button).toBeDisabled()
|
|
38
|
+
|
|
39
|
+
fireEvent.click(button)
|
|
40
|
+
expect(onPress).not.toHaveBeenCalled()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('respects isLoading prop', () => {
|
|
44
|
+
const onPress = vi.fn()
|
|
45
|
+
render(
|
|
46
|
+
<Button isLoading loadingText="Loading..." onPress={onPress}>
|
|
47
|
+
<ButtonText>Submit</ButtonText>
|
|
48
|
+
</Button>
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument()
|
|
52
|
+
|
|
53
|
+
fireEvent.click(screen.getByRole('button'))
|
|
54
|
+
expect(onPress).not.toHaveBeenCalled()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('applies variant styles correctly', () => {
|
|
58
|
+
const { rerender } = render(
|
|
59
|
+
<Button variant="solid" testID="button">
|
|
60
|
+
<ButtonText>Solid</ButtonText>
|
|
61
|
+
</Button>
|
|
62
|
+
)
|
|
63
|
+
// Button should have solid styles
|
|
64
|
+
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
65
|
+
|
|
66
|
+
rerender(
|
|
67
|
+
<Button variant="outline" testID="button">
|
|
68
|
+
<ButtonText>Outline</ButtonText>
|
|
69
|
+
</Button>
|
|
70
|
+
)
|
|
71
|
+
// Button should have outline styles
|
|
72
|
+
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('applies size styles correctly', () => {
|
|
76
|
+
const { rerender } = render(
|
|
77
|
+
<Button size="sm">
|
|
78
|
+
<ButtonText>Small</ButtonText>
|
|
79
|
+
</Button>
|
|
80
|
+
)
|
|
81
|
+
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
82
|
+
|
|
83
|
+
rerender(
|
|
84
|
+
<Button size="lg">
|
|
85
|
+
<ButtonText>Large</ButtonText>
|
|
86
|
+
</Button>
|
|
87
|
+
)
|
|
88
|
+
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('applies color variants correctly', () => {
|
|
92
|
+
render(
|
|
93
|
+
<Button color="primary">
|
|
94
|
+
<ButtonText>Primary</ButtonText>
|
|
95
|
+
</Button>
|
|
96
|
+
)
|
|
97
|
+
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('accessibility', () => {
|
|
101
|
+
it('has no accessibility violations', async () => {
|
|
102
|
+
const { container } = render(
|
|
103
|
+
<Button>
|
|
104
|
+
<ButtonText>Accessible Button</ButtonText>
|
|
105
|
+
</Button>
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const results = await axe(container)
|
|
109
|
+
expect(results).toHaveNoViolations()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('has correct accessibility role', () => {
|
|
113
|
+
render(
|
|
114
|
+
<Button>
|
|
115
|
+
<ButtonText>Button</ButtonText>
|
|
116
|
+
</Button>
|
|
117
|
+
)
|
|
118
|
+
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('communicates disabled state to assistive technology', () => {
|
|
122
|
+
render(
|
|
123
|
+
<Button isDisabled>
|
|
124
|
+
<ButtonText>Disabled</ButtonText>
|
|
125
|
+
</Button>
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
const button = screen.getByRole('button')
|
|
129
|
+
expect(button).toHaveAttribute('aria-disabled', 'true')
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
})
|