@khanacademy/wonder-blocks-accordion 1.3.7 → 1.3.9
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/CHANGELOG.md +20 -0
- package/dist/components/accordion-section.d.ts +2 -2
- package/dist/components/accordion.d.ts +2 -2
- package/package.json +6 -6
- package/src/components/__tests__/accordion-section-header.test.tsx +0 -211
- package/src/components/__tests__/accordion-section.test.tsx +0 -361
- package/src/components/__tests__/accordion.test.tsx +0 -956
- package/src/components/accordion-section-header.tsx +0 -271
- package/src/components/accordion-section.tsx +0 -432
- package/src/components/accordion.tsx +0 -295
- package/src/index.ts +0 -4
- package/src/utils.test.ts +0 -59
- package/src/utils.ts +0 -58
- package/tsconfig-build.json +0 -15
- package/tsconfig-build.tsbuildinfo +0 -1
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import {StyleSheet} from "aphrodite";
|
|
3
|
-
import caretDown from "@phosphor-icons/core/bold/caret-down-bold.svg";
|
|
4
|
-
|
|
5
|
-
import Clickable from "@khanacademy/wonder-blocks-clickable";
|
|
6
|
-
import {View} from "@khanacademy/wonder-blocks-core";
|
|
7
|
-
import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon";
|
|
8
|
-
import {HeadingSmall} from "@khanacademy/wonder-blocks-typography";
|
|
9
|
-
import * as tokens from "@khanacademy/wonder-blocks-tokens";
|
|
10
|
-
import type {StyleType} from "@khanacademy/wonder-blocks-core";
|
|
11
|
-
|
|
12
|
-
import type {AccordionCornerKindType} from "./accordion";
|
|
13
|
-
import type {TagType} from "./accordion-section";
|
|
14
|
-
import {getRoundedValuesForHeader} from "../utils";
|
|
15
|
-
|
|
16
|
-
type Props = {
|
|
17
|
-
// Unique ID for this section's button.
|
|
18
|
-
id: string;
|
|
19
|
-
// Header content.
|
|
20
|
-
header: string | React.ReactElement;
|
|
21
|
-
// Whether the caret shows up at the start or end of the header block.
|
|
22
|
-
caretPosition: "start" | "end";
|
|
23
|
-
// Corner roundedness type.
|
|
24
|
-
cornerKind: AccordionCornerKindType;
|
|
25
|
-
// Whether the section is collapsible or not. If false, the header will
|
|
26
|
-
// not be clickable, and the section will stay expanded at all times.
|
|
27
|
-
collapsible?: boolean;
|
|
28
|
-
// Whether the section is expanded or not.
|
|
29
|
-
expanded: boolean;
|
|
30
|
-
// Whether to include animation on the header. This should be false
|
|
31
|
-
// if the user has `prefers-reduced-motion` opted in. Defaults to false.
|
|
32
|
-
animated: boolean;
|
|
33
|
-
// Called on header click.
|
|
34
|
-
onClick?: () => void;
|
|
35
|
-
// The ID for the content that the header's `aria-controls` should
|
|
36
|
-
// point to.
|
|
37
|
-
sectionContentUniqueId: string;
|
|
38
|
-
// Custom styles for the header container.
|
|
39
|
-
headerStyle?: StyleType;
|
|
40
|
-
// The semantic tag for this clickable header (e.g. "h1", "h2", etc.)
|
|
41
|
-
// Please use this to ensure that the header is hierarchically correct.
|
|
42
|
-
tag?: TagType;
|
|
43
|
-
// The test ID used for e2e testing.
|
|
44
|
-
testId?: string;
|
|
45
|
-
// Whether this section is the first section in the accordion.
|
|
46
|
-
// For internal use only.
|
|
47
|
-
isFirstSection: boolean;
|
|
48
|
-
// Whether this section is the last section in the accordion.
|
|
49
|
-
// For internal use only.
|
|
50
|
-
isLastSection: boolean;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const AccordionSectionHeader = React.forwardRef(function AccordionSectionHeader(
|
|
54
|
-
props: Props,
|
|
55
|
-
ref: React.ForwardedRef<HTMLButtonElement>,
|
|
56
|
-
) {
|
|
57
|
-
const {
|
|
58
|
-
id,
|
|
59
|
-
header,
|
|
60
|
-
caretPosition,
|
|
61
|
-
cornerKind,
|
|
62
|
-
collapsible = true,
|
|
63
|
-
expanded,
|
|
64
|
-
animated,
|
|
65
|
-
onClick,
|
|
66
|
-
sectionContentUniqueId,
|
|
67
|
-
headerStyle,
|
|
68
|
-
tag = "h2",
|
|
69
|
-
testId,
|
|
70
|
-
isFirstSection,
|
|
71
|
-
isLastSection,
|
|
72
|
-
} = props;
|
|
73
|
-
|
|
74
|
-
const headerIsString = typeof header === "string";
|
|
75
|
-
|
|
76
|
-
const {roundedTop, roundedBottom} = getRoundedValuesForHeader(
|
|
77
|
-
cornerKind,
|
|
78
|
-
isFirstSection,
|
|
79
|
-
isLastSection,
|
|
80
|
-
expanded,
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<HeadingSmall tag={tag} style={styles.heading}>
|
|
85
|
-
<Clickable
|
|
86
|
-
id={id}
|
|
87
|
-
aria-expanded={expanded}
|
|
88
|
-
aria-controls={sectionContentUniqueId}
|
|
89
|
-
onClick={onClick}
|
|
90
|
-
disabled={!collapsible}
|
|
91
|
-
testId={testId ? `${testId}-header` : undefined}
|
|
92
|
-
style={[
|
|
93
|
-
styles.headerWrapper,
|
|
94
|
-
animated && styles.headerWrapperWithAnimation,
|
|
95
|
-
caretPosition === "start" && styles.headerWrapperCaretStart,
|
|
96
|
-
roundedTop && styles.roundedTop,
|
|
97
|
-
roundedBottom && styles.roundedBottom,
|
|
98
|
-
headerStyle,
|
|
99
|
-
!collapsible && styles.disabled,
|
|
100
|
-
]}
|
|
101
|
-
ref={ref}
|
|
102
|
-
>
|
|
103
|
-
{() => (
|
|
104
|
-
<>
|
|
105
|
-
<View
|
|
106
|
-
style={[
|
|
107
|
-
styles.headerContent,
|
|
108
|
-
headerIsString && styles.headerString,
|
|
109
|
-
]}
|
|
110
|
-
>
|
|
111
|
-
{headerIsString ? (
|
|
112
|
-
<View
|
|
113
|
-
style={[
|
|
114
|
-
caretPosition === "end"
|
|
115
|
-
? styles.headerStringCaretEnd
|
|
116
|
-
: styles.headerStringCaretStart,
|
|
117
|
-
]}
|
|
118
|
-
>
|
|
119
|
-
{header}
|
|
120
|
-
</View>
|
|
121
|
-
) : (
|
|
122
|
-
header
|
|
123
|
-
)}
|
|
124
|
-
</View>
|
|
125
|
-
{collapsible && (
|
|
126
|
-
<PhosphorIcon
|
|
127
|
-
icon={caretDown}
|
|
128
|
-
color={tokens.color.offBlack64}
|
|
129
|
-
size="small"
|
|
130
|
-
style={[
|
|
131
|
-
animated && styles.iconWithAnimation,
|
|
132
|
-
caretPosition === "start"
|
|
133
|
-
? styles.iconStart
|
|
134
|
-
: styles.iconEnd,
|
|
135
|
-
expanded && styles.iconExpanded,
|
|
136
|
-
]}
|
|
137
|
-
testId={
|
|
138
|
-
testId ? `${testId}-caret-icon` : undefined
|
|
139
|
-
}
|
|
140
|
-
/>
|
|
141
|
-
)}
|
|
142
|
-
</>
|
|
143
|
-
)}
|
|
144
|
-
</Clickable>
|
|
145
|
-
</HeadingSmall>
|
|
146
|
-
);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// The AccordionSection border radius for rounded corners is 12px.
|
|
150
|
-
// If we set the inner radius to the same value, there ends up being
|
|
151
|
-
// a 1px gap between the border and the outline. To fix this, we
|
|
152
|
-
// subtract 1 from the border radius.
|
|
153
|
-
const INNER_BORDER_RADIUS = tokens.spacing.small_12 - 1;
|
|
154
|
-
const ANIMATION_LENGTH = "300ms";
|
|
155
|
-
|
|
156
|
-
const styles = StyleSheet.create({
|
|
157
|
-
heading: {
|
|
158
|
-
// As this is a grid item, it has a default minWidth of auto,
|
|
159
|
-
// which means it would grow to fit its content. minWidth 0 is
|
|
160
|
-
// necessary here to stop a custom header from overflowing out of
|
|
161
|
-
// it container when its content is too long (See AccordionSection's
|
|
162
|
-
// "React Element in Header" story).
|
|
163
|
-
minWidth: 0,
|
|
164
|
-
marginTop: 0,
|
|
165
|
-
},
|
|
166
|
-
headerWrapper: {
|
|
167
|
-
display: "flex",
|
|
168
|
-
flexDirection: "row",
|
|
169
|
-
alignItems: "center",
|
|
170
|
-
overflow: "hidden",
|
|
171
|
-
minWidth: "auto",
|
|
172
|
-
width: "100%",
|
|
173
|
-
// Always make the header's outline show up in front of
|
|
174
|
-
// the content panel.
|
|
175
|
-
position: "relative",
|
|
176
|
-
zIndex: 1,
|
|
177
|
-
|
|
178
|
-
":active": {
|
|
179
|
-
outline: `2px solid ${tokens.color.activeBlue}`,
|
|
180
|
-
},
|
|
181
|
-
|
|
182
|
-
":hover": {
|
|
183
|
-
outline: `2px solid ${tokens.color.blue}`,
|
|
184
|
-
},
|
|
185
|
-
|
|
186
|
-
// Provide basic, default focus styles on older browsers (e.g.
|
|
187
|
-
// Safari 14)
|
|
188
|
-
":focus": {
|
|
189
|
-
boxShadow: `0 0 0 2px ${tokens.color.blue}`,
|
|
190
|
-
},
|
|
191
|
-
|
|
192
|
-
// Remove default focus styles for mouse users ONLY if
|
|
193
|
-
// :focus-visible is supported on this platform.
|
|
194
|
-
":focus:not(:focus-visible)": {
|
|
195
|
-
boxShadow: "none",
|
|
196
|
-
},
|
|
197
|
-
|
|
198
|
-
":focus-visible": {
|
|
199
|
-
outline: `2px solid ${tokens.color.blue}`,
|
|
200
|
-
},
|
|
201
|
-
},
|
|
202
|
-
headerWrapperWithAnimation: {
|
|
203
|
-
transition: `border-radius ${ANIMATION_LENGTH}`,
|
|
204
|
-
},
|
|
205
|
-
headerWrapperCaretStart: {
|
|
206
|
-
flexDirection: "row-reverse",
|
|
207
|
-
},
|
|
208
|
-
// Even though the border radius is already set on the AccordionSection,
|
|
209
|
-
// the hover/focus outline is on the header. We need to have the same
|
|
210
|
-
// border radius here to round out the outline so it looks right over
|
|
211
|
-
// the border.
|
|
212
|
-
roundedTop: {
|
|
213
|
-
borderStartStartRadius: INNER_BORDER_RADIUS,
|
|
214
|
-
borderStartEndRadius: INNER_BORDER_RADIUS,
|
|
215
|
-
},
|
|
216
|
-
roundedBottom: {
|
|
217
|
-
borderEndStartRadius: INNER_BORDER_RADIUS,
|
|
218
|
-
borderEndEndRadius: INNER_BORDER_RADIUS,
|
|
219
|
-
},
|
|
220
|
-
headerContent: {
|
|
221
|
-
flexGrow: 1,
|
|
222
|
-
textAlign: "start",
|
|
223
|
-
},
|
|
224
|
-
headerString: {
|
|
225
|
-
paddingTop: tokens.spacing.medium_16,
|
|
226
|
-
paddingBottom: tokens.spacing.medium_16,
|
|
227
|
-
},
|
|
228
|
-
headerStringCaretEnd: {
|
|
229
|
-
paddingInlineEnd: tokens.spacing.small_12,
|
|
230
|
-
paddingInlineStart: tokens.spacing.medium_16,
|
|
231
|
-
},
|
|
232
|
-
headerStringCaretStart: {
|
|
233
|
-
paddingInlineEnd: tokens.spacing.medium_16,
|
|
234
|
-
paddingInlineStart: tokens.spacing.small_12,
|
|
235
|
-
},
|
|
236
|
-
iconWithAnimation: {
|
|
237
|
-
transition: `transform ${ANIMATION_LENGTH}`,
|
|
238
|
-
},
|
|
239
|
-
iconExpanded: {
|
|
240
|
-
// Turn the caret upside down
|
|
241
|
-
transform: "rotate(180deg)",
|
|
242
|
-
},
|
|
243
|
-
iconStart: {
|
|
244
|
-
marginInlineStart: tokens.spacing.medium_16,
|
|
245
|
-
},
|
|
246
|
-
iconEnd: {
|
|
247
|
-
marginInlineEnd: tokens.spacing.medium_16,
|
|
248
|
-
},
|
|
249
|
-
disabled: {
|
|
250
|
-
pointerEvents: "none",
|
|
251
|
-
color: "inherit",
|
|
252
|
-
|
|
253
|
-
// Provide basic, default focus styles on older browsers (e.g.
|
|
254
|
-
// Safari 14)
|
|
255
|
-
":focus": {
|
|
256
|
-
boxShadow: `0 0 0 2px ${tokens.color.offBlack32}`,
|
|
257
|
-
},
|
|
258
|
-
|
|
259
|
-
// Remove default focus styles for mouse users ONLY if
|
|
260
|
-
// :focus-visible is supported on this platform.
|
|
261
|
-
":focus:not(:focus-visible)": {
|
|
262
|
-
boxShadow: "none",
|
|
263
|
-
},
|
|
264
|
-
|
|
265
|
-
":focus-visible": {
|
|
266
|
-
outline: `2px solid ${tokens.color.offBlack32}`,
|
|
267
|
-
},
|
|
268
|
-
},
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
export default AccordionSectionHeader;
|
|
@@ -1,432 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import {StyleSheet} from "aphrodite";
|
|
3
|
-
import type {StyleDeclaration} from "aphrodite";
|
|
4
|
-
|
|
5
|
-
import {useUniqueIdWithMock, View} from "@khanacademy/wonder-blocks-core";
|
|
6
|
-
import * as tokens from "@khanacademy/wonder-blocks-tokens";
|
|
7
|
-
import {Body} from "@khanacademy/wonder-blocks-typography";
|
|
8
|
-
import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
|
|
9
|
-
|
|
10
|
-
import type {AccordionCornerKindType} from "./accordion";
|
|
11
|
-
import AccordionSectionHeader from "./accordion-section-header";
|
|
12
|
-
|
|
13
|
-
export type TagType = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
|
|
14
|
-
|
|
15
|
-
type Props = AriaProps & {
|
|
16
|
-
/**
|
|
17
|
-
* The unique identifier for the accordion section.
|
|
18
|
-
*/
|
|
19
|
-
id?: string;
|
|
20
|
-
/**
|
|
21
|
-
* The content to display when this section is shown. If a string is
|
|
22
|
-
* passed in, it will automatically be given Body typography from
|
|
23
|
-
* Wonder Blocks Typography.
|
|
24
|
-
*/
|
|
25
|
-
children: string | React.ReactElement;
|
|
26
|
-
/**
|
|
27
|
-
* The header for this section. If a string is passed in, it will
|
|
28
|
-
* automatically be given Body typography from Wonder Blocks Typography.
|
|
29
|
-
*/
|
|
30
|
-
header: string | React.ReactElement;
|
|
31
|
-
/**
|
|
32
|
-
* Whether to put the caret at the start or end of the header block
|
|
33
|
-
* in this section. "start" means it’s on the left of a left-to-right
|
|
34
|
-
* language (and on the right of a right-to-left language), and "end"
|
|
35
|
-
* means it’s on the right of a left-to-right language
|
|
36
|
-
* (and on the left of a right-to-left language).
|
|
37
|
-
* Defaults to "end".
|
|
38
|
-
*
|
|
39
|
-
* If this prop is specified both here in the AccordionSection and
|
|
40
|
-
* within a parent Accordion component, the AccordionSection’s caretPosition
|
|
41
|
-
* value is prioritized.
|
|
42
|
-
*/
|
|
43
|
-
caretPosition?: "start" | "end";
|
|
44
|
-
/**
|
|
45
|
-
* The preset styles for the corners of this accordion.
|
|
46
|
-
* `square` - corners have no border radius.
|
|
47
|
-
* `rounded` - the overall container's corners are rounded.
|
|
48
|
-
* `rounded-per-section` - each section's corners are rounded, and there
|
|
49
|
-
* is white space between each section.
|
|
50
|
-
*
|
|
51
|
-
* If this prop is specified both here in the AccordionSection and
|
|
52
|
-
* within a parent Accordion component, the AccordionSection’s cornerKind
|
|
53
|
-
* value is prioritized.
|
|
54
|
-
*/
|
|
55
|
-
cornerKind?: AccordionCornerKindType;
|
|
56
|
-
/**
|
|
57
|
-
* Whether this section is collapsible. If false, the header will not be
|
|
58
|
-
* clickable, and the section will stay expanded at all times.
|
|
59
|
-
*/
|
|
60
|
-
collapsible?: boolean;
|
|
61
|
-
/**
|
|
62
|
-
* Whether this section is expanded or closed.
|
|
63
|
-
*
|
|
64
|
-
* NOTE: This prop is NOT used when this AccordionSection is rendered
|
|
65
|
-
* within an Accordion component. In that case, the Accordion component
|
|
66
|
-
* manages the expanded state of the AccordionSection.
|
|
67
|
-
*/
|
|
68
|
-
expanded?: boolean;
|
|
69
|
-
/**
|
|
70
|
-
* Whether to include animation on the header. This should be false
|
|
71
|
-
* if the user has `prefers-reduced-motion` opted in. Defaults to false.
|
|
72
|
-
*
|
|
73
|
-
* If this prop is specified both here in the AccordionSection and
|
|
74
|
-
* within a parent Accordion component, the AccordionSection’s animated
|
|
75
|
-
* value is prioritized.
|
|
76
|
-
*/
|
|
77
|
-
animated?: boolean;
|
|
78
|
-
/**
|
|
79
|
-
* Called when the header is clicked.
|
|
80
|
-
* Takes the new expanded state as an argument. This way, the function
|
|
81
|
-
* returned from React.useState can be passed in directly.
|
|
82
|
-
*/
|
|
83
|
-
onToggle?: (newExpandedState: boolean) => unknown;
|
|
84
|
-
/**
|
|
85
|
-
* Custom styles for the overall accordion section container.
|
|
86
|
-
*/
|
|
87
|
-
style?: StyleType;
|
|
88
|
-
/**
|
|
89
|
-
* Custom styles for the header.
|
|
90
|
-
*/
|
|
91
|
-
headerStyle?: StyleType;
|
|
92
|
-
/**
|
|
93
|
-
* The semantic tag for this clickable header (e.g. "h1", "h2", etc).
|
|
94
|
-
* Please use this to ensure that the header is hierarchically correct.
|
|
95
|
-
* Defaults to "h2".
|
|
96
|
-
* */
|
|
97
|
-
tag?: TagType;
|
|
98
|
-
/**
|
|
99
|
-
* The test ID used to locate this component in automated tests.
|
|
100
|
-
*/
|
|
101
|
-
testId?: string;
|
|
102
|
-
/**
|
|
103
|
-
* Whether this section is the first section in the accordion.
|
|
104
|
-
* For internal use only.
|
|
105
|
-
* @ignore
|
|
106
|
-
*/
|
|
107
|
-
isFirstSection?: boolean;
|
|
108
|
-
/**
|
|
109
|
-
* Whether this section is the last section in the accordion.
|
|
110
|
-
* For internal use only.
|
|
111
|
-
* @ignore
|
|
112
|
-
*/
|
|
113
|
-
isLastSection?: boolean;
|
|
114
|
-
/**
|
|
115
|
-
* Whether this section should have role="region". True by default.
|
|
116
|
-
* According to W3, the panel container should have role region except
|
|
117
|
-
* when there are more than six panels in an accordion, in which case
|
|
118
|
-
* we should set this prop to false.
|
|
119
|
-
* For internal use only.
|
|
120
|
-
* @ignore
|
|
121
|
-
*/
|
|
122
|
-
isRegion?: boolean;
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* An AccordionSection displays a section of content that can be shown or
|
|
127
|
-
* hidden by clicking its header. This is generally used within the Accordion
|
|
128
|
-
* component, but it can also be used on its own if you need only one
|
|
129
|
-
* collapsible section.
|
|
130
|
-
*
|
|
131
|
-
* ### Usage
|
|
132
|
-
*
|
|
133
|
-
* ```jsx
|
|
134
|
-
* import {
|
|
135
|
-
* Accordion,
|
|
136
|
-
* AccordionSection
|
|
137
|
-
* } from "@khanacademy/wonder-blocks-accordion";
|
|
138
|
-
*
|
|
139
|
-
* // Within an Accordion
|
|
140
|
-
* <Accordion>
|
|
141
|
-
* <AccordionSection header="First section">
|
|
142
|
-
* This is the information present in the first section
|
|
143
|
-
* </AccordionSection>
|
|
144
|
-
* <AccordionSection header="Second section">
|
|
145
|
-
* This is the information present in the second section
|
|
146
|
-
* </AccordionSection>
|
|
147
|
-
* <AccordionSection header="Third section">
|
|
148
|
-
* This is the information present in the third section
|
|
149
|
-
* </AccordionSection>
|
|
150
|
-
* </Accordion>
|
|
151
|
-
*
|
|
152
|
-
* // On its own, controlled
|
|
153
|
-
* const [expanded, setExpanded] = React.useState(false);
|
|
154
|
-
* <AccordionSection
|
|
155
|
-
* header="A standalone section"
|
|
156
|
-
* expanded={expanded}
|
|
157
|
-
* onToggle={setExpanded}
|
|
158
|
-
* >
|
|
159
|
-
* This is the information present in the standalone section
|
|
160
|
-
* </AccordionSection>
|
|
161
|
-
*
|
|
162
|
-
* // On its own, uncontrolled
|
|
163
|
-
* <AccordionSection header="A standalone section">
|
|
164
|
-
* This is the information present in the standalone section
|
|
165
|
-
* </AccordionSection>
|
|
166
|
-
* ```
|
|
167
|
-
*/
|
|
168
|
-
const AccordionSection = React.forwardRef(function AccordionSection(
|
|
169
|
-
props: Props,
|
|
170
|
-
// Using a button ref here beacuse the ref is pointing to the
|
|
171
|
-
// section header, which is a button.
|
|
172
|
-
ref: React.ForwardedRef<HTMLButtonElement>,
|
|
173
|
-
) {
|
|
174
|
-
const {
|
|
175
|
-
children,
|
|
176
|
-
id,
|
|
177
|
-
header,
|
|
178
|
-
collapsible,
|
|
179
|
-
expanded,
|
|
180
|
-
animated = false,
|
|
181
|
-
onToggle,
|
|
182
|
-
caretPosition = "end",
|
|
183
|
-
cornerKind = "rounded",
|
|
184
|
-
style,
|
|
185
|
-
headerStyle,
|
|
186
|
-
tag,
|
|
187
|
-
testId,
|
|
188
|
-
// Assume it's the first section and last section by default
|
|
189
|
-
// in case this component is being used standalone. If it's part
|
|
190
|
-
// of an accordion, these will be overridden by the Accordion
|
|
191
|
-
// parent component.
|
|
192
|
-
isFirstSection = true,
|
|
193
|
-
isLastSection = true,
|
|
194
|
-
// Assume it's a region by default. Override this to be false
|
|
195
|
-
// if we know there are more than six panels in an accordion.
|
|
196
|
-
isRegion = true,
|
|
197
|
-
...ariaProps
|
|
198
|
-
} = props;
|
|
199
|
-
|
|
200
|
-
const [internalExpanded, setInternalExpanded] = React.useState(
|
|
201
|
-
expanded ?? false,
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
const controlledMode = expanded !== undefined && onToggle;
|
|
205
|
-
|
|
206
|
-
const ids = useUniqueIdWithMock();
|
|
207
|
-
const sectionId = id ?? ids.get("accordion-section");
|
|
208
|
-
// We need an ID for the header so that the content section's
|
|
209
|
-
// aria-labelledby attribute can point to it.
|
|
210
|
-
const headerId = id ? `${id}-header` : ids.get("accordion-section-header");
|
|
211
|
-
// We need an ID for the content section so that the opener's
|
|
212
|
-
// aria-controls attribute can point to it.
|
|
213
|
-
const sectionContentUniqueId = ids.get("accordion-section-content");
|
|
214
|
-
|
|
215
|
-
const sectionStyles = _generateStyles(
|
|
216
|
-
cornerKind,
|
|
217
|
-
isFirstSection,
|
|
218
|
-
isLastSection,
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
const handleClick = () => {
|
|
222
|
-
// Controlled mode
|
|
223
|
-
if (controlledMode) {
|
|
224
|
-
onToggle(!expanded);
|
|
225
|
-
} else {
|
|
226
|
-
// Uncontrolled mode
|
|
227
|
-
setInternalExpanded(!internalExpanded);
|
|
228
|
-
if (onToggle) {
|
|
229
|
-
onToggle(!internalExpanded);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
let expandedState;
|
|
235
|
-
if (collapsible === false) {
|
|
236
|
-
// If the section is disabled (not collapsible), it should
|
|
237
|
-
// always be expanded.
|
|
238
|
-
expandedState = true;
|
|
239
|
-
// If the expanded prop is undefined, we're in uncontrolled mode and
|
|
240
|
-
// should use the internal state to determine the expanded state.
|
|
241
|
-
// Otherwise, we're in controlled mode and should use the expanded prop
|
|
242
|
-
// that's passed in to determine the expanded state.
|
|
243
|
-
} else {
|
|
244
|
-
expandedState = controlledMode ? expanded : internalExpanded;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return (
|
|
248
|
-
<View
|
|
249
|
-
id={sectionId}
|
|
250
|
-
style={[
|
|
251
|
-
styles.wrapper,
|
|
252
|
-
animated && styles.wrapperWithAnimation,
|
|
253
|
-
sectionStyles.wrapper,
|
|
254
|
-
expandedState
|
|
255
|
-
? styles.wrapperExpanded
|
|
256
|
-
: styles.wrapperCollapsed,
|
|
257
|
-
style,
|
|
258
|
-
]}
|
|
259
|
-
testId={testId}
|
|
260
|
-
{...ariaProps}
|
|
261
|
-
>
|
|
262
|
-
<AccordionSectionHeader
|
|
263
|
-
id={headerId}
|
|
264
|
-
header={header}
|
|
265
|
-
caretPosition={caretPosition}
|
|
266
|
-
cornerKind={cornerKind}
|
|
267
|
-
collapsible={collapsible}
|
|
268
|
-
expanded={expandedState}
|
|
269
|
-
animated={animated}
|
|
270
|
-
onClick={handleClick}
|
|
271
|
-
sectionContentUniqueId={sectionContentUniqueId}
|
|
272
|
-
headerStyle={headerStyle}
|
|
273
|
-
tag={tag}
|
|
274
|
-
testId={testId}
|
|
275
|
-
isFirstSection={isFirstSection}
|
|
276
|
-
isLastSection={isLastSection}
|
|
277
|
-
ref={ref}
|
|
278
|
-
/>
|
|
279
|
-
<View
|
|
280
|
-
id={sectionContentUniqueId}
|
|
281
|
-
role={isRegion ? "region" : undefined}
|
|
282
|
-
aria-labelledby={headerId}
|
|
283
|
-
style={[
|
|
284
|
-
styles.contentWrapper,
|
|
285
|
-
expandedState
|
|
286
|
-
? styles.contentWrapperExpanded
|
|
287
|
-
: styles.conentWrapperCollapsed,
|
|
288
|
-
sectionStyles.contentWrapper,
|
|
289
|
-
]}
|
|
290
|
-
testId={testId ? `${testId}-content-panel` : undefined}
|
|
291
|
-
>
|
|
292
|
-
{typeof children === "string" ? (
|
|
293
|
-
<Body style={styles.stringContent}>{children}</Body>
|
|
294
|
-
) : (
|
|
295
|
-
children
|
|
296
|
-
)}
|
|
297
|
-
</View>
|
|
298
|
-
</View>
|
|
299
|
-
);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
const styles = StyleSheet.create({
|
|
303
|
-
wrapper: {
|
|
304
|
-
// Use grid layout for clean animations.
|
|
305
|
-
display: "grid",
|
|
306
|
-
// Remove the View's default relative position because it creates
|
|
307
|
-
// overlap issues with the outline. In this case, it's safe to
|
|
308
|
-
// remove the stacking context beacuse accordion sections are always
|
|
309
|
-
// vertically stacked.
|
|
310
|
-
position: "static",
|
|
311
|
-
boxSizing: "border-box",
|
|
312
|
-
backgroundColor: tokens.color.white,
|
|
313
|
-
},
|
|
314
|
-
wrapperWithAnimation: {
|
|
315
|
-
transition: "grid-template-rows 300ms",
|
|
316
|
-
},
|
|
317
|
-
wrapperCollapsed: {
|
|
318
|
-
gridTemplateRows: "min-content 0fr",
|
|
319
|
-
},
|
|
320
|
-
wrapperExpanded: {
|
|
321
|
-
gridTemplateRows: "min-content 1fr",
|
|
322
|
-
},
|
|
323
|
-
contentWrapper: {
|
|
324
|
-
overflow: "hidden",
|
|
325
|
-
},
|
|
326
|
-
conentWrapperCollapsed: {
|
|
327
|
-
// Make sure screen readers don't read the content when it's
|
|
328
|
-
// collapsed.
|
|
329
|
-
visibility: "hidden",
|
|
330
|
-
},
|
|
331
|
-
contentWrapperExpanded: {
|
|
332
|
-
visibility: "visible",
|
|
333
|
-
},
|
|
334
|
-
stringContent: {
|
|
335
|
-
padding: tokens.spacing.medium_16,
|
|
336
|
-
},
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
const cornerStyles: Record<string, any> = {};
|
|
340
|
-
|
|
341
|
-
const _generateStyles = (
|
|
342
|
-
cornerKind: AccordionCornerKindType,
|
|
343
|
-
isFirstSection: boolean,
|
|
344
|
-
isLastSection: boolean,
|
|
345
|
-
) => {
|
|
346
|
-
const sectionType = `${cornerKind}-${isFirstSection.toString()}-${isLastSection.toString()}`;
|
|
347
|
-
if (cornerStyles[sectionType]) {
|
|
348
|
-
return cornerStyles[sectionType];
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
let wrapperStyle: StyleType = Object.freeze({});
|
|
352
|
-
let contentWrapperStyle: StyleType = Object.freeze({});
|
|
353
|
-
let firstSectionStyle: StyleType = Object.freeze({});
|
|
354
|
-
let lastSectionStyle: StyleType = Object.freeze({});
|
|
355
|
-
|
|
356
|
-
if (cornerKind === "square") {
|
|
357
|
-
wrapperStyle = {
|
|
358
|
-
border: `1px solid ${tokens.color.offBlack16}`,
|
|
359
|
-
borderBottom: "none",
|
|
360
|
-
borderRadius: 0,
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
if (isLastSection) {
|
|
364
|
-
lastSectionStyle = {
|
|
365
|
-
borderBottom: `1px solid ${tokens.color.offBlack16}`,
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (cornerKind === "rounded") {
|
|
371
|
-
wrapperStyle = {
|
|
372
|
-
border: `1px solid ${tokens.color.offBlack16}`,
|
|
373
|
-
borderBottom: "none",
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
if (isFirstSection) {
|
|
377
|
-
firstSectionStyle = {
|
|
378
|
-
borderStartStartRadius: tokens.spacing.small_12,
|
|
379
|
-
borderStartEndRadius: tokens.spacing.small_12,
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (isLastSection) {
|
|
384
|
-
lastSectionStyle = {
|
|
385
|
-
borderBottom: `1px solid ${tokens.color.offBlack16}`,
|
|
386
|
-
borderEndStartRadius: tokens.spacing.small_12,
|
|
387
|
-
borderEndEndRadius: tokens.spacing.small_12,
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
contentWrapperStyle = {
|
|
391
|
-
// Give the last section's content wrapper the same bottom
|
|
392
|
-
// border radius as the wrapper so that the content doesn't
|
|
393
|
-
// overflow out the corners. This issue can't be solved by
|
|
394
|
-
// putting `overflow: "hidden"` on the overall container
|
|
395
|
-
// because that cuts off the header's focus outline.
|
|
396
|
-
borderEndEndRadius: tokens.spacing.small_12,
|
|
397
|
-
borderEndStartRadius: tokens.spacing.small_12,
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
if (cornerKind === "rounded-per-section") {
|
|
403
|
-
wrapperStyle = {
|
|
404
|
-
border: `1px solid ${tokens.color.offBlack16}`,
|
|
405
|
-
borderRadius: tokens.spacing.small_12,
|
|
406
|
-
marginBottom: tokens.spacing.medium_16,
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
contentWrapperStyle = {
|
|
410
|
-
// Give the content wrapper the same border radius as the wrapper
|
|
411
|
-
// so that the content doesn't overflow out the corners. We
|
|
412
|
-
// can't put `overflow: "hidden"` on the overall container
|
|
413
|
-
// because it cuts off the header's focus outline.
|
|
414
|
-
borderEndEndRadius: tokens.spacing.small_12,
|
|
415
|
-
borderEndStartRadius: tokens.spacing.small_12,
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const newStyles: StyleDeclaration = {
|
|
420
|
-
wrapper: {
|
|
421
|
-
...wrapperStyle,
|
|
422
|
-
...firstSectionStyle,
|
|
423
|
-
...lastSectionStyle,
|
|
424
|
-
},
|
|
425
|
-
contentWrapper: contentWrapperStyle,
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
cornerStyles[sectionType] = StyleSheet.create(newStyles);
|
|
429
|
-
return cornerStyles[sectionType];
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
export default AccordionSection;
|