Skip to content

Commit e10dc26

Browse files
fix(unity-react-core): fix long accordion scroll issue
# Conflicts: # packages/unity-react-core/src/components/Accordion/Accordion.jsx # packages/unity-react-core/src/components/Accordion/AccordionCard/AccordionCard.jsx
1 parent d824790 commit e10dc26

File tree

4 files changed

+193
-64
lines changed

4 files changed

+193
-64
lines changed

packages/unity-react-core/src/components/Accordion/Accordion.jsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// @ts-nocheck
22
import PropTypes from "prop-types";
3-
import React, { useState } from "react";
3+
import React, { useId, useRef, useState } from "react";
44

5+
import { adjustShrinkingElementIfAboveViewport } from "../../../../../shared";
56
import { accordionCardPropTypes } from "../../core/models/shared-prop-types";
67
import { AccordionCard } from "./AccordionCard/AccordionCard";
78

@@ -16,32 +17,45 @@ const defaultGAEvent = {
1617
* @typedef {import('../../core/types/shared-types').AccordionProps} AccordionProps
1718
*/
1819

20+
/**
21+
* @typedef {import('../../../core/types/shared-types').AccordionCardItemProps} AccordionCardItemProps
22+
*/
23+
1924
/**
2025
* @param {AccordionProps} props
2126
* @returns {JSX.Element}
2227
*/
2328
const Accordion = ({ cards, openedCard }) => {
2429
const [currentOpenCard, setCurrentOpenCard] = useState(openedCard);
30+
const parentId = `accordion-${useId()}`;
31+
const cardsRef = useRef(/** @type { HTMLDivElement[]} */ []);
2532

2633
const toggleCard = (event, card) => {
2734
event.preventDefault();
28-
2935
if (currentOpenCard !== card) {
36+
const closingCard = cardsRef.current[currentOpenCard - 1];
37+
const closingCardBody = closingCard?.lastElementChild;
38+
adjustShrinkingElementIfAboveViewport(closingCardBody);
3039
setCurrentOpenCard(card);
3140
} else {
3241
setCurrentOpenCard(null);
3342
}
3443
};
3544

3645
return (
37-
<div className="accordion">
46+
<div className="accordion" id={parentId}>
3847
{cards?.map(
3948
(card, key) =>
4049
card.content.body &&
4150
card.content.header && (
4251
<AccordionCard
52+
ref={element => {
53+
cardsRef.current[key] = element;
54+
}}
55+
// eslint-disable-next-line react/no-array-index-key
4356
key={key + 1}
4457
id={key + 1}
58+
parentId={parentId}
4559
item={card}
4660
openCard={currentOpenCard}
4761
onClick={toggleCard}

packages/unity-react-core/src/components/Accordion/Accordion.stories.jsx

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
/* eslint-disable no-plusplus */
12
/* eslint react/jsx-props-no-spreading: "off" */
23
import React from "react";
34

5+
import { getLoremSentences } from "../../../../../shared/constants/strings";
46
import { Accordion } from "./Accordion";
57

68
export default {
@@ -18,13 +20,28 @@ including rules on the use of Call-to-Action buttons and tags.
1820
View component examples and source code below.
1921
2022
This story includes another components for demostration purposes.
21-
`,
23+
24+
## Bootstrap HTML
25+
26+
The \`.accordion\` class is a bootstrap5 class that is used to create an accordion.
27+
28+
- The accordions will conform to the width of the surrounding container.
29+
- There is a recommended character limit of 75 characters for the text within the header of a foldable card.
30+
31+
The recommended markup for an accordion begins from the following pattern.
32+
33+
***Note:*** The \`.accordion-body\` class must be wrapped in a div with the \`.collapse\` class, or else the accordion will have a jerky animation when opening and closing.
34+
`,
2235
},
2336
},
2437
},
2538
};
2639

27-
const Template = args => <Accordion {...args} />;
40+
const Template = args => (
41+
<>
42+
<Accordion {...args} />
43+
</>
44+
);
2845

2946
export const Default = Template.bind({});
3047
Default.args = {
@@ -50,6 +67,75 @@ Default.args = {
5067
],
5168
openedCard: 3,
5269
};
70+
71+
let i = 0;
72+
export const LargeContent = {
73+
render: args => (
74+
<>
75+
<div className="container" style={{ paddingTop: "50vh" }}>
76+
<p>{getLoremSentences(12, i++)}</p>
77+
<p>{getLoremSentences(22, i++)}</p>
78+
<p>{getLoremSentences(10, i++)}</p>
79+
<Accordion {...args} />
80+
<br />
81+
<p>{getLoremSentences(35, i++)}</p>
82+
<p>{getLoremSentences(22, i++)}</p>
83+
<p>{getLoremSentences(12, i++)}</p>
84+
<p>{getLoremSentences(42, i++)}</p>
85+
<p>{getLoremSentences(19, i++)}</p>
86+
<p>{getLoremSentences(12, i++)}</p>
87+
<p>{getLoremSentences(10, i++)}</p>
88+
<p>{getLoremSentences(35, i++)}</p>
89+
<p>{getLoremSentences(22, i++)}</p>
90+
<p>{getLoremSentences(12, i++)}</p>
91+
<p>{getLoremSentences(42, i++)}</p>
92+
<p>{getLoremSentences(19, i++)}</p>
93+
<p>{getLoremSentences(12, i++)}</p>
94+
<p>{getLoremSentences(10, i++)}</p>
95+
</div>
96+
</>
97+
),
98+
args: {
99+
cards: [
100+
{
101+
content: {
102+
header: "Accordion Card 1",
103+
body: "<h4>Quatrenary Headline</h4><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud</p><h5>This is a level five headline. There's a fancy word for that too.</h5><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud</p>",
104+
},
105+
},
106+
{
107+
content: {
108+
header: "Accordion Card 2, Large Content",
109+
body: `<h4>Quatrenary Headline</h4>
110+
<p>${getLoremSentences(35, i++)}</p>
111+
<p>${getLoremSentences(22, i++)}</p>
112+
<p>${getLoremSentences(12, i++)}</p>
113+
<p>${getLoremSentences(42, i++)}</p>
114+
<p>${getLoremSentences(19, i++)}</p>
115+
<p>${getLoremSentences(12, i++)}</p>
116+
<p>${getLoremSentences(10, i++)}</p>
117+
<p>${getLoremSentences(35, i++)}</p>
118+
<p>${getLoremSentences(22, i++)}</p>
119+
<p>${getLoremSentences(12, i++)}</p>
120+
<p>${getLoremSentences(42, i++)}</p>
121+
<p>${getLoremSentences(19, i++)}</p>
122+
<p>${getLoremSentences(12, i++)}</p>
123+
<p>${getLoremSentences(10, i++)}</p>
124+
`,
125+
},
126+
},
127+
{
128+
content: {
129+
header: "Accordion Card 3",
130+
body: `<h4>Quatrenary Headline</h4><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud</p><h5>This is a level five headline. There's a fancy word for that too.</h5><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud</p>
131+
<p>${getLoremSentences(35, i++)}</p>
132+
<p>${getLoremSentences(22, i++)}</p>`,
133+
},
134+
},
135+
],
136+
openedCard: 2,
137+
},
138+
};
53139
export const ColorCombinations = Template.bind({});
54140
ColorCombinations.args = {
55141
cards: [

packages/unity-react-core/src/components/Accordion/AccordionCard/AccordionCard.jsx

Lines changed: 86 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { sanitizeDangerousMarkup } from "@asu/shared";
33
import classNames from "classnames";
44
import PropTypes from "prop-types";
5-
import React from "react";
5+
import React, { forwardRef } from "react";
66

77
import { accordionCardPropTypes } from "../../../core/models/shared-prop-types";
88
import { GaEventWrapper } from "../../GaEventWrapper/GaEventWrapper";
@@ -11,76 +11,104 @@ import { GaEventWrapper } from "../../GaEventWrapper/GaEventWrapper";
1111
* @typedef {import('../../../core/types/shared-types').AccordionCardItemProps} AccordionCardItemProps
1212
*/
1313

14+
/**
15+
* @typedef {import('../../../core/types/shared-types').AccordionCard} AccordionCard
16+
*/
17+
1418
/**
1519
* @param {AccordionCardItemProps} props
1620
* @returns {JSX.Element}
1721
* @ignore
1822
*/
19-
export const AccordionCard = ({ id, item, openCard, onClick, gaData }) => {
20-
const isOpen = id === openCard;
21-
/**
22-
* event to open accordion is happening on the closed accordion
23-
* so the action seem backwards but:
24-
* open will send an event with action "close"
25-
* close will send an event with action "open"
26-
* */
23+
export const AccordionCard = forwardRef(
24+
(
25+
/** @type {AccordionCardItemProps} */ {
26+
id,
27+
parentId,
28+
/** @type {AccordionCard} */ item,
29+
openCard,
30+
onClick,
31+
gaData,
32+
},
33+
ref
34+
) => {
35+
const isOpen = id === openCard;
36+
/**
37+
* event to open accordion is happening on the closed accordion
38+
* so the action seem backwards but:
39+
* open will send an event with action "close"
40+
* close will send an event with action "open"
41+
* */
2742

28-
const gaAction = !isOpen ? "close" : "open";
29-
return (
30-
<div
31-
className={classNames("accordion-item", "mt-3", {
32-
[`accordion-item-${item.color}`]: item.color,
33-
[`accordion-header-icon`]: item.content?.icon,
34-
})}
35-
>
36-
<div className="accordion-header">
37-
<h4>
38-
<GaEventWrapper
39-
gaData={{ ...gaData, action: gaAction, text: item.content.header }}
40-
>
41-
<a
42-
data-testid="accordion-opener"
43-
className={classNames({ [`collapsed`]: !isOpen })}
44-
data-bs-toggle="collapse"
45-
href={`#card-body-${id}`}
46-
role="button"
47-
aria-expanded={isOpen}
48-
aria-controls={`card-body-${id}`}
49-
onClick={e => onClick(e, id)}
43+
const gaAction = !isOpen ? "close" : "open";
44+
return (
45+
<div
46+
ref={ref}
47+
className={classNames("accordion-item", "mt-3", {
48+
[`accordion-item-${item.color}`]: item.color,
49+
[`accordion-header-icon`]: item.content?.icon,
50+
})}
51+
>
52+
<div className="accordion-header">
53+
<h4>
54+
<GaEventWrapper
55+
gaData={{
56+
...gaData,
57+
action: gaAction,
58+
text: item.content.header,
59+
}}
5060
>
51-
{item.content?.icon ? (
52-
<span className="accordion-icon">
53-
<i
54-
className={`${item.content.icon?.[0]} fa-${item.content.icon?.[1]} me-2`}
55-
/>
56-
{item.content.header}
57-
</span>
58-
) : (
59-
item.content?.header
61+
<button
62+
data-testid="accordion-opener"
63+
className={classNames({ [`collapsed`]: !isOpen })}
64+
data-bs-toggle="collapse"
65+
// @ts-expect-error href is needed for Bootstrap collapse
66+
href={`#card-body-${id}`}
67+
type="button"
68+
aria-expanded={isOpen}
69+
aria-controls={`card-body-${id}`}
70+
onClick={e => onClick(e, id)}
71+
>
72+
{item.content?.icon ? (
73+
<span className="accordion-icon">
74+
<i
75+
className={`${item.content.icon?.[0]} fa-${item.content.icon?.[1]} me-2`}
76+
/>
77+
{item.content.header}
78+
</span>
79+
) : (
80+
item.content?.header
81+
)}
82+
<i className="fas fa-chevron-up" />
83+
</button>
84+
</GaEventWrapper>
85+
</h4>
86+
</div>
87+
{item.content?.body && (
88+
<div
89+
data-bs-parent={`#${parentId}`}
90+
id={`card-body-${id}`}
91+
className={classNames("collapse", { show: isOpen })}
92+
>
93+
<div
94+
className="accordion-body"
95+
// eslint-disable-next-line react/no-danger
96+
dangerouslySetInnerHTML={sanitizeDangerousMarkup(
97+
item.content.body
6098
)}
61-
<i className="fas fa-chevron-up" />
62-
</a>
63-
</GaEventWrapper>
64-
</h4>
99+
/>
100+
</div>
101+
)}
65102
</div>
66-
{item.content?.body && (
67-
<div
68-
id={`card-body-${id}`}
69-
className={classNames("collapse", { show: isOpen })}
70-
>
71-
<div
72-
className="accordion-body"
73-
dangerouslySetInnerHTML={sanitizeDangerousMarkup(item.content.body)}
74-
/>
75-
</div>
76-
)}
77-
</div>
78-
);
79-
};
103+
);
104+
}
105+
);
80106

81107
AccordionCard.propTypes = {
82108
id: PropTypes.number,
109+
// @ts-ignore a technical type mismatch between PropTypes definition and your TypeScript
83110
item: accordionCardPropTypes,
111+
parentId: PropTypes.string,
84112
openCard: PropTypes.number,
85113
onClick: PropTypes.func,
86114

packages/unity-react-core/src/core/types/shared-types.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686

8787
/**
8888
* @callback ReactMouseEvent
89-
* @param {React.MouseEvent<HTMLAnchorElement, MouseEvent>} event
89+
* @param {React.MouseEvent<HTMLButtonElement, MouseEvent>} event
9090
* @param {number} id
9191
* @param {string} [cardTitle] // @deprecated
9292
* @returns {void}
@@ -95,6 +95,7 @@
9595
/**
9696
* @typedef {Object} AccordionCardItemProps
9797
* @property {number} id
98+
* @property {string} parentId
9899
* @property {AccordionCard} item
99100
* @property {number} openCard
100101
* @property {ReactMouseEvent} onClick

0 commit comments

Comments
 (0)