@butternutbox/pawprint-native 0.0.1 → 0.1.0
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/.turbo/turbo-build.log +15 -15
- package/CHANGELOG.md +16 -0
- package/COMPONENT_GUIDELINES.md +111 -4
- package/dist/index.cjs +12370 -1455
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1110 -11
- package/dist/index.d.ts +1110 -11
- package/dist/index.js +12324 -1455
- package/dist/index.js.map +1 -1
- package/package.json +28 -9
- package/src/__mocks__/asset-stub.ts +1 -0
- package/src/__mocks__/emotion-native.tsx +18 -0
- package/src/__mocks__/react-native-gesture-handler.tsx +41 -0
- package/src/__mocks__/react-native-reanimated.tsx +79 -0
- package/src/__mocks__/react-native-safe-area-context.tsx +6 -0
- package/src/__mocks__/react-native-svg.tsx +27 -0
- package/src/__mocks__/react-native-worklets.tsx +11 -0
- package/src/__mocks__/react-native.tsx +338 -0
- package/src/__mocks__/rn-primitives/avatar.tsx +24 -0
- package/src/__mocks__/rn-primitives/checkbox.tsx +19 -0
- package/src/__mocks__/rn-primitives/select.tsx +116 -0
- package/src/__mocks__/rn-primitives/slider.tsx +40 -0
- package/src/__mocks__/rn-primitives/slot.tsx +30 -0
- package/src/__mocks__/rn-primitives/switch.tsx +24 -0
- package/src/__mocks__/rn-primitives/toggle.tsx +16 -0
- package/src/components/atoms/Avatar/Avatar.stories.tsx +57 -49
- package/src/components/atoms/Avatar/Avatar.test.tsx +269 -0
- package/src/components/atoms/Avatar/Avatar.tsx +68 -22
- package/src/components/atoms/Avatar/index.ts +1 -6
- package/src/components/atoms/Badge/Badge.stories.tsx +5 -29
- package/src/components/atoms/Badge/Badge.test.tsx +90 -0
- package/src/components/atoms/Button/Button.test.tsx +123 -0
- package/src/components/atoms/Button/Button.tsx +1 -1
- package/src/components/atoms/CarouselControls/CarouselControls.stories.tsx +217 -0
- package/src/components/atoms/CarouselControls/CarouselControls.tsx +127 -0
- package/src/components/atoms/CarouselControls/index.ts +2 -0
- package/src/components/atoms/Hint/Hint.test.tsx +36 -0
- package/src/components/atoms/Icon/Icon.test.tsx +98 -0
- package/src/components/atoms/Icon/Icon.tsx +5 -1
- package/src/components/atoms/IconButton/IconButton.test.tsx +101 -0
- package/src/components/atoms/Illustration/Illustration.test.tsx +55 -0
- package/src/components/atoms/Input/Input.stories.tsx +129 -86
- package/src/components/atoms/Input/Input.test.tsx +306 -0
- package/src/components/atoms/Input/Input.tsx +9 -1
- package/src/components/atoms/Input/InputField.tsx +226 -74
- package/src/components/atoms/Link/Link.test.tsx +89 -0
- package/src/components/atoms/Logo/Logo.registry.ts +30 -5
- package/src/components/atoms/Logo/Logo.stories.tsx +108 -0
- package/src/components/atoms/Logo/Logo.test.tsx +56 -0
- package/src/components/atoms/Logo/assets/BCorp.tsx +113 -0
- package/src/components/atoms/Logo/assets/ButternutFavicon.tsx +33 -0
- package/src/components/atoms/Logo/assets/ButternutPrimary.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutTabbedBottom.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutTabbedTop.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutWordmark.tsx +274 -0
- package/src/components/atoms/Logo/assets/PsiBufetFavicon.tsx +45 -0
- package/src/components/atoms/Logo/assets/PsiBufetPrimary.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetTabbedBottom.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetTabbedTop.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetWordmark.tsx +195 -0
- package/src/components/atoms/Logo/assets/index.ts +11 -0
- package/src/components/atoms/NumberInput/NumberInput.stories.tsx +183 -0
- package/src/components/atoms/NumberInput/NumberInput.test.tsx +261 -0
- package/src/components/atoms/NumberInput/NumberInput.tsx +129 -0
- package/src/components/atoms/NumberInput/NumberInputField.tsx +77 -0
- package/src/components/atoms/NumberInput/index.ts +4 -0
- package/src/components/atoms/Spinner/Spinner.test.tsx +46 -0
- package/src/components/atoms/Spinner/Spinner.tsx +14 -5
- package/src/components/atoms/Switch/Switch.test.tsx +92 -0
- package/src/components/atoms/Switch/Switch.tsx +16 -13
- package/src/components/atoms/Tag/Tag.test.tsx +70 -0
- package/src/components/atoms/TextArea/TextArea.stories.tsx +303 -0
- package/src/components/atoms/TextArea/TextArea.test.tsx +416 -0
- package/src/components/atoms/TextArea/TextArea.tsx +171 -0
- package/src/components/atoms/TextArea/TextAreaField.tsx +304 -0
- package/src/components/atoms/TextArea/TextAreaLabel.tsx +103 -0
- package/src/components/atoms/TextArea/index.ts +6 -0
- package/src/components/atoms/Typography/Typography.test.tsx +94 -0
- package/src/components/atoms/index.ts +3 -0
- package/src/components/molecules/Accordion/Accordion.stories.tsx +177 -0
- package/src/components/molecules/Accordion/Accordion.test.tsx +185 -0
- package/src/components/molecules/Accordion/Accordion.tsx +284 -0
- package/src/components/molecules/Accordion/index.ts +6 -0
- package/src/components/molecules/Animated/Animated.stories.tsx +254 -0
- package/src/components/molecules/Animated/Animated.tsx +283 -0
- package/src/components/molecules/Animated/index.ts +10 -0
- package/src/components/molecules/ButtonDock/ButtonDock.test.tsx +83 -0
- package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +8 -14
- package/src/components/molecules/ButtonGroup/ButtonGroup.test.tsx +73 -0
- package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +25 -3
- package/src/components/molecules/Checkbox/Checkbox.stories.tsx +72 -0
- package/src/components/molecules/Checkbox/Checkbox.test.tsx +117 -0
- package/src/components/molecules/Checkbox/Checkbox.tsx +101 -95
- package/src/components/molecules/CopyField/CopyField.stories.tsx +313 -0
- package/src/components/molecules/CopyField/CopyField.test.tsx +431 -0
- package/src/components/molecules/CopyField/CopyField.tsx +156 -0
- package/src/components/molecules/CopyField/CopyFieldInput.tsx +127 -0
- package/src/components/molecules/CopyField/hooks/index.ts +1 -0
- package/src/components/molecules/CopyField/hooks/useCopyField.ts +25 -0
- package/src/components/molecules/CopyField/index.ts +4 -0
- package/src/components/molecules/DatePicker/DatePicker.stories.tsx +298 -0
- package/src/components/molecules/DatePicker/DatePicker.test.tsx +201 -0
- package/src/components/molecules/DatePicker/DatePicker.tsx +590 -0
- package/src/components/molecules/DatePicker/index.ts +2 -0
- package/src/components/molecules/Drawer/Drawer.stories.tsx +285 -0
- package/src/components/molecules/Drawer/Drawer.test.tsx +180 -0
- package/src/components/molecules/Drawer/Drawer.tsx +187 -0
- package/src/components/molecules/Drawer/DrawerBody.tsx +80 -0
- package/src/components/molecules/Drawer/DrawerClose.tsx +76 -0
- package/src/components/molecules/Drawer/DrawerContent.tsx +339 -0
- package/src/components/molecules/Drawer/DrawerContext.ts +19 -0
- package/src/components/molecules/Drawer/DrawerDescription.tsx +31 -0
- package/src/components/molecules/Drawer/DrawerDragContext.ts +11 -0
- package/src/components/molecules/Drawer/DrawerFooter.tsx +49 -0
- package/src/components/molecules/Drawer/DrawerFooterContext.ts +6 -0
- package/src/components/molecules/Drawer/DrawerGrabber.tsx +62 -0
- package/src/components/molecules/Drawer/DrawerHeader.tsx +244 -0
- package/src/components/molecules/Drawer/DrawerHeaderContext.ts +13 -0
- package/src/components/molecules/Drawer/DrawerOverlay.tsx +53 -0
- package/src/components/molecules/Drawer/DrawerTitle.tsx +32 -0
- package/src/components/molecules/Drawer/index.ts +12 -0
- package/src/components/molecules/FilterTab/FilterTab.stories.tsx +210 -0
- package/src/components/molecules/FilterTab/FilterTab.tsx +310 -0
- package/src/components/molecules/FilterTab/index.ts +2 -0
- package/src/components/molecules/MessageCard/MessageCard.stories.tsx +169 -0
- package/src/components/molecules/MessageCard/MessageCard.tsx +362 -0
- package/src/components/molecules/MessageCard/index.ts +10 -0
- package/src/components/molecules/Notification/Notification.stories.tsx +219 -0
- package/src/components/molecules/Notification/Notification.tsx +426 -0
- package/src/components/molecules/Notification/index.ts +2 -0
- package/src/components/molecules/NumberField/NumberField.stories.tsx +231 -0
- package/src/components/molecules/NumberField/NumberField.tsx +186 -0
- package/src/components/molecules/NumberField/NumberFieldInput.tsx +287 -0
- package/src/components/molecules/NumberField/index.ts +2 -0
- package/src/components/molecules/PasswordField/PasswordField.stories.tsx +362 -0
- package/src/components/molecules/PasswordField/PasswordField.test.tsx +369 -0
- package/src/components/molecules/PasswordField/PasswordField.tsx +194 -0
- package/src/components/molecules/PasswordField/PasswordFieldError.tsx +52 -0
- package/src/components/molecules/PasswordField/PasswordFieldInput.tsx +73 -0
- package/src/components/molecules/PasswordField/PasswordFieldRequirements.tsx +92 -0
- package/src/components/molecules/PasswordField/hooks/index.ts +2 -0
- package/src/components/molecules/PasswordField/hooks/usePasswordField.ts +113 -0
- package/src/components/molecules/PasswordField/index.ts +10 -0
- package/src/components/molecules/PictureSelector/PictureSelector.stories.tsx +243 -0
- package/src/components/molecules/PictureSelector/PictureSelector.tsx +313 -0
- package/src/components/molecules/PictureSelector/index.ts +5 -0
- package/src/components/molecules/Progress/Progress.stories.tsx +145 -0
- package/src/components/molecules/Progress/Progress.tsx +184 -0
- package/src/components/molecules/Progress/index.ts +2 -0
- package/src/components/molecules/Radio/Radio.test.tsx +104 -0
- package/src/components/molecules/Radio/Radio.tsx +1 -2
- package/src/components/molecules/SearchField/SearchField.stories.tsx +242 -0
- package/src/components/molecules/SearchField/SearchField.test.tsx +318 -0
- package/src/components/molecules/SearchField/SearchField.tsx +143 -0
- package/src/components/molecules/SearchField/SearchFieldInput.tsx +63 -0
- package/src/components/molecules/SearchField/hooks/index.ts +1 -0
- package/src/components/molecules/SearchField/hooks/useSearchField.ts +56 -0
- package/src/components/molecules/SearchField/index.ts +4 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +31 -8
- package/src/components/molecules/SegmentedControl/SegmentedControl.test.tsx +141 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +237 -23
- package/src/components/molecules/SelectField/SelectField.stories.tsx +320 -0
- package/src/components/molecules/SelectField/SelectField.test.tsx +254 -0
- package/src/components/molecules/SelectField/SelectField.tsx +236 -0
- package/src/components/molecules/SelectField/SelectFieldContent.tsx +85 -0
- package/src/components/molecules/SelectField/SelectFieldItem.tsx +133 -0
- package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +170 -0
- package/src/components/molecules/SelectField/SelectFieldValue.tsx +31 -0
- package/src/components/molecules/SelectField/hooks/index.ts +2 -0
- package/src/components/molecules/SelectField/hooks/useSelectField.ts +84 -0
- package/src/components/molecules/SelectField/index.ts +10 -0
- package/src/components/molecules/Slider/Slider.test.tsx +102 -0
- package/src/components/molecules/Slider/Slider.tsx +293 -180
- package/src/components/molecules/Tooltip/Tooltip.stories.tsx +168 -0
- package/src/components/molecules/Tooltip/Tooltip.tsx +326 -0
- package/src/components/molecules/Tooltip/index.ts +2 -0
- package/src/components/molecules/index.ts +15 -0
- package/src/test-utils.tsx +20 -0
- package/tsconfig.json +1 -1
- package/tsup.config.ts +16 -2
- package/vitest.config.ts +114 -0
- package/vitest.setup.ts +16 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { screen, fireEvent } from "@testing-library/react"
|
|
3
|
+
import { describe, it, expect, vi } from "vitest"
|
|
4
|
+
import { renderWithTheme } from "../../../test-utils"
|
|
5
|
+
import { Accordion } from "./Accordion"
|
|
6
|
+
|
|
7
|
+
describe("Accordion", () => {
|
|
8
|
+
describe("rendering", () => {
|
|
9
|
+
it("renders item titles", () => {
|
|
10
|
+
renderWithTheme(
|
|
11
|
+
<Accordion>
|
|
12
|
+
<Accordion.Item title="Question one">Answer one</Accordion.Item>
|
|
13
|
+
<Accordion.Item title="Question two">Answer two</Accordion.Item>
|
|
14
|
+
</Accordion>
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
expect(screen.getByText("Question one")).toBeInTheDocument()
|
|
18
|
+
expect(screen.getByText("Question two")).toBeInTheDocument()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("hides panel content when closed", () => {
|
|
22
|
+
renderWithTheme(
|
|
23
|
+
<Accordion>
|
|
24
|
+
<Accordion.Item title="Question">Hidden answer</Accordion.Item>
|
|
25
|
+
</Accordion>
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
expect(screen.queryByText("Hidden answer")).not.toBeInTheDocument()
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe("interaction", () => {
|
|
33
|
+
it("opens panel when trigger is pressed", () => {
|
|
34
|
+
renderWithTheme(
|
|
35
|
+
<Accordion>
|
|
36
|
+
<Accordion.Item title="Question">Revealed answer</Accordion.Item>
|
|
37
|
+
</Accordion>
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
fireEvent.click(screen.getByRole("button", { name: /Question/ }))
|
|
41
|
+
expect(screen.getByText("Revealed answer")).toBeInTheDocument()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("closes panel when trigger is pressed again", () => {
|
|
45
|
+
renderWithTheme(
|
|
46
|
+
<Accordion>
|
|
47
|
+
<Accordion.Item title="Question">Answer</Accordion.Item>
|
|
48
|
+
</Accordion>
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const trigger = screen.getByRole("button", { name: /Question/ })
|
|
52
|
+
fireEvent.click(trigger)
|
|
53
|
+
fireEvent.click(trigger)
|
|
54
|
+
|
|
55
|
+
expect(screen.queryByText("Answer")).not.toBeInTheDocument()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("closes sibling when a new item opens (single mode)", () => {
|
|
59
|
+
renderWithTheme(
|
|
60
|
+
<Accordion>
|
|
61
|
+
<Accordion.Item title="First">First answer</Accordion.Item>
|
|
62
|
+
<Accordion.Item title="Second">Second answer</Accordion.Item>
|
|
63
|
+
</Accordion>
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
fireEvent.click(screen.getByRole("button", { name: /First/ }))
|
|
67
|
+
expect(screen.getByText("First answer")).toBeInTheDocument()
|
|
68
|
+
|
|
69
|
+
fireEvent.click(screen.getByRole("button", { name: /Second/ }))
|
|
70
|
+
expect(screen.getByText("Second answer")).toBeInTheDocument()
|
|
71
|
+
expect(screen.queryByText("First answer")).not.toBeInTheDocument()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("allows multiple open items when multiple is true", () => {
|
|
75
|
+
renderWithTheme(
|
|
76
|
+
<Accordion multiple>
|
|
77
|
+
<Accordion.Item title="First">First answer</Accordion.Item>
|
|
78
|
+
<Accordion.Item title="Second">Second answer</Accordion.Item>
|
|
79
|
+
</Accordion>
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
fireEvent.click(screen.getByRole("button", { name: /First/ }))
|
|
83
|
+
fireEvent.click(screen.getByRole("button", { name: /Second/ }))
|
|
84
|
+
|
|
85
|
+
expect(screen.getByText("First answer")).toBeInTheDocument()
|
|
86
|
+
expect(screen.getByText("Second answer")).toBeInTheDocument()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("calls onValueChange when an item opens", () => {
|
|
90
|
+
const onValueChange = vi.fn()
|
|
91
|
+
|
|
92
|
+
renderWithTheme(
|
|
93
|
+
<Accordion onValueChange={onValueChange}>
|
|
94
|
+
<Accordion.Item value="item-1" title="Question">
|
|
95
|
+
Answer
|
|
96
|
+
</Accordion.Item>
|
|
97
|
+
</Accordion>
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
fireEvent.click(screen.getByRole("button", { name: /Question/ }))
|
|
101
|
+
expect(onValueChange).toHaveBeenCalledWith(["item-1"])
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe("controlled state", () => {
|
|
106
|
+
it("opens items specified in value prop", () => {
|
|
107
|
+
renderWithTheme(
|
|
108
|
+
<Accordion value={["item-1"]}>
|
|
109
|
+
<Accordion.Item value="item-1" title="Question">
|
|
110
|
+
Controlled answer
|
|
111
|
+
</Accordion.Item>
|
|
112
|
+
</Accordion>
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
expect(screen.getByText("Controlled answer")).toBeInTheDocument()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("opens items specified in defaultValue", () => {
|
|
119
|
+
renderWithTheme(
|
|
120
|
+
<Accordion defaultValue={["item-1"]}>
|
|
121
|
+
<Accordion.Item value="item-1" title="Question">
|
|
122
|
+
Default open answer
|
|
123
|
+
</Accordion.Item>
|
|
124
|
+
</Accordion>
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
expect(screen.getByText("Default open answer")).toBeInTheDocument()
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe("disabled state", () => {
|
|
132
|
+
it("prevents opening when item is disabled", () => {
|
|
133
|
+
renderWithTheme(
|
|
134
|
+
<Accordion>
|
|
135
|
+
<Accordion.Item title="Question" disabled>
|
|
136
|
+
Hidden answer
|
|
137
|
+
</Accordion.Item>
|
|
138
|
+
</Accordion>
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
fireEvent.click(screen.getByRole("button", { name: /Question/ }))
|
|
142
|
+
expect(screen.queryByText("Hidden answer")).not.toBeInTheDocument()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it("disables all items when root is disabled", () => {
|
|
146
|
+
renderWithTheme(
|
|
147
|
+
<Accordion disabled>
|
|
148
|
+
<Accordion.Item title="Question">Answer</Accordion.Item>
|
|
149
|
+
</Accordion>
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
fireEvent.click(screen.getByRole("button", { name: /Question/ }))
|
|
153
|
+
expect(screen.queryByText("Answer")).not.toBeInTheDocument()
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
describe("accessibility", () => {
|
|
158
|
+
it("renders trigger as a button", () => {
|
|
159
|
+
renderWithTheme(
|
|
160
|
+
<Accordion>
|
|
161
|
+
<Accordion.Item title="Question">Answer</Accordion.Item>
|
|
162
|
+
</Accordion>
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
expect(
|
|
166
|
+
screen.getByRole("button", { name: /Question/ })
|
|
167
|
+
).toBeInTheDocument()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it("sets expanded state on trigger", () => {
|
|
171
|
+
renderWithTheme(
|
|
172
|
+
<Accordion defaultValue={["q"]}>
|
|
173
|
+
<Accordion.Item value="q" title="Question">
|
|
174
|
+
Answer
|
|
175
|
+
</Accordion.Item>
|
|
176
|
+
</Accordion>
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
expect(screen.getByRole("button", { name: /Question/ })).toHaveAttribute(
|
|
180
|
+
"aria-expanded",
|
|
181
|
+
"true"
|
|
182
|
+
)
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
})
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, Pressable, ViewProps } from "react-native"
|
|
3
|
+
import styled from "@emotion/native"
|
|
4
|
+
import { useTheme } from "@emotion/react"
|
|
5
|
+
import {
|
|
6
|
+
AddCircle,
|
|
7
|
+
AddCircleOutline,
|
|
8
|
+
RemoveCircle,
|
|
9
|
+
RemoveCircleOutline
|
|
10
|
+
} from "@butternutbox/pawprint-icons/core"
|
|
11
|
+
import { Icon } from "../../atoms/Icon"
|
|
12
|
+
import { Typography } from "../../atoms/Typography"
|
|
13
|
+
|
|
14
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export type AccordionSize = "small" | "large"
|
|
17
|
+
|
|
18
|
+
type AccordionRootOwnProps = {
|
|
19
|
+
size?: AccordionSize
|
|
20
|
+
multiple?: boolean
|
|
21
|
+
defaultValue?: string[]
|
|
22
|
+
value?: string[]
|
|
23
|
+
onValueChange?: (value: string[]) => void
|
|
24
|
+
disabled?: boolean
|
|
25
|
+
children: React.ReactNode
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type AccordionProps = AccordionRootOwnProps &
|
|
29
|
+
Omit<ViewProps, keyof AccordionRootOwnProps>
|
|
30
|
+
|
|
31
|
+
type AccordionItemOwnProps = {
|
|
32
|
+
title: string
|
|
33
|
+
children: React.ReactNode
|
|
34
|
+
value?: string
|
|
35
|
+
disabled?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type AccordionItemProps = AccordionItemOwnProps &
|
|
39
|
+
Omit<ViewProps, keyof AccordionItemOwnProps>
|
|
40
|
+
|
|
41
|
+
// ─── Context ──────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
type AccordionContextValue = {
|
|
44
|
+
openValues: string[]
|
|
45
|
+
toggle: (value: string) => void
|
|
46
|
+
disabled: boolean
|
|
47
|
+
size: AccordionSize
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const AccordionContext = React.createContext<AccordionContextValue>({
|
|
51
|
+
openValues: [],
|
|
52
|
+
toggle: () => {},
|
|
53
|
+
disabled: false,
|
|
54
|
+
size: "small"
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
60
|
+
|
|
61
|
+
// ─── Styled Components ────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
const StyledRoot = styled(View)<{ rootGap: number }>(({ rootGap }) => ({
|
|
64
|
+
width: "100%",
|
|
65
|
+
flexDirection: "column",
|
|
66
|
+
gap: rootGap
|
|
67
|
+
}))
|
|
68
|
+
|
|
69
|
+
const StyledItem = styled(View)({
|
|
70
|
+
flexDirection: "column"
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const StyledTrigger = styled(Pressable)<{
|
|
74
|
+
triggerOpacity: number
|
|
75
|
+
}>(({ triggerOpacity }) => ({
|
|
76
|
+
flexDirection: "row",
|
|
77
|
+
alignItems: "center",
|
|
78
|
+
justifyContent: "space-between",
|
|
79
|
+
width: "100%",
|
|
80
|
+
opacity: triggerOpacity
|
|
81
|
+
}))
|
|
82
|
+
|
|
83
|
+
const StyledPanel = styled(View)<{ panelGap: number }>(({ panelGap }) => ({
|
|
84
|
+
marginTop: panelGap
|
|
85
|
+
}))
|
|
86
|
+
|
|
87
|
+
const StyledDivider = styled(View)<{
|
|
88
|
+
dividerColor: string
|
|
89
|
+
dividerMarginTop: number
|
|
90
|
+
}>(({ dividerColor, dividerMarginTop }) => ({
|
|
91
|
+
height: 1,
|
|
92
|
+
backgroundColor: dividerColor,
|
|
93
|
+
marginTop: dividerMarginTop
|
|
94
|
+
}))
|
|
95
|
+
|
|
96
|
+
// ─── AccordionItem ────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A single item within an `Accordion`. Renders a pressable header and
|
|
100
|
+
* collapsible panel content.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} title - The header text shown at all times.
|
|
103
|
+
* @param {React.ReactNode} children - Content revealed when open.
|
|
104
|
+
* @param {string} [value] - Unique identifier for controlled state.
|
|
105
|
+
* @param {boolean} [disabled=false] - Prevents the item from being opened.
|
|
106
|
+
*/
|
|
107
|
+
const AccordionItem = React.forwardRef<View, AccordionItemProps>(
|
|
108
|
+
({ title, children, value, disabled: itemDisabled, ...rest }, ref) => {
|
|
109
|
+
const theme = useTheme()
|
|
110
|
+
const { accordion } = theme.tokens.components
|
|
111
|
+
const {
|
|
112
|
+
openValues,
|
|
113
|
+
toggle,
|
|
114
|
+
disabled: rootDisabled,
|
|
115
|
+
size
|
|
116
|
+
} = React.useContext(AccordionContext)
|
|
117
|
+
|
|
118
|
+
const itemValue = value ?? title
|
|
119
|
+
const isOpen = openValues.includes(itemValue)
|
|
120
|
+
const isDisabled = itemDisabled || rootDisabled
|
|
121
|
+
const titleToken =
|
|
122
|
+
size === "large"
|
|
123
|
+
? accordion.typography.desktop.title
|
|
124
|
+
: accordion.typography.mobile.title
|
|
125
|
+
|
|
126
|
+
const handlePress = () => {
|
|
127
|
+
if (!isDisabled) {
|
|
128
|
+
toggle(itemValue)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<StyledItem ref={ref} {...rest}>
|
|
134
|
+
<StyledTrigger
|
|
135
|
+
onPress={handlePress}
|
|
136
|
+
disabled={isDisabled}
|
|
137
|
+
accessibilityRole="button"
|
|
138
|
+
accessibilityState={{ expanded: isOpen, disabled: isDisabled }}
|
|
139
|
+
triggerOpacity={isDisabled ? 0.4 : 1}
|
|
140
|
+
>
|
|
141
|
+
{({ pressed, hovered }: { pressed: boolean; hovered?: boolean }) => {
|
|
142
|
+
const isFilled = pressed || hovered
|
|
143
|
+
const icon = isOpen
|
|
144
|
+
? isFilled
|
|
145
|
+
? RemoveCircle
|
|
146
|
+
: RemoveCircleOutline
|
|
147
|
+
: isFilled
|
|
148
|
+
? AddCircle
|
|
149
|
+
: AddCircleOutline
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<>
|
|
153
|
+
<View style={{ flex: 1 }}>
|
|
154
|
+
<Typography
|
|
155
|
+
token={titleToken}
|
|
156
|
+
color={accordion.colour.text.title}
|
|
157
|
+
>
|
|
158
|
+
{title}
|
|
159
|
+
</Typography>
|
|
160
|
+
</View>
|
|
161
|
+
<Icon icon={icon} size="md" colour="primary" />
|
|
162
|
+
</>
|
|
163
|
+
)
|
|
164
|
+
}}
|
|
165
|
+
</StyledTrigger>
|
|
166
|
+
|
|
167
|
+
{isOpen && (
|
|
168
|
+
<StyledPanel
|
|
169
|
+
panelGap={parseTokenValue(accordion.spacing.content.gap)}
|
|
170
|
+
>
|
|
171
|
+
<Typography
|
|
172
|
+
token={accordion.typography.subText}
|
|
173
|
+
color={accordion.colour.text.subText}
|
|
174
|
+
>
|
|
175
|
+
{children}
|
|
176
|
+
</Typography>
|
|
177
|
+
</StyledPanel>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
<StyledDivider
|
|
181
|
+
dividerColor={theme.tokens.semantics.colour.border.default}
|
|
182
|
+
dividerMarginTop={parseTokenValue(accordion.spacing.gap)}
|
|
183
|
+
/>
|
|
184
|
+
</StyledItem>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
AccordionItem.displayName = "Accordion.Item"
|
|
190
|
+
|
|
191
|
+
// ─── Accordion (Root) ─────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* A vertically stacked list of items that expand or collapse to reveal
|
|
195
|
+
* associated content with full accessibility support.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```tsx
|
|
199
|
+
* <Accordion>
|
|
200
|
+
* <Accordion.Item title="What ingredients do you use?">
|
|
201
|
+
* We use 60% human-quality protein with fresh vegetables.
|
|
202
|
+
* </Accordion.Item>
|
|
203
|
+
* <Accordion.Item title="How is the food delivered?">
|
|
204
|
+
* Fresh-frozen and delivered straight to your door.
|
|
205
|
+
* </Accordion.Item>
|
|
206
|
+
* </Accordion>
|
|
207
|
+
* ```
|
|
208
|
+
*
|
|
209
|
+
* @param {"small" | "large"} [size="small"] - Title size variant.
|
|
210
|
+
* @param {boolean} [multiple=false] - Allow multiple items open at once.
|
|
211
|
+
* @param {string[]} [defaultValue] - Uncontrolled: items open by default.
|
|
212
|
+
* @param {string[]} [value] - Controlled open items (requires `onValueChange`).
|
|
213
|
+
* @param {(value: string[]) => void} [onValueChange] - Fires when open items change.
|
|
214
|
+
* @param {boolean} [disabled=false] - Disables all items.
|
|
215
|
+
* @param {React.ReactNode} children - One or more `Accordion.Item` elements.
|
|
216
|
+
*/
|
|
217
|
+
const AccordionRoot = React.forwardRef<View, AccordionProps>(
|
|
218
|
+
(
|
|
219
|
+
{
|
|
220
|
+
children,
|
|
221
|
+
size = "small",
|
|
222
|
+
multiple = false,
|
|
223
|
+
defaultValue,
|
|
224
|
+
value: controlledValue,
|
|
225
|
+
onValueChange,
|
|
226
|
+
disabled = false,
|
|
227
|
+
...rest
|
|
228
|
+
},
|
|
229
|
+
ref
|
|
230
|
+
) => {
|
|
231
|
+
const theme = useTheme()
|
|
232
|
+
const { accordion } = theme.tokens.components
|
|
233
|
+
|
|
234
|
+
const isControlled = controlledValue !== undefined
|
|
235
|
+
const [internalValue, setInternalValue] = React.useState<string[]>(
|
|
236
|
+
defaultValue ?? []
|
|
237
|
+
)
|
|
238
|
+
const openValues = isControlled ? controlledValue : internalValue
|
|
239
|
+
|
|
240
|
+
const toggle = React.useCallback(
|
|
241
|
+
(itemValue: string) => {
|
|
242
|
+
const isOpen = openValues.includes(itemValue)
|
|
243
|
+
let next: string[]
|
|
244
|
+
|
|
245
|
+
if (isOpen) {
|
|
246
|
+
next = openValues.filter((v) => v !== itemValue)
|
|
247
|
+
} else if (multiple) {
|
|
248
|
+
next = [...openValues, itemValue]
|
|
249
|
+
} else {
|
|
250
|
+
next = [itemValue]
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!isControlled) {
|
|
254
|
+
setInternalValue(next)
|
|
255
|
+
}
|
|
256
|
+
onValueChange?.(next)
|
|
257
|
+
},
|
|
258
|
+
[openValues, multiple, isControlled, onValueChange]
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
const ctx = React.useMemo(
|
|
262
|
+
() => ({ openValues, toggle, disabled, size }),
|
|
263
|
+
[openValues, toggle, disabled, size]
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<AccordionContext.Provider value={ctx}>
|
|
268
|
+
<StyledRoot
|
|
269
|
+
ref={ref}
|
|
270
|
+
rootGap={parseTokenValue(accordion.spacing.gap)}
|
|
271
|
+
{...rest}
|
|
272
|
+
>
|
|
273
|
+
{children}
|
|
274
|
+
</StyledRoot>
|
|
275
|
+
</AccordionContext.Provider>
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
AccordionRoot.displayName = "Accordion"
|
|
281
|
+
|
|
282
|
+
export const Accordion = Object.assign(AccordionRoot, {
|
|
283
|
+
Item: AccordionItem
|
|
284
|
+
})
|