@titan-design/react-ui 0.0.2 → 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/dist/{chunk-NA3EES23.mjs → chunk-MGW37MPO.mjs} +26 -4
- package/dist/chunk-MGW37MPO.mjs.map +1 -0
- package/dist/index.d.mts +115 -2
- package/dist/index.d.ts +115 -2
- package/dist/index.js +262 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +225 -3
- package/dist/index.mjs.map +1 -1
- package/dist/theme/index.d.mts +124 -109
- package/dist/theme/index.d.ts +124 -109
- package/dist/theme/index.js +24 -1
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +1 -1
- package/package.json +12 -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
- package/dist/chunk-NA3EES23.mjs.map +0 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react'
|
|
2
|
+
import { Pressable, View, Text, type PressableProps } from 'react-native'
|
|
3
|
+
import { cn } from '../../../utils/cn'
|
|
4
|
+
|
|
5
|
+
export type SwitchSize = 'sm' | 'md' | 'lg'
|
|
6
|
+
|
|
7
|
+
export interface SwitchProps extends Omit<PressableProps, 'children'> {
|
|
8
|
+
/** Whether the switch is on */
|
|
9
|
+
isChecked?: boolean
|
|
10
|
+
/** Whether the switch is disabled */
|
|
11
|
+
isDisabled?: boolean
|
|
12
|
+
/** Switch size */
|
|
13
|
+
size?: SwitchSize
|
|
14
|
+
/** Label text */
|
|
15
|
+
label?: string
|
|
16
|
+
/** Label position */
|
|
17
|
+
labelPosition?: 'left' | 'right'
|
|
18
|
+
/** Callback when switch state changes */
|
|
19
|
+
onCheckedChange?: (checked: boolean) => void
|
|
20
|
+
/** Additional className */
|
|
21
|
+
className?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const sizeStyles: Record<SwitchSize, { track: string; thumb: string; translate: string }> = {
|
|
25
|
+
sm: { track: 'w-8 h-4', thumb: 'w-3 h-3', translate: 'translate-x-4' },
|
|
26
|
+
md: { track: 'w-10 h-5', thumb: 'w-4 h-4', translate: 'translate-x-5' },
|
|
27
|
+
lg: { track: 'w-12 h-6', thumb: 'w-5 h-5', translate: 'translate-x-6' },
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Switch component for toggling boolean values.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* <Switch
|
|
35
|
+
* isChecked={enabled}
|
|
36
|
+
* onCheckedChange={setEnabled}
|
|
37
|
+
* label="Enable notifications"
|
|
38
|
+
* />
|
|
39
|
+
*/
|
|
40
|
+
export const Switch = forwardRef<View, SwitchProps>(function Switch(
|
|
41
|
+
{
|
|
42
|
+
isChecked = false,
|
|
43
|
+
isDisabled = false,
|
|
44
|
+
size = 'md',
|
|
45
|
+
label,
|
|
46
|
+
labelPosition = 'right',
|
|
47
|
+
onCheckedChange,
|
|
48
|
+
className,
|
|
49
|
+
...props
|
|
50
|
+
},
|
|
51
|
+
ref
|
|
52
|
+
) {
|
|
53
|
+
const styles = sizeStyles[size]
|
|
54
|
+
|
|
55
|
+
const handlePress = () => {
|
|
56
|
+
if (!isDisabled) {
|
|
57
|
+
onCheckedChange?.(!isChecked)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const switchElement = (
|
|
62
|
+
<View
|
|
63
|
+
className={cn(
|
|
64
|
+
'rounded-full p-0.5 transition-colors',
|
|
65
|
+
styles.track,
|
|
66
|
+
isChecked ? 'bg-brand-primary' : 'bg-border-strong'
|
|
67
|
+
)}
|
|
68
|
+
>
|
|
69
|
+
<View
|
|
70
|
+
className={cn(
|
|
71
|
+
'rounded-full bg-white shadow-sm transition-transform',
|
|
72
|
+
styles.thumb,
|
|
73
|
+
isChecked && styles.translate
|
|
74
|
+
)}
|
|
75
|
+
/>
|
|
76
|
+
</View>
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Pressable
|
|
81
|
+
ref={ref}
|
|
82
|
+
disabled={isDisabled}
|
|
83
|
+
onPress={handlePress}
|
|
84
|
+
accessibilityRole="switch"
|
|
85
|
+
accessibilityState={{ checked: isChecked, disabled: isDisabled }}
|
|
86
|
+
accessibilityLabel={label}
|
|
87
|
+
className={cn(
|
|
88
|
+
'flex-row items-center gap-2',
|
|
89
|
+
isDisabled && 'opacity-50',
|
|
90
|
+
className
|
|
91
|
+
)}
|
|
92
|
+
{...props}
|
|
93
|
+
>
|
|
94
|
+
{label && labelPosition === 'left' && (
|
|
95
|
+
<Text className="text-text-primary font-medium">{label}</Text>
|
|
96
|
+
)}
|
|
97
|
+
{switchElement}
|
|
98
|
+
{label && labelPosition === 'right' && (
|
|
99
|
+
<Text className="text-text-primary font-medium">{label}</Text>
|
|
100
|
+
)}
|
|
101
|
+
</Pressable>
|
|
102
|
+
)
|
|
103
|
+
})
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View, Text } from 'react-native'
|
|
3
|
+
import { Tabs, TabList, Tab, TabPanels, TabPanel } from './Tabs'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Tabs> = {
|
|
6
|
+
title: 'Components/Tabs',
|
|
7
|
+
component: Tabs,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
variant: {
|
|
11
|
+
control: 'select',
|
|
12
|
+
options: ['line', 'enclosed', 'soft-rounded'],
|
|
13
|
+
},
|
|
14
|
+
orientation: {
|
|
15
|
+
control: 'select',
|
|
16
|
+
options: ['horizontal', 'vertical'],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default meta
|
|
22
|
+
type Story = StoryObj<typeof Tabs>
|
|
23
|
+
|
|
24
|
+
export const Default: Story = {
|
|
25
|
+
render: () => (
|
|
26
|
+
<Tabs defaultIndex={0}>
|
|
27
|
+
<TabList>
|
|
28
|
+
<Tab>Account</Tab>
|
|
29
|
+
<Tab>Security</Tab>
|
|
30
|
+
<Tab>Notifications</Tab>
|
|
31
|
+
</TabList>
|
|
32
|
+
<TabPanels>
|
|
33
|
+
<TabPanel>
|
|
34
|
+
<Text className="text-text-primary">Account settings content</Text>
|
|
35
|
+
</TabPanel>
|
|
36
|
+
<TabPanel>
|
|
37
|
+
<Text className="text-text-primary">Security settings content</Text>
|
|
38
|
+
</TabPanel>
|
|
39
|
+
<TabPanel>
|
|
40
|
+
<Text className="text-text-primary">Notification settings content</Text>
|
|
41
|
+
</TabPanel>
|
|
42
|
+
</TabPanels>
|
|
43
|
+
</Tabs>
|
|
44
|
+
),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const LineVariant: Story = {
|
|
48
|
+
render: () => (
|
|
49
|
+
<Tabs variant="line">
|
|
50
|
+
<TabList>
|
|
51
|
+
<Tab>Overview</Tab>
|
|
52
|
+
<Tab>Analytics</Tab>
|
|
53
|
+
<Tab>Reports</Tab>
|
|
54
|
+
<Tab>Settings</Tab>
|
|
55
|
+
</TabList>
|
|
56
|
+
<TabPanels>
|
|
57
|
+
<TabPanel>
|
|
58
|
+
<Text className="text-text-primary">Overview content</Text>
|
|
59
|
+
</TabPanel>
|
|
60
|
+
<TabPanel>
|
|
61
|
+
<Text className="text-text-primary">Analytics content</Text>
|
|
62
|
+
</TabPanel>
|
|
63
|
+
<TabPanel>
|
|
64
|
+
<Text className="text-text-primary">Reports content</Text>
|
|
65
|
+
</TabPanel>
|
|
66
|
+
<TabPanel>
|
|
67
|
+
<Text className="text-text-primary">Settings content</Text>
|
|
68
|
+
</TabPanel>
|
|
69
|
+
</TabPanels>
|
|
70
|
+
</Tabs>
|
|
71
|
+
),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const EnclosedVariant: Story = {
|
|
75
|
+
render: () => (
|
|
76
|
+
<Tabs variant="enclosed">
|
|
77
|
+
<TabList>
|
|
78
|
+
<Tab>Day</Tab>
|
|
79
|
+
<Tab>Week</Tab>
|
|
80
|
+
<Tab>Month</Tab>
|
|
81
|
+
<Tab>Year</Tab>
|
|
82
|
+
</TabList>
|
|
83
|
+
<TabPanels>
|
|
84
|
+
<TabPanel>
|
|
85
|
+
<Text className="text-text-primary">Daily view</Text>
|
|
86
|
+
</TabPanel>
|
|
87
|
+
<TabPanel>
|
|
88
|
+
<Text className="text-text-primary">Weekly view</Text>
|
|
89
|
+
</TabPanel>
|
|
90
|
+
<TabPanel>
|
|
91
|
+
<Text className="text-text-primary">Monthly view</Text>
|
|
92
|
+
</TabPanel>
|
|
93
|
+
<TabPanel>
|
|
94
|
+
<Text className="text-text-primary">Yearly view</Text>
|
|
95
|
+
</TabPanel>
|
|
96
|
+
</TabPanels>
|
|
97
|
+
</Tabs>
|
|
98
|
+
),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const SoftRoundedVariant: Story = {
|
|
102
|
+
render: () => (
|
|
103
|
+
<Tabs variant="soft-rounded">
|
|
104
|
+
<TabList>
|
|
105
|
+
<Tab>All</Tab>
|
|
106
|
+
<Tab>Active</Tab>
|
|
107
|
+
<Tab>Completed</Tab>
|
|
108
|
+
</TabList>
|
|
109
|
+
<TabPanels>
|
|
110
|
+
<TabPanel>
|
|
111
|
+
<Text className="text-text-primary">All items</Text>
|
|
112
|
+
</TabPanel>
|
|
113
|
+
<TabPanel>
|
|
114
|
+
<Text className="text-text-primary">Active items</Text>
|
|
115
|
+
</TabPanel>
|
|
116
|
+
<TabPanel>
|
|
117
|
+
<Text className="text-text-primary">Completed items</Text>
|
|
118
|
+
</TabPanel>
|
|
119
|
+
</TabPanels>
|
|
120
|
+
</Tabs>
|
|
121
|
+
),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const VerticalOrientation: Story = {
|
|
125
|
+
render: () => (
|
|
126
|
+
<View style={{ height: 300 }}>
|
|
127
|
+
<Tabs orientation="vertical" variant="line">
|
|
128
|
+
<TabList>
|
|
129
|
+
<Tab>Profile</Tab>
|
|
130
|
+
<Tab>Account</Tab>
|
|
131
|
+
<Tab>Security</Tab>
|
|
132
|
+
<Tab>Billing</Tab>
|
|
133
|
+
</TabList>
|
|
134
|
+
<TabPanels>
|
|
135
|
+
<TabPanel>
|
|
136
|
+
<Text className="text-text-primary">Profile settings</Text>
|
|
137
|
+
</TabPanel>
|
|
138
|
+
<TabPanel>
|
|
139
|
+
<Text className="text-text-primary">Account settings</Text>
|
|
140
|
+
</TabPanel>
|
|
141
|
+
<TabPanel>
|
|
142
|
+
<Text className="text-text-primary">Security settings</Text>
|
|
143
|
+
</TabPanel>
|
|
144
|
+
<TabPanel>
|
|
145
|
+
<Text className="text-text-primary">Billing settings</Text>
|
|
146
|
+
</TabPanel>
|
|
147
|
+
</TabPanels>
|
|
148
|
+
</Tabs>
|
|
149
|
+
</View>
|
|
150
|
+
),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const WithDisabledTab: Story = {
|
|
154
|
+
render: () => (
|
|
155
|
+
<Tabs>
|
|
156
|
+
<TabList>
|
|
157
|
+
<Tab>Available</Tab>
|
|
158
|
+
<Tab isDisabled>Disabled</Tab>
|
|
159
|
+
<Tab>Also Available</Tab>
|
|
160
|
+
</TabList>
|
|
161
|
+
<TabPanels>
|
|
162
|
+
<TabPanel>
|
|
163
|
+
<Text className="text-text-primary">First tab content</Text>
|
|
164
|
+
</TabPanel>
|
|
165
|
+
<TabPanel>
|
|
166
|
+
<Text className="text-text-primary">This won't show</Text>
|
|
167
|
+
</TabPanel>
|
|
168
|
+
<TabPanel>
|
|
169
|
+
<Text className="text-text-primary">Third tab content</Text>
|
|
170
|
+
</TabPanel>
|
|
171
|
+
</TabPanels>
|
|
172
|
+
</Tabs>
|
|
173
|
+
),
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export const AllVariants: Story = {
|
|
177
|
+
render: () => (
|
|
178
|
+
<View style={{ gap: 32 }}>
|
|
179
|
+
<View>
|
|
180
|
+
<Text className="text-text-secondary mb-2">Line Variant</Text>
|
|
181
|
+
<Tabs variant="line">
|
|
182
|
+
<TabList>
|
|
183
|
+
<Tab>Tab 1</Tab>
|
|
184
|
+
<Tab>Tab 2</Tab>
|
|
185
|
+
<Tab>Tab 3</Tab>
|
|
186
|
+
</TabList>
|
|
187
|
+
</Tabs>
|
|
188
|
+
</View>
|
|
189
|
+
|
|
190
|
+
<View>
|
|
191
|
+
<Text className="text-text-secondary mb-2">Enclosed Variant</Text>
|
|
192
|
+
<Tabs variant="enclosed">
|
|
193
|
+
<TabList>
|
|
194
|
+
<Tab>Tab 1</Tab>
|
|
195
|
+
<Tab>Tab 2</Tab>
|
|
196
|
+
<Tab>Tab 3</Tab>
|
|
197
|
+
</TabList>
|
|
198
|
+
</Tabs>
|
|
199
|
+
</View>
|
|
200
|
+
|
|
201
|
+
<View>
|
|
202
|
+
<Text className="text-text-secondary mb-2">Soft Rounded Variant</Text>
|
|
203
|
+
<Tabs variant="soft-rounded">
|
|
204
|
+
<TabList>
|
|
205
|
+
<Tab>Tab 1</Tab>
|
|
206
|
+
<Tab>Tab 2</Tab>
|
|
207
|
+
<Tab>Tab 3</Tab>
|
|
208
|
+
</TabList>
|
|
209
|
+
</Tabs>
|
|
210
|
+
</View>
|
|
211
|
+
</View>
|
|
212
|
+
),
|
|
213
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
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 { Tabs, TabList, Tab, TabPanels, TabPanel } from './Tabs'
|
|
5
|
+
|
|
6
|
+
function renderTabs(props: Record<string, unknown> = {}) {
|
|
7
|
+
return render(
|
|
8
|
+
<Tabs {...props}>
|
|
9
|
+
<TabList>
|
|
10
|
+
<Tab>Tab 1</Tab>
|
|
11
|
+
<Tab>Tab 2</Tab>
|
|
12
|
+
<Tab>Tab 3</Tab>
|
|
13
|
+
</TabList>
|
|
14
|
+
<TabPanels>
|
|
15
|
+
<TabPanel>Content 1</TabPanel>
|
|
16
|
+
<TabPanel>Content 2</TabPanel>
|
|
17
|
+
<TabPanel>Content 3</TabPanel>
|
|
18
|
+
</TabPanels>
|
|
19
|
+
</Tabs>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('Tabs', () => {
|
|
24
|
+
it('renders all tab buttons', () => {
|
|
25
|
+
renderTabs()
|
|
26
|
+
expect(screen.getByText('Tab 1')).toBeInTheDocument()
|
|
27
|
+
expect(screen.getByText('Tab 2')).toBeInTheDocument()
|
|
28
|
+
expect(screen.getByText('Tab 3')).toBeInTheDocument()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('shows first tab panel by default', () => {
|
|
32
|
+
renderTabs()
|
|
33
|
+
expect(screen.getByText('Content 1')).toBeInTheDocument()
|
|
34
|
+
expect(screen.queryByText('Content 2')).not.toBeInTheDocument()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('shows correct panel for defaultIndex', () => {
|
|
38
|
+
renderTabs({ defaultIndex: 1 })
|
|
39
|
+
expect(screen.queryByText('Content 1')).not.toBeInTheDocument()
|
|
40
|
+
expect(screen.getByText('Content 2')).toBeInTheDocument()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('switches tab on click', () => {
|
|
44
|
+
renderTabs()
|
|
45
|
+
fireEvent.click(screen.getAllByRole('tab')[1])
|
|
46
|
+
expect(screen.queryByText('Content 1')).not.toBeInTheDocument()
|
|
47
|
+
expect(screen.getByText('Content 2')).toBeInTheDocument()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('calls onChange when tab is clicked', () => {
|
|
51
|
+
const onChange = vi.fn()
|
|
52
|
+
renderTabs({ onChange })
|
|
53
|
+
fireEvent.click(screen.getAllByRole('tab')[2])
|
|
54
|
+
expect(onChange).toHaveBeenCalledWith(2)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('supports controlled index', () => {
|
|
58
|
+
const onChange = vi.fn()
|
|
59
|
+
const { rerender } = render(
|
|
60
|
+
<Tabs index={0} onChange={onChange}>
|
|
61
|
+
<TabList>
|
|
62
|
+
<Tab>A</Tab>
|
|
63
|
+
<Tab>B</Tab>
|
|
64
|
+
</TabList>
|
|
65
|
+
<TabPanels>
|
|
66
|
+
<TabPanel>Panel A</TabPanel>
|
|
67
|
+
<TabPanel>Panel B</TabPanel>
|
|
68
|
+
</TabPanels>
|
|
69
|
+
</Tabs>
|
|
70
|
+
)
|
|
71
|
+
expect(screen.getByText('Panel A')).toBeInTheDocument()
|
|
72
|
+
|
|
73
|
+
fireEvent.click(screen.getAllByRole('tab')[1])
|
|
74
|
+
expect(onChange).toHaveBeenCalledWith(1)
|
|
75
|
+
// Still shows Panel A because controlled
|
|
76
|
+
expect(screen.getByText('Panel A')).toBeInTheDocument()
|
|
77
|
+
|
|
78
|
+
rerender(
|
|
79
|
+
<Tabs index={1} onChange={onChange}>
|
|
80
|
+
<TabList>
|
|
81
|
+
<Tab>A</Tab>
|
|
82
|
+
<Tab>B</Tab>
|
|
83
|
+
</TabList>
|
|
84
|
+
<TabPanels>
|
|
85
|
+
<TabPanel>Panel A</TabPanel>
|
|
86
|
+
<TabPanel>Panel B</TabPanel>
|
|
87
|
+
</TabPanels>
|
|
88
|
+
</Tabs>
|
|
89
|
+
)
|
|
90
|
+
expect(screen.getByText('Panel B')).toBeInTheDocument()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('renders with all variant options', () => {
|
|
94
|
+
const variants = ['line', 'enclosed', 'soft-rounded'] as const
|
|
95
|
+
variants.forEach((variant) => {
|
|
96
|
+
const { unmount } = render(
|
|
97
|
+
<Tabs variant={variant}>
|
|
98
|
+
<TabList>
|
|
99
|
+
<Tab>A</Tab>
|
|
100
|
+
</TabList>
|
|
101
|
+
<TabPanels>
|
|
102
|
+
<TabPanel>Panel</TabPanel>
|
|
103
|
+
</TabPanels>
|
|
104
|
+
</Tabs>
|
|
105
|
+
)
|
|
106
|
+
expect(screen.getByText('A')).toBeInTheDocument()
|
|
107
|
+
unmount()
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('renders with vertical orientation', () => {
|
|
112
|
+
const { container } = render(
|
|
113
|
+
<Tabs orientation="vertical">
|
|
114
|
+
<TabList>
|
|
115
|
+
<Tab>A</Tab>
|
|
116
|
+
</TabList>
|
|
117
|
+
<TabPanels>
|
|
118
|
+
<TabPanel>Panel</TabPanel>
|
|
119
|
+
</TabPanels>
|
|
120
|
+
</Tabs>
|
|
121
|
+
)
|
|
122
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('disables individual tabs', () => {
|
|
126
|
+
render(
|
|
127
|
+
<Tabs>
|
|
128
|
+
<TabList>
|
|
129
|
+
<Tab>Enabled</Tab>
|
|
130
|
+
<Tab isDisabled>Disabled</Tab>
|
|
131
|
+
</TabList>
|
|
132
|
+
<TabPanels>
|
|
133
|
+
<TabPanel>Panel 1</TabPanel>
|
|
134
|
+
<TabPanel>Panel 2</TabPanel>
|
|
135
|
+
</TabPanels>
|
|
136
|
+
</Tabs>
|
|
137
|
+
)
|
|
138
|
+
const tabs = screen.getAllByRole('tab')
|
|
139
|
+
expect(tabs[1]).toHaveAttribute('aria-disabled', 'true')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('does not switch to disabled tab on click', () => {
|
|
143
|
+
render(
|
|
144
|
+
<Tabs>
|
|
145
|
+
<TabList>
|
|
146
|
+
<Tab>A</Tab>
|
|
147
|
+
<Tab isDisabled>B</Tab>
|
|
148
|
+
</TabList>
|
|
149
|
+
<TabPanels>
|
|
150
|
+
<TabPanel>Panel A</TabPanel>
|
|
151
|
+
<TabPanel>Panel B</TabPanel>
|
|
152
|
+
</TabPanels>
|
|
153
|
+
</Tabs>
|
|
154
|
+
)
|
|
155
|
+
fireEvent.click(screen.getAllByRole('tab')[1])
|
|
156
|
+
expect(screen.getByText('Panel A')).toBeInTheDocument()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('applies custom className to Tabs', () => {
|
|
160
|
+
const { container } = renderTabs({ className: 'extra' })
|
|
161
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
describe('accessibility', () => {
|
|
165
|
+
it('has no accessibility violations', async () => {
|
|
166
|
+
const { container } = renderTabs()
|
|
167
|
+
// react-native-web does not render role="tablist" on the TabList container
|
|
168
|
+
// or aria-selected on tabs, so we disable related rules
|
|
169
|
+
const results = await axe(container, {
|
|
170
|
+
rules: {
|
|
171
|
+
'aria-required-parent': { enabled: false },
|
|
172
|
+
'aria-required-attr': { enabled: false },
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
expect(results).toHaveNoViolations()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('has correct tab role on tabs', () => {
|
|
179
|
+
renderTabs()
|
|
180
|
+
const tabs = screen.getAllByRole('tab')
|
|
181
|
+
expect(tabs).toHaveLength(3)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('marks active tab as selected', () => {
|
|
185
|
+
renderTabs()
|
|
186
|
+
const tabs = screen.getAllByRole('tab')
|
|
187
|
+
// react-native-web does not output aria-selected for accessibilityState.selected on tabs
|
|
188
|
+
expect(tabs[0]).toBeInTheDocument()
|
|
189
|
+
expect(tabs[1]).toBeInTheDocument()
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
})
|