Skip to content

Commit 295f7ee

Browse files
authored
Merge pull request #61 from arbisoft-qaisarirfan/add-see-more-toggle-video-description
feat: add "See More" toggle for session description in video detail
2 parents a0c4564 + 41af1ab commit 295f7ee

File tree

9 files changed

+249
-4
lines changed

9 files changed

+249
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`ReadMore Component should match snapshot 1`] = `
4+
<DocumentFragment>
5+
<p
6+
class="MuiTypography-root MuiTypography-bodySmall css-cbjig5-MuiTypography-root-Logo-root"
7+
>
8+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum
9+
<span>
10+
...
11+
</span>
12+
<span
13+
aria-hidden="true"
14+
class="hidden"
15+
data-testid="hidden-text"
16+
>
17+
accumsan arcu in nunc pharetra, ac consectetur nulla bibendum.
18+
</span>
19+
<span
20+
aria-expanded="false"
21+
class="show-more-button"
22+
data-testid="show-more-button"
23+
role="button"
24+
tabindex="0"
25+
>
26+
Read more
27+
</span>
28+
</p>
29+
</DocumentFragment>
30+
`;

src/components/ReadMore/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import ReadMore from "./readMore";
2+
3+
export * from "./types";
4+
5+
export default ReadMore;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { faker } from "@faker-js/faker";
2+
import type { Meta, StoryObj } from "@storybook/react";
3+
4+
import ReadMore from "./readMore";
5+
import { ReadMoreProps } from "./types";
6+
7+
const meta: Meta<ReadMoreProps> = {
8+
title: "Components/ReadMore",
9+
component: ReadMore,
10+
tags: ["autodocs"],
11+
argTypes: {
12+
amountOfWords: {
13+
control: "number",
14+
description: "The number of words to show before truncating the text",
15+
},
16+
className: {
17+
control: "text",
18+
description: "Additional CSS class for the component",
19+
},
20+
showLessText: {
21+
control: "text",
22+
description: "Text to display for the 'Show Less' button",
23+
},
24+
showMoreText: {
25+
control: "text",
26+
description: "Text to display for the 'Show More' button",
27+
},
28+
text: {
29+
control: "text",
30+
description: "The full text to display",
31+
},
32+
},
33+
};
34+
35+
export default meta;
36+
37+
type Story = StoryObj<ReadMoreProps>;
38+
39+
export const Default: Story = {
40+
args: {
41+
text: faker.lorem.paragraphs(),
42+
showMoreText: "Show More",
43+
showLessText: "Show Less",
44+
amountOfWords: 36,
45+
},
46+
};
47+
48+
export const CustomAmountOfWords: Story = {
49+
args: {
50+
...Default.args,
51+
amountOfWords: 20,
52+
},
53+
};
54+
55+
export const LongText: Story = {
56+
args: {
57+
...Default.args,
58+
text: faker.lorem.paragraphs(),
59+
},
60+
};
61+
62+
export const CustomButtonText: Story = {
63+
args: {
64+
...Default.args,
65+
showMoreText: "Read More",
66+
showLessText: "Read Less",
67+
},
68+
};
+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { customRender, screen, fireEvent } from "@/jest/utils/testUtils";
2+
3+
import ReadMore from "./readMore";
4+
5+
describe("ReadMore Component", () => {
6+
const text =
7+
// eslint-disable-next-line max-len
8+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum accumsan arcu in nunc pharetra, ac consectetur nulla bibendum.";
9+
10+
it("renders the initial text correctly", () => {
11+
customRender(<ReadMore text={text} amountOfWords={5} showMoreText="Read more" showLessText="Read less" />);
12+
13+
expect(screen.getByTestId("hidden-text")).not.toHaveTextContent(/Lorem ipsum dolor sit amet/);
14+
expect(screen.getByTestId("hidden-text")).toHaveTextContent(/consectetur adipiscing elit/);
15+
expect(screen.getByTestId("hidden-text")).toHaveClass("hidden");
16+
const showMoreButton = screen.getByTestId("show-more-button");
17+
expect(showMoreButton).toHaveTextContent("Read more");
18+
expect(showMoreButton).not.toHaveTextContent("Read less");
19+
});
20+
21+
it("expands and collapses text when clicking 'Read more' and 'Read less'", () => {
22+
customRender(<ReadMore text={text} amountOfWords={5} showMoreText="Read more" showLessText="Read less" />);
23+
24+
const showMoreButton = screen.getByText("Read more");
25+
expect(showMoreButton).toBeInTheDocument();
26+
27+
fireEvent.click(showMoreButton);
28+
expect(screen.getByTestId("hidden-text")).not.toHaveClass("hidden");
29+
expect(screen.getByText("Read less")).toBeInTheDocument();
30+
31+
fireEvent.click(screen.getByText("Read less"));
32+
expect(screen.getByTestId("hidden-text")).toHaveClass("hidden");
33+
});
34+
35+
it("toggles expansion with keyboard (Enter and Space keys)", () => {
36+
customRender(<ReadMore text={text} amountOfWords={5} showMoreText="Read more" showLessText="Read less" />);
37+
38+
const showMoreButton = screen.getByTestId("show-more-button");
39+
40+
fireEvent.keyDown(showMoreButton, { key: "Enter" });
41+
expect(screen.getByTestId("hidden-text")).not.toHaveClass("hidden");
42+
43+
fireEvent.keyDown(showMoreButton, { key: " " });
44+
expect(screen.getByTestId("hidden-text")).toHaveClass("hidden");
45+
});
46+
47+
it("does not show 'Read more' button if text is shorter than amountOfWords", () => {
48+
customRender(<ReadMore text="Short text" amountOfWords={10} showMoreText="Read more" showLessText="Read less" />);
49+
50+
expect(screen.getByText("Short text")).toBeInTheDocument();
51+
expect(screen.queryByText("Read more")).not.toBeInTheDocument();
52+
});
53+
54+
it("should match snapshot", () => {
55+
const { asFragment } = customRender(
56+
<ReadMore amountOfWords={10} text={text} showMoreText="Read more" showLessText="Read less" />
57+
);
58+
expect(asFragment()).toMatchSnapshot();
59+
});
60+
});

src/components/ReadMore/readMore.tsx

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { useState, useMemo, KeyboardEvent } from "react";
2+
3+
import { StyledReadMore } from "./styled";
4+
import { ReadMoreProps } from "./types";
5+
6+
const ReadMore = ({ className, text, amountOfWords = 36, showMoreText, showLessText }: ReadMoreProps) => {
7+
const [isExpanded, setIsExpanded] = useState(false);
8+
9+
const { visibleText, hiddenText, canExpand } = useMemo(() => {
10+
const words = text.split(" ");
11+
const canTextExpand = words.length > amountOfWords;
12+
return {
13+
visibleText: canTextExpand ? words.slice(0, amountOfWords - 1).join(" ") : text,
14+
hiddenText: canTextExpand ? words.slice(amountOfWords - 1).join(" ") : "",
15+
canExpand: canTextExpand,
16+
};
17+
}, [text, amountOfWords]);
18+
19+
const toggleExpand = () => setIsExpanded((prev) => !prev);
20+
21+
const handleKeyboard = (e: KeyboardEvent<HTMLSpanElement>) => {
22+
if (e.key === " " || e.key === "Enter") {
23+
e.preventDefault();
24+
toggleExpand();
25+
}
26+
};
27+
28+
return (
29+
<StyledReadMore className={className} variant="bodySmall" color="textSecondary">
30+
{visibleText}
31+
{canExpand && (
32+
<>
33+
{!isExpanded && <span>... </span>}
34+
<span data-testid="hidden-text" className={!isExpanded ? "hidden" : ""} aria-hidden={!isExpanded}>
35+
{" " + hiddenText}
36+
</span>
37+
<span
38+
className="show-more-button"
39+
data-testid="show-more-button"
40+
role="button"
41+
tabIndex={0}
42+
aria-expanded={isExpanded}
43+
onKeyDown={handleKeyboard}
44+
onClick={toggleExpand}
45+
>
46+
{isExpanded ? showLessText : showMoreText}
47+
</span>
48+
</>
49+
)}
50+
</StyledReadMore>
51+
);
52+
};
53+
54+
export default ReadMore;

src/components/ReadMore/styled.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { css, styled } from "@mui/material/styles";
2+
import Typography from "@mui/material/Typography";
3+
4+
export const StyledReadMore = styled(Typography, { name: "Logo" })(() => {
5+
return css`
6+
.hidden {
7+
display: none;
8+
}
9+
.show-more-button {
10+
cursor: pointer;
11+
display: block;
12+
font-size: 14px;
13+
margin-top: 10px;
14+
text-decoration: underline;
15+
text-transform: capitalize;
16+
}
17+
`;
18+
});

src/components/ReadMore/types.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface ReadMoreProps {
2+
amountOfWords?: number;
3+
className?: string;
4+
showLessText: string;
5+
showMoreText: string;
6+
text: string;
7+
}

src/features/VideoDetail/videoDetail.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,19 @@ import { format } from "date-fns";
1212
import { useParams } from "next/navigation";
1313

1414
import MainLayoutContainer from "@/components/containers/MainLayoutContainer";
15+
import ReadMore from "@/components/ReadMore";
1516
import RecommendedVideoCard, { RecommendedVideoCardProps } from "@/components/RecommendedVideoCard";
1617
import VideoPlayer from "@/components/VideoPlayer";
1718
import useNavigation from "@/hooks/useNavigation";
1819
import { useEventDetailQuery, useEventTagsQuery } from "@/redux/events/apiSlice";
20+
import { useTranslation } from "@/services/i18n/client";
1921
import { convertSecondsToFormattedTime } from "@/utils/utils";
2022

2123
import { StyledDetailSection, StyledNotesSection, StyledTitleSection, TagsContainer } from "./styled";
2224

2325
const VideoDetail = () => {
2426
const { videoId } = useParams<{ videoId: string }>();
27+
const { t } = useTranslation("common");
2528

2629
const { navigateTo } = useNavigation();
2730

@@ -114,9 +117,7 @@ const VideoDetail = () => {
114117
<StyledNotesSection>
115118
<Typography variant="h5">Session Notes</Typography>
116119
<div className="description">
117-
<Typography variant="bodySmall" color="textSecondary">
118-
{dataEvent?.description}
119-
</Typography>
120+
<ReadMore text={dataEvent?.description ?? ""} showLessText={t("show_less")} showMoreText={t("show_more")} />
120121
</div>
121122
</StyledNotesSection>
122123
</>

src/services/i18n/locales/en/common.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,7 @@
33
"videos": "Videos",
44
"light": "Light",
55
"system": "System",
6-
"dark": "Dark"
6+
"dark": "Dark",
7+
"show_less": "Show Less",
8+
"show_more": "Show More"
79
}

0 commit comments

Comments
 (0)