@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,307 @@
|
|
|
1
|
+
import React, { useState, useCallback, createContext, useContext } from 'react'
|
|
2
|
+
import { View, Text, Pressable, type ViewProps, StyleSheet, Platform } from 'react-native'
|
|
3
|
+
import { cn } from '../../../utils/cn'
|
|
4
|
+
import { neumorphicShadows, getHoverColors } from '../../../theme'
|
|
5
|
+
|
|
6
|
+
export type ToolbarButtonVariant = 'default' | 'neumorphic'
|
|
7
|
+
export type ToolbarButtonSize = 'sm' | 'md' | 'lg'
|
|
8
|
+
|
|
9
|
+
interface ToolbarButtonContextType {
|
|
10
|
+
isOpen: boolean
|
|
11
|
+
setIsOpen: (open: boolean) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ToolbarButtonContext = createContext<ToolbarButtonContextType>({
|
|
15
|
+
isOpen: false,
|
|
16
|
+
setIsOpen: () => {},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export interface ToolbarButtonProps extends ViewProps {
|
|
20
|
+
/** Button label */
|
|
21
|
+
label: string
|
|
22
|
+
/** Icon component */
|
|
23
|
+
icon?: React.ReactNode
|
|
24
|
+
/**
|
|
25
|
+
* Whether the button is in active/selected state.
|
|
26
|
+
* - `true`: pressed/active appearance (darker, white text, orange icon)
|
|
27
|
+
* - `undefined`: pressed appearance (darker, white text/icon)
|
|
28
|
+
* - `false`: raised/inactive appearance (lighter, gray text/icon)
|
|
29
|
+
*/
|
|
30
|
+
isActive?: boolean
|
|
31
|
+
/** Whether the button is disabled */
|
|
32
|
+
isDisabled?: boolean
|
|
33
|
+
/** Size of the button */
|
|
34
|
+
size?: ToolbarButtonSize
|
|
35
|
+
/** Visual variant */
|
|
36
|
+
variant?: ToolbarButtonVariant
|
|
37
|
+
/** Tooltip text (shown when label is hidden) */
|
|
38
|
+
tooltip?: string
|
|
39
|
+
/** Whether to show label (false = icon only) */
|
|
40
|
+
showLabel?: boolean
|
|
41
|
+
/** Press handler */
|
|
42
|
+
onPress?: () => void
|
|
43
|
+
/** Menu content (renders in popover) */
|
|
44
|
+
menuContent?: React.ReactNode
|
|
45
|
+
/** Additional className */
|
|
46
|
+
className?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Base button color
|
|
50
|
+
const BUTTON_BG = '#3C3C3C'
|
|
51
|
+
const MENU_BG = '#2C2C2C'
|
|
52
|
+
|
|
53
|
+
// Calculate hover colors using color math
|
|
54
|
+
const hoverColors = getHoverColors(BUTTON_BG, 'medium')
|
|
55
|
+
|
|
56
|
+
// Size style maps
|
|
57
|
+
const sizeStyles: Record<ToolbarButtonSize, string> = {
|
|
58
|
+
sm: 'px-2 py-1 min-h-[26px]',
|
|
59
|
+
md: 'px-2.5 py-1 min-h-[30px]',
|
|
60
|
+
lg: 'px-3 py-1.5 min-h-[36px]',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const textSizeStyles: Record<ToolbarButtonSize, string> = {
|
|
64
|
+
sm: 'text-xs',
|
|
65
|
+
md: 'text-sm',
|
|
66
|
+
lg: 'text-base',
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* ToolbarButton component for toolbar actions with toggle state support.
|
|
71
|
+
*
|
|
72
|
+
* Features:
|
|
73
|
+
* - Neumorphic styling with inset shadows for pressed/raised 3D effect
|
|
74
|
+
* - Active (pressed) state is the default visual appearance
|
|
75
|
+
* - Set isActive={false} explicitly for the raised/inactive appearance
|
|
76
|
+
* - Orange accent color on icon when isActive={true}
|
|
77
|
+
* - Hover states that lighten background and remove shadows
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* // Toggle button - orange icon when active
|
|
81
|
+
* <ToolbarButton
|
|
82
|
+
* label="Settings"
|
|
83
|
+
* icon={<SettingsIcon />}
|
|
84
|
+
* isActive={isSettingsOpen}
|
|
85
|
+
* onPress={() => setIsSettingsOpen(!isSettingsOpen)}
|
|
86
|
+
* />
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* // Explicitly inactive (raised appearance)
|
|
90
|
+
* <ToolbarButton
|
|
91
|
+
* label="Filter"
|
|
92
|
+
* icon={<FilterIcon />}
|
|
93
|
+
* isActive={false}
|
|
94
|
+
* />
|
|
95
|
+
*/
|
|
96
|
+
export function ToolbarButton({
|
|
97
|
+
label,
|
|
98
|
+
icon,
|
|
99
|
+
isActive,
|
|
100
|
+
isDisabled = false,
|
|
101
|
+
size = 'md',
|
|
102
|
+
variant = 'neumorphic',
|
|
103
|
+
tooltip,
|
|
104
|
+
showLabel = true,
|
|
105
|
+
onPress,
|
|
106
|
+
menuContent,
|
|
107
|
+
className,
|
|
108
|
+
children,
|
|
109
|
+
...props
|
|
110
|
+
}: ToolbarButtonProps) {
|
|
111
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
112
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
113
|
+
|
|
114
|
+
const handlePress = useCallback(() => {
|
|
115
|
+
if (isDisabled) return
|
|
116
|
+
if (menuContent) {
|
|
117
|
+
setIsOpen((prev) => !prev)
|
|
118
|
+
}
|
|
119
|
+
onPress?.()
|
|
120
|
+
}, [isDisabled, menuContent, onPress])
|
|
121
|
+
|
|
122
|
+
const handleClose = useCallback(() => {
|
|
123
|
+
setIsOpen(false)
|
|
124
|
+
}, [])
|
|
125
|
+
|
|
126
|
+
// Match original logic: active !== false means "active" (sunken) appearance
|
|
127
|
+
const selected = isActive === undefined ? isOpen : isActive
|
|
128
|
+
const showActive = isActive !== false
|
|
129
|
+
|
|
130
|
+
// Determine icon color based on state
|
|
131
|
+
const getIconColor = () => {
|
|
132
|
+
if (isActive === true) return '#FF7900' // brand-primary
|
|
133
|
+
return showActive ? '#FFFFFF' : '#D1D1D1'
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Get shadow and background styles for neumorphic variant
|
|
137
|
+
const getNeumorphicStyle = () => {
|
|
138
|
+
if (isDisabled) {
|
|
139
|
+
return styles.disabledBg
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const shadows = neumorphicShadows.charcoal
|
|
143
|
+
|
|
144
|
+
if (showActive) {
|
|
145
|
+
// Active = pressed/sunken appearance
|
|
146
|
+
return {
|
|
147
|
+
backgroundColor: isHovered ? hoverColors.pressed : BUTTON_BG,
|
|
148
|
+
...(isHovered ? shadows.pressedHover.subtle : shadows.pressed.subtle),
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
// Inactive = raised/elevated appearance
|
|
152
|
+
return {
|
|
153
|
+
backgroundColor: isHovered ? hoverColors.raised : BUTTON_BG,
|
|
154
|
+
...(isHovered ? shadows.raisedHover.subtle : shadows.raised.subtle),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<ToolbarButtonContext.Provider value={{ isOpen, setIsOpen }}>
|
|
161
|
+
<View className="relative" {...props}>
|
|
162
|
+
<Pressable
|
|
163
|
+
onPress={handlePress}
|
|
164
|
+
onHoverIn={() => setIsHovered(true)}
|
|
165
|
+
onHoverOut={() => setIsHovered(false)}
|
|
166
|
+
disabled={isDisabled}
|
|
167
|
+
accessibilityRole="button"
|
|
168
|
+
accessibilityState={{ selected, disabled: isDisabled }}
|
|
169
|
+
accessibilityLabel={label}
|
|
170
|
+
accessibilityHint={tooltip}
|
|
171
|
+
className={cn(
|
|
172
|
+
// Base styles
|
|
173
|
+
'flex-row items-center justify-center rounded',
|
|
174
|
+
'web:transition-all web:duration-150 web:cursor-pointer',
|
|
175
|
+
// Size
|
|
176
|
+
sizeStyles[size],
|
|
177
|
+
// Disabled
|
|
178
|
+
isDisabled && 'opacity-40 web:cursor-default web:pointer-events-none',
|
|
179
|
+
className
|
|
180
|
+
)}
|
|
181
|
+
style={[
|
|
182
|
+
variant === 'neumorphic' && getNeumorphicStyle(),
|
|
183
|
+
variant === 'default' && {
|
|
184
|
+
backgroundColor: showActive ? BUTTON_BG : '#6E6E6E',
|
|
185
|
+
},
|
|
186
|
+
]}
|
|
187
|
+
>
|
|
188
|
+
{icon && (
|
|
189
|
+
<View className="w-5 h-5 items-center justify-center">
|
|
190
|
+
{React.isValidElement(icon)
|
|
191
|
+
? React.cloneElement(icon as React.ReactElement<any>, {
|
|
192
|
+
size: 20,
|
|
193
|
+
width: 20,
|
|
194
|
+
height: 20,
|
|
195
|
+
color: getIconColor(),
|
|
196
|
+
style: {
|
|
197
|
+
color: getIconColor(),
|
|
198
|
+
width: 20,
|
|
199
|
+
height: 20,
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
: icon
|
|
203
|
+
}
|
|
204
|
+
</View>
|
|
205
|
+
)}
|
|
206
|
+
{showLabel && (
|
|
207
|
+
<Text
|
|
208
|
+
className={cn(
|
|
209
|
+
'font-bold',
|
|
210
|
+
textSizeStyles[size],
|
|
211
|
+
icon && 'ml-2',
|
|
212
|
+
showActive ? 'text-white' : 'text-[#D1D1D1]'
|
|
213
|
+
)}
|
|
214
|
+
numberOfLines={1}
|
|
215
|
+
>
|
|
216
|
+
{label}
|
|
217
|
+
</Text>
|
|
218
|
+
)}
|
|
219
|
+
</Pressable>
|
|
220
|
+
|
|
221
|
+
{/* Popover Menu */}
|
|
222
|
+
{menuContent && isOpen && (
|
|
223
|
+
<>
|
|
224
|
+
{/* Backdrop */}
|
|
225
|
+
<Pressable
|
|
226
|
+
onPress={handleClose}
|
|
227
|
+
style={StyleSheet.absoluteFill}
|
|
228
|
+
className="z-40"
|
|
229
|
+
/>
|
|
230
|
+
{/* Menu Content */}
|
|
231
|
+
<View
|
|
232
|
+
className={cn(
|
|
233
|
+
'absolute z-50 top-full left-0 mt-1',
|
|
234
|
+
'rounded-lg shadow-lg border border-border-default',
|
|
235
|
+
'min-w-[150px] overflow-hidden',
|
|
236
|
+
`bg-[${MENU_BG}]`
|
|
237
|
+
)}
|
|
238
|
+
>
|
|
239
|
+
{menuContent}
|
|
240
|
+
</View>
|
|
241
|
+
</>
|
|
242
|
+
)}
|
|
243
|
+
</View>
|
|
244
|
+
</ToolbarButtonContext.Provider>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Styles that can't be easily expressed in Tailwind
|
|
249
|
+
const styles = StyleSheet.create({
|
|
250
|
+
// Disabled - flat gray background, no shadows
|
|
251
|
+
disabledBg: {
|
|
252
|
+
backgroundColor: 'rgba(255, 255, 255, 0.12)',
|
|
253
|
+
...Platform.select({
|
|
254
|
+
web: { boxShadow: 'none' } as any,
|
|
255
|
+
default: { shadowOpacity: 0, elevation: 0 },
|
|
256
|
+
}),
|
|
257
|
+
},
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
export interface ToolbarButtonGroupProps extends ViewProps {
|
|
261
|
+
/** Orientation of the button group */
|
|
262
|
+
orientation?: 'horizontal' | 'vertical'
|
|
263
|
+
/** Gap between buttons */
|
|
264
|
+
gap?: 'none' | 'sm' | 'md'
|
|
265
|
+
/** Additional className */
|
|
266
|
+
className?: string
|
|
267
|
+
children?: React.ReactNode
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const gapStyles: Record<string, string> = {
|
|
271
|
+
none: 'gap-0',
|
|
272
|
+
sm: 'gap-1',
|
|
273
|
+
md: 'gap-2',
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Container for grouping toolbar buttons together.
|
|
278
|
+
*/
|
|
279
|
+
export function ToolbarButtonGroup({
|
|
280
|
+
orientation = 'horizontal',
|
|
281
|
+
gap = 'sm',
|
|
282
|
+
className,
|
|
283
|
+
children,
|
|
284
|
+
...props
|
|
285
|
+
}: ToolbarButtonGroupProps) {
|
|
286
|
+
return (
|
|
287
|
+
<View
|
|
288
|
+
className={cn(
|
|
289
|
+
orientation === 'horizontal' ? 'flex-row' : 'flex-col',
|
|
290
|
+
gapStyles[gap],
|
|
291
|
+
'items-center',
|
|
292
|
+
className
|
|
293
|
+
)}
|
|
294
|
+
accessibilityRole="toolbar"
|
|
295
|
+
{...props}
|
|
296
|
+
>
|
|
297
|
+
{children}
|
|
298
|
+
</View>
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Hook to access toolbar button context (for custom menu content)
|
|
304
|
+
*/
|
|
305
|
+
export function useToolbarButton() {
|
|
306
|
+
return useContext(ToolbarButtonContext)
|
|
307
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View, Text } from 'react-native'
|
|
3
|
+
import { Tooltip } from './Tooltip'
|
|
4
|
+
import { Button, ButtonText } from '../button'
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Tooltip> = {
|
|
7
|
+
title: 'Components/Tooltip',
|
|
8
|
+
component: Tooltip,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
argTypes: {
|
|
11
|
+
placement: {
|
|
12
|
+
control: 'select',
|
|
13
|
+
options: ['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'right'],
|
|
14
|
+
},
|
|
15
|
+
hasArrow: { control: 'boolean' },
|
|
16
|
+
isDisabled: { control: 'boolean' },
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default meta
|
|
21
|
+
type Story = StoryObj<typeof Tooltip>
|
|
22
|
+
|
|
23
|
+
export const Default: Story = {
|
|
24
|
+
render: () => (
|
|
25
|
+
<View style={{ padding: 100 }}>
|
|
26
|
+
<Tooltip label="This is a tooltip">
|
|
27
|
+
<Button>
|
|
28
|
+
<ButtonText>Hover me</ButtonText>
|
|
29
|
+
</Button>
|
|
30
|
+
</Tooltip>
|
|
31
|
+
</View>
|
|
32
|
+
),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const AllPlacements: Story = {
|
|
36
|
+
render: () => (
|
|
37
|
+
<View style={{ padding: 100, gap: 24 }}>
|
|
38
|
+
<View style={{ flexDirection: 'row', justifyContent: 'center', gap: 16 }}>
|
|
39
|
+
<Tooltip label="Top Start" placement="top-start">
|
|
40
|
+
<Button variant="outline" size="sm">
|
|
41
|
+
<ButtonText>Top Start</ButtonText>
|
|
42
|
+
</Button>
|
|
43
|
+
</Tooltip>
|
|
44
|
+
<Tooltip label="Top" placement="top">
|
|
45
|
+
<Button variant="outline" size="sm">
|
|
46
|
+
<ButtonText>Top</ButtonText>
|
|
47
|
+
</Button>
|
|
48
|
+
</Tooltip>
|
|
49
|
+
<Tooltip label="Top End" placement="top-end">
|
|
50
|
+
<Button variant="outline" size="sm">
|
|
51
|
+
<ButtonText>Top End</ButtonText>
|
|
52
|
+
</Button>
|
|
53
|
+
</Tooltip>
|
|
54
|
+
</View>
|
|
55
|
+
|
|
56
|
+
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
|
57
|
+
<Tooltip label="Left" placement="left">
|
|
58
|
+
<Button variant="outline" size="sm">
|
|
59
|
+
<ButtonText>Left</ButtonText>
|
|
60
|
+
</Button>
|
|
61
|
+
</Tooltip>
|
|
62
|
+
<Tooltip label="Right" placement="right">
|
|
63
|
+
<Button variant="outline" size="sm">
|
|
64
|
+
<ButtonText>Right</ButtonText>
|
|
65
|
+
</Button>
|
|
66
|
+
</Tooltip>
|
|
67
|
+
</View>
|
|
68
|
+
|
|
69
|
+
<View style={{ flexDirection: 'row', justifyContent: 'center', gap: 16 }}>
|
|
70
|
+
<Tooltip label="Bottom Start" placement="bottom-start">
|
|
71
|
+
<Button variant="outline" size="sm">
|
|
72
|
+
<ButtonText>Bottom Start</ButtonText>
|
|
73
|
+
</Button>
|
|
74
|
+
</Tooltip>
|
|
75
|
+
<Tooltip label="Bottom" placement="bottom">
|
|
76
|
+
<Button variant="outline" size="sm">
|
|
77
|
+
<ButtonText>Bottom</ButtonText>
|
|
78
|
+
</Button>
|
|
79
|
+
</Tooltip>
|
|
80
|
+
<Tooltip label="Bottom End" placement="bottom-end">
|
|
81
|
+
<Button variant="outline" size="sm">
|
|
82
|
+
<ButtonText>Bottom End</ButtonText>
|
|
83
|
+
</Button>
|
|
84
|
+
</Tooltip>
|
|
85
|
+
</View>
|
|
86
|
+
</View>
|
|
87
|
+
),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const WithDelay: Story = {
|
|
91
|
+
render: () => (
|
|
92
|
+
<View style={{ padding: 100 }}>
|
|
93
|
+
<Tooltip label="Appears after 500ms" openDelay={500}>
|
|
94
|
+
<Button>
|
|
95
|
+
<ButtonText>Hover (with delay)</ButtonText>
|
|
96
|
+
</Button>
|
|
97
|
+
</Tooltip>
|
|
98
|
+
</View>
|
|
99
|
+
),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const WithoutArrow: Story = {
|
|
103
|
+
render: () => (
|
|
104
|
+
<View style={{ padding: 100 }}>
|
|
105
|
+
<Tooltip label="No arrow" hasArrow={false}>
|
|
106
|
+
<Button>
|
|
107
|
+
<ButtonText>Hover me</ButtonText>
|
|
108
|
+
</Button>
|
|
109
|
+
</Tooltip>
|
|
110
|
+
</View>
|
|
111
|
+
),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const OnText: Story = {
|
|
115
|
+
render: () => (
|
|
116
|
+
<View style={{ padding: 100 }}>
|
|
117
|
+
<Text className="text-text-primary">
|
|
118
|
+
Hover over the{' '}
|
|
119
|
+
<Tooltip label="This is helpful information">
|
|
120
|
+
<Text className="text-brand-primary underline">underlined text</Text>
|
|
121
|
+
</Tooltip>
|
|
122
|
+
{' '}to see a tooltip.
|
|
123
|
+
</Text>
|
|
124
|
+
</View>
|
|
125
|
+
),
|
|
126
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { Tooltip } from './Tooltip'
|
|
5
|
+
|
|
6
|
+
function hoverTrigger(triggerText: string) {
|
|
7
|
+
const button = screen.getByText(triggerText)
|
|
8
|
+
// The Pressable wrapper is the parent element with tabindex
|
|
9
|
+
const pressable = button.closest('[tabindex]') ?? button
|
|
10
|
+
fireEvent.mouseEnter(pressable)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function unhoverTrigger(triggerText: string) {
|
|
14
|
+
const button = screen.getByText(triggerText)
|
|
15
|
+
const pressable = button.closest('[tabindex]') ?? button
|
|
16
|
+
fireEvent.mouseLeave(pressable)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('Tooltip', () => {
|
|
20
|
+
it('renders trigger children', () => {
|
|
21
|
+
render(
|
|
22
|
+
<Tooltip label="Tooltip text">
|
|
23
|
+
<button>Hover me</button>
|
|
24
|
+
</Tooltip>
|
|
25
|
+
)
|
|
26
|
+
expect(screen.getByText('Hover me')).toBeInTheDocument()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('does not show tooltip content by default', () => {
|
|
30
|
+
render(
|
|
31
|
+
<Tooltip label="Hidden tooltip">
|
|
32
|
+
<button>Trigger</button>
|
|
33
|
+
</Tooltip>
|
|
34
|
+
)
|
|
35
|
+
expect(screen.queryByText('Hidden tooltip')).not.toBeInTheDocument()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('shows tooltip on hover', () => {
|
|
39
|
+
render(
|
|
40
|
+
<Tooltip label="Visible tooltip">
|
|
41
|
+
<button>Hover me</button>
|
|
42
|
+
</Tooltip>
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
hoverTrigger('Hover me')
|
|
46
|
+
expect(screen.getByText('Visible tooltip')).toBeInTheDocument()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('hides tooltip on hover out', () => {
|
|
50
|
+
render(
|
|
51
|
+
<Tooltip label="Tooltip text">
|
|
52
|
+
<button>Hover me</button>
|
|
53
|
+
</Tooltip>
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
hoverTrigger('Hover me')
|
|
57
|
+
expect(screen.getByText('Tooltip text')).toBeInTheDocument()
|
|
58
|
+
|
|
59
|
+
unhoverTrigger('Hover me')
|
|
60
|
+
expect(screen.queryByText('Tooltip text')).not.toBeInTheDocument()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('disabled state', () => {
|
|
64
|
+
it('does not show tooltip when isDisabled is true', () => {
|
|
65
|
+
render(
|
|
66
|
+
<Tooltip label="Disabled tooltip" isDisabled>
|
|
67
|
+
<button>Trigger</button>
|
|
68
|
+
</Tooltip>
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
hoverTrigger('Trigger')
|
|
72
|
+
expect(screen.queryByText('Disabled tooltip')).not.toBeInTheDocument()
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('placement', () => {
|
|
77
|
+
it('renders with top placement (default)', () => {
|
|
78
|
+
render(
|
|
79
|
+
<Tooltip label="Top tooltip" placement="top">
|
|
80
|
+
<button>Trigger</button>
|
|
81
|
+
</Tooltip>
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
hoverTrigger('Trigger')
|
|
85
|
+
expect(screen.getByText('Top tooltip')).toBeInTheDocument()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('renders with bottom placement', () => {
|
|
89
|
+
render(
|
|
90
|
+
<Tooltip label="Bottom tooltip" placement="bottom">
|
|
91
|
+
<button>Trigger</button>
|
|
92
|
+
</Tooltip>
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
hoverTrigger('Trigger')
|
|
96
|
+
expect(screen.getByText('Bottom tooltip')).toBeInTheDocument()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('renders with left placement', () => {
|
|
100
|
+
render(
|
|
101
|
+
<Tooltip label="Left tooltip" placement="left">
|
|
102
|
+
<button>Trigger</button>
|
|
103
|
+
</Tooltip>
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
hoverTrigger('Trigger')
|
|
107
|
+
expect(screen.getByText('Left tooltip')).toBeInTheDocument()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('renders with right placement', () => {
|
|
111
|
+
render(
|
|
112
|
+
<Tooltip label="Right tooltip" placement="right">
|
|
113
|
+
<button>Trigger</button>
|
|
114
|
+
</Tooltip>
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
hoverTrigger('Trigger')
|
|
118
|
+
expect(screen.getByText('Right tooltip')).toBeInTheDocument()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('renders with top-start placement', () => {
|
|
122
|
+
render(
|
|
123
|
+
<Tooltip label="Top-start" placement="top-start">
|
|
124
|
+
<button>Trigger</button>
|
|
125
|
+
</Tooltip>
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
hoverTrigger('Trigger')
|
|
129
|
+
expect(screen.getByText('Top-start')).toBeInTheDocument()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('renders with bottom-end placement', () => {
|
|
133
|
+
render(
|
|
134
|
+
<Tooltip label="Bottom-end" placement="bottom-end">
|
|
135
|
+
<button>Trigger</button>
|
|
136
|
+
</Tooltip>
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
hoverTrigger('Trigger')
|
|
140
|
+
expect(screen.getByText('Bottom-end')).toBeInTheDocument()
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('arrow', () => {
|
|
145
|
+
it('renders with arrow by default', () => {
|
|
146
|
+
render(
|
|
147
|
+
<Tooltip label="With arrow">
|
|
148
|
+
<button>Trigger</button>
|
|
149
|
+
</Tooltip>
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
hoverTrigger('Trigger')
|
|
153
|
+
expect(screen.getByText('With arrow')).toBeInTheDocument()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('renders without arrow when hasArrow is false', () => {
|
|
157
|
+
render(
|
|
158
|
+
<Tooltip label="No arrow" hasArrow={false}>
|
|
159
|
+
<button>Trigger</button>
|
|
160
|
+
</Tooltip>
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
hoverTrigger('Trigger')
|
|
164
|
+
expect(screen.getByText('No arrow')).toBeInTheDocument()
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe('accessibility', () => {
|
|
169
|
+
it('has no accessibility violations', async () => {
|
|
170
|
+
const { container } = render(
|
|
171
|
+
<Tooltip label="Accessible tooltip">
|
|
172
|
+
<button>Trigger button</button>
|
|
173
|
+
</Tooltip>
|
|
174
|
+
)
|
|
175
|
+
const results = await axe(container)
|
|
176
|
+
expect(results).toHaveNoViolations()
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
})
|