From a072968b2398c68a88564f2409aea2757dda526e Mon Sep 17 00:00:00 2001 From: Michael Webber Date: Wed, 5 Aug 2026 10:24:06 -0700 Subject: [PATCH 01/42] feat(unity-react-core): modal auto focus and disabled tab to content outside the modal --- .../unity-bootstrap-theme/src/js/modals.js | 89 ++++++++++++- .../ButtonIconOnly/ButtonIconOnly.jsx | 2 + .../src/components/Modal/Modal.tsx | 119 ++++++++++++++---- .../src/core/types/shared-types.js | 1 + 4 files changed, 181 insertions(+), 30 deletions(-) diff --git a/packages/unity-bootstrap-theme/src/js/modals.js b/packages/unity-bootstrap-theme/src/js/modals.js index 622eb6d204..304506d597 100644 --- a/packages/unity-bootstrap-theme/src/js/modals.js +++ b/packages/unity-bootstrap-theme/src/js/modals.js @@ -1,22 +1,101 @@ import { EventHandler } from "./bootstrap-helper"; +function openModal() { + document.getElementById("uds-modal")?.classList.add("open"); + let closeModalButton = document.getElementById("closeModalButton"); + setTimeout(() => { + if (closeModalButton) { + // Wait for dom to update before setting focus + closeModalButton?.focus(); + } + }, 200); + + // const mainContent = document.getElementById("main-content"); + // mainContent.setAttribute("inert", ""); + // const mainContentChildren = mainContent.children; + // let mainContentChildrenArray = Array.from(mainContentChildren); + // for (let i = 0; i < mainContentChildrenArray.length; i++) { + // mainContentChildrenArray[i].setAttribute("inert", ""); + // } + + // Disable navigation to everything accept for the modal content + // Source: https://stackoverflow.com/questions/4195616/how-to-set-the-focus-on-a-javascript-modal-window + const focusableElements = + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; + const modal = document.getElementsByClassName("uds-modal-container")[0]; + const firstFocusableElement = modal?.querySelectorAll(focusableElements)[0]; + const focusableContent = modal?.querySelectorAll(focusableElements); + const lastFocusableElement = focusableContent + ? focusableContent[focusableContent?.length - 1] + : undefined; + + const handleTabKey = e => { + let isTabPressed = e.key === "Tab"; // || e.keyCode === 9; + + if (!isTabPressed) { + return; + } + + if (e.shiftKey) { + // if shift key pressed for shift + tab combination + if (document.activeElement === firstFocusableElement) { + lastFocusableElement?.focus(); // add focus for the last focusable element + e.preventDefault(); + } + } else { + // if tab key is pressed + if (document.activeElement === lastFocusableElement) { + // if focused has reached to last focusable element then focus first focusable element after pressing tab + firstFocusableElement?.focus(); // add focus for the first focusable element + e.preventDefault(); + } + } + }; + + if (lastFocusableElement && firstFocusableElement) { + document.addEventListener("keydown", handleTabKey); + // firstFocusableElement?.focus(); + return () => document.removeEventListener("keydown", handleTabKey); + } +} + +function closeModal() { + document.getElementById("uds-modal").classList.remove("open"); + + let openModalButton = document.getElementById("openModalButton"); + setTimeout(() => { + if (openModalButton) { + // Wait for dom to update before setting focus + openModalButton?.focus(); + } + }, 200); + + // const mainContent = document.getElementById("main-content"); + // mainContent?.removeAttribute("inert"); + // const mainContentChildren = mainContent.children; + // let mainContentChildrenArray = Array.from(mainContentChildren); + // for (let i = 0; i < mainContentChildrenArray.length; i++) { + // mainContentChildrenArray[i].removeAttribute("inert"); + // } +} + function initModals() { document .getElementById("openModalButton") ?.addEventListener("click", function () { - document.getElementById("uds-modal")?.classList.add("open"); - document.getElementById("closeModalButton")?.focus(); + openModal(); }); document .getElementById("closeModalButton") ?.addEventListener("click", function () { - document.getElementById("uds-modal").classList.remove("open"); + closeModal(); }); document?.addEventListener("keydown", function (event) { - event.key === "Escape" && - document.getElementById("uds-modal")?.classList.remove("open"); + if (event.key === "Escape") { + closeModal(); + } }); } diff --git a/packages/unity-react-core/src/components/ButtonIconOnly/ButtonIconOnly.jsx b/packages/unity-react-core/src/components/ButtonIconOnly/ButtonIconOnly.jsx index 86ed0abd8e..c125a637a1 100644 --- a/packages/unity-react-core/src/components/ButtonIconOnly/ButtonIconOnly.jsx +++ b/packages/unity-react-core/src/components/ButtonIconOnly/ButtonIconOnly.jsx @@ -23,6 +23,7 @@ const gaDefaultObject = { */ export const ButtonIconOnly = ({ color = "gray", + autoFocus = undefined, icon = undefined, innerRef = undefined, onClick = undefined, @@ -46,6 +47,7 @@ export const ButtonIconOnly = ({ }} > - - {(openState || isBootstrap) && ( + if (isBootstrap) { + return ( +
+ {/* Disable main content on modal open */} +
+ +
- )} -
- ); + + ); + } else { + return ( +
+ {/* Disable main content on modal open */} +
+ +
+ + {openState && ( + + )} +
+ ); + } }; diff --git a/packages/unity-react-core/src/core/types/shared-types.js b/packages/unity-react-core/src/core/types/shared-types.js index 8a31446f6a..b9c02e27e4 100644 --- a/packages/unity-react-core/src/core/types/shared-types.js +++ b/packages/unity-react-core/src/core/types/shared-types.js @@ -24,6 +24,7 @@ * @typedef {Object} ButtonIconOnlyProps * @property {Array.} icon * @property {string} [color] + * @property {boolean} [autoFocus] * @property {React.RefObject} [innerRef] * @property {function():void} [onClick] * @property {"large"|"small"} [size] From abaad47a435fd075d536b85e43fe88a495bcac3c Mon Sep 17 00:00:00 2001 From: Michael Webber Date: Tue, 11 Aug 2026 17:56:26 -0700 Subject: [PATCH 02/42] feat(unity-react-core): close modal on backdrop and children props update for modal content --- .../unity-bootstrap-theme/src/js/modals.js | 24 ++-- .../src/scss/extends/_modals.scss | 10 ++ .../src/components/Modal/Modal.stories.tsx | 12 ++ .../src/components/Modal/Modal.tsx | 104 ++++++++++-------- 4 files changed, 86 insertions(+), 64 deletions(-) diff --git a/packages/unity-bootstrap-theme/src/js/modals.js b/packages/unity-bootstrap-theme/src/js/modals.js index 304506d597..49dc869e03 100644 --- a/packages/unity-bootstrap-theme/src/js/modals.js +++ b/packages/unity-bootstrap-theme/src/js/modals.js @@ -2,6 +2,7 @@ import { EventHandler } from "./bootstrap-helper"; function openModal() { document.getElementById("uds-modal")?.classList.add("open"); + document.getElementById("uds-modal-backdrop")?.classList.add("open"); let closeModalButton = document.getElementById("closeModalButton"); setTimeout(() => { if (closeModalButton) { @@ -10,14 +11,6 @@ function openModal() { } }, 200); - // const mainContent = document.getElementById("main-content"); - // mainContent.setAttribute("inert", ""); - // const mainContentChildren = mainContent.children; - // let mainContentChildrenArray = Array.from(mainContentChildren); - // for (let i = 0; i < mainContentChildrenArray.length; i++) { - // mainContentChildrenArray[i].setAttribute("inert", ""); - // } - // Disable navigation to everything accept for the modal content // Source: https://stackoverflow.com/questions/4195616/how-to-set-the-focus-on-a-javascript-modal-window const focusableElements = @@ -61,6 +54,7 @@ function openModal() { function closeModal() { document.getElementById("uds-modal").classList.remove("open"); + document.getElementById("uds-modal-backdrop").classList.remove("open"); let openModalButton = document.getElementById("openModalButton"); setTimeout(() => { @@ -69,14 +63,6 @@ function closeModal() { openModalButton?.focus(); } }, 200); - - // const mainContent = document.getElementById("main-content"); - // mainContent?.removeAttribute("inert"); - // const mainContentChildren = mainContent.children; - // let mainContentChildrenArray = Array.from(mainContentChildren); - // for (let i = 0; i < mainContentChildrenArray.length; i++) { - // mainContentChildrenArray[i].removeAttribute("inert"); - // } } function initModals() { @@ -92,6 +78,12 @@ function initModals() { closeModal(); }); + document + .getElementById("uds-modal-backdrop") + ?.addEventListener("click", function () { + closeModal(); + }); + document?.addEventListener("keydown", function (event) { if (event.key === "Escape") { closeModal(); diff --git a/packages/unity-bootstrap-theme/src/scss/extends/_modals.scss b/packages/unity-bootstrap-theme/src/scss/extends/_modals.scss index a3e6eba659..1e92aa0004 100644 --- a/packages/unity-bootstrap-theme/src/scss/extends/_modals.scss +++ b/packages/unity-bootstrap-theme/src/scss/extends/_modals.scss @@ -79,3 +79,13 @@ } } } + +.uds-modal-main { + background-color: #0000; + pointer-events: none; +} + +.uds-modal-container { + pointer-events: all; +} + diff --git a/packages/unity-react-core/src/components/Modal/Modal.stories.tsx b/packages/unity-react-core/src/components/Modal/Modal.stories.tsx index ce72416743..2202a5c6db 100644 --- a/packages/unity-react-core/src/components/Modal/Modal.stories.tsx +++ b/packages/unity-react-core/src/components/Modal/Modal.stories.tsx @@ -6,6 +6,18 @@ export default { component: Modal, args: { open: false, + children: ( + <> +

Content test

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod incididuntåç ut labore et dolore magna aliqua eiusmod tempo. +

+ + + ), }, }; diff --git a/packages/unity-react-core/src/components/Modal/Modal.tsx b/packages/unity-react-core/src/components/Modal/Modal.tsx index 18663c0e46..5148efcac8 100644 --- a/packages/unity-react-core/src/components/Modal/Modal.tsx +++ b/packages/unity-react-core/src/components/Modal/Modal.tsx @@ -26,9 +26,10 @@ export interface ModalProps { section: string; ga: string; }; + children?: JSX.Element; } -export const Modal: React.FC = ({ open, gaData }) => { +export const Modal: React.FC = ({ children, open, gaData }) => { const { isReact, isBootstrap } = useBaseSpecificFramework(); const [openState, setOpen] = React.useState(open); @@ -110,7 +111,21 @@ export const Modal: React.FC = ({ open, gaData }) => { } }, [openState]); - const modalTitle = "Content"; + let modalHeaderText = "Modal"; // default aria-label value + + if (children && children.props && children.props.children) { + for (let i = 0; i < children.props.children.length; i++) { + if (children.props.children[i].type === "h1") { + if ( + children.props.children[i].props && + children.props.children[i].props.children && + typeof children.props.children[i].props.children === "string" + ) { + modalHeaderText = children.props.children[i].props.children; + } + } + } + } if (isBootstrap) { return ( @@ -121,8 +136,6 @@ export const Modal: React.FC = ({ open, gaData }) => { autoFocus inert={isBootstrap ? undefined : openState ?? false} type="button" - // data-bs-toggle={isBootstrap && "modal"} - // data-bs-target={isBootstrap && "#uds-modal"} onClick={isReact ? handleOpen : undefined} id="openModalButton" className="btn btn-dark" @@ -130,12 +143,19 @@ export const Modal: React.FC = ({ open, gaData }) => { Show modal +
@@ -171,8 +181,6 @@ export const Modal: React.FC = ({ open, gaData }) => { autoFocus inert={openState ?? undefined} type="button" - // data-bs-toggle={isBootstrap && "modal"} - // data-bs-target={isBootstrap && "#uds-modal"} onClick={isReact ? handleOpen : undefined} id="openModalButtonR" className="btn btn-dark" @@ -182,36 +190,36 @@ export const Modal: React.FC = ({ open, gaData }) => { {openState && ( - ); From 3526760bc63eb478c3b5ff82abc24387f836533c Mon Sep 17 00:00:00 2001 From: Juan Pablo Mitriatti Date: Fri, 17 Jul 2026 10:01:00 -0300 Subject: [PATCH 03/42] fix(app-webdir-ui): render profile card title as bold text instead of h4 The Web Directory profile card rendered the person's work title in an h4, causing a skipped heading level on consuming sites where the card name is not a heading (accessibility issue). - ProfileCard (app-webdir-ui) and PersonProfile (unity-react-core) now render the title as p.person-profession-title > strong - unity-bootstrap-theme styles the new class identically to the old h4 (1rem, 700 weight, 1.625rem line-height, 2-line clamp); h4 selectors kept for backward compatibility with published consumers - person-profile Storybook template updated to the new markup --- .../app-webdir-ui/src/ProfileCard/index.js | 4 +- .../src/ProfileCard/index.test.js | 40 +++++++++++++++++++ .../src/scss/extends/_person-profile.scss | 13 +++++- .../person-profile.templates.stories.js | 12 +++--- .../PersonProfile/PersonProfile.test.tsx | 26 ++++++++++-- .../PersonProfile/PersonProfile.tsx | 26 +++++++----- 6 files changed, 97 insertions(+), 24 deletions(-) create mode 100644 packages/app-webdir-ui/src/ProfileCard/index.test.js diff --git a/packages/app-webdir-ui/src/ProfileCard/index.js b/packages/app-webdir-ui/src/ProfileCard/index.js index 77b30066fc..e4b373d4c7 100644 --- a/packages/app-webdir-ui/src/ProfileCard/index.js +++ b/packages/app-webdir-ui/src/ProfileCard/index.js @@ -90,7 +90,9 @@ const ProfileCard = ({ ...props }) => { )} {!props.profileURL &&

{props.name}

}
-

{title}

+

+ {title} +

{department && {department}}
{props.size !== "micro" && ( diff --git a/packages/app-webdir-ui/src/ProfileCard/index.test.js b/packages/app-webdir-ui/src/ProfileCard/index.test.js new file mode 100644 index 0000000000..a6a3bb4edb --- /dev/null +++ b/packages/app-webdir-ui/src/ProfileCard/index.test.js @@ -0,0 +1,40 @@ +import { render, screen } from "@testing-library/react"; +import React from "react"; + +import { ProfileCard } from "./index"; + +const defaultProps = { + name: "John Smith", + matchedAffiliationTitle: "Regents Professor", + matchedAffiliationDept: "Edplus at ASU", + imgURL: "/test-image.jpg", + profileURL: "https://search.asu.edu/profile/12345", + email: "email@asu.edu", + size: "default", + GASource: "profile card", +}; + +describe("ProfileCard", () => { + it("should render the name and title", () => { + render(); + expect(screen.getByText("John Smith")).toBeInTheDocument(); + expect(screen.getByText("Regents Professor")).toBeInTheDocument(); + }); + + it("should not render the title as a heading", () => { + const { container } = render(); + expect(container.querySelector(".person-profession h4")).toBeNull(); + expect( + screen.queryByRole("heading", { name: "Regents Professor" }) + ).toBeNull(); + }); + + it("should render the title as bold text in a paragraph", () => { + const { container } = render(); + const title = container.querySelector( + ".person-profession p.person-profession-title strong" + ); + expect(title).toBeInTheDocument(); + expect(title).toHaveTextContent("Regents Professor"); + }); +}); diff --git a/packages/unity-bootstrap-theme/src/scss/extends/_person-profile.scss b/packages/unity-bootstrap-theme/src/scss/extends/_person-profile.scss index f9230bc979..78160e0d67 100644 --- a/packages/unity-bootstrap-theme/src/scss/extends/_person-profile.scss +++ b/packages/unity-bootstrap-theme/src/scss/extends/_person-profile.scss @@ -21,7 +21,8 @@ Desktop styles --name-top-margin: 0; .person-profession { - h4:not(:first-child) { + h4:not(:first-child), + .person-profession-title:not(:first-child) { display: none; } } @@ -63,7 +64,10 @@ Desktop styles margin: 0; line-height: 1; margin-bottom: var(--person-profession-bottom-margin); - h4 { + // h4 kept for backward compatibility with consumers + // still rendering the profession title as a heading + h4, + .person-profession-title { margin: 0; font-size: $uds-size-font-medium; overflow: hidden; @@ -73,6 +77,11 @@ Desktop styles line-clamp: 2; -webkit-box-orient: vertical; } + .person-profession-title { + font-weight: $heading-font-weight; + line-height: $heading-four-line-height; + letter-spacing: $heading-four-letter-spacing; + } } .more-link { line-height: 1; diff --git a/packages/unity-bootstrap-theme/stories/molecules/person-profile/person-profile.templates.stories.js b/packages/unity-bootstrap-theme/stories/molecules/person-profile/person-profile.templates.stories.js index d39506d196..cbd3330068 100644 --- a/packages/unity-bootstrap-theme/stories/molecules/person-profile/person-profile.templates.stories.js +++ b/packages/unity-bootstrap-theme/stories/molecules/person-profile/person-profile.templates.stories.js @@ -53,12 +53,12 @@ const PersonProfile = ({ size, fill }) => (

John Smith

-

- Regents Professor -

-

- Edplus at ASU -

+

+ Regents Professor +

+

+ Edplus at ASU +

{size !== "micro" && (
    diff --git a/packages/unity-react-core/src/components/PersonProfile/PersonProfile.test.tsx b/packages/unity-react-core/src/components/PersonProfile/PersonProfile.test.tsx index b837cc06c4..074a2dc83e 100644 --- a/packages/unity-react-core/src/components/PersonProfile/PersonProfile.test.tsx +++ b/packages/unity-react-core/src/components/PersonProfile/PersonProfile.test.tsx @@ -85,15 +85,33 @@ describe("PersonProfile tests", () => { describe("accessibility tests", () => { it("should have proper aria labels for contact links", () => { - expect(screen.getByLabelText(`Email ${component.container.querySelector("a[href*='mailto:']").innerHTML}`)).toBeInTheDocument(); - expect(screen.getByLabelText(`Phone ${component.container.querySelector("a[href*='tel:']").innerHTML.replace(/\-/g, " ").replace(/\(/g, "").replace(/\)/g, "")}`)).toBeInTheDocument(); + expect( + screen.getByLabelText( + `Email ${component.container.querySelector("a[href*='mailto:']").innerHTML}` + ) + ).toBeInTheDocument(); + expect( + screen.getByLabelText( + `Phone ${component.container.querySelector("a[href*='tel:']").innerHTML.replace(/\-/g, " ").replace(/\(/g, "").replace(/\)/g, "")}` + ) + ).toBeInTheDocument(); }); it("should use semantic HTML elements", () => { expect(component.container.querySelector("address")).toBeInTheDocument(); expect(component.container.querySelector("h3")).toBeInTheDocument(); - expect(component.container.querySelector("h4")).toBeInTheDocument(); }); - }); + it("should not render profession as headings", () => { + expect( + component.container.querySelector(".person-profession h4") + ).toBeNull(); + const titles = component.container.querySelectorAll( + ".person-profession p.person-profession-title strong" + ); + expect(titles).toHaveLength(2); + expect(titles[0]).toHaveTextContent("Regents Professor"); + expect(titles[1]).toHaveTextContent("Edplus at ASU"); + }); + }); }); diff --git a/packages/unity-react-core/src/components/PersonProfile/PersonProfile.tsx b/packages/unity-react-core/src/components/PersonProfile/PersonProfile.tsx index b5a5643a3d..8cd64295d6 100644 --- a/packages/unity-react-core/src/components/PersonProfile/PersonProfile.tsx +++ b/packages/unity-react-core/src/components/PersonProfile/PersonProfile.tsx @@ -85,27 +85,31 @@ const PersonProfile: React.FC = ({

    {name}

    -

    - {profession.title} -

    -

    - {profession.department} -

    +

    + {profession.title} +

    +

    + {profession.department} +

    • - - + + {contactInfo.email}
    • - + {contactInfo.phone} From 9b209f138022b8c6e01a8d2e7aa1a199e21d72de Mon Sep 17 00:00:00 2001 From: Scott Williams <5209283+scott-williams-az@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:43:29 -0700 Subject: [PATCH 04/42] fix(app-webdir-ui): fix alpha scrolling --- packages/app-webdir-ui/package.json | 2 +- packages/app-webdir-ui/src/helpers/Filter/index.js | 7 +------ .../app-webdir-ui/src/helpers/Filter/index.styles.js | 9 +++------ yarn.lock | 2 +- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/app-webdir-ui/package.json b/packages/app-webdir-ui/package.json index bad32a63a7..aef072718b 100644 --- a/packages/app-webdir-ui/package.json +++ b/packages/app-webdir-ui/package.json @@ -39,7 +39,7 @@ "postdocs": "node ../../scripts/process-readme-props.js" }, "dependencies": { - "@asu/unity-react-core": "^1.0.0", + "@asu/unity-react-core": "^2.0.0", "@babel/preset-env": "^7.15.0", "@babel/preset-react": "^7.14.5", "axios": "~1.16.0", diff --git a/packages/app-webdir-ui/src/helpers/Filter/index.js b/packages/app-webdir-ui/src/helpers/Filter/index.js index 991b4845a0..135987ccd0 100644 --- a/packages/app-webdir-ui/src/helpers/Filter/index.js +++ b/packages/app-webdir-ui/src/helpers/Filter/index.js @@ -103,12 +103,7 @@ const FilterComponent = ({ = scrollableWidth - 5} // account for offset in scrollableWidth - clickPrev={() => { - slideNav(-1); - }} - clickNext={() => { - slideNav(1); - }} + slideNav={slideNav} />
      Date: Sat, 25 Jul 2026 00:22:30 +0000 Subject: [PATCH 05/42] chore(release): 5.0.16 [skip ci] # [@asu/app-webdir-ui-v5.0.16](https://github.com/asu/asu-unity-stack/compare/@asu/app-webdir-ui-v5.0.15...@asu/app-webdir-ui-v5.0.16) (2026-07-25) ### Bug Fixes * **app-webdir-ui:** fix alpha scrolling ([c7c8944](https://github.com/asu/asu-unity-stack/commit/c7c8944405e676396bee80935ba21d9d96234f21)) --- packages/app-webdir-ui/CHANGELOG.md | 7 +++++++ packages/app-webdir-ui/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/app-webdir-ui/CHANGELOG.md b/packages/app-webdir-ui/CHANGELOG.md index 578f378143..d583cde489 100644 --- a/packages/app-webdir-ui/CHANGELOG.md +++ b/packages/app-webdir-ui/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@asu/app-webdir-ui-v5.0.16](https://github.com/asu/asu-unity-stack/compare/@asu/app-webdir-ui-v5.0.15...@asu/app-webdir-ui-v5.0.16) (2026-07-25) + + +### Bug Fixes + +* **app-webdir-ui:** fix alpha scrolling ([c7c8944](https://github.com/asu/asu-unity-stack/commit/c7c8944405e676396bee80935ba21d9d96234f21)) + # [@asu/app-webdir-ui-v5.0.15](https://github.com/asu/asu-unity-stack/compare/@asu/app-webdir-ui-v5.0.14...@asu/app-webdir-ui-v5.0.15) (2026-07-06) diff --git a/packages/app-webdir-ui/package.json b/packages/app-webdir-ui/package.json index aef072718b..fe3d1f565d 100644 --- a/packages/app-webdir-ui/package.json +++ b/packages/app-webdir-ui/package.json @@ -1,6 +1,6 @@ { "name": "@asu/app-webdir-ui", - "version": "5.0.15", + "version": "5.0.16", "description": "App Webdir UI", "main": "./dist/webdirUI.cjs.js", "browser": "./dist/webdirUI.umd.js", From be2e48608c856948f0f642367dacfc46dab84af9 Mon Sep 17 00:00:00 2001 From: david ornelas Date: Mon, 27 Jul 2026 15:50:18 -0700 Subject: [PATCH 06/42] fix(app-webdir-ui): fix nav controls and anon image for webdir --- packages/app-webdir-ui/.storybook/main.js | 1 + packages/app-webdir-ui/.storybook/preview.js | 10 + packages/app-webdir-ui/package.json | 7 + .../app-webdir-ui/public/mockServiceWorker.js | 349 ++++++++++++++++++ .../src/FacultyRankComponent/index.stories.js | 2 + .../app-webdir-ui/src/ProfileCard/index.js | 12 +- .../src/ProfileCard/index.test.js | 72 ++-- .../WebDirectoryComponent/index.stories.js | 2 + .../src/helpers/Filter/index.test.js | 97 +++++ .../src/helpers/webDirectoryMockHandlers.js | 97 +++++ .../src/helpers/webDirectoryMockProfiles.js | 256 +++++++++++++ .../src/components/Modal/Modal.stories.tsx | 18 +- .../src/components/Modal/Modal.tsx | 123 ++++-- yarn.lock | 45 +-- 14 files changed, 992 insertions(+), 99 deletions(-) create mode 100644 packages/app-webdir-ui/public/mockServiceWorker.js create mode 100644 packages/app-webdir-ui/src/helpers/Filter/index.test.js create mode 100644 packages/app-webdir-ui/src/helpers/webDirectoryMockHandlers.js create mode 100644 packages/app-webdir-ui/src/helpers/webDirectoryMockProfiles.js diff --git a/packages/app-webdir-ui/.storybook/main.js b/packages/app-webdir-ui/.storybook/main.js index 6d77b30767..1b98b1bf90 100644 --- a/packages/app-webdir-ui/.storybook/main.js +++ b/packages/app-webdir-ui/.storybook/main.js @@ -6,6 +6,7 @@ function getAbsolutePath(value) { } const config = { + staticDirs: ["../public"], addons: [ fileURLToPath(import.meta.resolve("../../../.storybook-config/index.js")), fileURLToPath(import.meta.resolve("../../../.storybook-config/dataLayerListener/index.js")), diff --git a/packages/app-webdir-ui/.storybook/preview.js b/packages/app-webdir-ui/.storybook/preview.js index c0af51ca71..75e76f263b 100644 --- a/packages/app-webdir-ui/.storybook/preview.js +++ b/packages/app-webdir-ui/.storybook/preview.js @@ -1,9 +1,18 @@ import React, { useEffect} from "react"; import { MemoryRouter, useLocation, useSearchParams } from "react-router-dom"; +import { initialize, mswLoader } from "msw-storybook-addon"; import { useArgs } from 'storybook/preview-api'; import "@asu/unity-bootstrap-theme/src/scss/unity-bootstrap-theme.bundle.scss"; +// The live Web Directory API blocks requests from localhost, so msw mocks +// those endpoints in Storybook. See src/helpers/webDirectoryMockHandlers.js. +initialize({ + serviceWorker: { + url: "./mockServiceWorker.js", + }, +}); + const parameters = { actions: { argTypesRegex: "^on[A-Z].*" }, }; @@ -67,6 +76,7 @@ const preview = { argTypes, args, decorators, + loaders: [mswLoader], }; export default preview; diff --git a/packages/app-webdir-ui/package.json b/packages/app-webdir-ui/package.json index fe3d1f565d..b001abda16 100644 --- a/packages/app-webdir-ui/package.json +++ b/packages/app-webdir-ui/package.json @@ -80,6 +80,8 @@ "jsdoc-to-markdown": "^9.0.0", "jsdoc-ts-utils": "^2.0.1", "jsdom-screenshot": "^4.0.0", + "msw": "^2.7.0", + "msw-storybook-addon": "^2.0.0", "postcss-loader": "^6.1.1", "raw-loader": "^4.0.2", "sass": "^1.39.2", @@ -98,5 +100,10 @@ }, "volta": { "extends": "../../package.json" + }, + "msw": { + "workerDirectory": [ + "public" + ] } } diff --git a/packages/app-webdir-ui/public/mockServiceWorker.js b/packages/app-webdir-ui/public/mockServiceWorker.js new file mode 100644 index 0000000000..33dde9e770 --- /dev/null +++ b/packages/app-webdir-ui/public/mockServiceWorker.js @@ -0,0 +1,349 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = '2.14.6' +const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +addEventListener('install', function () { + self.skipWaiting() +}) + +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +addEventListener('fetch', function (event) { + const requestInterceptedAt = Date.now() + + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been terminated (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) +}) + +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse( + event, + client, + requestId, + requestInterceptedAt, + ) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, + }, + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) + } + + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + interceptedAt: requestInterceptedAt, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) + }) +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/packages/app-webdir-ui/src/FacultyRankComponent/index.stories.js b/packages/app-webdir-ui/src/FacultyRankComponent/index.stories.js index 7767f4fb8e..1ac6a36a0f 100644 --- a/packages/app-webdir-ui/src/FacultyRankComponent/index.stories.js +++ b/packages/app-webdir-ui/src/FacultyRankComponent/index.stories.js @@ -2,9 +2,11 @@ import React from "react"; import { FullLayout } from "@asu/shared"; import { WebDirectory } from "../WebDirectoryComponent/index"; +import { webDirectoryHandlers } from "../helpers/webDirectoryMockHandlers"; export default { title: "Organisms/Web Directory/Templates", + parameters: { msw: { handlers: webDirectoryHandlers } }, decorators: [story => {story()}], }; diff --git a/packages/app-webdir-ui/src/ProfileCard/index.js b/packages/app-webdir-ui/src/ProfileCard/index.js index e4b373d4c7..b1c00e55ca 100644 --- a/packages/app-webdir-ui/src/ProfileCard/index.js +++ b/packages/app-webdir-ui/src/ProfileCard/index.js @@ -12,6 +12,7 @@ import { profileCardType } from "./models"; * @param {string} [props.matchedAffiliationTitle] - The matched affiliation title of the user. * @param {string} [props.matchedAffiliationDept] - The matched affiliation department of the user. * @param {string} [props.imgURL] - The URL of the user's profile image. + * @param {string} [props.anonImgURL] - Fallback placeholder image URL used when `imgURL` is empty or fails to load. * @param {string} [props.profileURL] - The URL of the user's profile page. * @param {string} [props.email] - The email address of the user. * @param {string} [props.telephone] - The telephone number of the user. @@ -45,7 +46,14 @@ const ProfileCard = ({ ...props }) => { : ""; const hideNonExistantImages = e => { - e.target.style.display = "none"; + // Fall back to the anon placeholder image instead of hiding the image + // entirely. Guard against the anon image itself failing to load so we + // don't loop indefinitely + if (props.anonImgURL && e.target.src !== props.anonImgURL) { + e.target.src = props.anonImgURL; + } else { + e.target.style.display = "none"; + } }; let formattedTelephone = props.telephone; if (formattedTelephone) { @@ -76,7 +84,7 @@ const ProfileCard = ({ ...props }) => {
      tag would be rendered without its "src" attribute, which is not a good practice and should be avoided. + src={props.imgURL || props.anonImgURL} alt={props.name} onError={hideNonExistantImages} /> diff --git a/packages/app-webdir-ui/src/ProfileCard/index.test.js b/packages/app-webdir-ui/src/ProfileCard/index.test.js index a6a3bb4edb..7ad0c4c1ca 100644 --- a/packages/app-webdir-ui/src/ProfileCard/index.test.js +++ b/packages/app-webdir-ui/src/ProfileCard/index.test.js @@ -1,40 +1,58 @@ -import { render, screen } from "@testing-library/react"; import React from "react"; +import { fireEvent, render, screen } from "@testing-library/react"; + import { ProfileCard } from "./index"; -const defaultProps = { - name: "John Smith", - matchedAffiliationTitle: "Regents Professor", - matchedAffiliationDept: "Edplus at ASU", - imgURL: "/test-image.jpg", - profileURL: "https://search.asu.edu/profile/12345", - email: "email@asu.edu", - size: "default", - GASource: "profile card", -}; +const IMG_URL = "https://example.com/photo.jpg"; +const ANON_IMG_URL = "https://example.com/anon.png"; describe("ProfileCard", () => { - it("should render the name and title", () => { - render(); - expect(screen.getByText("John Smith")).toBeInTheDocument(); - expect(screen.getByText("Regents Professor")).toBeInTheDocument(); + it("uses anonImgURL as a fallback when imgURL is empty", () => { + render( + + ); + + expect(screen.getByAltText("Morgan Denke")).toHaveAttribute( + "src", + ANON_IMG_URL + ); }); - it("should not render the title as a heading", () => { - const { container } = render(); - expect(container.querySelector(".person-profession h4")).toBeNull(); - expect( - screen.queryByRole("heading", { name: "Regents Professor" }) - ).toBeNull(); + it("swaps to anonImgURL when the profile image fails to load", () => { + render( + + ); + + const img = screen.getByAltText("Morgan Denke"); + expect(img).toHaveAttribute("src", IMG_URL); + + fireEvent.error(img); + + expect(img).toHaveAttribute("src", ANON_IMG_URL); + expect(img).toBeVisible(); }); - it("should render the title as bold text in a paragraph", () => { - const { container } = render(); - const title = container.querySelector( - ".person-profession p.person-profession-title strong" + it("hides the image instead of looping when the anon image itself fails to load", () => { + render( + ); - expect(title).toBeInTheDocument(); - expect(title).toHaveTextContent("Regents Professor"); + + const img = screen.getByAltText("Morgan Denke"); + fireEvent.error(img); + expect(img).toHaveAttribute("src", ANON_IMG_URL); + + fireEvent.error(img); + + expect(img).toHaveAttribute("src", ANON_IMG_URL); + expect(img).not.toBeVisible(); }); }); diff --git a/packages/app-webdir-ui/src/WebDirectoryComponent/index.stories.js b/packages/app-webdir-ui/src/WebDirectoryComponent/index.stories.js index 4786c236bd..85f9df48b6 100644 --- a/packages/app-webdir-ui/src/WebDirectoryComponent/index.stories.js +++ b/packages/app-webdir-ui/src/WebDirectoryComponent/index.stories.js @@ -3,6 +3,7 @@ import React from "react"; import { WebDirectory } from "./index"; import { FullLayout } from "@asu/shared"; +import { webDirectoryHandlers } from "../helpers/webDirectoryMockHandlers"; export default { title: "Organisms/Web Directory/Templates", @@ -18,6 +19,7 @@ export default { }, }, args: { alphaFilter: "false" }, + parameters: { msw: { handlers: webDirectoryHandlers } }, decorators: [story => {story()}], }; diff --git a/packages/app-webdir-ui/src/helpers/Filter/index.test.js b/packages/app-webdir-ui/src/helpers/Filter/index.test.js new file mode 100644 index 0000000000..a4bab990b8 --- /dev/null +++ b/packages/app-webdir-ui/src/helpers/Filter/index.test.js @@ -0,0 +1,97 @@ +import React from "react"; + +import { fireEvent, render, screen } from "@testing-library/react"; + +import { FilterComponent } from "./index"; + +const CHOICES = ["A", "B", "C", "D", "E", "F", "G"]; + +/** + * jsdom never actually lays out content, so `scrollWidth`/`offsetWidth` + * default to 0. Mock them on the rendered `.choices-container` and fire the + * events `FilterComponent` listens for, mirroring what a real overflowing + * browser layout would report. + */ +const mockScrollableContainer = ({ scrollWidth, offsetWidth }) => { + const container = screen.getByRole("radiogroup"); + Object.defineProperty(container, "scrollWidth", { + configurable: true, + value: scrollWidth, + }); + Object.defineProperty(container, "offsetWidth", { + configurable: true, + value: offsetWidth, + }); + Object.defineProperty(container, "clientWidth", { + configurable: true, + value: offsetWidth, + }); + container.scrollTo = jest.fn(); + fireEvent(container, new Event("resize")); + return container; +}; + +describe("FilterComponent nav controls", () => { + it("renders the prev/next controls with the classes their CSS depends on", () => { + render( + + ); + const container = mockScrollableContainer({ + scrollWidth: 600, + offsetWidth: 300, + }); + + // Nothing scrolled yet: only the "next" control should be visible. + expect( + document.querySelector(".scroll-control-next .carousel-control-next-icon") + ).toBeInTheDocument(); + expect(document.querySelector(".scroll-control-prev")).not.toBeInTheDocument(); + + // Simulate having scrolled all the way over: "prev" appears, "next" hides. + Object.defineProperty(container, "scrollLeft", { + configurable: true, + value: 300, + }); + fireEvent.scroll(container); + + expect( + document.querySelector(".scroll-control-prev .carousel-control-prev-icon") + ).toBeInTheDocument(); + expect(document.querySelector(".scroll-control-next")).not.toBeInTheDocument(); + }); + + it("scrolls the choices container when the next control is clicked", () => { + render( + + ); + const container = mockScrollableContainer({ + scrollWidth: 600, + offsetWidth: 300, + }); + + fireEvent.click(document.querySelector(".scroll-control-next")); + + expect(container.scrollTo).toHaveBeenCalledWith({ + left: 200, + behavior: "smooth", + }); + }); + + it("clamps the scroll position to the container's max scrollable width", () => { + render( + + ); + const container = mockScrollableContainer({ + scrollWidth: 350, + offsetWidth: 300, + }); + + fireEvent.click(document.querySelector(".scroll-control-next")); + + // maxScrollLeft is only 50, even though a "next" click nominally asks for +200. + expect(container.scrollTo).toHaveBeenCalledWith({ + left: 50, + behavior: "smooth", + }); + }); +}); diff --git a/packages/app-webdir-ui/src/helpers/webDirectoryMockHandlers.js b/packages/app-webdir-ui/src/helpers/webDirectoryMockHandlers.js new file mode 100644 index 0000000000..e4f75758d9 --- /dev/null +++ b/packages/app-webdir-ui/src/helpers/webDirectoryMockHandlers.js @@ -0,0 +1,97 @@ +// @ts-check +import { http, HttpResponse } from "msw"; + +import { + getMockProfileByAsuriteId, + mockDirectoryProfiles, +} from "./webDirectoryMockProfiles"; + +const MOCK_CSRF_TOKEN = "mock-csrf-token"; + +/** + * Paginates the mock profile pool the same way the real + * `webdir-profiles/faculty-staff/filtered` endpoint would, and wraps it in + * the `{ meta: { page }, results }` envelope expected by `helpers/search.js`. + * When a `rank_group` param is present (used by `FacultyRankComponent`'s + * tabs), only profiles seeded with a matching `rankGroup` are included, so + * each tab shows distinct data instead of the same full pool. + * @param {URL} url + */ +function buildFilteredProfilesResponse(url) { + const page = Number(url.searchParams.get("page")) || 1; + const size = Number(url.searchParams.get("size")) || 6; + const rankGroup = url.searchParams.get("rank_group"); + const pool = rankGroup + ? mockDirectoryProfiles.filter( + profile => profile.rank_group.raw === rankGroup + ) + : mockDirectoryProfiles; + const start = (page - 1) * size; + const results = pool.slice(start, start + size); + + return { + meta: { + request_id: "mock-request-id", + page: { + current: page, + total_pages: Math.ceil(pool.length / size), + total_results: pool.length, + size, + }, + }, + results, + }; +} + +/** + * Handles the POST `webdir-profiles/department` endpoint, which is used both + * for the "people"/"people_departments" Web Directory search (`full_records: + * true`) and for enriching GET results with title/department info + * (`full_records: false`). + * @param {{ full_records?: boolean, profiles?: { asurite_id: string, dept_id?: string }[] }} body + */ +function buildDepartmentProfilesResponse(body) { + const profiles = body.profiles || []; + + if (!body.full_records) { + return profiles.map(profile => { + const mock = getMockProfileByAsuriteId(profile.asurite_id); + return { + title: mock.titles.raw, + dept_name: mock.departments.raw[0], + }; + }); + } + + return profiles.map((profile, index) => { + const fullRecord = getMockProfileByAsuriteId(profile.asurite_id, index); + return { + asurite_id: profile.asurite_id, + dept_id: profile.dept_id, + ...(index === 0 ? { total_results: profiles.length } : {}), + full_record: fullRecord, + }; + }); +} + +/** + * MSW request handlers for the Web Directory endpoints hit by + * `WebDirectoryComponent` and `FacultyRankComponent` stories. Registered via + * each story's `parameters.msw.handlers`. + */ +export const webDirectoryHandlers = [ + http.get("*/session/token", () => HttpResponse.text(MOCK_CSRF_TOKEN)), + + http.get("*/webdir-profiles/faculty-staff/filtered", ({ request }) => { + const url = new URL(request.url); + return HttpResponse.json(buildFilteredProfilesResponse(url)); + }), + + http.post("*/webdir-profiles/department", async ({ request }) => { + const body = + /** @type {{ full_records?: boolean, profiles?: { asurite_id: string, dept_id?: string }[] }} */ ( + await request.json() + ); + return HttpResponse.json(buildDepartmentProfilesResponse(body)); + }), +]; diff --git a/packages/app-webdir-ui/src/helpers/webDirectoryMockProfiles.js b/packages/app-webdir-ui/src/helpers/webDirectoryMockProfiles.js new file mode 100644 index 0000000000..99720010c5 --- /dev/null +++ b/packages/app-webdir-ui/src/helpers/webDirectoryMockProfiles.js @@ -0,0 +1,256 @@ +// @ts-check +/** + * Mock profile "seed" data used to build fake Web Directory API responses for + * Storybook (via msw). The real `webdir-profiles/*` endpoints block requests + * coming from localhost, so these fixtures let the Web Directory stories keep + * working without hitting the live API. + */ + +/** + * @typedef {Object} MockProfileSeed + * @property {string} asuriteId + * @property {string} eid + * @property {string} deptId + * @property {string} displayName + * @property {string} firstName + * @property {string} lastName + * @property {string[]} titles + * @property {string} deptName + * @property {string} email + * @property {string} phone + * @property {"1"|"2"|"3"|null} [rankGroup] - Matches `FacultyRankComponent`'s + * `rank_group` filter (1=Faculty, 2=Academic Professionals, 3=Other), or + * `null`/omitted for profiles outside those groups. + */ + +/** @type {MockProfileSeed[]} */ +const mockProfileSeeds = [ + { + asuriteId: "mcrow", + eid: "454517", + deptId: "1350", + displayName: "Michael Crow", + firstName: "Michael", + lastName: "Crow", + titles: ["President"], + deptName: "Office of the President", + email: "michael.crow@asu.edu", + phone: "480/965-1234", + // Not a Faculty/Academic Professional rank group; excluded from Faculty Rank tabs. + rankGroup: null, + }, + { + asuriteId: "mdenke", + eid: "1350001", + deptId: "1350", + displayName: "Morgan Denke", + firstName: "Morgan", + lastName: "Denke", + titles: ["Associate Professor"], + deptName: "School of Life Sciences", + email: "morgan.denke@asu.edu", + phone: "480/965-2345", + rankGroup: "1", // Faculty + }, + { + asuriteId: "jagarc50", + eid: "1350002", + deptId: "1350", + displayName: "Javier Garcia", + firstName: "Javier", + lastName: "Garcia", + titles: ["Lecturer"], + deptName: "School of Life Sciences", + email: "javier.garcia@asu.edu", + phone: "480/965-3456", + rankGroup: "1", // Faculty + }, + { + asuriteId: "lhillzev", + eid: "1353001", + deptId: "1353", + displayName: "Lena Hillzev", + firstName: "Lena", + lastName: "Hillzev", + titles: ["Administrative Assistant"], + deptName: "College of Health Solutions", + email: "lena.hillzev@asu.edu", + phone: "480/965-4567", + rankGroup: "3", // Other Faculty and Academic Professionals + }, + { + asuriteId: "tgrandli", + eid: "1344001", + deptId: "1344", + displayName: "Taylor Grandli", + firstName: "Taylor", + lastName: "Grandli", + titles: ["Academic Advisor"], + deptName: "W. P. Carey School of Business", + email: "taylor.grandli@asu.edu", + phone: "480/965-5678", + rankGroup: "2", // Academic Professionals + }, + { + asuriteId: "jcunnin8", + eid: "1358001", + deptId: "1358", + displayName: "Jordan Cunningham", + firstName: "Jordan", + lastName: "Cunningham", + titles: ["Professor"], + deptName: "Ira A. Fulton Schools of Engineering", + email: "jordan.cunningham@asu.edu", + phone: "480/965-6789", + rankGroup: "1", // Faculty + }, + { + asuriteId: "ccherrer", + eid: "1358002", + deptId: "1358", + displayName: "Casey Cherrer", + firstName: "Casey", + lastName: "Cherrer", + titles: ["Assistant Professor"], + deptName: "Ira A. Fulton Schools of Engineering", + email: "casey.cherrer@asu.edu", + phone: "480/965-7890", + rankGroup: "1", // Faculty + }, + { + asuriteId: "csmudde", + eid: "1358003", + deptId: "1358", + displayName: "Charlie Smudde", + firstName: "Charlie", + lastName: "Smudde", + titles: ["Research Scientist"], + deptName: "Ira A. Fulton Schools of Engineering", + email: "charlie.smudde@asu.edu", + phone: "480/965-8901", + rankGroup: "2", // Academic Professionals + }, + { + asuriteId: "abarnett", + eid: "1349001", + deptId: "1349", + displayName: "Alex Barnett", + firstName: "Alex", + lastName: "Barnett", + titles: ["Faculty"], + deptName: "College of Global Futures", + email: "alex.barnett@asu.edu", + phone: "480/965-9012", + rankGroup: "3", // Other Faculty and Academic Professionals + }, + { + asuriteId: "rjmoreno", + eid: "1518001", + deptId: "1518", + displayName: "Riley Moreno", + firstName: "Riley", + lastName: "Moreno", + titles: ["Staff"], + deptName: "Herberger Institute for Design and the Arts", + email: "riley.moreno@asu.edu", + phone: "480/965-0123", + rankGroup: null, + }, + { + asuriteId: "spatel12", + eid: "1520001", + deptId: "1520", + displayName: "Sam Patel", + firstName: "Sam", + lastName: "Patel", + titles: ["Professor"], + deptName: "Watts College of Public Service and Community Solutions", + email: "sam.patel@asu.edu", + phone: "480/965-1122", + rankGroup: "1", // Faculty + }, + { + asuriteId: "ekowalsk", + eid: "3534001", + deptId: "3534", + displayName: "Emerson Kowalski", + firstName: "Emerson", + lastName: "Kowalski", + titles: ["Associate Dean"], + deptName: "Mary Lou Fulton Teachers College", + email: "emerson.kowalski@asu.edu", + phone: "480/965-2233", + rankGroup: "2", // Academic Professionals + }, +]; + +/** + * Builds a Web Directory search-API-shaped raw record from a mock profile seed. + * @param {MockProfileSeed} seed + * @param {number} index + * @returns {Record} + */ +function toRawProfile(seed, index) { + return { + id: { raw: `mock-${seed.asuriteId}` }, + asurite_id: { raw: seed.asuriteId }, + eid: { raw: seed.eid }, + deptids: { raw: [seed.deptId] }, + display_name: { raw: seed.displayName }, + first_name: { raw: seed.firstName }, + last_name: { raw: seed.lastName }, + titles: { raw: seed.titles }, + departments: { raw: [seed.deptName] }, + email_address: { raw: seed.email }, + phone: { raw: seed.phone }, + rank_group: { raw: seed.rankGroup ?? null }, + campus_address: { raw: "ASU Tempe Campus" }, + city: { raw: "Tempe" }, + state: { raw: "AZ" }, + photo_url: { + raw: `https://source.unsplash.com/random/400x400?sig=${index}`, + }, + bio: { raw: `${seed.displayName} is a member of ${seed.deptName}.` }, + short_bio: { raw: `${seed.titles[0]} in ${seed.deptName}` }, + facebook: { raw: null }, + linkedin: { raw: null }, + twitter: { raw: null }, + website: { raw: "" }, + _meta: { + engine: "web-dir-faculty-staff", + score: 5, + id: `mock-${seed.asuriteId}`, + }, + }; +} + +export const mockDirectoryProfiles = mockProfileSeeds.map(toRawProfile); + +/** + * Looks up (or falls back to generating) a mock raw profile for the given + * asurite ID, so POST payloads referencing arbitrary IDs still resolve. + * @param {string} asuriteId + * @param {number} index + * @returns {Record} + */ +export function getMockProfileByAsuriteId(asuriteId, index = 0) { + const found = mockProfileSeeds.find(seed => seed.asuriteId === asuriteId); + if (found) { + return toRawProfile(found, index); + } + return toRawProfile( + { + asuriteId, + eid: "0", + deptId: "1350", + displayName: asuriteId, + firstName: asuriteId, + lastName: "", + titles: ["Staff"], + deptName: "Arizona State University", + email: `${asuriteId}@asu.edu`, + phone: "480/965-0000", + }, + index + ); +} diff --git a/packages/unity-react-core/src/components/Modal/Modal.stories.tsx b/packages/unity-react-core/src/components/Modal/Modal.stories.tsx index 2202a5c6db..1dfbd8f042 100644 --- a/packages/unity-react-core/src/components/Modal/Modal.stories.tsx +++ b/packages/unity-react-core/src/components/Modal/Modal.stories.tsx @@ -6,9 +6,24 @@ export default { component: Modal, args: { open: false, + setOpen: undefined, //Untested. Could potentially be used as a custom react js useState setter function for handling the modal open state value + // openModalInput: ( //Untested with react js as it requires a custom useState which would violate the rules of hooks if defined here. Using a custom JSX input option will replace the default button allowing any type of custom input options for handling the modal + // + // ), + openModalButtonClassName: "btn-dark", + openModalButtonText: "Show modal", children: ( <> -

      Content test

      +

      Content

      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod incididuntåç ut labore et dolore magna aliqua eiusmod tempo. @@ -21,6 +36,7 @@ export default { }, }; +//@ts-ignore const modalTemplate = args => ; export const Overview = { diff --git a/packages/unity-react-core/src/components/Modal/Modal.tsx b/packages/unity-react-core/src/components/Modal/Modal.tsx index 5148efcac8..2e0f13a937 100644 --- a/packages/unity-react-core/src/components/Modal/Modal.tsx +++ b/packages/unity-react-core/src/components/Modal/Modal.tsx @@ -16,7 +16,14 @@ const defaultGaData = { }; export interface ModalProps { + /** + * Modal open/closed state + */ open?: boolean; + /** + * React useState custom setter + */ + setOpen?: React.Dispatch> | undefined; gaData?: { name: string; event: string; @@ -26,35 +33,74 @@ export interface ModalProps { section: string; ga: string; }; + /** + * Custom JSX to replace the default open modal button + */ + openModalInput?: JSX.Element; + /** + * Style class for the default open modal button + */ + openModalButtonClassName?: string; + /** + * Display text for the default open modal button + */ + openModalButtonText?: string; + /** + * JSX for the content displayed within the modal + */ children?: JSX.Element; } -export const Modal: React.FC = ({ children, open, gaData }) => { +export const Modal: React.FC = ({ + children, + open, + setOpen, + openModalInput, + openModalButtonClassName, + openModalButtonText, + gaData, +}) => { const { isReact, isBootstrap } = useBaseSpecificFramework(); - const [openState, setOpen] = React.useState(open); + const [defaultOpenState, defaultSetOpen] = React.useState(open); + + const handleSetOpen = (e: boolean) => { + if (setOpen) { + setOpen(e); + } else { + defaultSetOpen(e); + } + }; + + const getOpenState = () => { + if (setOpen) { + return open; + } else { + return defaultOpenState; + } + }; const handleOpen = () => { - setOpen(true); + handleSetOpen(true); }; const handleClose = () => { - setOpen(false); + handleSetOpen(false); }; useEffect(() => { const handleKeyDown = (event: any) => { - if (event.key === "Escape") setOpen(false); // Close on Esc key + if (event.key === "Escape") handleSetOpen(false); // Close on Esc key }; - if (openState) { + if (getOpenState()) { document.addEventListener("keydown", handleKeyDown); } return () => document.removeEventListener("keydown", handleKeyDown); - }, [openState, setOpen]); + }, [getOpenState(), handleSetOpen]); useEffect(() => { - if (!openState) { + if (!getOpenState()) { let openModalButton = document.getElementById("openModalButtonR"); setTimeout(() => { if (openModalButton) { @@ -109,7 +155,7 @@ export const Modal: React.FC = ({ children, open, gaData }) => { }, 200); return () => document.removeEventListener("keydown", handleTabKey); } - }, [openState]); + }, [getOpenState()]); let modalHeaderText = "Modal"; // default aria-label value @@ -132,21 +178,25 @@ export const Modal: React.FC = ({ children, open, gaData }) => {

      {/* Disable main content on modal open */}
      - + {openModalInput ? ( + openModalInput + ) : ( + + )}
      = ({ children, open, gaData }) => { aria-modal="true" aria-label={modalHeaderText} className={classNames("uds-modal", "uds-modal-main", { - open: openState, + open: getOpenState(), })} >
      @@ -176,25 +226,28 @@ export const Modal: React.FC = ({ children, open, gaData }) => { return (
      {/* Disable main content on modal open */} -
      - +
      + {openModalInput ? ( + openModalInput + ) : ( + + )}
      - {openState && ( + {getOpenState() && ( <>
      = ({ children, open, gaData }) => { aria-modal="true" aria-label={modalHeaderText} className={classNames("uds-modal", "uds-modal-main", { - open: openState, + open: getOpenState(), })} >
      diff --git a/yarn.lock b/yarn.lock index ac5ddc5da8..9758a3d106 100644 --- a/yarn.lock +++ b/yarn.lock @@ -66,7 +66,7 @@ __metadata: resolution: "@asu/app-degree-pages@workspace:packages/app-degree-pages" dependencies: "@asu/shared": "npm:*" - "@asu/unity-react-core": "npm:^1.0.0" + "@asu/unity-react-core": "npm:^2.0.0" "@babel/core": "npm:^7.13.14" "@babel/plugin-syntax-jsx": "npm:^7.14.5" "@babel/plugin-transform-react-jsx": "npm:^7.13.12" @@ -118,8 +118,8 @@ __metadata: resolution: "@asu/app-rfi@workspace:packages/app-rfi" dependencies: "@asu/shared": "npm:*" - "@asu/unity-bootstrap-theme": "npm:^1.0.0" - "@asu/unity-react-core": "npm:^1.0.0" + "@asu/unity-bootstrap-theme": "npm:*" + "@asu/unity-react-core": "npm:^2.0.0" "@babel/core": "npm:^7.13.14" "@babel/eslint-parser": "npm:^7.13.14" "@babel/plugin-proposal-class-properties": "npm:^7.13.0" @@ -203,6 +203,8 @@ __metadata: jsdoc-to-markdown: "npm:^9.0.0" jsdoc-ts-utils: "npm:^2.0.1" jsdom-screenshot: "npm:^4.0.0" + msw: "npm:^2.7.0" + msw-storybook-addon: "npm:^2.0.0" postcss-loader: "npm:^6.1.1" prop-types: "npm:^15.7.2" raw-loader: "npm:^4.0.2" @@ -227,7 +229,7 @@ __metadata: version: 0.0.0-use.local resolution: "@asu/component-events@workspace:packages/component-events" dependencies: - "@asu/unity-react-core": "npm:^1.0.0" + "@asu/unity-react-core": "npm:^2.0.0" "@babel/core": "npm:^7.13.14" "@babel/plugin-syntax-jsx": "npm:^7.14.5" "@babel/plugin-transform-runtime": "npm:^7.14.5" @@ -335,7 +337,7 @@ __metadata: version: 0.0.0-use.local resolution: "@asu/component-news@workspace:packages/component-news" dependencies: - "@asu/unity-react-core": "npm:^1.0.0" + "@asu/unity-react-core": "npm:^2.0.0" "@babel/core": "npm:^7.13.14" "@babel/plugin-syntax-jsx": "npm:^7.14.5" "@babel/plugin-transform-react-jsx": "npm:^7.13.12" @@ -406,7 +408,7 @@ __metadata: resolution: "@asu/static-site@workspace:packages/static-site" dependencies: "@asu/component-header-footer": "npm:^1.0.0" - "@asu/unity-bootstrap-theme": "npm:^1.20" + "@asu/unity-bootstrap-theme": "npm:^2.0.0" "@asu/unity-react-core": "npm:^2.0.0" "@fortawesome/fontawesome-svg-core": "npm:^6.4.2" "@fortawesome/free-brands-svg-icons": "npm:^6.4.2" @@ -428,16 +430,7 @@ __metadata: languageName: unknown linkType: soft -"@asu/unity-bootstrap-theme@npm:^1.0.0, @asu/unity-bootstrap-theme@npm:^1.20, @asu/unity-bootstrap-theme@npm:^1.21.3": - version: 1.39.7 - resolution: "@asu/unity-bootstrap-theme@npm:1.39.7::__archiveUrl=https%3A%2F%2Fnpm.pkg.github.com%2Fdownload%2F%40asu%2Funity-bootstrap-theme%2F1.39.7%2F3cd9a99aa2e5a872bf867247a19e19cb6ee73fab" - peerDependencies: - "@fortawesome/fontawesome-free": ^5.15.3 - checksum: 10c0/9eb5e02a944026c0206412778ecfaffb18ab9b566be957bb179f908a4268129823b4695104ab74a89da163d061694d07140910dd2075852da96052f69edc77f2 - languageName: node - linkType: hard - -"@asu/unity-bootstrap-theme@workspace:packages/unity-bootstrap-theme": +"@asu/unity-bootstrap-theme@npm:*, @asu/unity-bootstrap-theme@npm:^2.0.0, @asu/unity-bootstrap-theme@workspace:packages/unity-bootstrap-theme": version: 0.0.0-use.local resolution: "@asu/unity-bootstrap-theme@workspace:packages/unity-bootstrap-theme" dependencies: @@ -491,29 +484,13 @@ __metadata: languageName: unknown linkType: soft -"@asu/unity-react-core@npm:^1.0.0": - version: 1.11.4 - resolution: "@asu/unity-react-core@npm:1.11.4::__archiveUrl=https%3A%2F%2Fnpm.pkg.github.com%2Fdownload%2F%40asu%2Funity-react-core%2F1.11.4%2Fb75273d3986c565d7ac8fb20de6659accd130f3d" - dependencies: - "@glidejs/glide": "npm:^3.6.0" - react-share: "npm:^5.3" - peerDependencies: - "@asu/unity-bootstrap-theme": ^1.21.3 - "@fortawesome/fontawesome-free": ^5.15.3 - react: ^19.0.0 - react-dom: ^19.0.0 - react-router-dom: ">= 5.2.0 < 7" - checksum: 10c0/0116ad0df26c1ff7f003e8e0134dba074390428d1a23469f946316b635955c1c8f35a34adcb74b27f8721960c14de5176939f1a8587aa262f41b55825f2d9ca4 - languageName: node - linkType: hard - "@asu/unity-react-core@npm:^2.0.0, @asu/unity-react-core@workspace:packages/unity-react-core": version: 0.0.0-use.local resolution: "@asu/unity-react-core@workspace:packages/unity-react-core" dependencies: "@asu/component-header-footer": "npm:*" "@asu/shared": "npm:*" - "@asu/unity-bootstrap-theme": "npm:^1.21.3" + "@asu/unity-bootstrap-theme": "npm:*" "@babel/cli": "npm:^7.19.3" "@babel/core": "npm:^7.21.3" "@babel/plugin-transform-runtime": "npm:^7.19.6" @@ -566,7 +543,7 @@ __metadata: typescript: "npm:5.6.2" vite: "npm:^6.0.0" peerDependencies: - "@asu/unity-bootstrap-theme": ^1.21.3 + "@asu/unity-bootstrap-theme": ^2.0.0 "@fortawesome/fontawesome-free": ^5.15.3 react: ^19.0.0 react-dom: ^19.0.0 From 2c028cb77a1cbbe13e5dd771fd86bbefc7a45322 Mon Sep 17 00:00:00 2001 From: david ornelas Date: Mon, 27 Jul 2026 15:59:03 -0700 Subject: [PATCH 07/42] fix(unity-react-core): restore nav-control styles and stop bundling react-dom/server - Restore the .scroll-control-prev/-next and carousel-control-*-icon styles into NavControls.styles.js (co-located with the component instead of unity-bootstrap-theme). PR #1698's TabbedPanels redesign removed this CSS assuming it was dead code from the old carousel-based tabs, but NavControls is still used standalone by app-webdir-ui's Filter component ("Filter by Last Initial"), which regressed to unstyled buttons as a result. - Split getBootstrapHTML (react-dom/server, Storybook/dev-tooling only) out of useBaseSpecificFramework.js into its own file. Every component imports useBaseSpecificFramework at runtime, so bundling react-dom/server there was pulling server-rendering internals (MessageChannel, TextEncoder, etc.) into every consumer's published dist for no runtime benefit, and broke jsdom-based tests in app-webdir-ui. --- .../.storybook/decorators.tsx | 2 +- .../.storybook/renderToHTML.jsx | 2 +- packages/unity-react-core/package.json | 4 +- .../scripts/check-changes.tsx | 2 +- .../GaEventWrapper/getBootstrapHTML.js | 12 +++ .../useBaseSpecificFramework.js | 4 - .../components/NavControls.styles.js | 80 ++++++++++++++++++- 7 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 packages/unity-react-core/src/components/GaEventWrapper/getBootstrapHTML.js diff --git a/packages/unity-react-core/.storybook/decorators.tsx b/packages/unity-react-core/.storybook/decorators.tsx index 8308b03839..22d67aa1a6 100644 --- a/packages/unity-react-core/.storybook/decorators.tsx +++ b/packages/unity-react-core/.storybook/decorators.tsx @@ -4,7 +4,7 @@ import { Decorator } from "@storybook/react"; import React, { ReactNode, StrictMode, useEffect } from "react"; -import { getBootstrapHTML } from "../src/components/GaEventWrapper/useBaseSpecificFramework"; +import { getBootstrapHTML } from "../src/components/GaEventWrapper/getBootstrapHTML"; import { useChannel } from "storybook/preview-api"; declare interface ContainerProps { diff --git a/packages/unity-react-core/.storybook/renderToHTML.jsx b/packages/unity-react-core/.storybook/renderToHTML.jsx index 7ebbd94919..9797b2e6fd 100644 --- a/packages/unity-react-core/.storybook/renderToHTML.jsx +++ b/packages/unity-react-core/.storybook/renderToHTML.jsx @@ -1,5 +1,5 @@ -import { getBootstrapHTML } from '../src/components/GaEventWrapper/useBaseSpecificFramework.js'; +import { getBootstrapHTML } from '../src/components/GaEventWrapper/getBootstrapHTML.js'; import { formatCode } from './formatCode.js'; export { formatCode }; diff --git a/packages/unity-react-core/package.json b/packages/unity-react-core/package.json index c187db18e3..aa7c62da20 100644 --- a/packages/unity-react-core/package.json +++ b/packages/unity-react-core/package.json @@ -51,7 +51,7 @@ "devDependencies": { "@asu/component-header-footer": "*", "@asu/shared": "*", - "@asu/unity-bootstrap-theme": "^1.21.3", + "@asu/unity-bootstrap-theme": "*", "@babel/cli": "^7.19.3", "@babel/core": "^7.21.3", "@babel/plugin-transform-runtime": "^7.19.6", @@ -107,7 +107,7 @@ "react-share": "^5.3" }, "peerDependencies": { - "@asu/unity-bootstrap-theme": "^1.21.3", + "@asu/unity-bootstrap-theme": "^2.0.0", "@fortawesome/fontawesome-free": "^5.15.3", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/packages/unity-react-core/scripts/check-changes.tsx b/packages/unity-react-core/scripts/check-changes.tsx index e1c21e9d66..0cddb90767 100644 --- a/packages/unity-react-core/scripts/check-changes.tsx +++ b/packages/unity-react-core/scripts/check-changes.tsx @@ -31,7 +31,7 @@ import { SidebarMenu } from "../src/components/SidebarMenu/SidebarMenu"; import { SystemAlert } from "../src/components/SystemAlert/SystemAlert"; import { Table } from "../src/components/Tables/Tables"; import { Loader } from "../src/components/Loader/Loader"; -import { getBootstrapHTML } from "../src/components/GaEventWrapper/useBaseSpecificFramework.js"; +import { getBootstrapHTML } from "../src/components/GaEventWrapper/getBootstrapHTML.js"; import { initializeServerEnvironment, cleanupServerEnvironment } from "./server-utils.js"; import fs from "fs"; import path from "path"; diff --git a/packages/unity-react-core/src/components/GaEventWrapper/getBootstrapHTML.js b/packages/unity-react-core/src/components/GaEventWrapper/getBootstrapHTML.js new file mode 100644 index 0000000000..427fa36c2d --- /dev/null +++ b/packages/unity-react-core/src/components/GaEventWrapper/getBootstrapHTML.js @@ -0,0 +1,12 @@ +// Storybook/dev-tooling only: renders a component to static HTML to preview +// its "Bootstrap" (non-React, static markup + vanilla JS) consumption path. +// Deliberately kept out of useBaseSpecificFramework.js, which every +// component imports at runtime for the isBootstrap/isReact hook — importing +// react-dom/server there would bundle server-rendering internals into every +// consuming app's dist output for no benefit. +import { renderToStaticMarkup } from "react-dom/server"; + +import { identifierPrefix } from "./useBaseSpecificFramework"; + +export const getBootstrapHTML = jsx => + renderToStaticMarkup(jsx, { identifierPrefix }); diff --git a/packages/unity-react-core/src/components/GaEventWrapper/useBaseSpecificFramework.js b/packages/unity-react-core/src/components/GaEventWrapper/useBaseSpecificFramework.js index 62ee346f46..c7f44f2af9 100644 --- a/packages/unity-react-core/src/components/GaEventWrapper/useBaseSpecificFramework.js +++ b/packages/unity-react-core/src/components/GaEventWrapper/useBaseSpecificFramework.js @@ -1,13 +1,9 @@ // where does this hook belong? There might be a better location for it. import { useId } from "react"; -import { renderToStaticMarkup } from "react-dom/server"; // used inside the StaticStory component in packages/unity-react-core/.storybook/decorators.tsx export const identifierPrefix = "staticMarkup"; -export const getBootstrapHTML = jsx => - renderToStaticMarkup(jsx, { identifierPrefix }); - export function useBaseSpecificFramework() { const id = useId(); /** diff --git a/packages/unity-react-core/src/components/TabbedPanels/components/NavControls.styles.js b/packages/unity-react-core/src/components/TabbedPanels/components/NavControls.styles.js index 374446b17d..6efbd3129f 100644 --- a/packages/unity-react-core/src/components/TabbedPanels/components/NavControls.styles.js +++ b/packages/unity-react-core/src/components/TabbedPanels/components/NavControls.styles.js @@ -1,14 +1,90 @@ // @ts-check import styled from "styled-components"; -// TODO Why is this not in unity-bootstrap-theme? -// TODO move to unity-bootstrap-theme or update comment explaining why it's here +// Co-located here (rather than in unity-bootstrap-theme) so `NavControls` +// renders correctly for any consumer out of the box, without depending on a +// shared/global stylesheet that may not know this component still needs it. const NavControlButtons = styled.div` button { padding: 16px 0; border: none; outline: none; } + + .scroll-control-prev, + .scroll-control-next { + outline: none; + border: none; + width: 80px; + position: absolute; + height: 100%; + top: 0; + } + + .scroll-control-prev { + background: linear-gradient( + 90deg, + rgba(25, 25, 25, 0.25) 0%, + rgba(25, 25, 25, 0) 100% + ); + left: 0; + } + + .scroll-control-next { + right: 0; + background: linear-gradient( + 90deg, + rgba(25, 25, 25, 0) 0%, + rgba(25, 25, 25, 0.25) 100% + ); + + .carousel-control-next-icon { + margin: 0 12px 0 42px; + } + } + + .scroll-control-prev .carousel-control-prev-icon, + .scroll-control-next .carousel-control-next-icon { + background-size: 60% 60%; + display: block; + opacity: 1; + padding: 12px; + position: relative; + top: 50%; + left: 0; + transform: translate(0, -50%); + background-color: #fafafa; // $asu-gray-7 + border: solid 1px #d0d0d0; // $asu-gray-5 + border-radius: 100%; + color: #000; + } + + .carousel-control-next-icon { + background-image: url("data:image/svg+xml; utf8, "); + background-position: 80% 50%; + } + + .carousel-control-prev-icon { + background-image: url("data:image/svg+xml; utf8, "); + background-position: 60% 50%; + } + + @media screen and (max-width: 768px) { + // $uds-breakpoint-md + .scroll-control-prev, + .scroll-control-next { + width: 48px; + } + + .scroll-control-next .carousel-control-next-icon, + .scroll-control-prev .carousel-control-prev-icon { + margin: 0px 12px 0px 8px; + } + + .scroll-control-prev .carousel-control-prev-icon { + margin-left: 0px; + } + } `; export { NavControlButtons }; From 832cd8c6200f3f1b16145106396625c65245d13d Mon Sep 17 00:00:00 2001 From: david ornelas Date: Mon, 27 Jul 2026 16:19:12 -0700 Subject: [PATCH 08/42] fix(app-rfi): correct stale @asu/unity-bootstrap-theme and @asu/unity-react-core dependency ranges Both were pinned to ^1.x ranges that no longer match the workspace's current major versions (unity-bootstrap-theme 2.x, unity-react-core 2.x), causing yarn to resolve a stale published copy from the registry instead of symlinking the local workspace package. --- packages/app-rfi/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app-rfi/package.json b/packages/app-rfi/package.json index 5da4e88477..8804ff8be1 100644 --- a/packages/app-rfi/package.json +++ b/packages/app-rfi/package.json @@ -44,7 +44,7 @@ }, "devDependencies": { "@asu/shared": "*", - "@asu/unity-bootstrap-theme": "^1.0.0", + "@asu/unity-bootstrap-theme": "*", "@babel/core": "^7.13.14", "@babel/eslint-parser": "^7.13.14", "@babel/plugin-proposal-class-properties": "^7.13.0", @@ -79,7 +79,7 @@ "webpack-merge": "^5.8.0" }, "dependencies": { - "@asu/unity-react-core": "^1.0.0", + "@asu/unity-react-core": "^2.0.0", "formik": "^2.1.4", "prop-types": "^15.7.2", "react-phone-input-2": "2.15.1", From 2f2152bba94e7570de32f764e6752d4f65166d6d Mon Sep 17 00:00:00 2001 From: david ornelas Date: Mon, 27 Jul 2026 16:19:23 -0700 Subject: [PATCH 09/42] fix(component-events): correct stale @asu/unity-react-core dependency range Was pinned to ^1.0.0, which no longer matches the workspace's current 2.x version, causing yarn to resolve a stale published copy from the registry instead of symlinking the local workspace package. --- packages/component-events/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/component-events/package.json b/packages/component-events/package.json index 3402038070..c341e375a7 100644 --- a/packages/component-events/package.json +++ b/packages/component-events/package.json @@ -39,7 +39,7 @@ "postdocs": "node ../../scripts/process-readme-props.js" }, "dependencies": { - "@asu/unity-react-core": "^1.0.0", + "@asu/unity-react-core": "^2.0.0", "prop-types": "^15.7.2", "styled-components": "^6.0.0" }, From a9fe2def17cff6997e6439544776e7f1df5be6c8 Mon Sep 17 00:00:00 2001 From: david ornelas Date: Mon, 27 Jul 2026 16:19:35 -0700 Subject: [PATCH 10/42] fix(component-news): correct stale @asu/unity-react-core dependency range Was pinned to ^1.0.0, which no longer matches the workspace's current 2.x version, causing yarn to resolve a stale published copy from the registry instead of symlinking the local workspace package. --- packages/component-news/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/component-news/package.json b/packages/component-news/package.json index d363f6d395..bc0690cedc 100644 --- a/packages/component-news/package.json +++ b/packages/component-news/package.json @@ -39,7 +39,7 @@ "postdocs": "node ../../scripts/process-readme-props.js" }, "dependencies": { - "@asu/unity-react-core": "^1.0.0", + "@asu/unity-react-core": "^2.0.0", "prop-types": "^15.7.2", "styled-components": "^6.0.0" }, From d711bec352005c8afb7ae87507b497fdf090108a Mon Sep 17 00:00:00 2001 From: david ornelas Date: Mon, 27 Jul 2026 16:19:44 -0700 Subject: [PATCH 11/42] fix(static-site): correct stale @asu/unity-bootstrap-theme dependency range Was pinned to ^1.20, which no longer matches the workspace's current 2.x version, causing yarn to resolve a stale published copy from the registry instead of symlinking the local workspace package. --- packages/static-site/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/static-site/package.json b/packages/static-site/package.json index 693845b713..f5647e601b 100644 --- a/packages/static-site/package.json +++ b/packages/static-site/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@asu/component-header-footer": "^1.0.0", - "@asu/unity-bootstrap-theme": "^1.20", + "@asu/unity-bootstrap-theme": "^2.0.0", "@asu/unity-react-core": "^2.0.0", "@fortawesome/fontawesome-svg-core": "^6.4.2", "@fortawesome/free-brands-svg-icons": "^6.4.2", From f9a6f9e4c6e43f60623800d33f046d27089804c4 Mon Sep 17 00:00:00 2001 From: david ornelas Date: Mon, 27 Jul 2026 16:20:07 -0700 Subject: [PATCH 12/42] fix(app-degree-pages): correct stale @asu/unity-react-core dependency range Was pinned to ^1.0.0, which no longer matches the workspace's current 2.x version, causing yarn to resolve a stale published copy from the registry instead of symlinking the local workspace package. --- packages/app-degree-pages/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-degree-pages/package.json b/packages/app-degree-pages/package.json index 242f95c8de..a9fdd9b61f 100644 --- a/packages/app-degree-pages/package.json +++ b/packages/app-degree-pages/package.json @@ -43,7 +43,7 @@ "postdocs": "node ../../scripts/process-readme-props.js" }, "dependencies": { - "@asu/unity-react-core": "^1.0.0", + "@asu/unity-react-core": "^2.0.0", "@popperjs/core": "^2.9.2", "classnames": "^2.3.1", "prop-types": "^15.7.2", From dd06802f7b58888a04d970144a593239d768cec4 Mon Sep 17 00:00:00 2001 From: Scott Williams <5209283+scott-williams-az@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:16:05 -0700 Subject: [PATCH 13/42] chore(app-webdir-ui): add proxy server to allow api with local dev --- packages/app-webdir-ui/.gitignore | 4 + packages/app-webdir-ui/.storybook/main.js | 98 ++++- packages/app-webdir-ui/.storybook/preview.js | 75 ++-- packages/app-webdir-ui/package.json | 6 +- .../app-webdir-ui/public/mockServiceWorker.js | 349 ------------------ packages/app-webdir-ui/server.js | 218 +++++++++++ .../src/FacultyRankComponent/index.stories.js | 9 +- .../src/SearchPage/index.stories.js | 7 +- .../WebDirectoryComponent/index.stories.js | 23 +- .../src/helpers/webDirectoryMockHandlers.js | 97 ----- .../src/helpers/webDirectoryMockProfiles.js | 256 ------------- yarn.lock | 90 +++-- 12 files changed, 444 insertions(+), 788 deletions(-) delete mode 100644 packages/app-webdir-ui/public/mockServiceWorker.js create mode 100644 packages/app-webdir-ui/server.js delete mode 100644 packages/app-webdir-ui/src/helpers/webDirectoryMockHandlers.js delete mode 100644 packages/app-webdir-ui/src/helpers/webDirectoryMockProfiles.js diff --git a/packages/app-webdir-ui/.gitignore b/packages/app-webdir-ui/.gitignore index 905cf52a77..bcbf6faaac 100644 --- a/packages/app-webdir-ui/.gitignore +++ b/packages/app-webdir-ui/.gitignore @@ -5,3 +5,7 @@ package-lock.json yarn-lock.json docs/** !docs/README.props.md + +# Environment overrides (personal/local config) +.env.local +.env*.local diff --git a/packages/app-webdir-ui/.storybook/main.js b/packages/app-webdir-ui/.storybook/main.js index 1b98b1bf90..23e70f33a1 100644 --- a/packages/app-webdir-ui/.storybook/main.js +++ b/packages/app-webdir-ui/.storybook/main.js @@ -1,21 +1,113 @@ -import { dirname } from "path"; +import { existsSync, readFileSync } from "fs"; +import { dirname, resolve } from "path"; import { fileURLToPath } from "url"; function getAbsolutePath(value) { return dirname(fileURLToPath(import.meta.resolve(value))); } +const storybookDir = dirname(fileURLToPath(import.meta.url)); +const packageRoot = resolve(storybookDir, ".."); + +function loadEnvFile(filePath) { + if (!existsSync(filePath)) { + return; + } + + const fileContent = readFileSync(filePath, "utf8"); + for (const rawLine of fileContent.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line || line.startsWith("#")) { + continue; + } + + const separatorIndex = line.indexOf("="); + if (separatorIndex < 0) { + continue; + } + + const key = line.slice(0, separatorIndex).trim(); + if (!key || process.env[key] !== undefined) { + continue; + } + + const value = line.slice(separatorIndex + 1).trim(); + process.env[key] = value.replace(/^['"]|['"]$/g, ""); + } +} + +function loadStorybookEnv(configType) { + const envFileName = + configType === "PRODUCTION" ? ".env.storybook-build" : ".env.development"; + loadEnvFile(resolve(packageRoot, envFileName)); +} + const config = { - staticDirs: ["../public"], addons: [ fileURLToPath(import.meta.resolve("../../../.storybook-config/index.js")), - fileURLToPath(import.meta.resolve("../../../.storybook-config/dataLayerListener/index.js")), + fileURLToPath( + import.meta + .resolve("../../../.storybook-config/dataLayerListener/index.js") + ), getAbsolutePath("@storybook/addon-a11y"), ], stories: ["../src/**/*.stories.js"], framework: { name: getAbsolutePath("@storybook/react-vite"), }, + /** + * Configure Vite proxy for real API data in dev environment. + * Proxy target is read from env as VITE_API_URL + VITE_SEARCH_API_VERSION. + * Proxy only API routes to avoid intercepting Storybook/Vite internal assets. + */ + viteFinal: async (config, { configType }) => { + loadStorybookEnv(configType); + + const apiUrl = process.env.VITE_API_URL; + const searchApiVersion = process.env.VITE_SEARCH_API_VERSION; + const target = `${apiUrl || ""}${searchApiVersion || ""}`; + const apiPath = `/${(searchApiVersion || "") + .replace(/^\/+/, "") + .replace(/\/+$/, "")}`; + + if (!target) { + throw new Error( + "Missing Storybook proxy target. Set VITE_API_URL and VITE_SEARCH_API_VERSION in env files." + ); + } + + config.server = config.server || {}; + config.server.proxy = { + [apiPath]: { + target, + changeOrigin: true, + secure: false, + logLevel: "info", + }, + "/session/token": { + target, + changeOrigin: true, + secure: false, + logLevel: "info", + }, + "/webdir-profiles": { + target, + changeOrigin: true, + secure: false, + logLevel: "info", + }, + }; + + config.optimizeDeps = config.optimizeDeps || {}; + config.optimizeDeps.esbuildOptions = + config.optimizeDeps.esbuildOptions || {}; + config.optimizeDeps.esbuildOptions.loader = { + ...(config.optimizeDeps.esbuildOptions.loader || {}), + ".js": "jsx", + }; + + return config; + }, }; export default config; diff --git a/packages/app-webdir-ui/.storybook/preview.js b/packages/app-webdir-ui/.storybook/preview.js index 75e76f263b..85dbf5398e 100644 --- a/packages/app-webdir-ui/.storybook/preview.js +++ b/packages/app-webdir-ui/.storybook/preview.js @@ -1,18 +1,9 @@ -import React, { useEffect} from "react"; +import React, { useEffect, useRef } from "react"; import { MemoryRouter, useLocation, useSearchParams } from "react-router-dom"; -import { initialize, mswLoader } from "msw-storybook-addon"; -import { useArgs } from 'storybook/preview-api'; +import { useArgs } from "storybook/preview-api"; import "@asu/unity-bootstrap-theme/src/scss/unity-bootstrap-theme.bundle.scss"; -// The live Web Directory API blocks requests from localhost, so msw mocks -// those endpoints in Storybook. See src/helpers/webDirectoryMockHandlers.js. -initialize({ - serviceWorker: { - url: "./mockServiceWorker.js", - }, -}); - const parameters = { actions: { argTypesRegex: "^on[A-Z].*" }, }; @@ -23,50 +14,67 @@ const argTypes = { control: { type: "object", }, - } + }, }; const args = { - searchParams: {} + searchParams: {}, }; const getParamObject = (paramArray = []) => { const result = {}; - for (const entry of (paramArray.entries())) { + for (const entry of paramArray.entries()) { result[entry[0]] = entry[1]; } return result; -} +}; -const Wrapper = ({ args, updateArgs,...props}) => { +const areObjectsEqual = (left = {}, right = {}) => + JSON.stringify(left) === JSON.stringify(right); + +const Wrapper = ({ args, updateArgs, ...props }) => { const location = useLocation(); - const [searchParams, setSearchParams] = useSearchParams(); + const [, setSearchParams] = useSearchParams(); + const seededFromArgsRef = useRef(false); + + useEffect(() => { + const nextSearchParams = getParamObject(new URLSearchParams(location.search)); + if (!areObjectsEqual(args.searchParams, nextSearchParams)) { + updateArgs({ + searchParams: nextSearchParams, + }); + } + }, [args.searchParams, location.search, updateArgs]); - useEffect(()=>{ - updateArgs({ - ...args, - searchParams: getParamObject(new URLSearchParams(location.search)), - }) - },[location.search]) + useEffect(() => { + if (seededFromArgsRef.current) { + return; + } - useEffect(()=>{ - setSearchParams({ - ...getParamObject(searchParams), - ...args.searchParams, - }); - },[args.searchParams]) + const currentSearchParams = getParamObject(new URLSearchParams(location.search)); + if ( + Object.keys(currentSearchParams).length === 0 && + Object.keys(args.searchParams || {}).length > 0 + ) { + setSearchParams(args.searchParams, { replace: true }); + } + + seededFromArgsRef.current = true; + }, [args.searchParams, location.search, setSearchParams]); return props.children; -} +}; const decorators = [ - (Story) => { + Story => { const [args, updateArgs] = useArgs(); - return + return ( + - + + ); }, ]; @@ -76,7 +84,6 @@ const preview = { argTypes, args, decorators, - loaders: [mswLoader], }; export default preview; diff --git a/packages/app-webdir-ui/package.json b/packages/app-webdir-ui/package.json index b001abda16..28021f3cc3 100644 --- a/packages/app-webdir-ui/package.json +++ b/packages/app-webdir-ui/package.json @@ -31,7 +31,9 @@ "build": "vite build && cp -r src/assets dist/", "build:stats": "webpack -c webpack/webpack.prod.js --profile --json=compilation-stats.json", "start:dev": "webpack-dashboard -- webpack serve -c webpack/webpack.dev.js", - "storybook": "storybook dev -p 9030", + "server": "node server.js", + "storybook:ui": "storybook dev -p 9030", + "storybook": "concurrently \"npm run server\" \"npm run storybook:ui\" --names \"server,storybook\" --prefix \"[{name}]\" --kill-others-on-exit", "build-storybook": "storybook build -o ../../build/$npm_package_name", "jsdoc": "jsdoc -c jsdoc.config.js", "predocs": "mkdir -p ./docs", @@ -63,6 +65,7 @@ "@testing-library/react": "^16.0.0", "@vitejs/plugin-react": "^4.3.1", "babel-loader": "^8.2.2", + "concurrently": "^10.0.3", "copy-webpack-plugin": "^9.0.1", "css-loader": "^5.2.4", "dotenv-webpack": "^7.0.3", @@ -81,7 +84,6 @@ "jsdoc-ts-utils": "^2.0.1", "jsdom-screenshot": "^4.0.0", "msw": "^2.7.0", - "msw-storybook-addon": "^2.0.0", "postcss-loader": "^6.1.1", "raw-loader": "^4.0.2", "sass": "^1.39.2", diff --git a/packages/app-webdir-ui/public/mockServiceWorker.js b/packages/app-webdir-ui/public/mockServiceWorker.js deleted file mode 100644 index 33dde9e770..0000000000 --- a/packages/app-webdir-ui/public/mockServiceWorker.js +++ /dev/null @@ -1,349 +0,0 @@ -/* eslint-disable */ -/* tslint:disable */ - -/** - * Mock Service Worker. - * @see https://github.com/mswjs/msw - * - Please do NOT modify this file. - */ - -const PACKAGE_VERSION = '2.14.6' -const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' -const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') -const activeClientIds = new Set() - -addEventListener('install', function () { - self.skipWaiting() -}) - -addEventListener('activate', function (event) { - event.waitUntil(self.clients.claim()) -}) - -addEventListener('message', async function (event) { - const clientId = Reflect.get(event.source || {}, 'id') - - if (!clientId || !self.clients) { - return - } - - const client = await self.clients.get(clientId) - - if (!client) { - return - } - - const allClients = await self.clients.matchAll({ - type: 'window', - }) - - switch (event.data) { - case 'KEEPALIVE_REQUEST': { - sendToClient(client, { - type: 'KEEPALIVE_RESPONSE', - }) - break - } - - case 'INTEGRITY_CHECK_REQUEST': { - sendToClient(client, { - type: 'INTEGRITY_CHECK_RESPONSE', - payload: { - packageVersion: PACKAGE_VERSION, - checksum: INTEGRITY_CHECKSUM, - }, - }) - break - } - - case 'MOCK_ACTIVATE': { - activeClientIds.add(clientId) - - sendToClient(client, { - type: 'MOCKING_ENABLED', - payload: { - client: { - id: client.id, - frameType: client.frameType, - }, - }, - }) - break - } - - case 'CLIENT_CLOSED': { - activeClientIds.delete(clientId) - - const remainingClients = allClients.filter((client) => { - return client.id !== clientId - }) - - // Unregister itself when there are no more clients - if (remainingClients.length === 0) { - self.registration.unregister() - } - - break - } - } -}) - -addEventListener('fetch', function (event) { - const requestInterceptedAt = Date.now() - - // Bypass navigation requests. - if (event.request.mode === 'navigate') { - return - } - - // Opening the DevTools triggers the "only-if-cached" request - // that cannot be handled by the worker. Bypass such requests. - if ( - event.request.cache === 'only-if-cached' && - event.request.mode !== 'same-origin' - ) { - return - } - - // Bypass all requests when there are no active clients. - // Prevents the self-unregistered worked from handling requests - // after it's been terminated (still remains active until the next reload). - if (activeClientIds.size === 0) { - return - } - - const requestId = crypto.randomUUID() - event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) -}) - -/** - * @param {FetchEvent} event - * @param {string} requestId - * @param {number} requestInterceptedAt - */ -async function handleRequest(event, requestId, requestInterceptedAt) { - const client = await resolveMainClient(event) - const requestCloneForEvents = event.request.clone() - const response = await getResponse( - event, - client, - requestId, - requestInterceptedAt, - ) - - // Send back the response clone for the "response:*" life-cycle events. - // Ensure MSW is active and ready to handle the message, otherwise - // this message will pend indefinitely. - if (client && activeClientIds.has(client.id)) { - const serializedRequest = await serializeRequest(requestCloneForEvents) - - // Clone the response so both the client and the library could consume it. - const responseClone = response.clone() - - sendToClient( - client, - { - type: 'RESPONSE', - payload: { - isMockedResponse: IS_MOCKED_RESPONSE in response, - request: { - id: requestId, - ...serializedRequest, - }, - response: { - type: responseClone.type, - status: responseClone.status, - statusText: responseClone.statusText, - headers: Object.fromEntries(responseClone.headers.entries()), - body: responseClone.body, - }, - }, - }, - responseClone.body ? [serializedRequest.body, responseClone.body] : [], - ) - } - - return response -} - -/** - * Resolve the main client for the given event. - * Client that issues a request doesn't necessarily equal the client - * that registered the worker. It's with the latter the worker should - * communicate with during the response resolving phase. - * @param {FetchEvent} event - * @returns {Promise} - */ -async function resolveMainClient(event) { - const client = await self.clients.get(event.clientId) - - if (activeClientIds.has(event.clientId)) { - return client - } - - if (client?.frameType === 'top-level') { - return client - } - - const allClients = await self.clients.matchAll({ - type: 'window', - }) - - return allClients - .filter((client) => { - // Get only those clients that are currently visible. - return client.visibilityState === 'visible' - }) - .find((client) => { - // Find the client ID that's recorded in the - // set of clients that have registered the worker. - return activeClientIds.has(client.id) - }) -} - -/** - * @param {FetchEvent} event - * @param {Client | undefined} client - * @param {string} requestId - * @param {number} requestInterceptedAt - * @returns {Promise} - */ -async function getResponse(event, client, requestId, requestInterceptedAt) { - // Clone the request because it might've been already used - // (i.e. its body has been read and sent to the client). - const requestClone = event.request.clone() - - function passthrough() { - // Cast the request headers to a new Headers instance - // so the headers can be manipulated with. - const headers = new Headers(requestClone.headers) - - // Remove the "accept" header value that marked this request as passthrough. - // This prevents request alteration and also keeps it compliant with the - // user-defined CORS policies. - const acceptHeader = headers.get('accept') - if (acceptHeader) { - const values = acceptHeader.split(',').map((value) => value.trim()) - const filteredValues = values.filter( - (value) => value !== 'msw/passthrough', - ) - - if (filteredValues.length > 0) { - headers.set('accept', filteredValues.join(', ')) - } else { - headers.delete('accept') - } - } - - return fetch(requestClone, { headers }) - } - - // Bypass mocking when the client is not active. - if (!client) { - return passthrough() - } - - // Bypass initial page load requests (i.e. static assets). - // The absence of the immediate/parent client in the map of the active clients - // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet - // and is not ready to handle requests. - if (!activeClientIds.has(client.id)) { - return passthrough() - } - - // Notify the client that a request has been intercepted. - const serializedRequest = await serializeRequest(event.request) - const clientMessage = await sendToClient( - client, - { - type: 'REQUEST', - payload: { - id: requestId, - interceptedAt: requestInterceptedAt, - ...serializedRequest, - }, - }, - [serializedRequest.body], - ) - - switch (clientMessage.type) { - case 'MOCK_RESPONSE': { - return respondWithMock(clientMessage.data) - } - - case 'PASSTHROUGH': { - return passthrough() - } - } - - return passthrough() -} - -/** - * @param {Client} client - * @param {any} message - * @param {Array} transferrables - * @returns {Promise} - */ -function sendToClient(client, message, transferrables = []) { - return new Promise((resolve, reject) => { - const channel = new MessageChannel() - - channel.port1.onmessage = (event) => { - if (event.data && event.data.error) { - return reject(event.data.error) - } - - resolve(event.data) - } - - client.postMessage(message, [ - channel.port2, - ...transferrables.filter(Boolean), - ]) - }) -} - -/** - * @param {Response} response - * @returns {Response} - */ -function respondWithMock(response) { - // Setting response status code to 0 is a no-op. - // However, when responding with a "Response.error()", the produced Response - // instance will have status code set to 0. Since it's not possible to create - // a Response instance with status code 0, handle that use-case separately. - if (response.status === 0) { - return Response.error() - } - - const mockedResponse = new Response(response.body, response) - - Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { - value: true, - enumerable: true, - }) - - return mockedResponse -} - -/** - * @param {Request} request - */ -async function serializeRequest(request) { - return { - url: request.url, - mode: request.mode, - method: request.method, - headers: Object.fromEntries(request.headers.entries()), - cache: request.cache, - credentials: request.credentials, - destination: request.destination, - integrity: request.integrity, - redirect: request.redirect, - referrer: request.referrer, - referrerPolicy: request.referrerPolicy, - body: await request.arrayBuffer(), - keepalive: request.keepalive, - } -} diff --git a/packages/app-webdir-ui/server.js b/packages/app-webdir-ui/server.js new file mode 100644 index 0000000000..4d2e8e8060 --- /dev/null +++ b/packages/app-webdir-ui/server.js @@ -0,0 +1,218 @@ +/** + * Development backend server for Web Directory API endpoints. + * Proxies requests to the real API for Storybook development. + * + * Endpoints: + * - GET /session/token + * - GET /webdir-profiles/faculty-staff/filtered + * - POST /webdir-profiles/department + */ + +const express = require("express"); +const app = express(); +const PORT = Number(process.env.PORT || 3000); +const API_ORIGIN = "https://asuapp2dev.prod.acquia-sites.com"; +// API_PATH_PREFIX is for local routing within this server +const API_PATH_PREFIX = "/api/v1"; +// SEARCH_API_PATH is for upstream URL construction +const SEARCH_API_PATH = `${API_PATH_PREFIX}/`; + +app.use(express.json()); + +app.use((req, res, next) => { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS"); + res.setHeader( + "Access-Control-Allow-Headers", + "Content-Type, X-CSRF-Token, Authorization" + ); + + if (req.method === "OPTIONS") { + res.sendStatus(204); + return; + } + + next(); +}); + +function getTargetUrl(path, queryParams, useSearchApiPath = true) { + const baseUrl = useSearchApiPath + ? new URL(SEARCH_API_PATH, API_ORIGIN) + : new URL("/", API_ORIGIN); + const normalizedPath = path.startsWith("/") ? path.slice(1) : path; + const url = new URL(normalizedPath, baseUrl); + if (queryParams) { + for (const [key, value] of Object.entries(queryParams)) { + if (value !== undefined) { + url.searchParams.append(key, value); + } + } + } + return url; +} + +async function proxyRequest({ + path, + method = "GET", + query, + body, + headers = {}, + useSearchApiPath = true, +}) { + const targetUrl = getTargetUrl(path, query, useSearchApiPath); + const requestOptions = { + method, + headers, + }; + + if (body !== undefined) { + requestOptions.body = JSON.stringify(body); + requestOptions.headers = { + "Content-Type": "application/json", + ...headers, + }; + } + + return fetch(targetUrl, requestOptions); +} + +function withApiPrefix(path) { + return [path, `${API_PATH_PREFIX}${path}`]; +} + +function toUpstreamPath(requestPath) { + if (requestPath.startsWith(`${API_PATH_PREFIX}/`)) { + return requestPath.slice(API_PATH_PREFIX.length + 1); + } + if (requestPath.startsWith("/")) { + return requestPath.slice(1); + } + return requestPath; +} + +async function relayUpstreamResponse(upstreamResponse, res) { + const contentType = upstreamResponse.headers.get("content-type") || ""; + + if (contentType.includes("application/json")) { + const payload = await upstreamResponse.json(); + res.status(upstreamResponse.status).json(payload); + return; + } + + const payload = await upstreamResponse.text(); + res + .status(upstreamResponse.status) + .type(contentType || "text/plain") + .send(payload); +} + +// Routes +app.get(withApiPrefix("/session/token"), async (req, res) => { + console.log("[API] GET /session/token"); + try { + const upstreamResponse = await proxyRequest({ + path: "/session/token", + method: "GET", + useSearchApiPath: false, + }); + const token = await upstreamResponse.text(); + res.status(upstreamResponse.status).send(token); + } catch (error) { + console.error("[API] Error:", error.message); + res.status(500).json({ error: error.message }); + } +}); + +app.get( + withApiPrefix("/webdir-profiles/faculty-staff/filtered"), + async (req, res) => { + console.log("[API] GET /webdir-profiles/faculty-staff/filtered", req.query); + try { + const upstreamResponse = await proxyRequest({ + path: "webdir-profiles/faculty-staff/filtered", + method: "GET", + query: req.query, + }); + const response = await upstreamResponse.json(); + res.status(upstreamResponse.status).json(response); + } catch (error) { + console.error("[API] Error:", error.message); + res.status(500).json({ error: error.message }); + } + } +); + +app.get(withApiPrefix("/webdir-profiles/*"), async (req, res) => { + console.log("[API] GET", req.path, req.query); + try { + const upstreamResponse = await proxyRequest({ + path: toUpstreamPath(req.path), + method: "GET", + query: req.query, + }); + await relayUpstreamResponse(upstreamResponse, res); + } catch (error) { + console.error("[API] Error:", error.message); + res.status(500).json({ error: error.message }); + } +}); + +app.post(withApiPrefix("/webdir-profiles/department"), async (req, res) => { + console.log("[API] POST /webdir-profiles/department", req.body); + try { + const upstreamResponse = await proxyRequest({ + path: "webdir-profiles/department", + method: "POST", + query: req.query, + body: req.body, + headers: { + "X-CSRF-Token": req.headers["x-csrf-token"] || "", + }, + }); + const response = await upstreamResponse.json(); + res.status(upstreamResponse.status).json(response); + } catch (error) { + console.error("[API] Error:", error.message); + res.status(500).json({ error: error.message }); + } +}); + +app.post(withApiPrefix("/webdir-profiles/*"), async (req, res) => { + console.log("[API] POST", req.path, req.body); + try { + const upstreamResponse = await proxyRequest({ + path: toUpstreamPath(req.path), + method: "POST", + query: req.query, + body: req.body, + headers: { + "X-CSRF-Token": req.headers["x-csrf-token"] || "", + }, + }); + await relayUpstreamResponse(upstreamResponse, res); + } catch (error) { + console.error("[API] Error:", error.message); + res.status(500).json({ error: error.message }); + } +}); + +// Health check endpoint +app.get("/health", (req, res) => { + res.json({ status: "ok", timestamp: new Date().toISOString() }); +}); + +// Start server +app.listen(PORT, () => { + console.log( + `\n[Web Directory API Server] Running on http://localhost:${PORT}` + ); + console.log( + `[Web Directory API Server] Proxy target: ${API_ORIGIN}${SEARCH_API_PATH}\n` + ); +}); + +// Graceful shutdown +process.on("SIGTERM", () => { + console.log("\n[Web Directory API Server] Shutting down...\n"); + process.exit(0); +}); diff --git a/packages/app-webdir-ui/src/FacultyRankComponent/index.stories.js b/packages/app-webdir-ui/src/FacultyRankComponent/index.stories.js index 1ac6a36a0f..6bf0d9d58c 100644 --- a/packages/app-webdir-ui/src/FacultyRankComponent/index.stories.js +++ b/packages/app-webdir-ui/src/FacultyRankComponent/index.stories.js @@ -2,11 +2,12 @@ import React from "react"; import { FullLayout } from "@asu/shared"; import { WebDirectory } from "../WebDirectoryComponent/index"; -import { webDirectoryHandlers } from "../helpers/webDirectoryMockHandlers"; + +const API_URL = import.meta.env.VITE_API_URL; +const searchApiVersion = import.meta.env.VITE_SEARCH_API_VERSION; export default { title: "Organisms/Web Directory/Templates", - parameters: { msw: { handlers: webDirectoryHandlers } }, decorators: [story => {story()}], }; @@ -37,8 +38,8 @@ export const FacultyRankWebDirectory = args => { {story()}], @@ -12,8 +15,8 @@ export default { export const searchPageExample = () => (
      diff --git a/packages/app-webdir-ui/src/WebDirectoryComponent/index.stories.js b/packages/app-webdir-ui/src/WebDirectoryComponent/index.stories.js index 85f9df48b6..43e38d550e 100644 --- a/packages/app-webdir-ui/src/WebDirectoryComponent/index.stories.js +++ b/packages/app-webdir-ui/src/WebDirectoryComponent/index.stories.js @@ -3,7 +3,9 @@ import React from "react"; import { WebDirectory } from "./index"; import { FullLayout } from "@asu/shared"; -import { webDirectoryHandlers } from "../helpers/webDirectoryMockHandlers"; + +const API_URL = import.meta.env.VITE_API_URL; +const searchApiVersion = import.meta.env.VITE_SEARCH_API_VERSION; export default { title: "Organisms/Web Directory/Templates", @@ -19,7 +21,6 @@ export default { }, }, args: { alphaFilter: "false" }, - parameters: { msw: { handlers: webDirectoryHandlers } }, decorators: [story => {story()}], }; @@ -52,8 +53,8 @@ export const webDirectoryExampleDepartments = args => { {
      ); }; +webDirectoryExampleDepartments.args = { + alphaFilter: "true", +}; export const webDirectoryExamplePeople = args => { return ( @@ -73,8 +77,8 @@ export const webDirectoryExamplePeople = args => { { {
      ); }; +webDirectoryExampleDepartmentsAndPeople.args = { + alphaFilter: "true", +}; diff --git a/packages/app-webdir-ui/src/helpers/webDirectoryMockHandlers.js b/packages/app-webdir-ui/src/helpers/webDirectoryMockHandlers.js deleted file mode 100644 index e4f75758d9..0000000000 --- a/packages/app-webdir-ui/src/helpers/webDirectoryMockHandlers.js +++ /dev/null @@ -1,97 +0,0 @@ -// @ts-check -import { http, HttpResponse } from "msw"; - -import { - getMockProfileByAsuriteId, - mockDirectoryProfiles, -} from "./webDirectoryMockProfiles"; - -const MOCK_CSRF_TOKEN = "mock-csrf-token"; - -/** - * Paginates the mock profile pool the same way the real - * `webdir-profiles/faculty-staff/filtered` endpoint would, and wraps it in - * the `{ meta: { page }, results }` envelope expected by `helpers/search.js`. - * When a `rank_group` param is present (used by `FacultyRankComponent`'s - * tabs), only profiles seeded with a matching `rankGroup` are included, so - * each tab shows distinct data instead of the same full pool. - * @param {URL} url - */ -function buildFilteredProfilesResponse(url) { - const page = Number(url.searchParams.get("page")) || 1; - const size = Number(url.searchParams.get("size")) || 6; - const rankGroup = url.searchParams.get("rank_group"); - const pool = rankGroup - ? mockDirectoryProfiles.filter( - profile => profile.rank_group.raw === rankGroup - ) - : mockDirectoryProfiles; - const start = (page - 1) * size; - const results = pool.slice(start, start + size); - - return { - meta: { - request_id: "mock-request-id", - page: { - current: page, - total_pages: Math.ceil(pool.length / size), - total_results: pool.length, - size, - }, - }, - results, - }; -} - -/** - * Handles the POST `webdir-profiles/department` endpoint, which is used both - * for the "people"/"people_departments" Web Directory search (`full_records: - * true`) and for enriching GET results with title/department info - * (`full_records: false`). - * @param {{ full_records?: boolean, profiles?: { asurite_id: string, dept_id?: string }[] }} body - */ -function buildDepartmentProfilesResponse(body) { - const profiles = body.profiles || []; - - if (!body.full_records) { - return profiles.map(profile => { - const mock = getMockProfileByAsuriteId(profile.asurite_id); - return { - title: mock.titles.raw, - dept_name: mock.departments.raw[0], - }; - }); - } - - return profiles.map((profile, index) => { - const fullRecord = getMockProfileByAsuriteId(profile.asurite_id, index); - return { - asurite_id: profile.asurite_id, - dept_id: profile.dept_id, - ...(index === 0 ? { total_results: profiles.length } : {}), - full_record: fullRecord, - }; - }); -} - -/** - * MSW request handlers for the Web Directory endpoints hit by - * `WebDirectoryComponent` and `FacultyRankComponent` stories. Registered via - * each story's `parameters.msw.handlers`. - */ -export const webDirectoryHandlers = [ - http.get("*/session/token", () => HttpResponse.text(MOCK_CSRF_TOKEN)), - - http.get("*/webdir-profiles/faculty-staff/filtered", ({ request }) => { - const url = new URL(request.url); - return HttpResponse.json(buildFilteredProfilesResponse(url)); - }), - - http.post("*/webdir-profiles/department", async ({ request }) => { - const body = - /** @type {{ full_records?: boolean, profiles?: { asurite_id: string, dept_id?: string }[] }} */ ( - await request.json() - ); - return HttpResponse.json(buildDepartmentProfilesResponse(body)); - }), -]; diff --git a/packages/app-webdir-ui/src/helpers/webDirectoryMockProfiles.js b/packages/app-webdir-ui/src/helpers/webDirectoryMockProfiles.js deleted file mode 100644 index 99720010c5..0000000000 --- a/packages/app-webdir-ui/src/helpers/webDirectoryMockProfiles.js +++ /dev/null @@ -1,256 +0,0 @@ -// @ts-check -/** - * Mock profile "seed" data used to build fake Web Directory API responses for - * Storybook (via msw). The real `webdir-profiles/*` endpoints block requests - * coming from localhost, so these fixtures let the Web Directory stories keep - * working without hitting the live API. - */ - -/** - * @typedef {Object} MockProfileSeed - * @property {string} asuriteId - * @property {string} eid - * @property {string} deptId - * @property {string} displayName - * @property {string} firstName - * @property {string} lastName - * @property {string[]} titles - * @property {string} deptName - * @property {string} email - * @property {string} phone - * @property {"1"|"2"|"3"|null} [rankGroup] - Matches `FacultyRankComponent`'s - * `rank_group` filter (1=Faculty, 2=Academic Professionals, 3=Other), or - * `null`/omitted for profiles outside those groups. - */ - -/** @type {MockProfileSeed[]} */ -const mockProfileSeeds = [ - { - asuriteId: "mcrow", - eid: "454517", - deptId: "1350", - displayName: "Michael Crow", - firstName: "Michael", - lastName: "Crow", - titles: ["President"], - deptName: "Office of the President", - email: "michael.crow@asu.edu", - phone: "480/965-1234", - // Not a Faculty/Academic Professional rank group; excluded from Faculty Rank tabs. - rankGroup: null, - }, - { - asuriteId: "mdenke", - eid: "1350001", - deptId: "1350", - displayName: "Morgan Denke", - firstName: "Morgan", - lastName: "Denke", - titles: ["Associate Professor"], - deptName: "School of Life Sciences", - email: "morgan.denke@asu.edu", - phone: "480/965-2345", - rankGroup: "1", // Faculty - }, - { - asuriteId: "jagarc50", - eid: "1350002", - deptId: "1350", - displayName: "Javier Garcia", - firstName: "Javier", - lastName: "Garcia", - titles: ["Lecturer"], - deptName: "School of Life Sciences", - email: "javier.garcia@asu.edu", - phone: "480/965-3456", - rankGroup: "1", // Faculty - }, - { - asuriteId: "lhillzev", - eid: "1353001", - deptId: "1353", - displayName: "Lena Hillzev", - firstName: "Lena", - lastName: "Hillzev", - titles: ["Administrative Assistant"], - deptName: "College of Health Solutions", - email: "lena.hillzev@asu.edu", - phone: "480/965-4567", - rankGroup: "3", // Other Faculty and Academic Professionals - }, - { - asuriteId: "tgrandli", - eid: "1344001", - deptId: "1344", - displayName: "Taylor Grandli", - firstName: "Taylor", - lastName: "Grandli", - titles: ["Academic Advisor"], - deptName: "W. P. Carey School of Business", - email: "taylor.grandli@asu.edu", - phone: "480/965-5678", - rankGroup: "2", // Academic Professionals - }, - { - asuriteId: "jcunnin8", - eid: "1358001", - deptId: "1358", - displayName: "Jordan Cunningham", - firstName: "Jordan", - lastName: "Cunningham", - titles: ["Professor"], - deptName: "Ira A. Fulton Schools of Engineering", - email: "jordan.cunningham@asu.edu", - phone: "480/965-6789", - rankGroup: "1", // Faculty - }, - { - asuriteId: "ccherrer", - eid: "1358002", - deptId: "1358", - displayName: "Casey Cherrer", - firstName: "Casey", - lastName: "Cherrer", - titles: ["Assistant Professor"], - deptName: "Ira A. Fulton Schools of Engineering", - email: "casey.cherrer@asu.edu", - phone: "480/965-7890", - rankGroup: "1", // Faculty - }, - { - asuriteId: "csmudde", - eid: "1358003", - deptId: "1358", - displayName: "Charlie Smudde", - firstName: "Charlie", - lastName: "Smudde", - titles: ["Research Scientist"], - deptName: "Ira A. Fulton Schools of Engineering", - email: "charlie.smudde@asu.edu", - phone: "480/965-8901", - rankGroup: "2", // Academic Professionals - }, - { - asuriteId: "abarnett", - eid: "1349001", - deptId: "1349", - displayName: "Alex Barnett", - firstName: "Alex", - lastName: "Barnett", - titles: ["Faculty"], - deptName: "College of Global Futures", - email: "alex.barnett@asu.edu", - phone: "480/965-9012", - rankGroup: "3", // Other Faculty and Academic Professionals - }, - { - asuriteId: "rjmoreno", - eid: "1518001", - deptId: "1518", - displayName: "Riley Moreno", - firstName: "Riley", - lastName: "Moreno", - titles: ["Staff"], - deptName: "Herberger Institute for Design and the Arts", - email: "riley.moreno@asu.edu", - phone: "480/965-0123", - rankGroup: null, - }, - { - asuriteId: "spatel12", - eid: "1520001", - deptId: "1520", - displayName: "Sam Patel", - firstName: "Sam", - lastName: "Patel", - titles: ["Professor"], - deptName: "Watts College of Public Service and Community Solutions", - email: "sam.patel@asu.edu", - phone: "480/965-1122", - rankGroup: "1", // Faculty - }, - { - asuriteId: "ekowalsk", - eid: "3534001", - deptId: "3534", - displayName: "Emerson Kowalski", - firstName: "Emerson", - lastName: "Kowalski", - titles: ["Associate Dean"], - deptName: "Mary Lou Fulton Teachers College", - email: "emerson.kowalski@asu.edu", - phone: "480/965-2233", - rankGroup: "2", // Academic Professionals - }, -]; - -/** - * Builds a Web Directory search-API-shaped raw record from a mock profile seed. - * @param {MockProfileSeed} seed - * @param {number} index - * @returns {Record} - */ -function toRawProfile(seed, index) { - return { - id: { raw: `mock-${seed.asuriteId}` }, - asurite_id: { raw: seed.asuriteId }, - eid: { raw: seed.eid }, - deptids: { raw: [seed.deptId] }, - display_name: { raw: seed.displayName }, - first_name: { raw: seed.firstName }, - last_name: { raw: seed.lastName }, - titles: { raw: seed.titles }, - departments: { raw: [seed.deptName] }, - email_address: { raw: seed.email }, - phone: { raw: seed.phone }, - rank_group: { raw: seed.rankGroup ?? null }, - campus_address: { raw: "ASU Tempe Campus" }, - city: { raw: "Tempe" }, - state: { raw: "AZ" }, - photo_url: { - raw: `https://source.unsplash.com/random/400x400?sig=${index}`, - }, - bio: { raw: `${seed.displayName} is a member of ${seed.deptName}.` }, - short_bio: { raw: `${seed.titles[0]} in ${seed.deptName}` }, - facebook: { raw: null }, - linkedin: { raw: null }, - twitter: { raw: null }, - website: { raw: "" }, - _meta: { - engine: "web-dir-faculty-staff", - score: 5, - id: `mock-${seed.asuriteId}`, - }, - }; -} - -export const mockDirectoryProfiles = mockProfileSeeds.map(toRawProfile); - -/** - * Looks up (or falls back to generating) a mock raw profile for the given - * asurite ID, so POST payloads referencing arbitrary IDs still resolve. - * @param {string} asuriteId - * @param {number} index - * @returns {Record} - */ -export function getMockProfileByAsuriteId(asuriteId, index = 0) { - const found = mockProfileSeeds.find(seed => seed.asuriteId === asuriteId); - if (found) { - return toRawProfile(found, index); - } - return toRawProfile( - { - asuriteId, - eid: "0", - deptId: "1350", - displayName: asuriteId, - firstName: asuriteId, - lastName: "", - titles: ["Staff"], - deptName: "Arizona State University", - email: `${asuriteId}@asu.edu`, - phone: "480/965-0000", - }, - index - ); -} diff --git a/yarn.lock b/yarn.lock index 9758a3d106..9d9655b715 100644 --- a/yarn.lock +++ b/yarn.lock @@ -186,6 +186,7 @@ __metadata: "@vitejs/plugin-react": "npm:^4.3.1" axios: "npm:~1.16.0" babel-loader: "npm:^8.2.2" + concurrently: "npm:^10.0.3" copy-webpack-plugin: "npm:^9.0.1" css-loader: "npm:^5.2.4" dotenv-webpack: "npm:^7.0.3" @@ -204,7 +205,6 @@ __metadata: jsdoc-ts-utils: "npm:^2.0.1" jsdom-screenshot: "npm:^4.0.0" msw: "npm:^2.7.0" - msw-storybook-addon: "npm:^2.0.0" postcss-loader: "npm:^6.1.1" prop-types: "npm:^15.7.2" raw-loader: "npm:^4.0.2" @@ -11898,6 +11898,13 @@ __metadata: languageName: node linkType: hard +"chalk@npm:5.6.2, chalk@npm:^5.2.0, chalk@npm:^5.3.0, chalk@npm:^5.4.1, chalk@npm:^5.6.2": + version: 5.6.2 + resolution: "chalk@npm:5.6.2" + checksum: 10c0/99a4b0f0e7991796b1e7e3f52dceb9137cae2a9dfc8fc0784a550dc4c558e15ab32ed70b14b21b52beb2679b4892b41a0aa44249bcb996f01e125d58477c6976 + languageName: node + linkType: hard + "chalk@npm:^1.1.3": version: 1.1.3 resolution: "chalk@npm:1.1.3" @@ -11942,13 +11949,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.2.0, chalk@npm:^5.3.0, chalk@npm:^5.4.1, chalk@npm:^5.6.2": - version: 5.6.2 - resolution: "chalk@npm:5.6.2" - checksum: 10c0/99a4b0f0e7991796b1e7e3f52dceb9137cae2a9dfc8fc0784a550dc4c558e15ab32ed70b14b21b52beb2679b4892b41a0aa44249bcb996f01e125d58477c6976 - languageName: node - linkType: hard - "change-case@npm:^3.1.0": version: 3.1.0 resolution: "change-case@npm:3.1.0" @@ -12698,6 +12698,23 @@ __metadata: languageName: node linkType: hard +"concurrently@npm:^10.0.3": + version: 10.0.3 + resolution: "concurrently@npm:10.0.3" + dependencies: + chalk: "npm:5.6.2" + rxjs: "npm:7.8.2" + shell-quote: "npm:1.8.4" + supports-color: "npm:10.2.2" + tree-kill: "npm:1.2.2" + yargs: "npm:18.0.0" + bin: + conc: dist/bin/index.js + concurrently: dist/bin/index.js + checksum: 10c0/59a4d9a7946fdbfbfa380543e5e5a40eb5b993cde18862126e8c7fe117813ebd26d67aa67b64781543c0e357eb23fb551f303b622e439354398ff0d7d71735b6 + languageName: node + linkType: hard + "concurrently@npm:^6.4.0": version: 6.5.1 resolution: "concurrently@npm:6.5.1" @@ -27527,6 +27544,15 @@ __metadata: languageName: node linkType: hard +"rxjs@npm:7.8.2, rxjs@npm:^7.5.5, rxjs@npm:^7.8.1": + version: 7.8.2 + resolution: "rxjs@npm:7.8.2" + dependencies: + tslib: "npm:^2.1.0" + checksum: 10c0/1fcd33d2066ada98ba8f21fcbbcaee9f0b271de1d38dc7f4e256bfbc6ffcdde68c8bfb69093de7eeb46f24b1fb820620bf0223706cff26b4ab99a7ff7b2e2c45 + languageName: node + linkType: hard + "rxjs@npm:^6.4.0, rxjs@npm:^6.6.0, rxjs@npm:^6.6.2, rxjs@npm:^6.6.3": version: 6.6.7 resolution: "rxjs@npm:6.6.7" @@ -27536,15 +27562,6 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^7.5.5, rxjs@npm:^7.8.1": - version: 7.8.2 - resolution: "rxjs@npm:7.8.2" - dependencies: - tslib: "npm:^2.1.0" - checksum: 10c0/1fcd33d2066ada98ba8f21fcbbcaee9f0b271de1d38dc7f4e256bfbc6ffcdde68c8bfb69093de7eeb46f24b1fb820620bf0223706cff26b4ab99a7ff7b2e2c45 - languageName: node - linkType: hard - "safe-array-concat@npm:^1.1.3": version: 1.1.4 resolution: "safe-array-concat@npm:1.1.4" @@ -28114,6 +28131,13 @@ __metadata: languageName: node linkType: hard +"shell-quote@npm:1.8.4": + version: 1.8.4 + resolution: "shell-quote@npm:1.8.4" + checksum: 10c0/86c93678bc394cb81f5ddcdc87df9c95d279ef9652775cd1cd1eed361404169a8d8cbaacaeed232ab09919e36ee1e5363863570390d78571f8c22b7f6312fb40 + languageName: node + linkType: hard + "shell-quote@npm:^1.6.1": version: 1.8.3 resolution: "shell-quote@npm:1.8.3" @@ -29409,7 +29433,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^10.2.2": +"supports-color@npm:10.2.2, supports-color@npm:^10.2.2": version: 10.2.2 resolution: "supports-color@npm:10.2.2" checksum: 10c0/fb28dd7e0cdf80afb3f2a41df5e068d60c8b4f97f7140de2eaed5b42e075d82a0e980b20a2c0efd2b6d73cfacb55555285d8cc719fa0472220715aefeaa1da7c @@ -30159,7 +30183,7 @@ __metadata: languageName: node linkType: hard -"tree-kill@npm:^1.2.2": +"tree-kill@npm:1.2.2, tree-kill@npm:^1.2.2": version: 1.2.2 resolution: "tree-kill@npm:1.2.2" bin: @@ -32359,6 +32383,20 @@ __metadata: languageName: node linkType: hard +"yargs@npm:18.0.0, yargs@npm:^18.0.0": + version: 18.0.0 + resolution: "yargs@npm:18.0.0" + dependencies: + cliui: "npm:^9.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + string-width: "npm:^7.2.0" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^22.0.0" + checksum: 10c0/bf290e4723876ea9c638c786a5c42ac28e03c9ca2325e1424bf43b94e5876456292d3ed905b853ebbba6daf43ed29e772ac2a6b3c5fb1b16533245d6211778f3 + languageName: node + linkType: hard + "yargs@npm:^15.0.2, yargs@npm:^15.4.1": version: 15.4.1 resolution: "yargs@npm:15.4.1" @@ -32393,20 +32431,6 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^18.0.0": - version: 18.0.0 - resolution: "yargs@npm:18.0.0" - dependencies: - cliui: "npm:^9.0.1" - escalade: "npm:^3.1.1" - get-caller-file: "npm:^2.0.5" - string-width: "npm:^7.2.0" - y18n: "npm:^5.0.5" - yargs-parser: "npm:^22.0.0" - checksum: 10c0/bf290e4723876ea9c638c786a5c42ac28e03c9ca2325e1424bf43b94e5876456292d3ed905b853ebbba6daf43ed29e772ac2a6b3c5fb1b16533245d6211778f3 - languageName: node - linkType: hard - "yauzl@npm:^2.10.0": version: 2.10.0 resolution: "yauzl@npm:2.10.0" From de8861e313cc81cf430945bcdeb077d8f90b93f8 Mon Sep 17 00:00:00 2001 From: Scott Williams <5209283+scott-williams-az@users.noreply.github.com> Date: Mon, 27 Jul 2026 23:05:21 -0700 Subject: [PATCH 14/42] chore(app-webdir-ui): environment files were not included --- .gitignore | 3 +++ packages/app-webdir-ui/.env.development | 11 +++++++++++ packages/app-webdir-ui/.env.example | 13 +++++++++++++ packages/app-webdir-ui/.env.production | 5 +++++ packages/app-webdir-ui/.storybook/main.js | 2 +- 5 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 packages/app-webdir-ui/.env.development create mode 100644 packages/app-webdir-ui/.env.example create mode 100644 packages/app-webdir-ui/.env.production diff --git a/.gitignore b/.gitignore index 73e7161059..dde73f6e9f 100644 --- a/.gitignore +++ b/.gitignore @@ -122,6 +122,9 @@ tests/__image_snapshots__/__diff_output__ !.yarn/versions *.env.* !.env.yarn.example +!.env.example +!.env.development +!.env.production # --- Adversarial coding pipeline (project-local .kiro) --- # Committed: .kiro/agents, .kiro/skills, .kiro/hooks, .kiro/scripts, diff --git a/packages/app-webdir-ui/.env.development b/packages/app-webdir-ui/.env.development new file mode 100644 index 0000000000..46db4d9b82 --- /dev/null +++ b/packages/app-webdir-ui/.env.development @@ -0,0 +1,11 @@ +# Web Directory UI - Development Environment Configuration +# +# This file is loaded by Vite when running `yarn storybook:dev` +# It enables the backend API proxy for real data testing in Storybook + +# Local proxy server port used by `node server.js` +PORT=3000 + +# Search engine request base URL + version used by Storybook stories +VITE_API_URL=http://localhost:${PORT}/ +VITE_SEARCH_API_VERSION=api/v1/ diff --git a/packages/app-webdir-ui/.env.example b/packages/app-webdir-ui/.env.example new file mode 100644 index 0000000000..88fd025020 --- /dev/null +++ b/packages/app-webdir-ui/.env.example @@ -0,0 +1,13 @@ +# Web Directory UI - Environment Configuration Template +# +# Copy this file to: +# - .env.development (git-tracked, shared dev defaults) +# - .env.local (git-ignored, personal overrides) + +# Local proxy server port used by `node server.js` +# PORT=3000 + +# API_URL + searchApiVersion used by stories/components +# Default base URL + version pair: +# VITE_API_URL=http://localhost:${PORT}/ +# VITE_SEARCH_API_VERSION=/api/v1/ diff --git a/packages/app-webdir-ui/.env.production b/packages/app-webdir-ui/.env.production new file mode 100644 index 0000000000..f34ae92331 --- /dev/null +++ b/packages/app-webdir-ui/.env.production @@ -0,0 +1,5 @@ +# Web Directory UI - Storybook build environment +# Loaded by the build-storybook script for deterministic static build config. + +VITE_API_URL=https://asuapp2dev.prod.acquia-sites.com/ +VITE_SEARCH_API_VERSION=api/v1/ diff --git a/packages/app-webdir-ui/.storybook/main.js b/packages/app-webdir-ui/.storybook/main.js index 23e70f33a1..531618621c 100644 --- a/packages/app-webdir-ui/.storybook/main.js +++ b/packages/app-webdir-ui/.storybook/main.js @@ -38,7 +38,7 @@ function loadEnvFile(filePath) { function loadStorybookEnv(configType) { const envFileName = - configType === "PRODUCTION" ? ".env.storybook-build" : ".env.development"; + configType === "PRODUCTION" ? ".env.production" : ".env.development"; loadEnvFile(resolve(packageRoot, envFileName)); } From 22c05380657e7c94431e8d997991a17d15a7109e Mon Sep 17 00:00:00 2001 From: Scott Williams <5209283+scott-williams-az@users.noreply.github.com> Date: Tue, 28 Jul 2026 08:45:55 -0700 Subject: [PATCH 15/42] chore(app-webdir-ui): removed long paths in favor of wildcard paths --- packages/app-webdir-ui/server.js | 43 ++------------------------------ 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/packages/app-webdir-ui/server.js b/packages/app-webdir-ui/server.js index 4d2e8e8060..246e00d70c 100644 --- a/packages/app-webdir-ui/server.js +++ b/packages/app-webdir-ui/server.js @@ -4,8 +4,8 @@ * * Endpoints: * - GET /session/token - * - GET /webdir-profiles/faculty-staff/filtered - * - POST /webdir-profiles/department + * - GET /webdir-profiles/* + * - POST /webdir-profiles/* */ const express = require("express"); @@ -123,25 +123,6 @@ app.get(withApiPrefix("/session/token"), async (req, res) => { } }); -app.get( - withApiPrefix("/webdir-profiles/faculty-staff/filtered"), - async (req, res) => { - console.log("[API] GET /webdir-profiles/faculty-staff/filtered", req.query); - try { - const upstreamResponse = await proxyRequest({ - path: "webdir-profiles/faculty-staff/filtered", - method: "GET", - query: req.query, - }); - const response = await upstreamResponse.json(); - res.status(upstreamResponse.status).json(response); - } catch (error) { - console.error("[API] Error:", error.message); - res.status(500).json({ error: error.message }); - } - } -); - app.get(withApiPrefix("/webdir-profiles/*"), async (req, res) => { console.log("[API] GET", req.path, req.query); try { @@ -157,26 +138,6 @@ app.get(withApiPrefix("/webdir-profiles/*"), async (req, res) => { } }); -app.post(withApiPrefix("/webdir-profiles/department"), async (req, res) => { - console.log("[API] POST /webdir-profiles/department", req.body); - try { - const upstreamResponse = await proxyRequest({ - path: "webdir-profiles/department", - method: "POST", - query: req.query, - body: req.body, - headers: { - "X-CSRF-Token": req.headers["x-csrf-token"] || "", - }, - }); - const response = await upstreamResponse.json(); - res.status(upstreamResponse.status).json(response); - } catch (error) { - console.error("[API] Error:", error.message); - res.status(500).json({ error: error.message }); - } -}); - app.post(withApiPrefix("/webdir-profiles/*"), async (req, res) => { console.log("[API] POST", req.path, req.body); try { From 97b6c26ed719b55bbd60819845f5ed3eb2d9db7e Mon Sep 17 00:00:00 2001 From: Scott Williams <5209283+scott-williams-az@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:08:47 -0700 Subject: [PATCH 16/42] chore(app-webdir-ui): update readme --- packages/app-webdir-ui/README.md | 175 +++++++++++++++++++++---------- 1 file changed, 122 insertions(+), 53 deletions(-) diff --git a/packages/app-webdir-ui/README.md b/packages/app-webdir-ui/README.md index 11a51c2a9d..ba70946532 100644 --- a/packages/app-webdir-ui/README.md +++ b/packages/app-webdir-ui/README.md @@ -2,70 +2,139 @@ This package is intended to provde the components needed for Search, the designs for which can be found [here](https://xd.adobe.com/view/41641639-f009-41e2-802c-6859906edb2c-1437/grid/). -In practice, only two components will ever be used, to wit `SearchPage` and `WebDirectoryComponent`, the rest are to be used within those components. -Let's take them one at a time. +In practice, only two components of this package are intended for use `SearchPage` and `WebDirectoryComponent`, the rest are to be used within those components. + +## Shared Props +Used by `SearchPage` and `WebDirectoryComponent` + +| Prop Name | Type | Description | Default Value | +|-----------|------|-------------|---------------| +| `API_URL` | string | URL endpoint for searching. | - | +| `searchApiVersion` | string | API version string for constructing the API URL. | - | false | + +**Full API URL format:** + +`{{API_URL}}{{searchApiVersion}}endpoint/path/with?parameters`. + +**Example result:** + +`https://asuapp2dev.prod.acquia-sites.com/api/v1/webdir-profiles/faculty-staff` + ## `SearchPage` -The `SearchPage` component can take two props, `searchURL` and `loggedIn`. -- `searchURL` is a string that tells the component where to search. The endpoint names -will be appended to this. For example, by providing the URL "https://dev-asu-isearch.ws.asu.edu/api/v1/, the staff data will be found at https://dev-asu-isearch.ws.asu.edu/api/v1/webdir-profiles/faculty-staff. -- `loggedIn` is a boolean that tells the component if the user is currently logged in. In -practice this simply controls whether the admin buttons are displayed. +The `SearchPage` component has 3 props: `API_URL`, `searchApiVersion`, and `loggedIn`. + +| Prop Name | Type | Description | Default Value | +|-----------|------|-------------|---------------| +| `API_URL` | string | See [Shared Props](#shared-props) section| - | +| `searchApiVersion` | string | See [Shared Props](#shared-props) section| - | +| `loggedIn` | boolean | Indicates whether the user is currently logged in. Controls whether admin buttons are displayed. | false | -To see an example of how to use this page, take a look at `examples/index.html`. -You can also see the storybook example at `src/SearchPage/index.stories.js`. +To see an example of how to use this page, take a look at the storybook example at `src/SearchPage/index.stories.js`. ## `WebDirectoryComponent` -This component is a bit more complicated. It's props, `searchType`, `ids`, and `searchURL`, dictate which of three different scenarios is active. -- `searchType` can be one of four different values: - - departments - - people - - people_departments - - faculty_rank -- `ids` is either a lit of comma-separated ids (when `searchType` is 'departments) or -a list of objects containing `asuriteid` and `dept` pairs (when `searchType` is 'people' or 'people_departments') -- `searchURL` is a string that tells the component where to search. The endpoint names -will be appended to this. For example, by providing the URL "https://dev-asu-isearch.ws.asu.edu/api/v1/, the data will be found at https://dev-asu-isearch.ws.asu.edu/api/v1/webdir-departments/profiles. +The `WebDirectoryComponent` supports multiple display scenarios based on `searchType` and ID inputs. -Here's some examples of the props: +| Prop Name | Type | Description | Default Value | +|-----------|------|-------------|---------------| +| `API_URL` | string | See [Shared Props](#shared-props) section| - | +| `searchApiVersion` | string | See [Shared Props](#shared-props) section| - | +| `searchType` | string | Search mode. Supported values: `departments`, `faculty_rank`, `people`, `people_departments`. | - | +| `ids` | string | Comma-separated IDs. For `people` and `people_departments`, pass pairs in `asurite:dept` format (example: `jdoe:1234,asmith:5678`). | - | +| `deptIds` | string | Comma-separated department IDs. Used by `departments` and `faculty_rank`. | - | +| `profileURLBase` | string | Base URL used for profile links. | `https://search.asu.edu` | +| `appPathFolder` | string | Optional app path folder used in generated links. | - | +| `display` | object | Display config (pager, sort, profiles per page, grid mode, profile exclusions). | - | +| `filters` | object | Filters sent with requests (employee, expertise, title, campuses). | - | +| `alphaFilter` | string | Enables last-initial filter UI when set to `"true"`. | `"false"` | +Examples of props: + +```javascript +// Scenario 1 - Display Web Directory for departments +{ + searchType: "departments", + deptIds: "1457,1374", + ids: "", + API_URL: "https://asuapp2dev.prod.acquia-sites.com/", + searchApiVersion: "api/v1/", + filters: {}, +} ``` -Scenario 1 - Display Web Directory for departments: - webdirUI.initWebDirectory({ - targetSelector: "#searchPageContainer", - props: { - searchType: 'departments', - ids: ['1457', '1374'], - searchURL="https://dev-asu-isearch.ws.asu.edu/api/v1/" - filters: {...}, - }, - }); -``` + +```javascript +// Scenario 2 - Display Web Directory for people +{ + searchType: "people", + ids: "jdoe:1457,asmith:1374", + deptIds: "", + API_URL: "https://asuapp2dev.prod.acquia-sites.com/", + searchApiVersion: "api/v1/", + filters: {}, +} ``` -Scenario 2 - Display Web Directory for people: - webdirUI.initWebDirectory({ - targetSelector: "#searchPageContainer", - props: { - searchType: 'people', - ids: [{asuriteid: 123, dept: 1457}, {asuriteid: 456, dept: 1374}], - searchURL="https://dev-asu-isearch.ws.asu.edu/api/v1/" - filters: {...}, - }, - }); + +```javascript +// Scenario 3 - Display Web Directory for people and departments +{ + searchType: "people_departments", + ids: "jdoe:1457,asmith:1374", + deptIds: "", + API_URL: "https://asuapp2dev.prod.acquia-sites.com/", + searchApiVersion: "api/v1/", + filters: {}, +} ``` + +```javascript +// Scenario 4 - Departments view with custom display options and employee/title filters +{ + searchType: "departments", + deptIds: "1457,1374", + ids: "", + API_URL: "https://asuapp2dev.prod.acquia-sites.com/", + searchApiVersion: "api/v1/", + display: { + defaultSort: "last_name", + profilesPerPage: "12", + usePager: "1", + grid: "true", + doNotDisplayProfiles: "jdoe,asmith", + }, + filters: { + employee: "1", + title: "Professor", + }, + alphaFilter: "true", +} ``` -Scenario 3 - Display Web Directory for people and departments: - webdirUI.initWebDirectory({ - targetSelector: "#searchPageContainer", - props: { - searchType: 'people_departments', - ids: [{asuriteid: 123, dept: 1457}, {asuriteid: 456, dept: 1374}], - searchURL="https://dev-asu-isearch.ws.asu.edu/api/v1/" - filters: {...}, - }, - }); + +```javascript +// Scenario 5 - Faculty rank view with expertise/campus filters and compact list display +{ + searchType: "faculty_rank", + deptIds: "1457", + ids: "", + API_URL: "https://asuapp2dev.prod.acquia-sites.com/", + searchApiVersion: "api/v1/", + display: { + defaultSort: "people_order", + profilesPerPage: "6", + usePager: "0", + grid: "false", + }, + filters: { + expertise: "Data Science", + campuses: "Tempe", + }, + alphaFilter: "false", +} ``` -To see an example of how to use this page, take a look at `examples/web-directory.html`. -You can also see the storybook example at `src/WebDirectoryComponent/index.stories.js`. +Display behavior note: + +- For `departments`, `people`, and `people_departments`, rendering goes through `ASUSearchResultsList`, so options like `profilesPerPage`, `usePager`, `defaultSort`, and `doNotDisplayProfiles` are applied there. +- For `faculty_rank`, rendering goes through `FacultyRankTabPanels`. Grid mode and filter inputs still apply, but list-specific behavior may differ from non-`faculty_rank` modes. + +To see an example of how to use this page, take a look at the storybook example at `src/WebDirectoryComponent/index.stories.js`. From 98870cb68701bf6e6e8226454654c4b324e228d6 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 28 Jul 2026 19:47:07 +0000 Subject: [PATCH 17/42] chore(release): 2.1.1 [skip ci] # [@asu/unity-react-core-v2.1.1](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-react-core-v2.1.0...@asu/unity-react-core-v2.1.1) (2026-07-28) ### Bug Fixes * **unity-react-core:** restore nav-control styles and stop bundling react-dom/server ([cb41952](https://github.com/ASU/asu-unity-stack/commit/cb41952b835e1da077d6e39333e410105a7ac352)) --- packages/unity-react-core/CHANGELOG.md | 7 +++++++ packages/unity-react-core/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/unity-react-core/CHANGELOG.md b/packages/unity-react-core/CHANGELOG.md index 5bcd21abca..56732eabcb 100644 --- a/packages/unity-react-core/CHANGELOG.md +++ b/packages/unity-react-core/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@asu/unity-react-core-v2.1.1](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-react-core-v2.1.0...@asu/unity-react-core-v2.1.1) (2026-07-28) + + +### Bug Fixes + +* **unity-react-core:** restore nav-control styles and stop bundling react-dom/server ([cb41952](https://github.com/ASU/asu-unity-stack/commit/cb41952b835e1da077d6e39333e410105a7ac352)) + # [@asu/unity-react-core-v2.1.0](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-react-core-v2.0.0...@asu/unity-react-core-v2.1.0) (2026-07-22) diff --git a/packages/unity-react-core/package.json b/packages/unity-react-core/package.json index aa7c62da20..cd4d141b0f 100644 --- a/packages/unity-react-core/package.json +++ b/packages/unity-react-core/package.json @@ -1,6 +1,6 @@ { "name": "@asu/unity-react-core", - "version": "2.1.0", + "version": "2.1.1", "main": "./dist/unityReactCore.umd.js", "module": "./dist/unityReactCore.es.js", "browser": "./dist/unityReactCore.umd.js", From 45b964852b2ea04a87a7870f8890179e2a067d3a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 28 Jul 2026 19:48:09 +0000 Subject: [PATCH 18/42] chore(release): 3.2.3 [skip ci] # [@asu/app-degree-pages-v3.2.3](https://github.com/asu/asu-unity-stack/compare/@asu/app-degree-pages-v3.2.2...@asu/app-degree-pages-v3.2.3) (2026-07-28) ### Bug Fixes * **app-degree-pages:** correct stale @asu/unity-react-core dependency range ([81ce102](https://github.com/asu/asu-unity-stack/commit/81ce102c79ab8f5e299d1bb4d50e38af2cd71a58)) --- packages/app-degree-pages/CHANGELOG.md | 7 +++++++ packages/app-degree-pages/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/app-degree-pages/CHANGELOG.md b/packages/app-degree-pages/CHANGELOG.md index 7d16c85a4b..94d4c868e4 100644 --- a/packages/app-degree-pages/CHANGELOG.md +++ b/packages/app-degree-pages/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@asu/app-degree-pages-v3.2.3](https://github.com/asu/asu-unity-stack/compare/@asu/app-degree-pages-v3.2.2...@asu/app-degree-pages-v3.2.3) (2026-07-28) + + +### Bug Fixes + +* **app-degree-pages:** correct stale @asu/unity-react-core dependency range ([81ce102](https://github.com/asu/asu-unity-stack/commit/81ce102c79ab8f5e299d1bb4d50e38af2cd71a58)) + # [@asu/app-degree-pages-v3.2.2](https://github.com/asu/asu-unity-stack/compare/@asu/app-degree-pages-v3.2.1...@asu/app-degree-pages-v3.2.2) (2026-07-07) diff --git a/packages/app-degree-pages/package.json b/packages/app-degree-pages/package.json index a9fdd9b61f..ebe2e9113f 100644 --- a/packages/app-degree-pages/package.json +++ b/packages/app-degree-pages/package.json @@ -1,6 +1,6 @@ { "name": "@asu/app-degree-pages", - "version": "3.2.2", + "version": "3.2.3", "description": "ASU implementation of degree pages", "main": "./dist/degreePages.cjs.js", "browser": "./dist/degreePages.umd.js", From eea9bef965ddb82a11bbbfbd3f099cced5ac6688 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 28 Jul 2026 19:48:30 +0000 Subject: [PATCH 19/42] chore(release): 3.10.3 [skip ci] # [@asu/app-rfi-v3.10.3](https://github.com/asu/asu-unity-stack/compare/@asu/app-rfi-v3.10.2...@asu/app-rfi-v3.10.3) (2026-07-28) ### Bug Fixes * **app-rfi:** correct stale @asu/unity-bootstrap-theme and @asu/unity-react-core dependency ranges ([7214d47](https://github.com/asu/asu-unity-stack/commit/7214d478609d54a14d9ff30e10363d9015f04a84)) --- packages/app-rfi/CHANGELOG.md | 7 +++++++ packages/app-rfi/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/app-rfi/CHANGELOG.md b/packages/app-rfi/CHANGELOG.md index 4fe580b933..2085cd6933 100644 --- a/packages/app-rfi/CHANGELOG.md +++ b/packages/app-rfi/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@asu/app-rfi-v3.10.3](https://github.com/asu/asu-unity-stack/compare/@asu/app-rfi-v3.10.2...@asu/app-rfi-v3.10.3) (2026-07-28) + + +### Bug Fixes + +* **app-rfi:** correct stale @asu/unity-bootstrap-theme and @asu/unity-react-core dependency ranges ([7214d47](https://github.com/asu/asu-unity-stack/commit/7214d478609d54a14d9ff30e10363d9015f04a84)) + # [@asu/app-rfi-v3.10.2](https://github.com/asu/asu-unity-stack/compare/@asu/app-rfi-v3.10.1...@asu/app-rfi-v3.10.2) (2026-07-01) diff --git a/packages/app-rfi/package.json b/packages/app-rfi/package.json index 8804ff8be1..10d987e010 100644 --- a/packages/app-rfi/package.json +++ b/packages/app-rfi/package.json @@ -1,6 +1,6 @@ { "name": "@asu/app-rfi", - "version": "3.10.2", + "version": "3.10.3", "description": "ASU Request For Information (RFI) form", "main": "./dist/appRfi.cjs.js", "browser": "./dist/appRfi.umd.js", From 811aecf8bf8bcfd59bae773bccea5ddcc4abcebc Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 28 Jul 2026 19:48:52 +0000 Subject: [PATCH 20/42] chore(release): 5.0.17 [skip ci] # [@asu/app-webdir-ui-v5.0.17](https://github.com/asu/asu-unity-stack/compare/@asu/app-webdir-ui-v5.0.16...@asu/app-webdir-ui-v5.0.17) (2026-07-28) ### Bug Fixes * **app-webdir-ui:** fix nav controls and anon image for webdir ([0e68046](https://github.com/asu/asu-unity-stack/commit/0e680469219ebb656f5e4da74130d0e9e1bc1b04)) --- packages/app-webdir-ui/CHANGELOG.md | 7 +++++++ packages/app-webdir-ui/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/app-webdir-ui/CHANGELOG.md b/packages/app-webdir-ui/CHANGELOG.md index d583cde489..1eaba04098 100644 --- a/packages/app-webdir-ui/CHANGELOG.md +++ b/packages/app-webdir-ui/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@asu/app-webdir-ui-v5.0.17](https://github.com/asu/asu-unity-stack/compare/@asu/app-webdir-ui-v5.0.16...@asu/app-webdir-ui-v5.0.17) (2026-07-28) + + +### Bug Fixes + +* **app-webdir-ui:** fix nav controls and anon image for webdir ([0e68046](https://github.com/asu/asu-unity-stack/commit/0e680469219ebb656f5e4da74130d0e9e1bc1b04)) + # [@asu/app-webdir-ui-v5.0.16](https://github.com/asu/asu-unity-stack/compare/@asu/app-webdir-ui-v5.0.15...@asu/app-webdir-ui-v5.0.16) (2026-07-25) diff --git a/packages/app-webdir-ui/package.json b/packages/app-webdir-ui/package.json index 28021f3cc3..e89073556a 100644 --- a/packages/app-webdir-ui/package.json +++ b/packages/app-webdir-ui/package.json @@ -1,6 +1,6 @@ { "name": "@asu/app-webdir-ui", - "version": "5.0.16", + "version": "5.0.17", "description": "App Webdir UI", "main": "./dist/webdirUI.cjs.js", "browser": "./dist/webdirUI.umd.js", From dcc4812c96f632ff9d6156cdbc79d19df5aec363 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 28 Jul 2026 19:49:31 +0000 Subject: [PATCH 21/42] chore(release): 3.2.1 [skip ci] # [@asu/component-events-v3.2.1](https://github.com/asu/asu-unity-stack/compare/@asu/component-events-v3.2.0...@asu/component-events-v3.2.1) (2026-07-28) ### Bug Fixes * **component-events:** correct stale @asu/unity-react-core dependency range ([8b66069](https://github.com/asu/asu-unity-stack/commit/8b660693d5304e76ed98121dd390001352e5e390)) --- packages/component-events/CHANGELOG.md | 7 +++++++ packages/component-events/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/component-events/CHANGELOG.md b/packages/component-events/CHANGELOG.md index 0b32c53944..491514cefa 100644 --- a/packages/component-events/CHANGELOG.md +++ b/packages/component-events/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@asu/component-events-v3.2.1](https://github.com/asu/asu-unity-stack/compare/@asu/component-events-v3.2.0...@asu/component-events-v3.2.1) (2026-07-28) + + +### Bug Fixes + +* **component-events:** correct stale @asu/unity-react-core dependency range ([8b66069](https://github.com/asu/asu-unity-stack/commit/8b660693d5304e76ed98121dd390001352e5e390)) + # [@asu/component-events-v3.2.0](https://github.com/asu/asu-unity-stack/compare/@asu/component-events-v3.1.0...@asu/component-events-v3.2.0) (2026-07-14) diff --git a/packages/component-events/package.json b/packages/component-events/package.json index c341e375a7..a3fbdc7543 100644 --- a/packages/component-events/package.json +++ b/packages/component-events/package.json @@ -1,6 +1,6 @@ { "name": "@asu/component-events", - "version": "3.2.0", + "version": "3.2.1", "description": "ASU Events component", "main": "./dist/asuEvents.cjs.js", "browser": "./dist/asuEvents.umd.js", From 258ed34fb43b53992dbacd0c4b352529ed29efa5 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 28 Jul 2026 19:49:53 +0000 Subject: [PATCH 22/42] chore(release): 4.2.2 [skip ci] # [@asu/component-news-v4.2.2](https://github.com/asu/asu-unity-stack/compare/@asu/component-news-v4.2.1...@asu/component-news-v4.2.2) (2026-07-28) ### Bug Fixes * **component-news:** correct stale @asu/unity-react-core dependency range ([d5f3869](https://github.com/asu/asu-unity-stack/commit/d5f3869740f833f8c35139e5af12fbea4449fb90)) --- packages/component-news/CHANGELOG.md | 7 +++++++ packages/component-news/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/component-news/CHANGELOG.md b/packages/component-news/CHANGELOG.md index 94a448297c..bb5fe093e3 100644 --- a/packages/component-news/CHANGELOG.md +++ b/packages/component-news/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@asu/component-news-v4.2.2](https://github.com/asu/asu-unity-stack/compare/@asu/component-news-v4.2.1...@asu/component-news-v4.2.2) (2026-07-28) + + +### Bug Fixes + +* **component-news:** correct stale @asu/unity-react-core dependency range ([d5f3869](https://github.com/asu/asu-unity-stack/commit/d5f3869740f833f8c35139e5af12fbea4449fb90)) + # [@asu/component-news-v4.2.1](https://github.com/asu/asu-unity-stack/compare/@asu/component-news-v4.2.0...@asu/component-news-v4.2.1) (2026-04-08) diff --git a/packages/component-news/package.json b/packages/component-news/package.json index bc0690cedc..a828fd2cf5 100644 --- a/packages/component-news/package.json +++ b/packages/component-news/package.json @@ -1,6 +1,6 @@ { "name": "@asu/component-news", - "version": "4.2.1", + "version": "4.2.2", "description": "ASU News component", "main": "./dist/asuNews.cjs.js", "browser": "./dist/asuNews.umd.js", From 159fe2512e8c1acf512dc2e6a2f69e69d5093353 Mon Sep 17 00:00:00 2001 From: david ornelas Date: Wed, 29 Jul 2026 13:03:38 -0700 Subject: [PATCH 23/42] fix(unity-bootstrap-theme): fix modal close button focus style issue --- packages/unity-bootstrap-theme/src/scss/extends/_modals.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/unity-bootstrap-theme/src/scss/extends/_modals.scss b/packages/unity-bootstrap-theme/src/scss/extends/_modals.scss index 1e92aa0004..ab3bdf0e2d 100644 --- a/packages/unity-bootstrap-theme/src/scss/extends/_modals.scss +++ b/packages/unity-bootstrap-theme/src/scss/extends/_modals.scss @@ -53,6 +53,11 @@ background-color: $uds-color-background-white; opacity: .7; } + + &:focus, + &:focus-visible { + background-color: $uds-color-background-white; + } } } From 07ad8814810cb946f7e5572f6186e13abc2ef872 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 29 Jul 2026 20:12:13 +0000 Subject: [PATCH 24/42] chore(release): 2.2.0 [skip ci] # [@asu/unity-bootstrap-theme-v2.2.0](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-bootstrap-theme-v2.1.1...@asu/unity-bootstrap-theme-v2.2.0) (2026-07-29) ### Bug Fixes * **unity-bootstrap-theme:** fix modal close button focus style issue ([c288469](https://github.com/ASU/asu-unity-stack/commit/c288469935d3daca33d5d5381c6ea41857304e43)) ### Features * modal accessibility and escape key features for unity react and bootstrap ([04bdeea](https://github.com/ASU/asu-unity-stack/commit/04bdeea87d74c3ab1fd47a7f272504a843129aad)) ### Performance Improvements * **unity-react-core:** useEffect update for React js async key listener handling ([b25ab85](https://github.com/ASU/asu-unity-stack/commit/b25ab851381632a9d825d83c607f8036f9eeb93a)) --- packages/unity-bootstrap-theme/CHANGELOG.md | 17 +++++++++++++++++ packages/unity-bootstrap-theme/package.json | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/unity-bootstrap-theme/CHANGELOG.md b/packages/unity-bootstrap-theme/CHANGELOG.md index 717b73462c..e0e91cb513 100644 --- a/packages/unity-bootstrap-theme/CHANGELOG.md +++ b/packages/unity-bootstrap-theme/CHANGELOG.md @@ -1,3 +1,20 @@ +# [@asu/unity-bootstrap-theme-v2.2.0](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-bootstrap-theme-v2.1.1...@asu/unity-bootstrap-theme-v2.2.0) (2026-07-29) + + +### Bug Fixes + +* **unity-bootstrap-theme:** fix modal close button focus style issue ([c288469](https://github.com/ASU/asu-unity-stack/commit/c288469935d3daca33d5d5381c6ea41857304e43)) + + +### Features + +* modal accessibility and escape key features for unity react and bootstrap ([04bdeea](https://github.com/ASU/asu-unity-stack/commit/04bdeea87d74c3ab1fd47a7f272504a843129aad)) + + +### Performance Improvements + +* **unity-react-core:** useEffect update for React js async key listener handling ([b25ab85](https://github.com/ASU/asu-unity-stack/commit/b25ab851381632a9d825d83c607f8036f9eeb93a)) + # [@asu/unity-bootstrap-theme-v2.1.1](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-bootstrap-theme-v2.1.0...@asu/unity-bootstrap-theme-v2.1.1) (2026-07-22) diff --git a/packages/unity-bootstrap-theme/package.json b/packages/unity-bootstrap-theme/package.json index 4b602f0c51..d353a0cb14 100644 --- a/packages/unity-bootstrap-theme/package.json +++ b/packages/unity-bootstrap-theme/package.json @@ -1,6 +1,6 @@ { "name": "@asu/unity-bootstrap-theme", - "version": "2.1.1", + "version": "2.2.0", "description": "Please see @asu/unity-react-core for up-to-date stories and examples.\nBase UI theme for ASU Web Standards 2.0 developed with Bootstrap 5", "homepage": "https://github.com/ASU/asu-unity-stack#readme", "license": "MIT", From bf3098092c74bb61a58eb8c733dd6da537ef755d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 29 Jul 2026 20:12:46 +0000 Subject: [PATCH 25/42] chore(release): 2.2.0 [skip ci] # [@asu/unity-react-core-v2.2.0](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-react-core-v2.1.1...@asu/unity-react-core-v2.2.0) (2026-07-29) ### Features * modal accessibility and escape key features for unity react and bootstrap ([04bdeea](https://github.com/ASU/asu-unity-stack/commit/04bdeea87d74c3ab1fd47a7f272504a843129aad)) * **unity-react-core:** aria-label update ([eeaebef](https://github.com/ASU/asu-unity-stack/commit/eeaebeff45a85e308ac510f398995b6af708697f)) ### Performance Improvements * **unity-react-core:** useEffect update for React js async key listener handling ([b25ab85](https://github.com/ASU/asu-unity-stack/commit/b25ab851381632a9d825d83c607f8036f9eeb93a)) --- packages/unity-react-core/CHANGELOG.md | 13 +++++++++++++ packages/unity-react-core/package.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/unity-react-core/CHANGELOG.md b/packages/unity-react-core/CHANGELOG.md index 56732eabcb..6cd16b5420 100644 --- a/packages/unity-react-core/CHANGELOG.md +++ b/packages/unity-react-core/CHANGELOG.md @@ -1,3 +1,16 @@ +# [@asu/unity-react-core-v2.2.0](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-react-core-v2.1.1...@asu/unity-react-core-v2.2.0) (2026-07-29) + + +### Features + +* modal accessibility and escape key features for unity react and bootstrap ([04bdeea](https://github.com/ASU/asu-unity-stack/commit/04bdeea87d74c3ab1fd47a7f272504a843129aad)) +* **unity-react-core:** aria-label update ([eeaebef](https://github.com/ASU/asu-unity-stack/commit/eeaebeff45a85e308ac510f398995b6af708697f)) + + +### Performance Improvements + +* **unity-react-core:** useEffect update for React js async key listener handling ([b25ab85](https://github.com/ASU/asu-unity-stack/commit/b25ab851381632a9d825d83c607f8036f9eeb93a)) + # [@asu/unity-react-core-v2.1.1](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-react-core-v2.1.0...@asu/unity-react-core-v2.1.1) (2026-07-28) diff --git a/packages/unity-react-core/package.json b/packages/unity-react-core/package.json index cd4d141b0f..2106c284a7 100644 --- a/packages/unity-react-core/package.json +++ b/packages/unity-react-core/package.json @@ -1,6 +1,6 @@ { "name": "@asu/unity-react-core", - "version": "2.1.1", + "version": "2.2.0", "main": "./dist/unityReactCore.umd.js", "module": "./dist/unityReactCore.es.js", "browser": "./dist/unityReactCore.umd.js", From fc9eba6087983ec64c67bafdcb0b2ee95f90e05b Mon Sep 17 00:00:00 2001 From: Travis Butterfield Date: Tue, 28 Jul 2026 13:42:39 -0700 Subject: [PATCH 26/42] fix(unity-bootstrap-theme): allow ordered lists to be reversed WHMN-334 --- .../src/scss/extends/_list.scss | 11 +- .../atoms/list/list.examples.stories.js | 369 ++++++++++++++++-- 2 files changed, 330 insertions(+), 50 deletions(-) diff --git a/packages/unity-bootstrap-theme/src/scss/extends/_list.scss b/packages/unity-bootstrap-theme/src/scss/extends/_list.scss index 06fe043840..c6e9ef598a 100644 --- a/packages/unity-bootstrap-theme/src/scss/extends/_list.scss +++ b/packages/unity-bootstrap-theme/src/scss/extends/_list.scss @@ -76,21 +76,21 @@ ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol, ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol, ol>li>ol>li>ol>li>ol, ol { - --li-before-content: counter(listcounter) '. '; + --li-before-content: counter(list-item) '. '; } ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol, ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol, ol>li>ol>li>ol>li>ol>li>ol, ol>li>ol { - --li-before-content: counter(listcounter, lower-alpha) '. '; + --li-before-content: counter(list-item, lower-alpha) '. '; } ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol, ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol, ol>li>ol>li>ol>li>ol>li>ol>li>ol, ol>li>ol>li>ol { - --li-before-content: counter(listcounter, lower-roman) '. '; + --li-before-content: counter(list-item, lower-roman) '. '; } /* General UL rules. */ @@ -188,12 +188,9 @@ ol.uds-list, // Tweak the mix-in's left padding due to OL's needing more space for double // and triple digits. Not supported: > 3 digits. --list-padding-left: 3rem; - // We manually manage the counter since we need to remove the trailing periods. - counter-reset: listcounter; & > li:before { --li-before-font-size: 1rem; - counter-increment: listcounter; left: -3rem; text-align: right; position: absolute; @@ -222,7 +219,7 @@ ol.uds-list, &:before { --li-before-background-color: #{$asu-gray-1}; - --li-before-content: counter(listcounter); // Remove space because it messes with centering. + --li-before-content: counter(list-item); // Remove space because it messes with centering. --li-before-color: #{$asu-gray-7}; --li-before-font-size: 1.25rem; border-radius: 50rem; diff --git a/packages/unity-bootstrap-theme/stories/atoms/list/list.examples.stories.js b/packages/unity-bootstrap-theme/stories/atoms/list/list.examples.stories.js index d7ca4e0a42..7dc93d0b86 100644 --- a/packages/unity-bootstrap-theme/stories/atoms/list/list.examples.stories.js +++ b/packages/unity-bootstrap-theme/stories/atoms/list/list.examples.stories.js @@ -30,17 +30,32 @@ export default { type: "radio", }, }, + reversed: { + name: "Reversed", + control: { + type: "boolean", + }, + }, }, args: { bulletColor: "Default", backgroundColor: "Default", + reversed: false, }, }; -export const UnorderedListMultiLevel = ({ bulletColor, backgroundColor }) => { - return ( -
        +export const UnorderedListMultiLevel = ({ + bulletColor, + backgroundColor, + reversed = false, +}) => { + const html = ( +
        • + {Boolean(reversed) && (Reversed has no effect on UL)} Lorem ipsum dolor sit amet
          • @@ -97,39 +112,86 @@ export const UnorderedListMultiLevel = ({ bulletColor, backgroundColor }) => {
          • Lorem ipsum dolor sit amet
          ); + // force rerender to apply reversed attribute correctly + return reversed ? html :
          {html}
          ; }; -export const OrderedListMultiLevel = ({ bulletColor, backgroundColor }) => { - return ( -
            +export const OrderedListMultiLevel = ({ + bulletColor, + backgroundColor, + reversed = false, +}) => { + const html = ( +
            1. - Lorem ipsum dolor sit amet -
                + Lorem ipsum dolor sit amet{" "} + {Boolean(reversed) && ( + (Reversed changes Number order, not the html order) + )} +
                1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. -
                    +
                    1. Lorem ipsum dolor sit amet, consectetur adipiscing elit. -
                        +
                        1. Lorem ipsum dolor sit amet -
                            +
                            1. Lorem ipsum dolor sit amet -
                                +
                                1. Lorem ipsum dolor sit amet -
                                    +
                                    1. Lorem ipsum dolor sit amet -
                                        +
                                        1. Lorem ipsum dolor sit amet -
                                            +
                                            1. Lorem ipsum dolor sit amet -
                                                +
                                                1. Lorem ipsum dolor sit amet
                                                2. Lorem ipsum dolor sit amet
                                                @@ -159,39 +221,214 @@ export const OrderedListMultiLevel = ({ bulletColor, backgroundColor }) => {
                                              1. Lorem ipsum dolor sit amet
                                              ); + // force rerender to apply reversed attribute correctly + return reversed ? html :
                                              {html}
                                              ; +}; + +export const ReversedOrderedListMultiLevel = ({ + bulletColor, + backgroundColor, + reversed = false, +}) => { + const html = ( + <> +

                                              Basic example:

                                              + {Boolean(reversed) && ( +

                                              (Reversed changes Number order, not the html order)

                                              + )} +
                                                +
                                              1. Lorem ipsum dolor sit amet (1st html element)
                                              2. +
                                              3. Lorem ipsum dolor sit amet (2nd html element)
                                              4. +
                                              5. Lorem ipsum dolor sit amet (3rd html element)
                                              6. +
                                              7. Lorem ipsum dolor sit amet (4th html element)
                                              8. +
                                              9. Lorem ipsum dolor sit amet (5th html element)
                                              10. +
                                              +
                                              +

                                              A more Complex example:

                                              +
                                                +
                                              1. + Lorem ipsum dolor sit amet{" "} + {Boolean(reversed) && ( + (Reversed changes Number order, not the html order) + )} +
                                                  +
                                                1. + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. +
                                                    +
                                                  1. + Lorem ipsum dolor sit amet, consectetur adipiscing elit. +
                                                      +
                                                    1. + Lorem ipsum dolor sit amet +
                                                        +
                                                      1. + Lorem ipsum dolor sit amet +
                                                          +
                                                        1. + Lorem ipsum dolor sit amet +
                                                            +
                                                          1. + Lorem ipsum dolor sit amet +
                                                              +
                                                            1. + Lorem ipsum dolor sit amet +
                                                                +
                                                              1. + Lorem ipsum dolor sit amet +
                                                                  +
                                                                1. Lorem ipsum dolor sit amet
                                                                2. +
                                                                3. Lorem ipsum dolor sit amet
                                                                4. +
                                                                +
                                                              2. +
                                                              3. Lorem ipsum dolor sit amet
                                                              4. +
                                                              +
                                                            2. +
                                                            3. Lorem ipsum dolor sit amet
                                                            4. +
                                                            +
                                                          2. +
                                                          3. Lorem ipsum dolor sit amet
                                                          4. +
                                                          +
                                                        2. +
                                                        3. Lorem ipsum dolor sit amet
                                                        4. +
                                                        +
                                                      2. +
                                                      3. Lorem ipsum dolor sit amet
                                                      4. +
                                                      +
                                                    2. +
                                                    +
                                                  2. +
                                                  3. Lorem ipsum dolor sit amet
                                                  4. +
                                                  +
                                                2. +
                                                +
                                              2. +
                                              3. Lorem ipsum dolor sit amet
                                              4. +
                                              + + ); + // force rerender to apply reversed attribute correctly + return reversed ? html :
                                              {html}
                                              ; +}; +ReversedOrderedListMultiLevel.args = { + reversed: true, }; -export const MixedListMultiLevel = ({ bulletColor, backgroundColor }) => { - return ( -
                                                +export const MixedListMultiLevel = ({ + bulletColor, + backgroundColor, + reversed = false, +}) => { + const html = ( +
                                                1. - Lorem ipsum dolor sit amet -
                                                    + Lorem ipsum dolor sit amet{" "} + {Boolean(reversed) && (Reversed only works on OL)} +
                                                    1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. -
                                                        +
                                                        1. Lorem ipsum dolor sit amet, consectetur adipiscing elit. -
                                                            +
                                                            1. Lorem ipsum dolor sit amet -
                                                                +
                                                                1. Lorem ipsum dolor sit amet -
                                                                    +
                                                                    • Lorem ipsum dolor sit amet -
                                                                        +
                                                                        • Lorem ipsum dolor sit amet -
                                                                            +
                                                                            • Lorem ipsum dolor sit amet -
                                                                                +
                                                                                1. Lorem ipsum dolor sit amet -
                                                                                    +
                                                                                    1. Lorem ipsum dolor sit amet
                                                                                    2. Lorem ipsum dolor sit amet
                                                                                    @@ -221,39 +458,82 @@ export const MixedListMultiLevel = ({ bulletColor, backgroundColor }) => {
                                                                                  1. Lorem ipsum dolor sit amet
                                                                                  ); + + // force rerender to apply reversed attribute correctly + return reversed ? html :
                                                                                  {html}
                                                                                  ; }; -export const Mixed2ListMultiLevel = ({ bulletColor, backgroundColor }) => { - return ( -
                                                                                    +export const Mixed2ListMultiLevel = ({ + bulletColor, + backgroundColor, + reversed = false, +}) => { + const html = ( +
                                                                                    • - Lorem ipsum dolor sit amet -
                                                                                        + Lorem ipsum dolor sit amet{" "} + {Boolean(reversed) && (Reversed has no effect on UL)} +
                                                                                        • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. -
                                                                                            +
                                                                                            • Lorem ipsum dolor sit amet, consectetur adipiscing elit. -
                                                                                                +
                                                                                                • Lorem ipsum dolor sit amet -
                                                                                                    +
                                                                                                    • Lorem ipsum dolor sit amet -
                                                                                                        +
                                                                                                        1. - Lorem ipsum dolor sit amet -
                                                                                                            + Lorem ipsum dolor sit amet{" "} + {reversed === true && ( + (Reversed only works on OL) + )} +
                                                                                                            1. Lorem ipsum dolor sit amet -
                                                                                                                +
                                                                                                                1. Lorem ipsum dolor sit amet -
                                                                                                                    +
                                                                                                                    • Lorem ipsum dolor sit amet -
                                                                                                                        +
                                                                                                                        • Lorem ipsum dolor sit amet
                                                                                                                        • Lorem ipsum dolor sit amet
                                                                                                                        @@ -283,4 +563,7 @@ export const Mixed2ListMultiLevel = ({ bulletColor, backgroundColor }) => {
                                                                                                                      • Lorem ipsum dolor sit amet
                                                                                                                      ); + + // force rerender to apply reversed attribute correctly + return reversed ? html :
                                                                                                                      {html}
                                                                                                                      ; }; From 14af541fdcaed0b378c03377ce98ce1d57c2dce9 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 30 Jul 2026 23:07:33 +0000 Subject: [PATCH 27/42] chore(release): 2.2.1 [skip ci] # [@asu/unity-bootstrap-theme-v2.2.1](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-bootstrap-theme-v2.2.0...@asu/unity-bootstrap-theme-v2.2.1) (2026-07-30) ### Bug Fixes * **unity-bootstrap-theme:** allow ordered lists to be reversed ([d33c05e](https://github.com/ASU/asu-unity-stack/commit/d33c05e0b6eed1c4d5de461f1e545d29e19e10cb)) --- packages/unity-bootstrap-theme/CHANGELOG.md | 7 +++++++ packages/unity-bootstrap-theme/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/unity-bootstrap-theme/CHANGELOG.md b/packages/unity-bootstrap-theme/CHANGELOG.md index e0e91cb513..a68f110f17 100644 --- a/packages/unity-bootstrap-theme/CHANGELOG.md +++ b/packages/unity-bootstrap-theme/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@asu/unity-bootstrap-theme-v2.2.1](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-bootstrap-theme-v2.2.0...@asu/unity-bootstrap-theme-v2.2.1) (2026-07-30) + + +### Bug Fixes + +* **unity-bootstrap-theme:** allow ordered lists to be reversed ([d33c05e](https://github.com/ASU/asu-unity-stack/commit/d33c05e0b6eed1c4d5de461f1e545d29e19e10cb)) + # [@asu/unity-bootstrap-theme-v2.2.0](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-bootstrap-theme-v2.1.1...@asu/unity-bootstrap-theme-v2.2.0) (2026-07-29) diff --git a/packages/unity-bootstrap-theme/package.json b/packages/unity-bootstrap-theme/package.json index d353a0cb14..1787490356 100644 --- a/packages/unity-bootstrap-theme/package.json +++ b/packages/unity-bootstrap-theme/package.json @@ -1,6 +1,6 @@ { "name": "@asu/unity-bootstrap-theme", - "version": "2.2.0", + "version": "2.2.1", "description": "Please see @asu/unity-react-core for up-to-date stories and examples.\nBase UI theme for ASU Web Standards 2.0 developed with Bootstrap 5", "homepage": "https://github.com/ASU/asu-unity-stack#readme", "license": "MIT", From 150d2fbf682e0eddfd880b862241dd27db461ee6 Mon Sep 17 00:00:00 2001 From: Michael Webber Date: Thu, 23 Jul 2026 17:37:46 -0700 Subject: [PATCH 28/42] fix(component-header-footer): fixed large headers not wrapping on smaller screens --- .../HeaderMain/Title/index.styles.js | 2 +- .../src/header/index.stories.js | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/component-header-footer/src/header/components/HeaderMain/Title/index.styles.js b/packages/component-header-footer/src/header/components/HeaderMain/Title/index.styles.js index 1c1e59b1f5..d404f48ae0 100644 --- a/packages/component-header-footer/src/header/components/HeaderMain/Title/index.styles.js +++ b/packages/component-header-footer/src/header/components/HeaderMain/Title/index.styles.js @@ -15,7 +15,7 @@ const TitleWrapper = styled.div` letter-spacing: -1px; display: inline-block; margin: 0; - width: max-content; + width: fit-content; &.active { background-position: -200%; diff --git a/packages/component-header-footer/src/header/index.stories.js b/packages/component-header-footer/src/header/index.stories.js index fe3a49a121..c6f5966cde 100644 --- a/packages/component-header-footer/src/header/index.stories.js +++ b/packages/component-header-footer/src/header/index.stories.js @@ -121,7 +121,7 @@ Default.args = { loggedIn: false, userName: "", navTree: basicNavTree, - title: "Subdomain name", + title: "Subdomain name Lorem ipsum dolor sit amet, consectetur adipiscing elit", breakpoint: "Lg", searchUrl: "https://search.asu.edu/search", site: "subdomain", @@ -133,7 +133,7 @@ Empty.args = {}; export const NoNavigation = Template.bind({}); NoNavigation.args = { - title: "Subdomain name", + title: "Subdomain name Lorem ipsum dolor sit amet, consectetur adipiscing elit", loggedIn: true, userName: "Sparky", logoutLink: "/caslogout", @@ -144,7 +144,7 @@ NoNavigation.args = { export const NoNavigationWithButtons = Template.bind({}); NoNavigationWithButtons.args = { - title: "Subdomain name", + title: "Subdomain name Lorem ipsum dolor sit amet, consectetur adipiscing elit", buttons: [ { href: "/", @@ -172,7 +172,7 @@ BreakpointXL.args = { logoutLink: "/caslogout", loginLink: "/cas", navTree: basicNavTree, - title: "Subdomain name", + title: "Subdomain name Lorem ipsum dolor sit amet, consectetur adipiscing elit", parentOrg: "Parent unit name", parentOrgUrl: "https://engineering.asu.edu", breakpoint: "Xl", @@ -188,7 +188,7 @@ WithMobileNavTree.args = { loginLink: "/cas", navTree: basicNavTree, mobileNavTree, - title: "Subdomain name", + title: "Subdomain name Lorem ipsum dolor sit amet, consectetur adipiscing elit", parentOrg: "Parent unit name", parentOrgUrl: "https://engineering.asu.edu", breakpoint: "Xl", @@ -199,7 +199,7 @@ WithMobileNavTree.args = { export const WithButtons = Template.bind({}); WithButtons.args = { navTree: navTreeWithButtons, - title: "Subdomain name", + title: "Subdomain name Lorem ipsum dolor sit amet, consectetur adipiscing elit", buttons: [ { href: "/", @@ -224,7 +224,7 @@ WithButtons.args = { export const WithMenuColumns = Template.bind({}); WithMenuColumns.args = { navTree: navTreeMega, - title: "Subdomain name", + title: "Subdomain name Lorem ipsum dolor sit amet, consectetur adipiscing elit", parentOrg: "Parent unit name", parentOrgUrl: "https://engineering.asu.edu", loggedIn: true, @@ -238,7 +238,7 @@ WithMenuColumns.args = { export const ExpandOnHover = Template.bind({}); ExpandOnHover.args = { navTree: navTreeMega, - title: "Subdomain name", + title: "Subdomain name Lorem ipsum dolor sit amet, consectetur adipiscing elit", parentOrg: "Parent unit name", parentOrgUrl: "https://engineering.asu.edu", loggedIn: true, @@ -263,7 +263,7 @@ Partner.args = { export const AnimatedTitle = AnimatedTitleTemplate.bind({}); AnimatedTitle.args = { - title: "Subdomain name", + title: "Subdomain name Lorem ipsum dolor sit amet, consectetur adipiscing elit", navTree: basicNavTree, loggedIn: false, logoutLink: "/caslogout", From e885f97a51aefbb33aff4ad3d00486fcb1c1cb77 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 31 Jul 2026 22:42:22 +0000 Subject: [PATCH 29/42] chore(release): 1.4.6 [skip ci] # [@asu/component-header-footer-v1.4.6](https://github.com/asu/asu-unity-stack/compare/@asu/component-header-footer-v1.4.5...@asu/component-header-footer-v1.4.6) (2026-07-31) ### Bug Fixes * **component-header-footer:** fixed large headers not wrapping on smaller screens ([ed046e1](https://github.com/asu/asu-unity-stack/commit/ed046e160fa4dee1456f5bf4862eb524f577a94c)) --- packages/component-header-footer/CHANGELOG.md | 7 +++++++ packages/component-header-footer/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/component-header-footer/CHANGELOG.md b/packages/component-header-footer/CHANGELOG.md index 1fb9f19b32..8c5fa0d53d 100644 --- a/packages/component-header-footer/CHANGELOG.md +++ b/packages/component-header-footer/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@asu/component-header-footer-v1.4.6](https://github.com/asu/asu-unity-stack/compare/@asu/component-header-footer-v1.4.5...@asu/component-header-footer-v1.4.6) (2026-07-31) + + +### Bug Fixes + +* **component-header-footer:** fixed large headers not wrapping on smaller screens ([ed046e1](https://github.com/asu/asu-unity-stack/commit/ed046e160fa4dee1456f5bf4862eb524f577a94c)) + # [@asu/component-header-footer-v1.4.5](https://github.com/asu/asu-unity-stack/compare/@asu/component-header-footer-v1.4.4...@asu/component-header-footer-v1.4.5) (2026-07-06) diff --git a/packages/component-header-footer/package.json b/packages/component-header-footer/package.json index 52e7c1cf93..d481433b32 100644 --- a/packages/component-header-footer/package.json +++ b/packages/component-header-footer/package.json @@ -1,6 +1,6 @@ { "name": "@asu/component-header-footer", - "version": "1.4.5", + "version": "1.4.6", "description": "ASU Global Header and Footer", "main": "./dist/asuHeaderFooter.cjs.js", "browser": "./dist/asuHeaderFooter.umd.js", From 53a38d2fcc44d23e88d54b0193de18d7fe5376f9 Mon Sep 17 00:00:00 2001 From: Michael Webber Date: Thu, 23 Jul 2026 18:48:23 -0700 Subject: [PATCH 30/42] fix(unity-bootstrap-theme): tabindex update for hover cards --- .../image-based-card-and-hover.templates.stories.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/unity-bootstrap-theme/stories/molecules/image-based-card-and-hover/image-based-card-and-hover.templates.stories.js b/packages/unity-bootstrap-theme/stories/molecules/image-based-card-and-hover/image-based-card-and-hover.templates.stories.js index 5ba1467013..30daeb0905 100644 --- a/packages/unity-bootstrap-theme/stories/molecules/image-based-card-and-hover/image-based-card-and-hover.templates.stories.js +++ b/packages/unity-bootstrap-theme/stories/molecules/image-based-card-and-hover/image-based-card-and-hover.templates.stories.js @@ -14,7 +14,7 @@ export const ImageBasedCardsWithCTA = () => (
                                                                                                                      alt text
                                                                                                                      -
                                                                                                                      +

                                                                                                                      Serving all learners at every stage of life

                                                                                                                      From 3ea733ce48a3e683b00bdeccb1010e6ca999febd Mon Sep 17 00:00:00 2001 From: david ornelas Date: Mon, 3 Aug 2026 12:26:05 -0700 Subject: [PATCH 31/42] fix(unity-bootstrap-theme): add unity defgault focus ring to hover card --- .../scss/extends/_image-based-card-and-hover.scss | 6 ++++++ .../src/scss/extends/_misc.scss | 13 ++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/unity-bootstrap-theme/src/scss/extends/_image-based-card-and-hover.scss b/packages/unity-bootstrap-theme/src/scss/extends/_image-based-card-and-hover.scss index 161cd5f24c..e1eee6beb9 100644 --- a/packages/unity-bootstrap-theme/src/scss/extends/_image-based-card-and-hover.scss +++ b/packages/unity-bootstrap-theme/src/scss/extends/_image-based-card-and-hover.scss @@ -86,6 +86,12 @@ .content-holder { width: calc(100% - #{$uds-size-spacing-8}); left: $uds-size-spacing-4; + +// Match the theme's standard focus treatment (see _misc.scss) + &:focus { + @include focus-ring; + } + .content-bg { @include regular-transition; h3 { diff --git a/packages/unity-bootstrap-theme/src/scss/extends/_misc.scss b/packages/unity-bootstrap-theme/src/scss/extends/_misc.scss index 814ac77f2d..fd7fe2e193 100644 --- a/packages/unity-bootstrap-theme/src/scss/extends/_misc.scss +++ b/packages/unity-bootstrap-theme/src/scss/extends/_misc.scss @@ -134,12 +134,19 @@ label { width: 1px; } +// Standard theme focus treatment: a white/gray double ring that stays +// visible over photo or color backgrounds, used in place of the default +// browser outline or the global `:focus` blue glow. +@mixin focus-ring { + outline: none !important; + box-shadow: 0px 0px 0px 2px $uds-color-base-white, 0px 0px 0px 4px $asu-gray-1 !important; + z-index: 1; +} + button:focus, a:focus, input:focus, textarea:focus, select:focus { - outline: none !important; - box-shadow: 0px 0px 0px 2px $uds-color-base-white, 0px 0px 0px 4px $asu-gray-1 !important; - z-index: 1; + @include focus-ring; } From 64acc34130fa805a4dd6de68922cdd25a7a4720e Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 3 Aug 2026 20:22:35 +0000 Subject: [PATCH 32/42] chore(release): 2.2.2 [skip ci] # [@asu/unity-bootstrap-theme-v2.2.2](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-bootstrap-theme-v2.2.1...@asu/unity-bootstrap-theme-v2.2.2) (2026-08-03) ### Bug Fixes * **unity-bootstrap-theme:** add unity defgault focus ring to hover card ([441a16a](https://github.com/ASU/asu-unity-stack/commit/441a16adaa8fde2b6c3c8d14eab674562f3a0c44)) * **unity-bootstrap-theme:** tabindex update for hover cards ([1bd6834](https://github.com/ASU/asu-unity-stack/commit/1bd683441a6e929d3a08760d57281330490f66a6)) --- packages/unity-bootstrap-theme/CHANGELOG.md | 8 ++++++++ packages/unity-bootstrap-theme/package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/unity-bootstrap-theme/CHANGELOG.md b/packages/unity-bootstrap-theme/CHANGELOG.md index a68f110f17..f9f4410c1c 100644 --- a/packages/unity-bootstrap-theme/CHANGELOG.md +++ b/packages/unity-bootstrap-theme/CHANGELOG.md @@ -1,3 +1,11 @@ +# [@asu/unity-bootstrap-theme-v2.2.2](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-bootstrap-theme-v2.2.1...@asu/unity-bootstrap-theme-v2.2.2) (2026-08-03) + + +### Bug Fixes + +* **unity-bootstrap-theme:** add unity defgault focus ring to hover card ([441a16a](https://github.com/ASU/asu-unity-stack/commit/441a16adaa8fde2b6c3c8d14eab674562f3a0c44)) +* **unity-bootstrap-theme:** tabindex update for hover cards ([1bd6834](https://github.com/ASU/asu-unity-stack/commit/1bd683441a6e929d3a08760d57281330490f66a6)) + # [@asu/unity-bootstrap-theme-v2.2.1](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-bootstrap-theme-v2.2.0...@asu/unity-bootstrap-theme-v2.2.1) (2026-07-30) diff --git a/packages/unity-bootstrap-theme/package.json b/packages/unity-bootstrap-theme/package.json index 1787490356..c93e6a2e04 100644 --- a/packages/unity-bootstrap-theme/package.json +++ b/packages/unity-bootstrap-theme/package.json @@ -1,6 +1,6 @@ { "name": "@asu/unity-bootstrap-theme", - "version": "2.2.1", + "version": "2.2.2", "description": "Please see @asu/unity-react-core for up-to-date stories and examples.\nBase UI theme for ASU Web Standards 2.0 developed with Bootstrap 5", "homepage": "https://github.com/ASU/asu-unity-stack#readme", "license": "MIT", From 8b0ff056a5f8c9aed05910722bc377e8e492a38f Mon Sep 17 00:00:00 2001 From: mlsamuelson Date: Thu, 6 Aug 2026 08:26:10 -0700 Subject: [PATCH 33/42] chore: update kiro acp-reviewer model --- .kiro/README.md | 5 +++-- .kiro/agents/acp-reviewer.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.kiro/README.md b/.kiro/README.md index 1f831cb718..89da9a183f 100644 --- a/.kiro/README.md +++ b/.kiro/README.md @@ -78,8 +78,9 @@ Validate manually with `.kiro/scripts/validate-handoff.sh `. ## Notes -- Models are foundry defaults (Opus architect/orchestrator, Sonnet coder, GLM - reviewer, Sonnet vision). If a model id isn't available in your Kiro, it falls +- Models are foundry defaults (Opus architect/orchestrator, Sonnet coder, GPT + reviewer, Sonnet vision). The reviewer uses a non-Anthropic family on purpose + for adversarial model diversity. If a model id isn't available in your Kiro, it falls back to the default — adjust `model` in `agents/acp-*.json` as needed. - `asu-brand` / `asu-design-a11y` are shared with Webspark (this copy is canonical). `asu-brand` is a candidate to upstream to the foundry later. diff --git a/.kiro/agents/acp-reviewer.json b/.kiro/agents/acp-reviewer.json index 0e41acfd93..95ecaeac8a 100644 --- a/.kiro/agents/acp-reviewer.json +++ b/.kiro/agents/acp-reviewer.json @@ -1,6 +1,6 @@ { "name": "acp-reviewer", - "model": "glm-5", + "model": "gpt-5.6-terra", "description": "Three-phase adversarial code reviewer: spec compliance, attack surface analysis, and QA verification.", "prompt": "file://prompts/reviewer.md", "tools": ["fs_read", "execute_bash", "grep", "glob", "code"], From 488d7fa100850951989f017acbaf2627b63bc66d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 20:25:54 +0000 Subject: [PATCH 34/42] build(deps): bump undici from 6.27.0 to 6.28.0 Bumps [undici](https://github.com/nodejs/undici) from 6.27.0 to 6.28.0. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v6.27.0...v6.28.0) --- updated-dependencies: - dependency-name: undici dependency-version: 6.28.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9d9655b715..a270a21578 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30714,9 +30714,9 @@ __metadata: linkType: hard "undici@npm:^6.23.0, undici@npm:^6.25.0": - version: 6.27.0 - resolution: "undici@npm:6.27.0" - checksum: 10c0/f88c3dae3957dbf9d93cb481440aced317bd3c4941b5914fea5efba516d51138988cdb5c76006f0bb1337e41d56c3443351055d492e73af2428521c37ba2a76f + version: 6.28.0 + resolution: "undici@npm:6.28.0" + checksum: 10c0/3029a70df06b38b5b2f30732932a1e92544c753cd82c8abdf0d35afad48e0ba91612e79fe3a442dbbb9434d6a9eba2b714d5ea28984c903dda2b5d5444f38354 languageName: node linkType: hard From 830ac7576c5be5428fa2ad3e3a1dd68afb541675 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 6 Aug 2026 23:59:16 +0000 Subject: [PATCH 35/42] chore(release): 2.2.3 [skip ci] # [@asu/unity-bootstrap-theme-v2.2.3](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-bootstrap-theme-v2.2.2...@asu/unity-bootstrap-theme-v2.2.3) (2026-08-06) ### Bug Fixes * **app-webdir-ui:** render profile card title as bold text instead of h4 ([f7fb027](https://github.com/ASU/asu-unity-stack/commit/f7fb02750db7b2a457beb6b107e5ce970f31ae61)) --- packages/unity-bootstrap-theme/CHANGELOG.md | 7 +++++++ packages/unity-bootstrap-theme/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/unity-bootstrap-theme/CHANGELOG.md b/packages/unity-bootstrap-theme/CHANGELOG.md index f9f4410c1c..460425baca 100644 --- a/packages/unity-bootstrap-theme/CHANGELOG.md +++ b/packages/unity-bootstrap-theme/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@asu/unity-bootstrap-theme-v2.2.3](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-bootstrap-theme-v2.2.2...@asu/unity-bootstrap-theme-v2.2.3) (2026-08-06) + + +### Bug Fixes + +* **app-webdir-ui:** render profile card title as bold text instead of h4 ([f7fb027](https://github.com/ASU/asu-unity-stack/commit/f7fb02750db7b2a457beb6b107e5ce970f31ae61)) + # [@asu/unity-bootstrap-theme-v2.2.2](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-bootstrap-theme-v2.2.1...@asu/unity-bootstrap-theme-v2.2.2) (2026-08-03) diff --git a/packages/unity-bootstrap-theme/package.json b/packages/unity-bootstrap-theme/package.json index c93e6a2e04..905ac5d4c7 100644 --- a/packages/unity-bootstrap-theme/package.json +++ b/packages/unity-bootstrap-theme/package.json @@ -1,6 +1,6 @@ { "name": "@asu/unity-bootstrap-theme", - "version": "2.2.2", + "version": "2.2.3", "description": "Please see @asu/unity-react-core for up-to-date stories and examples.\nBase UI theme for ASU Web Standards 2.0 developed with Bootstrap 5", "homepage": "https://github.com/ASU/asu-unity-stack#readme", "license": "MIT", From fe291f11d9201e4c0171799ed069fbcd74e33357 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 6 Aug 2026 23:59:52 +0000 Subject: [PATCH 36/42] chore(release): 2.2.1 [skip ci] # [@asu/unity-react-core-v2.2.1](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-react-core-v2.2.0...@asu/unity-react-core-v2.2.1) (2026-08-06) ### Bug Fixes * **app-webdir-ui:** render profile card title as bold text instead of h4 ([f7fb027](https://github.com/ASU/asu-unity-stack/commit/f7fb02750db7b2a457beb6b107e5ce970f31ae61)) --- packages/unity-react-core/CHANGELOG.md | 7 +++++++ packages/unity-react-core/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/unity-react-core/CHANGELOG.md b/packages/unity-react-core/CHANGELOG.md index 6cd16b5420..452c20cde2 100644 --- a/packages/unity-react-core/CHANGELOG.md +++ b/packages/unity-react-core/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@asu/unity-react-core-v2.2.1](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-react-core-v2.2.0...@asu/unity-react-core-v2.2.1) (2026-08-06) + + +### Bug Fixes + +* **app-webdir-ui:** render profile card title as bold text instead of h4 ([f7fb027](https://github.com/ASU/asu-unity-stack/commit/f7fb02750db7b2a457beb6b107e5ce970f31ae61)) + # [@asu/unity-react-core-v2.2.0](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-react-core-v2.1.1...@asu/unity-react-core-v2.2.0) (2026-07-29) diff --git a/packages/unity-react-core/package.json b/packages/unity-react-core/package.json index 2106c284a7..c0364c0cd1 100644 --- a/packages/unity-react-core/package.json +++ b/packages/unity-react-core/package.json @@ -1,6 +1,6 @@ { "name": "@asu/unity-react-core", - "version": "2.2.0", + "version": "2.2.1", "main": "./dist/unityReactCore.umd.js", "module": "./dist/unityReactCore.es.js", "browser": "./dist/unityReactCore.umd.js", From d7181322a8ed9536c6b17fa954cabceffd181ba2 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 7 Aug 2026 00:01:02 +0000 Subject: [PATCH 37/42] chore(release): 5.0.18 [skip ci] # [@asu/app-webdir-ui-v5.0.18](https://github.com/asu/asu-unity-stack/compare/@asu/app-webdir-ui-v5.0.17...@asu/app-webdir-ui-v5.0.18) (2026-08-07) ### Bug Fixes * **app-webdir-ui:** render profile card title as bold text instead of h4 ([f7fb027](https://github.com/asu/asu-unity-stack/commit/f7fb02750db7b2a457beb6b107e5ce970f31ae61)) --- packages/app-webdir-ui/CHANGELOG.md | 7 +++++++ packages/app-webdir-ui/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/app-webdir-ui/CHANGELOG.md b/packages/app-webdir-ui/CHANGELOG.md index 1eaba04098..2352c6e80f 100644 --- a/packages/app-webdir-ui/CHANGELOG.md +++ b/packages/app-webdir-ui/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@asu/app-webdir-ui-v5.0.18](https://github.com/asu/asu-unity-stack/compare/@asu/app-webdir-ui-v5.0.17...@asu/app-webdir-ui-v5.0.18) (2026-08-07) + + +### Bug Fixes + +* **app-webdir-ui:** render profile card title as bold text instead of h4 ([f7fb027](https://github.com/asu/asu-unity-stack/commit/f7fb02750db7b2a457beb6b107e5ce970f31ae61)) + # [@asu/app-webdir-ui-v5.0.17](https://github.com/asu/asu-unity-stack/compare/@asu/app-webdir-ui-v5.0.16...@asu/app-webdir-ui-v5.0.17) (2026-07-28) diff --git a/packages/app-webdir-ui/package.json b/packages/app-webdir-ui/package.json index e89073556a..4adc840b0a 100644 --- a/packages/app-webdir-ui/package.json +++ b/packages/app-webdir-ui/package.json @@ -1,6 +1,6 @@ { "name": "@asu/app-webdir-ui", - "version": "5.0.17", + "version": "5.0.18", "description": "App Webdir UI", "main": "./dist/webdirUI.cjs.js", "browser": "./dist/webdirUI.umd.js", From b6abc70968a128f958bdca8139f6fdac200e7550 Mon Sep 17 00:00:00 2001 From: Ojas Atkar Date: Tue, 7 Jul 2026 11:42:22 -0700 Subject: [PATCH 38/42] fix(app-degree-pages): delete degree detail page component --- .../src/components/ListingPage/index.jsx | 2 +- .../src/core/components/Breadcrumbs/index.jsx | 80 +++++++++++++++++++ .../src/core/components/index.js | 1 + packages/app-degree-pages/src/index.js | 1 - 4 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 packages/app-degree-pages/src/core/components/Breadcrumbs/index.jsx diff --git a/packages/app-degree-pages/src/components/ListingPage/index.jsx b/packages/app-degree-pages/src/components/ListingPage/index.jsx index 0c0e3882f7..0c7a519ff4 100644 --- a/packages/app-degree-pages/src/components/ListingPage/index.jsx +++ b/packages/app-degree-pages/src/components/ListingPage/index.jsx @@ -28,7 +28,7 @@ import { } from "../../core/models/app-prop-types"; import { filterData, sortPrograms } from "../../core/services"; import { urlResolver } from "../../core/utils"; -import { Breadcrumbs } from "../DetailPage/components/Breadcrumbs"; +import { Breadcrumbs } from "../../core/components/Breadcrumbs"; import { BrowseTitle } from "./components/BrowseTitle"; import { Filters, INITIAL_FILTER_STATE } from "./components/Filters"; import { FiltersSummary } from "./components/FiltersSummary"; diff --git a/packages/app-degree-pages/src/core/components/Breadcrumbs/index.jsx b/packages/app-degree-pages/src/core/components/Breadcrumbs/index.jsx new file mode 100644 index 0000000000..68f9d4e3ec --- /dev/null +++ b/packages/app-degree-pages/src/core/components/Breadcrumbs/index.jsx @@ -0,0 +1,80 @@ +// @ts-check +import { idGenerator } from "@asu/shared"; +import PropTypes from "prop-types"; +import React from "react"; + +import { linkPropShape } from "../../../../core/models"; +import { trackGAEvent } from "../../../../core/services/google-analytics"; + +/** + * + * @param {{ + * breadcrumbs: import("src/core/types/detail-page-types").BreadcrumbItem [], + * section: string + * }} param0 + * @returns + */ +function Breadcrumbs({ breadcrumbs, section }) { + const genId = idGenerator("breadcrumb-"); + + return ( + breadcrumbs && ( + + ) + ); +} + +Breadcrumbs.propTypes = { + breadcrumbs: PropTypes.arrayOf(linkPropShape), + section: PropTypes.string, +}; + +export { Breadcrumbs }; diff --git a/packages/app-degree-pages/src/core/components/index.js b/packages/app-degree-pages/src/core/components/index.js index eb58c78372..54ec08507f 100644 --- a/packages/app-degree-pages/src/core/components/index.js +++ b/packages/app-degree-pages/src/core/components/index.js @@ -7,3 +7,4 @@ export * from "./OverlapContentImage"; export * from "./ParagrapList"; export * from "./Styles"; export * from "./icons"; +export * from "./Breadcrumbs"; diff --git a/packages/app-degree-pages/src/index.js b/packages/app-degree-pages/src/index.js index c939ea2d95..f0935db386 100644 --- a/packages/app-degree-pages/src/index.js +++ b/packages/app-degree-pages/src/index.js @@ -3,5 +3,4 @@ export * from "./components"; export { initListingPage, - initProgramDetailPage, } from "./core/utils/init-page-degree"; From f2ccc0a85b61f2652d02e13c94f8a1c5aa89dd38 Mon Sep 17 00:00:00 2001 From: david ornelas Date: Thu, 6 Aug 2026 15:41:47 -0700 Subject: [PATCH 39/42] fix(app-degree-pages): fix relative path in Breadcrumbs import --- .../src/core/components/Breadcrumbs/index.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app-degree-pages/src/core/components/Breadcrumbs/index.jsx b/packages/app-degree-pages/src/core/components/Breadcrumbs/index.jsx index 68f9d4e3ec..f7163cf39c 100644 --- a/packages/app-degree-pages/src/core/components/Breadcrumbs/index.jsx +++ b/packages/app-degree-pages/src/core/components/Breadcrumbs/index.jsx @@ -3,8 +3,8 @@ import { idGenerator } from "@asu/shared"; import PropTypes from "prop-types"; import React from "react"; -import { linkPropShape } from "../../../../core/models"; -import { trackGAEvent } from "../../../../core/services/google-analytics"; +import { linkPropShape } from "../../../core/models"; +import { trackGAEvent } from "../../../core/services/google-analytics"; /** * From d64b5fdf9dfed11d32c8eb7069044ac19b36ef1e Mon Sep 17 00:00:00 2001 From: david ornelas Date: Thu, 6 Aug 2026 16:02:27 -0700 Subject: [PATCH 40/42] chore(app-degree-pages): remove unused code --- .../app-degree-pages/src/core/constants/component-constants.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/app-degree-pages/src/core/constants/component-constants.js b/packages/app-degree-pages/src/core/constants/component-constants.js index abd8575770..396440f94b 100644 --- a/packages/app-degree-pages/src/core/constants/component-constants.js +++ b/packages/app-degree-pages/src/core/constants/component-constants.js @@ -1,7 +1,5 @@ // @ts-check -import { title } from "process"; - const tagHeadings = { h1: "h1", h2: "h2", From 35dc634c6471421a8b5d077f853d2eb2e1645ff7 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 7 Aug 2026 16:42:56 +0000 Subject: [PATCH 41/42] chore(release): 3.2.4 [skip ci] # [@asu/app-degree-pages-v3.2.4](https://github.com/asu/asu-unity-stack/compare/@asu/app-degree-pages-v3.2.3...@asu/app-degree-pages-v3.2.4) (2026-08-07) ### Bug Fixes * **app-degree-pages:** delete degree detail page component ([83e8992](https://github.com/asu/asu-unity-stack/commit/83e8992e36c56dd89994714f11b37f0fb211477f)) * **app-degree-pages:** fix relative path in Breadcrumbs import ([83338bf](https://github.com/asu/asu-unity-stack/commit/83338bf96a814708024bf9c5c3f194367e99eede)) --- packages/app-degree-pages/CHANGELOG.md | 8 ++++++++ packages/app-degree-pages/package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/app-degree-pages/CHANGELOG.md b/packages/app-degree-pages/CHANGELOG.md index 94d4c868e4..c2f04328f8 100644 --- a/packages/app-degree-pages/CHANGELOG.md +++ b/packages/app-degree-pages/CHANGELOG.md @@ -1,3 +1,11 @@ +# [@asu/app-degree-pages-v3.2.4](https://github.com/asu/asu-unity-stack/compare/@asu/app-degree-pages-v3.2.3...@asu/app-degree-pages-v3.2.4) (2026-08-07) + + +### Bug Fixes + +* **app-degree-pages:** delete degree detail page component ([83e8992](https://github.com/asu/asu-unity-stack/commit/83e8992e36c56dd89994714f11b37f0fb211477f)) +* **app-degree-pages:** fix relative path in Breadcrumbs import ([83338bf](https://github.com/asu/asu-unity-stack/commit/83338bf96a814708024bf9c5c3f194367e99eede)) + # [@asu/app-degree-pages-v3.2.3](https://github.com/asu/asu-unity-stack/compare/@asu/app-degree-pages-v3.2.2...@asu/app-degree-pages-v3.2.3) (2026-07-28) diff --git a/packages/app-degree-pages/package.json b/packages/app-degree-pages/package.json index ebe2e9113f..7436c6d19f 100644 --- a/packages/app-degree-pages/package.json +++ b/packages/app-degree-pages/package.json @@ -1,6 +1,6 @@ { "name": "@asu/app-degree-pages", - "version": "3.2.3", + "version": "3.2.4", "description": "ASU implementation of degree pages", "main": "./dist/degreePages.cjs.js", "browser": "./dist/degreePages.umd.js", From 8bf8112f31fca37754b4032d6924aa08944f5d5a Mon Sep 17 00:00:00 2001 From: Michael Webber Date: Mon, 17 Aug 2026 18:18:10 -0700 Subject: [PATCH 42/42] feat(unity-react-core): custom button, input, and modal state handling options --- .../unity-react-core/src/components/Modal/Modal.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/unity-react-core/src/components/Modal/Modal.tsx b/packages/unity-react-core/src/components/Modal/Modal.tsx index 2e0f13a937..1e1fce6b29 100644 --- a/packages/unity-react-core/src/components/Modal/Modal.tsx +++ b/packages/unity-react-core/src/components/Modal/Modal.tsx @@ -61,21 +61,21 @@ export const Modal: React.FC = ({ gaData, }) => { const { isReact, isBootstrap } = useBaseSpecificFramework(); - const [defaultOpenState, defaultSetOpen] = React.useState(open); + const [defaultOpenState, defaultSetOpen] = React.useState(open ?? false); const handleSetOpen = (e: boolean) => { if (setOpen) { - setOpen(e); + setOpen(e); // custom set open prop } else { - defaultSetOpen(e); + defaultSetOpen(e); // default set open function } }; const getOpenState = () => { if (setOpen) { - return open; + return open; // custom open state value } else { - return defaultOpenState; + return defaultOpenState; // default open state value } }; @@ -177,7 +177,7 @@ export const Modal: React.FC = ({ return (
                                                                                                                      {/* Disable main content on modal open */} -
                                                                                                                      +
                                                                                                                      {openModalInput ? ( openModalInput ) : (