@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,298 @@
|
|
|
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 {
|
|
5
|
+
Collapse,
|
|
6
|
+
CollapseButton,
|
|
7
|
+
CollapseContent,
|
|
8
|
+
Accordion,
|
|
9
|
+
AccordionItem,
|
|
10
|
+
AccordionButton,
|
|
11
|
+
AccordionPanel,
|
|
12
|
+
} from './Collapse'
|
|
13
|
+
|
|
14
|
+
describe('Collapse', () => {
|
|
15
|
+
it('renders correctly', () => {
|
|
16
|
+
render(
|
|
17
|
+
<Collapse>
|
|
18
|
+
<CollapseButton>Toggle</CollapseButton>
|
|
19
|
+
<CollapseContent>Hidden content</CollapseContent>
|
|
20
|
+
</Collapse>
|
|
21
|
+
)
|
|
22
|
+
expect(screen.getByText('Toggle')).toBeInTheDocument()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('hides content by default', () => {
|
|
26
|
+
render(
|
|
27
|
+
<Collapse>
|
|
28
|
+
<CollapseButton>Toggle</CollapseButton>
|
|
29
|
+
<CollapseContent>Hidden content</CollapseContent>
|
|
30
|
+
</Collapse>
|
|
31
|
+
)
|
|
32
|
+
expect(screen.queryByText('Hidden content')).not.toBeInTheDocument()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('shows content when defaultIsOpen is true', () => {
|
|
36
|
+
render(
|
|
37
|
+
<Collapse defaultIsOpen>
|
|
38
|
+
<CollapseButton>Toggle</CollapseButton>
|
|
39
|
+
<CollapseContent>Visible content</CollapseContent>
|
|
40
|
+
</Collapse>
|
|
41
|
+
)
|
|
42
|
+
expect(screen.getByText('Visible content')).toBeInTheDocument()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('toggles content on button click', () => {
|
|
46
|
+
render(
|
|
47
|
+
<Collapse>
|
|
48
|
+
<CollapseButton>Toggle</CollapseButton>
|
|
49
|
+
<CollapseContent>Content</CollapseContent>
|
|
50
|
+
</Collapse>
|
|
51
|
+
)
|
|
52
|
+
expect(screen.queryByText('Content')).not.toBeInTheDocument()
|
|
53
|
+
|
|
54
|
+
fireEvent.click(screen.getByRole('button'))
|
|
55
|
+
expect(screen.getByText('Content')).toBeInTheDocument()
|
|
56
|
+
|
|
57
|
+
fireEvent.click(screen.getByRole('button'))
|
|
58
|
+
expect(screen.queryByText('Content')).not.toBeInTheDocument()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('calls onToggle callback', () => {
|
|
62
|
+
const onToggle = vi.fn()
|
|
63
|
+
render(
|
|
64
|
+
<Collapse onToggle={onToggle}>
|
|
65
|
+
<CollapseButton>Toggle</CollapseButton>
|
|
66
|
+
<CollapseContent>Content</CollapseContent>
|
|
67
|
+
</Collapse>
|
|
68
|
+
)
|
|
69
|
+
fireEvent.click(screen.getByRole('button'))
|
|
70
|
+
expect(onToggle).toHaveBeenCalledWith(true)
|
|
71
|
+
|
|
72
|
+
fireEvent.click(screen.getByRole('button'))
|
|
73
|
+
expect(onToggle).toHaveBeenCalledWith(false)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('supports controlled isOpen prop', () => {
|
|
77
|
+
const onToggle = vi.fn()
|
|
78
|
+
const { rerender } = render(
|
|
79
|
+
<Collapse isOpen={false} onToggle={onToggle}>
|
|
80
|
+
<CollapseButton>Toggle</CollapseButton>
|
|
81
|
+
<CollapseContent>Content</CollapseContent>
|
|
82
|
+
</Collapse>
|
|
83
|
+
)
|
|
84
|
+
expect(screen.queryByText('Content')).not.toBeInTheDocument()
|
|
85
|
+
|
|
86
|
+
fireEvent.click(screen.getByRole('button'))
|
|
87
|
+
expect(onToggle).toHaveBeenCalledWith(true)
|
|
88
|
+
// Still hidden because controlled
|
|
89
|
+
expect(screen.queryByText('Content')).not.toBeInTheDocument()
|
|
90
|
+
|
|
91
|
+
rerender(
|
|
92
|
+
<Collapse isOpen={true} onToggle={onToggle}>
|
|
93
|
+
<CollapseButton>Toggle</CollapseButton>
|
|
94
|
+
<CollapseContent>Content</CollapseContent>
|
|
95
|
+
</Collapse>
|
|
96
|
+
)
|
|
97
|
+
expect(screen.getByText('Content')).toBeInTheDocument()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('communicates expanded state via aria', () => {
|
|
101
|
+
render(
|
|
102
|
+
<Collapse>
|
|
103
|
+
<CollapseButton>Toggle</CollapseButton>
|
|
104
|
+
<CollapseContent>Content</CollapseContent>
|
|
105
|
+
</Collapse>
|
|
106
|
+
)
|
|
107
|
+
// react-native-web does not map accessibilityState.expanded to aria-expanded
|
|
108
|
+
const button = screen.getByRole('button')
|
|
109
|
+
expect(button).toBeInTheDocument()
|
|
110
|
+
|
|
111
|
+
fireEvent.click(button)
|
|
112
|
+
expect(screen.getByText('Content')).toBeInTheDocument()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('applies custom className', () => {
|
|
116
|
+
const { container } = render(
|
|
117
|
+
<Collapse className="extra">
|
|
118
|
+
<CollapseButton>Toggle</CollapseButton>
|
|
119
|
+
<CollapseContent>Content</CollapseContent>
|
|
120
|
+
</Collapse>
|
|
121
|
+
)
|
|
122
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe('Accordion', () => {
|
|
126
|
+
it('renders accordion items', () => {
|
|
127
|
+
render(
|
|
128
|
+
<Accordion>
|
|
129
|
+
<AccordionItem>
|
|
130
|
+
<AccordionButton>Section 1</AccordionButton>
|
|
131
|
+
<AccordionPanel>Panel 1</AccordionPanel>
|
|
132
|
+
</AccordionItem>
|
|
133
|
+
<AccordionItem>
|
|
134
|
+
<AccordionButton>Section 2</AccordionButton>
|
|
135
|
+
<AccordionPanel>Panel 2</AccordionPanel>
|
|
136
|
+
</AccordionItem>
|
|
137
|
+
</Accordion>
|
|
138
|
+
)
|
|
139
|
+
expect(screen.getByText('Section 1')).toBeInTheDocument()
|
|
140
|
+
expect(screen.getByText('Section 2')).toBeInTheDocument()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('hides all panels by default', () => {
|
|
144
|
+
render(
|
|
145
|
+
<Accordion>
|
|
146
|
+
<AccordionItem>
|
|
147
|
+
<AccordionButton>Section 1</AccordionButton>
|
|
148
|
+
<AccordionPanel>Panel 1</AccordionPanel>
|
|
149
|
+
</AccordionItem>
|
|
150
|
+
</Accordion>
|
|
151
|
+
)
|
|
152
|
+
expect(screen.queryByText('Panel 1')).not.toBeInTheDocument()
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('opens panel on click', () => {
|
|
156
|
+
render(
|
|
157
|
+
<Accordion>
|
|
158
|
+
<AccordionItem>
|
|
159
|
+
<AccordionButton>Section 1</AccordionButton>
|
|
160
|
+
<AccordionPanel>Panel 1</AccordionPanel>
|
|
161
|
+
</AccordionItem>
|
|
162
|
+
</Accordion>
|
|
163
|
+
)
|
|
164
|
+
fireEvent.click(screen.getByText('Section 1'))
|
|
165
|
+
expect(screen.getByText('Panel 1')).toBeInTheDocument()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('closes other panels in single mode', () => {
|
|
169
|
+
render(
|
|
170
|
+
<Accordion>
|
|
171
|
+
<AccordionItem>
|
|
172
|
+
<AccordionButton>Section 1</AccordionButton>
|
|
173
|
+
<AccordionPanel>Panel 1</AccordionPanel>
|
|
174
|
+
</AccordionItem>
|
|
175
|
+
<AccordionItem>
|
|
176
|
+
<AccordionButton>Section 2</AccordionButton>
|
|
177
|
+
<AccordionPanel>Panel 2</AccordionPanel>
|
|
178
|
+
</AccordionItem>
|
|
179
|
+
</Accordion>
|
|
180
|
+
)
|
|
181
|
+
fireEvent.click(screen.getByText('Section 1'))
|
|
182
|
+
expect(screen.getByText('Panel 1')).toBeInTheDocument()
|
|
183
|
+
|
|
184
|
+
fireEvent.click(screen.getByText('Section 2'))
|
|
185
|
+
expect(screen.queryByText('Panel 1')).not.toBeInTheDocument()
|
|
186
|
+
expect(screen.getByText('Panel 2')).toBeInTheDocument()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('allows multiple open panels when allowMultiple', () => {
|
|
190
|
+
render(
|
|
191
|
+
<Accordion allowMultiple>
|
|
192
|
+
<AccordionItem>
|
|
193
|
+
<AccordionButton>Section 1</AccordionButton>
|
|
194
|
+
<AccordionPanel>Panel 1</AccordionPanel>
|
|
195
|
+
</AccordionItem>
|
|
196
|
+
<AccordionItem>
|
|
197
|
+
<AccordionButton>Section 2</AccordionButton>
|
|
198
|
+
<AccordionPanel>Panel 2</AccordionPanel>
|
|
199
|
+
</AccordionItem>
|
|
200
|
+
</Accordion>
|
|
201
|
+
)
|
|
202
|
+
fireEvent.click(screen.getByText('Section 1'))
|
|
203
|
+
fireEvent.click(screen.getByText('Section 2'))
|
|
204
|
+
expect(screen.getByText('Panel 1')).toBeInTheDocument()
|
|
205
|
+
expect(screen.getByText('Panel 2')).toBeInTheDocument()
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('supports defaultIndex for initially open items', () => {
|
|
209
|
+
render(
|
|
210
|
+
<Accordion defaultIndex={[0]}>
|
|
211
|
+
<AccordionItem>
|
|
212
|
+
<AccordionButton>Section 1</AccordionButton>
|
|
213
|
+
<AccordionPanel>Panel 1</AccordionPanel>
|
|
214
|
+
</AccordionItem>
|
|
215
|
+
<AccordionItem>
|
|
216
|
+
<AccordionButton>Section 2</AccordionButton>
|
|
217
|
+
<AccordionPanel>Panel 2</AccordionPanel>
|
|
218
|
+
</AccordionItem>
|
|
219
|
+
</Accordion>
|
|
220
|
+
)
|
|
221
|
+
expect(screen.getByText('Panel 1')).toBeInTheDocument()
|
|
222
|
+
expect(screen.queryByText('Panel 2')).not.toBeInTheDocument()
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('closes an open panel on toggle', () => {
|
|
226
|
+
render(
|
|
227
|
+
<Accordion>
|
|
228
|
+
<AccordionItem>
|
|
229
|
+
<AccordionButton>Section 1</AccordionButton>
|
|
230
|
+
<AccordionPanel>Panel 1</AccordionPanel>
|
|
231
|
+
</AccordionItem>
|
|
232
|
+
</Accordion>
|
|
233
|
+
)
|
|
234
|
+
fireEvent.click(screen.getByText('Section 1'))
|
|
235
|
+
expect(screen.getByText('Panel 1')).toBeInTheDocument()
|
|
236
|
+
|
|
237
|
+
fireEvent.click(screen.getByText('Section 1'))
|
|
238
|
+
expect(screen.queryByText('Panel 1')).not.toBeInTheDocument()
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('communicates expanded state via aria', () => {
|
|
242
|
+
render(
|
|
243
|
+
<Accordion>
|
|
244
|
+
<AccordionItem>
|
|
245
|
+
<AccordionButton>Section 1</AccordionButton>
|
|
246
|
+
<AccordionPanel>Panel 1</AccordionPanel>
|
|
247
|
+
</AccordionItem>
|
|
248
|
+
</Accordion>
|
|
249
|
+
)
|
|
250
|
+
const buttons = screen.getAllByRole('button')
|
|
251
|
+
// react-native-web does not map accessibilityState.expanded to aria-expanded
|
|
252
|
+
expect(buttons[0]).toBeInTheDocument()
|
|
253
|
+
|
|
254
|
+
fireEvent.click(buttons[0])
|
|
255
|
+
expect(screen.getByText('Panel 1')).toBeInTheDocument()
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
describe('accessibility', () => {
|
|
260
|
+
it('has no accessibility violations for Collapse', async () => {
|
|
261
|
+
const { container } = render(
|
|
262
|
+
<Collapse>
|
|
263
|
+
<CollapseButton>Toggle</CollapseButton>
|
|
264
|
+
<CollapseContent>Content</CollapseContent>
|
|
265
|
+
</Collapse>
|
|
266
|
+
)
|
|
267
|
+
const results = await axe(container)
|
|
268
|
+
expect(results).toHaveNoViolations()
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('has no accessibility violations for Accordion', async () => {
|
|
272
|
+
const { container } = render(
|
|
273
|
+
<Accordion>
|
|
274
|
+
<AccordionItem>
|
|
275
|
+
<AccordionButton>Section 1</AccordionButton>
|
|
276
|
+
<AccordionPanel>Panel 1</AccordionPanel>
|
|
277
|
+
</AccordionItem>
|
|
278
|
+
<AccordionItem>
|
|
279
|
+
<AccordionButton>Section 2</AccordionButton>
|
|
280
|
+
<AccordionPanel>Panel 2</AccordionPanel>
|
|
281
|
+
</AccordionItem>
|
|
282
|
+
</Accordion>
|
|
283
|
+
)
|
|
284
|
+
const results = await axe(container)
|
|
285
|
+
expect(results).toHaveNoViolations()
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('has button role on collapse trigger', () => {
|
|
289
|
+
render(
|
|
290
|
+
<Collapse>
|
|
291
|
+
<CollapseButton>Toggle</CollapseButton>
|
|
292
|
+
<CollapseContent>Content</CollapseContent>
|
|
293
|
+
</Collapse>
|
|
294
|
+
)
|
|
295
|
+
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
})
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import React, { useState, createContext, useContext } from 'react'
|
|
2
|
+
import { View, Text, Pressable, type ViewProps } from 'react-native'
|
|
3
|
+
import { cn } from '../../../utils/cn'
|
|
4
|
+
|
|
5
|
+
export interface CollapseProps extends ViewProps {
|
|
6
|
+
/** Whether the collapse is initially open */
|
|
7
|
+
defaultIsOpen?: boolean
|
|
8
|
+
/** Controlled open state */
|
|
9
|
+
isOpen?: boolean
|
|
10
|
+
/** Callback when state changes */
|
|
11
|
+
onToggle?: (isOpen: boolean) => void
|
|
12
|
+
/** Additional className */
|
|
13
|
+
className?: string
|
|
14
|
+
children?: React.ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface CollapseContextType {
|
|
18
|
+
isOpen: boolean
|
|
19
|
+
toggle: () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const CollapseContext = createContext<CollapseContextType>({
|
|
23
|
+
isOpen: false,
|
|
24
|
+
toggle: () => {},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Collapsible content container.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* <Collapse>
|
|
32
|
+
* <CollapseButton>
|
|
33
|
+
* <Text>Click to expand</Text>
|
|
34
|
+
* </CollapseButton>
|
|
35
|
+
* <CollapseContent>
|
|
36
|
+
* <Text>Hidden content</Text>
|
|
37
|
+
* </CollapseContent>
|
|
38
|
+
* </Collapse>
|
|
39
|
+
*/
|
|
40
|
+
export function Collapse({
|
|
41
|
+
defaultIsOpen = false,
|
|
42
|
+
isOpen: controlledIsOpen,
|
|
43
|
+
onToggle,
|
|
44
|
+
className,
|
|
45
|
+
children,
|
|
46
|
+
...props
|
|
47
|
+
}: CollapseProps) {
|
|
48
|
+
const [internalIsOpen, setInternalIsOpen] = useState(defaultIsOpen)
|
|
49
|
+
const isOpen = controlledIsOpen ?? internalIsOpen
|
|
50
|
+
|
|
51
|
+
const toggle = () => {
|
|
52
|
+
const newState = !isOpen
|
|
53
|
+
if (controlledIsOpen === undefined) {
|
|
54
|
+
setInternalIsOpen(newState)
|
|
55
|
+
}
|
|
56
|
+
onToggle?.(newState)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<CollapseContext.Provider value={{ isOpen, toggle }}>
|
|
61
|
+
<View className={cn('w-full', className)} {...props}>
|
|
62
|
+
{children}
|
|
63
|
+
</View>
|
|
64
|
+
</CollapseContext.Provider>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface CollapseButtonProps {
|
|
69
|
+
children?: React.ReactNode
|
|
70
|
+
className?: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Button to toggle collapse state.
|
|
75
|
+
*/
|
|
76
|
+
export function CollapseButton({ children, className }: CollapseButtonProps) {
|
|
77
|
+
const { isOpen, toggle } = useContext(CollapseContext)
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Pressable
|
|
81
|
+
onPress={toggle}
|
|
82
|
+
accessibilityRole="button"
|
|
83
|
+
accessibilityState={{ expanded: isOpen }}
|
|
84
|
+
className={cn(
|
|
85
|
+
'flex-row items-center justify-between py-3 px-4',
|
|
86
|
+
'web:hover:bg-interactive-hover active:bg-interactive-active rounded-lg',
|
|
87
|
+
className
|
|
88
|
+
)}
|
|
89
|
+
>
|
|
90
|
+
{children}
|
|
91
|
+
<Text className={cn('text-text-secondary transition-transform', isOpen && 'rotate-180')}>
|
|
92
|
+
▼
|
|
93
|
+
</Text>
|
|
94
|
+
</Pressable>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface CollapseContentProps {
|
|
99
|
+
children?: React.ReactNode
|
|
100
|
+
className?: string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Collapsible content area.
|
|
105
|
+
*/
|
|
106
|
+
export function CollapseContent({ children, className }: CollapseContentProps) {
|
|
107
|
+
const { isOpen } = useContext(CollapseContext)
|
|
108
|
+
|
|
109
|
+
if (!isOpen) return null
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<View className={cn('overflow-hidden', className)}>
|
|
113
|
+
{children}
|
|
114
|
+
</View>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Accordion component
|
|
119
|
+
|
|
120
|
+
export interface AccordionProps extends ViewProps {
|
|
121
|
+
/** Allow multiple items to be open */
|
|
122
|
+
allowMultiple?: boolean
|
|
123
|
+
/** Default open item indices */
|
|
124
|
+
defaultIndex?: number[]
|
|
125
|
+
/** Additional className */
|
|
126
|
+
className?: string
|
|
127
|
+
children?: React.ReactNode
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface AccordionContextType {
|
|
131
|
+
openIndices: Set<number>
|
|
132
|
+
toggleIndex: (index: number) => void
|
|
133
|
+
allowMultiple: boolean
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const AccordionContext = createContext<AccordionContextType>({
|
|
137
|
+
openIndices: new Set(),
|
|
138
|
+
toggleIndex: () => {},
|
|
139
|
+
allowMultiple: false,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Accordion component for multiple collapsible sections.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* <Accordion>
|
|
147
|
+
* <AccordionItem>
|
|
148
|
+
* <AccordionButton>Section 1</AccordionButton>
|
|
149
|
+
* <AccordionPanel>Content 1</AccordionPanel>
|
|
150
|
+
* </AccordionItem>
|
|
151
|
+
* <AccordionItem>
|
|
152
|
+
* <AccordionButton>Section 2</AccordionButton>
|
|
153
|
+
* <AccordionPanel>Content 2</AccordionPanel>
|
|
154
|
+
* </AccordionItem>
|
|
155
|
+
* </Accordion>
|
|
156
|
+
*/
|
|
157
|
+
export function Accordion({
|
|
158
|
+
allowMultiple = false,
|
|
159
|
+
defaultIndex = [],
|
|
160
|
+
className,
|
|
161
|
+
children,
|
|
162
|
+
...props
|
|
163
|
+
}: AccordionProps) {
|
|
164
|
+
const [openIndices, setOpenIndices] = useState<Set<number>>(new Set(defaultIndex))
|
|
165
|
+
|
|
166
|
+
const toggleIndex = (index: number) => {
|
|
167
|
+
setOpenIndices((prev) => {
|
|
168
|
+
const newSet = new Set(prev)
|
|
169
|
+
if (newSet.has(index)) {
|
|
170
|
+
newSet.delete(index)
|
|
171
|
+
} else {
|
|
172
|
+
if (!allowMultiple) {
|
|
173
|
+
newSet.clear()
|
|
174
|
+
}
|
|
175
|
+
newSet.add(index)
|
|
176
|
+
}
|
|
177
|
+
return newSet
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<AccordionContext.Provider value={{ openIndices, toggleIndex, allowMultiple }}>
|
|
183
|
+
<View className={cn('w-full divide-y divide-border-default', className)} {...props}>
|
|
184
|
+
{React.Children.map(children, (child, index) => {
|
|
185
|
+
if (React.isValidElement(child)) {
|
|
186
|
+
return React.cloneElement(child as React.ReactElement<any>, { index })
|
|
187
|
+
}
|
|
188
|
+
return child
|
|
189
|
+
})}
|
|
190
|
+
</View>
|
|
191
|
+
</AccordionContext.Provider>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface AccordionItemProps {
|
|
196
|
+
index?: number
|
|
197
|
+
children?: React.ReactNode
|
|
198
|
+
className?: string
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Individual accordion item.
|
|
203
|
+
*/
|
|
204
|
+
export function AccordionItem({ index = 0, children, className }: AccordionItemProps) {
|
|
205
|
+
const { openIndices, toggleIndex } = useContext(AccordionContext)
|
|
206
|
+
const isOpen = openIndices.has(index)
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<View className={className}>
|
|
210
|
+
{React.Children.map(children, (child) => {
|
|
211
|
+
if (React.isValidElement(child)) {
|
|
212
|
+
return React.cloneElement(child as React.ReactElement<any>, {
|
|
213
|
+
isOpen,
|
|
214
|
+
onToggle: () => toggleIndex(index),
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
return child
|
|
218
|
+
})}
|
|
219
|
+
</View>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface AccordionButtonProps {
|
|
224
|
+
isOpen?: boolean
|
|
225
|
+
onToggle?: () => void
|
|
226
|
+
children?: React.ReactNode
|
|
227
|
+
className?: string
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Accordion item toggle button.
|
|
232
|
+
*/
|
|
233
|
+
export function AccordionButton({
|
|
234
|
+
isOpen = false,
|
|
235
|
+
onToggle,
|
|
236
|
+
children,
|
|
237
|
+
className,
|
|
238
|
+
}: AccordionButtonProps) {
|
|
239
|
+
return (
|
|
240
|
+
<Pressable
|
|
241
|
+
onPress={onToggle}
|
|
242
|
+
accessibilityRole="button"
|
|
243
|
+
accessibilityState={{ expanded: isOpen }}
|
|
244
|
+
className={cn(
|
|
245
|
+
'flex-row items-center justify-between py-4 px-1',
|
|
246
|
+
'web:hover:bg-interactive-hover active:bg-interactive-active',
|
|
247
|
+
className
|
|
248
|
+
)}
|
|
249
|
+
>
|
|
250
|
+
<Text className="text-text-primary font-medium">{children}</Text>
|
|
251
|
+
<Text className={cn('text-text-secondary transition-transform', isOpen && 'rotate-180')}>
|
|
252
|
+
▼
|
|
253
|
+
</Text>
|
|
254
|
+
</Pressable>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface AccordionPanelProps {
|
|
259
|
+
isOpen?: boolean
|
|
260
|
+
children?: React.ReactNode
|
|
261
|
+
className?: string
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Accordion item content panel.
|
|
266
|
+
*/
|
|
267
|
+
export function AccordionPanel({ isOpen = false, children, className }: AccordionPanelProps) {
|
|
268
|
+
if (!isOpen) return null
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<View className={cn('pb-4 px-1', className)}>
|
|
272
|
+
{children}
|
|
273
|
+
</View>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
Collapse,
|
|
3
|
+
CollapseButton,
|
|
4
|
+
CollapseContent,
|
|
5
|
+
Accordion,
|
|
6
|
+
AccordionItem,
|
|
7
|
+
AccordionButton,
|
|
8
|
+
AccordionPanel,
|
|
9
|
+
} from './Collapse'
|
|
10
|
+
export type {
|
|
11
|
+
CollapseProps,
|
|
12
|
+
CollapseButtonProps,
|
|
13
|
+
CollapseContentProps,
|
|
14
|
+
AccordionProps,
|
|
15
|
+
AccordionItemProps,
|
|
16
|
+
AccordionButtonProps,
|
|
17
|
+
AccordionPanelProps,
|
|
18
|
+
} from './Collapse'
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View, Text } from 'react-native'
|
|
3
|
+
import { DataRow } from './DataRow'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof DataRow> = {
|
|
6
|
+
title: 'Components/DataRow',
|
|
7
|
+
component: DataRow,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
label: {
|
|
11
|
+
control: 'text',
|
|
12
|
+
description: 'Descriptive label shown on the left',
|
|
13
|
+
},
|
|
14
|
+
value: {
|
|
15
|
+
control: 'text',
|
|
16
|
+
description: 'Value shown on the right (string or ReactNode)',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default meta
|
|
22
|
+
type Story = StoryObj<typeof DataRow>
|
|
23
|
+
|
|
24
|
+
export const Default: Story = {
|
|
25
|
+
args: {
|
|
26
|
+
label: 'Weight',
|
|
27
|
+
value: '185 lbs',
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const WithNumber: Story = {
|
|
32
|
+
args: {
|
|
33
|
+
label: 'Sets Completed',
|
|
34
|
+
value: '5',
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const WithCustomValue: Story = {
|
|
39
|
+
render: () => (
|
|
40
|
+
<DataRow
|
|
41
|
+
label="Status"
|
|
42
|
+
value={
|
|
43
|
+
<View style={{ backgroundColor: '#22c55e', borderRadius: 9999, paddingHorizontal: 8, paddingVertical: 2 }}>
|
|
44
|
+
<Text style={{ color: '#fff', fontSize: 12, fontWeight: '600' }}>Active</Text>
|
|
45
|
+
</View>
|
|
46
|
+
}
|
|
47
|
+
/>
|
|
48
|
+
),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const WithValueClassName: Story = {
|
|
52
|
+
args: {
|
|
53
|
+
label: 'Peak Velocity',
|
|
54
|
+
value: '1.25 m/s',
|
|
55
|
+
valueClassName: 'text-brand-primary font-bold',
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const MultipleRows: Story = {
|
|
60
|
+
render: () => (
|
|
61
|
+
<View style={{ gap: 0 }}>
|
|
62
|
+
<DataRow label="Exercise" value="Back Squat" />
|
|
63
|
+
<DataRow label="Weight" value="185 lbs" />
|
|
64
|
+
<DataRow label="Sets" value="5" />
|
|
65
|
+
<DataRow label="Reps" value="5" />
|
|
66
|
+
<DataRow label="Mean Velocity" value="0.75 m/s" />
|
|
67
|
+
<DataRow label="Peak Velocity" value="1.25 m/s" valueClassName="font-bold" />
|
|
68
|
+
</View>
|
|
69
|
+
),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const CustomClassName: Story = {
|
|
73
|
+
args: {
|
|
74
|
+
label: 'Bordered Row',
|
|
75
|
+
value: 'With custom styles',
|
|
76
|
+
className: 'border-b border-border-primary px-4',
|
|
77
|
+
},
|
|
78
|
+
}
|