diff --git a/packages/unity-bootstrap-theme/src/scss/extends/_images.scss b/packages/unity-bootstrap-theme/src/scss/extends/_images.scss index 337f2eea61..b600e61d51 100644 --- a/packages/unity-bootstrap-theme/src/scss/extends/_images.scss +++ b/packages/unity-bootstrap-theme/src/scss/extends/_images.scss @@ -23,6 +23,7 @@ // Images with captions .uds-figure { + margin: 0; width: 100%; img.img-original { width: initial; @@ -60,8 +61,14 @@ &.uds-img-drop-shadow { box-shadow: $uds-size-spacing-0 $uds-size-spacing-1 $uds-size-spacing-2 rgba(25, 25, 25, 0.2); - .uds-figure { - margin-bottom: $uds-size-spacing-0; + } +} + +.uds-card-arrangement { + .uds-img { + img { + aspect-ratio: 4/3; + object-fit: cover; } } } diff --git a/packages/unity-react-core/src/components/Card/Card.jsx b/packages/unity-react-core/src/components/Card/Card.jsx index 990516f3fe..6e5a7812ea 100644 --- a/packages/unity-react-core/src/components/Card/Card.jsx +++ b/packages/unity-react-core/src/components/Card/Card.jsx @@ -46,7 +46,69 @@ export const Card = ({ tags, showBorders = true, cardLink, + cards = [], + columns = "0", + layout = "auto" }) => { + // If multiple cards are provided, render them in a card container + if (cards.length > 1) { + const getColumnClass = () => { + switch (columns) { + case "2": + return ""; + case "3": + return "three-columns"; + case "4": + return "four-columns"; + default: + return ""; + } + }; + + return ( +
+
+ {cards.map((card, index) => ( + + ))} +
+
+ ); + } + + // Single card - render as before return ( ( +
+
+
+ +
+
+
+); + +export const MultipleCardsTwoColumns = MultipleCardsTemplate.bind({}); +MultipleCardsTwoColumns.args = { + columns: "2", + cards: [ + { + title: "First Card", + body: "This is the first card in a two-column layout.", + image: img1, + imageAltText: "First card image", + type: "default", + }, + { + title: "Second Card", + body: "This is the second card in a two-column layout.", + image: img1, + imageAltText: "Second card image", + type: "default", + }, + { + title: "Third Card", + body: "This is the third card in a two-column layout.", + image: img1, + imageAltText: "Third card image", + type: "default", + }, + { + title: "Fourth Card", + body: "This is the fourth card in a two-column layout.", + image: img1, + imageAltText: "Fourth card image", + type: "default", + }, + ], +}; + +export const MultipleCardsFourColumns = MultipleCardsTemplate.bind({}); +MultipleCardsFourColumns.args = { + columns: "4", + cards: [ + { + title: "First Card", + body: "This is the first card in a four-column layout.", + image: img1, + imageAltText: "First card image", + type: "default", + }, + { + title: "Second Card", + body: "This is the second card in a four-column layout.", + image: img1, + imageAltText: "Second card image", + type: "default", + }, + { + title: "Third Card", + body: "This is the third card in a four-column layout.", + image: img1, + imageAltText: "Third card image", + type: "default", + }, + { + title: "Fourth Card", + body: "This is the fourth card in a four-column layout.", + image: img1, + imageAltText: "Fourth card image", + type: "default", + }, + ], +}; + +export const MultipleCardsThreeColumns = MultipleCardsTemplate.bind({}); +MultipleCardsThreeColumns.args = { + columns: "3", + cards: [ + { + title: "First Card", + body: "This is the first card in a three-column layout.", + image: img1, + imageAltText: "First card image", + type: "default", + }, + { + title: "Second Card", + body: "This is the second card in a three-column layout.", + image: img1, + imageAltText: "Second card image", + type: "default", + }, + { + title: "Third Card", + body: "This is the third card in a three-column layout.", + image: img1, + imageAltText: "Third card image", + type: "default", + }, + { + title: "Fourth Card", + body: "This is the fourth card in a three-column layout.", + image: img1, + imageAltText: "Fourth card image", + type: "default", + }, + { + title: "Fifth Card", + body: "This is the fifth card in a three-column layout.", + image: img1, + imageAltText: "Fifth card image", + type: "default", + }, + { + title: "Sixth Card", + body: "This is the sixth card in a three-column layout.", + image: img1, + imageAltText: "Sixth card image", + type: "default", + }, + ], +}; diff --git a/packages/unity-react-core/src/components/Card/Card.test.jsx b/packages/unity-react-core/src/components/Card/Card.test.jsx index f9337c26f3..0c8056c454 100644 --- a/packages/unity-react-core/src/components/Card/Card.test.jsx +++ b/packages/unity-react-core/src/components/Card/Card.test.jsx @@ -1,7 +1,7 @@ // @ts-check import { render, cleanup } from "@testing-library/react"; import React from "react"; -import { expect, describe, it, afterEach, beforeEach, test } from "vitest"; +import { expect, describe, it, afterEach, beforeEach, test, vi } from "vitest"; import { Card } from "./Card"; @@ -100,4 +100,58 @@ describe("#Card options", () => { className ); }); + + describe("Multiple Cards", () => { + const multipleCardsArgs = { + columns: "2", + cards: [ + { + title: "First Card", + body: "First card body text", + image: "https://picsum.photos/300/200", + imageAltText: "First card image", + }, + { + title: "Second Card", + body: "Second card body text", + image: "https://picsum.photos/300/200", + imageAltText: "Second card image", + }, + ], + }; + + beforeEach(() => { + component = renderCard(multipleCardsArgs); + }); + + it("should render multiple cards container", () => { + const parentContainer = component.container.querySelector( + ".uds-card-arrangement" + ); + const cardContainer = component.container.querySelector( + ".uds-card-arrangement-card-container" + ); + vi.waitFor(() => { + expect(parentContainer).toBeInTheDocument(); + expect(cardContainer).toBeInTheDocument(); + }); + }); + + it("should render correct number of cards", () => { + const cards = component.container.querySelectorAll(".card"); + expect(cards).toHaveLength(2); + }); + + it("should apply auto-arrangement class", () => { + const container = component.container.querySelector(".auto-arrangement"); + expect(container).toBeInTheDocument(); + }); + + it("should apply four-columns class when columns is 4", () => { + const fourColumnArgs = { ...multipleCardsArgs, columns: "4" }; + component = renderCard(fourColumnArgs); + const container = component.container.querySelector(".four-columns"); + expect(container).toBeInTheDocument(); + }); + }); }); diff --git a/packages/unity-react-core/src/components/Image/Image.jsx b/packages/unity-react-core/src/components/Image/Image.jsx index 485094821c..5693f479ad 100644 --- a/packages/unity-react-core/src/components/Image/Image.jsx +++ b/packages/unity-react-core/src/components/Image/Image.jsx @@ -7,6 +7,7 @@ import React from "react"; /** * @typedef {import('../../core/types/image-types').ImageComponentProps} ImageComponentProps + * @typedef {import('../../core/types/image-types').ImageItemProps} ImageItemProps */ /** @@ -14,11 +15,11 @@ import React from "react"; */ /** - * @param {ImageComponentProps} props + * Base Image component for rendering individual images + * @param {ImageItemProps} props * @returns {JSX.Element} */ - -export const Image = ({ +const BaseImage = ({ src, alt, cssClasses, @@ -89,15 +90,80 @@ export const Image = ({ ); }; +/** + * @param {ImageComponentProps} props + * @returns {JSX.Element} + */ + +export const Image = ({ + src, + alt, + cssClasses, + loading = "lazy", + decoding = "async", + dataTestId, + fetchPriority = "auto", + width, + height, + cardLink, + title, + caption, + captionTitle, + border, + dropShadow, + images, + columns, +}) => { + // If images array is provided, render multiple images + if (images && Array.isArray(images) && images.length > 0) { + const containerClasses = classNames("uds-card-arrangement-card-container", { + "auto-arrangement": !columns || columns === "0", + "three-columns": columns === "3", + "four-columns": columns === "4", + }); + + return ( +
+
+ {images.map((imageItem, index) => ( + + ))} +
+
+ ); + } + + // Otherwise render single image (backward compatibility) + return ( + + ); +}; + Image.propTypes = { /** * Image source (We keep the same name as in the whole project) */ - src: PropTypes.string.isRequired, + src: PropTypes.string, /** * Image alt text */ - alt: PropTypes.string.isRequired, + alt: PropTypes.string, /** * Array classes for the image */ @@ -129,4 +195,30 @@ Image.propTypes = { captionTitle: PropTypes.string, border: PropTypes.bool, dropShadow: PropTypes.bool, + /** + * Array of image objects for multiple images display + */ + images: PropTypes.arrayOf( + PropTypes.shape({ + src: PropTypes.string.isRequired, + alt: PropTypes.string.isRequired, + cssClasses: PropTypes.arrayOf(PropTypes.string), + loading: PropTypes.oneOf(["lazy", "eager"]), + decoding: PropTypes.oneOf(["sync", "async", "auto"]), + fetchPriority: PropTypes.oneOf(["auto", "high", "low"]), + width: PropTypes.string, + height: PropTypes.string, + dataTestId: PropTypes.string, + cardLink: PropTypes.string, + title: PropTypes.string, + caption: PropTypes.string, + captionTitle: PropTypes.string, + border: PropTypes.bool, + dropShadow: PropTypes.bool, + }) + ), + /** + * Number of columns for multiple images display (0 for auto, 3 for three columns, 4 for four columns) + */ + columns: PropTypes.oneOf(["0", "3", "4"]), }; diff --git a/packages/unity-react-core/src/components/Image/Image.stories.jsx b/packages/unity-react-core/src/components/Image/Image.stories.jsx index c163e3d573..f3a4b41007 100644 --- a/packages/unity-react-core/src/components/Image/Image.stories.jsx +++ b/packages/unity-react-core/src/components/Image/Image.stories.jsx @@ -3,10 +3,7 @@ import { imageAny } from "@asu/shared"; import React from "react"; -const img1 = imageAny(); // Placeholder for an example image -const img2 = imageAny(); // Placeholder for an example image -const img3 = imageAny(); // Placeholder for an example image -const img4 = imageAny(); // Placeholder for an example image +const img1 = imageAny(); import { Image } from "./Image"; @@ -41,7 +38,7 @@ ImageWithNoCaptionBorderless.args = { export const ImageWithCaption = Template.bind({}); ImageWithCaption.args = { - src: img2, + src: img1, alt: "Placeholder image", caption: "This is a caption.", captionTitle: "Caption title", @@ -50,7 +47,7 @@ ImageWithCaption.args = { export const ImageWithCaptionAndDropshadow = Template.bind({}); ImageWithCaptionAndDropshadow.args = { - src: img3, + src: img1, alt: "Placeholder image", caption: "This is a caption.", captionTitle: "Caption title", @@ -78,5 +75,83 @@ export const GridImages = GridTemplate.bind({}); GridImages.args = { alt: "Placeholder image", width: "100%", - src: img4, + src: img1, +}; + +const MultipleImagesTemplate = args => ; + +export const TwoImagesAutoArrangement = MultipleImagesTemplate.bind({}); +TwoImagesAutoArrangement.args = { + images: [ + { + src: img1, + alt: "First image", + border: true, + caption: "Caption for first image", + captionTitle: "First Image Title", + }, + { + src: img1, + alt: "Second image", + border: true, + caption: "Caption for second image", + captionTitle: "Second Image Title", + dropShadow: true, + }, + ], + columns: "0", +}; + +export const ThreeImagesArrangement = MultipleImagesTemplate.bind({}); +ThreeImagesArrangement.args = { + images: [ + { + src: img1, + alt: "First image", + border: true, + caption: "Caption for first image", + }, + { + src: img1, + alt: "Second image", + border: true, + caption: "Caption for second image", + dropShadow: true, + }, + { + src: img1, + alt: "Third image", + border: true, + caption: "Caption for third image", + }, + ], + columns: "3", +}; + +export const FourImagesArrangement = MultipleImagesTemplate.bind({}); +FourImagesArrangement.args = { + images: [ + { + src: img1, + alt: "First image", + border: true, + }, + { + src: img1, + alt: "Second image", + border: true, + dropShadow: true, + }, + { + src: img1, + alt: "Third image", + border: true, + }, + { + src: img1, + alt: "Fourth image", + border: true, + }, + ], + columns: "4", }; diff --git a/packages/unity-react-core/src/components/Image/Image.test.jsx b/packages/unity-react-core/src/components/Image/Image.test.jsx index ed5a146724..94a724d9b4 100644 --- a/packages/unity-react-core/src/components/Image/Image.test.jsx +++ b/packages/unity-react-core/src/components/Image/Image.test.jsx @@ -6,6 +6,7 @@ import { expect, describe, it, afterEach, beforeEach } from "vitest"; // @ts-ignore import { Image } from "./Image"; const img = imageAny(); +const img2 = imageAny(); const renderImage = props => { return render(); @@ -26,4 +27,58 @@ describe("#Image", () => { it("should define component", () => { expect(component).toBeDefined(); }); + + describe("Multiple Images", () => { + it("should render multiple images with auto arrangement", () => { + const { container } = renderImage({ + images: [ + { src: img, alt: "First image" }, + { src: img2, alt: "Second image" }, + ], + columns: "0", + }); + + expect(container.querySelector(".uds-card-arrangement")).toBeTruthy(); + expect(container.querySelector(".auto-arrangement")).toBeTruthy(); + expect(container.querySelectorAll("img")).toHaveLength(2); + }); + + it("should render multiple images with three columns", () => { + const { container } = renderImage({ + images: [ + { src: img, alt: "First image" }, + { src: img2, alt: "Second image" }, + ], + columns: "3", + }); + + expect(container.querySelector(".uds-card-arrangement")).toBeTruthy(); + expect(container.querySelector(".three-columns")).toBeTruthy(); + expect(container.querySelectorAll("img")).toHaveLength(2); + }); + + it("should render multiple images with four columns", () => { + const { container } = renderImage({ + images: [ + { src: img, alt: "First image" }, + { src: img2, alt: "Second image" }, + ], + columns: "4", + }); + + expect(container.querySelector(".uds-card-arrangement")).toBeTruthy(); + expect(container.querySelector(".four-columns")).toBeTruthy(); + expect(container.querySelectorAll("img")).toHaveLength(2); + }); + + it("should render single image when images prop is not provided", () => { + const { container } = renderImage({ + src: img, + alt: "Single image", + }); + + expect(container.querySelector(".uds-card-arrangement")).toBeFalsy(); + expect(container.querySelectorAll("img")).toHaveLength(1); + }); + }); }); diff --git a/packages/unity-react-core/src/components/RankingCard/RankingCard.jsx b/packages/unity-react-core/src/components/RankingCard/RankingCard.jsx index 15e8d007a4..64e8ef23e6 100644 --- a/packages/unity-react-core/src/components/RankingCard/RankingCard.jsx +++ b/packages/unity-react-core/src/components/RankingCard/RankingCard.jsx @@ -7,6 +7,10 @@ import { GaEventWrapper } from "../GaEventWrapper/GaEventWrapper"; import { useBaseSpecificFramework } from "../GaEventWrapper/useBaseSpecificFramework"; import { Image } from "../Image/Image"; +/** + * @typedef {import('../../core/types/ranking-card-types').RankingCardProps} RankingCardProps + */ + const gaDefaultObject = { name: "onclick", event: "link", @@ -119,7 +123,7 @@ InfoLayerWrapper.propTypes = { readMoreLink: PropTypes.string, }; -export const RankingCard = ({ +const BaseRankingCard = ({ imageSize = "large", image, imageAlt, @@ -161,27 +165,106 @@ export const RankingCard = ({ ); }; +BaseRankingCard.propTypes = { + imageSize: PropTypes.oneOf(["small", "large"]), + image: PropTypes.string.isRequired, + imageAlt: PropTypes.string.isRequired, + heading: PropTypes.string.isRequired, + body: PropTypes.string.isRequired, + readMoreLink: PropTypes.string, + citation: PropTypes.string, +}; + +/** + * @param {RankingCardProps} props + * @returns {JSX.Element} + */ +export const RankingCard = ({ + imageSize = "large", + image, + imageAlt, + heading, + body, + readMoreLink = "", + citation, + cards = [], + columns = "0", +}) => { + // If multiple cards are provided, render them in a card container + if (cards.length > 1) { + const getColumnClass = () => { + switch (columns) { + case "2": + return ""; + case "3": + return "three-columns"; + case "4": + return "four-columns"; + default: + return ""; + } + }; + + return ( +
+
+ {cards.map((card, index) => ( + + ))} +
+
+ ); + } + + return ( + + ); +}; + RankingCard.propTypes = { /** * Size of ranking card */ - imageSize: PropTypes.oneOf(["small", "large"]).isRequired, + imageSize: PropTypes.oneOf(["small", "large"]), /** * Ranking card image */ - image: PropTypes.string.isRequired, + image: PropTypes.string, /** * Card header image alt text */ - imageAlt: PropTypes.string.isRequired, + imageAlt: PropTypes.string, /** * Ranking card heading */ - heading: PropTypes.string.isRequired, + heading: PropTypes.string, /** * Ranking card body content */ - body: PropTypes.string.isRequired, + body: PropTypes.string, /** * Link for read more */ @@ -190,4 +273,22 @@ RankingCard.propTypes = { * Ranking card citation content (Required for small size only) */ citation: PropTypes.string, + /** + * Array of ranking card objects for rendering multiple cards + */ + cards: PropTypes.arrayOf( + PropTypes.shape({ + imageSize: PropTypes.oneOf(["small", "large"]), + image: PropTypes.string.isRequired, + imageAlt: PropTypes.string.isRequired, + heading: PropTypes.string.isRequired, + body: PropTypes.string.isRequired, + readMoreLink: PropTypes.string, + citation: PropTypes.string, + }) + ), + /** + * Number of columns for multiple cards layout (0, 2, 3, or 4) + */ + columns: PropTypes.oneOf(["0", "2", "3", "4"]), }; diff --git a/packages/unity-react-core/src/components/RankingCard/RankingCard.stories.jsx b/packages/unity-react-core/src/components/RankingCard/RankingCard.stories.jsx index a3bf3445eb..e09571b6b3 100644 --- a/packages/unity-react-core/src/components/RankingCard/RankingCard.stories.jsx +++ b/packages/unity-react-core/src/components/RankingCard/RankingCard.stories.jsx @@ -28,7 +28,7 @@ Large.args = { image: img, imageAlt: "Image alt text", heading: "Ranking title goes here, under the photo", - body: "ASU has topped U.S. News & World Report’s “Most Innovative Schools list since the inception of the category in 2016. ASU again placed ahead of Stanford and MIT on the list, based on a survey of peers. College presidents, provosts and admissions deans around the country nominated up to 10 colleges or universities that are making the most innovative improvements.", + body: 'ASU has topped U.S. News & World Report\'s "Most Innovative Schools" list since the inception of the category in 2016. ASU again placed ahead of Stanford and MIT on the list, based on a survey of peers. College presidents, provosts and admissions deans around the country nominated up to 10 colleges or universities that are making the most innovative improvements.', readMoreLink: "https://www.asu.edu/", }; @@ -38,8 +38,221 @@ Small.args = { image: img, imageAlt: "Image alt text", heading: "Ranking title goes here, under the photo", - body: "ASU has topped U.S. News & World Report’s “Most Innovative Schools list since the inception of the category in 2016. ASU again placed ahead of Stanford and MIT on the list, based on a survey of peers. College presidents, provosts and admissions deans around the country nominated up to 10 colleges or universities that are making the most innovative improvements.", + body: 'ASU has topped U.S. News & World Report\'s "Most Innovative Schools" list since the inception of the category in 2016. ASU again placed ahead of Stanford and MIT on the list, based on a survey of peers. College presidents, provosts and admissions deans around the country nominated up to 10 colleges or universities that are making the most innovative improvements.', readMoreLink: "https://www.asu.edu/", citation: "Citation of the ranking should go under the headline, regular body style text", }; + +const MultipleRankingCardsTemplate = args => ( +
+
+
+ +
+
+
+); + +export const MultipleRankingCardsTwoColumns = MultipleRankingCardsTemplate.bind( + {} +); +MultipleRankingCardsTwoColumns.args = { + columns: "2", + cards: [ + { + imageSize: "large", + image: img, + imageAlt: "First ranking image", + heading: "Most Innovative University", + body: "ASU has topped U.S. News & World Report's Most Innovative Schools list since the inception of the category in 2016.", + readMoreLink: "https://www.asu.edu/", + }, + { + imageSize: "large", + image: img, + imageAlt: "Second ranking image", + heading: "Top Online Programs", + body: "ASU Online is ranked #1 for innovation and #2 for best online bachelor's programs by U.S. News & World Report.", + readMoreLink: "https://www.asu.edu/", + }, + { + imageSize: "small", + image: img, + imageAlt: "Third ranking image", + heading: "Research Excellence", + body: "ASU is classified as a Research 1 university by the Carnegie Classification of Institutions of Higher Education.", + readMoreLink: "https://www.asu.edu/", + citation: "Carnegie Classification 2021", + }, + { + imageSize: "small", + image: img, + imageAlt: "Fourth ranking image", + heading: "Sustainability Leader", + body: "ASU is recognized as a leader in sustainability and climate action by multiple organizations.", + readMoreLink: "https://www.asu.edu/", + citation: "Times Higher Education Impact Rankings 2023", + }, + ], +}; + +export const MultipleRankingCardsFourColumns = + MultipleRankingCardsTemplate.bind({}); +MultipleRankingCardsFourColumns.args = { + columns: "4", + cards: [ + { + imageSize: "large", + image: img, + imageAlt: "First ranking image", + heading: "Most Innovative", + body: "ASU has topped U.S. News & World Report's Most Innovative Schools list since 2016.", + readMoreLink: "https://www.asu.edu/", + }, + { + imageSize: "large", + image: img, + imageAlt: "Second ranking image", + heading: "Online Excellence", + body: "ASU Online is ranked #1 for innovation in online education programs.", + readMoreLink: "https://www.asu.edu/", + }, + { + imageSize: "large", + image: img, + imageAlt: "Third ranking image", + heading: "Research Impact", + body: "ASU is a Carnegie R1 research university with significant research impact.", + readMoreLink: "https://www.asu.edu/", + }, + { + imageSize: "large", + image: img, + imageAlt: "Fourth ranking image", + heading: "Sustainability", + body: "ASU leads in sustainability and climate action initiatives globally.", + readMoreLink: "https://www.asu.edu/", + }, + { + imageSize: "large", + image: img, + imageAlt: "First ranking image", + heading: "Most Innovative", + body: "ASU has topped U.S. News & World Report's Most Innovative Schools list since 2016.", + readMoreLink: "https://www.asu.edu/", + }, + { + imageSize: "large", + image: img, + imageAlt: "Second ranking image", + heading: "Online Excellence", + body: "ASU Online is ranked #1 for innovation in online education programs.", + readMoreLink: "https://www.asu.edu/", + }, + { + imageSize: "large", + image: img, + imageAlt: "Third ranking image", + heading: "Research Impact", + body: "ASU is a Carnegie R1 research university with significant research impact.", + readMoreLink: "https://www.asu.edu/", + }, + { + imageSize: "large", + image: img, + imageAlt: "Fourth ranking image", + heading: "Sustainability", + body: "ASU leads in sustainability and climate action initiatives globally.", + readMoreLink: "https://www.asu.edu/", + }, + { + imageSize: "large", + image: img, + imageAlt: "First ranking image", + heading: "Most Innovative", + body: "ASU has topped U.S. News & World Report's Most Innovative Schools list since 2016.", + readMoreLink: "https://www.asu.edu/", + }, + { + imageSize: "large", + image: img, + imageAlt: "Second ranking image", + heading: "Online Excellence", + body: "ASU Online is ranked #1 for innovation in online education programs.", + readMoreLink: "https://www.asu.edu/", + }, + { + imageSize: "large", + image: img, + imageAlt: "Third ranking image", + heading: "Research Impact", + body: "ASU is a Carnegie R1 research university with significant research impact.", + readMoreLink: "https://www.asu.edu/", + }, + { + imageSize: "large", + image: img, + imageAlt: "Fourth ranking image", + heading: "Sustainability", + body: "ASU leads in sustainability and climate action initiatives globally.", + readMoreLink: "https://www.asu.edu/", + }, + ], +}; + +export const MultipleRankingCardsThreeColumns = + MultipleRankingCardsTemplate.bind({}); +MultipleRankingCardsThreeColumns.args = { + columns: "3", + cards: [ + { + imageSize: "large", + image: img, + imageAlt: "First ranking image", + heading: "Most Innovative", + body: "ASU has topped U.S. News & World Report's Most Innovative Schools list since 2016.", + readMoreLink: "https://www.asu.edu/", + }, + { + imageSize: "large", + image: img, + imageAlt: "Second ranking image", + heading: "Online Excellence", + body: "ASU Online is ranked #1 for innovation in online education programs.", + readMoreLink: "https://www.asu.edu/", + }, + { + imageSize: "large", + image: img, + imageAlt: "Third ranking image", + heading: "Research Impact", + body: "ASU is a Carnegie R1 research university with significant research impact.", + readMoreLink: "https://www.asu.edu/", + }, + { + imageSize: "large", + image: img, + imageAlt: "Fourth ranking image", + heading: "Sustainability", + body: "ASU leads in sustainability and climate action initiatives globally.", + readMoreLink: "https://www.asu.edu/", + }, + { + imageSize: "large", + image: img, + imageAlt: "Fifth ranking image", + heading: "Student Success", + body: "ASU excels in student outcomes and graduation rates across all programs.", + readMoreLink: "https://www.asu.edu/", + }, + { + imageSize: "large", + image: img, + imageAlt: "Sixth ranking image", + heading: "Global Reach", + body: "ASU's global impact and international partnerships span all continents.", + readMoreLink: "https://www.asu.edu/", + }, + ], +}; diff --git a/packages/unity-react-core/src/components/RankingCard/RankingCard.test.jsx b/packages/unity-react-core/src/components/RankingCard/RankingCard.test.jsx index 25dcd3917e..46d71cf52c 100644 --- a/packages/unity-react-core/src/components/RankingCard/RankingCard.test.jsx +++ b/packages/unity-react-core/src/components/RankingCard/RankingCard.test.jsx @@ -73,3 +73,64 @@ describe("RankingCard small layout", () => { expect(infoLayer).toHaveClass("show"); }); }); + +describe("Multiple RankingCards", () => { + const multipleRankingCardsArgs = { + columns: "2", + cards: [ + { + imageSize: "large", + image: img, + imageAlt: "First ranking image", + heading: "Most Innovative University", + body: "ASU has topped U.S. News & World Report's Most Innovative Schools list since 2016.", + readMoreLink: "https://www.asu.edu/", + }, + { + imageSize: "small", + image: img, + imageAlt: "Second ranking image", + heading: "Top Online Programs", + body: "ASU Online is ranked #1 for innovation.", + readMoreLink: "https://www.asu.edu/", + citation: "U.S. News & World Report 2023", + }, + ], + }; + + let component; + + beforeEach(() => { + component = renderRankingCard(multipleRankingCardsArgs); + }); + + afterEach(cleanup); + + it("should render multiple ranking cards container", () => { + const parentContainer = component.container.querySelector( + ".uds-card-arrangement" + ); + const cardContainer = component.container.querySelector( + ".uds-card-arrangement-card-container" + ); + expect(parentContainer).toBeInTheDocument(); + expect(cardContainer).toBeInTheDocument(); + }); + + it("should render correct number of ranking cards", () => { + const cards = component.container.querySelectorAll(".card-ranking"); + expect(cards).toHaveLength(2); + }); + + it("should apply auto-arrangement class", () => { + const container = component.container.querySelector(".auto-arrangement"); + expect(container).toBeInTheDocument(); + }); + + it("should apply four-columns class when columns is 4", () => { + const fourColumnArgs = { ...multipleRankingCardsArgs, columns: "4" }; + component = renderRankingCard(fourColumnArgs); + const container = component.container.querySelector(".four-columns"); + expect(container).toBeInTheDocument(); + }); +}); diff --git a/packages/unity-react-core/src/core/types/card-types.js b/packages/unity-react-core/src/core/types/card-types.js index 346b4caa22..81449dcc38 100644 --- a/packages/unity-react-core/src/core/types/card-types.js +++ b/packages/unity-react-core/src/core/types/card-types.js @@ -11,6 +11,30 @@ * @property {boolean} [horizontal] * @property {string} [image] * @property {string} [imageAltText] + * @property {string} [title] + * @property {string[]} [icon] + * @property {string} [body] + * @property {string} [eventLocation] + * @property {string} [eventTime] + * @property {string} [linkLabel] + * @property {string} [linkUrl] + * @property {ButtonProps[]} [buttons] + * @property {"stack"|"inline"} [eventFormat] + * @property {"25%"|"50%"|"75%"|"100%"} [width] + * @property {TagsProps[]} [tags] + * @property {boolean} [showBorders] + * @property {string} [cardLink] + * @property {CardItemProps[]} [cards] + * @property {"0"|"2"|"3"|"4"} [columns] + * @property {"vertical" | "auto" } [layout] + */ + +/** + * @typedef {Object} CardItemProps + * @property {string} [type] + * @property {boolean} [horizontal] + * @property {string} [image] + * @property {string} [imageAltText] * @property {string} title * @property {string[]} [icon] * @property {string} [body] diff --git a/packages/unity-react-core/src/core/types/image-types.js b/packages/unity-react-core/src/core/types/image-types.js index d46ecf0f5f..63f17269bb 100644 --- a/packages/unity-react-core/src/core/types/image-types.js +++ b/packages/unity-react-core/src/core/types/image-types.js @@ -1,7 +1,7 @@ // @ts-check /** - * @typedef {Object} ImageComponentProps + * @typedef {Object} ImageItemProps * @property {string} src * @property {string} alt * @property {Array.} [cssClasses] @@ -19,6 +19,27 @@ * @property {boolean} [dropShadow] */ +/** + * @typedef {Object} ImageComponentProps + * @property {string} [src] + * @property {string} [alt] + * @property {Array.} [cssClasses] + * @property {"lazy"|"eager"} [loading] + * @property {"sync"|"async"|"auto"} [decoding] + * @property {"auto"|"high"|"low"} [fetchPriority] + * @property {string} [width] + * @property {string} [height] + * @property {string} [dataTestId] + * @property {string} [cardLink] + * @property {string} [title] + * @property {string} [caption] + * @property {string} [captionTitle] + * @property {boolean} [border] + * @property {boolean} [dropShadow] + * @property {Array.} [images] - Array of image objects for multiple images display + * @property {"0"|"3"|"4"} [columns] - Number of columns for multiple images display (0 for auto, 3 for three columns, 4 for four columns) + */ + /** * This help VSCODE and JSOC to recognize the syntax * `import(FILE_PATH).EXPORTED_THING` diff --git a/packages/unity-react-core/src/core/types/ranking-card-types.js b/packages/unity-react-core/src/core/types/ranking-card-types.js new file mode 100644 index 0000000000..14037c5974 --- /dev/null +++ b/packages/unity-react-core/src/core/types/ranking-card-types.js @@ -0,0 +1,32 @@ +// @ts-check + +/** + * @typedef {Object} RankingCardProps + * @property {"small"|"large"} [imageSize] + * @property {string} [image] + * @property {string} [imageAlt] + * @property {string} [heading] + * @property {string} [body] + * @property {string} [readMoreLink] + * @property {string} [citation] + * @property {RankingCardItemProps[]} [cards] + * @property {"0"|"2"|"3"|"4"} [columns] + */ + +/** + * @typedef {Object} RankingCardItemProps + * @property {"small"|"large"} [imageSize] + * @property {string} image + * @property {string} imageAlt + * @property {string} heading + * @property {string} body + * @property {string} [readMoreLink] + * @property {string} [citation] + */ + +/** + * This help VSCODE and JSOC to recognize the syntax + * `import(FILE_PATH).EXPORTED_THING` + * @ignore + */ +export const JSDOC = "jsdoc";