From a514e932a2ac454686e86b0f7813bae4f7438c01 Mon Sep 17 00:00:00 2001 From: david ornelas Date: Thu, 4 Sep 2025 13:43:09 -0700 Subject: [PATCH 1/7] feat(unity-react-core): add multiple cards rendering option --- .../src/components/Card/Card.jsx | 95 +++++++- .../src/components/Card/Card.stories.jsx | 129 +++++++++++ .../src/components/Card/Card.test.jsx | 48 ++++ .../src/components/Image/Image.jsx | 100 +++++++- .../src/components/Image/Image.stories.jsx | 78 +++++++ .../src/components/Image/Image.test.jsx | 55 +++++ .../components/RankingCard/RankingCard.jsx | 107 ++++++++- .../RankingCard/RankingCard.stories.jsx | 213 +++++++++++++++++- .../RankingCard/RankingCard.test.jsx | 57 +++++ .../src/core/types/card-types.js | 23 ++ .../src/core/types/image-types.js | 23 +- .../src/core/types/ranking-card-types.js | 32 +++ 12 files changed, 945 insertions(+), 15 deletions(-) create mode 100644 packages/unity-react-core/src/core/types/ranking-card-types.js diff --git a/packages/unity-react-core/src/components/Card/Card.jsx b/packages/unity-react-core/src/components/Card/Card.jsx index 990516f3fe..5e87e0208e 100644 --- a/packages/unity-react-core/src/components/Card/Card.jsx +++ b/packages/unity-react-core/src/components/Card/Card.jsx @@ -46,7 +46,53 @@ export const Card = ({ tags, showBorders = true, cardLink, + 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) => ( + + ))} +
+ ); + } + + // 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..873bedcc19 100644 --- a/packages/unity-react-core/src/components/Card/Card.test.jsx +++ b/packages/unity-react-core/src/components/Card/Card.test.jsx @@ -100,4 +100,52 @@ 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"); + 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..ba055a8154 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,28 @@ 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..54f008fc91 100644 --- a/packages/unity-react-core/src/components/Image/Image.stories.jsx +++ b/packages/unity-react-core/src/components/Image/Image.stories.jsx @@ -80,3 +80,81 @@ GridImages.args = { width: "100%", src: img4, }; + +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: img2, + 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: img2, + alt: "Second image", + border: true, + caption: "Caption for second image", + dropShadow: true, + }, + { + src: img3, + 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: img2, + alt: "Second image", + border: true, + dropShadow: true, + }, + { + src: img3, + alt: "Third image", + border: true, + }, + { + src: img4, + 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..32eb64e940 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,100 @@ 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 +267,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..394ebc752d 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,217 @@ 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..fdb7a02dd1 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,60 @@ 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..97a69c0c48 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,29 @@ * @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] + */ + +/** + * @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"; From a0d63e6d084adaaa0afc6268d252e50ff1d1c096 Mon Sep 17 00:00:00 2001 From: david ornelas Date: Fri, 5 Sep 2025 14:07:52 -0700 Subject: [PATCH 2/7] chore(unity-react-core): fix multiple card tests --- .../src/components/Card/Card.jsx | 58 +++++++++++-------- .../src/components/Card/Card.test.jsx | 16 +++-- .../src/components/Image/Image.jsx | 36 ++++++------ .../src/components/Image/Image.stories.jsx | 2 +- .../components/RankingCard/RankingCard.jsx | 8 ++- .../RankingCard/RankingCard.stories.jsx | 14 +++-- .../RankingCard/RankingCard.test.jsx | 8 ++- 7 files changed, 87 insertions(+), 55 deletions(-) diff --git a/packages/unity-react-core/src/components/Card/Card.jsx b/packages/unity-react-core/src/components/Card/Card.jsx index 5e87e0208e..2e7f5fdd99 100644 --- a/packages/unity-react-core/src/components/Card/Card.jsx +++ b/packages/unity-react-core/src/components/Card/Card.jsx @@ -65,30 +65,40 @@ export const Card = ({ }; return ( -
- {cards.map((card, index) => ( - - ))} -
+
+ {cards.map((card, index) => ( + + ))} +
); } 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 873bedcc19..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"; @@ -125,10 +125,16 @@ describe("#Card options", () => { }); it("should render multiple 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(); + 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", () => { diff --git a/packages/unity-react-core/src/components/Image/Image.jsx b/packages/unity-react-core/src/components/Image/Image.jsx index ba055a8154..5693f479ad 100644 --- a/packages/unity-react-core/src/components/Image/Image.jsx +++ b/packages/unity-react-core/src/components/Image/Image.jsx @@ -198,23 +198,25 @@ Image.propTypes = { /** * 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, - })), + 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) */ 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 54f008fc91..1c1f90fcb1 100644 --- a/packages/unity-react-core/src/components/Image/Image.stories.jsx +++ b/packages/unity-react-core/src/components/Image/Image.stories.jsx @@ -95,7 +95,7 @@ TwoImagesAutoArrangement.args = { }, { src: img2, - alt: "Second image", + alt: "Second image", border: true, caption: "Caption for second image", captionTitle: "Second Image Title", diff --git a/packages/unity-react-core/src/components/RankingCard/RankingCard.jsx b/packages/unity-react-core/src/components/RankingCard/RankingCard.jsx index 32eb64e940..64e8ef23e6 100644 --- a/packages/unity-react-core/src/components/RankingCard/RankingCard.jsx +++ b/packages/unity-react-core/src/components/RankingCard/RankingCard.jsx @@ -207,7 +207,13 @@ export const RankingCard = ({ return (
-
+
{cards.map((card, index) => ( (
); -export const MultipleRankingCardsTwoColumns = MultipleRankingCardsTemplate.bind({}); +export const MultipleRankingCardsTwoColumns = MultipleRankingCardsTemplate.bind( + {} +); MultipleRankingCardsTwoColumns.args = { columns: "2", cards: [ @@ -95,7 +97,8 @@ MultipleRankingCardsTwoColumns.args = { ], }; -export const MultipleRankingCardsFourColumns = MultipleRankingCardsTemplate.bind({}); +export const MultipleRankingCardsFourColumns = + MultipleRankingCardsTemplate.bind({}); MultipleRankingCardsFourColumns.args = { columns: "4", cards: [ @@ -198,7 +201,8 @@ MultipleRankingCardsFourColumns.args = { ], }; -export const MultipleRankingCardsThreeColumns = MultipleRankingCardsTemplate.bind({}); +export const MultipleRankingCardsThreeColumns = + MultipleRankingCardsTemplate.bind({}); MultipleRankingCardsThreeColumns.args = { columns: "3", cards: [ 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 fdb7a02dd1..46d71cf52c 100644 --- a/packages/unity-react-core/src/components/RankingCard/RankingCard.test.jsx +++ b/packages/unity-react-core/src/components/RankingCard/RankingCard.test.jsx @@ -107,8 +107,12 @@ describe("Multiple RankingCards", () => { 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"); + const parentContainer = component.container.querySelector( + ".uds-card-arrangement" + ); + const cardContainer = component.container.querySelector( + ".uds-card-arrangement-card-container" + ); expect(parentContainer).toBeInTheDocument(); expect(cardContainer).toBeInTheDocument(); }); From 5853528306c9132eda361f53aed47e3370fc78db Mon Sep 17 00:00:00 2001 From: david ornelas Date: Fri, 12 Sep 2025 10:22:41 -0700 Subject: [PATCH 3/7] fix(unity-react-core): update card arrangement render classes --- packages/unity-react-core/src/components/Card/Card.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/unity-react-core/src/components/Card/Card.jsx b/packages/unity-react-core/src/components/Card/Card.jsx index 2e7f5fdd99..98d17ed85f 100644 --- a/packages/unity-react-core/src/components/Card/Card.jsx +++ b/packages/unity-react-core/src/components/Card/Card.jsx @@ -65,6 +65,7 @@ export const Card = ({ }; return ( +
))}
+
); } From 555aca0e7605e14bc6edb0fe6227b0628f7ed36d Mon Sep 17 00:00:00 2001 From: david ornelas Date: Mon, 15 Sep 2025 12:05:59 -0700 Subject: [PATCH 4/7] chore(unity-react-core): update card stories for grid examples --- .../src/components/Card/Card.stories.jsx | 2 +- .../src/components/Image/Image.stories.jsx | 23 ++++++++----------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/unity-react-core/src/components/Card/Card.stories.jsx b/packages/unity-react-core/src/components/Card/Card.stories.jsx index 58370b9d7f..72d2e64b62 100644 --- a/packages/unity-react-core/src/components/Card/Card.stories.jsx +++ b/packages/unity-react-core/src/components/Card/Card.stories.jsx @@ -1,7 +1,7 @@ import { imageAny } from "@asu/shared"; import classNames from "classnames"; import React from "react"; -const img1 = imageAny(); // Placeholder for an example image +const img1 = imageAny(); // @ts-ignore import { Card } from "./Card"; 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 1c1f90fcb1..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,7 +75,7 @@ export const GridImages = GridTemplate.bind({}); GridImages.args = { alt: "Placeholder image", width: "100%", - src: img4, + src: img1, }; const MultipleImagesTemplate = args => ; @@ -94,7 +91,7 @@ TwoImagesAutoArrangement.args = { captionTitle: "First Image Title", }, { - src: img2, + src: img1, alt: "Second image", border: true, caption: "Caption for second image", @@ -115,14 +112,14 @@ ThreeImagesArrangement.args = { caption: "Caption for first image", }, { - src: img2, + src: img1, alt: "Second image", border: true, caption: "Caption for second image", dropShadow: true, }, { - src: img3, + src: img1, alt: "Third image", border: true, caption: "Caption for third image", @@ -140,18 +137,18 @@ FourImagesArrangement.args = { border: true, }, { - src: img2, + src: img1, alt: "Second image", border: true, dropShadow: true, }, { - src: img3, + src: img1, alt: "Third image", border: true, }, { - src: img4, + src: img1, alt: "Fourth image", border: true, }, From ca052d2f513a0a4e345cb43c77b42a3f102bfd0b Mon Sep 17 00:00:00 2001 From: david ornelas Date: Mon, 15 Sep 2025 12:06:34 -0700 Subject: [PATCH 5/7] fix(unity-bootstrap-theme): fix card drop shadow in multiple cards arrangement --- packages/unity-bootstrap-theme/src/scss/extends/_images.scss | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/unity-bootstrap-theme/src/scss/extends/_images.scss b/packages/unity-bootstrap-theme/src/scss/extends/_images.scss index 337f2eea61..3536acadf6 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,5 @@ &.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; - } } } From 60ca49f82bf7356d0c60f0e372b86207e8b3b888 Mon Sep 17 00:00:00 2001 From: david ornelas Date: Thu, 18 Sep 2025 13:54:03 -0700 Subject: [PATCH 6/7] fix(unity-react-core): update vertical layout options --- .../unity-react-core/src/components/Card/Card.jsx | 12 ++++++++++-- .../unity-react-core/src/core/types/card-types.js | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/unity-react-core/src/components/Card/Card.jsx b/packages/unity-react-core/src/components/Card/Card.jsx index 98d17ed85f..6e5a7812ea 100644 --- a/packages/unity-react-core/src/components/Card/Card.jsx +++ b/packages/unity-react-core/src/components/Card/Card.jsx @@ -48,6 +48,7 @@ export const Card = ({ cardLink, cards = [], columns = "0", + layout = "auto" }) => { // If multiple cards are provided, render them in a card container if (cards.length > 1) { @@ -65,11 +66,14 @@ export const Card = ({ }; return ( -
+
@@ -257,6 +261,10 @@ Card.propTypes = { * Number of columns for multiple cards layout (0, 2, 3, or 4) */ columns: PropTypes.oneOf(["0", "2", "3", "4"]), + /** + * Vertical or normal layout + */ + layout: PropTypes.oneOf(["vertical", "auto"]) }; /* 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 97a69c0c48..81449dcc38 100644 --- a/packages/unity-react-core/src/core/types/card-types.js +++ b/packages/unity-react-core/src/core/types/card-types.js @@ -26,6 +26,7 @@ * @property {string} [cardLink] * @property {CardItemProps[]} [cards] * @property {"0"|"2"|"3"|"4"} [columns] + * @property {"vertical" | "auto" } [layout] */ /** From 778091a086c32d3bb3276868743f41b08c6b963a Mon Sep 17 00:00:00 2001 From: david ornelas Date: Thu, 2 Oct 2025 15:34:21 -0700 Subject: [PATCH 7/7] fix(unity-bootstrap-theme): update arrangement styles for image component The aspect ratio of 4:3 is enforced when the .uds-card-arrangement is wrapped around an image component. No changes to previous behavior --- .../unity-bootstrap-theme/src/scss/extends/_images.scss | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/unity-bootstrap-theme/src/scss/extends/_images.scss b/packages/unity-bootstrap-theme/src/scss/extends/_images.scss index 3536acadf6..b600e61d51 100644 --- a/packages/unity-bootstrap-theme/src/scss/extends/_images.scss +++ b/packages/unity-bootstrap-theme/src/scss/extends/_images.scss @@ -63,3 +63,12 @@ rgba(25, 25, 25, 0.2); } } + +.uds-card-arrangement { + .uds-img { + img { + aspect-ratio: 4/3; + object-fit: cover; + } + } +}