): void;
+ /**
+ * The status element to display after the label
+ */
+ severity?: (typeof statusSeverityValues)[number];
+ /**
+ * The icon element to display at the start of the Nav Item
+ */
+ startIcon?: ReactElement;
+ /**
+ * The label to display inside the status
+ */
+ statusLabel?: string;
+ target?: string;
} & (
| {
/**
@@ -95,49 +94,55 @@ export type SideNavItem = {
);
export type SideNavFooterItem = {
+ href: string;
id: string;
label: string;
- href: string;
};
const SideNavCollapsedContainer = styled("div", {
shouldForwardProp: (prop) =>
- prop !== "odysseyDesignTokens" && prop !== "sideNavCollapsed",
+ prop !== "odysseyDesignTokens" && prop !== "isSideNavCollapsed",
})(
({
odysseyDesignTokens,
- sideNavCollapsed,
+ isSideNavCollapsed,
}: {
odysseyDesignTokens: DesignTokens;
- sideNavCollapsed: boolean;
+ isSideNavCollapsed: boolean;
}) => ({
backgroundColor: odysseyDesignTokens.HueNeutral300,
paddingTop: odysseyDesignTokens.Spacing5,
cursor: "pointer",
- width: sideNavCollapsed ? "auto" : 0,
- visibility: sideNavCollapsed ? "visible" : "hidden",
+ width: isSideNavCollapsed ? "auto" : 0,
+ opacity: isSideNavCollapsed ? 1 : 0,
+ visibility: isSideNavCollapsed ? "visible" : "hidden",
+ transitionProperty: "opacity, visibility, width",
+ transitionDuration: odysseyDesignTokens.TransitionDurationMain,
+ transitionTimingFunction: odysseyDesignTokens.TransitionTimingMain,
}),
);
const SideNavContainer = styled("div", {
shouldForwardProp: (prop) =>
- prop !== "odysseyDesignTokens" && prop !== "sideNavCollapsed",
+ prop !== "odysseyDesignTokens" && prop !== "isSideNavCollapsed",
})(
({
odysseyDesignTokens,
- sideNavCollapsed,
+ isSideNavCollapsed,
}: {
odysseyDesignTokens: DesignTokens;
- sideNavCollapsed: boolean;
+ isSideNavCollapsed: boolean;
}) => ({
backgroundColor: odysseyDesignTokens.HueNeutralWhite,
flexDirection: "column",
display: "flex",
- visibility: sideNavCollapsed ? "hidden" : "visible",
- width: sideNavCollapsed ? "0" : "100%",
- transitionProperty: "width, visibility",
+ opacity: isSideNavCollapsed ? 0 : 1,
+ visibility: isSideNavCollapsed ? "hidden" : "visible",
+ width: isSideNavCollapsed ? "0" : "100%",
+ transitionProperty: "opacity, visibility, width",
transitionDuration: odysseyDesignTokens.TransitionDurationMain,
transitionTimingFunction: odysseyDesignTokens.TransitionTimingMain,
+ transitionDelay: odysseyDesignTokens.TransitionDurationMain,
}),
);
@@ -156,43 +161,23 @@ const SideNavHeaderContainer = styled("div", {
const CollapseIcon = ({ onClick }: { onClick?(): void }): ReactElement => {
const odysseyDesignTokens = useOdysseyDesignTokens();
return (
- {
- event.key === "Enter" && onClick && onClick();
+ button": {
+ height: "32px",
+ width: "32px",
+ color: odysseyDesignTokens.HueNeutral400,
+ },
}}
>
-
-
-
-
+ }
+ ariaLabel="collapse side navigation"
+ />
+
);
};
@@ -253,19 +238,16 @@ const SideNavItemLabelContainer = styled("div", {
alignItems: "center",
fontSize: odysseyDesignTokens.TypographyScale0,
fontWeight: odysseyDesignTokens.TypographyWeightHeading,
- marginLeft: isIconVisible ? odysseyDesignTokens.Spacing3 : 0,
- "& > a": {
+ marginLeft: isIconVisible ? odysseyDesignTokens.Spacing2 : 0,
+ "& a": {
color: `${odysseyDesignTokens.TypographyColorHeading} !important`,
+ fontSize: odysseyDesignTokens.TypographyScale0,
pointerEvents: isDisabled ? "none" : "auto",
},
- "& > a:hover": {
+ "& a:hover": {
textDecoration: "none",
cursor: isDisabled ? "default" : "pointer",
},
- "& > a:visited": {
- color: odysseyDesignTokens.TypographyColorHeading,
- fontSize: odysseyDesignTokens.TypographyScale0,
- },
}));
const SideNavListItemContainer = styled("li", {
@@ -281,33 +263,32 @@ const SideNavListItemContainer = styled("li", {
}>(({ odysseyDesignTokens, isSelected, isDisabled }) => ({
display: "flex",
alignItems: "center",
- minHeight: "48px",
- opacity: isDisabled ? "0.38" : "1",
cursor: isDisabled ? "default" : "pointer",
pointerEvents: isDisabled ? "none" : "auto",
- backgroundColor: isSelected ? odysseyDesignTokens.HueNeutral50 : "auto",
- "&:hover": {
- backgroundColor: !isDisabled ? odysseyDesignTokens.HueNeutral50 : "auto",
- },
+ backgroundColor: isSelected ? odysseyDesignTokens.HueNeutral50 : "unset",
+ margin: "4px 0",
"&:last-child": {
marginBottom: odysseyDesignTokens.Spacing2,
},
- "& > a": {
+ "& a": {
display: "flex",
alignItems: "center",
width: "100%",
- paddingRight: odysseyDesignTokens.Spacing4,
- paddingLeft: odysseyDesignTokens.Spacing4,
+ minHeight: "45px",
+ padding: `${odysseyDesignTokens.Spacing3} ${odysseyDesignTokens.Spacing4}`,
color: `${odysseyDesignTokens.TypographyColorHeading} !important`,
pointerEvents: isDisabled ? "none" : "auto",
},
- "& > a:hover": {
+ "& a:hover": {
textDecoration: "none",
cursor: isDisabled ? "default" : "pointer",
+ backgroundColor: !isDisabled ? odysseyDesignTokens.HueNeutral50 : "inherit",
},
- "& > a:visited": {
- color: odysseyDesignTokens.TypographyColorHeading,
- fontSize: odysseyDesignTokens.TypographyScale0,
+ "& a:focus-visible": {
+ outlineOffset: 0,
+ borderRadius: 0,
+ outlineWidth: "2px",
+ backgroundColor: !isDisabled ? odysseyDesignTokens.HueNeutral50 : "inherit",
},
}));
@@ -346,6 +327,44 @@ const SideNavFooter = ({ id, label, href }: SideNavFooterItem) => {
);
};
+const SideNavItemLinkContent = ({
+ label,
+ startIcon,
+ endIcon,
+ isDisabled,
+ severity,
+ statusLabel,
+}: Pick<
+ SideNavItem,
+ "label" | "startIcon" | "endIcon" | "severity" | "statusLabel"
+> & {
+ isDisabled?: boolean;
+}): ReactNode => {
+ const odysseyDesignTokens = useOdysseyDesignTokens();
+ return (
+ <>
+ {startIcon && startIcon}
+
+ {label}
+ {severity && (
+
+
+
+ )}
+
+ {endIcon && endIcon}
+ >
+ );
+};
+
const SideNavItemContent = ({
id,
label,
@@ -378,30 +397,46 @@ const SideNavItemContent = ({
id={id}
key={id}
disabled={isDisabled}
+ aria-disabled={isDisabled}
isDisabled={isDisabled}
isSelected={isSelected}
odysseyDesignTokens={odysseyDesignTokens}
>
-
- {startIcon && startIcon}
-
- {label}
- {severity && (
-
-
-
- )}
-
- {endIcon && endIcon}
-
+ {
+ // Use Link for accessible nav items and div for disabled items
+ isDisabled ? (
+
+
+
+ ) : (
+
+
+
+ )
+ }
);
};
@@ -415,6 +450,10 @@ export type SideNavProps = {
* Determines whether the side nav is collapsible
*/
isCollapsible?: boolean;
+ /**
+ * Footer items in the side nav
+ */
+ footerItems?: SideNavFooterItem[];
/**
* Triggers when the side nav is collapsed
*/
@@ -423,10 +462,6 @@ export type SideNavProps = {
* Nav items in the side nav
*/
sideNavItems: SideNavItem[];
- /**
- * Footer items in the side nav
- */
- footerItems?: SideNavFooterItem[];
} & Pick;
const SideNav = ({
@@ -436,7 +471,7 @@ const SideNav = ({
sideNavItems,
footerItems,
}: SideNavProps) => {
- const [sideNavCollapsed, setSideNavCollapsed] = useState(false);
+ const [isSideNavCollapsed, setSideNavCollapsed] = useState(false);
const odysseyDesignTokens = useOdysseyDesignTokens();
const processedSideNavItems = useMemo(
@@ -449,6 +484,12 @@ const SideNav = ({
})),
[sideNavItems],
);
+
+ const sideNavCollapseHandler = useCallback(() => {
+ setSideNavCollapsed(!isSideNavCollapsed);
+ onCollapse && onCollapse();
+ }, [isSideNavCollapsed, setSideNavCollapsed, onCollapse]);
+
return (
setSideNavCollapsed(!sideNavCollapsed)}
+ isSideNavCollapsed={isSideNavCollapsed}
+ onClick={() => setSideNavCollapsed(!isSideNavCollapsed)}
onKeyDown={(event) => {
- event.key === "Enter" && setSideNavCollapsed(!sideNavCollapsed);
+ (event.key === "Enter" || event.code === "Space") &&
+ setSideNavCollapsed(!isSideNavCollapsed);
}}
>
+ />
{
- setSideNavCollapsed(!sideNavCollapsed);
- onCollapse && onCollapse();
- }}
+ onCollapse={sideNavCollapseHandler}
/>
({
marginBlock: 0,
@@ -145,6 +155,11 @@ export const components = ({
root: () => ({
paddingInline: odysseyTokens.Spacing3,
paddingBlock: odysseyTokens.Spacing4,
+ "&.nav-accordion-details": {
+ paddingTop: 0,
+ paddingBottom: 0,
+ paddingLeft: odysseyTokens.Spacing2,
+ },
}),
},
},
@@ -2012,6 +2027,9 @@ export const components = ({
display: "inline-block",
height: "1em",
lineHeight: 1,
+ "& svg": {
+ fontSize: "1em",
+ },
},
".Link-indicator": {
@@ -2022,7 +2040,7 @@ export const components = ({
marginInlineEnd: odysseyTokens.Spacing1,
},
svg: {
- fontSize: "1em",
+ fontSize: "1.2em",
height: "1em",
position: "relative",
insetBlockStart: "-0.0625em",
diff --git a/packages/odyssey-storybook/src/components/odyssey-labs/SideNav/SideNav.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-labs/SideNav/SideNav.stories.tsx
index 2883ba180d..85df991c88 100644
--- a/packages/odyssey-storybook/src/components/odyssey-labs/SideNav/SideNav.stories.tsx
+++ b/packages/odyssey-storybook/src/components/odyssey-labs/SideNav/SideNav.stories.tsx
@@ -156,9 +156,11 @@ const storybookMeta: Meta = {
isDisabled: true,
},
{
- id: "item0-3",
- label: "Resource Management",
- isSectionHeader: true,
+ id: "item2",
+ href: "/",
+ label: "Applications",
+ startIcon: ,
+ isSelected: true,
},
{
id: "item0-1-2",
@@ -167,11 +169,9 @@ const storybookMeta: Meta = {
startIcon: ,
},
{
- id: "item2",
- href: "/",
- label: "Applications",
- startIcon: ,
- isSelected: true,
+ id: "item0-3",
+ label: "Resource Management",
+ isSectionHeader: true,
},
{
id: "item3-2-1",
@@ -186,7 +186,7 @@ const storybookMeta: Meta = {
id: "item5",
href: "/",
label: "Reports",
- endIcon: ,
+ startIcon: ,
},
{
id: "item3-1-0",
@@ -220,6 +220,7 @@ const storybookMeta: Meta = {
href: "/",
label: "Settings",
startIcon: ,
+ isDefaultExpanded: true,
children: [
{
id: "item4-1",