@khanacademy/wonder-blocks-accordion 1.3.6 → 1.3.8

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.
@@ -1,295 +0,0 @@
1
- import * as React from "react";
2
- import {StyleSheet} from "aphrodite";
3
-
4
- import {addStyle} from "@khanacademy/wonder-blocks-core";
5
- import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
6
-
7
- import AccordionSection from "./accordion-section";
8
-
9
- const StyledUnorderedList = addStyle("ul");
10
-
11
- export type AccordionCornerKindType =
12
- | "square"
13
- | "rounded"
14
- | "rounded-per-section";
15
-
16
- type Props = AriaProps & {
17
- /**
18
- * The unique identifier for the accordion.
19
- */
20
- id?: string;
21
- /**
22
- * The AccordionSection components to display within this Accordion.
23
- */
24
- children: Array<
25
- React.ReactElement<React.ComponentProps<typeof AccordionSection>>
26
- >;
27
- /**
28
- * The index of the AccordionSection that should be expanded when the
29
- * Accordion is first rendered. If not specified, no AccordionSections
30
- * will be expanded when the Accordion is first rendered.
31
- */
32
- initialExpandedIndex?: number;
33
- /**
34
- * Whether multiple AccordionSections can be expanded at the same time.
35
- * If not specified, multiple AccordionSections can be expanded at a time.
36
- */
37
- allowMultipleExpanded?: boolean;
38
- /**
39
- * Whether to put the caret at the start or end of the header block
40
- * in this section. "start" means it’s on the left of a left-to-right
41
- * language (and on the right of a right-to-left language), and "end"
42
- * means it’s on the right of a left-to-right language
43
- * (and on the left of a right-to-left language).
44
- * Defaults to "end".
45
- *
46
- * If this prop is specified both here in the Accordion and within
47
- * a child AccordionSection component, the AccordionSection’s caretPosition
48
- * value is prioritized.
49
- */
50
- caretPosition?: "start" | "end";
51
- /**
52
- * The preset styles for the corners of this accordion.
53
- * `square` - corners have no border radius.
54
- * `rounded` - the overall container's corners are rounded.
55
- * `rounded-per-section` - each section's corners are rounded,
56
- * and there is vertical white space between each section.
57
- *
58
- * If this prop is specified both here in the Accordion and within
59
- * a child AccordionSection component, the AccordionSection’s cornerKind
60
- * value is prioritized.
61
- */
62
- cornerKind?: AccordionCornerKindType;
63
- /**
64
- * Whether to include animation on the header. This should be false
65
- * if the user has `prefers-reduced-motion` opted in. Defaults to false.
66
- *
67
- * If this prop is specified both here in the Accordion and within
68
- * a child AccordionSection component, the AccordionSection’s animated
69
- * value is prioritized.
70
- */
71
- animated?: boolean;
72
- /**
73
- * Custom styles for the overall accordion container.
74
- */
75
- style?: StyleType;
76
- };
77
-
78
- const LANDMARK_PROLIFERATION_THRESHOLD = 6;
79
-
80
- /**
81
- * An accordion displays a vertically stacked list of sections, each of which
82
- * contains content that can be shown or hidden by clicking its header.
83
- *
84
- * The Wonder Blocks Accordion component is a styled wrapper for a list of
85
- * AccordionSection components. It also wraps the AccordionSection
86
- * components in list items.
87
- *
88
- * ### Usage
89
- *
90
- * ```jsx
91
- * import {
92
- * Accordion,
93
- * AccordionSection
94
- * } from "@khanacademy/wonder-blocks-accordion";
95
- *
96
- * <Accordion>
97
- * <AccordionSection header="First section">
98
- * This is the information present in the first section
99
- * </AccordionSection>
100
- * <AccordionSection header="Second section">
101
- * This is the information present in the second section
102
- * </AccordionSection>
103
- * <AccordionSection header="Third section">
104
- * This is the information present in the third section
105
- * </AccordionSection>
106
- * </Accordion>
107
- * ```
108
- */
109
- const Accordion = React.forwardRef(function Accordion(
110
- props: Props,
111
- ref: React.ForwardedRef<HTMLUListElement>,
112
- ) {
113
- const {
114
- children,
115
- id,
116
- initialExpandedIndex,
117
- allowMultipleExpanded = true,
118
- caretPosition,
119
- cornerKind = "rounded",
120
- animated,
121
- style,
122
- ...ariaProps
123
- } = props;
124
-
125
- // Starting array for the initial expanded state of each section.
126
- const startingArray = Array(children.length).fill(false);
127
- // If initialExpandedIndex is specified, we want to open that section.
128
- if (initialExpandedIndex !== undefined) {
129
- startingArray[initialExpandedIndex] = true;
130
- }
131
- const [sectionsOpened, setSectionsOpened] = React.useState(startingArray);
132
-
133
- // NOTE: It may seem like we should filter out non-collapsible sections
134
- // here as they are effectively disabled. However, we should keep these
135
- // disabled sections in the focus order as they'd receive focus anyway
136
- // with `aria-disabled` and visually impaired users should still know
137
- // they are there. Screenreaders will read them out as disabled, the
138
- // status will still be clear to users.
139
- const childRefs: Array<React.RefObject<HTMLButtonElement>> = Array(
140
- children.length,
141
- ).fill(null);
142
-
143
- // If the number of sections is greater than the threshold,
144
- // we don't want to use the `region` role on the AccordionSection
145
- // components because it will cause too many landmarks to be created.
146
- // (See https://www.w3.org/WAI/ARIA/apg/patterns/accordion/)
147
- const sectionsAreRegions =
148
- children.length <= LANDMARK_PROLIFERATION_THRESHOLD;
149
-
150
- const handleSectionClick = (
151
- index: number,
152
- childOnToggle?: (newExpandedState: boolean) => unknown,
153
- ) => {
154
- // If allowMultipleExpanded is false, we want to close all other
155
- // sections when one is opened.
156
- const newSectionsOpened = allowMultipleExpanded
157
- ? [...sectionsOpened]
158
- : Array(children.length).fill(false);
159
- const newOpenedValueAtIndex = !sectionsOpened[index];
160
-
161
- newSectionsOpened[index] = newOpenedValueAtIndex;
162
- setSectionsOpened(newSectionsOpened);
163
-
164
- if (childOnToggle) {
165
- childOnToggle(newOpenedValueAtIndex);
166
- }
167
- };
168
-
169
- /**
170
- * Keyboard navigation for keys: ArrowUp, ArrowDown, Home, and End.
171
- */
172
- const handleKeyDown = (event: React.KeyboardEvent) => {
173
- // From https://www.w3.org/WAI/ARIA/apg/patterns/accordion/
174
- // - Down Arrow: If focus is on an accordion header, moves focus to the next accordion header. If focus is on the last accordion header, either does nothing or moves focus to the first accordion header.
175
- // - Up Arrow: If focus is on an accordion header, moves focus to the previous accordion header. If focus is on the first accordion header, either does nothing or moves focus to the last accordion header.
176
- // - Home: When focus is on an accordion header, moves focus to the first accordion header.
177
- // - End: When focus is on an accordion header, moves focus to the last accordion header.
178
-
179
- const currentlyFocusedHeaderIndex = childRefs.findIndex(
180
- (ref) => ref.current === document.activeElement,
181
- );
182
-
183
- // If the currently focused element is not a header, do nothing.
184
- if (currentlyFocusedHeaderIndex === -1) {
185
- return;
186
- }
187
-
188
- switch (event.key) {
189
- // ArrowUp focuses on the previous section.
190
- case "ArrowUp":
191
- // Stop the page from scrolling when the up arrow is pressed.
192
- event.preventDefault();
193
- // Get the previous section, or cycle to last section if
194
- // the first section is currently focused.
195
- const previousSectionIndex =
196
- (currentlyFocusedHeaderIndex + children.length - 1) %
197
- children.length;
198
- const previousChildRef = childRefs[previousSectionIndex];
199
- previousChildRef.current?.focus();
200
-
201
- break;
202
- // ArrowDown focuses on the next section.
203
- case "ArrowDown":
204
- // Stop the page from scrolling when the down arrow is pressed.
205
- event.preventDefault();
206
- // Get the next section, or cycle to first section if
207
- // the last section is currently focused.
208
- const nextSectionIndex =
209
- (currentlyFocusedHeaderIndex + 1) % children.length;
210
- const nextChildRef = childRefs[nextSectionIndex];
211
- nextChildRef.current?.focus();
212
-
213
- break;
214
- // Home focuses on the first section.
215
- case "Home":
216
- // Stop the page from jumping up when the home key is pressed.
217
- event.preventDefault();
218
- const firstChildRef = childRefs[0];
219
- firstChildRef.current?.focus();
220
-
221
- break;
222
- // End focuses on the last section.
223
- case "End":
224
- // Stop the page from jumping down when the end key is pressed.
225
- event.preventDefault();
226
- const lastChildRef = childRefs[children.length - 1];
227
- lastChildRef.current?.focus();
228
-
229
- break;
230
- }
231
- };
232
-
233
- return (
234
- <StyledUnorderedList
235
- style={[styles.wrapper, style]}
236
- onKeyDown={handleKeyDown}
237
- {...ariaProps}
238
- ref={ref}
239
- >
240
- {children.map((child, index) => {
241
- const {
242
- caretPosition: childCaretPosition,
243
- cornerKind: childCornerKind,
244
- onToggle: childOnToggle,
245
- animated: childAnimated,
246
- } = child.props;
247
-
248
- // Create a ref for each child AccordionSection to
249
- // be able to focus on them with keyboard navigation.
250
- const childRef = React.createRef<HTMLButtonElement>();
251
- childRefs[index] = childRef;
252
-
253
- const isFirstChild = index === 0;
254
- const isLastChild = index === children.length - 1;
255
-
256
- return (
257
- // If the AccordionSections are rendered within the
258
- // Accordion, they are part of a list, so they should
259
- // be list items.
260
- <li key={index} id={id}>
261
- {React.cloneElement(child, {
262
- // Prioritize AccordionSection's props when
263
- // they're overloading Accordion's props.
264
- animated: childAnimated ?? animated,
265
- caretPosition: childCaretPosition ?? caretPosition,
266
- cornerKind: childCornerKind ?? cornerKind,
267
- // AccordionSection's expanded prop does not get
268
- // used here when it's rendered within Accordion
269
- // since the expanded state is managed by Accordion.
270
- expanded: sectionsOpened[index],
271
- onToggle: () =>
272
- handleSectionClick(index, childOnToggle),
273
- isFirstSection: isFirstChild,
274
- isLastSection: isLastChild,
275
- isRegion: sectionsAreRegions,
276
- ref: childRef,
277
- })}
278
- </li>
279
- );
280
- })}
281
- </StyledUnorderedList>
282
- );
283
- });
284
-
285
- const styles = StyleSheet.create({
286
- wrapper: {
287
- boxSizing: "border-box",
288
- listStyle: "none",
289
- // Reset the default padding for lists.
290
- padding: 0,
291
- width: "100%",
292
- },
293
- });
294
-
295
- export default Accordion;
package/src/index.ts DELETED
@@ -1,4 +0,0 @@
1
- import Accordion from "./components/accordion";
2
- import AccordionSection from "./components/accordion-section";
3
-
4
- export {Accordion, AccordionSection};
package/src/utils.test.ts DELETED
@@ -1,59 +0,0 @@
1
- import {getRoundedValuesForHeader} from "./utils";
2
-
3
- /**
4
- * This is a test for the function getRoundedValuesForHeader. This is used
5
- * to determine if the AccordionSectionHeader should be rounded based on the
6
- * cornerKind prop, if it is the first or last section, and if it is expanded.
7
- *
8
- * If the cornerKind is "rounded-per-section", the section should always be
9
- * rounded at the top, and only rounded at the bottom if the section is closed.
10
- * If the cornerKind is "rounded", the section should be rounded at the top
11
- * if it is the first section, and only rounded at the bottom if it is the
12
- * last section and the section is closed.
13
- */
14
- describe("getRoundedValuesForHeader", () => {
15
- test.each`
16
- cornerKind | isFirstSection | isLastSection | expanded | roundedTop | roundedBottom
17
- ${"rounded-per-section"} | ${true} | ${true} | ${true} | ${true} | ${false}
18
- ${"rounded-per-section"} | ${true} | ${true} | ${false} | ${true} | ${true}
19
- ${"rounded-per-section"} | ${true} | ${false} | ${true} | ${true} | ${false}
20
- ${"rounded-per-section"} | ${true} | ${false} | ${false} | ${true} | ${true}
21
- ${"rounded-per-section"} | ${false} | ${true} | ${true} | ${true} | ${false}
22
- ${"rounded-per-section"} | ${false} | ${true} | ${false} | ${true} | ${true}
23
- ${"rounded-per-section"} | ${false} | ${false} | ${true} | ${true} | ${false}
24
- ${"rounded-per-section"} | ${false} | ${false} | ${false} | ${true} | ${true}
25
- ${"rounded"} | ${true} | ${true} | ${true} | ${true} | ${false}
26
- ${"rounded"} | ${true} | ${true} | ${false} | ${true} | ${true}
27
- ${"rounded"} | ${true} | ${false} | ${true} | ${true} | ${false}
28
- ${"rounded"} | ${true} | ${false} | ${false} | ${true} | ${false}
29
- ${"rounded"} | ${false} | ${true} | ${true} | ${false} | ${false}
30
- ${"rounded"} | ${false} | ${true} | ${false} | ${false} | ${true}
31
- ${"rounded"} | ${false} | ${false} | ${true} | ${false} | ${false}
32
- ${"rounded"} | ${false} | ${false} | ${false} | ${false} | ${false}
33
- ${"square"} | ${true} | ${true} | ${true} | ${false} | ${false}
34
- `(
35
- `returns $roundedTop roundedTop and $roundedBottom roundedBottom when
36
- cornerKind is $cornerKind, isFirstSection is $isFirstSection,
37
- isLastSection is $isLastSection, and expanded is $expanded`,
38
- ({
39
- cornerKind,
40
- isFirstSection,
41
- isLastSection,
42
- expanded,
43
- roundedTop,
44
- roundedBottom,
45
- }) => {
46
- expect(
47
- getRoundedValuesForHeader(
48
- cornerKind,
49
- isFirstSection,
50
- isLastSection,
51
- expanded,
52
- ),
53
- ).toEqual({
54
- roundedTop,
55
- roundedBottom,
56
- });
57
- },
58
- );
59
- });
package/src/utils.ts DELETED
@@ -1,58 +0,0 @@
1
- import type {AccordionCornerKindType} from "./components/accordion";
2
-
3
- /**
4
- * Determine if the AccordionSectionHeader should be rounded based on the
5
- * cornerKind prop, if it is the first or last section, and if it is expanded.
6
- * It is important to determine this for the AccordionSectionHeader instead
7
- * of the AccordionSection so that the focus outline around the clickable
8
- * header is rounded correctly.
9
- *
10
- * If the cornerKind is "rounded-per-section", the section should always be
11
- * rounded at the top, and only rounded at the bottom if the section is closed.
12
- * If the cornerKind is "rounded", the section should be rounded at the top
13
- * if it is the first section, and only rounded at the bottom if it is the
14
- * last section and the section is closed.
15
- *
16
- * @param cornerKind The cornerKind prop passed into the AccordionSeciton.
17
- * This can be "rounded-per-section", "rounded", or "square".
18
- * @param isFirstSection Whether or not the section is the first section
19
- * in the Accordion.
20
- * @param isLastSection Whether or not the section is the last section
21
- * in the Accordion.
22
- * @param expanded Whether or not the accordion section is expanded.
23
- * @returns An object with the roundedTop and roundedBottom values.
24
- */
25
- export function getRoundedValuesForHeader(
26
- cornerKind: AccordionCornerKindType,
27
- isFirstSection: boolean,
28
- isLastSection: boolean,
29
- expanded: boolean,
30
- ) {
31
- switch (cornerKind) {
32
- case "rounded-per-section":
33
- return {
34
- roundedTop: true,
35
- // When the section is closed, the header is the last element
36
- // in the AccordionSection, so we need to round the bottom.
37
- // If the section is expanded, the content expands under the
38
- // header, so it should not longer be rounded (the content
39
- // will be rounded instead).
40
- roundedBottom: !expanded,
41
- };
42
- case "rounded":
43
- return {
44
- roundedTop: isFirstSection,
45
- // When the section is closed, the header is the last element
46
- // in the AccordionSection, so we need to round the bottom.
47
- // If the section is expanded, the content expands under the
48
- // header, so it should not longer be rounded (the content
49
- // will be rounded instead).
50
- roundedBottom: isLastSection && !expanded,
51
- };
52
- default:
53
- return {
54
- roundedTop: false,
55
- roundedBottom: false,
56
- };
57
- }
58
- }
@@ -1,15 +0,0 @@
1
- {
2
- "exclude": ["dist"],
3
- "extends": "../tsconfig-shared.json",
4
- "compilerOptions": {
5
- "outDir": "./dist",
6
- "rootDir": "src",
7
- },
8
- "references": [
9
- {"path": "../wonder-blocks-clickable/tsconfig-build.json"},
10
- {"path": "../wonder-blocks-core/tsconfig-build.json"},
11
- {"path": "../wonder-blocks-icon/tsconfig-build.json"},
12
- {"path": "../wonder-blocks-tokens/tsconfig-build.json"},
13
- {"path": "../wonder-blocks-typography/tsconfig-build.json"},
14
- ]
15
- }