UNPKG

@utahdts/utah-design-system

Version:
1,388 lines 296 kB
import { childrenMenuTypes, events, getUtahHeaderSettings, popupFocusHandler, renderDOMSingle, setUtahHeaderSettings } from "@utahdts/utah-design-system-header"; import React, { createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react"; import { arrow, autoUpdate, flip, offset, shift, useFloating } from "@floating-ui/react-dom"; import { castArray, cloneDeep, identity, isArray, isEmpty, isEqual, isFunction, isObject, split, trim, uniq } from "lodash-es"; import { Fragment, jsx, jsxs } from "react/jsx-runtime"; import { useImmer } from "use-immer"; import { add, format, isValid, parse } from "date-fns"; import { v4 } from "uuid"; var package_default = { name: "@utahdts/utah-design-system", description: "Utah Design System React Library", displayName: "Utah Design System React Library", version: "5.1.0", exports: { ".": { "development-local": "./index.js", "development": "./dist/utah-design-system.es.js", "production": "./dist/utah-design-system.es.js", "import": { "types": "./dist/index.d.ts", "default": "./dist/utah-design-system.es.js" }, "types": "./dist/index.d.ts", "require": "./dist/utah-design-system.umd.js", "default": [ "./dist/utah-design-system.es.js", "./dist/utah-design-system.umd.js", "./index.js" ] }, "./css": "./dist/style.css", "./dist/*": "./dist/*", "./src/css/": "./src/css/", "./css/index.scss": "./css/index.scss" }, main: "index.js", types: "./dist/index.d.ts", files: [ "css", "dist", "react" ], scripts: { "build": "vite build", "buildw": "vite build --watch", "buildTypes": "mkdir -p dist && cp ./artifacts/index.d.ts ./dist/", "generateTypes": "npx tsc", "preview": "vite preview", "publishLibrary": "npm publish --access public", "test:ci": "vitest run --coverage --mode development-local", "test-publish": "npm publish --dry-run", "test": "vitest", "testOnce": "vitest run", "tsc": "tsc", "tscw": "tsc --watch --skipLibCheck", "watch": "vite build --watch" }, repository: { "type": "git", "url": "https://github.com:utahdts/utah-design-system.git", "directory": "library" }, keywords: [ "design system", "dts", "utah", "components" ], author: "DTS Digital Experience <dxp@utah.gov>", license: "Apache 2.0", bugs: { "url": "https://github.com/utahdts/utah-design-system/issues" }, homepage: "https://github.com/utahdts/utah-design-system", peerDependencies: { "react": "^18.0.0 || ^19.0.0" }, dependencies: { "@floating-ui/react-dom": "^2.1.8", "@utahdts/utah-design-system-header": "5.1.0", "date-fns": "4.1.0", "immer": "11.1.4", "lodash-es": "4.18.1", "use-immer": "0.11.0", "uuid": "14.0.0" }, devDependencies: { "@types/lodash-es": "4.17.12", "@types/react": "^19.2.14", "@vitejs/plugin-react": "^6.0.1", "@vitest/coverage-istanbul": "^4.1.5", "@vitest/ui": "^4.1.5", "jsdom": "^29.1.1", "prop-types": "15.8.1", "sass": "^1.99.0", "typescript": "^6.0.3", "vite": "^8.0.10", "vitest": "^4.1.5" }, type: "module" }; //#endregion //#region react/enums/popupPlacement.js /** @typedef {import('@utahdts/utah-design-system-header').PopupPlacement} PopupPlacement */ /** @enum {PopupPlacement} */ var popupPlacement = { BOTTOM: "bottom", BOTTOM_START: "bottom-start", BOTTOM_END: "bottom-end", LEFT: "left", LEFT_START: "left-start", LEFT_END: "left-end", RIGHT: "right", RIGHT_START: "right-start", RIGHT_END: "right-end", TOP: "top", TOP_START: "top-start", TOP_END: "top-end" }; //#endregion //#region react/hooks/usePopupDelay.js var NO_POP_UP_TIMEOUT_MS = 350; var POP_UP_TIMEOUT_MS = 350; var PopupDelay = class { #popupTimeoutId = NaN; #noPopupTimeoutId = NaN; #isImmediatePopup = false; /** wait a little while after a popup closes before turning off immediate popup flag */ startNoPopupTimer = () => { clearTimeout(this.#noPopupTimeoutId); clearTimeout(this.#popupTimeoutId); if (this.#isImmediatePopup) this.#noPopupTimeoutId = window.setTimeout(() => { this.#isImmediatePopup = false; }, NO_POP_UP_TIMEOUT_MS); }; /** * wait to pop unless the popping period has already lapsed * Make sure to call startNoPopupTimer when the popup goes away * @param {() => void} callback function to call when waiting is done */ startPopupTimer = (callback) => { clearTimeout(this.#noPopupTimeoutId); clearTimeout(this.#popupTimeoutId); if (this.#isImmediatePopup) callback(); else this.#popupTimeoutId = window.setTimeout(() => { this.#isImmediatePopup = true; callback(); }, POP_UP_TIMEOUT_MS); }; }; var POPUP_DELAY = new PopupDelay(); /** * This could easily have been a context, BUT trying to avoid requiring global contexts for the Design System library * plus this doesn't have to be a context because changing the isImmediatePopup state doesn't need to trigger a rerender (locally nor globally) * @returns {{startNoPopupTimer: () => void, startPopupTimer: (callback: () => void) => void}} */ function usePopupDelay() { return POPUP_DELAY; } //#endregion //#region react/util/joinClassNames.js /** * pass in comma separated list of class name strings to be trimmed, filtered, and joined together with a space * @param {(string | boolean | any[] | null | undefined)[]} classNames really can be anything, but should be string if truey * @returns {string} */ function joinClassNames(...classNames) { return castArray(Array.from(classNames)).flat(Infinity).map((className) => typeof className === "string" ? trim(className) : className).filter(identity).join(" "); } //#endregion //#region react/components/tooltip/Tooltip.jsx /** @typedef {import('@utahdts/utah-design-system-header').PopupPlacement} PopupPlacement */ /** * A ToolTip is only in charge of positioning and rendering a tooltip. * Pass in a "referenceElement" to have "zero-config" for onMouseEnter and onMouseLeave triggering * Pass in a isPopupVisible and setIsPopupVisible to have it be a controlled component * @param {object} props * @param {import('react').ReactNode} props.children The content of the tool tip * @param {string} [props.className] CSS class to apply to the popup * @param {import('react').RefObject<HTMLDivElement | null>} [props.innerRef] ref of the popup wrapper * @param {boolean} [props.isPopupVisible] controlled value for telling if tool tip is visible * @param {number | {mainAxis: number, crossAxis: number, alignmentAxis?: number}} [props.offset] default offset is 5 (see popup documentation * for details) * @param {PopupPlacement} [props.placement] where to put the tooltip in reference to the referenceElement * @param {HTMLElement | null} props.referenceElement the referenceElement from which the tool tip will toggle (first render will most likely be null) * @returns {import('react').JSX.Element} */ function Tooltip({ children, className, innerRef: draftInnerRef, isPopupVisible, offset: offset$2 = 5, placement = popupPlacement.BOTTOM, referenceElement: draftReferenceElement }) { const [isPopupVisibleInternal, setIsPopupVisibleInternal] = useState(false); const [popupElement, setPopupElement] = useState(null); const [arrowElement, setArrowElement] = useState(null); const { floatingStyles, middlewareData } = useFloating({ elements: { reference: draftReferenceElement, floating: popupElement }, middleware: [ offset(offset$2), flip(), shift(), arrow({ element: arrowElement }) ], open: !(isPopupVisible ?? isPopupVisibleInternal), placement, whileElementsMounted: autoUpdate }); const { startNoPopupTimer, startPopupTimer } = usePopupDelay(); const onEscape = (e) => { if (e.code === "Escape" || e.key === "Escape") { setIsPopupVisibleInternal(false); document.removeEventListener("keyup", onEscape); } }; useEffect(() => { if (draftReferenceElement && isPopupVisible === void 0) { if (draftReferenceElement.onmouseenter) throw new Error("ToolTip: onMouseEnter previously set"); if (draftReferenceElement.onmouseleave) throw new Error("ToolTip: onMouseLeave previously set"); if (draftReferenceElement.onfocus) throw new Error("ToolTip: onfocus previously set"); if (draftReferenceElement.onblur) throw new Error("ToolTip: onblur previously set"); draftReferenceElement.onmouseenter = () => startPopupTimer(() => { setIsPopupVisibleInternal(true); }); draftReferenceElement.onfocus = () => { setIsPopupVisibleInternal(true); }; draftReferenceElement.onmouseleave = () => { startNoPopupTimer(); setIsPopupVisibleInternal(false); }; draftReferenceElement.onblur = () => setIsPopupVisibleInternal(false); document.addEventListener("keyup", onEscape); } return (() => { if (draftReferenceElement) { draftReferenceElement.onmouseenter = null; draftReferenceElement.onmouseleave = null; draftReferenceElement.onfocus = null; draftReferenceElement.onblur = null; } }); }, [ draftReferenceElement, isPopupVisible, startNoPopupTimer, startPopupTimer, onEscape ]); return /* @__PURE__ */ jsx("div", { ref: (refValue) => { setPopupElement(refValue); if (draftInnerRef) draftInnerRef.current = refValue; }, style: floatingStyles, className: joinClassNames(className, "tooltip__wrapper", !(isPopupVisible ?? isPopupVisibleInternal) && "tooltip__wrapper--hidden"), "aria-hidden": "true", "data-popup-placement": middlewareData?.offset?.placement || placement, children: /* @__PURE__ */ jsxs("div", { className: "tooltip__content", children: [children, /* @__PURE__ */ jsx("div", { ref: setArrowElement, style: { left: middlewareData.arrow?.x, top: middlewareData.arrow?.y }, className: "tooltip__arrow" })] }) }); } //#endregion //#region react/enums/buttonEnums.js /** @typedef {import('@utahdts/utah-design-system').ButtonAppearance} ButtonAppearance */ /** @typedef {import('@utahdts/utah-design-system').ButtonTypes} ButtonTypes */ /** @typedef {import('@utahdts/utah-design-system').IconButtonAppearance} IconButtonAppearance */ /** @enum {ButtonAppearance} */ var BUTTON_APPEARANCE = { SOLID: "solid", OUTLINED: "outlined" }; /** @enum {ButtonTypes} */ var BUTTON_TYPES = { BUTTON: "button", RESET: "reset", SUBMIT: "submit" }; /** @enum {IconButtonAppearance} */ var ICON_BUTTON_APPEARANCE = { SOLID: "solid", OUTLINED: "outlined", BORDERLESS: "borderless" }; //#endregion //#region react/enums/componentColors.js /** @typedef {import('@utahdts/utah-design-system').ComponentColors} ComponentColors */ /** @enum {ComponentColors} */ var componentColors = { PRIMARY: "primary", SECONDARY: "secondary", ACCENT: "accent", NONE: "none" }; //#endregion //#region react/enums/formElementSizesEnum.js /** @typedef {import('@utahdts/utah-design-system').FormElementSizes} FormElementSizes */ /** @enum {FormElementSizes} */ var formElementSizesEnum = { SMALL3X: "small3x", SMALL2X: "small2x", SMALL1X: "small1x", SMALL: "small", MEDIUM: "medium", LARGE: "large", LARGE1X: "large1x" }; //#endregion //#region react/util/handleEvent.js /** * A function used as a callback often needs to the triggering event * from triggering other events. Wrapping the function in this handleEvent function * automatically stops the event propagation. ie handleEvent(() => { ... do something ... }) * @param {import('react').MouseEventHandler<HTMLButtonElement>} func The function to run * @returns {import('react').MouseEventHandler<HTMLButtonElement>} */ function handleEvent(func) { return (e) => { if (e.preventDefault) e.preventDefault(); if (e.stopPropagation) e.stopPropagation(); if (e?.nativeEvent?.stopImmediatePropagation) e.nativeEvent.stopImmediatePropagation(); func.call(e.target, e); }; } //#endregion //#region react/components/widgetsIndicators/Spinner.jsx /** * @param {object} props * @param {import('react').ReactNode} [props.children] * @param {string} [props.className] * @param {string} [props.id] * @param {import('react').RefObject<HTMLDivElement>} [props.innerRef] * @param {number} [props.size] * @param {number} [props.strokeWidth] * @param {number} [props.value] * @returns {import('react').JSX.Element} */ function Spinner({ children, className, id, innerRef, size = 60, strokeWidth = 10, value = NaN, ...rest }) { const strokeWidthUse = Number.isNaN(strokeWidth) ? 10 : strokeWidth; const strokeWidthPlus1Use = Number.isNaN(strokeWidth) ? 10 : (strokeWidth ?? 0) + 1; const widthUse = Number.isNaN(size) ? void 0 : size; return /* @__PURE__ */ jsxs("div", { "aria-valuemax": 100, "aria-valuemin": 0, "aria-valuenow": Number.isNaN(value) ? void 0 : (value ?? 0) * 100, className: joinClassNames(className, "spinner", Number.isNaN(value) ? "spinner--indeterminate" : "spinner--determinate"), id: id ?? void 0, ref: innerRef, role: "progressbar", "aria-label": Number.isNaN(value) ? "Loading..." : `Loading ${(value ?? 0) * 100}% complete`, ...rest, children: [/* @__PURE__ */ jsx("div", { className: "spinner__animation", children: /* @__PURE__ */ jsxs("svg", { height: widthUse, role: "presentation", viewBox: "-10.00 -10.00 120.00 120.00", width: widthUse, children: [/* @__PURE__ */ jsx("path", { strokeWidth: strokeWidthUse, className: "spinner__track", d: "M 50,50 m 0,-45 a 45,45 0 1 1 0,90 a 45,45 0 1 1 0,-90" }), /* @__PURE__ */ jsx("path", { className: "spinner__value", d: "M 50,50 m 0,-45 a 45,45 0 1 1 0,90 a 45,45 0 1 1 0,-90", pathLength: "360", strokeDasharray: "360 360", strokeDashoffset: 360 * (1 - (Number.isNaN(value) ? .25 : value ?? 0)), strokeWidth: strokeWidthPlus1Use })] }) }), /* @__PURE__ */ jsx("div", { className: "spinner__children", children })] }); } //#endregion //#region react/components/buttons/Button.jsx /** @typedef {import('@utahdts/utah-design-system').ButtonAppearance} ButtonAppearance */ /** @typedef {import('@utahdts/utah-design-system').ButtonTypes} ButtonTypes */ /** @typedef {import('@utahdts/utah-design-system').ComponentColors} ComponentColors */ /** @typedef {import('@utahdts/utah-design-system').FormElementSizes} FormElementSizes */ /** * @param {object} props * @param {ButtonAppearance} [props.appearance] * @param {import('react').ReactNode} props.children most often is the title of the button, but can also contain most anything * @param {string} [props.className] modify your button via className like 'button--primary' and other modifiers found in the button.scss * @param {ComponentColors} [props.color] the base color of the button * @param {import('react').RefObject<HTMLButtonElement | null>} [props.innerRef] a ref to attach to the actual DOM <button> element * @param {import('react').ReactNode} [props.iconLeft] * @param {import('react').ReactNode} [props.iconRight] * @param {string} [props.id] * @param {boolean} [props.isBusy] if the button is busy then it shows a spinner indicator on it and disables the button * @param {boolean} [props.isDisabled] * @param {import('react').MouseEventHandler<HTMLButtonElement>} props.onClick * @param {FormElementSizes} [props.size] * @param {ButtonTypes} [props.type] * @returns {import('react').JSX.Element} */ function Button({ appearance = BUTTON_APPEARANCE.OUTLINED, children, className, color = componentColors.NONE, innerRef, isBusy, iconLeft, iconRight, isDisabled, id, onClick, size = "medium", type = BUTTON_TYPES.BUTTON, ...rest }) { return /* @__PURE__ */ jsxs("button", { className: joinClassNames("button", className, `button--${appearance}`, color && color !== componentColors.NONE ? `button--${color}-color` : null, size && size !== formElementSizesEnum.MEDIUM ? `button--${size}` : null), disabled: isDisabled || isBusy, id, onClick: handleEvent((e) => onClick?.(e)), ref: innerRef, type, ...rest, children: [ iconLeft ? /* @__PURE__ */ jsx("span", { className: "button--icon button--icon-left", children: iconLeft }) : null, children, isBusy ? /* @__PURE__ */ jsx(Spinner, { className: "ml-spacing-xs", size: size === formElementSizesEnum.LARGE1X ? 24 : 22, strokeWidth: size === formElementSizesEnum.LARGE1X ? 14 : 12 }) : null, iconRight ? /* @__PURE__ */ jsx("span", { className: "button--icon button--icon-right", children: iconRight }) : null ] }); } //#endregion //#region react/components/buttons/ClickableTag.jsx /** @typedef {import('@utahdts/utah-design-system').FormElementSizes} FormElementSizes */ /** * @param {object} props * @param {import('react').ReactNode} props.children most often is the title of the tag, but can also contain most anything * @param {string} [props.className] modify your tag via className like 'tag--primary' and other modifiers found in the tag.scss * @param {string} [props.id] * @param {import('react').RefObject<HTMLDivElement>} [props.innerRef] a ref to attach to the wrapper <div> * @param {import('react').ReactNode} [props.iconLeft] an icon for the left side of props.children * @param {import('react').ReactNode} [props.iconRight] an icon for the right side of props.children * @param {boolean} [props.isDisabled] * @param {boolean} [props.isSelected] * @param {import('react').MouseEventHandler<HTMLButtonElement>} [props.onClick] * @param {FormElementSizes} [props.size] * @returns {import('react').JSX.Element} */ function ClickableTag({ children, className, id, iconLeft, iconRight, innerRef, isDisabled, isSelected, onClick, size = formElementSizesEnum.MEDIUM, ...rest }) { return /* @__PURE__ */ jsx("div", { className: "tag__wrapper", ref: innerRef, children: /* @__PURE__ */ jsxs("button", { "aria-pressed": isSelected, className: joinClassNames("tag", "tag__button", `tag--${size}`, className, isSelected ? "tag--selected" : ""), disabled: isDisabled, id, onClick: onClick && handleEvent((e) => onClick(e)), type: "button", ...rest, children: [ iconLeft ? /* @__PURE__ */ jsx("span", { className: "tag--icon tag--icon-left", children: iconLeft }) : null, children, iconRight ? /* @__PURE__ */ jsx("span", { className: "tag--icon tag--icon-right", children: iconRight }) : null ] }) }); } //#endregion //#region react/components/buttons/ConfirmationButton/context/ConfirmationButtonContext.js var ConfirmationButtonContext = createContext(false); //#endregion //#region react/components/buttons/ConfirmationButton/context/ConfirmationButtonContextProvider.jsx /** * @param {object} props * @param {import('react').ReactNode} props.children * @param {boolean} props.isClicked * @returns {import('react').JSX.Element} */ function ConfirmationButtonContextProvider({ children, isClicked }) { return /* @__PURE__ */ jsx(ConfirmationButtonContext.Provider, { value: isClicked, children }); } //#endregion //#region react/components/buttons/ConfirmationButton/ConfirmationButton.jsx /** @typedef {import('@utahdts/utah-design-system').ButtonAppearance} ButtonAppearance */ /** @typedef {import('@utahdts/utah-design-system').ButtonTypes} ButtonTypes */ /** @typedef {import('@utahdts/utah-design-system').ComponentColors} ComponentColors */ /** @typedef {import('@utahdts/utah-design-system').FormElementSizes} FormElementSizes */ /** @typedef {import('@utahdts/utah-design-system').WrapInElement} WrapInElement */ /** * @param {object} props * @param {ButtonAppearance} [props.appearance] * @param {import('react').ReactNode} props.children * @param {string} [props.className] * @param {ComponentColors} [props.color] * @param {ComponentColors} [props.confirmationColor] * @param {import('react').RefObject<HTMLButtonElement>} [props.innerRef] * @param {boolean} [props.isBusy] * @param {boolean} [props.isDisabled] * @param {string} [props.id] * @param {import('react').MouseEventHandler} props.onClick * @param {FormElementSizes} [props.size] * @param {ButtonTypes} [props.type] * @returns {import('react').JSX.Element} */ function ConfirmationButton({ appearance = BUTTON_APPEARANCE.OUTLINED, children, className, color = componentColors.NONE, confirmationColor, id, innerRef, isBusy, isDisabled, onClick, size = "medium", type = BUTTON_TYPES.BUTTON, ...rest }) { const [isClicked, setIsClicked] = useState(false); const resetButton = useCallback(() => { setIsClicked(false); }, []); const handleOnClick = handleEvent((e) => { if (!isBusy) if (isClicked) { onClick?.(e); resetButton(); } else setIsClicked(true); }); const onClickCallback = useCallback(handleOnClick, [handleOnClick]); return /* @__PURE__ */ jsxs("button", { className: joinClassNames("button", className, `button--${appearance}`, color && color !== "none" && !(isClicked && confirmationColor) ? `button--${color}-color` : null, size && size !== formElementSizesEnum.MEDIUM ? `button--${size}` : null, isClicked ? "button--confirm" : null, isClicked && confirmationColor ? `button--${confirmationColor}-color` : null), disabled: isDisabled || isBusy, id, ref: innerRef, onClick: onClickCallback, onBlur: resetButton, onKeyUp: handleKeyPress("Escape", resetButton), type, ...rest, children: [/* @__PURE__ */ jsx(ConfirmationButtonContextProvider, { isClicked, children }), isBusy ? /* @__PURE__ */ jsx(Spinner, { className: "ml-spacing-xs", size: size === formElementSizesEnum.LARGE1X ? 24 : 22, strokeWidth: size === formElementSizesEnum.LARGE1X ? 14 : 12 }) : null] }); } //#endregion //#region react/components/buttons/ConfirmationButton/context/useConfirmationButtonContext.js /** @returns {boolean} */ function useConfirmationButtonContext() { return useContext(ConfirmationButtonContext); } //#endregion //#region react/components/buttons/ConfirmationButton/ConfirmationChildren.jsx /** * @param {object} props * @param {import('react').ReactNode} props.children * @returns {import('react').JSX.Element | null} */ function ConfirmationChildren({ children }) { return useConfirmationButtonContext() ? /* @__PURE__ */ jsx(Fragment, { children }) : null; } //#endregion //#region react/components/buttons/ConfirmationButton/InitialChildren.jsx /** * @param {object} props * @param {import('react').ReactNode} props.children * @returns {import('react').JSX.Element | null} */ function InitialChildren({ children }) { return useConfirmationButtonContext() ? null : /* @__PURE__ */ jsx(Fragment, { children }); } //#endregion //#region react/components/buttons/IconButton.jsx /** @typedef {import('@utahdts/utah-design-system').IconButtonAppearance} IconButtonAppearance */ /** * @param {object} props * @param {IconButtonAppearance} [props.appearance] * @param {import('react').ReactNode} [props.children] * @param {string} [props.className] * @param {'primary' | 'secondary' | 'accent' | 'none'} [props.color] * @param {import('react').ReactNode} props.icon * @param {string} [props.id] * @param {import('react').MutableRefObject<HTMLButtonElement | null>} [props.innerRef] * @param {boolean} [props.isDisabled] * @param {boolean} [props.isTitleVisible] * @param {import('react').MouseEventHandler<HTMLButtonElement>} [props.onClick] what to do when the button is clicked * @param {'small1x' | 'small' | 'medium' | 'large' | 'large1x'} [props.size] * @param {string} props.title A title is used for accessibility purposes to describe the button for screen readers * @param {string | null} [props.tooltipText] * @returns {import('react').JSX.Element} */ function IconButton({ appearance = ICON_BUTTON_APPEARANCE.OUTLINED, children, className, color = componentColors.NONE, icon, id, innerRef: draftInnerRef, isDisabled, isTitleVisible, onClick, size = "medium", title, tooltipText, ...rest }) { const [referenceElement, setReferenceElement] = useState(null); if (draftInnerRef && referenceElement) draftInnerRef.current = referenceElement; return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("button", { className: joinClassNames("button icon-button", className, `${appearance === ICON_BUTTON_APPEARANCE.BORDERLESS ? "icon-button--" : "button--"}${appearance}`, color && color !== "none" ? `button--${color}-color` : null, isTitleVisible ? "icon-button--visible-title" : null, size && size !== formElementSizesEnum.MEDIUM ? `icon-button--${size}` : null), disabled: isDisabled, id: id || void 0, onClick, ref: setReferenceElement, type: "button", ...rest, children: [ icon, /* @__PURE__ */ jsx("span", { className: isTitleVisible ? void 0 : "visually-hidden", children: title }), children ] }), referenceElement ? /* @__PURE__ */ jsx(Tooltip, { referenceElement, children: tooltipText ?? title }) : null] }); } //#endregion //#region react/components/buttons/Tag.jsx /** @typedef {import('@utahdts/utah-design-system').FormElementSizes} FormElementSizes */ /** * @param {object} props * @param {import('react').ReactNode} props.children most often is the title of the tag, but can also contain most anything * @param {string} [props.className] * @param {string} [props.clearMessage] the message to show when hover the "x" icon * @param {string} [props.id] the tag id * @param {object} [props.iconButtonProps] props for the icon button * @param {import('react').RefObject<HTMLDivElement>} [props.innerRef] a ref to attach to the actual DOM <button> or <span> element * @param {import('react').ReactNode} [props.iconLeft] an icon for the left side * @param {import('react').ReactNode} [props.iconRight] an icon for the right side * @param {boolean} [props.isDisabled] tag isDisabled state * @param {import('react').MouseEventHandler<HTMLButtonElement>} [props.onClear] * @param {FormElementSizes} [props.size] * @returns {import('react').JSX.Element} */ function Tag({ children, className, clearMessage = "Clear Tag", iconButtonProps = {}, innerRef, iconLeft, iconRight, isDisabled, id, onClear, size = formElementSizesEnum.MEDIUM, ...rest }) { return /* @__PURE__ */ jsxs("div", { className: joinClassNames("tag__wrapper", onClear && "tag--clearable"), ref: innerRef, ...rest, children: [/* @__PURE__ */ jsxs("span", { className: joinClassNames("tag", className, `tag--${size}`), id, children: [ iconLeft ? /* @__PURE__ */ jsx("span", { className: "tag--icon tag--icon-left", children: iconLeft }) : null, children, iconRight ? /* @__PURE__ */ jsx("span", { className: "tag--icon tag--icon-right", children: iconRight }) : null ] }), onClear ? /* @__PURE__ */ jsx(IconButton, { className: "tag__clear-button icon-button--borderless icon-button--small1x", icon: /* @__PURE__ */ jsx("span", { className: "utds-icon-before-x-icon", "aria-hidden": true }), onClick: onClear, title: clearMessage, isDisabled, ...iconButtonProps }) : null] }); } //#endregion //#region react/components/containers/accordion/Accordion.jsx /** * @param {object} props * @param {import('react').ReactNode} props.children * @param {string} [props.className] * @param {string} [props.contentClassName] * @param {number} [props.headingLevel] * @param {string} [props.headerClassName] * @param {import('react').ReactNode} props.headerContent * @param {string} props.id * @param {boolean} [props.isOpen] * @param {(previousIsOpen: boolean) => void} [props.onToggle] * @returns {import('react').JSX.Element} */ function Accordion({ children, className, contentClassName, headingLevel = 2, headerClassName, headerContent, id, isOpen, onToggle }) { const [stateIsOpen, setStateIsOpen] = useState(isOpen || false); useEffect(() => { setStateIsOpen(!!isOpen); }, [isOpen]); function toggleAccordion() { if (onToggle) onToggle(stateIsOpen); else setStateIsOpen(!stateIsOpen); } const HeadingTag = `h${headingLevel}`; return /* @__PURE__ */ jsxs("div", { className: joinClassNames(["accordion", className]), id, children: [/* @__PURE__ */ jsxs("button", { "aria-expanded": stateIsOpen, "aria-controls": `accordion-content__${id}`, className: joinClassNames([ "accordion__header", headerClassName, stateIsOpen ? "accordion__header--open" : "" ]), id: `accordion-button__${id}`, onClick: handleEvent(toggleAccordion), type: "button", children: [/* @__PURE__ */ jsx(HeadingTag, { id: `accordion-heading__${id}`, children: headerContent }), /* @__PURE__ */ jsx("span", { className: `utds-icon-before-circle-chevron-up icon-button__icon ${stateIsOpen ? "" : "icon-button__icon--rotate180"}`, "aria-hidden": "true" })] }), /* @__PURE__ */ jsx("div", { "aria-hidden": !stateIsOpen, "aria-labelledby": `accordion-button__${id}`, inert: !stateIsOpen, className: joinClassNames([ "accordion__content", contentClassName, stateIsOpen ? "accordion__content--open" : "" ]), id: `accordion-content__${id}`, role: "region", children })] }); } //#endregion //#region react/contexts/UtahDesignSystemContext/UtahDesignSystemContext.js /** @typedef {import('@utahdts/utah-design-system').UtahDesignSystemContextValue} UtahDesignSystemContextValue */ /** @typedef {import('use-immer').ImmerHook<UtahDesignSystemContextValue>} ImmerHookUtahDesignSystemContext */ var UtahDesignSystemContext = createContext([{ ariaLive: { assertiveMessages: [], politeMessages: [] }, banners: [] }, () => {}]); //#endregion //#region react/contexts/UtahDesignSystemContext/useUtahDesignSystemContext.js /** @typedef {import('@utahdts/utah-design-system').UtahDesignSystemContextValue} UtahDesignSystemContextValue */ /** @typedef {import('use-immer').ImmerHook<UtahDesignSystemContextValue>} ImmerHookUtahDesignSystemContext */ /** @returns {ImmerHookUtahDesignSystemContext} */ function useUtahDesignSystemContext() { return useContext(UtahDesignSystemContext); } //#endregion //#region react/contexts/UtahDesignSystemContext/hooks/useAriaMessaging.js /** @returns {{addAssertiveMessage: (message: string) => void, addPoliteMessage: (message: string) => void}} */ function useAriaMessaging() { const [, setState] = useUtahDesignSystemContext(); const addPoliteMessage = useCallback( /** * @param {string} message * @returns {void} */ (message) => { setState((draftState) => { draftState.ariaLive.politeMessages.push(message); }); }, [setState] ); const addAssertiveMessage = useCallback( /** * @param {string} message * @returns {void} */ (message) => { setState((draftState) => { draftState.ariaLive.assertiveMessages.push(message); }); }, [setState] ); return useMemo(() => ({ addAssertiveMessage, addPoliteMessage }), [addAssertiveMessage, addPoliteMessage]); } //#endregion //#region react/util/getFocusableElements.js /** * Based on the list from https://api.jqueryui.com/tabbable-selector/ * Used to get a list of focusable elements within a modal component * @param {HTMLDialogElement} element * @returns {HTMLElement[]} */ function getFocusableElements(element) { return [...element.querySelectorAll("a[href], area[href], button, input, textarea, select, object, [tabindex]:not([tabindex=\"-1\"])")].filter((item) => !item.hasAttribute("disabled")); } //#endregion //#region react/hooks/useHandleEscape.js /** * @param {import('react').KeyboardEventHandler} [onEscape] * @returns {(...args: any[]) => void} func */ function useHandleEscape(onEscape) { return useCallback((e) => { if (e.code === "Escape" || e.key === "Escape") { e.preventDefault(); e.stopPropagation(); if (onEscape) onEscape(e); } }, [onEscape]); } //#endregion //#region react/hooks/useHandleTab.js /** * @param {HTMLElement | undefined} firstTabElement * @param {HTMLElement | undefined} lastTabElement * @returns {(...args: any[]) => void} func */ function useHandleTab(firstTabElement, lastTabElement) { return useCallback((e) => { if (e.code === "Tab" || e.key === "Tab") { if (e.shiftKey) { if (document.activeElement === firstTabElement) { lastTabElement?.focus(); e.preventDefault(); } } else if (document.activeElement === lastTabElement) { firstTabElement?.focus(); e.preventDefault(); } } }, [firstTabElement, lastTabElement]); } //#endregion //#region react/enums/drawerPlacement.js /** @typedef {import('@utahdts/utah-design-system').DrawerPlacement} DrawerPlacement */ /** * Positions for banners * @enum {DrawerPlacement} */ var DRAWER_PLACEMENT = { RIGHT: "drawer--right", LEFT: "drawer--left" }; //#endregion //#region react/components/containers/drawer/Drawer.jsx /** @typedef {import('@utahdts/utah-design-system').DrawerPlacement} DrawerPlacement */ /** * @param {object} props * @param {string} props.ariaLabelledBy Must match the id of the title of the drawer * @param {import('react').ReactNode} props.children * @param {string} [props.className] * @param {string} props.id * @param {import('react').Ref<HTMLDivElement>} [props.innerRef] * @param {import('react').KeyboardEventHandler} [props.onEscape] * @param {import('react').MouseEventHandler} [props.onClose] * @param {DrawerPlacement} [props.position] * @returns {React.JSX.Element} */ function Drawer({ ariaLabelledBy, children, className, id, innerRef, onClose, onEscape, position = DRAWER_PLACEMENT.RIGHT }) { const ref = useRef(null); const [lastActiveElement] = useImmer( /** @type {HTMLElement | undefined} */ document.activeElement ); const [firstTabElement, setFirstTabElement] = useImmer( /** @type {HTMLElement | undefined} */ void 0 ); const [lastTabElement, setLastTabElement] = useImmer( /** @type {HTMLElement | undefined} */ void 0 ); const { addAssertiveMessage } = useAriaMessaging(); const handleEscape = useHandleEscape(onEscape); const handleTab = useHandleTab(firstTabElement, lastTabElement); useEffect(() => { if (ref.current) { const list = getFocusableElements(ref.current); if (list.length) { const firstElement = list[0]; setFirstTabElement(firstElement); const lastElement = list[list.length - 1]; setLastTabElement(lastElement); firstElement?.focus(); } else console.warn("No focusable element found. Make sure to include a way to close the drawer."); } }, []); useEffect(() => () => { addAssertiveMessage("Closing drawer."); lastActiveElement?.focus(); }, []); return /* @__PURE__ */ jsx("div", { className: "drawer-wrapper", ref: innerRef, children: /* @__PURE__ */ jsx("div", { className: "drawer__backdrop backdrop-dark", onClick: onClose, children: /* @__PURE__ */ jsxs("dialog", { "aria-labelledby": ariaLabelledBy, className: joinClassNames("drawer__inner", position, className), id, onClick: (e) => e.stopPropagation(), onKeyDown: handleTab, onKeyUp: handleEscape, ref, children: [children, onClose ? /* @__PURE__ */ jsx(IconButton, { appearance: ICON_BUTTON_APPEARANCE.BORDERLESS, className: "drawer__close-button", icon: /* @__PURE__ */ jsx("span", { className: "utds-icon-before-x-icon", "aria-hidden": "true" }), onClick: onClose, size: "small", title: "Close" }) : void 0] }) }) }); } //#endregion //#region react/components/containers/drawer/DrawerContent.jsx /** * @param {object} props * @param {import('react').ReactNode} props.children * @param {string} [props.className] * @param {string} [props.id] * @returns {import('react').JSX.Element} */ function DrawerContent({ children, className, id }) { return /* @__PURE__ */ jsx("div", { className: joinClassNames("drawer__content", className), id, children }); } //#endregion //#region react/components/containers/drawer/DrawerFooter.jsx /** * @param {object} props * @param {import('react').ReactNode} props.children * @param {string} [props.className] * @param {string} [props.id] * @returns {import('react').JSX.Element} */ function DrawerFooter({ children, className, id }) { return /* @__PURE__ */ jsx("div", { className: joinClassNames("drawer__footer", className), id, children }); } //#endregion //#region react/components/containers/drawer/DrawerTitle.jsx /** * @param {object} props * @param {import('react').ReactNode} props.children * @param {string} [props.className] * @param {string} props.id Make sure to match the ariaLabelledBy of the drawer * @param {string} [props.tagName] * @returns {import('react').JSX.Element} */ function DrawerTitle({ children, className, id, tagName: TagName = "div" }) { return /* @__PURE__ */ jsx(TagName, { className: joinClassNames("drawer__title", className), id, children }); } //#endregion //#region react/components/containers/tabs/context/TabGroupContext.jsx /** @typedef {import('@utahdts/utah-design-system').TabGroupContextValue} TabGroupContextValue */ var TabGroupContext = createContext({ isVertical: false, navigateNext: () => {}, navigatePrevious: () => {}, registerTab: () => {}, selectedTabId: "", setSelectedTabId: () => {}, tabGroupId: "", unRegisterTab: () => {} }); //#endregion //#region react/components/containers/tabs/context/useTabGroupContext.js /** @typedef { import('@utahdts/utah-design-system').TabGroupContextValue} TabGroupContextType */ /** @returns {TabGroupContextType} */ function useTabGroupContext() { return useContext(TabGroupContext); } //#endregion //#region react/components/containers/tabs/functions/generateTabId.js /** * @param {string} tabGroupId * @param {string} tabId * @returns {string} */ function generateTabId(tabGroupId, tabId) { return `tab__${tabGroupId}__${tabId}`; } //#endregion //#region react/components/containers/tabs/Tab.jsx /** * @param {object} props * @param {import('react').ReactNode} props.children * @param {string} props.id * @returns {import('react').JSX.Element} */ function Tab({ children, id }) { const tabRef = useRef( /** @type {HTMLButtonElement | null} */ null ); const { isVertical, navigateNext, navigatePrevious, registerTab, selectedTabId, setSelectedTabId, tabGroupId, unRegisterTab } = useTabGroupContext(); const onKeyChange = (event) => { if ([ "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown" ].includes(event.key)) event.preventDefault(); switch (event.key) { case "ArrowLeft": if (!isVertical) navigatePrevious(); break; case "ArrowRight": if (!isVertical) navigateNext(); break; case "ArrowUp": if (isVertical) navigatePrevious(); break; case "ArrowDown": if (isVertical) navigateNext(); break; default: break; } }; useEffect(() => { if (tabRef) registerTab(tabRef); }, []); useEffect(() => (() => { unRegisterTab(tabRef); }), []); return /* @__PURE__ */ jsx("div", { className: joinClassNames(selectedTabId === id && "tab-group__tab--selected", "tab-group__tab"), role: "presentation", children: /* @__PURE__ */ jsx("button", { "aria-controls": `tabpanel__${tabGroupId}__${id}`, "aria-selected": selectedTabId === id, className: joinClassNames(selectedTabId === id && "tab-group__tab-button--selected", "tab-group__tab-button"), id: generateTabId(tabGroupId, id), onClick: handleEvent(() => setSelectedTabId(id)), onKeyDown: onKeyChange, ref: tabRef, role: "tab", tabIndex: selectedTabId === id ? 0 : -1, type: "button", children }) }); } //#endregion //#region react/components/containers/tabs/TabGroup.jsx /** @typedef {import('@utahdts/utah-design-system').TabGroupContextValue} TabGroupContextValue */ /** * @param {object} props * @param {import('react').ReactNode} props.children * @param {string} [props.className] * @param {string} [props.defaultValue] * @param {boolean} [props.isVertical] * @param {(newTabId: string) => void} [props.onChange] * @param {string} [props.value] * @returns {import('react').JSX.Element} */ function TabGroup({ children, className, defaultValue, isVertical, onChange, value }) { const tabGroupId = useId(); const tabGroupRef = useRef( /** @type {HTMLDivElement | null} */ null ); const [tabGroupState, setTabGroupState] = useImmer(() => ({ selectedTabId: defaultValue || "", tabGroupId, tabs: [] })); const navigateTab = useCallback((tab) => { if (tab) { tab?.focus(); tab?.click(); } }, []); const findCurrentTabIndex = useCallback(() => tabGroupState.tabs.findIndex((tab) => tab?.id === generateTabId(tabGroupState.tabGroupId, tabGroupState.selectedTabId)), [tabGroupState]); const registerTab = useCallback((tab) => { setTabGroupState((draftState) => { const checkTab = draftState.tabs.find((tabSearch) => tabSearch?.id === tab?.current?.id); if (checkTab) Object.assign(checkTab, tab?.current); else draftState.tabs.push(tab?.current); }); }, [tabGroupState, setTabGroupState]); const unRegisterTab = useCallback((tab) => { setTabGroupState((draftState) => { draftState.tabs = draftState.tabs.filter((filterTab) => filterTab?.id !== tab?.current?.id); }); }, []); useEffect(() => { if (value !== void 0) setTabGroupState((draftState) => { draftState.selectedTabId = value; }); }, [value]); /** @type {TabGroupContextValue} */ const contextValue = useMemo(() => ({ isVertical: !!isVertical, navigateNext() { const nextIndex = (findCurrentTabIndex() + 1) % tabGroupState.tabs.length; navigateTab(tabGroupState?.tabs?.[nextIndex] || null); }, navigatePrevious() { const nextIndex = (findCurrentTabIndex() + tabGroupState.tabs.length - 1) % tabGroupState.tabs.length; navigateTab(tabGroupState?.tabs?.[nextIndex] || null); }, registerTab, selectedTabId: value || tabGroupState.selectedTabId || "", setSelectedTabId(tabId) { if (onChange) onChange(tabId); else setTabGroupState((draftState) => { draftState.selectedTabId = tabId; }); }, tabGroupId: tabGroupState.tabGroupId, unRegisterTab }), [tabGroupState, isVertical]); return /* @__PURE__ */ jsx(TabGroupContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx("div", { className: joinClassNames("tab-group", className, isVertical && "tab-group--vertical"), id: `tab-group__${tabGroupState.tabGroupId}`, ref: tabGroupRef, children }) }); } //#endregion //#region react/components/containers/tabs/TabGroupTitle.jsx /** * @param {object} props * @param {import('react').ReactNode} props.children * @param {string} [props.className] * @param {string} [props.tagName] * @returns {import('react').JSX.Element} */ function TabGroupTitle({ children, className, tagName: TagName = "div" }) { const { tabGroupId } = useTabGroupContext(); return /* @__PURE__ */ jsx(TagName, { id: `tab-group__${tabGroupId}`, className: joinClassNames("tag-group__title", className), children }); } //#endregion //#region react/components/containers/tabs/TabList.jsx /** * @param {object} props * @param {import('react').ReactNode} props.children * @param {string} [props.className] * @returns {import('react').JSX.Element} */ function TabList({ children, className }) { const { isVertical } = useTabGroupContext(); return /* @__PURE__ */ jsx("div", { className: joinClassNames(className, "tab-group__list"), role: "tablist", "aria-orientation": isVertical ? "vertical" : "horizontal", children }); } //#endregion //#region react/components/containers/tabs/TabPanel.jsx /** * @param {object} props * @param {import('react').ReactNode} props.children * @param {string} [props.className] * @param {string} props.tabId * @returns {import('react').JSX.Element} */ function TabPanel({ children, className, tabId }) { const { selectedTabId, tabGroupId } = useTabGroupContext(); return /* @__PURE__ */ jsx("div", { "aria-labelledby": `tab__${tabGroupId}__${tabId}`, className: joinClassNames(className, selectedTabId !== tabId && "visually-hidden", "tab-group__panel"), id: `tabpanel__${tabGroupId}__${tabId}`, role: "tabpanel", tabIndex: selectedTabId === tabId ? 0 : -1, inert: selectedTabId !== tabId, children }); } //#endregion //#region react/components/containers/tabs/TabPanels.jsx /** * @param {object} props * @param {import('react').ReactNode} props.children * @returns {import('react').JSX.Element} */ function TabPanels({ children }) { return /* @__PURE__ */ jsx("div", { className: "tab-group__panels", role: "presentation", children }); } //#endregion //#region react/components/footer/FooterAgencyInformation.jsx /** * @param {object} props * @param {import('react').ReactNode} props.children * @returns {import('react').JSX.Element} */ function FooterAgencyInformation({ children }) { return /* @__PURE__ */ jsx("div", { className: "utah-design-system", children: /* @__PURE__ */ jsx("div", { className: "footer-agency-information", children }) }); } //#endregion //#region react/components/footer/FooterAgencyInformationColumn.jsx /** * @param {object} props * @param {import('react').ReactNode} props.children * @returns {import('react').JSX.Element} */ function FooterAgencyInformationColumn({ children }) { return /* @__PURE__ */ jsx("div", { className: "footer-agency-information__column", children }); } //#endregion //#region react/components/footer/FooterAgencyInformationInfo.jsx /** @typedef {import('@utahdts/utah-design-system').Address} Address */ /** * @param {object} props * @param {string} props.agencyTitleFirstLine ie Utah Department of (smaller font above main title) * @param {string} [props.agencyTitleSecondLine] ie Government Operations (larger font below firstLine) * @param {Address} props.address * @param {string} props.email * @param {import('react').ReactNode} props.logo * @param {string} [props.phone] * @returns {import('react').JSX.Element} */ function FooterAgencyInformationInfo({ agencyTitleFirstLine, agencyTitleSecondLine, address, email, logo, phone }) { return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsxs("div", { className: "footer-agency-information__title-wrapper", children: [/* @__PURE__ */ jsx("div", { className: "footer-agency-information__title-image", children: logo }), /* @__PURE__ */ jsxs("div", { className: "footer-agency-information__title", children: [/* @__PURE__ */ jsx("div", { className: "footer-agency-information__first-line", children: agencyTitleFirstLine }), /* @__PURE__ */ jsx("div", { className: "footer-agency-information__second-line", children: agencyTitleSecondLine })] })] }), /* @__PURE__ */ jsxs("div", { className: "footer-agency-information__address", children: [ /* @__PURE__ */ jsx("span", { className: "footer-agency-information__address-street-address-1", children: address.streetAddress1 }), /* @__PURE__ */ jsx("br", {}), address.streetAddress2 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", { className: "footer-agency-information__address-street-address-2", children: address.streetAddress2 }), /* @__PURE__ */ jsx("br", {})] }) : void 0, /* @__PURE__ */ jsxs("span", { className: "footer-agency-information__address-city-state-zip", children: [ address.city, ", ", address.state, " ", address.zipCode ] }) ] }), /* @__PURE__ */ jsx("div", { className: "footer-agency-information__email", children: /* @__PURE__ */ jsx("a", { href: `mailto:${email}`, children: email }) }), phone && /* @__PURE__ */ jsx("div", { className: "footer-agency-information__phone", children: /* @__PURE__ */ jsx("a", { href: `tel:${phone}`, children: phone }) }) ] }); } //#endregion //#region react/components/footer/FooterSocialMediaBar.jsx /** * @param {object} props * @param {import('react').ReactNode} props.children * @param {string | null} props.title * @returns {import('react').JSX.Element} */ function FooterSocialMediaBar({ children, title = "Follow us online" }) { return /* @__PURE__ */ jsx("div", { className: "utah-design-system", children: /* @__PURE__ */ jsxs("div", { className: "footer-social-media-bar", children: [/* @__PURE__ */ jsx("div", { className: "footer-social-media-bar__follow-us", children: title }), /* @__PURE__ */ jsx("div", { className: "footer-social-media-bar__icon-bar", children })] }) }); } //#endregion //#region react/util/useOnKeyUp.js /** * @template KeyboardEventElementT * @param {string} targetKey which key to watch for (ie 'Enter') https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values * @param {import('react').KeyboardEventHandler<KeyboardEventElementT>} func the function to call when the given key is pressed * @param {boolean} [stopPropagation] * @returns {(event: React.KeyboardEvent<KeyboardEventElementT>) => boolean} function that checks for the keypress and fires function when pressed */ function useOnKeyUp(targetKey, func, stopPropagation) { return useCallback((e) => { const isMatchingKey = e.key === targetKey; if (isMatchingKey) { if (stopPropagation) { e.stopPropagation(); e.preventDefault(); } func(e); } return isMatchingKey; }, [ func, stopPropagation, targetKey ]); } //#endregion //#region react/components/forms/ErrorMessage.jsx /** * @param {object} props * @param {string} [props.errorMessage] * @param {string} props.id * @returns {import('react').JSX.Element | null} */ function ErrorMessage({ errorMessage, id }) { return errorMessage ? /* @__PURE__ */ jsx("div", { className: "input-wrapper__error-message", id: `error__${id}`, children: errorMessage }) : null; } //#endregion //#region react/components/forms/RequiredStar.jsx function RequiredStar() { return /* @__PURE__ */ jsx("span", { className: "required-star", "aria-hidden": true, children: "*" }); } //#endregion //#region react/components/forms/CalendarInput/calendarGrid.js /** @typedef {import('@utahdts/utah-design-system').CalendarGridValue} CalendarGridValue */ /** @typedef {import('@utahdts/utah-design-system').CalendarGridMonth} CalendarGridMonth */ /** * @param {Date} dateA * @param {Date} dateB * @returns {number} negative, 0, or positive indicative of sort order */ function dateIsEqualYM(dateA, dateB) { return dateA.getFullY