@sproutsocial/seeds-react-accordion 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/.eslintignore +6 -0
- package/.eslintrc.js +4 -0
- package/.turbo/turbo-build.log +21 -0
- package/CHANGELOG.md +7 -0
- package/dist/esm/index.js +267 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/index.d.mts +48 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +300 -0
- package/dist/index.js.map +1 -0
- package/jest.config.js +9 -0
- package/package.json +47 -0
- package/src/Accordion.stories.tsx +563 -0
- package/src/Accordion.tsx +63 -0
- package/src/AccordionContent.tsx +23 -0
- package/src/AccordionItem.tsx +11 -0
- package/src/AccordionTrigger.tsx +188 -0
- package/src/AccordionTypes.ts +54 -0
- package/src/__tests__/accordion.test.tsx +419 -0
- package/src/index.ts +6 -0
- package/src/styled.d.ts +7 -0
- package/src/styles.ts +194 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +12 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { StyledRadixAccordionTrigger, TitleStyles } from "./styles";
|
|
2
|
+
import {
|
|
3
|
+
type TypeAccordionSystemProps,
|
|
4
|
+
type TypeRelatedAction,
|
|
5
|
+
type TypeOverflowMenuConfig,
|
|
6
|
+
} from "./AccordionTypes";
|
|
7
|
+
import { FlexCenter, StyledAccordionArea, TriggerContainer } from "./styles";
|
|
8
|
+
import { Box } from "@sproutsocial/seeds-react-box";
|
|
9
|
+
import { Button } from "@sproutsocial/seeds-react-button";
|
|
10
|
+
import { Icon } from "@sproutsocial/seeds-react-icon";
|
|
11
|
+
import {
|
|
12
|
+
ActionMenu,
|
|
13
|
+
MenuContent,
|
|
14
|
+
MenuItem,
|
|
15
|
+
MenuGroup,
|
|
16
|
+
MenuToggleButton,
|
|
17
|
+
} from "@sproutsocial/seeds-react-menu";
|
|
18
|
+
import { useContext } from "react";
|
|
19
|
+
import { AccordionContext } from "./Accordion";
|
|
20
|
+
|
|
21
|
+
const MAX_RELATED_ACTIONS = 2;
|
|
22
|
+
|
|
23
|
+
interface TypeAccordionTriggerProps extends TypeAccordionSystemProps {
|
|
24
|
+
title: string;
|
|
25
|
+
leftSlot?: React.ReactNode;
|
|
26
|
+
relatedActions?: TypeRelatedAction[];
|
|
27
|
+
overflowMenu?: TypeOverflowMenuConfig;
|
|
28
|
+
rightSlot?: React.ReactNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const AccordionTrigger = ({
|
|
32
|
+
children,
|
|
33
|
+
leftSlot,
|
|
34
|
+
relatedActions,
|
|
35
|
+
overflowMenu,
|
|
36
|
+
rightSlot,
|
|
37
|
+
title,
|
|
38
|
+
...rest
|
|
39
|
+
}: TypeAccordionTriggerProps) => {
|
|
40
|
+
const { triggerIcon, triggerPosition, styled } = useContext(AccordionContext);
|
|
41
|
+
|
|
42
|
+
// Validate and limit related actions
|
|
43
|
+
const validatedActions = relatedActions?.slice(0, MAX_RELATED_ACTIONS);
|
|
44
|
+
|
|
45
|
+
// Extract system props to distribute to appropriate container
|
|
46
|
+
const {
|
|
47
|
+
color,
|
|
48
|
+
padding,
|
|
49
|
+
paddingBottom,
|
|
50
|
+
paddingTop,
|
|
51
|
+
paddingX,
|
|
52
|
+
paddingY,
|
|
53
|
+
paddingLeft,
|
|
54
|
+
paddingRight,
|
|
55
|
+
p,
|
|
56
|
+
pb,
|
|
57
|
+
pt,
|
|
58
|
+
pr,
|
|
59
|
+
pl,
|
|
60
|
+
px,
|
|
61
|
+
py,
|
|
62
|
+
fontFamily,
|
|
63
|
+
fontStyle,
|
|
64
|
+
fontWeight,
|
|
65
|
+
lineHeight,
|
|
66
|
+
textAlign,
|
|
67
|
+
...triggerProps
|
|
68
|
+
} = rest;
|
|
69
|
+
|
|
70
|
+
const spacingProps = {
|
|
71
|
+
padding,
|
|
72
|
+
paddingBottom,
|
|
73
|
+
paddingTop,
|
|
74
|
+
paddingX,
|
|
75
|
+
paddingY,
|
|
76
|
+
paddingLeft,
|
|
77
|
+
paddingRight,
|
|
78
|
+
p,
|
|
79
|
+
pb,
|
|
80
|
+
pt,
|
|
81
|
+
pr,
|
|
82
|
+
pl,
|
|
83
|
+
px,
|
|
84
|
+
py,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// When you destructure color from rest, it might be null, which is incompatible with what the styled component expects. We need to filter out null or undefined values from typographyProps before spreading them.
|
|
88
|
+
|
|
89
|
+
const typographyProps = Object.fromEntries(
|
|
90
|
+
Object.entries({
|
|
91
|
+
color,
|
|
92
|
+
fontFamily,
|
|
93
|
+
fontStyle,
|
|
94
|
+
fontWeight,
|
|
95
|
+
lineHeight,
|
|
96
|
+
textAlign,
|
|
97
|
+
}).filter(([_, value]) => value != null)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Render overflow menu from config
|
|
101
|
+
const renderedOverflowMenu = overflowMenu && (
|
|
102
|
+
<ActionMenu
|
|
103
|
+
menuToggleElement={
|
|
104
|
+
<MenuToggleButton
|
|
105
|
+
aria-label={overflowMenu["aria-label"]}
|
|
106
|
+
appearance="unstyled"
|
|
107
|
+
>
|
|
108
|
+
<Icon name="ellipsis-horizontal-outline" aria-hidden="true" />
|
|
109
|
+
</MenuToggleButton>
|
|
110
|
+
}
|
|
111
|
+
>
|
|
112
|
+
<MenuContent>
|
|
113
|
+
<MenuGroup id="overflow-actions">
|
|
114
|
+
{overflowMenu.items.map((item, index) => {
|
|
115
|
+
const { iconName, id, onClick, children, ...menuItemProps } = item;
|
|
116
|
+
return (
|
|
117
|
+
<MenuItem
|
|
118
|
+
key={id || `overflow-item-${index}`}
|
|
119
|
+
id={id || `overflow-item-${index}`}
|
|
120
|
+
onClick={onClick}
|
|
121
|
+
{...menuItemProps}
|
|
122
|
+
>
|
|
123
|
+
{iconName ? (
|
|
124
|
+
<Box display="flex" alignItems="center" gap="300">
|
|
125
|
+
<Icon name={iconName} />
|
|
126
|
+
{children}
|
|
127
|
+
</Box>
|
|
128
|
+
) : (
|
|
129
|
+
children
|
|
130
|
+
)}
|
|
131
|
+
</MenuItem>
|
|
132
|
+
);
|
|
133
|
+
})}
|
|
134
|
+
</MenuGroup>
|
|
135
|
+
</MenuContent>
|
|
136
|
+
</ActionMenu>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Render related actions from config
|
|
140
|
+
const renderedRelatedActions = validatedActions &&
|
|
141
|
+
validatedActions.length > 0 && (
|
|
142
|
+
<Box display="flex">
|
|
143
|
+
{validatedActions.map((action, index) => (
|
|
144
|
+
<Button
|
|
145
|
+
key={`${action.iconName}-${index}`}
|
|
146
|
+
onClick={action.onClick}
|
|
147
|
+
aria-label={action["aria-label"]}
|
|
148
|
+
>
|
|
149
|
+
<Icon name={action.iconName} aria-hidden="true" />
|
|
150
|
+
</Button>
|
|
151
|
+
))}
|
|
152
|
+
</Box>
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<TriggerContainer data-styled={styled} {...triggerProps}>
|
|
157
|
+
<StyledRadixAccordionTrigger data-styled={styled} {...spacingProps}>
|
|
158
|
+
{triggerPosition === "right" ? (
|
|
159
|
+
<StyledAccordionArea>
|
|
160
|
+
<FlexCenter>
|
|
161
|
+
{leftSlot}
|
|
162
|
+
<TitleStyles data-styled={styled} {...typographyProps}>
|
|
163
|
+
{title}
|
|
164
|
+
</TitleStyles>
|
|
165
|
+
{rightSlot}
|
|
166
|
+
</FlexCenter>
|
|
167
|
+
{triggerIcon}
|
|
168
|
+
</StyledAccordionArea>
|
|
169
|
+
) : (
|
|
170
|
+
<StyledAccordionArea>
|
|
171
|
+
<FlexCenter>
|
|
172
|
+
<Box mr={300}>{triggerIcon}</Box>
|
|
173
|
+
{leftSlot}
|
|
174
|
+
<TitleStyles data-styled={styled} {...typographyProps}>
|
|
175
|
+
{title}
|
|
176
|
+
</TitleStyles>
|
|
177
|
+
</FlexCenter>
|
|
178
|
+
{rightSlot}
|
|
179
|
+
</StyledAccordionArea>
|
|
180
|
+
)}
|
|
181
|
+
</StyledRadixAccordionTrigger>
|
|
182
|
+
<Box mr={300} display="flex">
|
|
183
|
+
{renderedOverflowMenu}
|
|
184
|
+
{renderedRelatedActions}
|
|
185
|
+
</Box>
|
|
186
|
+
</TriggerContainer>
|
|
187
|
+
);
|
|
188
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
type TypeSystemCommonProps,
|
|
4
|
+
type TypeBorderSystemProps,
|
|
5
|
+
type TypeFlexboxSystemProps,
|
|
6
|
+
type TypeLayoutSystemProps,
|
|
7
|
+
type TypeStyledComponentsCommonProps,
|
|
8
|
+
type TypeTypographySystemProps,
|
|
9
|
+
} from "@sproutsocial/seeds-react-system-props";
|
|
10
|
+
import { type TypeIconName } from "@sproutsocial/seeds-react-icon";
|
|
11
|
+
import { type TypeMenuItemProps } from "@sproutsocial/seeds-react-menu";
|
|
12
|
+
|
|
13
|
+
export interface TypeAccordionSystemProps
|
|
14
|
+
extends Omit<React.ComponentPropsWithoutRef<"div">, "color">,
|
|
15
|
+
TypeStyledComponentsCommonProps,
|
|
16
|
+
TypeSystemCommonProps,
|
|
17
|
+
TypeBorderSystemProps,
|
|
18
|
+
TypeFlexboxSystemProps,
|
|
19
|
+
TypeLayoutSystemProps,
|
|
20
|
+
TypeTypographySystemProps {}
|
|
21
|
+
|
|
22
|
+
export interface TypeAccordionProps {
|
|
23
|
+
children?: React.ReactNode;
|
|
24
|
+
collapsible?: boolean;
|
|
25
|
+
defaultValue: string | [string];
|
|
26
|
+
triggerIcon?: React.ReactNode;
|
|
27
|
+
triggerPosition?: "left" | "right";
|
|
28
|
+
type?: "single" | "multiple";
|
|
29
|
+
styled?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TypeRelatedAction {
|
|
33
|
+
iconName: TypeIconName;
|
|
34
|
+
onClick: () => void;
|
|
35
|
+
"aria-label": string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TypeOverflowMenuItem extends TypeMenuItemProps {
|
|
39
|
+
iconName?: TypeIconName;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TypeOverflowMenuConfig {
|
|
43
|
+
/** Menu items to be rendered in the overflow menu */
|
|
44
|
+
items: TypeOverflowMenuItem[];
|
|
45
|
+
/** Aria label for the overflow menu trigger button. Defaults to "More actions" */
|
|
46
|
+
"aria-label"?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface TypeAccordionItemProps {
|
|
50
|
+
children: React.ReactNode;
|
|
51
|
+
relatedActions?: TypeRelatedAction[];
|
|
52
|
+
overflowMenu?: TypeOverflowMenuConfig;
|
|
53
|
+
value: string;
|
|
54
|
+
}
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
render,
|
|
4
|
+
screen,
|
|
5
|
+
fireEvent,
|
|
6
|
+
} from "@sproutsocial/seeds-react-testing-library";
|
|
7
|
+
import { Accordion } from "../Accordion";
|
|
8
|
+
import { AccordionItem } from "../AccordionItem";
|
|
9
|
+
import { AccordionTrigger } from "../AccordionTrigger";
|
|
10
|
+
import { AccordionContent } from "../AccordionContent";
|
|
11
|
+
|
|
12
|
+
describe("Accordion", () => {
|
|
13
|
+
it("renders accordion with title", () => {
|
|
14
|
+
render(
|
|
15
|
+
<Accordion defaultValue={["item-1"]}>
|
|
16
|
+
<AccordionItem value="item-1">
|
|
17
|
+
<AccordionTrigger title="Test Title" />
|
|
18
|
+
<AccordionContent>Content</AccordionContent>
|
|
19
|
+
</AccordionItem>
|
|
20
|
+
</Accordion>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
expect(screen.getByText("Test Title")).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("relatedActions", () => {
|
|
27
|
+
it("renders action buttons with proper aria-labels", () => {
|
|
28
|
+
render(
|
|
29
|
+
<Accordion defaultValue={["item-1"]}>
|
|
30
|
+
<AccordionItem value="item-1">
|
|
31
|
+
<AccordionTrigger
|
|
32
|
+
title="Test"
|
|
33
|
+
relatedActions={[
|
|
34
|
+
{
|
|
35
|
+
iconName: "alarm-clock",
|
|
36
|
+
onClick: jest.fn(),
|
|
37
|
+
"aria-label": "Set alarm",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
iconName: "ellipsis-horizontal-outline",
|
|
41
|
+
onClick: jest.fn(),
|
|
42
|
+
"aria-label": "More options",
|
|
43
|
+
},
|
|
44
|
+
]}
|
|
45
|
+
/>
|
|
46
|
+
<AccordionContent>Content</AccordionContent>
|
|
47
|
+
</AccordionItem>
|
|
48
|
+
</Accordion>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(screen.getByLabelText("Set alarm")).toBeInTheDocument();
|
|
52
|
+
expect(screen.getByLabelText("More options")).toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("limits related actions to maximum of 3", () => {
|
|
56
|
+
render(
|
|
57
|
+
<Accordion defaultValue={["item-1"]}>
|
|
58
|
+
<AccordionItem value="item-1">
|
|
59
|
+
<AccordionTrigger
|
|
60
|
+
title="Test"
|
|
61
|
+
relatedActions={[
|
|
62
|
+
{
|
|
63
|
+
iconName: "alarm-clock",
|
|
64
|
+
onClick: jest.fn(),
|
|
65
|
+
"aria-label": "Action 1",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
iconName: "ellipsis-horizontal-outline",
|
|
69
|
+
onClick: jest.fn(),
|
|
70
|
+
"aria-label": "Action 2",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
iconName: "alarm-clock",
|
|
74
|
+
onClick: jest.fn(),
|
|
75
|
+
"aria-label": "Action 3",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
iconName: "ellipsis-horizontal-outline",
|
|
79
|
+
onClick: jest.fn(),
|
|
80
|
+
"aria-label": "Action 4",
|
|
81
|
+
},
|
|
82
|
+
]}
|
|
83
|
+
/>
|
|
84
|
+
<AccordionContent>Content</AccordionContent>
|
|
85
|
+
</AccordionItem>
|
|
86
|
+
</Accordion>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(screen.getByLabelText("Action 1")).toBeInTheDocument();
|
|
90
|
+
expect(screen.getByLabelText("Action 2")).toBeInTheDocument();
|
|
91
|
+
expect(screen.getByLabelText("Action 3")).toBeInTheDocument();
|
|
92
|
+
expect(screen.queryByLabelText("Action 4")).not.toBeInTheDocument();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("renders icons with aria-hidden for accessibility", () => {
|
|
96
|
+
render(
|
|
97
|
+
<Accordion defaultValue={["item-1"]}>
|
|
98
|
+
<AccordionItem value="item-1">
|
|
99
|
+
<AccordionTrigger
|
|
100
|
+
title="Test"
|
|
101
|
+
relatedActions={[
|
|
102
|
+
{
|
|
103
|
+
iconName: "alarm-clock",
|
|
104
|
+
onClick: jest.fn(),
|
|
105
|
+
"aria-label": "Set alarm",
|
|
106
|
+
},
|
|
107
|
+
]}
|
|
108
|
+
/>
|
|
109
|
+
<AccordionContent>Content</AccordionContent>
|
|
110
|
+
</AccordionItem>
|
|
111
|
+
</Accordion>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const button = screen.getByLabelText("Set alarm");
|
|
115
|
+
const icon = button.querySelector('[aria-hidden="true"]');
|
|
116
|
+
expect(icon).toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("calls onClick handler when action button is clicked", () => {
|
|
120
|
+
const mockOnClick = jest.fn();
|
|
121
|
+
render(
|
|
122
|
+
<Accordion defaultValue={["item-1"]}>
|
|
123
|
+
<AccordionItem value="item-1">
|
|
124
|
+
<AccordionTrigger
|
|
125
|
+
title="Test"
|
|
126
|
+
relatedActions={[
|
|
127
|
+
{
|
|
128
|
+
iconName: "alarm-clock",
|
|
129
|
+
onClick: mockOnClick,
|
|
130
|
+
"aria-label": "Set alarm",
|
|
131
|
+
},
|
|
132
|
+
]}
|
|
133
|
+
/>
|
|
134
|
+
<AccordionContent>Content</AccordionContent>
|
|
135
|
+
</AccordionItem>
|
|
136
|
+
</Accordion>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const actionButton = screen.getByLabelText("Set alarm");
|
|
140
|
+
fireEvent.click(actionButton);
|
|
141
|
+
|
|
142
|
+
expect(mockOnClick).toHaveBeenCalledTimes(1);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("does not toggle accordion when clicking action button", () => {
|
|
146
|
+
const mockOnClick = jest.fn();
|
|
147
|
+
render(
|
|
148
|
+
<Accordion defaultValue={["item-1"]}>
|
|
149
|
+
<AccordionItem value="item-1">
|
|
150
|
+
<AccordionTrigger
|
|
151
|
+
title="Test"
|
|
152
|
+
relatedActions={[
|
|
153
|
+
{
|
|
154
|
+
iconName: "alarm-clock",
|
|
155
|
+
onClick: mockOnClick,
|
|
156
|
+
"aria-label": "Set alarm",
|
|
157
|
+
},
|
|
158
|
+
]}
|
|
159
|
+
/>
|
|
160
|
+
<AccordionContent>Content</AccordionContent>
|
|
161
|
+
</AccordionItem>
|
|
162
|
+
</Accordion>
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// Verify accordion is open initially
|
|
166
|
+
expect(screen.getByText("Content")).toBeInTheDocument();
|
|
167
|
+
|
|
168
|
+
// Click the action button
|
|
169
|
+
const actionButton = screen.getByLabelText("Set alarm");
|
|
170
|
+
fireEvent.click(actionButton);
|
|
171
|
+
|
|
172
|
+
// Accordion should still be open
|
|
173
|
+
expect(screen.getByText("Content")).toBeInTheDocument();
|
|
174
|
+
expect(mockOnClick).toHaveBeenCalled();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("handles multiple onClick handlers correctly", () => {
|
|
178
|
+
const mockOnClick1 = jest.fn();
|
|
179
|
+
const mockOnClick2 = jest.fn();
|
|
180
|
+
render(
|
|
181
|
+
<Accordion defaultValue={["item-1"]}>
|
|
182
|
+
<AccordionItem value="item-1">
|
|
183
|
+
<AccordionTrigger
|
|
184
|
+
title="Test"
|
|
185
|
+
relatedActions={[
|
|
186
|
+
{
|
|
187
|
+
iconName: "alarm-clock",
|
|
188
|
+
onClick: mockOnClick1,
|
|
189
|
+
"aria-label": "Action 1",
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
iconName: "ellipsis-horizontal-outline",
|
|
193
|
+
onClick: mockOnClick2,
|
|
194
|
+
"aria-label": "Action 2",
|
|
195
|
+
},
|
|
196
|
+
]}
|
|
197
|
+
/>
|
|
198
|
+
<AccordionContent>Content</AccordionContent>
|
|
199
|
+
</AccordionItem>
|
|
200
|
+
</Accordion>
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
fireEvent.click(screen.getByLabelText("Action 1"));
|
|
204
|
+
expect(mockOnClick1).toHaveBeenCalledTimes(1);
|
|
205
|
+
expect(mockOnClick2).not.toHaveBeenCalled();
|
|
206
|
+
|
|
207
|
+
fireEvent.click(screen.getByLabelText("Action 2"));
|
|
208
|
+
expect(mockOnClick1).toHaveBeenCalledTimes(1);
|
|
209
|
+
expect(mockOnClick2).toHaveBeenCalledTimes(1);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("trigger icon", () => {
|
|
214
|
+
it("renders default chevron-down icon", () => {
|
|
215
|
+
const { container } = render(
|
|
216
|
+
<Accordion defaultValue={["item-1"]}>
|
|
217
|
+
<AccordionItem value="item-1">
|
|
218
|
+
<AccordionTrigger title="Test Title" />
|
|
219
|
+
<AccordionContent>Content</AccordionContent>
|
|
220
|
+
</AccordionItem>
|
|
221
|
+
</Accordion>
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const icon = container.querySelector(".triggerIcon");
|
|
225
|
+
expect(icon).toBeInTheDocument();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("renders custom trigger icon", () => {
|
|
229
|
+
const CustomIcon = () => <div data-testid="custom-icon">Custom Icon</div>;
|
|
230
|
+
|
|
231
|
+
render(
|
|
232
|
+
<Accordion defaultValue={["item-1"]} triggerIcon={<CustomIcon />}>
|
|
233
|
+
<AccordionItem value="item-1">
|
|
234
|
+
<AccordionTrigger title="Test Title" />
|
|
235
|
+
<AccordionContent>Content</AccordionContent>
|
|
236
|
+
</AccordionItem>
|
|
237
|
+
</Accordion>
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
expect(screen.getByTestId("custom-icon")).toBeInTheDocument();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("positions trigger icon on the right by default", () => {
|
|
244
|
+
const { container } = render(
|
|
245
|
+
<Accordion defaultValue={["item-1"]}>
|
|
246
|
+
<AccordionItem value="item-1">
|
|
247
|
+
<AccordionTrigger title="Test Title" />
|
|
248
|
+
<AccordionContent>Content</AccordionContent>
|
|
249
|
+
</AccordionItem>
|
|
250
|
+
</Accordion>
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const icon = container.querySelector(".triggerIcon");
|
|
254
|
+
expect(icon).toBeInTheDocument();
|
|
255
|
+
// Icon should be rendered after title (right position)
|
|
256
|
+
const trigger = screen.getByText("Test Title").closest("button");
|
|
257
|
+
const iconParent = icon?.parentElement ?? null;
|
|
258
|
+
expect(trigger).toContainElement(iconParent);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("positions trigger icon on the left when triggerPosition is 'left'", () => {
|
|
262
|
+
const { container } = render(
|
|
263
|
+
<Accordion defaultValue={["item-1"]} triggerPosition="left">
|
|
264
|
+
<AccordionItem value="item-1">
|
|
265
|
+
<AccordionTrigger title="Test Title" />
|
|
266
|
+
<AccordionContent>Content</AccordionContent>
|
|
267
|
+
</AccordionItem>
|
|
268
|
+
</Accordion>
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const icon = container.querySelector(".triggerIcon");
|
|
272
|
+
expect(icon).toBeInTheDocument();
|
|
273
|
+
const trigger = screen.getByText("Test Title").closest("button");
|
|
274
|
+
const iconParent = icon?.parentElement ?? null;
|
|
275
|
+
expect(trigger).toContainElement(iconParent);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("positions trigger icon on the right when triggerPosition is 'right'", () => {
|
|
279
|
+
const { container } = render(
|
|
280
|
+
<Accordion defaultValue={["item-1"]} triggerPosition="right">
|
|
281
|
+
<AccordionItem value="item-1">
|
|
282
|
+
<AccordionTrigger title="Test Title" />
|
|
283
|
+
<AccordionContent>Content</AccordionContent>
|
|
284
|
+
</AccordionItem>
|
|
285
|
+
</Accordion>
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const icon = container.querySelector(".triggerIcon");
|
|
289
|
+
expect(icon).toBeInTheDocument();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe("slots", () => {
|
|
294
|
+
it("renders leftSlot content", () => {
|
|
295
|
+
render(
|
|
296
|
+
<Accordion defaultValue={["item-1"]}>
|
|
297
|
+
<AccordionItem value="item-1">
|
|
298
|
+
<AccordionTrigger
|
|
299
|
+
title="Test Title"
|
|
300
|
+
leftSlot={<div data-testid="left-content">Left Content</div>}
|
|
301
|
+
/>
|
|
302
|
+
<AccordionContent>Content</AccordionContent>
|
|
303
|
+
</AccordionItem>
|
|
304
|
+
</Accordion>
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
expect(screen.getByTestId("left-content")).toBeInTheDocument();
|
|
308
|
+
expect(screen.getByText("Left Content")).toBeInTheDocument();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("renders rightSlot content", () => {
|
|
312
|
+
render(
|
|
313
|
+
<Accordion defaultValue={["item-1"]}>
|
|
314
|
+
<AccordionItem value="item-1">
|
|
315
|
+
<AccordionTrigger
|
|
316
|
+
title="Test Title"
|
|
317
|
+
rightSlot={<div data-testid="right-content">Right Content</div>}
|
|
318
|
+
/>
|
|
319
|
+
<AccordionContent>Content</AccordionContent>
|
|
320
|
+
</AccordionItem>
|
|
321
|
+
</Accordion>
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
expect(screen.getByTestId("right-content")).toBeInTheDocument();
|
|
325
|
+
expect(screen.getByText("Right Content")).toBeInTheDocument();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("renders both leftSlot and rightSlot together", () => {
|
|
329
|
+
render(
|
|
330
|
+
<Accordion defaultValue={["item-1"]}>
|
|
331
|
+
<AccordionItem value="item-1">
|
|
332
|
+
<AccordionTrigger
|
|
333
|
+
title="Test Title"
|
|
334
|
+
leftSlot={<div data-testid="left-content">Left Content</div>}
|
|
335
|
+
rightSlot={<div data-testid="right-content">Right Content</div>}
|
|
336
|
+
/>
|
|
337
|
+
<AccordionContent>Content</AccordionContent>
|
|
338
|
+
</AccordionItem>
|
|
339
|
+
</Accordion>
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
expect(screen.getByTestId("left-content")).toBeInTheDocument();
|
|
343
|
+
expect(screen.getByTestId("right-content")).toBeInTheDocument();
|
|
344
|
+
expect(screen.getByText("Test Title")).toBeInTheDocument();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("renders slots with complex components", () => {
|
|
348
|
+
const LeftComponent = () => (
|
|
349
|
+
<div data-testid="left-complex">
|
|
350
|
+
<span>Icon</span>
|
|
351
|
+
<span>Badge</span>
|
|
352
|
+
</div>
|
|
353
|
+
);
|
|
354
|
+
const RightComponent = () => (
|
|
355
|
+
<div data-testid="right-complex">
|
|
356
|
+
<button>Action</button>
|
|
357
|
+
</div>
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
render(
|
|
361
|
+
<Accordion defaultValue={["item-1"]}>
|
|
362
|
+
<AccordionItem value="item-1">
|
|
363
|
+
<AccordionTrigger
|
|
364
|
+
title="Test Title"
|
|
365
|
+
leftSlot={<LeftComponent />}
|
|
366
|
+
rightSlot={<RightComponent />}
|
|
367
|
+
/>
|
|
368
|
+
<AccordionContent>Content</AccordionContent>
|
|
369
|
+
</AccordionItem>
|
|
370
|
+
</Accordion>
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
expect(screen.getByTestId("left-complex")).toBeInTheDocument();
|
|
374
|
+
expect(screen.getByTestId("right-complex")).toBeInTheDocument();
|
|
375
|
+
expect(screen.getByText("Icon")).toBeInTheDocument();
|
|
376
|
+
expect(screen.getByText("Badge")).toBeInTheDocument();
|
|
377
|
+
expect(screen.getByText("Action")).toBeInTheDocument();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("positions leftSlot before title and rightSlot after title when triggerPosition is right", () => {
|
|
381
|
+
render(
|
|
382
|
+
<Accordion defaultValue={["item-1"]} triggerPosition="right">
|
|
383
|
+
<AccordionItem value="item-1">
|
|
384
|
+
<AccordionTrigger
|
|
385
|
+
title="Test Title"
|
|
386
|
+
leftSlot={<span data-testid="left">Left</span>}
|
|
387
|
+
rightSlot={<span data-testid="right">Right</span>}
|
|
388
|
+
/>
|
|
389
|
+
<AccordionContent>Content</AccordionContent>
|
|
390
|
+
</AccordionItem>
|
|
391
|
+
</Accordion>
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const trigger = screen.getByText("Test Title").closest("button");
|
|
395
|
+
expect(trigger).toBeInTheDocument();
|
|
396
|
+
expect(screen.getByTestId("left")).toBeInTheDocument();
|
|
397
|
+
expect(screen.getByTestId("right")).toBeInTheDocument();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("renders slots correctly when triggerPosition is left", () => {
|
|
401
|
+
render(
|
|
402
|
+
<Accordion defaultValue={["item-1"]} triggerPosition="left">
|
|
403
|
+
<AccordionItem value="item-1">
|
|
404
|
+
<AccordionTrigger
|
|
405
|
+
title="Test Title"
|
|
406
|
+
leftSlot={<span data-testid="left">Left</span>}
|
|
407
|
+
rightSlot={<span data-testid="right">Right</span>}
|
|
408
|
+
/>
|
|
409
|
+
<AccordionContent>Content</AccordionContent>
|
|
410
|
+
</AccordionItem>
|
|
411
|
+
</Accordion>
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
expect(screen.getByTestId("left")).toBeInTheDocument();
|
|
415
|
+
expect(screen.getByTestId("right")).toBeInTheDocument();
|
|
416
|
+
expect(screen.getByText("Test Title")).toBeInTheDocument();
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
});
|
package/src/index.ts
ADDED
package/src/styled.d.ts
ADDED