diff --git a/packages/odyssey-react-mui/src/createContrastColors.ts b/packages/odyssey-react-mui/src/createContrastColors.ts new file mode 100644 index 000000000..e2ca332e8 --- /dev/null +++ b/packages/odyssey-react-mui/src/createContrastColors.ts @@ -0,0 +1,106 @@ +/*! + * Copyright (c) 2022-present, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + +import { DesignTokens } from "./OdysseyDesignTokensContext"; + +type RgbColorObject = { + red: number; + green: number; + blue: number; +}; + +export type ContrastColors = { + focusRingColor: string | undefined; + fontColor: string | undefined; + itemDisabledFontColor: string | undefined; + itemHoverBackgroundColor: string | undefined; + itemSelectedBackgroundColor: string | undefined; +}; + +const hexToRgb = (hexBackgroundColor: string): RgbColorObject | undefined => { + const formattedHexString = hexBackgroundColor.includes("#") + ? hexBackgroundColor.split("#")[1] + : hexBackgroundColor; + + return { + red: parseInt(formattedHexString.slice(0, 2), 16), + green: parseInt(formattedHexString.slice(2, 4), 16), + blue: parseInt(formattedHexString.slice(4, 6), 16), + }; +}; + +export const generateContrastColors = ( + backgroundColor: string, + odysseyDesignTokens: DesignTokens, +) => { + // Convert hex to RGB + const rgbFromHex = hexToRgb(backgroundColor); + + if (rgbFromHex) { + const { red, green, blue } = rgbFromHex; + + // Calculate relative luminance + // @see https://contrastchecker.online/color-relative-luminance-calculator#:~:text=For%20the%20sRGB%20colorspace%2C%20the,%2B0.055)%2F1.055)%20%5E%202.4 + // returns a number between 0(black) and 255(white) + const luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue; + + // 128 is a magic number. This feels like roughly where we should switch from dark to light. + const LUMINANCE_THRESHOLD = 128; + const LUMINANCE_EDGE_MIN = 108; + const LUMINANCE_EDGE_MAX = 142; + + // Luminance values between LUMINANCE_EDGE_MIN-LUMINANCE_EDGE_MAX can cause contrast ration issues + // Using #000000 helps in these cases + const luminanceValueInEdgeRange = + luminance > LUMINANCE_EDGE_MIN && luminance < LUMINANCE_EDGE_MAX; + + // Determine if the color is light or dark. + const isLight = luminance > LUMINANCE_THRESHOLD; + + const fontColor = luminanceValueInEdgeRange + ? "#000000" + : isLight + ? odysseyDesignTokens.TypographyColorBody + : odysseyDesignTokens.HueNeutralWhite; + + const calculatedFontColorInRgb = hexToRgb(fontColor); + const lightFontColorInRgb = hexToRgb(odysseyDesignTokens.HueNeutralWhite); + const darkFontColorInRgb = hexToRgb( + odysseyDesignTokens.TypographyColorBody, + ); + + const calculatedFontRgbString = `${calculatedFontColorInRgb?.red}, ${calculatedFontColorInRgb?.green}, ${calculatedFontColorInRgb?.blue}`; + const lightFontRgbString = `${lightFontColorInRgb?.red}, ${lightFontColorInRgb?.green}, ${lightFontColorInRgb?.blue}`; + const darkFontRgbString = `${darkFontColorInRgb?.red}, ${darkFontColorInRgb?.green}, ${darkFontColorInRgb?.blue}`; + + const getHighlightColor: ( + luminanceValueInEdgeRange: boolean, + isLight: boolean, + ) => string = (luminanceValueInEdgeRange, isLight) => { + if (luminanceValueInEdgeRange) { + return isLight ? darkFontRgbString : lightFontRgbString; + } + + return calculatedFontRgbString; + }; + + return { + fontColor, + focusRingColor: `rgba(${calculatedFontRgbString}, .8)`, + itemDisabledFontColor: `rgba(${calculatedFontRgbString}, .4)`, + itemHoverBackgroundColor: `rgba(${getHighlightColor(luminanceValueInEdgeRange, isLight)}, .1)`, + itemSelectedBackgroundColor: `rgba(${getHighlightColor(luminanceValueInEdgeRange, isLight)}, .15)`, + }; + } + + return undefined; +}; diff --git a/packages/odyssey-react-mui/src/labs/SideNav/NavAccordion.tsx b/packages/odyssey-react-mui/src/labs/SideNav/NavAccordion.tsx index f2ccc3416..73fbb445a 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/NavAccordion.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/NavAccordion.tsx @@ -27,6 +27,26 @@ import { } from "../../OdysseyDesignTokensContext"; import { Support } from "../../Typography"; import { useUniqueId } from "../../useUniqueId"; +import { + UiShellColors, + useUiShellContrastColorContext, +} from "../../ui-shell/UiShell/UiShellColorsProvider"; +import { ContrastColors } from "../../createContrastColors"; + +const SideNavAccordionContainer = styled("div", { + shouldForwardProp: (prop) => + prop !== "backgroundColor" && prop !== "fontColor", +})<{ + backgroundColor?: UiShellColors["sideNavBackgroundColor"]; + fontColor?: ContrastColors["fontColor"]; +}>(({ backgroundColor, fontColor }) => ({ + width: "100%", + + ".MuiAccordion-root": { + backgroundColor: backgroundColor, + color: fontColor || "inherit", + }, +})); export type NavAccordionProps = { /** @@ -66,47 +86,95 @@ export type NavAccordionProps = { const AccordionLabelContainer = styled("span", { shouldForwardProp: (prop) => - prop !== "odysseyDesignTokens" && prop !== "isIconVisible", + prop !== "odysseyDesignTokens" && + prop !== "isIconVisible" && + prop !== "sideNavContrastColors", })<{ - odysseyDesignTokens: DesignTokens; + sideNavContrastColors?: UiShellColors["sideNavContrastColors"]; isIconVisible: boolean; -}>(({ odysseyDesignTokens, isIconVisible }) => ({ + odysseyDesignTokens: DesignTokens; +}>(({ sideNavContrastColors, odysseyDesignTokens, isIconVisible }) => ({ width: "100%", marginInlineStart: isIconVisible ? odysseyDesignTokens.Spacing3 : 0, fontWeight: odysseyDesignTokens.TypographyWeightHeading, - color: odysseyDesignTokens.TypographyColorHeading, + color: + sideNavContrastColors?.fontColor || + odysseyDesignTokens.TypographyColorHeading, + + ".Mui-disabled &": { + color: odysseyDesignTokens.TypographyColorDisabled, + + ...(sideNavContrastColors?.itemDisabledFontColor && { + color: sideNavContrastColors?.itemDisabledFontColor, + }), + }, })); const AccordionSummaryContainer = styled(MuiAccordionSummary, { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens" && prop !== "isCompact" && - prop !== "isDisabled", + prop !== "isDisabled" && + prop !== "sideNavContrastColors", })<{ - odysseyDesignTokens: DesignTokens; + sideNavContrastColors?: UiShellColors["sideNavContrastColors"]; isCompact?: boolean; isDisabled?: boolean; -}>(({ odysseyDesignTokens, isCompact, isDisabled }) => ({ + odysseyDesignTokens: DesignTokens; +}>(({ odysseyDesignTokens, sideNavContrastColors, isCompact, isDisabled }) => ({ borderRadius: odysseyDesignTokens.BorderRadiusMain, paddingBlock: odysseyDesignTokens.Spacing3, paddingInline: odysseyDesignTokens.Spacing4, + ...(isDisabled && { + opacity: "1 !important", + + ...(sideNavContrastColors?.itemDisabledFontColor && { + svg: { + path: { + fill: `${sideNavContrastColors.itemDisabledFontColor} !important`, + }, + }, + }), + }), + + ...(!isDisabled && { + "&:hover": { + backgroundColor: odysseyDesignTokens.HueNeutral50, + }, + }), + + ...(!isDisabled && + sideNavContrastColors?.fontColor && { + svg: { + path: { + fill: `${sideNavContrastColors.fontColor} !important`, + }, + }, + }), + + ...(sideNavContrastColors?.itemHoverBackgroundColor && { + ...(!isDisabled && { + "&:hover": { + backgroundColor: sideNavContrastColors.itemHoverBackgroundColor, + }, + }), + }), + "&:focus-visible": { backgroundColor: "unset", outline: "none", boxShadow: `inset 0 0 0 2px ${odysseyDesignTokens.PalettePrimaryMain}`, + + ...(sideNavContrastColors?.focusRingColor && { + boxShadow: `inset 0 0 0 2px ${sideNavContrastColors.focusRingColor}`, + }), }, ...(isCompact && { paddingBlock: odysseyDesignTokens.Spacing2, minHeight: "unset", }), - - ...(!isDisabled && { - "&:hover": { - backgroundColor: odysseyDesignTokens.HueNeutral50, - }, - }), })); const NavAccordion = ({ @@ -124,41 +192,48 @@ const NavAccordion = ({ const headerId = `${id}-header`; const contentId = `${id}-content`; const odysseyDesignTokens = useOdysseyDesignTokens(); + const shellContrastColors = useUiShellContrastColorContext(); return ( - - } - id={headerId} - odysseyDesignTokens={odysseyDesignTokens} - isCompact={isCompact} - isDisabled={isDisabled} - > - - {startIcon && startIcon} - - {label} - - - - - {children} - - + } + sideNavContrastColors={shellContrastColors?.sideNavContrastColors} + id={headerId} + isCompact={isCompact} + isDisabled={isDisabled} + odysseyDesignTokens={odysseyDesignTokens} + > + + {startIcon && startIcon} + + {label} + + + + + {children} + + + ); }; diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index b8bb3b7a0..4ddbd25db 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -23,6 +23,7 @@ import { import { Skeleton } from "@mui/material"; import { useTranslation } from "react-i18next"; +import { ContrastColors } from "../../createContrastColors"; import { NavAccordion } from "./NavAccordion"; import { DesignTokens, @@ -42,6 +43,10 @@ import { SortableList } from "./SortableList/SortableList"; import { Overline } from "../../Typography"; // eslint-disable-next-line import/no-extraneous-dependencies import { arrayMove } from "@dnd-kit/sortable"; +import { + UiShellColors, + useUiShellContrastColorContext, +} from "../../ui-shell/UiShell/UiShellColorsProvider"; export const DEFAULT_SIDE_NAV_WIDTH = "300px"; @@ -101,19 +106,30 @@ const StyledOpacityTransitionContainer = styled("div", { const StyledSideNav = styled("nav", { shouldForwardProp: (prop) => - prop !== "odysseyDesignTokens" && prop !== "isSideNavCollapsed", + prop !== "backgroundColor" && + prop !== "odysseyDesignTokens" && + prop !== "isAppContentWhiteBackground" && + prop !== "isSideNavCollapsed", })( ({ - odysseyDesignTokens, + backgroundColor, + isAppContentWhiteBackground, isSideNavCollapsed, + odysseyDesignTokens, }: { - odysseyDesignTokens: DesignTokens; + isAppContentWhiteBackground: boolean; + backgroundColor?: UiShellColors["sideNavBackgroundColor"]; isSideNavCollapsed: boolean; + odysseyDesignTokens: DesignTokens; }) => ({ position: "relative", display: "inline-block", height: "100%", - backgroundColor: odysseyDesignTokens.HueNeutralWhite, + backgroundColor: backgroundColor || odysseyDesignTokens.HueNeutralWhite, + + ...(isAppContentWhiteBackground && { + borderRight: `${odysseyDesignTokens.BorderWidthMain} ${odysseyDesignTokens.BorderStyleMain} ${odysseyDesignTokens.HueNeutral100}`, + }), "&::after": { backgroundColor: odysseyDesignTokens.HueNeutral200, @@ -155,12 +171,16 @@ const StyledSideNav = styled("nav", { const SideNavHeaderContainer = styled("div", { shouldForwardProp: (prop) => - prop !== "hasContentScrolled" && prop !== "odysseyDesignTokens", + prop !== "borderColor" && + prop !== "hasContentScrolled" && + prop !== "odysseyDesignTokens", })( ({ + borderColor, hasContentScrolled, odysseyDesignTokens, }: { + borderColor: ContrastColors["fontColor"]; hasContentScrolled: boolean; odysseyDesignTokens: DesignTokens; }) => ({ @@ -168,7 +188,11 @@ const SideNavHeaderContainer = styled("div", { // The bottom border should appear only if the scrollable region has been scrolled ...(hasContentScrolled && ({ - borderBottom: `${odysseyDesignTokens.BorderWidthMain} ${odysseyDesignTokens.BorderStyleMain} ${odysseyDesignTokens.HueNeutral50}`, + borderBottom: `${odysseyDesignTokens.BorderWidthMain} ${odysseyDesignTokens.BorderStyleMain} ${odysseyDesignTokens.HueNeutral100}`, + + ...(borderColor && { + borderBottom: `${odysseyDesignTokens.BorderWidthMain} ${odysseyDesignTokens.BorderStyleMain} ${borderColor + 15}`, + }), } satisfies CSSObject)), }), ); @@ -191,21 +215,43 @@ const SideNavScrollableContainer = styled("div", { })); const SectionHeaderContainer = styled("li", { - shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", -})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ - paddingBlock: odysseyDesignTokens.Spacing1, - paddingInline: odysseyDesignTokens.Spacing4, - marginBlock: `${odysseyDesignTokens.Spacing3}`, - color: odysseyDesignTokens.HueNeutral600, -})); + shouldForwardProp: (prop) => + prop !== "odysseyDesignTokens" && prop !== "contrastFontColor", +})( + ({ + contrastFontColor, + odysseyDesignTokens, + }: { + contrastFontColor: ContrastColors["fontColor"]; + odysseyDesignTokens: DesignTokens; + }) => ({ + paddingBlock: odysseyDesignTokens.Spacing1, + paddingInline: odysseyDesignTokens.Spacing4, + marginBlock: `${odysseyDesignTokens.Spacing3}`, + color: contrastFontColor || odysseyDesignTokens.HueNeutral600, + }), +); const SideNavFooter = styled("div", { - shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", -})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ - flexShrink: 0, - padding: odysseyDesignTokens.Spacing4, - backgroundColor: odysseyDesignTokens.HueNeutralWhite, -})); + shouldForwardProp: (prop) => + prop !== "odysseyDesignTokens" && prop !== "sideNavBackgroundColor", +})( + ({ + odysseyDesignTokens, + sideNavBackgroundColor, + }: { + odysseyDesignTokens: DesignTokens; + sideNavBackgroundColor?: UiShellColors["sideNavBackgroundColor"]; + }) => ({ + flexShrink: 0, + padding: odysseyDesignTokens.Spacing4, + backgroundColor: odysseyDesignTokens.HueNeutralWhite, + + ...(sideNavBackgroundColor && { + backgroundColor: sideNavBackgroundColor, + }), + }), +); const PersistentSideNavFooter = styled(SideNavFooter, { shouldForwardProp: (prop) => @@ -230,27 +276,48 @@ const PersistentSideNavFooter = styled(SideNavFooter, { ); const SideNavFooterItemsContainer = styled("div", { - shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", -})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ - display: "flex", - flexWrap: "wrap", - alignItems: "center", - fontSize: odysseyDesignTokens.TypographySizeOverline, - - "a, span": { - color: odysseyDesignTokens.HueNeutral600, - transition: `color ${odysseyDesignTokens.TransitionDurationMain}`, + shouldForwardProp: (prop) => + prop !== "odysseyDesignTokens" && prop !== "sideNavContrastColors", +})( + ({ + odysseyDesignTokens, + sideNavContrastColors, + }: { + odysseyDesignTokens: DesignTokens; + sideNavContrastColors: UiShellColors["sideNavContrastColors"]; + }) => ({ + display: "flex", + flexWrap: "wrap", + alignItems: "center", + fontSize: odysseyDesignTokens.TypographySizeOverline, - "&:visited": { + "a, span": { color: odysseyDesignTokens.HueNeutral600, - }, + transition: `color ${odysseyDesignTokens.TransitionDurationMain}`, + + "&:visited": { + color: odysseyDesignTokens.HueNeutral600, + + ...(sideNavContrastColors?.fontColor && { + color: sideNavContrastColors?.fontColor, + }), + }, - "&:hover": { - textDecoration: "none", - color: odysseyDesignTokens.HueNeutral900, + "&:hover": { + textDecoration: "none", + color: odysseyDesignTokens.HueNeutral900, + + ...(sideNavContrastColors?.fontColor && { + color: sideNavContrastColors?.fontColor, + }), + }, + + ...(sideNavContrastColors?.fontColor && { + color: sideNavContrastColors?.fontColor, + }), }, - }, -})); + }), +); const LoadingItemContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", @@ -296,12 +363,15 @@ const SideNav = ({ const [isSideNavCollapsed, setSideNavCollapsed] = useState(false); const [hasContentScrolled, setHasContentScrolled] = useState(false); const [isContentScrollable, setIsContentScrollable] = useState(false); + const [sideNavItemsList, updateSideNavItemsList] = useState(sideNavItems); + + const shellColors = useUiShellContrastColorContext(); + const odysseyDesignTokens: DesignTokens = useOdysseyDesignTokens(); + const { t } = useTranslation(); + const scrollableContentRef = useRef(null); const resizeObserverRef = useRef(null); const intersectionObserverRef = useRef(null); - const odysseyDesignTokens: DesignTokens = useOdysseyDesignTokens(); - const { t } = useTranslation(); - const [sideNavItemsList, updateSideNavItemsList] = useState(sideNavItems); // The default value (sideNavItems) passed to useState is ONLY used by the useState hook for // the very first value. Subsequent updates to the prop (sideNavItems) need to cause the state @@ -540,7 +610,11 @@ const SideNav = ({ return ( @@ -563,8 +637,9 @@ const SideNav = ({ odysseyDesignTokens={odysseyDesignTokens} > {!isLoading && footerItems && !hasCustomFooter && ( - + diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx index 2fbef7d2f..ee88aced5 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx @@ -21,20 +21,31 @@ import type { SideNavFooterItem } from "./types"; import { Box } from "../../Box"; import { Link } from "../../Link"; import { useTranslation } from "react-i18next"; +import { useUiShellContrastColorContext } from "../../ui-shell/UiShell/UiShellColorsProvider"; +import { ContrastColors } from "../../createContrastColors"; const StyledFooterNav = styled("nav")({ display: "flex", }); const StyledFooterItemContainer = styled("div", { - shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", -})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ - "& + &": { - marginInlineStart: odysseyDesignTokens.Spacing4, - paddingInlineStart: odysseyDesignTokens.Spacing4, - borderInlineStart: `1px solid ${odysseyDesignTokens.HueNeutral300}`, - }, -})); + shouldForwardProp: (prop) => + prop !== "odysseyDesignTokens" && prop !== "borderColor", +})( + ({ + borderColor, + odysseyDesignTokens, + }: { + borderColor: ContrastColors["fontColor"]; + odysseyDesignTokens: DesignTokens; + }) => ({ + "& + &": { + marginInlineStart: odysseyDesignTokens.Spacing4, + paddingInlineStart: odysseyDesignTokens.Spacing4, + borderInlineStart: `1px solid ${borderColor || odysseyDesignTokens.HueNeutral300}`, + }, + }), +); const SideNavFooterContent = ({ footerItems, @@ -43,10 +54,12 @@ const SideNavFooterContent = ({ }) => { const odysseyDesignTokens = useOdysseyDesignTokens(); const { t } = useTranslation(); + const shellColors = useUiShellContrastColorContext(); const memoizedFooterContent = useMemo(() => { return footerItems?.map((item) => ( )); - }, [footerItems, odysseyDesignTokens]); + }, [footerItems, odysseyDesignTokens, shellColors]); return ( diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx index 4c9be4e77..b313dd8ca 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx @@ -19,51 +19,72 @@ import { useOdysseyDesignTokens, } from "../../OdysseyDesignTokensContext"; import { SideNavLogo } from "./SideNavLogo"; -import { SideNavProps } from "./types"; +import { SideNavLogoProps, SideNavProps } from "./types"; import { Heading5 } from "../../Typography"; import { TOP_NAV_HEIGHT } from "../TopNav"; +import { ContrastColors } from "../../createContrastColors"; +import { useUiShellContrastColorContext } from "../../ui-shell/UiShell/UiShellColorsProvider"; -const SideNavHeaderContainer = styled("div", { - shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", -})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ +const SideNavHeaderContainer = styled("div")(() => ({ position: "relative", display: "flex", flexDirection: "column", - backgroundColor: odysseyDesignTokens.HueNeutralWhite, zIndex: 1, })); const SideNavLogoContainer = styled("div", { - shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", -})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ - display: "flex", - alignItems: "center", - height: TOP_NAV_HEIGHT, - paddingBlock: odysseyDesignTokens.Spacing4, - paddingInline: odysseyDesignTokens.Spacing5, + shouldForwardProp: (prop) => + prop !== "odysseyDesignTokens" && prop !== "isSameBackgroundAsMain", +})( + ({ + isSameBackgroundAsMain, + odysseyDesignTokens, + }: { + isSameBackgroundAsMain: SideNavLogoProps["isSameBackgroundAsMain"]; + odysseyDesignTokens: DesignTokens; + }) => ({ + display: "flex", + alignItems: "center", + height: TOP_NAV_HEIGHT, + paddingBlock: odysseyDesignTokens.Spacing4, + paddingInline: odysseyDesignTokens.Spacing5, + backgroundColor: isSameBackgroundAsMain + ? "transparent" + : odysseyDesignTokens.HueNeutralWhite, - "svg, img": { - maxHeight: "100%", - width: "auto", - maxWidth: "100%", - }, -})); + "svg, img": { + maxHeight: "100%", + width: "auto", + maxWidth: "100%", + }, + }), +); const SideNavHeadingContainer = styled("div", { - shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", -})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ - display: "flex", - justifyContent: "space-between", - alignItems: "center", - paddingBlock: odysseyDesignTokens.Spacing4, - paddingInline: odysseyDesignTokens.Spacing5, - width: "100%", - - ["& .MuiTypography-root"]: { - margin: 0, + shouldForwardProp: (prop) => + prop !== "odysseyDesignTokens" && prop !== "contrastFontColor", +})( + ({ + contrastFontColor, + odysseyDesignTokens, + }: { + contrastFontColor: ContrastColors["fontColor"]; + odysseyDesignTokens: DesignTokens; + }) => ({ + display: "flex", + justifyContent: "space-between", + alignItems: "center", + paddingBlock: odysseyDesignTokens.Spacing4, + paddingInline: odysseyDesignTokens.Spacing5, width: "100%", - }, -})); + + ["& .MuiTypography-root"]: { + margin: 0, + width: "100%", + color: contrastFontColor || "inherit", + }, + }), +); export type SideNavHeaderProps = { /** @@ -82,10 +103,14 @@ const SideNavHeader = ({ logoProps, }: SideNavHeaderProps) => { const odysseyDesignTokens = useOdysseyDesignTokens(); + const shellContrastColors = useUiShellContrastColorContext(); return ( - - + + {isLoading ? ( // The skeleton takes the hardcoded dimensions of the Okta logo @@ -95,7 +120,12 @@ const SideNavHeader = ({ {appName && ( - + {isLoading ? : appName} diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx index 90ae78e0f..1aad39e4c 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx @@ -32,15 +32,23 @@ import { useSideNavItemContent, } from "./SideNavItemContentContext"; import { ExternalLinkIcon } from "../../icons.generated"; +import { + UiShellColors, + useUiShellContrastColorContext, +} from "../../ui-shell/UiShell/UiShellColorsProvider"; export const StyledSideNavListItem = styled("li", { shouldForwardProp: (prop) => - prop !== "odysseyDesignTokens" && prop !== "isSelected", + prop !== "isSelected" && + prop !== "odysseyDesignTokens" && + prop !== "sideNavContrastColors", })<{ + sideNavContrastColors?: UiShellColors["sideNavContrastColors"]; odysseyDesignTokens: DesignTokens; isSelected?: boolean; + itemSelectedBackgroundColor?: string; disabled?: boolean; -}>(({ odysseyDesignTokens, isSelected }) => ({ +}>(({ isSelected, odysseyDesignTokens, sideNavContrastColors }) => ({ display: "flex", alignItems: "center", backgroundColor: "unset", @@ -48,8 +56,12 @@ export const StyledSideNavListItem = styled("li", { transition: `backgroundColor ${odysseyDesignTokens.TransitionDurationMain}, color ${odysseyDesignTokens.TransitionDurationMain}`, ...(isSelected && { - color: `${odysseyDesignTokens.TypographyColorAction} !important`, - backgroundColor: odysseyDesignTokens.HueBlue50, + color: sideNavContrastColors?.fontColor + ? `${sideNavContrastColors.fontColor} !important` + : `${odysseyDesignTokens.TypographyColorAction} !important`, + backgroundColor: + sideNavContrastColors?.itemSelectedBackgroundColor || + odysseyDesignTokens.HueBlue50, }), })); @@ -68,19 +80,23 @@ type ScrollIntoViewHandle = { }; export const getBaseNavItemContentStyles = ({ - odysseyDesignTokens, isDisabled, isSelected, + odysseyDesignTokens, + sideNavContrastColors, }: { - odysseyDesignTokens: DesignTokens; - isDisabled?: boolean; isSelected?: boolean; + isDisabled?: boolean; + odysseyDesignTokens: DesignTokens; + sideNavContrastColors: UiShellColors["sideNavContrastColors"]; }) => ({ display: "flex", alignItems: "center", width: "100%", textDecoration: "none", - color: `${odysseyDesignTokens.TypographyColorHeading} !important`, + color: sideNavContrastColors?.fontColor + ? `${sideNavContrastColors?.fontColor} !important` + : `${odysseyDesignTokens.TypographyColorHeading} !important`, minHeight: "unset", paddingBlock: odysseyDesignTokens.Spacing3, paddingInlineEnd: odysseyDesignTokens.Spacing4, @@ -92,11 +108,17 @@ export const getBaseNavItemContentStyles = ({ "&:hover, [data-sortable-container='true']:has(button:hover, button:focus, button:focus-visible) &": { textDecoration: "none", - backgroundColor: odysseyDesignTokens.HueNeutral50, + backgroundColor: + sideNavContrastColors?.itemHoverBackgroundColor || + odysseyDesignTokens.HueNeutral50, ...(isSelected && { - backgroundColor: odysseyDesignTokens.HueBlue50, - color: odysseyDesignTokens.TypographyColorAction, + backgroundColor: + sideNavContrastColors?.itemSelectedBackgroundColor || + odysseyDesignTokens.HueBlue50, + color: + sideNavContrastColors?.fontColor || + odysseyDesignTokens.TypographyColorAction, }), ...(isDisabled && { @@ -105,18 +127,24 @@ export const getBaseNavItemContentStyles = ({ }, ...(isSelected && { - color: `${odysseyDesignTokens.TypographyColorAction}`, + color: sideNavContrastColors?.fontColor + ? `${sideNavContrastColors?.fontColor} !important` + : `${odysseyDesignTokens.TypographyColorAction} !important`, fontWeight: odysseyDesignTokens.TypographyWeightBodyBold, }), ...(isDisabled && { cursor: "default", color: `${odysseyDesignTokens.TypographyColorDisabled} !important`, + + ...(sideNavContrastColors?.itemDisabledFontColor && { + color: `${sideNavContrastColors?.itemDisabledFontColor} !important`, + }), }), "&:focus-visible": { outline: "none", - boxShadow: `inset 0 0 0 2px ${odysseyDesignTokens.PalettePrimaryMain}`, + boxShadow: `inset 0 0 0 2px ${sideNavContrastColors?.focusRingColor || odysseyDesignTokens.PalettePrimaryMain}`, }, }); @@ -143,48 +171,70 @@ const NavItemContentContainer = styled("div", { prop !== "odysseyDesignTokens" && prop != "contextValue" && prop !== "isDisabled" && + prop !== "sideNavContrastColors" && prop !== "isSelected", })<{ contextValue: SideNavItemContentContextValue; odysseyDesignTokens: DesignTokens; + sideNavContrastColors: UiShellColors["sideNavContrastColors"]; isSelected?: boolean; isDisabled?: boolean; -}>(({ contextValue, odysseyDesignTokens, isDisabled, isSelected }) => ({ - ...getBaseNavItemContentStyles({ - odysseyDesignTokens, +}>( + ({ isDisabled, isSelected, - }), - - ...getNavItemContentStyles({ - odysseyDesignTokens, contextValue, + odysseyDesignTokens, + sideNavContrastColors, + }) => ({ + ...getBaseNavItemContentStyles({ + isDisabled, + isSelected, + odysseyDesignTokens, + sideNavContrastColors, + }), + + ...getNavItemContentStyles({ + odysseyDesignTokens, + contextValue, + }), }), -})); +); const StyledNavItemLink = styled(NavItemLink, { shouldForwardProp: (prop) => - prop !== "odysseyDesignTokens" && prop != "contextValue" && prop !== "isDisabled" && - prop !== "isSelected", + prop !== "isSelected" && + prop !== "odysseyDesignTokens" && + prop !== "sideNavContrastColors", })<{ contextValue: SideNavItemContentContextValue; odysseyDesignTokens: DesignTokens; + sideNavContrastColors: UiShellColors["sideNavContrastColors"]; isSelected?: boolean; isDisabled?: boolean; -}>(({ contextValue, odysseyDesignTokens, isDisabled, isSelected }) => ({ - ...getBaseNavItemContentStyles({ - odysseyDesignTokens, +}>( + ({ isDisabled, isSelected, - }), - - ...getNavItemContentStyles({ - odysseyDesignTokens, contextValue, + odysseyDesignTokens, + sideNavContrastColors, + }) => ({ + ...getBaseNavItemContentStyles({ + isDisabled, + isSelected, + odysseyDesignTokens, + sideNavContrastColors, + }), + + ...getNavItemContentStyles({ + odysseyDesignTokens, + contextValue, + }), }), -})); +); const SideNavItemContent = ({ count, @@ -222,11 +272,13 @@ const SideNavItemContent = ({ scrollRef?: React.RefObject; onItemSelected?(selectedItemId: string): void; }) => { + const shellContrastColors = useUiShellContrastColorContext(); const sidenavItemContentContext = useSideNavItemContent(); const contextValue = useMemo( () => sidenavItemContentContext, [sidenavItemContentContext], ); + const odysseyDesignTokens = useOdysseyDesignTokens(); const localScrollRef = useRef(null); @@ -269,23 +321,25 @@ const SideNavItemContent = ({ return ( { // Use Link for nav items with links and div for disabled or non-link items isDisabled ? ( ) : !href ? ( ) : ( - prop !== "odysseyDesignTokens" && prop !== "isSideNavCollapsed", + prop !== "odysseyDesignTokens" && + prop !== "isSideNavCollapsed" && + prop !== "toggleContrastColors", })( ({ isSideNavCollapsed, odysseyDesignTokens, + toggleContrastColors, }: { isSideNavCollapsed: boolean; odysseyDesignTokens: DesignTokens; + toggleContrastColors?: ContrastColors; }) => ({ backgroundColor: "transparent", position: "relative", @@ -52,6 +61,10 @@ const StyledToggleButton = styled(MuiButton, { "&:focus-visible": { boxShadow: `inset 0 0 0 2px ${odysseyDesignTokens.PalettePrimaryMain}`, outline: "none", + + ...(toggleContrastColors?.focusRingColor && { + boxShadow: `inset 0 0 0 2px ${toggleContrastColors.focusRingColor}`, + }), }, "&:hover, &:focus-visible": { @@ -134,6 +147,10 @@ const StyledToggleButton = styled(MuiButton, { backgroundColor: odysseyDesignTokens.HueNeutral600, transform: "translate3d(-50%, -50%, 0)", transition: `transform ${odysseyDesignTokens.TransitionDurationMain}`, + + ...(toggleContrastColors?.fontColor && { + backgroundColor: toggleContrastColors.fontColor, + }), }, }), ); @@ -170,9 +187,26 @@ const SideNavToggleButton = ({ }: SideNavToggleButtonProps) => { const odysseyDesignTokens = useOdysseyDesignTokens(); const { t } = useTranslation(); + const shellColors = useUiShellContrastColorContext(); const localButtonRef = useRef(null); + const toggleContrastColors = useMemo(() => { + const hasNonStandardAppBackgroundColor = + shellColors?.appBackgroundColor && + shellColors?.appBackgroundColor !== odysseyDesignTokens.HueNeutralWhite && + shellColors?.appBackgroundColor !== odysseyDesignTokens.HueNeutral50; + + if (hasNonStandardAppBackgroundColor) { + return generateContrastColors( + shellColors.appBackgroundColor, + odysseyDesignTokens, + ); + } + + return null; + }, [odysseyDesignTokens, shellColors]); + useImperativeHandle( buttonRef, () => ({ @@ -214,6 +248,7 @@ const SideNavToggleButton = ({ } }} tabIndex={tabIndex} + toggleContrastColors={toggleContrastColors} variant="floating" > @@ -228,6 +263,7 @@ const SideNavToggleButton = ({ odysseyDesignTokens, onClick, tabIndex, + toggleContrastColors, toggleLabel, ], ); diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SortableList/SortableItem.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SortableList/SortableItem.tsx index 4fa3a5c8c..3802489fb 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SortableList/SortableItem.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SortableList/SortableItem.tsx @@ -26,6 +26,11 @@ import { useOdysseyDesignTokens, } from "../../../OdysseyDesignTokensContext"; import { useTranslation } from "react-i18next"; +import { + UiShellColors, + useUiShellContrastColorContext, +} from "../../../ui-shell/UiShell/UiShellColorsProvider"; +import { ContrastColors } from "../../../createContrastColors"; type ItemProps = { id: UniqueIdentifier; @@ -49,11 +54,14 @@ const SortableItemContext = createContext({ const StyledSortableListItem = styled("li", { shouldForwardProp: (prop) => - prop !== "odysseyDesignTokens" && prop !== "isSelected", + prop !== "odysseyDesignTokens" && + prop !== "isSelected" && + prop !== "sideNavContrastColors", })<{ - odysseyDesignTokens: DesignTokens; isSelected?: boolean; -}>(({ odysseyDesignTokens, isSelected }) => ({ + odysseyDesignTokens: DesignTokens; + sideNavContrastColors: UiShellColors["sideNavContrastColors"]; +}>(({ odysseyDesignTokens, isSelected, sideNavContrastColors }) => ({ position: "relative", button: { @@ -65,6 +73,9 @@ const StyledSortableListItem = styled("li", { svg: { path: { fill: "currentColor", + ...(sideNavContrastColors?.fontColor && { + fill: sideNavContrastColors.fontColor, + }), }, }, @@ -80,6 +91,10 @@ const StyledSortableListItem = styled("li", { svg: { path: { fill: odysseyDesignTokens.TypographyColorAction, + + ...(sideNavContrastColors?.fontColor && { + fill: sideNavContrastColors.fontColor, + }), }, }, }), @@ -93,16 +108,17 @@ const StyledUl = styled("ul")({ const StyledDragHandleButton = styled("button", { shouldForwardProp: (prop) => - prop !== "odysseyDesignTokens" && prop !== "isDragging", + prop !== "odysseyDesignTokens" && + prop !== "isDragging" && + prop !== "focusRingColor", })<{ - odysseyDesignTokens: DesignTokens; + focusRingColor: ContrastColors["focusRingColor"]; isDragging?: boolean; -}>(({ odysseyDesignTokens, isDragging }) => ({ + odysseyDesignTokens: DesignTokens; +}>(({ odysseyDesignTokens, isDragging, focusRingColor }) => ({ position: "absolute", opacity: 0, - // paddingInlineStart: odysseyDesignTokens.Spacing4, padding: odysseyDesignTokens.Spacing2, - // paddingBlock: 0, border: "none", backgroundColor: "transparent", cursor: `${isDragging ? "grabbing" : "grab"}`, @@ -116,6 +132,10 @@ const StyledDragHandleButton = styled("button", { "&:focus, &:focus-visible": { outline: "none", boxShadow: `inset 0 0 0 2px ${odysseyDesignTokens.PalettePrimaryMain}`, + + ...(focusRingColor && { + boxShadow: `inset 0 0 0 2px ${focusRingColor}`, + }), }, })); @@ -128,15 +148,19 @@ export const DragHandle = ({ isDragging }: DragHandleProps) => { const { attributes, listeners, ref } = useContext(SortableItemContext); const odysseyDesignTokens: DesignTokens = useOdysseyDesignTokens(); const { t } = useTranslation(); + const shellContrastColors = useUiShellContrastColorContext(); return ( ({ attributes, @@ -180,21 +205,25 @@ export const SortableItem = ({ }), [attributes, listeners, setActivatorNodeRef], ); + const style: CSSProperties = { opacity: isDragging ? 0.4 : undefined, transform: CSS.Translate.toString(transform), transition, }; + const shellContrastColors = useUiShellContrastColorContext(); const odysseyDesignTokens: DesignTokens = useOdysseyDesignTokens(); + return ( {!isDisabled && isSortable && } {children} diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SortableList/SortableList.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SortableList/SortableList.tsx index 2247e5598..b3feb7263 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SortableList/SortableList.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SortableList/SortableList.tsx @@ -12,6 +12,7 @@ import React, { useMemo, useState } from "react"; import type { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; // eslint-disable-next-line import/no-extraneous-dependencies import { DndContext, @@ -29,7 +30,6 @@ import { import { SortableItem } from "./SortableItem"; import { SortableOverlay } from "./SortableOverlay"; -import { useTranslation } from "react-i18next"; export interface BaseItem { id: UniqueIdentifier; @@ -53,6 +53,7 @@ export const SortableList = ({ renderItem, }: ListProps) => { const [active, setActive] = useState(null); + const activeItem = useMemo( () => items.find((item) => item.id === active?.id), [active, items], diff --git a/packages/odyssey-react-mui/src/labs/SideNav/types.ts b/packages/odyssey-react-mui/src/labs/SideNav/types.ts index 817ed7133..5384d324d 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/types.ts +++ b/packages/odyssey-react-mui/src/labs/SideNav/types.ts @@ -16,6 +16,7 @@ import type { statusSeverityValues } from "../../Status"; export type SideNavLogoProps = { href?: string; + isSameBackgroundAsMain?: boolean; } & ( | { /** diff --git a/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx b/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx index dd8de2bb6..571f72d98 100644 --- a/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx +++ b/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx @@ -18,6 +18,10 @@ import { DesignTokens, useOdysseyDesignTokens, } from "../../OdysseyDesignTokensContext"; +import { + UiShellColors, + useUiShellContrastColorContext, +} from "../../ui-shell/UiShell/UiShellColorsProvider"; export const TOP_NAV_HEIGHT = `${64 / 14}rem`; @@ -31,13 +35,16 @@ const StyledRightSideContainer = styled("div")(() => ({ const StyledTopNavContainer = styled("div", { shouldForwardProp: (prop) => - prop !== "odysseyDesignTokens" && prop !== "isScrolled", + prop !== "odysseyDesignTokens" && + prop !== "isScrolled" && + prop !== "topNavBackgroundColor", })<{ - odysseyDesignTokens: DesignTokens; isScrolled?: boolean; -}>(({ odysseyDesignTokens, isScrolled }) => ({ + odysseyDesignTokens: DesignTokens; + topNavBackgroundColor?: UiShellColors["topNavBackgroundColor"]; +}>(({ odysseyDesignTokens, isScrolled, topNavBackgroundColor }) => ({ alignItems: "center", - backgroundColor: odysseyDesignTokens.HueNeutral50, + backgroundColor: topNavBackgroundColor, boxShadow: isScrolled ? odysseyDesignTokens.DepthMedium : undefined, clipPath: "inset(0 0 -100vh 0)", display: "flex", @@ -50,6 +57,10 @@ const StyledTopNavContainer = styled("div", { paddingInline: odysseyDesignTokens.Spacing8, transition: `box-shadow ${odysseyDesignTokens.TransitionDurationMain} ${odysseyDesignTokens.TransitionTimingMain}`, zIndex: 1, + + ...(topNavBackgroundColor === odysseyDesignTokens.HueNeutralWhite && { + borderBottom: `${odysseyDesignTokens.BorderWidthMain} ${odysseyDesignTokens.BorderStyleMain} ${odysseyDesignTokens.HueNeutral100}`, + }), })); export type TopNavProps = { @@ -73,11 +84,13 @@ const TopNav = ({ rightSideComponent, }: TopNavProps) => { const odysseyDesignTokens = useOdysseyDesignTokens(); + const shellColors = useUiShellContrastColorContext(); return ( {leftSideComponent ??
} diff --git a/packages/odyssey-react-mui/src/theme/components.tsx b/packages/odyssey-react-mui/src/theme/components.tsx index beb2f16e2..ac85caada 100644 --- a/packages/odyssey-react-mui/src/theme/components.tsx +++ b/packages/odyssey-react-mui/src/theme/components.tsx @@ -1675,10 +1675,6 @@ export const components = ({ paddingBlockStart: odysseyTokens.Spacing5, paddingBlockEnd: odysseyTokens.Spacing5, paddingInline: odysseyTokens.Spacing6, - - "& > .${ buttonClasses.root }": { - margin: "0 !important", - }, }, }, }, @@ -2306,10 +2302,6 @@ export const components = ({ borderBlockEnd: `1px solid ${odysseyTokens.BorderColorDisplay}`, }), - ...(!ownerState.isFullWidth && { - maxWidth: "100%", - }), - [`&.${menuItemClasses.disabled}`]: { opacity: 1, color: odysseyTokens.TypographyColorDisabled, diff --git a/packages/odyssey-react-mui/src/ui-shell/UiShell/UiShell.tsx b/packages/odyssey-react-mui/src/ui-shell/UiShell/UiShell.tsx index 1e5e60d6b..04ac3032b 100644 --- a/packages/odyssey-react-mui/src/ui-shell/UiShell/UiShell.tsx +++ b/packages/odyssey-react-mui/src/ui-shell/UiShell/UiShell.tsx @@ -20,6 +20,7 @@ import { type UiShellContentProps, type UiShellNavComponentProps, } from "./UiShellContent"; +import { UiShellColorsProvider } from "./UiShellColorsProvider"; import { type ReactRootElements } from "../../web-component"; export const defaultComponentProps: UiShellNavComponentProps = { @@ -45,9 +46,12 @@ export type UiShellProps = { componentProps: SetStateAction, ) => void, ) => () => void; + sideNavBackgroundColor?: string; + topNavBackgroundColor?: string; } & Pick & Pick< UiShellContentProps, + | "appBackgroundColor" | "appBackgroundContrastMode" | "appComponent" | "hasStandardAppContentPadding" @@ -64,6 +68,7 @@ export type UiShellProps = { * If an error occurs, this will revert to only showing the app. */ const UiShell = ({ + appBackgroundColor, appBackgroundContrastMode, appComponent, appRootElement, @@ -72,7 +77,9 @@ const UiShell = ({ onError = console.error, onSubscriptionCreated, optionalComponents, + sideNavBackgroundColor, stylesRootElement, + topNavBackgroundColor, subscribeToPropChanges, }: UiShellProps) => { const [componentProps, setComponentProps] = useState(defaultComponentProps); @@ -98,16 +105,21 @@ const UiShell = ({ > - - + sideNavBackgroundColor={sideNavBackgroundColor} + topNavBackgroundColor={topNavBackgroundColor} + > + + diff --git a/packages/odyssey-react-mui/src/ui-shell/UiShell/UiShellColorsProvider.tsx b/packages/odyssey-react-mui/src/ui-shell/UiShell/UiShellColorsProvider.tsx new file mode 100644 index 000000000..59926fd49 --- /dev/null +++ b/packages/odyssey-react-mui/src/ui-shell/UiShell/UiShellColorsProvider.tsx @@ -0,0 +1,90 @@ +/*! + * Copyright (c) 2022-present, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + +import { createContext, memo, PropsWithChildren, useContext } from "react"; +import { + generateContrastColors, + ContrastColors, +} from "../../createContrastColors"; +import { useOdysseyDesignTokens } from "../../OdysseyDesignTokensContext"; + +export type UiShellColors = { + appBackgroundColor: string; + sideNavBackgroundColor: string; + sideNavContrastColors?: ContrastColors | undefined; + topNavBackgroundColor: string; +}; + +const UiShellColorsContext = createContext( + undefined, +); + +export const useUiShellContrastColorContext = () => { + return useContext(UiShellColorsContext); +}; + +export type UiShellColorsProviderProps = { + appBackgroundColor?: string; + sideNavBackgroundColor?: string; + topNavBackgroundColor?: string; + appBackgroundContrastMode?: string; +}; + +const UiShellColorsProvider = ({ + appBackgroundColor, + appBackgroundContrastMode = "lowContrast", + sideNavBackgroundColor, + topNavBackgroundColor, + children, +}: PropsWithChildren) => { + const odysseyDesignTokens = useOdysseyDesignTokens(); + const defaultedSideNavBackgroundColor = + sideNavBackgroundColor || odysseyDesignTokens.HueNeutralWhite; + + const sideNavContrastColors = + defaultedSideNavBackgroundColor !== odysseyDesignTokens.HueNeutralWhite + ? generateContrastColors( + defaultedSideNavBackgroundColor, + odysseyDesignTokens, + ) + : undefined; + + const isAppBackgroundHightContrast = + appBackgroundContrastMode === "highContrast"; + + const defaultTopAndAppBackgroundColor = isAppBackgroundHightContrast + ? odysseyDesignTokens.HueNeutralWhite + : odysseyDesignTokens.HueNeutral50; + + const topNavColor = topNavBackgroundColor || defaultTopAndAppBackgroundColor; + + const appContentBackgroundColor = + appBackgroundColor || defaultTopAndAppBackgroundColor; + + return ( + + {children} + + ); +}; + +const MemoizedUiShellColorsProvider = memo(UiShellColorsProvider); + +export { MemoizedUiShellColorsProvider as UiShellColorsProvider }; diff --git a/packages/odyssey-react-mui/src/ui-shell/UiShell/UiShellContent.tsx b/packages/odyssey-react-mui/src/ui-shell/UiShell/UiShellContent.tsx index fc922c492..5e887695d 100644 --- a/packages/odyssey-react-mui/src/ui-shell/UiShell/UiShellContent.tsx +++ b/packages/odyssey-react-mui/src/ui-shell/UiShell/UiShellContent.tsx @@ -23,31 +23,32 @@ import { } from "../../OdysseyDesignTokensContext"; import { useScrollState } from "./useScrollState"; import { ContrastMode } from "../../useContrastMode"; +import { + UiShellColors, + useUiShellContrastColorContext, +} from "./UiShellColorsProvider"; const emptySideNavItems = [] satisfies SideNavProps["sideNavItems"]; const StyledAppContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens" && - prop !== "appBackgroundContrastMode" && + prop !== "appBackgroundColor" && prop !== "hasStandardAppContentPadding", })<{ - appBackgroundContrastMode: ContrastMode; + appBackgroundColor?: UiShellColors["appBackgroundColor"]; hasStandardAppContentPadding: UiShellContentProps["hasStandardAppContentPadding"]; odysseyDesignTokens: DesignTokens; }>( ({ - appBackgroundContrastMode, + appBackgroundColor, hasStandardAppContentPadding, odysseyDesignTokens, }) => ({ gridArea: "app-content", overflowX: "hidden", overflowY: "auto", - backgroundColor: - appBackgroundContrastMode === "highContrast" - ? odysseyDesignTokens.HueNeutralWhite - : odysseyDesignTokens.HueNeutral50, + backgroundColor: appBackgroundColor, ...(hasStandardAppContentPadding && { paddingBlock: odysseyDesignTokens.Spacing5, @@ -113,6 +114,7 @@ export type UiShellNavComponentProps = { }; export type UiShellContentProps = { + appBackgroundColor?: string; /** * Sets the background color for the app content area. */ @@ -153,7 +155,6 @@ export type UiShellContentProps = { * If an error occurs, this will revert to only showing the app. */ const UiShellContent = ({ - appBackgroundContrastMode = "lowContrast", appComponent, hasStandardAppContentPadding = true, initialVisibleSections = ["TopNav", "SideNav", "AppSwitcher"], @@ -165,6 +166,7 @@ const UiShellContent = ({ }: UiShellContentProps) => { const odysseyDesignTokens = useOdysseyDesignTokens(); const { isContentScrolled, scrollableContentRef } = useScrollState(); + const shellColors = useUiShellContrastColorContext(); return ( @@ -250,7 +252,7 @@ const UiShellContent = ({ ) => { const appRootElement = explicitAppRootElement || document.createElement("div"); @@ -108,6 +114,7 @@ export const renderUiShell = ({ getReactComponent: (reactRootElements) => ( ), diff --git a/packages/odyssey-storybook/src/components/odyssey-ui-shell/UiShell/UiShell.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-ui-shell/UiShell/UiShell.stories.tsx index 9a85bc93c..cd8d846da 100644 --- a/packages/odyssey-storybook/src/components/odyssey-ui-shell/UiShell/UiShell.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-ui-shell/UiShell/UiShell.stories.tsx @@ -11,6 +11,7 @@ */ import { Meta, StoryObj } from "@storybook/react"; + import { MuiThemeDecorator } from "../../../../.storybook/components"; import { Banner, @@ -28,9 +29,19 @@ import { type UiShellProps, } from "@okta/odyssey-react-mui/ui-shell"; import { - AddCircleIcon, + AppsIcon, + ClockIcon, + SettingsIcon, HomeIcon, + Fido2Icon, + LockIcon, + AddCircleIcon, + DownloadIcon, UserIcon, + DirectoryIcon, + ServerIcon, + FolderIcon, + NotificationIcon, } from "@okta/odyssey-react-mui/icons"; const storybookMeta: Meta = { @@ -107,6 +118,35 @@ const storybookMeta: Meta = { }, }, }, + appBackgroundColor: { + control: "color", + description: + "Custom color for app content background. Should only be used as a stop-gap to allow support for dark mode.", + table: { + type: { + summary: "hex color code", + }, + }, + }, + sideNavBackgroundColor: { + control: "color", + description: "Custom color for side nav background", + table: { + type: { + summary: "hex color code", + }, + }, + }, + topNavBackgroundColor: { + control: "color", + description: + "Custom color for top nav background. Should only be used as a stop-gap to allow support for dark mode.", + table: { + type: { + summary: "hex color code", + }, + }, + }, }, args: { appComponent:
, @@ -153,35 +193,210 @@ const sharedSideNavProps: UiShellNavComponentProps["sideNavProps"] = { isCollapsible: true, sideNavItems: [ { - id: "AddNewFolder", + id: "item1", label: "Add new folder", endIcon: , onClick: () => {}, }, { - id: "item0-0", + id: "item2", label: "Admin", isSectionHeader: true, }, { - id: "item0-1", - href: "/?path=/story/labs-components-switch--default", + id: "item3", + href: "/?path=/docs/mui-components-typography--docs", label: "Users", startIcon: , }, { - id: "item1", + id: "item4", label: "Dashboard", startIcon: , isDisabled: true, nestedNavItems: [ { - id: "item1-1", + id: "item4-1", href: "/", label: "Home", }, ], }, + { + id: "item5", + href: "/", + label: "Applications", + startIcon: , + }, + { + id: "item6", + label: "Onboarding", + startIcon: , + nestedNavItems: [ + { + id: "item6-1", + href: "/", + label: "Start", + }, + { + id: "item6-2", + href: "/", + label: "Tasks", + }, + { + id: "item6-3", + href: "/", + label: "Getting Started", + }, + ], + }, + { + id: "item7", + href: "/", + label: "Directory", + startIcon: , + }, + { + id: "item8", + label: "Resource Management", + isSectionHeader: true, + }, + { + id: "item9", + href: "/", + label: "Kubernetes", + startIcon: , + severity: "info", + statusLabel: "BETA", + }, + { + id: "item10", + href: "/", + label: "Reports", + startIcon: , + }, + { + id: "item11", + href: "/", + label: "Identify Governance", + target: "_blank", + isDisabled: true, + startIcon: , + }, + { + id: "item12", + href: "/", + label: "Workflows", + target: "_blank", + startIcon: , + }, + { + id: "item13", + label: "Security Administration", + isSectionHeader: true, + }, + { + id: "item14", + href: "/", + label: "Security", + startIcon: , + endIcon: , + }, + { + id: "item15", + label: "Settings", + isDefaultExpanded: true, + isSortable: true, + startIcon: , + nestedNavItems: [ + { + id: "item15-1", + href: "/", + label: "General", + }, + { + id: "item15-2", + href: "/", + label: "Custom Domain", + isSelected: true, + }, + { + id: "item15-3", + label: "Account Management", + }, + { + id: "item15-4", + href: "/", + label: "Authentication Policies", + isDisabled: true, + }, + { + id: "item15-5", + href: "/", + label: "IDP Configuration", + }, + ], + }, + { + id: "item16", + href: "/", + label: "System Configuration", + startIcon: , + }, + { + id: "item17-0", + label: "Enduser", + isSectionHeader: true, + }, + { + id: "item17", + label: "My Apps", + isDefaultExpanded: true, + isSortable: true, + startIcon: , + nestedNavItems: [ + { + id: "item17-1", + label: "Recently Used", + }, + { + id: "item17-2", + label: "Work", + }, + { + id: "item17-3", + label: "Add section", + endIcon: , + }, + ], + }, + { + id: "item18", + label: "Notifications", + startIcon: , + count: 1, + }, + { + id: "item19", + label: "Add apps", + startIcon: , + }, + ], + footerItems: [ + { + id: "footer-item-1", + label: "Docs", + href: "/", + }, + { + id: "footer-item-2", + label: "Privacy", + }, + { + id: "footer-item-3", + label: "Security", + href: "/", + }, ], }; @@ -298,6 +513,28 @@ export const LoadingData: StoryObj = { }, }; +export const WithCustomColors: StoryObj = { + args: { + sideNavBackgroundColor: undefined, + topNavBackgroundColor: undefined, + appBackgroundColor: undefined, + optionalComponents: sharedOptionalComponents, + subscribeToPropChanges: (subscriber) => { + subscriber({ + topNavProps: sharedTopNavProps, + sideNavProps: { + ...sharedSideNavProps, + logoProps: { + isSameBackgroundAsMain: true, + }, + }, + }); + + return () => {}; + }, + }, +}; + export const WithoutAppContent: StoryObj = { args: { optionalComponents: sharedOptionalComponents,