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 (